Skip to content
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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-100502: Add pathlib.PurePath.pathmod attribute #106533

Merged
merged 7 commits into from Jul 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions Doc/library/pathlib.rst
Expand Up @@ -303,6 +303,13 @@ Methods and properties

Pure paths provide the following methods and properties:

.. attribute:: PurePath.pathmod

The implementation of the :mod:`os.path` module used for low-level path
operations: either ``posixpath`` or ``ntpath``.

.. versionadded:: 3.13

.. attribute:: PurePath.drive

A string representing the drive letter or name, if any::
Expand Down
84 changes: 42 additions & 42 deletions Lib/pathlib.py
Expand Up @@ -56,8 +56,8 @@ def _ignore_error(exception):


@functools.cache
def _is_case_sensitive(flavour):
return flavour.normcase('Aa') == 'Aa'
def _is_case_sensitive(pathmod):
return pathmod.normcase('Aa') == 'Aa'

#
# Globbing helpers
Expand Down Expand Up @@ -293,7 +293,7 @@ class PurePath:
# path. It's set when `__hash__()` is called for the first time.
'_hash',
)
_flavour = os.path
pathmod = os.path

def __new__(cls, *args, **kwargs):
"""Construct a PurePath from one or several strings and or existing
Expand All @@ -314,7 +314,7 @@ def __init__(self, *args):
paths = []
for arg in args:
if isinstance(arg, PurePath):
if arg._flavour is ntpath and self._flavour is posixpath:
if arg.pathmod is ntpath and self.pathmod is posixpath:
# GH-103631: Convert separators for backwards compatibility.
paths.extend(path.replace('\\', '/') for path in arg._raw_paths)
else:
Expand Down Expand Up @@ -343,11 +343,11 @@ def with_segments(self, *pathsegments):
def _parse_path(cls, path):
if not path:
return '', '', []
sep = cls._flavour.sep
altsep = cls._flavour.altsep
sep = cls.pathmod.sep
altsep = cls.pathmod.altsep
if altsep:
path = path.replace(altsep, sep)
drv, root, rel = cls._flavour.splitroot(path)
drv, root, rel = cls.pathmod.splitroot(path)
if not root and drv.startswith(sep) and not drv.endswith(sep):
drv_parts = drv.split(sep)
if len(drv_parts) == 4 and drv_parts[2] not in '?.':
Expand All @@ -366,7 +366,7 @@ def _load_parts(self):
elif len(paths) == 1:
path = paths[0]
else:
path = self._flavour.join(*paths)
path = self.pathmod.join(*paths)
drv, root, tail = self._parse_path(path)
self._drv = drv
self._root = root
Expand All @@ -384,10 +384,10 @@ def _from_parsed_parts(self, drv, root, tail):
@classmethod
def _format_parsed_parts(cls, drv, root, tail):
if drv or root:
return drv + root + cls._flavour.sep.join(tail)
elif tail and cls._flavour.splitdrive(tail[0])[0]:
return drv + root + cls.pathmod.sep.join(tail)
elif tail and cls.pathmod.splitdrive(tail[0])[0]:
tail = ['.'] + tail
return cls._flavour.sep.join(tail)
return cls.pathmod.sep.join(tail)

def __str__(self):
"""Return the string representation of the path, suitable for
Expand All @@ -405,8 +405,7 @@ def __fspath__(self):
def as_posix(self):
"""Return the string representation of the path with forward (/)
slashes."""
f = self._flavour
return str(self).replace(f.sep, '/')
return str(self).replace(self.pathmod.sep, '/')

def __bytes__(self):
"""Return the bytes representation of the path. This is only
Expand Down Expand Up @@ -442,7 +441,7 @@ def _str_normcase(self):
try:
return self._str_normcase_cached
except AttributeError:
if _is_case_sensitive(self._flavour):
if _is_case_sensitive(self.pathmod):
self._str_normcase_cached = str(self)
else:
self._str_normcase_cached = str(self).lower()
Expand All @@ -454,7 +453,7 @@ def _parts_normcase(self):
try:
return self._parts_normcase_cached
except AttributeError:
self._parts_normcase_cached = self._str_normcase.split(self._flavour.sep)
self._parts_normcase_cached = self._str_normcase.split(self.pathmod.sep)
return self._parts_normcase_cached

@property
Expand All @@ -467,14 +466,14 @@ def _lines(self):
if path_str == '.':
self._lines_cached = ''
else:
trans = _SWAP_SEP_AND_NEWLINE[self._flavour.sep]
trans = _SWAP_SEP_AND_NEWLINE[self.pathmod.sep]
self._lines_cached = path_str.translate(trans)
return self._lines_cached

def __eq__(self, other):
if not isinstance(other, PurePath):
return NotImplemented
return self._str_normcase == other._str_normcase and self._flavour is other._flavour
return self._str_normcase == other._str_normcase and self.pathmod is other.pathmod

def __hash__(self):
try:
Expand All @@ -484,22 +483,22 @@ def __hash__(self):
return self._hash

def __lt__(self, other):
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
if not isinstance(other, PurePath) or self.pathmod is not other.pathmod:
return NotImplemented
return self._parts_normcase < other._parts_normcase

def __le__(self, other):
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
if not isinstance(other, PurePath) or self.pathmod is not other.pathmod:
return NotImplemented
return self._parts_normcase <= other._parts_normcase

def __gt__(self, other):
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
if not isinstance(other, PurePath) or self.pathmod is not other.pathmod:
return NotImplemented
return self._parts_normcase > other._parts_normcase

def __ge__(self, other):
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
if not isinstance(other, PurePath) or self.pathmod is not other.pathmod:
return NotImplemented
return self._parts_normcase >= other._parts_normcase

Expand Down Expand Up @@ -584,9 +583,9 @@ def with_name(self, name):
"""Return a new path with the file name changed."""
if not self.name:
raise ValueError("%r has an empty name" % (self,))
f = self._flavour
drv, root, tail = f.splitroot(name)
if drv or root or not tail or f.sep in tail or (f.altsep and f.altsep in tail):
m = self.pathmod
drv, root, tail = m.splitroot(name)
if drv or root or not tail or m.sep in tail or (m.altsep and m.altsep in tail):
raise ValueError("Invalid name %r" % (name))
return self._from_parsed_parts(self.drive, self.root,
self._tail[:-1] + [name])
Expand All @@ -600,8 +599,8 @@ def with_suffix(self, suffix):
has no suffix, add given suffix. If the given suffix is an empty
string, remove the suffix from the path.
"""
f = self._flavour
if f.sep in suffix or f.altsep and f.altsep in suffix:
m = self.pathmod
if m.sep in suffix or m.altsep and m.altsep in suffix:
raise ValueError("Invalid suffix %r" % (suffix,))
if suffix and not suffix.startswith('.') or suffix == '.':
raise ValueError("Invalid suffix %r" % (suffix))
Expand Down Expand Up @@ -702,22 +701,22 @@ def parents(self):
def is_absolute(self):
"""True if the path is absolute (has both a root and, if applicable,
a drive)."""
if self._flavour is ntpath:
if self.pathmod is ntpath:
# ntpath.isabs() is defective - see GH-44626.
return bool(self.drive and self.root)
elif self._flavour is posixpath:
elif self.pathmod is posixpath:
# Optimization: work with raw paths on POSIX.
for path in self._raw_paths:
if path.startswith('/'):
return True
return False
else:
return self._flavour.isabs(str(self))
return self.pathmod.isabs(str(self))

def is_reserved(self):
"""Return True if the path contains one of the special names reserved
by the system, if any."""
if self._flavour is posixpath or not self._tail:
if self.pathmod is posixpath or not self._tail:
return False

# NOTE: the rules for reserved names seem somewhat complicated
Expand All @@ -737,7 +736,7 @@ def match(self, path_pattern, *, case_sensitive=None):
if not isinstance(path_pattern, PurePath):
path_pattern = self.with_segments(path_pattern)
if case_sensitive is None:
case_sensitive = _is_case_sensitive(self._flavour)
case_sensitive = _is_case_sensitive(self.pathmod)
pattern = _compile_pattern_lines(path_pattern._lines, case_sensitive)
if path_pattern.drive or path_pattern.root:
return pattern.match(self._lines) is not None
Expand All @@ -758,7 +757,7 @@ class PurePosixPath(PurePath):
On a POSIX system, instantiating a PurePath should return this object.
However, you can also instantiate it directly on any system.
"""
_flavour = posixpath
pathmod = posixpath
__slots__ = ()


Expand All @@ -768,7 +767,7 @@ class PureWindowsPath(PurePath):
On a Windows system, instantiating a PurePath should return this object.
However, you can also instantiate it directly on any system.
"""
_flavour = ntpath
pathmod = ntpath
__slots__ = ()


Expand Down Expand Up @@ -858,7 +857,7 @@ def is_mount(self):
"""
Check if this path is a mount point
"""
return self._flavour.ismount(self)
return os.path.ismount(self)

def is_symlink(self):
"""
Expand All @@ -879,7 +878,7 @@ def is_junction(self):
"""
Whether this path is a junction.
"""
return self._flavour.isjunction(self)
return os.path.isjunction(self)

def is_block_device(self):
"""
Expand Down Expand Up @@ -954,7 +953,8 @@ def samefile(self, other_path):
other_st = other_path.stat()
except AttributeError:
other_st = self.with_segments(other_path).stat()
return self._flavour.samestat(st, other_st)
return (st.st_ino == other_st.st_ino and
st.st_dev == other_st.st_dev)

def open(self, mode='r', buffering=-1, encoding=None,
errors=None, newline=None):
Expand Down Expand Up @@ -1017,7 +1017,7 @@ def _scandir(self):
return os.scandir(self)

def _make_child_relpath(self, name):
sep = self._flavour.sep
sep = self.pathmod.sep
lines_name = name.replace('\n', sep)
lines_str = self._lines
path_str = str(self)
Expand Down Expand Up @@ -1062,7 +1062,7 @@ def _glob(self, pattern, case_sensitive, follow_symlinks):
raise ValueError("Unacceptable pattern: {!r}".format(pattern))

pattern_parts = list(path_pattern._tail)
if pattern[-1] in (self._flavour.sep, self._flavour.altsep):
if pattern[-1] in (self.pathmod.sep, self.pathmod.altsep):
# GH-65238: pathlib doesn't preserve trailing slash. Add it back.
pattern_parts.append('')
if pattern_parts[-1] == '**':
Expand All @@ -1071,7 +1071,7 @@ def _glob(self, pattern, case_sensitive, follow_symlinks):

if case_sensitive is None:
# TODO: evaluate case-sensitivity of each directory in _select_children().
case_sensitive = _is_case_sensitive(self._flavour)
case_sensitive = _is_case_sensitive(self.pathmod)

# If symlinks are handled consistently, and the pattern does not
# contain '..' components, then we can use a 'walk-and-match' strategy
Expand Down Expand Up @@ -1204,7 +1204,7 @@ def absolute(self):
return self
elif self.drive:
# There is a CWD on each drive-letter drive.
cwd = self._flavour.abspath(self.drive)
cwd = os.path.abspath(self.drive)
else:
cwd = os.getcwd()
# Fast path for "empty" paths, e.g. Path("."), Path("") or Path().
Expand All @@ -1230,7 +1230,7 @@ def check_eloop(e):
raise RuntimeError("Symlink loop from %r" % e.filename)

try:
s = self._flavour.realpath(self, strict=strict)
s = os.path.realpath(self, strict=strict)
except OSError as e:
check_eloop(e)
raise
Expand Down Expand Up @@ -1394,7 +1394,7 @@ def expanduser(self):
"""
if (not (self.drive or self.root) and
self._tail and self._tail[0][:1] == '~'):
homedir = self._flavour.expanduser(self._tail[0])
homedir = os.path.expanduser(self._tail[0])
if homedir[:1] == "~":
raise RuntimeError("Could not determine home directory.")
drv, root, tail = self._parse_path(homedir)
Expand Down