Skip to content

Commit

Permalink
pythonGH-127456: pathlib ABCs: add protocol for path parser (python#1…
Browse files Browse the repository at this point in the history
…27494)

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]>
  • Loading branch information
barneygale and picnixz authored Dec 9, 2024
1 parent e85f2f1 commit 5c89adf
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 107 deletions.
56 changes: 2 additions & 54 deletions Lib/pathlib/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -115,7 +63,7 @@ class PurePathBase:
# the `__init__()` method.
'_raw_paths',
)
parser = ParserBase()
parser = posixpath
_globber = PathGlobber

def __init__(self, *args):
Expand Down
22 changes: 22 additions & 0 deletions Lib/pathlib/_types.py
Original file line number Diff line number Diff line change
@@ -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: ...
61 changes: 8 additions & 53 deletions Lib/test/test_pathlib/test_pathlib_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
#
Expand All @@ -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__'))
Expand All @@ -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):
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1411,7 +1367,6 @@ class DummyPath(PathBase):
memory.
"""
__slots__ = ()
parser = posixpath

_files = {}
_directories = {}
Expand Down

0 comments on commit 5c89adf

Please sign in to comment.