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)