Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Dec 9, 2024
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
Loading