bpo-24132: Add pathlib._AbstractPath
by barneygale · Pull Request #31085 · python/cpython (original) (raw)
I love the overall idea of this PR, but I'm afraid I think trying to cram everything into one ABC is the wrong approach -- the interface still feels a little confused to me.
For example -- I'm a static-typing guy, and I have no idea how we'd type AbstractPath.absolute()
in typeshed. For a class that override AbstractPath.cwd()
, AbstractPath.absolute()
returns an instance of that class. But subclasses of AbstractPath
can't be guaranteed to override AbstractPath.cwd()
, because it's not an abstractmethod
, because it's not part of the core interface. Which means that we couldn't include this method in the typeshed stub for AbstractPath
at all, because it would be unsafe for an instance of an AbstractPath
subclass to ever call absolute()
. So, why is absolute()
in AbstractPath
at all?
I'm using cwd()
and absolute()
as an example, but I think this is a broader problem for all of the methods in AbstractPath
that raise NotImplementedError
but are not abstractmethods
(and, by extension, all of the mixin methods that call methods that might-or-might-not be implemented).
I would counsel splitting AbstractPath
up into several smaller ABCs. In typeshed we could pretend that these are PEP 544 protocols, in much the same way we do with os.PathLike
, which is an ABC at runtime but is considered a Protocol by static type checkers.
Instead of having a structure like this:
class AbstractPath(PurePath, ABC): # core interface @abstractmethod def iterdir(self): ... @abstractmethod def stat(self, *, follow_symlinks=True): ... @abstractmethod def open(self, mode='r', buffering=-1, encoding=None, errors=None, newline=None): ... @classmethod def cwd(cls): ... # Not abstract, but raises NotImplementedError def absolute(self): ... # Depends on cwd() being implemented def resolve(self): ... # Not abstract, but raises NotImplementedError
class Path(AbstractPath): ...
You could instead have something like this:
class AbstractBasePath(PurePath, metaclass=ABCMeta): """I represent the minimum requirements for a class to implement a pathlib-esque interface""" @abstractmethod def iterdir(self): ... @abstractmethod def stat(self, *, follow_symlinks=True): ... @abstractmethod def open(self, mode='r', buffering=-1, encoding=None, errors=None, newline=None): ...
class ResolveablePathMixin(metaclass=ABCMeta): """Mixin ABC for paths that can be resolved in some sense""" @classmethod @abstractmethod def cwd(cls): ... def absolute(self): ... # Concrete method that calls cwd() @abstractmethod def resolve(self): ...
class AbstractPath(AbstractBasePath, ResolveablePathMixin): """I represent the full interface for a pathlib Path""" # This class body would be completely empty # The only purpose of this class would be to accumulate # all of the abstractmethods from AbstractBasePath # and all of the mixin classes
class Path(AbstractPath):
# This class body would be filled with concrete implementations
# of all the abstract methods accumulated in AbstractPath
In this way, users could decide whether they want to implement just the core interface, by subclassing AbstractBaseClass
; the full pathlib
interface, by subclass AbstractPath
; or something custom, by using multiple inheritance to subclass AbstractBaseClass
and one of the smaller mixin classes at the same time.
(You're far more knowledgeable about pathlib
internals than I am, so: apologies if I've got any of the pathlib
-specific details wrong here!)