8000 GH-127456: pathlib ABCs: add protocol for path parser by barneygale · Pull Request #127494 · python/cpython · GitHub
[go: up one dir, main page]

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
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 2 additions & 55 deletions Lib/pathlib/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,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 @@ -121,7 +68,7 @@ class PurePathBase:
# work from occurring when `resolve()` calls `stat()` or `readlink()`.
'_resolving',
)
parser = ParserBase()
parser = posixpath
_globber = PathGlobber

def __init__(self, *args):
Expand Down Expand Up @@ -633,7 +580,7 @@ def write_text(self, data, encoding=None, errors=None, newline=None):
return f.write(data)

def scandir(self):
"""Yield os.DirEntry objects of the directory contents.
"""Yield DirEntry objects of the directory contents.

The children are yielded in arbitrary order, and the
special entries '.' and '..' are not included.
Expand Down
2 changes: 1 addition & 1 deletion Lib/pathlib/_local.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ def _filter_trailing_slash(self, paths):
yield path_str

def scandir(self):
"""Yield os.DirEntry objects of the directory contents.
"""Yield DirEntry objects of the directory contents.

The children are yielded in arbitrary order, and the
special entries '.' and '..' are not included.
Expand Down
56 changes: 56 additions & 0 deletions Lib/pathlib/_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
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: ...


@runtime_checkable
class DirEntry(Protocol):
"""Protocol for directory entries, which store information about directory
children.

Directory entries provide a subset of the os.DirEntry API, specifically
those methods and attributes needed to provide PathBase functionality
like glob() and walk(). Directory entry objects are generated by
PathBase.scandir().

Path and PathBase implement this protocol, but their is_*() methods always
fetch up-to-date information.
"""

name: str
def is_file(self, *, follow_symlinks: bool = True) -> bool: ...
def is_dir(self, *, follow_symlinks: bool = True) -> bool: ...
def is_symlink(self) -> bool: ...


@runtime_checkable
class StatResult(Protocol):
"""Protocol for stat results, which store low-level information about
files.

Stat results provide a subset of the os.stat_result API, specifically
attributes for the file type, permissions and offset.
"""

st_mode: int
st_ino: int
st_dev: int
73 changes: 17 additions & 56 deletions Lib/test/test_pathlib/test_pathlib_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,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, DirEntry, StatResult
import posixpath

from test.support import is_wasi
Expand Down Expand Up @@ -39,22 +40,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 @@ -63,37 +48,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 @@ -108,12 +62,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 @@ -144,6 +97,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 @@ -1367,10 +1323,9 @@ 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.absolute)
self.assertRaises(e, p.expanduser)
self.assertRaises(e, p.readlink)
self.assertRaises(e, p.symlink_to, 'foo')
Expand Down Expand Up @@ -1432,8 +1387,11 @@ def __init__(self, name, is_symlink, is_dir):
def is_symlink(self):
return self._is_symlink

def is_file(self, *, follow_symlinks=True):
return (follow_symlinks or not self._is_symlink) and not self._is_dir

def is_dir(self, *, follow_symlinks=True):
return self._is_dir and (follow_symlinks or not self._is_symlink)
return (follow_symlinks or not self._is_symlink) and self._is_dir


class DummyPath(PathBase):
Expand All @@ -1442,7 +1400,6 @@ class DummyPath(PathBase):
memory.
"""
__slots__ = ()
parser = posixpath

_files = {}
_directories = {}
Expand Down Expand Up @@ -2219,7 +2176,7 @@ def test_scandir(self):
with p.scandir() as entries:
for entry in entries:
child = p / entry.name
self.assertIsNotNone(entry)
self.assertIsInstance(entry, DirEntry)
self.assertEqual(entry.name, child.name)
self.assertEqual(entry.is_symlink(),
child.is_symlink())
Expand Down Expand Up @@ -2641,6 +2598,10 @@ def test_stat(self):
statA = self.cls(self.base).joinpath('fileA').stat()
statB = self.cls(self.base).joinpath('dirB', 'fileB').stat()
statC = self.cls(self.base).joinpath('dirC').stat()
# all instances of StatResult
self.assertIsInstance(statA, StatResult)
self.assertIsInstance(statB, StatResult)
self.assertIsInstance(statC, StatResult)
# st_mode: files are the same, directory differs.
self.assertIsInstance(statA.st_mode, int)
self.assertEqual(statA.st_mode, statB.st_mode)
Expand Down
Loading
0