Skip to content

Commit 5c89adf

Browse files
barneygalepicnixz
andauthored
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]>
1 parent e85f2f1 commit 5c89adf

File tree

3 files changed

+32
-107
lines changed

3 files changed

+32
-107
lines changed

Lib/pathlib/_abc.py

+2-54
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import functools
1515
import operator
16+
import posixpath
1617
from errno import EINVAL
1718
from glob import _GlobberBase, _no_recurse_symlinks
1819
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):
3334
return parser.normcase('Aa') == 'Aa'
3435

3536

36-
37-
class ParserBase:
38-
"""Base class for path parsers, which do low-level path manipulation.
39-
40-
Path parsers provide a subset of the os.path API, specifically those
41-
functions needed to provide PurePathBase functionality. Each PurePathBase
42-
subclass references its path parser via a 'parser' class attribute.
43-
44-
Every method in this base class raises an UnsupportedOperation exception.
45-
"""
46-
47-
@classmethod
48-
def _unsupported_msg(cls, attribute):
49-
return f"{cls.__name__}.{attribute} is unsupported"
50-
51-
@property
52-
def sep(self):
53-
"""The character used to separate path components."""
54-
raise UnsupportedOperation(self._unsupported_msg('sep'))
55-
56-
def join(self, path, *paths):
57-
"""Join path segments."""
58-
raise UnsupportedOperation(self._unsupported_msg('join()'))
59-
60-
def split(self, path):
61-
"""Split the path into a pair (head, tail), where *head* is everything
62-
before the final path separator, and *tail* is everything after.
63-
Either part may be empty.
64-
"""
65-
raise UnsupportedOperation(self._unsupported_msg('split()'))
66-
67-
def splitdrive(self, path):
68-
"""Split the path into a 2-item tuple (drive, tail), where *drive* is
69-
a device name or mount point, and *tail* is everything after the
70-
drive. Either part may be empty."""
71-
raise UnsupportedOperation(self._unsupported_msg('splitdrive()'))
72-
73-
def splitext(self, path):
74-
"""Split the path into a pair (root, ext), where *ext* is empty or
75-
begins with a period and contains at most one period,
76-
and *root* is everything before the extension."""
77-
raise UnsupportedOperation(self._unsupported_msg('splitext()'))
78-
79-
def normcase(self, path):
80-
"""Normalize the case of the path."""
81-
raise UnsupportedOperation(self._unsupported_msg('normcase()'))
82-
83-
def isabs(self, path):
84-
"""Returns whether the path is absolute, i.e. unaffected by the
85-
current directory or drive."""
86-
raise UnsupportedOperation(self._unsupported_msg('isabs()'))
87-
88-
8937
class PathGlobber(_GlobberBase):
9038
"""
9139
Class providing shell-style globbing for path objects.
@@ -115,7 +63,7 @@ class PurePathBase:
11563
# the `__init__()` method.
11664
'_raw_paths',
11765
)
118-
parser = ParserBase()
66+
parser = posixpath
11967
_globber = PathGlobber
12068

12169
def __init__(self, *args):

Lib/pathlib/_types.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"""
2+
Protocols for supporting classes in pathlib.
3+
"""
4+
from typing import Protocol, runtime_checkable
5+
6+
7+
@runtime_checkable
8+
class Parser(Protocol):
9+
"""Protocol for path parsers, which do low-level path manipulation.
10+
11+
Path parsers provide a subset of the os.path API, specifically those
12+
functions needed to provide PurePathBase functionality. Each PurePathBase
13+
subclass references its path parser via a 'parser' class attribute.
14+
"""
15+
16+
sep: str
17+
def join(self, path: str, *paths: str) -> str: ...
18+
def split(self, path: str) -> tuple[str, str]: ...
19+
def splitdrive(self, path: str) -> tuple[str, str]: ...
20+
def splitext(self, path: str) -> tuple[str, str]: ...
21+
def normcase(self, path: str) -> str: ...
22+
def isabs(self, path: str) -> bool: ...

Lib/test/test_pathlib/test_pathlib_abc.py

+8-53
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import stat
66
import unittest
77

8-
from pathlib._abc import UnsupportedOperation, ParserBase, PurePathBase, PathBase
8+
from pathlib._abc import UnsupportedOperation, PurePathBase, PathBase
9+
from pathlib._types import Parser
910
import posixpath
1011

1112
from test.support.os_helper import TESTFN
@@ -31,22 +32,6 @@ def test_is_notimplemented(self):
3132
self.assertTrue(issubclass(UnsupportedOperation, NotImplementedError))
3233
self.assertTrue(isinstance(UnsupportedOperation(), NotImplementedError))
3334

34-
35-
class ParserBaseTest(unittest.TestCase):
36-
cls = ParserBase
37-
38-
def test_unsupported_operation(self):
39-
m = self.cls()
40-
e = UnsupportedOperation
41-
with self.assertRaises(e):
42-
m.sep
43-
self.assertRaises(e, m.join, 'foo')
44-
self.assertRaises(e, m.split, 'foo')
45-
self.assertRaises(e, m.splitdrive, 'foo')
46-
self.assertRaises(e, m.splitext, 'foo')
47-
self.assertRaises(e, m.normcase, 'foo')
48-
self.assertRaises(e, m.isabs, 'foo')
49-
5035
#
5136
# Tests for the pure classes.
5237
#
@@ -55,37 +40,6 @@ def test_unsupported_operation(self):
5540
class PurePathBaseTest(unittest.TestCase):
5641
cls = PurePathBase
5742

58-
def test_unsupported_operation_pure(self):
59-
p = self.cls('foo')
60-
e = UnsupportedOperation
61-
with self.assertRaises(e):
62-
p.drive
63-
with self.assertRaises(e):
64-
p.root
65-
with self.assertRaises(e):
66-
p.anchor
67-
with self.assertRaises(e):
68-
p.parts
69-
with self.assertRaises(e):
70-
p.parent
71-
with self.assertRaises(e):
72-
p.parents
73-
with self.assertRaises(e):
74-
p.name
75-
with self.assertRaises(e):
76-
p.stem
77-
with self.assertRaises(e):
78-
p.suffix
79-
with self.assertRaises(e):
80-
p.suffixes
81-
self.assertRaises(e, p.with_name, 'bar')
82-
self.assertRaises(e, p.with_stem, 'bar')
83-
self.assertRaises(e, p.with_suffix, '.txt')
84-
self.assertRaises(e, p.relative_to, '')
85-
self.assertRaises(e, p.is_relative_to, '')
86-
self.assertRaises(e, p.is_absolute)
87-
self.assertRaises(e, p.match, '*')
88-
8943
def test_magic_methods(self):
9044
P = self.cls
9145
self.assertFalse(hasattr(P, '__fspath__'))
@@ -100,12 +54,11 @@ def test_magic_methods(self):
10054
self.assertIs(P.__ge__, object.__ge__)
10155

10256
def test_parser(self):
103-
self.assertIsInstance(self.cls.parser, ParserBase)
57+
self.assertIs(self.cls.parser, posixpath)
10458

10559

10660
class DummyPurePath(PurePathBase):
10761
__slots__ = ()
108-
parser = posixpath
10962

11063
def __eq__(self, other):
11164
if not isinstance(other, DummyPurePath):
@@ -136,6 +89,9 @@ def setUp(self):
13689
self.sep = self.parser.sep
13790
self.altsep = self.parser.altsep
13891

92+
def test_parser(self):
93+
self.assertIsInstance(self.cls.parser, Parser)
94+
13995
def test_constructor_common(self):
14096
P = self.cls
14197
p = P('a')
@@ -1359,8 +1315,8 @@ def test_unsupported_operation(self):
13591315
self.assertRaises(e, p.write_bytes, b'foo')
13601316
self.assertRaises(e, p.write_text, 'foo')
13611317
self.assertRaises(e, p.iterdir)
1362-
self.assertRaises(e, p.glob, '*')
1363-
self.assertRaises(e, p.rglob, '*')
1318+
self.assertRaises(e, lambda: list(p.glob('*')))
1319+
self.assertRaises(e, lambda: list(p.rglob('*')))
13641320
self.assertRaises(e, lambda: list(p.walk()))
13651321
self.assertRaises(e, p.expanduser)
13661322
self.assertRaises(e, p.readlink)
@@ -1411,7 +1367,6 @@ class DummyPath(PathBase):
14111367
memory.
14121368
"""
14131369
__slots__ = ()
1414-
parser = posixpath
14151370

14161371
_files = {}
14171372
_directories = {}

0 commit comments

Comments
 (0)