From f011491b0ad98e157261f52d5173119763328770 Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Fri, 30 Apr 2021 14:30:59 -0700 Subject: [PATCH 01/11] bpo-24132: Add stub SimplePath/FilePath classes to pathlib Introduce stub classes with initial tests on what will become subclassable alternatives to PurePath/Path that do not have inverted dependencies. --- Lib/pathlib.py | 16 ++++++++++++++++ Lib/test/test_pathlib.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 8e6eb48b9767ca..0b664fd5cd3313 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -35,6 +35,7 @@ _WINERROR_INVALID_NAME, _WINERROR_CANT_RESOLVE_FILENAME) + def _ignore_error(exception): return (getattr(exception, 'errno', None) in _IGNORED_ERROS or getattr(exception, 'winerror', None) in _IGNORED_WINERRORS) @@ -902,6 +903,7 @@ def match(self, path_pattern): return False return True + # Can't subclass os.PathLike from PurePath and keep the constructor # optimizations in PurePath._parse_args(). os.PathLike.register(PurePath) @@ -1438,6 +1440,7 @@ class PosixPath(Path, PurePosixPath): """ __slots__ = () + class WindowsPath(Path, PureWindowsPath): """Path subclass for Windows systems. @@ -1447,3 +1450,16 @@ class WindowsPath(Path, PureWindowsPath): def is_mount(self): raise NotImplementedError("Path.is_mount() is unsupported on this system") + + +class SimplePath: + # Stub for class which will, aside from name, match the class instantiated + # by PurePath, however unlike PurePath, will be able to be subclassed + # normally. + pass + + +class FilePath: + # Stub for class which will, aside from name, match the class instantiated + # by Path, however unlike Path, will be able to be subclassed normally. + pass diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 54b7977b43f235..cbc1b9395d4aa8 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1296,6 +1296,7 @@ def test_is_reserved(self): # UNC paths are never reserved. self.assertIs(False, P('//my/share/nul/con/aux').is_reserved()) + class PurePathTest(_BasePurePathTest, unittest.TestCase): cls = pathlib.PurePath @@ -1322,6 +1323,30 @@ def test_different_flavours_unordered(self): p >= q +class SubclassTestMixin: + + def test_can_subclass(self): + P = self.cls + try: + class Derived(P): + pass + except Exception as e: + self.fail(f"Failed to subclass {P}: {e}") + else: + self.assertTrue(issubclass(Derived, P)) + try: + derived = Derived() + except Exception as e: + self.fail("Failed to be able to instantiate a class " + f"derived from {P}: {e}") + else: + self.assertIsInstance(derived, Derived) + + +class SimplePathTest(unittest.TestCase, SubclassTestMixin): + cls = pathlib.SimplePath + + # # Tests for the concrete classes. # @@ -1336,10 +1361,12 @@ def test_different_flavours_unordered(self): only_posix = unittest.skipIf(os.name == 'nt', 'test requires a POSIX-compatible system') + @only_posix class PosixPathAsPureTest(PurePosixPathTest): cls = pathlib.PosixPath + @only_nt class WindowsPathAsPureTest(PureWindowsPathTest): cls = pathlib.WindowsPath @@ -2666,6 +2693,10 @@ def check(): check() +class FilePathTest(unittest.TestCase, SubclassTestMixin): + cls = pathlib.FilePath + + class CompatiblePathTest(unittest.TestCase): """ Test that a type can be made compatible with PurePath From ff1d4553c9d7613bcab4693ef3483b5ded18c17d Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Fri, 30 Apr 2021 16:18:25 -0700 Subject: [PATCH 02/11] bpo-24132: Replace hardcoded references where possible in pathlib Replace references in PurePath and changed function in _BasePurePathTest.test_ordering_common to test missing execution paths. This is preparation for seperating the base class and factory functionality in PurePath in order to fully implement SimplePath. --- Lib/pathlib.py | 29 ++++++++++++++++++++++------- Lib/test/test_pathlib.py | 2 ++ 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 0b664fd5cd3313..2af3ffcc9369e3 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -561,7 +561,7 @@ def _parse_args(cls, args): # canonicalize some constructor arguments. parts = [] for a in args: - if isinstance(a, PurePath): + if isinstance(a, cls): parts += a._parts else: a = os.fspath(a) @@ -650,9 +650,12 @@ def _cparts(self): return self._cached_cparts def __eq__(self, other): - if not isinstance(other, PurePath): + if not isinstance(other, self.__class__): return NotImplemented - return self._cparts == other._cparts and self._flavour is other._flavour + return ( + self._cparts == other._cparts + and self._flavour is other._flavour + ) def __hash__(self): try: @@ -662,22 +665,34 @@ def __hash__(self): return self._hash def __lt__(self, other): - if not isinstance(other, PurePath) or self._flavour is not other._flavour: + if ( + not isinstance(other, self.__class__) + or self._flavour is not other._flavour + ): return NotImplemented return self._cparts < other._cparts def __le__(self, other): - if not isinstance(other, PurePath) or self._flavour is not other._flavour: + if ( + not isinstance(other, self.__class__) + or self._flavour is not other._flavour + ): return NotImplemented return self._cparts <= other._cparts def __gt__(self, other): - if not isinstance(other, PurePath) or self._flavour is not other._flavour: + if ( + not isinstance(other, self.__class__) + or self._flavour is not other._flavour + ): return NotImplemented return self._cparts > other._cparts def __ge__(self, other): - if not isinstance(other, PurePath) or self._flavour is not other._flavour: + if ( + not isinstance(other, self.__class__) + or self._flavour is not other._flavour + ): return NotImplemented return self._cparts >= other._cparts diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index cbc1b9395d4aa8..8c3244563886f3 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -354,7 +354,9 @@ def test_ordering_common(self): # Ordering is tuple-alike. def assertLess(a, b): self.assertLess(a, b) + self.assertLessEqual(a, b) self.assertGreater(b, a) + self.assertGreaterEqual(b, a) P = self.cls a = P('a') b = P('a/b') From 488994f89eac8e00f84a6a05340f160175d38182 Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Mon, 3 May 2021 12:50:30 -0700 Subject: [PATCH 03/11] bpo-24132: Refactor PurePath methods into base class in pathlib Introduce new base class _PurePathBase and move all of the methods from PurePath into it (except for the factory __new__) for use in building SimplePath / FilePath. --- Lib/pathlib.py | 58 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 2af3ffcc9369e3..bacb6072f6abaa 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -491,10 +491,6 @@ def _select_from(self, parent_path, is_dir, exists, scandir): return -# -# Public API -# - class _PathParents(Sequence): """This object provides sequence-like access to the logical ancestors of a path. Don't try to construct it yourself.""" @@ -526,28 +522,24 @@ def __repr__(self): return "<{}.parents>".format(self._pathcls.__name__) -class PurePath(object): - """Base class for manipulating paths without I/O. +class _PurePathBase(object): + """ + Base class for manipulating paths without I/O. - PurePath represents a filesystem path and offers operations which - don't imply any actual filesystem I/O. Depending on your system, - instantiating a PurePath will return either a PurePosixPath or a - PureWindowsPath object. You can also instantiate either of these classes - directly, regardless of your system. + Classes deriving from _PurePathBase represent filesystem paths and + inherit methods that can be used for path manipulation but which + don't perform any actual filesystem I/O. """ + __slots__ = ( '_drv', '_root', '_parts', '_str', '_hash', '_pparts', '_cached_cparts', ) def __new__(cls, *args): - """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 + # Utilize __new__ rather __init__ in order to provide a setup + # method in common with PurePath and Path which are derived from + # this base class and require extra flexibility. return cls._from_parts(args) def __reduce__(self): @@ -919,6 +911,36 @@ def match(self, path_pattern): return True +# +# Public API +# + + +class PurePath(_PurePathBase): + """ + Factory to generate instances for manipulating paths without I/O. + + Instantiating PurePath returns an instance of either PureWindowsPath + or PurePosixPath in its place depending on the flavour of system + present. The resultant class can then be used for path manipulations + but will not have any methods which make calls to the filesystem. + Alternatively, you can also instantiate either of these classes + directly, regardless of your system flavour. + """ + + def __new__(cls, *args): + """ + Construct a PurePath from string(s) and/or existing PurePaths + + 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 cls._from_parts(args) + + # Can't subclass os.PathLike from PurePath and keep the constructor # optimizations in PurePath._parse_args(). os.PathLike.register(PurePath) From 6a69e0dd813d9fccf913971e62919f049a1b6cf6 Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Mon, 3 May 2021 13:59:32 -0700 Subject: [PATCH 04/11] bpo-24132: Completed SimplePath in pathlib SimplePath provides a subclassable alternative to PurePath which is otherwise functionally equivalent -- passing all of the existing PurePath tests. --- Lib/pathlib.py | 22 ++++++++++++++++++---- Lib/test/test_pathlib.py | 33 +++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index bacb6072f6abaa..ecb554f8c4670c 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -536,6 +536,14 @@ class _PurePathBase(object): '_str', '_hash', '_pparts', '_cached_cparts', ) + @classmethod + def __init_subclass__(cls, **kwargs): + mro_names = [class_.__name__ for class_ in cls.mro()[:-1]] + if "PurePath" not in mro_names: + is_posix = os.name == "posix" + cls._flavour = _posix_flavour if is_posix else _windows_flavour + return cls + def __new__(cls, *args): # Utilize __new__ rather __init__ in order to provide a setup # method in common with PurePath and Path which are derived from @@ -1489,10 +1497,16 @@ def is_mount(self): raise NotImplementedError("Path.is_mount() is unsupported on this system") -class SimplePath: - # Stub for class which will, aside from name, match the class instantiated - # by PurePath, however unlike PurePath, will be able to be subclassed - # normally. +class SimplePath(_PurePathBase): + """ + Class for manipulating paths without I/O. + + SimplePath represents a filesystem path and offers operations + which don't perform any actual filesystem I/O. The methods in this + class will return values that are appropriate for your system + flavour, be that Windows or Posix, though the return values are not + guaranteed to be the same for both platforms. + """ pass diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 8c3244563886f3..db2912592d7a9d 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -21,6 +21,12 @@ grp = pwd = None +only_nt = unittest.skipIf(os.name != 'nt', + 'test requires a Windows-compatible system') +only_posix = unittest.skipIf(os.name == 'nt', + 'test requires a POSIX-compatible system') + + class _BaseFlavourTest(object): def _check_parse_parts(self, arg, expected): @@ -703,6 +709,11 @@ def test_pickling_common(self): class PurePosixPathTest(_BasePurePathTest, unittest.TestCase): cls = pathlib.PurePosixPath + def test_flavour(self): + P = self.cls + self.assertEqual(P._flavour.__class__, + pathlib._PosixFlavour) + def test_root(self): P = self.cls self.assertEqual(P('/a/b').root, '/') @@ -792,6 +803,11 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase): ], }) + def test_flavour(self): + P = self.cls + self.assertEqual(P._flavour.__class__, + pathlib._WindowsFlavour) + def test_str(self): p = self.cls('a/b/c') self.assertEqual(str(p), 'a\\b\\c') @@ -1345,7 +1361,17 @@ class Derived(P): self.assertIsInstance(derived, Derived) -class SimplePathTest(unittest.TestCase, SubclassTestMixin): +class SimplePathTest(_BasePurePathTest, SubclassTestMixin, unittest.TestCase): + cls = pathlib.SimplePath + + +@only_posix +class SimplePathAsPurePosixPathTest(PurePosixPathTest, unittest.TestCase): + cls = pathlib.SimplePath + + +@only_nt +class SimplePathAsPureWindowsPathTest(PureWindowsPathTest, unittest.TestCase): cls = pathlib.SimplePath @@ -1358,11 +1384,6 @@ class SimplePathTest(unittest.TestCase, SubclassTestMixin): join = lambda *x: os.path.join(BASE, *x) rel_join = lambda *x: os.path.join(TESTFN, *x) -only_nt = unittest.skipIf(os.name != 'nt', - 'test requires a Windows-compatible system') -only_posix = unittest.skipIf(os.name == 'nt', - 'test requires a POSIX-compatible system') - @only_posix class PosixPathAsPureTest(PurePosixPathTest): From 09317e2e71e664938e5eef09cca08128bdab144c Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Mon, 3 May 2021 14:40:37 -0700 Subject: [PATCH 05/11] bpo-24132: Partial implmentation of FilePath in pathlib Partial implementation of FilePath which offers all of the PurePath and SimplePath functionality and passes the equivalent existing tests. --- Lib/pathlib.py | 13 ++++++++++--- Lib/test/test_pathlib.py | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index ecb554f8c4670c..cd62c0b893b7d9 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -1510,7 +1510,14 @@ class will return values that are appropriate for your system pass -class FilePath: - # Stub for class which will, aside from name, match the class instantiated - # by Path, however unlike Path, will be able to be subclassed normally. +class FilePath(SimplePath): + """ + SimplePath subclass that can make system calls. + + FilePath represents a filesystem path but unlike SimplePath, also + offers methods to do system calls on path objects. The methods in + this class will return values that are appropriate for your system + flavour, be that Windows or Posix, though the return values are not + guaranteed to be the same for both platforms. + """ pass diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index db2912592d7a9d..2e9bdd105797e9 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1405,6 +1405,20 @@ def test_group(self): P('c:/').group() +@only_posix +class FilePathAsPurePosixTest(PurePosixPathTest): + cls = pathlib.FilePath + + +@only_nt +class FilePathAsPureWindowsTest(PureWindowsPathTest): + cls = pathlib.FilePath + + +class FilePathAsPureTest(_BasePurePathTest, unittest.TestCase): + cls = pathlib.FilePath + + class _BasePathTest(object): """Tests for the FS-accessing functionalities of the Path classes.""" From d966acfb99f93db5f060b38ae698ae39bde9e6be Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Thu, 27 May 2021 11:09:36 -0700 Subject: [PATCH 06/11] bpo-24132: Fix inconsistent organization of is_mount in pathlib Existing is_mount method in Path was Posix only and not cross platform even though it was in a cross-platform base class. The existing design relied upon being overriden later in WindowsPath. Consolidated code from both into new Path.is_mount() which is now cross plaform similiar to all of the other Path methods. --- Lib/pathlib.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index cd62c0b893b7d9..0c303cb905b1f6 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -1369,6 +1369,9 @@ def is_mount(self): """ Check if this path is a POSIX mount point """ + if os.name != "posix": + raise NotImplementedError("Path.is_mount() is " + "unsupported on this system") # Need to exist and be a dir if not self.exists() or not self.is_dir(): return False @@ -1493,9 +1496,6 @@ class WindowsPath(Path, PureWindowsPath): """ __slots__ = () - def is_mount(self): - raise NotImplementedError("Path.is_mount() is unsupported on this system") - class SimplePath(_PurePathBase): """ From ed5f237ca46d150cc2343e64814453be8f54267d Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Mon, 3 May 2021 16:32:16 -0700 Subject: [PATCH 07/11] bpo-24132: Replace additional hardcoded references in pathlib Replace hardcorded references to pathlib.Path so that the base class and factory functionality of Path can be seperated. --- Lib/pathlib.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 0c303cb905b1f6..f827e45e1db53a 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -326,21 +326,24 @@ def touch(self, path, mode=0o666, exist_ok=True): readlink = os.readlink else: def readlink(self, path): - raise NotImplementedError("os.readlink() not available on this system") + raise NotImplementedError("os.readlink() is not available " + "on this system") def owner(self, path): try: import pwd return pwd.getpwuid(self.stat(path).st_uid).pw_name except ImportError: - raise NotImplementedError("Path.owner() is unsupported on this system") + raise NotImplementedError(f"{self.__class__.__name__}.owner() " + f"is unsupported on this system") def group(self, path): try: import grp return grp.getgrgid(self.stat(path).st_gid).gr_name except ImportError: - raise NotImplementedError("Path.group() is unsupported on this system") + raise NotImplementedError(f"{self.__class__.__name__}.group() " + f"is unsupported on this system") getcwd = os.getcwd @@ -1012,7 +1015,7 @@ def __exit__(self, t, v, tb): # In previous versions of pathlib, this method marked this path as # closed; subsequent attempts to perform I/O would raise an IOError. # This functionality was never documented, and had the effect of - # making Path objects mutable, contrary to PEP 428. In Python 3.9 the + # making path objects mutable, contrary to PEP 428. In Python 3.9 the # _closed attribute was removed, and this method made a no-op. # This method and __enter__()/__exit__() should be deprecated and # removed in the future. @@ -1059,7 +1062,7 @@ def glob(self, pattern): """Iterate over this subtree and yield all existing files (of any kind, including directories) matching the given relative pattern. """ - sys.audit("pathlib.Path.glob", self, pattern) + sys.audit(f"pathlib.{self.__class__.__name__}.glob", self, pattern) if not pattern: raise ValueError("Unacceptable pattern: {!r}".format(pattern)) drv, root, pattern_parts = self._flavour.parse_parts((pattern,)) @@ -1074,7 +1077,7 @@ def rglob(self, pattern): directories) matching the given relative pattern, anywhere in this subtree. """ - sys.audit("pathlib.Path.rglob", self, pattern) + sys.audit(f"pathlib.{self.__class__.__name__}.rglob", self, pattern) drv, root, pattern_parts = self._flavour.parse_parts((pattern,)) if drv or root: raise NotImplementedError("Non-relative patterns are unsupported") @@ -1262,9 +1265,9 @@ def rename(self, target): The target path may be absolute or relative. Relative paths are interpreted relative to the current working directory, *not* the - directory of the Path object. + directory of this object. - Returns the new Path instance pointing to the target path. + Returns the new class instance pointing to the target path. """ self._accessor.rename(self, target) return self.__class__(target) @@ -1275,9 +1278,9 @@ def replace(self, target): The target path may be absolute or relative. Relative paths are interpreted relative to the current working directory, *not* the - directory of the Path object. + directory of this object. - Returns the new Path instance pointing to the target path. + Returns the new class instance pointing to the target path. """ self._accessor.replace(self, target) return self.__class__(target) @@ -1303,15 +1306,16 @@ def link_to(self, target): Note this function does not make this path a hard link to *target*, despite the implication of the function and argument names. The order - of arguments (target, link) is the reverse of Path.symlink_to, but + of arguments (target, link) is the reverse of symlink_to, but matches that of os.link. Deprecated since Python 3.10 and scheduled for removal in Python 3.12. Use `hardlink_to()` instead. """ - warnings.warn("pathlib.Path.link_to() is deprecated and is scheduled " - "for removal in Python 3.12. " - "Use pathlib.Path.hardlink_to() instead.", + classname = self.__class__.__name__ + warnings.warn(f"pathlib.{classname}.link_to() is deprecated and is " + f"scheduled for removal in Python 3.12. " + f"Use pathlib.{classname}.hardlink_to() instead.", DeprecationWarning, stacklevel=2) self._accessor.link(self, target) @@ -1370,8 +1374,8 @@ def is_mount(self): Check if this path is a POSIX mount point """ if os.name != "posix": - raise NotImplementedError("Path.is_mount() is " - "unsupported on this system") + raise NotImplementedError(f"{self.__class__.__name__}.is_mount() " + f"is unsupported on this system") # Need to exist and be a dir if not self.exists() or not self.is_dir(): return False From ecc341ba9261bd6ed1e3daf1eb05e7591f909d85 Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Wed, 5 May 2021 14:33:26 -0700 Subject: [PATCH 08/11] bpo-24132: Completed FilePath in pathlib FilePath provides a subclassable alternative to Path which is otherwise functionally equivalent -- passing all of the existing Path tests. --- Lib/pathlib.py | 157 +++++++++++++++++++++------------------ Lib/test/test_pathlib.py | 12 ++- 2 files changed, 94 insertions(+), 75 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index f827e45e1db53a..aa89c153528847 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -922,84 +922,14 @@ def match(self, path_pattern): return True -# -# Public API -# - - -class PurePath(_PurePathBase): +class PathIOMixin: """ - Factory to generate instances for manipulating paths without I/O. + Provides I/O operations for FilePath and Path derivatives. - Instantiating PurePath returns an instance of either PureWindowsPath - or PurePosixPath in its place depending on the flavour of system - present. The resultant class can then be used for path manipulations - but will not have any methods which make calls to the filesystem. - Alternatively, you can also instantiate either of these classes - directly, regardless of your system flavour. + Requires _PurePathBase in the mro. """ - def __new__(cls, *args): - """ - Construct a PurePath from string(s) and/or existing PurePaths - - 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 cls._from_parts(args) - - -# Can't subclass os.PathLike from PurePath and keep the constructor -# optimizations in PurePath._parse_args(). -os.PathLike.register(PurePath) - - -class PurePosixPath(PurePath): - """PurePath subclass for non-Windows systems. - - On a POSIX system, instantiating a PurePath should return this object. - However, you can also instantiate it directly on any system. - """ - _flavour = _posix_flavour - __slots__ = () - - -class PureWindowsPath(PurePath): - """PurePath subclass for Windows systems. - - On a Windows system, instantiating a PurePath should return this object. - However, you can also instantiate it directly on any system. - """ - _flavour = _windows_flavour - __slots__ = () - - -# Filesystem-accessing classes - - -class Path(PurePath): - """PurePath subclass that can make system calls. - - Path represents a filesystem path but unlike PurePath, also offers - methods to do system calls on path objects. Depending on your system, - instantiating a Path will return either a PosixPath or a WindowsPath - object. You can also instantiate a PosixPath or WindowsPath directly, - but cannot instantiate a WindowsPath on a POSIX system or vice versa. - """ _accessor = _normal_accessor - __slots__ = () - - def __new__(cls, *args, **kwargs): - if cls is Path: - cls = WindowsPath if os.name == 'nt' else PosixPath - self = cls._from_parts(args) - if not self._flavour.is_supported: - raise NotImplementedError("cannot instantiate %r on your system" - % (cls.__name__,)) - return self def _make_child_relpath(self, part): # This is an optimization used for dir walking. `part` must be @@ -1485,6 +1415,85 @@ def expanduser(self): return self +# +# Public API +# + + +class PurePath(_PurePathBase): + """ + Factory to generate instances for manipulating paths without I/O. + + Instantiating PurePath returns an instance of either PureWindowsPath + or PurePosixPath in its place depending on the flavour of system + present. The resultant class can then be used for path manipulations + but will not have any methods which make calls to the filesystem. + Alternatively, you can also instantiate either of these classes + directly, regardless of your system flavour. + """ + + def __new__(cls, *args): + """ + Construct a PurePath from string(s) and/or existing PurePaths + + 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 cls._from_parts(args) + + +# Can't subclass os.PathLike from PurePath and keep the constructor +# optimizations in PurePath._parse_args(). +os.PathLike.register(PurePath) + + +class PurePosixPath(PurePath): + """PurePath subclass for non-Windows systems. + + On a POSIX system, instantiating a PurePath should return this object. + However, you can also instantiate it directly on any system. + """ + _flavour = _posix_flavour + __slots__ = () + + +class PureWindowsPath(PurePath): + """PurePath subclass for Windows systems. + + On a Windows system, instantiating a PurePath should return this object. + However, you can also instantiate it directly on any system. + """ + _flavour = _windows_flavour + __slots__ = () + + +# Filesystem-accessing classes + + +class Path(PurePath, PathIOMixin): + """PurePath subclass that can make system calls. + + Path represents a filesystem path but unlike PurePath, also offers + methods to do system calls on path objects. Depending on your system, + instantiating a Path will return either a PosixPath or a WindowsPath + object. You can also instantiate a PosixPath or WindowsPath directly, + but cannot instantiate a WindowsPath on a POSIX system or vice versa. + """ + __slots__ = () + + def __new__(cls, *args, **kwargs): + if cls is Path: + cls = WindowsPath if os.name == 'nt' else PosixPath + self = cls._from_parts(args) + if not self._flavour.is_supported: + raise NotImplementedError("cannot instantiate %r on your system" + % (cls.__name__,)) + return self + + class PosixPath(Path, PurePosixPath): """Path subclass for non-Windows systems. @@ -1514,7 +1523,7 @@ class will return values that are appropriate for your system pass -class FilePath(SimplePath): +class FilePath(SimplePath, PathIOMixin): """ SimplePath subclass that can make system calls. diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 2e9bdd105797e9..d95f771c43865e 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -2730,7 +2730,17 @@ def check(): check() -class FilePathTest(unittest.TestCase, SubclassTestMixin): +class FilePathTest(_BasePathTest, SubclassTestMixin, unittest.TestCase): + cls = pathlib.FilePath + + +@only_posix +class FilePathAsPosixPathTest(PosixPathTest): + cls = pathlib.FilePath + + +@only_nt +class FilePathAsWindowsPathTest(WindowsPathTest): cls = pathlib.FilePath From 2700e46b2a07fd928cbbc6f126eb416cbc053cff Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Tue, 25 May 2021 16:43:16 -0700 Subject: [PATCH 09/11] bpo-24123: Add SimplePosixPath and SimpleWindowsPath to pathlib SimplePosixPath and SimpleWindowsPath provide a subclassable alternative to PurePosixPath and PureWindowsPath which are otherwise functionally equivalent -- passing all of the existing PurePosixPath and PureWindowsPath tests. --- Lib/pathlib.py | 72 ++++++++++++++++++++++++++++++++++------ Lib/test/test_pathlib.py | 12 +++++++ 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index aa89c153528847..b8d8f189601455 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -541,10 +541,17 @@ class _PurePathBase(object): @classmethod def __init_subclass__(cls, **kwargs): - mro_names = [class_.__name__ for class_ in cls.mro()[:-1]] + mro_without_obj = cls.mro()[:-1] + mro_names = [class_.__name__ for class_ in mro_without_obj] if "PurePath" not in mro_names: - is_posix = os.name == "posix" - cls._flavour = _posix_flavour if is_posix else _windows_flavour + has_flavour = False + for class_ in mro_without_obj: + if hasattr(class_, "_flavour"): + has_flavour = True + break + if not has_flavour: + is_posix = os.name == "posix" + cls._flavour = _posix_flavour if is_posix else _windows_flavour return cls def __new__(cls, *args): @@ -922,12 +929,37 @@ def match(self, path_pattern): return True -class PathIOMixin: +class _PosixMixin: """ - Provides I/O operations for FilePath and Path derivatives. + Provides Posix flavour for PurePath/SimplePath and derivatives Requires _PurePathBase in the mro. """ + _flavour = _posix_flavour + __slots__ = () + + +class _WindowsMixin: + """ + Provides Windows flavour for PurePath/SimplePath and derivatives + + Requires _PurePathBase in the mro. + """ + _flavour = _windows_flavour + __slots__ = () + + +class PathIOMixin: + """ + Provides the default I/O operations for Posix and Windows. + + This mixin provides the methods used by FilePath and Path + derivatives in order to perform filesystem I/O. This can also be used + externally for custom Path/FilePath-like classes. To do so it must + be used in conjunction with either SimplePosixPath or + SimpleWindowsPath as it requires _PurePathBase to be in the method + resolution order. + """ _accessor = _normal_accessor @@ -1450,24 +1482,22 @@ def __new__(cls, *args): os.PathLike.register(PurePath) -class PurePosixPath(PurePath): +class PurePosixPath(PurePath, _PosixMixin): """PurePath subclass for non-Windows systems. On a POSIX system, instantiating a PurePath should return this object. However, you can also instantiate it directly on any system. """ - _flavour = _posix_flavour - __slots__ = () + pass -class PureWindowsPath(PurePath): +class PureWindowsPath(PurePath, _WindowsMixin): """PurePath subclass for Windows systems. On a Windows system, instantiating a PurePath should return this object. However, you can also instantiate it directly on any system. """ - _flavour = _windows_flavour - __slots__ = () + pass # Filesystem-accessing classes @@ -1523,6 +1553,26 @@ class will return values that are appropriate for your system pass +class SimplePosixPath(_PurePathBase, _PosixMixin): + """ + Class for manipulating Posix paths without I/O. + + SimplePosixPath represents a Posix specific filesystem path and + offers operations which don't perform any actual filesystem I/O. + """ + pass + + +class SimpleWindowsPath(_PurePathBase, _WindowsMixin): + """ + Class for manipulating Windows paths without I/O. + + SimpleWindowsPath represents a Windows specific filesystem path and + offers operations which don't perform any actual filesystem I/O. + """ + pass + + class FilePath(SimplePath, PathIOMixin): """ SimplePath subclass that can make system calls. diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index d95f771c43865e..03d56e366bc4d4 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1375,6 +1375,18 @@ class SimplePathAsPureWindowsPathTest(PureWindowsPathTest, unittest.TestCase): cls = pathlib.SimplePath +class SimplePosixPathAsPurePosixPathTest( + PurePosixPathTest, SubclassTestMixin, unittest.TestCase +): + cls = pathlib.SimplePosixPath + + +class SimpleWindowsPathAsPureWindowsPathTest( + PureWindowsPathTest, SubclassTestMixin, unittest.TestCase +): + cls = pathlib.SimpleWindowsPath + + # # Tests for the concrete classes. # From d15d8da8ebb3d23934f2ca476d063cee0480f863 Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Wed, 26 May 2021 14:10:10 -0700 Subject: [PATCH 10/11] bpo-24132: Fix validity of inter-path comparison in pathlib Fix _BasePurePath comparison operators so that all of the new path classes in pathlib are comparable with one another and return results that are consistent with the existing behavoir, i.e. Path('/') == PurePath('/'). --- Lib/pathlib.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index b8d8f189601455..d0b21d70e8a23f 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -660,7 +660,7 @@ def _cparts(self): return self._cached_cparts def __eq__(self, other): - if not isinstance(other, self.__class__): + if not isinstance(other, _PurePathBase): return NotImplemented return ( self._cparts == other._cparts @@ -676,7 +676,7 @@ def __hash__(self): def __lt__(self, other): if ( - not isinstance(other, self.__class__) + not isinstance(other, _PurePathBase) or self._flavour is not other._flavour ): return NotImplemented @@ -684,7 +684,7 @@ def __lt__(self, other): def __le__(self, other): if ( - not isinstance(other, self.__class__) + not isinstance(other, _PurePathBase) or self._flavour is not other._flavour ): return NotImplemented @@ -692,7 +692,7 @@ def __le__(self, other): def __gt__(self, other): if ( - not isinstance(other, self.__class__) + not isinstance(other, _PurePathBase) or self._flavour is not other._flavour ): return NotImplemented @@ -700,7 +700,7 @@ def __gt__(self, other): def __ge__(self, other): if ( - not isinstance(other, self.__class__) + not isinstance(other, _PurePathBase) or self._flavour is not other._flavour ): return NotImplemented From 6028b6de32ec2466cb046623ae26ef7b999477e6 Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Tue, 25 May 2021 21:08:32 -0700 Subject: [PATCH 11/11] bpo-24132: Document FilePath and SimplePath family in pathlib Document and make public FilePath, SimplePath, SimplePosixPath, SimpleWindowsPath, and PathIOMixin. Also add section to pathlib documentation regarding the limitations of PurePath and Path with regards to subclassing and how to use these new classes as alternatives. --- Doc/library/pathlib.rst | 134 ++++++++++++++++++ Lib/pathlib.py | 2 + Misc/ACKS | 1 + .../2021-05-25-21-25-39.bpo-24132.37mJNj.rst | 5 + 4 files changed, 142 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2021-05-25-21-25-39.bpo-24132.37mJNj.rst diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index b6507eb4d6fa2c..7d4e380470545b 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -173,6 +173,36 @@ we also call *flavours*: *pathsegments* is specified similarly to :class:`PurePath`. +.. class:: SimplePath(*pathsegments) + + A subclassable alternative to the factory :class:`PurePath`. While different + in name, when instantiated, it will otherwise behave similiarly to either + :class:`PurePosixPath` or :class:`PureWindowsPath` depending on the + respective system type on which it is being run. + + *pathsegments* is specified similarly to :class:`PurePath`. + + >>> PurePath('prog.txt').with_suffix('.py') # On Windows + PureWindowsPath('prog.py') + >>> class MySimplePath(SimplePath): pass + ... + >>> MySimplePath('prog.txt').with_suffix('.py') + MySimplePath('prog.py') + +.. class:: SimplePosixPath(*pathsegments) + + A subclassable alternative to :class:`PurePosixPath` which otherwise behaves + similarly. + + *pathsegments* is specified similarly to :class:`PurePath`. + +.. class:: SimpleWindowsPath(*pathsegments) + + A subclassable alternative to :class:`PurePosixPath` which otherwise behaves + similarly. + + *pathsegments* is specified similarly to :class:`PurePath`. + Regardless of the system you're running on, you can instantiate all of these classes, since they don't provide any operation that does system calls. @@ -268,6 +298,92 @@ property: (note how the drive and local root are regrouped in a single part) +Extensibility and Subclassing +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Due to the factory functionality of :class:`PurePath` and :class:`Path`, +subclassing them is not supported. However there are alternative classes +:class:`SimplePath` and :class:`FilePath` which can act as direct +replacements and which are subclassable. + +They test equivalently:: + + >>> PurePath('C:/Windows') == SimplePath('C:/Windows') + True + >>> Path('C:/Users') == FilePath('C:/Users') + True + +Moreover, for all methods and attributes they return similar (only +possibly differing in class name) values:: + + >>> PurePath('C:/Windows').drive == SimplePath('C:/Windows').drive + True + >>> Path('C:/Users').is_dir() == FilePath('C:/Users').is_dir() + True + +Note that SimplePath and FilePath are not factories that return a +different class. Instead they implement the type of class that would be +returned by the factory on your particular system type. For instance on +Windows:: + + >>> type(PurePath('.')) # On Windows + + >>> type(SimplePath('.')) + + +Similarly on Posix-based systems:: + + >>> type(Path('.')) # on Posix + + >>> type(FilePath('.')) + + +However unlike :class:`PurePath` and :class:`Path`, :class:`SimplePath` +and :class:`FilePath` can be subclassed and still function equivalently:: + + >>> class MySimplePath(SimplePath): pass # On Windows + ... + >>> MySimplePath('C:/Users').parent + MySimplePath('C:/') + >>> class MyFilePath(FilePath): pass + ... + >>> MyFilePath('C:/Windows/System32/drivers/etc/../..').resolve() + MyFilePath('C:/Windows/System32') + +Furthermore, if you are subclassing paths for a specific system type, +but which don't require accessing the filesystem, much like with +:class:`PurePath` you can use the derivatives of :class:`SimplePath`, +:class:`SimplePosixPath` or :class:`SimpleWindowsPath`:: + + >>> PureWindowsPath('C:/').drive + 'C:' + >>> MySimpleWindowsPath(SimpleWindowsPath): pass + ... + >>> MySimpleWindowsPath('C:/').drive + 'C:' + +If you want to write a subclass but do need to customize how the +methods that do access the filesystem are implemented, you can instead +use the :class:`PathIOMixin`. + +.. class:: PathIOMixin + + A mixin which provides a default filesystem I/O implementation for + Posix and Windows. It requires being used in conjunction with either + :class:`SimplePosixPath` or :class:`SimpleWindowsPath` to create a + full-fledged :class:`Path`/:class:`FilePath`-like class. This class + is actually the provider for all of the methods that are exclusively + available to :class:`Path` family and :class:`FilePath`. As such all + of these methods are available here to be extended by you. + + >>> MyRemoteServerIOMixin(PathIOMixin): + ... def iterdir(self): + ... ... # Override method to provide implementation + ... + >>> MyRemoteServerPath(SimplePosixPath, MyRemoteServerIOMixin): pass + ... + + Methods and properties ^^^^^^^^^^^^^^^^^^^^^^ @@ -675,6 +791,24 @@ bugs or failures in your application):: % (cls.__name__,)) NotImplementedError: cannot instantiate 'WindowsPath' on your system +.. class:: FilePath(*pathsegments) + + A subclass of :class:`SimplePath`, this is a subclassable alternative to + the factory :class:`Path`. It represents concrete paths of the system's + path flavour. + While different in name, when instantiated, it will otherwise behave + similarly to :class:`PosixPath` or :class:`Windows`Path` depending + on the respective system type on which it is being run.) + + *pathsegments* is specified similarly to :class:`PurePath`. + + >>> class MyFilePath(FilePath): pass # On Posix + ... + >>> MyFilePath('usr/local/bin/../../').resolve() + MyFilePath('/usr') + >>> Path('/usr/local/bin/../..').resolve() + PosixPath('/usr') + Methods ^^^^^^^ diff --git a/Lib/pathlib.py b/Lib/pathlib.py index d0b21d70e8a23f..8c81c6d3fa74e0 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -17,6 +17,8 @@ __all__ = [ "PurePath", "PurePosixPath", "PureWindowsPath", "Path", "PosixPath", "WindowsPath", + "SimplePath", "SimplePosixPath", "SimpleWindowsPath", + "FilePath", "PathIOMixin", ] # diff --git a/Misc/ACKS b/Misc/ACKS index b023bcb6d72fd3..553eb1a75bab37 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -545,6 +545,7 @@ Matt Fleming Hernán Martínez Foffani Benjamin Fogle Artem Fokin +Kevin Follstad Arnaud Fontaine Michael Foord Amaury Forgeot d'Arc diff --git a/Misc/NEWS.d/next/Library/2021-05-25-21-25-39.bpo-24132.37mJNj.rst b/Misc/NEWS.d/next/Library/2021-05-25-21-25-39.bpo-24132.37mJNj.rst new file mode 100644 index 00000000000000..88dc3f50150133 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-05-25-21-25-39.bpo-24132.37mJNj.rst @@ -0,0 +1,5 @@ +Add a drop-in, extensible alternative to :class:`pathlib.PurePath` named +:class:`pathlib.SimplePath`, which has accompanying classes +:class:`pathlib.SimplePosixPath` and :class:`pathlib.SimpleWindowsPath`. +Also add extensible alternative to :class:`pathlib.Path` named +:class:`pathlib.FilePath`.