diff --git a/Doc/whatsnew/3.10.rst b/Doc/whatsnew/3.10.rst index eb5ae01a7c04d4..7c0459852eb883 100644 --- a/Doc/whatsnew/3.10.rst +++ b/Doc/whatsnew/3.10.rst @@ -132,6 +132,14 @@ os Added :func:`os.cpu_count()` support for VxWorks RTOS. (Contributed by Peixing Xin in :issue:`41440`.) +pathlib +------- +Subclasses of :class:`pathlib.Path` and :class:`pathlib.PurePath` now call the +:func:`__new__` and :func:`__init__` functions of the subclasses when +instantiating new subclass objects returned by various :class:`pathlib.Path` and +:class:`pathlib.PurePath` functions and properties. (Contributed by Jeffrey +Kintscher in :issue:`41109`.) + py_compile ---------- diff --git a/Lib/pathlib.py b/Lib/pathlib.py index 9f5e27b91178e6..fb28532d76e709 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -629,7 +629,7 @@ def __len__(self): def __getitem__(self, idx): if idx < 0 or idx >= len(self): raise IndexError(idx) - return self._pathcls._from_parsed_parts(self._drv, self._root, + return self._pathcls()._new_from_parsed_parts(self._drv, self._root, self._parts[:-idx - 1]) def __repr__(self): @@ -658,7 +658,13 @@ def __new__(cls, *args): """ if cls is PurePath: cls = PureWindowsPath if os.name == 'nt' else PurePosixPath - return cls._from_parts(args) + return object.__new__(cls) + + def __init__(self, *args): + drv, root, parts = self._parse_args(args) + self._drv = drv + self._root = root + self._parts = parts def __reduce__(self): # Using the parts tuple helps share interned path parts @@ -687,6 +693,9 @@ def _parse_args(cls, args): @classmethod def _from_parts(cls, args, init=True): + import warnings + warnings.warn('_from_parsed_parts() is deprecated, use _new_from_parsed_parts() instead', + DeprecationWarning, 2) # We need to call _parse_args on the instance, so as to get the # right flavour. self = object.__new__(cls) @@ -700,6 +709,9 @@ def _from_parts(cls, args, init=True): @classmethod def _from_parsed_parts(cls, drv, root, parts, init=True): + import warnings + warnings.warn('_from_parsed_parts() is deprecated, use _new_from_parsed_parts() instead', + DeprecationWarning, 2) self = object.__new__(cls) self._drv = drv self._root = root @@ -708,6 +720,17 @@ def _from_parsed_parts(cls, drv, root, parts, init=True): self._init() return self + def _new_from_parts(self, args): + obj = type(self)(*args) + return obj + + def _new_from_parsed_parts(self, drv, root, parts): + obj = type(self)() + obj._drv = drv + obj._root = root + obj._parts = parts + return obj + @classmethod def _format_parsed_parts(cls, drv, root, parts): if drv or root: @@ -723,7 +746,7 @@ def _make_child(self, args): drv, root, parts = self._parse_args(args) drv, root, parts = self._flavour.join_parsed_parts( self._drv, self._root, self._parts, drv, root, parts) - return self._from_parsed_parts(drv, root, parts) + return self._new_from_parsed_parts(drv, root, parts) def __str__(self): """Return the string representation of the path, suitable for @@ -867,7 +890,7 @@ def with_name(self, name): if (not name or name[-1] in [self._flavour.sep, self._flavour.altsep] or drv or root or len(parts) != 1): raise ValueError("Invalid name %r" % (name)) - return self._from_parsed_parts(self._drv, self._root, + return self._new_from_parsed_parts(self._drv, self._root, self._parts[:-1] + [name]) def with_stem(self, stem): @@ -892,8 +915,8 @@ def with_suffix(self, suffix): name = name + suffix else: name = name[:-len(old_suffix)] + suffix - return self._from_parsed_parts(self._drv, self._root, - self._parts[:-1] + [name]) + return self._new_from_parsed_parts(self._drv, self._root, + self._parts[:-1] + [name]) def relative_to(self, *other): """Return the relative path to another path identified by the passed @@ -925,8 +948,8 @@ def relative_to(self, *other): raise ValueError("{!r} is not in the subpath of {!r}" " OR one path is relative and the other is absolute." .format(str(self), str(formatted))) - return self._from_parsed_parts('', root if n == 1 else '', - abs_parts[n:]) + return self._new_from_parsed_parts('', root if n == 1 else '', + abs_parts[n:]) def is_relative_to(self, *other): """Return True if the path is relative to another path or False. @@ -965,7 +988,7 @@ def __truediv__(self, key): def __rtruediv__(self, key): try: - return self._from_parts([key] + self._parts) + return self._new_from_parts([key] + self._parts) except TypeError: return NotImplemented @@ -977,7 +1000,7 @@ def parent(self): parts = self._parts if len(parts) == 1 and (drv or root): return self - return self._from_parsed_parts(drv, root, parts[:-1]) + return self._new_from_parsed_parts(drv, root, parts[:-1]) @property def parents(self): @@ -1065,13 +1088,16 @@ class Path(PurePath): def __new__(cls, *args, **kwargs): if cls is Path: cls = WindowsPath if os.name == 'nt' else PosixPath - self = cls._from_parts(args, init=False) + self = object.__new__(cls) if not self._flavour.is_supported: raise NotImplementedError("cannot instantiate %r on your system" % (cls.__name__,)) - self._init() return self + def __init__(self, *args): + super().__init__(*args) + self._init() + def _init(self, # Private non-constructor arguments template=None, @@ -1085,7 +1111,7 @@ def _make_child_relpath(self, part): # This is an optimization used for dir walking. `part` must be # a single part relative to this path. parts = self._parts + [part] - return self._from_parsed_parts(self._drv, self._root, parts) + return self._new_from_parsed_parts(self._drv, self._root, parts) def __enter__(self): return self @@ -1188,7 +1214,7 @@ def absolute(self): return self # FIXME this must defer to the specific flavour (and, under Windows, # use nt._getfullpathname()) - obj = self._from_parts([os.getcwd()] + self._parts, init=False) + obj = self._new_from_parts([os.getcwd()] + self._parts) obj._init(template=self) return obj @@ -1206,7 +1232,7 @@ def resolve(self, strict=False): s = str(self.absolute()) # Now we have no symlinks in the path, it's safe to normalize it. normed = self._flavour.pathmod.normpath(s) - obj = self._from_parts((normed,), init=False) + obj = self._new_from_parts((normed,)) obj._init(template=self) return obj @@ -1276,7 +1302,7 @@ def readlink(self): Return the path to which the symbolic link points. """ path = self._accessor.readlink(self) - obj = self._from_parts((path,), init=False) + obj = self._new_from_parts((path,)) obj._init(template=self) return obj @@ -1541,7 +1567,7 @@ def expanduser(self): if (not (self._drv or self._root) and self._parts and self._parts[0][:1] == '~'): homedir = self._flavour.gethomedir(self._parts[0][1:]) - return self._from_parts([homedir] + self._parts[1:]) + return self._new_from_parts([homedir] + self._parts[1:]) return self diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 04f7c3d86671bf..4d33e59505c2d2 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -2585,5 +2585,303 @@ def test_rtruediv(self): 10 / pathlib.PurePath("test") +class PurePosixPathSubclassNewAndInitTest(unittest.TestCase): + """ + Test that the __init__() and __new__() functions of subclasses + of PurePosixPath get called when PurePosixPath functions + and properties instantiate new objects of the subclass. + """ + cls = pathlib.PurePosixPath + + class ASubclass(cls): + new_called = False + init_called = False + + def __new__(cls, *args, **kwargs): + cls.new_called = True + return super().__new__(cls, *args, **kwargs) + + def __init__(self, *args, **kwargs): + self.init_called = True + super().__init__(*args, **kwargs) + + def validate_object(self, path, value): + self.assertIs(type(path), self.ASubclass) + self.assertTrue(path.new_called) + self.assertTrue(path.init_called) + self.assertEqual(str(path), value) + + def test_class_initialization(self): + self.validate_object(self.ASubclass('/a/b/c.foo'), '/a/b/c.foo') + self.validate_object(self.ASubclass('/', 'a', 'b', 'c.foo'), + '/a/b/c.foo') + self.validate_object(self.ASubclass(self.cls('/a/b')), '/a/b') + + def test_joinpath(self): + self.validate_object(self.ASubclass('a/b/c.foo').parent.joinpath('d/e'), + 'a/b/d/e') + + def test_parent(self): + self.validate_object(self.ASubclass('a/b/c.foo').parent, 'a/b') + + def test_parents(self): + path = self.ASubclass('a/b/c.foo') + self.validate_object(path.parents[0], 'a/b') + self.validate_object(path.parents[1], 'a') + + def test_relative_to(self): + self.validate_object(self.ASubclass('a/b/c.foo').relative_to('a'), + 'b/c.foo') + + def test_rtruediv(self): + self.validate_object('left' / self.ASubclass('test'), 'left/test') + + def test_truediv(self): + path = self.ASubclass('a/b/c.foo') + self.validate_object(path.parent / 'd' / path, 'a/b/d/a/b/c.foo') + + def test_with_name(self): + self.validate_object(self.ASubclass('a/b/c.foo').with_name('bar'), + 'a/b/bar') + + def test_with_suffix(self): + self.validate_object(self.ASubclass('a/b/c.foo').with_suffix('.bar'), + 'a/b/c.bar') + + +class PureWindowsPathSubclassNewAndInitTest(unittest.TestCase): + """ + Test that the __init__() and __new__() functions of subclasses + of PureWindowsPath get called when PureWindowsPath functions + and properties instantiate new objects of the subclass. + """ + cls = pathlib.PureWindowsPath + + class ASubclass(cls): + new_called = False + init_called = False + + def __new__(cls, *args, **kwargs): + cls.new_called = True + return super().__new__(cls, *args, **kwargs) + + def __init__(self, *args, **kwargs): + self.init_called = True + super().__init__(*args, **kwargs) + + def validate_object(self, path, value): + self.assertIs(type(path), self.ASubclass) + self.assertTrue(path.new_called) + self.assertTrue(path.init_called) + self.assertEqual(str(path), value) + + def test_class_initialization(self): + self.validate_object(self.ASubclass('c:\\a\\b\\c.foo'), + 'c:\\a\\b\\c.foo') + self.validate_object(self.ASubclass('c:', '\\', 'a', 'b', 'c.foo'), + 'c:\\a\\b\\c.foo') + self.validate_object(self.ASubclass(self.cls('c:\\a')), 'c:\\a') + + def test_joinpath(self): + self.validate_object(self.ASubclass('c:\\a\\b\\c.foo').parent.joinpath('d\\e'), + 'c:\\a\\b\\d\\e') + + def test_parent(self): + self.validate_object(self.ASubclass('c:\\a\\b\\c.foo').parent, 'c:\\a\\b') + + def test_parents(self): + path = self.ASubclass('c:\\a\\b\\c.foo') + self.validate_object(path.parents[0], 'c:\\a\\b') + self.validate_object(path.parents[1], 'c:\\a') + + def test_relative_to(self): + self.validate_object(self.ASubclass('c:\\a\\b\\c.foo').relative_to('c:\\a'), + 'b\\c.foo') + + def test_rtruediv(self): + self.validate_object('c:\\left' / self.ASubclass('test'), 'c:\\left\\test') + + def test_truediv(self): + path = self.ASubclass('c:\\a\\b\\c.foo') + self.validate_object(path.parent / 'd', 'c:\\a\\b\\d') + + def test_with_name(self): + self.validate_object(self.ASubclass('c:\\a\\b\\c.foo').with_name('bar'), + 'c:\\a\\b\\bar') + + def test_with_suffix(self): + self.validate_object(self.ASubclass('c:\\a\\b\\c.foo').with_suffix('.bar'), + 'c:\\a\\b\\c.bar') + + +class _BasePathSubclassNewAndInitTest(object): + """ + Test that the __init__() and __new__() functions of subclasses + of Path get called when Path functions + and properties instantiate new objects of the subclass. + """ + def setUp(self): + def cleanup(): + os_helper.rmtree(BASE) + self.addCleanup(cleanup) + os.mkdir(BASE) + with open(join(BASE, 'fileA'), 'wb') as f: + f.write(b"this is file A\n") + + def validate_object(self, path, value): + self.assertIs(type(path), self.ASubclass) + self.assertTrue(path.new_called) + self.assertTrue(path.init_called) + self.assertEqual(str(path), value) + + def test_glob(self): + d = BASE + filename = 'fileA' + path = self.ASubclass(d) + self.validate_object(path, d) + cases = [('f*', join(d, filename)), # _WildcardSelector + ('fileA', join(d, filename)), # _PreciseSelector + ('**', d)] # _RecursiveWildcardSelector + + for param, value in cases: + n = 0 + for name in path.glob(param): + self.validate_object(name, value) + n += 1 + self.assertEqual(n, 1) + + def test_rglob(self): + d = BASE + filename = 'fileA' + path = self.ASubclass(d) + self.validate_object(path, d) + n = 0 + for name in path.rglob('f*'): + self.validate_object(name, join(d, filename)) + n += 1 + self.assertEqual(n, 1) + + def test_iterdir(self): + d = BASE + filename = 'fileA' + path = self.ASubclass(d) + self.validate_object(path, d) + n = 0 + for name in path.iterdir(): + self.validate_object(name, join(d, filename)) + n += 1 + self.assertEqual(n, 1) + + @os_helper.skip_unless_symlink + def test_readlink(self): + d = BASE + filename = 'fileA' + linkname = join(d, 'linkA') + path = self.ASubclass(linkname) + self.validate_object(path, linkname) + os.symlink(filename, linkname) + try: + self.validate_object(path.readlink(), filename) + finally: + os.unlink(linkname) + + def test_resolve(self): + # create a file in a temp directory + d = BASE + filename = join(d, 'fileA') + path = self.ASubclass(d, filename) + self.validate_object(path, filename) + self.validate_object(path.resolve(strict=True), filename) + + +@only_posix +class PosixPathSubclassNewAndInitTest(_BasePathSubclassNewAndInitTest, + unittest.TestCase): + """ + Test that the __init__() and __new__() functions of subclasses + of PosixPath get called when PosixPath functions + and properties instantiate new objects of the subclass. + """ + cls = pathlib.PosixPath + + class ASubclass(cls): + new_called = False + init_called = False + + def __new__(cls, *args, **kwargs): + cls.new_called = True + return super().__new__(cls, *args, **kwargs) + + def __init__(self, *args, **kwargs): + self.init_called = True + super().__init__(*args, **kwargs) + + def test_class_initialization(self): + self.validate_object(self.ASubclass('/a/b/c.foo'), '/a/b/c.foo') + self.validate_object(self.ASubclass('/', 'a', 'b', 'c.foo'), + '/a/b/c.foo') + self.validate_object(self.ASubclass(self.cls('/a/b')), '/a/b') + + def test_absolute(self): + relpath = 'a/b/c.foo' + self.validate_object(self.ASubclass(relpath).absolute(), + join(os.getcwd(), relpath)) + + def test_expanduser(self): + import_helper.import_module('pwd') + import pwd + pwdent = pwd.getpwuid(os.getuid()) + userhome = pwdent.pw_dir.rstrip('/') or '/' + path = self.ASubclass('~/Documents') + with os_helper.EnvironmentVarGuard() as env: + env.pop('HOME', None) + self.validate_object(path.expanduser(), join(userhome, 'Documents')) + + +@only_nt +class WindowsPathSubclassNewAndInitTest(_BasePathSubclassNewAndInitTest, + unittest.TestCase): + """ + Test that the __init__() and __new__() functions of subclasses + of WindowsPath get called when WindowsPath functions + and properties instantiate new objects of the subclass. + """ + cls = pathlib.WindowsPath + + class ASubclass(cls): + new_called = False + init_called = False + + def __new__(cls, *args, **kwargs): + cls.new_called = True + return super().__new__(cls, *args, **kwargs) + + def __init__(self, *args, **kwargs): + self.init_called = True + super().__init__(*args, **kwargs) + + def test_class_initialization(self): + self.validate_object(self.ASubclass('c:\\a\\b\\c.foo'), + 'c:\\a\\b\\c.foo') + self.validate_object(self.ASubclass('c:', '\\', 'a', 'b', 'c.foo'), + 'c:\\a\\b\\c.foo') + self.validate_object(self.ASubclass(self.cls('c:\\a')), 'c:\\a') + + def test_absolute(self): + relpath = 'a\\b\\c.foo' + self.validate_object(self.ASubclass(relpath).absolute(), + join(os.getcwd(), relpath)) + + def test_expanduser(self): + import_helper.import_module('pwd') + import pwd + pwdent = pwd.getpwuid(os.getuid()) + userhome = pwdent.pw_dir.rstrip('\\') or '\\' + path = self.ASubclass('~\\Documents') + with os_helper.EnvironmentVarGuard() as env: + env.pop('HOME', None) + self.validate_object(path.expanduser(), join(userhome, 'Documents')) + + if __name__ == "__main__": unittest.main() diff --git a/Misc/NEWS.d/next/Library/2020-08-18-18-22-56.bpo-41109.QcFpg2.rst b/Misc/NEWS.d/next/Library/2020-08-18-18-22-56.bpo-41109.QcFpg2.rst new file mode 100644 index 00000000000000..a9ac25510cbbf1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-08-18-18-22-56.bpo-41109.QcFpg2.rst @@ -0,0 +1,4 @@ +Subclasses of :class:`pathlib.Path` and :class:`pathlib.PurePath` now call the +:func:`__new__` and :func:`__init__` functions of the subclasses when +instantiating new subclass objects returned by various :class:`pathlib.Path` and +:class:`pathlib.PurePath` functions and properties. Patch by Jeffrey Kintscher.