]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-127456: pathlib ABCs: add protocol for path parser (#127494)
authorBarney Gale <barney.gale@gmail.com>
Mon, 9 Dec 2024 18:31:22 +0000 (18:31 +0000)
committerGitHub <noreply@github.com>
Mon, 9 Dec 2024 18:31:22 +0000 (18:31 +0000)
Change the default value of `PurePathBase.parser` from `ParserBase()` to
`posixpath`. As a result, user subclasses of `PurePathBase` and `PathBase`
use POSIX path syntax by default, which is very often desirable.

Move `pathlib._abc.ParserBase` to `pathlib._types.Parser`, and convert it
to a runtime-checkable protocol.

Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
Lib/pathlib/_abc.py
Lib/pathlib/_types.py [new file with mode: 0644]
Lib/test/test_pathlib/test_pathlib_abc.py

index 309eab2ff855c3b391d5cf52070728f260b94141..f68685f21d6d79aedccd6fb8389b3db85526daa1 100644 (file)
@@ -13,6 +13,7 @@ resemble pathlib's PurePath and Path respectively.
 
 import functools
 import operator
+import posixpath
 from errno import EINVAL
 from glob import _GlobberBase, _no_recurse_symlinks
 from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
@@ -33,59 +34,6 @@ def _is_case_sensitive(parser):
     return parser.normcase('Aa') == 'Aa'
 
 
-
-class ParserBase:
-    """Base class for path parsers, which do low-level path manipulation.
-
-    Path parsers provide a subset of the os.path API, specifically those
-    functions needed to provide PurePathBase functionality. Each PurePathBase
-    subclass references its path parser via a 'parser' class attribute.
-
-    Every method in this base class raises an UnsupportedOperation exception.
-    """
-
-    @classmethod
-    def _unsupported_msg(cls, attribute):
-        return f"{cls.__name__}.{attribute} is unsupported"
-
-    @property
-    def sep(self):
-        """The character used to separate path components."""
-        raise UnsupportedOperation(self._unsupported_msg('sep'))
-
-    def join(self, path, *paths):
-        """Join path segments."""
-        raise UnsupportedOperation(self._unsupported_msg('join()'))
-
-    def split(self, path):
-        """Split the path into a pair (head, tail), where *head* is everything
-        before the final path separator, and *tail* is everything after.
-        Either part may be empty.
-        """
-        raise UnsupportedOperation(self._unsupported_msg('split()'))
-
-    def splitdrive(self, path):
-        """Split the path into a 2-item tuple (drive, tail), where *drive* is
-        a device name or mount point, and *tail* is everything after the
-        drive. Either part may be empty."""
-        raise UnsupportedOperation(self._unsupported_msg('splitdrive()'))
-
-    def splitext(self, path):
-        """Split the path into a pair (root, ext), where *ext* is empty or
-        begins with a period and contains at most one period,
-        and *root* is everything before the extension."""
-        raise UnsupportedOperation(self._unsupported_msg('splitext()'))
-
-    def normcase(self, path):
-        """Normalize the case of the path."""
-        raise UnsupportedOperation(self._unsupported_msg('normcase()'))
-
-    def isabs(self, path):
-        """Returns whether the path is absolute, i.e. unaffected by the
-        current directory or drive."""
-        raise UnsupportedOperation(self._unsupported_msg('isabs()'))
-
-
 class PathGlobber(_GlobberBase):
     """
     Class providing shell-style globbing for path objects.
@@ -115,7 +63,7 @@ class PurePathBase:
         # the `__init__()` method.
         '_raw_paths',
     )
-    parser = ParserBase()
+    parser = posixpath
     _globber = PathGlobber
 
     def __init__(self, *args):
diff --git a/Lib/pathlib/_types.py b/Lib/pathlib/_types.py
new file mode 100644 (file)
index 0000000..60df94d
--- /dev/null
@@ -0,0 +1,22 @@
+"""
+Protocols for supporting classes in pathlib.
+"""
+from typing import Protocol, runtime_checkable
+
+
+@runtime_checkable
+class Parser(Protocol):
+    """Protocol for path parsers, which do low-level path manipulation.
+
+    Path parsers provide a subset of the os.path API, specifically those
+    functions needed to provide PurePathBase functionality. Each PurePathBase
+    subclass references its path parser via a 'parser' class attribute.
+    """
+
+    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]: ...
+    def normcase(self, path: str) -> str: ...
+    def isabs(self, path: str) -> bool: ...
index 675abf30a9f13cc1adc07196f693de2cd5fc364c..dd9425ce39362399925975cc473b54daee2878d7 100644 (file)
@@ -5,7 +5,8 @@ import errno
 import stat
 import unittest
 
-from pathlib._abc import UnsupportedOperation, ParserBase, PurePathBase, PathBase
+from pathlib._abc import UnsupportedOperation, PurePathBase, PathBase
+from pathlib._types import Parser
 import posixpath
 
 from test.support.os_helper import TESTFN
@@ -31,22 +32,6 @@ class UnsupportedOperationTest(unittest.TestCase):
         self.assertTrue(issubclass(UnsupportedOperation, NotImplementedError))
         self.assertTrue(isinstance(UnsupportedOperation(), NotImplementedError))
 
-
-class ParserBaseTest(unittest.TestCase):
-    cls = ParserBase
-
-    def test_unsupported_operation(self):
-        m = self.cls()
-        e = UnsupportedOperation
-        with self.assertRaises(e):
-            m.sep
-        self.assertRaises(e, m.join, 'foo')
-        self.assertRaises(e, m.split, 'foo')
-        self.assertRaises(e, m.splitdrive, 'foo')
-        self.assertRaises(e, m.splitext, 'foo')
-        self.assertRaises(e, m.normcase, 'foo')
-        self.assertRaises(e, m.isabs, 'foo')
-
 #
 # Tests for the pure classes.
 #
@@ -55,37 +40,6 @@ class ParserBaseTest(unittest.TestCase):
 class PurePathBaseTest(unittest.TestCase):
     cls = PurePathBase
 
-    def test_unsupported_operation_pure(self):
-        p = self.cls('foo')
-        e = UnsupportedOperation
-        with self.assertRaises(e):
-            p.drive
-        with self.assertRaises(e):
-            p.root
-        with self.assertRaises(e):
-            p.anchor
-        with self.assertRaises(e):
-            p.parts
-        with self.assertRaises(e):
-            p.parent
-        with self.assertRaises(e):
-            p.parents
-        with self.assertRaises(e):
-            p.name
-        with self.assertRaises(e):
-            p.stem
-        with self.assertRaises(e):
-            p.suffix
-        with self.assertRaises(e):
-            p.suffixes
-        self.assertRaises(e, p.with_name, 'bar')
-        self.assertRaises(e, p.with_stem, 'bar')
-        self.assertRaises(e, p.with_suffix, '.txt')
-        self.assertRaises(e, p.relative_to, '')
-        self.assertRaises(e, p.is_relative_to, '')
-        self.assertRaises(e, p.is_absolute)
-        self.assertRaises(e, p.match, '*')
-
     def test_magic_methods(self):
         P = self.cls
         self.assertFalse(hasattr(P, '__fspath__'))
@@ -100,12 +54,11 @@ class PurePathBaseTest(unittest.TestCase):
         self.assertIs(P.__ge__, object.__ge__)
 
     def test_parser(self):
-        self.assertIsInstance(self.cls.parser, ParserBase)
+        self.assertIs(self.cls.parser, posixpath)
 
 
 class DummyPurePath(PurePathBase):
     __slots__ = ()
-    parser = posixpath
 
     def __eq__(self, other):
         if not isinstance(other, DummyPurePath):
@@ -136,6 +89,9 @@ class DummyPurePathTest(unittest.TestCase):
         self.sep = self.parser.sep
         self.altsep = self.parser.altsep
 
+    def test_parser(self):
+        self.assertIsInstance(self.cls.parser, Parser)
+
     def test_constructor_common(self):
         P = self.cls
         p = P('a')
@@ -1359,8 +1315,8 @@ class PathBaseTest(PurePathBaseTest):
         self.assertRaises(e, p.write_bytes, b'foo')
         self.assertRaises(e, p.write_text, 'foo')
         self.assertRaises(e, p.iterdir)
-        self.assertRaises(e, p.glob, '*')
-        self.assertRaises(e, p.rglob, '*')
+        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.expanduser)
         self.assertRaises(e, p.readlink)
@@ -1411,7 +1367,6 @@ class DummyPath(PathBase):
     memory.
     """
     __slots__ = ()
-    parser = posixpath
 
     _files = {}
     _directories = {}