From 710f5778302dbe4363a0e9a8b187275d6c1bc47c Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Fri, 11 Jun 2021 13:10:06 -0700 Subject: [PATCH 1/7] bpo-24132: Add tests for class properties in pathlib's PurePath/Path Add tests to verify previously untested black-box class properties of PurePath and Path in pathlib. Collectively, these tests verify all the ways the classes represent themselves via isinstance, issubclass, __repr__(), etc. Moreover all class instances spawned by PurePath or Path are also similarly tested. --- Lib/test/test_pathlib.py | 80 ++++++++++++++++++++++++++++++++++------ 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 54b7977b43f235..3f39b03a7cc2a8 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1296,13 +1296,50 @@ 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): + +class _PathPurePathCommonTest: + def assertClassProperties(self, instances, correct_cls): + _instances = instances if hasattr(instances, '__iter__') else [instances] + for instance in _instances: + self.assertEqual(instance.__class__, correct_cls) + self.assertEqual( + instance.__repr__(), + f"{correct_cls.__name__}('{instance.as_posix()}')" + ) + self.assertTrue(isinstance(instance, correct_cls)) + self.assertTrue(isinstance(instance, self.cls)) + self.assertTrue(issubclass(instance.__class__, correct_cls)) + self.assertTrue(issubclass(instance.__class__, self.cls)) + self.assertTrue(type(instance), correct_cls) + + def assertNonIONewInstancesClassProperties(self, instance, correct_cls): + self.assertClassProperties(instance.with_name('new_name'), correct_cls) + self.assertClassProperties(instance.with_stem('new_name2'), + correct_cls) + self.assertClassProperties(instance.with_suffix('.ext'), correct_cls) + self.assertClassProperties( + instance.relative_to(instance.drive, instance.root), correct_cls + ) + self.assertClassProperties(instance.joinpath('.'), correct_cls) + self.assertClassProperties(instance.parents, correct_cls) + self.assertClassProperties(instance / "subdir", correct_cls) + self.assertClassProperties("" / instance, correct_cls) + + +class PurePathTest( + _BasePurePathTest, _PathPurePathCommonTest, unittest.TestCase +): cls = pathlib.PurePath - def test_concrete_class(self): - p = self.cls('a') - self.assertIs(type(p), - pathlib.PureWindowsPath if os.name == 'nt' else pathlib.PurePosixPath) + def _get_class_to_generate(self): + is_posix = os.name == 'posix' + return pathlib.PurePosixPath if is_posix else pathlib.PureWindowsPath + + def test_instance_class_properties(self): + p = self.cls('placeholder') + correct_cls = self._get_class_to_generate() + self.assertClassProperties(p, correct_cls) + self.assertNonIONewInstancesClassProperties(p, correct_cls) def test_different_flavours_unequal(self): p = pathlib.PurePosixPath('a') @@ -2414,17 +2451,38 @@ def test_complex_symlinks_relative_dot_dot(self): self._check_complex_symlinks(os.path.join('dirA', '..')) -class PathTest(_BasePathTest, unittest.TestCase): +class PathTest(_BasePathTest, _PathPurePathCommonTest, unittest.TestCase): cls = pathlib.Path + def _get_class_to_generate(self): + is_posix = os.name == 'posix' + return pathlib.PosixPath if is_posix else pathlib.WindowsPath + + def assertIONoLinkNewInstancesClassProperties(self, instance, correct_cls): + self.assertClassProperties(instance.cwd(), correct_cls) + self.assertClassProperties(instance.home(), correct_cls) + self.assertClassProperties(instance.iterdir(), correct_cls) + self.assertClassProperties(instance.glob('*'), correct_cls) + self.assertClassProperties(instance.rglob('*'), correct_cls) + self.assertClassProperties(instance.absolute(), correct_cls) + self.assertClassProperties(instance.resolve(strict=False), correct_cls) + self.assertClassProperties(instance.expanduser(), correct_cls) + self.assertClassProperties(instance.replace(BASE), correct_cls) + + def test_instance_class_properties(self): + P = self.cls + p = P(BASE) + correct_cls = self._get_class_to_generate() + self.assertClassProperties(p, correct_cls) + self.assertNonIONewInstancesClassProperties(p, correct_cls) + self.assertIONoLinkNewInstancesClassProperties(p, correct_cls) + if os_helper.can_symlink(): + link = P(BASE, "linkA") + self.assertClassProperties(link.readlink(), correct_cls) + def test_class_getitem(self): self.assertIs(self.cls[str], self.cls) - def test_concrete_class(self): - p = self.cls('a') - self.assertIs(type(p), - pathlib.WindowsPath if os.name == 'nt' else pathlib.PosixPath) - def test_unsupported_flavour(self): if os.name == 'nt': self.assertRaises(NotImplementedError, pathlib.PosixPath) From a373969d084b89b60cf34bbe32becd1fec9e8707 Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Mon, 14 Jun 2021 14:43:14 -0700 Subject: [PATCH 2/7] bpo-24132: Add test for direct subclassing of PurePath/Path in pathlib Add expected failing tests to pathlib that will verify the ability of PurePath and Path to be subclassed by users. As part of this, test all of the black-box class properties of the newly subclassed Path-like class instances, and the instances generated by their methods. --- Lib/test/test_pathlib.py | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 3f39b03a7cc2a8..9d620a8a470dd8 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1341,6 +1341,30 @@ def test_instance_class_properties(self): self.assertClassProperties(p, correct_cls) self.assertNonIONewInstancesClassProperties(p, correct_cls) + @unittest.expectedFailure + def test_direct_subclassing(self): + P = self.cls + try: + class Derived(P): + pass + except Exception as e: + self.fail(f"Failed to subclass {P}: {e}") + else: + try: + derived = Derived('non_empty_pathsegment') + except Exception as e: + self.fail("Failed to be able to instantiate a class " + f"derived from {P}: {e}") + else: + # Unlike how PurePath behaves, when instantiated, the + # instances of its user-created direct subclass keep + # their original class name (instead of becoming a + # flavour-named variant). Hence we use Derived here. + correct_cls = Derived + self.assertClassProperties(derived, correct_cls) + self.assertNonIONewInstancesClassProperties(derived, + correct_cls) + def test_different_flavours_unequal(self): p = pathlib.PurePosixPath('a') q = pathlib.PureWindowsPath('a') @@ -2480,6 +2504,36 @@ def test_instance_class_properties(self): link = P(BASE, "linkA") self.assertClassProperties(link.readlink(), correct_cls) + @unittest.expectedFailure + def test_direct_subclassing(self): + P = self.cls + try: + class Derived(P): + pass + except Exception as e: + self.fail(f"Failed to subclass {P}: {e}") + else: + try: + derived_base = Derived(BASE) + except Exception as e: + self.fail("Failed to be able to instantiate a class " + f"derived from {P}: {e}") + else: + # Much like in the original version of this method, + # we use Derived here because unlike Path, user-created + # subclasses of Path keep their names when instantiated + # (instead of becoming a flavour-named variant). + correct_cls = Derived + self.assertClassProperties(derived_base, correct_cls) + self.assertNonIONewInstancesClassProperties(derived_base, + correct_cls) + self.assertIONoLinkNewInstancesClassProperties(derived_base, + correct_cls) + if os_helper.can_symlink(): + derived_link = Derived(BASE, "linkA") + self.assertClassProperties(derived_link.readlink(), + correct_cls) + def test_class_getitem(self): self.assertIs(self.cls[str], self.cls) From 35d8f789fad64674a7c7d254f2d5808049f0e504 Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Thu, 27 May 2021 11:09:36 -0700 Subject: [PATCH 3/7] 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 8e6eb48b9767ca..439a1bc4a1294e 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -1322,6 +1322,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 @@ -1444,6 +1447,3 @@ class WindowsPath(Path, PureWindowsPath): On a Windows system, instantiating a Path should return this object. """ __slots__ = () - - def is_mount(self): - raise NotImplementedError("Path.is_mount() is unsupported on this system") From bab67b412ebe5b9283dc85326abf1c396ca1232b Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Mon, 3 May 2021 16:32:16 -0700 Subject: [PATCH 4/7] bpo-24132: Replace additional hardcoded references in pathlib Replace hardcorded references to pathlib.Path to facilitate making Path extensible. --- Lib/pathlib.py | 36 ++++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 439a1bc4a1294e..1aade128f0603e 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -325,21 +325,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 @@ -965,7 +968,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. @@ -1012,7 +1015,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,)) @@ -1027,7 +1030,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") @@ -1215,9 +1218,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) @@ -1228,9 +1231,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) @@ -1256,15 +1259,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) @@ -1323,8 +1327,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 67faae045e7b95ffe1d4ccb742b5cd4006145dd0 Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Mon, 14 Jun 2021 16:35:48 -0700 Subject: [PATCH 5/7] bpo-24132: Replace factory functionality with masquerade in pathlib Replace factory functionality of PurePath and Path with a psuedo-factory which appears to generate it's derived subclasses, but instead just masquerades the new instance as one of the subclasses. Doing so makes the resultant versions of PurePath and Path directly subclassable and as such allows them to pass all of the previously failing subclass tests. --- Lib/pathlib.py | 42 +++++++++++++++++++++++++++++++++------- Lib/test/test_pathlib.py | 2 -- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 1aade128f0603e..913c7170de3c56 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -548,10 +548,23 @@ def __new__(cls, *args): 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 + if not hasattr(cls, '_flavour'): + is_posix = os.name == 'posix' + cls._flavour = _posix_flavour if is_posix else _windows_flavour return cls._from_parts(args) + def __init__(self, *pathsegments, **kwargs): + # __init__ was empty for 8 years, therefore one should avoid + # making any assumption below that super().__init__() + # will be called outside of the code in pathlib. + if self.__class__ is PurePath: + self._masquerade() + + def _masquerade(self): + is_posix_flavoured = self._flavour.__class__ == _PosixFlavour + disguise_cls = PurePosixPath if is_posix_flavoured else PureWindowsPath + self.__class__ = disguise_cls + def __reduce__(self): # Using the parts tuple helps share interned path parts # when pickling related paths. @@ -942,17 +955,24 @@ class Path(PurePath): object. You can also instantiate a PosixPath or WindowsPath directly, but cannot instantiate a WindowsPath on a POSIX system or vice versa. """ + _flavour = None _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: + if not hasattr(cls, '_flavour') or cls._flavour is None: + is_posix = os.name == 'posix' + cls._flavour = _posix_flavour if is_posix else _windows_flavour + if not cls._flavour.is_supported: raise NotImplementedError("cannot instantiate %r on your system" % (cls.__name__,)) - return self + return cls._from_parts(args) + + def __init__(self, *pathsegments, **kwargs): + # Similar to PurePath.__init__, avoid assuming that this will be + # called via super() outside of pathlib. + if self.__class__ is Path: + self._masquerade() def _make_child_relpath(self, part): # This is an optimization used for dir walking. `part` must be @@ -960,6 +980,11 @@ def _make_child_relpath(self, part): parts = self._parts + [part] return self._from_parsed_parts(self._drv, self._root, parts) + def _masquerade(self): + is_posix_flavoured = self._flavour.__class__ == _PosixFlavour + disguise_cls = PosixPath if is_posix_flavoured else WindowsPath + self.__class__ = disguise_cls + def __enter__(self): return self @@ -1443,11 +1468,14 @@ class PosixPath(Path, PurePosixPath): On a POSIX system, instantiating a Path should return this object. """ + _flavour = _posix_flavour __slots__ = () + class WindowsPath(Path, PureWindowsPath): """Path subclass for Windows systems. On a Windows system, instantiating a Path should return this object. """ + _flavour = _windows_flavour __slots__ = () diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 9d620a8a470dd8..b1eef749747a82 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1341,7 +1341,6 @@ def test_instance_class_properties(self): self.assertClassProperties(p, correct_cls) self.assertNonIONewInstancesClassProperties(p, correct_cls) - @unittest.expectedFailure def test_direct_subclassing(self): P = self.cls try: @@ -2504,7 +2503,6 @@ def test_instance_class_properties(self): link = P(BASE, "linkA") self.assertClassProperties(link.readlink(), correct_cls) - @unittest.expectedFailure def test_direct_subclassing(self): P = self.cls try: From 610f41ed81e96b525b6ac230117527255e79b73d Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Thu, 17 Jun 2021 10:35:10 -0700 Subject: [PATCH 6/7] bpo-24132: Add tests for comparing subclasses of PurePath in pathlib Add tests mirroring existing comparison tests for PurePosixPath and PureWindowsPath but extending them to direct subclasses of PurePath. --- Lib/test/test_pathlib.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index b1eef749747a82..6e6e498b704290 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1335,6 +1335,10 @@ def _get_class_to_generate(self): is_posix = os.name == 'posix' return pathlib.PurePosixPath if is_posix else pathlib.PureWindowsPath + def _get_anti_system_flavour_class(self): + is_posix = os.name == 'posix' + return pathlib.PureWindowsPath if is_posix else pathlib.PurePosixPath + def test_instance_class_properties(self): p = self.cls('placeholder') correct_cls = self._get_class_to_generate() @@ -1369,9 +1373,15 @@ def test_different_flavours_unequal(self): q = pathlib.PureWindowsPath('a') self.assertNotEqual(p, q) - def test_different_flavours_unordered(self): - p = pathlib.PurePosixPath('a') - q = pathlib.PureWindowsPath('a') + def test_subclass_different_flavours_unequal(self): + class Derived(pathlib.PurePath): + pass + p = Derived('a') + PureAntiFlavourPath = self._get_anti_system_flavour_class() + q = PureAntiFlavourPath('a') + self.assertNotEqual(p, q) + + def _test_different_flavours_unordered(self, p, q): with self.assertRaises(TypeError): p < q with self.assertRaises(TypeError): @@ -1381,6 +1391,19 @@ def test_different_flavours_unordered(self): with self.assertRaises(TypeError): p >= q + def test_different_flavours_unordered(self): + p = pathlib.PurePosixPath('a') + q = pathlib.PureWindowsPath('a') + self._test_different_flavours_unordered(p, q) + + def test_subclass_different_flavours_unordered(self): + class Derived(pathlib.PurePath): + pass + p = Derived('a') + PureAntiFlavourPath = self._get_anti_system_flavour_class() + q = PureAntiFlavourPath('a') + self._test_different_flavours_unordered(p, q) + # # Tests for the concrete classes. From a0c4998ffd2fdb32aa8901a7ba872a4d9ddaabba Mon Sep 17 00:00:00 2001 From: Kevin Follstad Date: Wed, 16 Jun 2021 15:53:28 -0700 Subject: [PATCH 7/7] bpo-24132: Document direct subclassing of PurePath/Path in pathlib Add documentation highlighting the new direct subclass functionality of PurePath and Path. Also add new is_posix & is_windows doctest conditions so that system specific doctest code can still be tested instead of just being taken as a literal block and always skipped. --- Doc/conf.py | 4 ++ Doc/library/pathlib.rst | 52 ++++++++++++++++++- Misc/ACKS | 1 + .../2021-06-16-17-38-36.bpo-24132.FqsJWY.rst | 2 + 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2021-06-16-17-38-36.bpo-24132.FqsJWY.rst diff --git a/Doc/conf.py b/Doc/conf.py index 2a1d0e3dfd873e..c90db7b72479b2 100644 --- a/Doc/conf.py +++ b/Doc/conf.py @@ -22,6 +22,10 @@ import _tkinter except ImportError: _tkinter = None +from os import name as system_name +is_posix = system_name == 'posix' +is_windows = system_name == 'nt' +del system_name ''' manpages_url = 'https://manpages.debian.org/{path}' diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index b6507eb4d6fa2c..c4e7a1d69040a9 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -273,7 +273,7 @@ Methods and properties .. testsetup:: - from pathlib import PurePath, PurePosixPath, PureWindowsPath + from pathlib import PurePath, PurePosixPath, PureWindowsPath, Path Pure paths provide the following methods and properties: @@ -1229,6 +1229,56 @@ call fails (for example because the path doesn't exist). .. versionchanged:: 3.10 The *newline* parameter was added. +Subclassing and Extensibility +----------------------------- + +Both :class:`PurePath` and :class:`Path` are directly subclassable and extensible as you +see fit: + + >>> class MyPath(Path): + ... def my_method(self, *args, **kwargs): + ... ... # Platform agnostic implementation + +.. note:: + Unlike :class:`PurePath` or :class:`Path`, instantiating the derived + class will not generate a differently named class: + + .. doctest:: + :pyversion: > 3.11 + :skipif: is_windows + + >>> Path('.') # On POSIX + PosixPath('.') + >>> MyPath('.') + MyPath('.') + + Despite this, the subclass will otherwise match the class that would be + returned by the factory on your particular system type. For instance, + when instantiated on a POSIX system: + + .. doctest:: + :pyversion: > 3.11 + :skipif: is_windows + + >>> [Path('/dir').is_absolute, MyPath('/dir').is_absolute()] + [True, True] + >>> [Path().home().drive, MyPath().home().drive] + ['', ''] + + However on Windows, the *same code* will instead return values which + apply to that system: + + .. doctest:: + :pyversion: > 3.11 + :skipif: is_posix + + >>> [Path('/dir').is_absolute(), MyPath('/dir').is_absolute()] + [False, False] + >>> [Path().home().drive, MyPath().home().drive] + ['C:', 'C:'] + +.. versionadded:: 3.11 + Correspondence to tools in the :mod:`os` module ----------------------------------------------- diff --git a/Misc/ACKS b/Misc/ACKS index 87de95b938c20e..3b7461d3253753 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-06-16-17-38-36.bpo-24132.FqsJWY.rst b/Misc/NEWS.d/next/Library/2021-06-16-17-38-36.bpo-24132.FqsJWY.rst new file mode 100644 index 00000000000000..de79d7029c48d1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-06-16-17-38-36.bpo-24132.FqsJWY.rst @@ -0,0 +1,2 @@ +Add a mechanism and support for direct subclassing of :class:`PurePath` +and :class:`Path` in pathlib.