]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-127807: pathlib ABCs: remove `PurePathBase._raw_paths` (#127883)
authorBarney Gale <barney.gale@gmail.com>
Sun, 22 Dec 2024 01:17:59 +0000 (01:17 +0000)
committerGitHub <noreply@github.com>
Sun, 22 Dec 2024 01:17:59 +0000 (01:17 +0000)
Remove the `PurePathBase` initializer, and make `with_segments()` and
`__str__()` abstract. This allows us to drop the `_raw_paths` attribute,
and also the `Parser.join()` protocol method.

Lib/pathlib/_abc.py
Lib/pathlib/_local.py
Lib/pathlib/_types.py
Lib/test/test_pathlib/test_pathlib.py
Lib/test/test_pathlib/test_pathlib_abc.py

index b4560295300c283796606f48c9653900d6e69808..4402efe3a0231078726dc9d136ea0e39b2df8132 100644 (file)
@@ -44,49 +44,25 @@ class PurePathBase:
     """Base class for pure path objects.
 
     This class *does not* provide several magic methods that are defined in
-    its subclass PurePath. They are: __fspath__, __bytes__, __reduce__,
-    __hash__, __eq__, __lt__, __le__, __gt__, __ge__. Its initializer and path
-    joining methods accept only strings, not os.PathLike objects more broadly.
+    its subclass PurePath. They are: __init__, __fspath__, __bytes__,
+    __reduce__, __hash__, __eq__, __lt__, __le__, __gt__, __ge__.
     """
 
-    __slots__ = (
-        # The `_raw_paths` slot stores unjoined string paths. This is set in
-        # the `__init__()` method.
-        '_raw_paths',
-    )
+    __slots__ = ()
     parser = posixpath
     _globber = PathGlobber
 
-    def __init__(self, *args):
-        for arg in args:
-            if not isinstance(arg, str):
-                raise TypeError(
-                    f"argument should be a str, not {type(arg).__name__!r}")
-        self._raw_paths = list(args)
-
     def with_segments(self, *pathsegments):
         """Construct a new path object from any number of path-like objects.
         Subclasses may override this method to customize how new path objects
         are created from methods like `iterdir()`.
         """
-        return type(self)(*pathsegments)
+        raise NotImplementedError
 
     def __str__(self):
         """Return the string representation of the path, suitable for
         passing to system calls."""
-        paths = self._raw_paths
-        if len(paths) == 1:
-            return paths[0]
-        elif paths:
-            # Join path segments from the initializer.
-            path = self.parser.join(*paths)
-            # Cache the joined path.
-            paths.clear()
-            paths.append(path)
-            return path
-        else:
-            paths.append('')
-            return ''
+        raise NotImplementedError
 
     def as_posix(self):
         """Return the string representation of the path with forward (/)
@@ -234,17 +210,17 @@ class PurePathBase:
         paths) or a totally different path (if one of the arguments is
         anchored).
         """
-        return self.with_segments(*self._raw_paths, *pathsegments)
+        return self.with_segments(str(self), *pathsegments)
 
     def __truediv__(self, key):
         try:
-            return self.with_segments(*self._raw_paths, key)
+            return self.with_segments(str(self), key)
         except TypeError:
             return NotImplemented
 
     def __rtruediv__(self, key):
         try:
-            return self.with_segments(key, *self._raw_paths)
+            return self.with_segments(key, str(self))
         except TypeError:
             return NotImplemented
 
index b933dd512eeb28ae0bd7276524883ed4648a6ec1..7689c10604d4e6d0f595ed3b752260b88cb563e1 100644 (file)
@@ -77,6 +77,10 @@ class PurePath(PurePathBase):
     """
 
     __slots__ = (
+        # The `_raw_paths` slot stores unjoined string paths. This is set in
+        # the `__init__()` method.
+        '_raw_paths',
+
         # The `_drv`, `_root` and `_tail_cached` slots store parsed and
         # normalized parts of the path. They are set when any of the `drive`,
         # `root` or `_tail` properties are accessed for the first time. The
@@ -140,9 +144,15 @@ class PurePath(PurePathBase):
                         "object where __fspath__ returns a str, "
                         f"not {type(path).__name__!r}")
                 paths.append(path)
-        # Avoid calling super().__init__, as an optimisation
         self._raw_paths = paths
 
+    def with_segments(self, *pathsegments):
+        """Construct a new path object from any number of path-like objects.
+        Subclasses may override this method to customize how new path objects
+        are created from methods like `iterdir()`.
+        """
+        return type(self)(*pathsegments)
+
     def joinpath(self, *pathsegments):
         """Combine this path with one or several arguments, and return a
         new path representing either a subpath (if all arguments are relative
@@ -304,14 +314,29 @@ class PurePath(PurePathBase):
             parts.append('')
         return parts
 
+    @property
+    def _raw_path(self):
+        paths = self._raw_paths
+        if len(paths) == 1:
+            return paths[0]
+        elif paths:
+            # Join path segments from the initializer.
+            path = self.parser.join(*paths)
+            # Cache the joined path.
+            paths.clear()
+            paths.append(path)
+            return path
+        else:
+            paths.append('')
+            return ''
+
     @property
     def drive(self):
         """The drive prefix (letter or UNC path), if any."""
         try:
             return self._drv
         except AttributeError:
-            raw_path = PurePathBase.__str__(self)
-            self._drv, self._root, self._tail_cached = self._parse_path(raw_path)
+            self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
             return self._drv
 
     @property
@@ -320,8 +345,7 @@ class PurePath(PurePathBase):
         try:
             return self._root
         except AttributeError:
-            raw_path = PurePathBase.__str__(self)
-            self._drv, self._root, self._tail_cached = self._parse_path(raw_path)
+            self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
             return self._root
 
     @property
@@ -329,8 +353,7 @@ class PurePath(PurePathBase):
         try:
             return self._tail_cached
         except AttributeError:
-            raw_path = PurePathBase.__str__(self)
-            self._drv, self._root, self._tail_cached = self._parse_path(raw_path)
+            self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
             return self._tail_cached
 
     @property
index 60df94d0b4604933782302a6f631ed2f1cd2f731..baa4a7e2af5b68114e0e7ecd5a8440b12ebcb57a 100644 (file)
@@ -14,7 +14,6 @@ class Parser(Protocol):
     """
 
     sep: str
-    def join(self, path: str, *paths: str) -> str: ...
     def split(self, path: str) -> tuple[str, str]: ...
     def splitdrive(self, path: str) -> tuple[str, str]: ...
     def splitext(self, path: str) -> tuple[str, str]: ...
index ac3a3b4f15c10e1ad38481fd03c9fe8d97aee391..ef482c311542fa9421a6c289a6128c47022f735e 100644 (file)
@@ -229,6 +229,31 @@ class PurePathTest(test_pathlib_abc.DummyPurePathTest):
         self._check_str(p.__fspath__(), ('a/b',))
         self._check_str(os.fspath(p), ('a/b',))
 
+    def test_bytes(self):
+        P = self.cls
+        with self.assertRaises(TypeError):
+            P(b'a')
+        with self.assertRaises(TypeError):
+            P(b'a', 'b')
+        with self.assertRaises(TypeError):
+            P('a', b'b')
+        with self.assertRaises(TypeError):
+            P('a').joinpath(b'b')
+        with self.assertRaises(TypeError):
+            P('a') / b'b'
+        with self.assertRaises(TypeError):
+            b'a' / P('b')
+        with self.assertRaises(TypeError):
+            P('a').match(b'b')
+        with self.assertRaises(TypeError):
+            P('a').relative_to(b'b')
+        with self.assertRaises(TypeError):
+            P('a').with_name(b'b')
+        with self.assertRaises(TypeError):
+            P('a').with_stem(b'b')
+        with self.assertRaises(TypeError):
+            P('a').with_suffix(b'b')
+
     def test_bytes_exc_message(self):
         P = self.cls
         message = (r"argument should be a str or an os\.PathLike object "
index e230dd188799a577155ed40d358f743d79a1d0a7..9198a0cbc45cee0aa2730a9bed02cb76b328f74e 100644 (file)
@@ -53,7 +53,15 @@ class PurePathBaseTest(unittest.TestCase):
 
 
 class DummyPurePath(PurePathBase):
-    __slots__ = ()
+    __slots__ = ('_segments',)
+
+    def __init__(self, *segments):
+        self._segments = segments
+
+    def __str__(self):
+        if self._segments:
+            return self.parser.join(*self._segments)
+        return ''
 
     def __eq__(self, other):
         if not isinstance(other, DummyPurePath):
@@ -66,6 +74,9 @@ class DummyPurePath(PurePathBase):
     def __repr__(self):
         return "{}({!r})".format(self.__class__.__name__, self.as_posix())
 
+    def with_segments(self, *pathsegments):
+        return type(self)(*pathsegments)
+
 
 class DummyPurePathTest(unittest.TestCase):
     cls = DummyPurePath
@@ -97,30 +108,11 @@ class DummyPurePathTest(unittest.TestCase):
         P('a/b/c')
         P('/a/b/c')
 
-    def test_bytes(self):
-        P = self.cls
-        with self.assertRaises(TypeError):
-            P(b'a')
-        with self.assertRaises(TypeError):
-            P(b'a', 'b')
-        with self.assertRaises(TypeError):
-            P('a', b'b')
-        with self.assertRaises(TypeError):
-            P('a').joinpath(b'b')
-        with self.assertRaises(TypeError):
-            P('a') / b'b'
-        with self.assertRaises(TypeError):
-            b'a' / P('b')
-        with self.assertRaises(TypeError):
-            P('a').match(b'b')
-        with self.assertRaises(TypeError):
-            P('a').relative_to(b'b')
-        with self.assertRaises(TypeError):
-            P('a').with_name(b'b')
-        with self.assertRaises(TypeError):
-            P('a').with_stem(b'b')
-        with self.assertRaises(TypeError):
-            P('a').with_suffix(b'b')
+    def test_fspath_common(self):
+        self.assertRaises(TypeError, os.fspath, self.cls(''))
+
+    def test_as_bytes_common(self):
+        self.assertRaises(TypeError, bytes, self.cls(''))
 
     def _check_str_subclass(self, *args):
         # Issue #21127: it should be possible to construct a PurePath object
@@ -1286,36 +1278,6 @@ class DummyPurePathTest(unittest.TestCase):
 # Tests for the virtual classes.
 #
 
-class PathBaseTest(PurePathBaseTest):
-    cls = PathBase
-
-    def test_not_implemented_error(self):
-        p = self.cls('')
-        e = NotImplementedError
-        self.assertRaises(e, p.stat)
-        self.assertRaises(e, p.exists)
-        self.assertRaises(e, p.is_dir)
-        self.assertRaises(e, p.is_file)
-        self.assertRaises(e, p.is_symlink)
-        self.assertRaises(e, p.open)
-        self.assertRaises(e, p.read_bytes)
-        self.assertRaises(e, p.read_text)
-        self.assertRaises(e, p.write_bytes, b'foo')
-        self.assertRaises(e, p.write_text, 'foo')
-        self.assertRaises(e, p.iterdir)
-        self.assertRaises(e, lambda: list(p.glob('*')))
-        self.assertRaises(e, lambda: list(p.rglob('*')))
-        self.assertRaises(e, lambda: list(p.walk()))
-        self.assertRaises(e, p.readlink)
-        self.assertRaises(e, p.symlink_to, 'foo')
-        self.assertRaises(e, p.mkdir)
-
-    def test_fspath_common(self):
-        self.assertRaises(TypeError, os.fspath, self.cls(''))
-
-    def test_as_bytes_common(self):
-        self.assertRaises(TypeError, bytes, self.cls(''))
-
 
 class DummyPathIO(io.BytesIO):
     """
@@ -1342,11 +1304,19 @@ class DummyPath(PathBase):
     Simple implementation of PathBase that keeps files and directories in
     memory.
     """
-    __slots__ = ()
+    __slots__ = ('_segments')
 
     _files = {}
     _directories = {}
 
+    def __init__(self, *segments):
+        self._segments = segments
+
+    def __str__(self):
+        if self._segments:
+            return self.parser.join(*self._segments)
+        return ''
+
     def __eq__(self, other):
         if not isinstance(other, DummyPath):
             return NotImplemented
@@ -1358,6 +1328,9 @@ class DummyPath(PathBase):
     def __repr__(self):
         return "{}({!r})".format(self.__class__.__name__, self.as_posix())
 
+    def with_segments(self, *pathsegments):
+        return type(self)(*pathsegments)
+
     def stat(self, *, follow_symlinks=True):
         path = str(self).rstrip('/')
         if path in self._files: