https://github.com/python/cpython/commit/5c89adf385aaaca97c2ee9074f8b1fda0f57ad26
commit: 5c89adf385aaaca97c2ee9074f8b1fda0f57ad26
branch: main
author: Barney Gale <[email protected]>
committer: barneygale <[email protected]>
date: 2024-12-09T18:31:22Z
summary:

GH-127456: pathlib ABCs: add protocol for path parser (#127494)

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 <[email protected]>

files:
A Lib/pathlib/_types.py
M Lib/pathlib/_abc.py
M Lib/test/test_pathlib/test_pathlib_abc.py

diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py
index 309eab2ff855c3..f68685f21d6d79 100644
--- a/Lib/pathlib/_abc.py
+++ b/Lib/pathlib/_abc.py
@@ -13,6 +13,7 @@
 
 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
index 00000000000000..60df94d0b46049
--- /dev/null
+++ b/Lib/pathlib/_types.py
@@ -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: ...
diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py 
b/Lib/test/test_pathlib/test_pathlib_abc.py
index 675abf30a9f13c..dd9425ce393623 100644
--- a/Lib/test/test_pathlib/test_pathlib_abc.py
+++ b/Lib/test/test_pathlib/test_pathlib_abc.py
@@ -5,7 +5,8 @@
 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 @@ def test_is_notimplemented(self):
         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 @@ def test_unsupported_operation(self):
 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 @@ def test_magic_methods(self):
         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 @@ def setUp(self):
         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 @@ def test_unsupported_operation(self):
         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 = {}

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to