From 9b8d385f957b25f2629cf65797dac19235135f88 Mon Sep 17 00:00:00 2001 From: barneygale Date: Thu, 28 Dec 2023 00:51:37 +0000 Subject: [PATCH] GH-113528: Deoptimise `pathlib._abc.PurePathBase.relative_to()` 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. --- Lib/pathlib/__init__.py | 34 ++++++++++++++++++++++- Lib/pathlib/_abc.py | 24 ++++------------ Lib/test/test_pathlib/test_pathlib.py | 13 +++++++++ Lib/test/test_pathlib/test_pathlib_abc.py | 7 ----- 4 files changed, 52 insertions(+), 26 deletions(-) diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index ab87b49d0277f3..bcd79af878fc03 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -9,6 +9,7 @@ import ntpath import os import posixpath +import warnings try: import pwd @@ -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(): @@ -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)) diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index efe56ec565c162..3c857975764fe1 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -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 @@ -384,7 +383,7 @@ 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. @@ -392,13 +391,7 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): 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: @@ -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 @@ -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 " diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index db5f3b2634be97..967c1dfd38f1e2 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -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 diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index 568a3183b40b8d..e556fee4de842c 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -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')) @@ -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')))