Solution was quite easy. Thanks to the user reply here: beehaw.org/comment/3535588 or programming.dev/comment/10034690 (Not sure if the links actually work as expected…)


Hi all. I have a little problem and don’t know how to solve. A CLI program in Python is broken since Python 3.12. It was working in Python 3.11. The reason is, that Python 3.12 changed how subclassing of a pathlib.Path works (basically fixed an issue), which now breaks a workaround.

The class in question is:

class File(PosixPath):
    def __new__(cls, *args: Any, **kwargs: Any) -> Any:
        return cls._from_parts(args).expanduser().resolve()  # type: ignore

    def __init__(self, source: str | Path, *args: Any) -> None:
        super().__init__()
        self.__source = Path(source)

    @property
    def source(self) -> Path:
        return self.__source

    @property
    def modified(self) -> Time:
        return Time.fromtimestamp(os.path.getmtime(self))

    @property
    def changed(self) -> Time:
        return Time.fromtimestamp(os.path.getctime(self))

    @property
    def accessed(self) -> Time:
        return Time.fromtimestamp(os.path.getatime(self))

    # Calculate sha512 hash of self file and compare result to the
    # checksum found in given file. Return True if identical.
    def verify_sha512(self, file: File, buffer_size: int = 4096) -> bool:
        compare_hash: str = file.read_text().split(" ")[0]
        self_hash: str = ""
        self_checksum = hashlib.sha512()
        with open(self.as_posix(), "rb") as f:
            for chunk in iter(lambda: f.read(buffer_size), b""):
                self_checksum.update(chunk)
            self_hash = self_checksum.hexdigest()
        return self_hash == compare_hash

and I get this error when running the script:

Traceback (most recent call last):
File "/home/tuncay/.local/bin/geprotondl", line 1415, in 
sys.exit(main())
^^^^^^
File "/home/tuncay/.local/bin/geprotondl", line 1334, in main
arguments, status = parse_arguments(argv)
^^^^^^^^^^^^^^^^^^^^^
File "/home/tuncay/.local/bin/geprotondl", line 1131, in parse_arguments
default, status = default_install_dir()
^^^^^^^^^^^^^^^^^^^^^
File "/home/tuncay/.local/bin/geprotondl", line 1101, in default_install_dir
steam_root: File = File(path)
^^^^^^^^^^
File "/home/tuncay/.local/bin/geprotondl", line 97, in __new__
return cls._from_parts(args).expanduser().resolve()  # type: ignore
^^^^^^^^^^^^^^^
AttributeError: type object 'File' has no attribute '_from_parts'. Did you mean: '_load_parts'?

Now replacing _from_parts with _load_parts does not work either and I get this message in that case:

Traceback (most recent call last):
File "/home/tuncay/.local/bin/geprotondl", line 1415, in 
sys.exit(main())
^^^^^^
File "/home/tuncay/.local/bin/geprotondl", line 1334, in main
arguments, status = parse_arguments(argv)
^^^^^^^^^^^^^^^^^^^^^
File "/home/tuncay/.local/bin/geprotondl", line 1131, in parse_arguments
default, status = default_install_dir()
^^^^^^^^^^^^^^^^^^^^^
File "/home/tuncay/.local/bin/geprotondl", line 1101, in default_install_dir
steam_root: File = File(path)
^^^^^^^^^^
File "/home/tuncay/.local/bin/geprotondl", line 97, in __new__
return cls._load_parts(args).expanduser().resolve()  # type: ignore
^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib/python3.12/pathlib.py", line 408, in _load_parts
paths = self._raw_paths
^^^^^^^^^^^^^^^
AttributeError: 'tuple' object has no attribute '_raw_paths'

I have searched the web and don’t understand how to fix this. Has anyone an idea what to do?

  • From https://github.com/python/cpython/blob/3.12/Lib/pathlib.py

    class PosixPath(Path, PurePosixPath):
        """Path subclass for non-Windows systems.
    
        On a POSIX system, instantiating a Path should return this object.
        """
        __slots__ = ()
    
        if os.name == 'nt':
            def __new__(cls, *args, **kwargs):
                raise NotImplementedError(
                    f"cannot instantiate {cls.__name__!r} on your system")
    

    Which sub classes PurePosixPath, which itself is basically just PurePath with a Posix flavour to it. And Path itself is PurePath as well. Not sure if I should copy paste them here.

    Edit: So I added these to my reply:

    PurePath

        def __new__(cls, *args, **kwargs):
            """Construct a PurePath from one or several strings and or existing
            PurePath objects.  The strings and path objects are combined so as
            to yield a canonicalized path, which is incorporated into the
            new PurePath object.
            """
            if cls is PurePath:
                cls = PureWindowsPath if os.name == 'nt' else PurePosixPath
            return object.__new__(cls)
    
    
        def __init__(self, *args):
            paths = []
            for arg in args:
                if isinstance(arg, PurePath):
                    if arg._flavour is ntpath and self._flavour is posixpath:
                        # GH-103631: Convert separators for backwards compatibility.
                        paths.extend(path.replace('\\', '/') for path in arg._raw_paths)
                    else:
                        paths.extend(arg._raw_paths)
                else:
                    try:
                        path = os.fspath(arg)
                    except TypeError:
                        path = arg
                    if not isinstance(path, str):
                        raise TypeError(
                            "argument should be a str or an os.PathLike "
                            "object where __fspath__ returns a str, "
                            f"not {type(path).__name__!r}")
                    paths.append(path)
            self._raw_paths = paths
    

    Path

    
        def __init__(self, *args, **kwargs):
            if kwargs:
                msg = ("support for supplying keyword arguments to pathlib.PurePath "
                       "is deprecated and scheduled for removal in Python {remove}")
                warnings._deprecated("pathlib.PurePath(**kwargs)", msg, remove=(3, 14))
            super().__init__(*args)
    
        def __new__(cls, *args, **kwargs):
            if cls is Path:
                cls = WindowsPath if os.name == 'nt' else PosixPath
            return object.__new__(cls)