Skip to content

Commit

Permalink
pythonGH-113528: Deoptimise pathlib._abc.PurePathBase.relative_to()
Browse files Browse the repository at this point in the history
Replace use of `_from_parsed_parts()` with `with_segments()` in
`PurePathBase.relative_to()`, and move the assignment of `_drv`, `_root`,
`_tail_cached` and `_str` slots into `PurePath.relative_to()`.

Also shuffle a deprecation warning into `PurePath.relative_to()` to
ensure it's raised at the right stack level, and do the same in
`is_relative_to()` for code consistency.
  • Loading branch information
barneygale committed Dec 28, 2023
1 parent 1b19d73 commit 9b8d385
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 26 deletions.
34 changes: 33 additions & 1 deletion Lib/pathlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import ntpath
import os
import posixpath
import warnings

try:
import pwd
Expand Down Expand Up @@ -164,6 +165,38 @@ def __ge__(self, other):
return NotImplemented
return self._parts_normcase >= other._parts_normcase

def relative_to(self, other, /, *_deprecated, walk_up=False):
"""Return the relative path to another path identified by the passed
arguments. If the operation is not possible (because this is not
related to the other path), raise ValueError.
The *walk_up* parameter controls whether `..` may be used to resolve
the path.
"""
if _deprecated:
msg = ("support for supplying more than one positional argument "
"to pathlib.PurePath.relative_to() is deprecated and "
"scheduled for removal in Python 3.14")
warnings.warn(msg, DeprecationWarning, stacklevel=2)
other = self.with_segments(other, *_deprecated)
path = _abc.PurePathBase.relative_to(self, other, walk_up=walk_up)
tail = path._raw_paths.copy()
path._str = self._format_parsed_parts('', '', tail) or '.'
path._drv = path._root = ''
path._tail_cached = tail
return path

def is_relative_to(self, other, /, *_deprecated):
"""Return True if the path is relative to another path or False.
"""
if _deprecated:
msg = ("support for supplying more than one argument to "
"pathlib.PurePath.is_relative_to() is deprecated and "
"scheduled for removal in Python 3.14")
warnings.warn(msg, DeprecationWarning, stacklevel=2)
other = self.with_segments(other, *_deprecated)
return _abc.PurePathBase.is_relative_to(self, other)

def as_uri(self):
"""Return the path as a URI."""
if not self.is_absolute():
Expand Down Expand Up @@ -230,7 +263,6 @@ def _unsupported(cls, method_name):

def __init__(self, *args, **kwargs):
if kwargs:
import warnings
msg = ("support for supplying keyword arguments to pathlib.PurePath "
"is deprecated and scheduled for removal in Python {remove}")
warnings._deprecated("pathlib.PurePath(**kwargs)", msg, remove=(3, 14))
Expand Down
24 changes: 6 additions & 18 deletions Lib/pathlib/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import ntpath
import posixpath
import sys
import warnings
from _collections_abc import Sequence
from errno import ENOENT, ENOTDIR, EBADF, ELOOP, EINVAL
from itertools import chain
Expand Down Expand Up @@ -384,21 +383,15 @@ def with_suffix(self, suffix):
else:
raise ValueError(f"Invalid suffix {suffix!r}")

def relative_to(self, other, /, *_deprecated, walk_up=False):
def relative_to(self, other, *, walk_up=False):
"""Return the relative path to another path identified by the passed
arguments. If the operation is not possible (because this is not
related to the other path), raise ValueError.
The *walk_up* parameter controls whether `..` may be used to resolve
the path.
"""
if _deprecated:
msg = ("support for supplying more than one positional argument "
"to pathlib.PurePath.relative_to() is deprecated and "
"scheduled for removal in Python 3.14")
warnings.warn(msg, DeprecationWarning, stacklevel=2)
other = self.with_segments(other, *_deprecated)
elif not isinstance(other, PurePathBase):
if not isinstance(other, PurePathBase):
other = self.with_segments(other)
for step, path in enumerate(chain([other], other.parents)):
if path == self or path in self.parents:
Expand All @@ -410,18 +403,12 @@ def relative_to(self, other, /, *_deprecated, walk_up=False):
else:
raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors")
parts = ['..'] * step + self._tail[len(path._tail):]
return self._from_parsed_parts('', '', parts)
return self.with_segments(*parts)

def is_relative_to(self, other, /, *_deprecated):
def is_relative_to(self, other):
"""Return True if the path is relative to another path or False.
"""
if _deprecated:
msg = ("support for supplying more than one argument to "
"pathlib.PurePath.is_relative_to() is deprecated and "
"scheduled for removal in Python 3.14")
warnings.warn(msg, DeprecationWarning, stacklevel=2)
other = self.with_segments(other, *_deprecated)
elif not isinstance(other, PurePathBase):
if not isinstance(other, PurePathBase):
other = self.with_segments(other)
return other == self or other in self.parents

Expand Down Expand Up @@ -832,6 +819,7 @@ def _glob(self, pattern, case_sensitive, follow_symlinks):
pattern_parts.append('')
if pattern_parts[-1] == '**':
# GH-70303: '**' only matches directories. Add trailing slash.
import warnings
warnings.warn(
"Pattern ending '**' will match files and directories in a "
"future Python release. Add a trailing slash to match only "
Expand Down
13 changes: 13 additions & 0 deletions Lib/test/test_pathlib/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,19 @@ def test_repr_roundtrips(self):
self.assertEqual(q, p)
self.assertEqual(repr(q), r)

def test_relative_to_several_args(self):
P = self.cls
p = P('a/b')
with self.assertWarns(DeprecationWarning):
p.relative_to('a', 'b')
p.relative_to('a', 'b', walk_up=True)

def test_is_relative_to_several_args(self):
P = self.cls
p = P('a/b')
with self.assertWarns(DeprecationWarning):
p.is_relative_to('a', 'b')


class PurePosixPathTest(PurePathTest):
cls = pathlib.PurePosixPath
Expand Down
7 changes: 0 additions & 7 deletions Lib/test/test_pathlib/test_pathlib_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,10 +543,6 @@ def test_relative_to_common(self):
self.assertEqual(p.relative_to('a/b/c', walk_up=True), P('..'))
self.assertEqual(p.relative_to(P('c'), walk_up=True), P('../a/b'))
self.assertEqual(p.relative_to('c', walk_up=True), P('../a/b'))
# With several args.
with self.assertWarns(DeprecationWarning):
p.relative_to('a', 'b')
p.relative_to('a', 'b', walk_up=True)
# Unrelated paths.
self.assertRaises(ValueError, p.relative_to, P('c'))
self.assertRaises(ValueError, p.relative_to, P('a/b/c'))
Expand Down Expand Up @@ -608,9 +604,6 @@ def test_is_relative_to_common(self):
self.assertTrue(p.is_relative_to('a/'))
self.assertTrue(p.is_relative_to(P('a/b')))
self.assertTrue(p.is_relative_to('a/b'))
# With several args.
with self.assertWarns(DeprecationWarning):
p.is_relative_to('a', 'b')
# Unrelated paths.
self.assertFalse(p.is_relative_to(P('c')))
self.assertFalse(p.is_relative_to(P('a/b/c')))
Expand Down

0 comments on commit 9b8d385

Please sign in to comment.