Skip to content

Commit 3e83274

Browse files
committed
fea: return posixpath for relative_to
1 parent 1a32d12 commit 3e83274

File tree

7 files changed

+88
-36
lines changed

7 files changed

+88
-36
lines changed

upath/_compat.py

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -253,34 +253,9 @@ def with_suffix(self, suffix):
253253
self.drive, self.root, self._tail[:-1] + [name]
254254
)
255255

256-
def relative_to(self, other, /, *_deprecated, walk_up=False):
257-
if _deprecated:
258-
msg = (
259-
"support for supplying more than one positional argument "
260-
"to pathlib.PurePath.relative_to() is deprecated and "
261-
"scheduled for removal in Python 3.14"
262-
)
263-
warnings.warn(
264-
f"pathlib.PurePath.relative_to(*args) {msg}",
265-
DeprecationWarning,
266-
stacklevel=2,
267-
)
268-
other = self.with_segments(other, *_deprecated)
269-
for step, path in enumerate([other] + list(other.parents)): # noqa: B007
270-
if self.is_relative_to(path):
271-
break
272-
elif not walk_up:
273-
raise ValueError(
274-
f"{str(self)!r} is not in the subpath of {str(other)!r}"
275-
)
276-
elif path.name == "..":
277-
raise ValueError(f"'..' segment in {str(other)!r} cannot be walked")
278-
else:
279-
raise ValueError(
280-
f"{str(self)!r} and {str(other)!r} have different anchors"
281-
)
282-
parts = [".."] * step + self._tail[len(path._tail) :]
283-
return self.with_segments(*parts)
256+
# NOTE relative_to was elevated to UPath as otherwise this would
257+
# cause a circular dependency
258+
# def relative_to(self, other, /, *_deprecated, walk_up=False):
284259

285260
def is_relative_to(self, other, /, *_deprecated):
286261
if _deprecated:

upath/core.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,34 @@ def relative_to( # type: ignore[override]
721721
"paths have different storage_options:"
722722
f" {self.storage_options!r} != {other.storage_options!r}"
723723
)
724-
return super().relative_to(other, *_deprecated, walk_up=walk_up)
724+
725+
if _deprecated:
726+
msg = (
727+
"support for supplying more than one positional argument "
728+
"to pathlib.PurePath.relative_to() is deprecated and "
729+
"scheduled for removal in Python 3.14"
730+
)
731+
warnings.warn(
732+
f"pathlib.PurePath.relative_to(*args) {msg}",
733+
DeprecationWarning,
734+
stacklevel=2,
735+
)
736+
other = self.with_segments(other, *_deprecated)
737+
for step, path in enumerate([other] + list(other.parents)): # noqa: B007
738+
if self.is_relative_to(path):
739+
break
740+
elif not walk_up:
741+
raise ValueError(
742+
f"{str(self)!r} is not in the subpath of {str(other)!r}"
743+
)
744+
elif path.name == "..":
745+
raise ValueError(f"'..' segment in {str(other)!r} cannot be walked")
746+
else:
747+
raise ValueError(
748+
f"{str(self)!r} and {str(other)!r} have different anchors"
749+
)
750+
parts = [".."] * step + self._tail[len(path._tail) :]
751+
return UPath(*parts, **self._storage_options)
725752

726753
def is_relative_to(self, other, /, *_deprecated) -> bool: # type: ignore[override]
727754
if isinstance(other, UPath) and self.storage_options != other.storage_options:

upath/implementations/cloud.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,7 @@ def iterdir(self):
5757

5858
def relative_to(self, other, /, *_deprecated, walk_up=False):
5959
# use the parent implementation for the ValueError logic
60-
super().relative_to(other, *_deprecated, walk_up=False)
61-
return self
60+
return super().relative_to(other, *_deprecated, walk_up=walk_up)
6261

6362

6463
class GCSPath(CloudPath):

upath/implementations/local.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from typing import Collection
1212
from typing import MutableMapping
1313
from urllib.parse import SplitResult
14+
import warnings
1415

1516
from upath._protocol import compatible_protocol
1617
from upath.core import UPath

upath/tests/implementations/test_azure.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from upath import UPath
44
from upath.implementations.cloud import AzurePath
5+
from upath.implementations.local import PosixUPath
56

67
from ..cases import BaseTests
78
from ..utils import skip_on_windows
@@ -61,3 +62,21 @@ def test_broken_mkdir(self):
6162

6263
(path / "file").write_text("foo")
6364
assert path.exists()
65+
66+
def test_relative_to(self):
67+
rel_path = UPath("az:///test_bucket/file.txt").relative_to(UPath("az:///test_bucket"))
68+
assert isinstance(rel_path, PosixUPath)
69+
assert not rel_path.is_absolute()
70+
assert 'file.txt' == rel_path.path
71+
72+
walk_path = UPath("az:///test_bucket/file.txt").relative_to(UPath("az:///other_test_bucket"), walk_up=True)
73+
assert isinstance(walk_path, PosixUPath)
74+
assert not walk_path.is_absolute()
75+
assert '../test_bucket/file.txt' == walk_path.path
76+
77+
with pytest.raises(ValueError):
78+
UPath("az:///test_bucket/file.txt").relative_to(UPath("az:///prod_bucket"))
79+
80+
with pytest.raises(ValueError):
81+
UPath("az:///test_bucket/file.txt").relative_to(UPath("file:///test_bucket"))
82+

upath/tests/implementations/test_local.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import pytest
22

33
from upath import UPath
4-
from upath.implementations.local import LocalPath
4+
from upath.implementations.local import LocalPath, PosixUPath
55
from upath.tests.cases import BaseTests
66
from upath.tests.utils import xfail_if_version
77

@@ -15,6 +15,24 @@ def path(self, local_testdir):
1515
def test_is_LocalPath(self):
1616
assert isinstance(self.path, LocalPath)
1717

18+
def test_relative_to(self):
19+
rel_path = UPath("file:///test_bucket/file.txt").relative_to(UPath("file:///test_bucket"))
20+
assert isinstance(rel_path, PosixUPath)
21+
assert not rel_path.is_absolute()
22+
assert 'file.txt' == rel_path.path
23+
24+
walk_path = UPath("file:///test_bucket/file.txt").relative_to(UPath("file:///other_test_bucket"), walk_up=True)
25+
assert isinstance(walk_path, PosixUPath)
26+
assert not walk_path.is_absolute()
27+
assert '../test_bucket/file.txt' == walk_path.path
28+
29+
with pytest.raises(ValueError):
30+
UPath("file:///test_bucket/file.txt").relative_to(UPath("file:///prod_bucket"))
31+
32+
with pytest.raises(ValueError):
33+
UPath("file:///test_bucket/file.txt").relative_to(UPath("s3:///test_bucket"))
34+
35+
1836

1937
@xfail_if_version("fsspec", lt="2023.10.0", reason="requires fsspec>=2023.10.0")
2038
class TestRayIOFSSpecLocal(BaseTests):

upath/tests/implementations/test_s3.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
import fsspec
77
import pytest # noqa: F401
88

9-
from upath import UPath
9+
from upath.core import UPath
10+
from upath.implementations.local import PosixUPath
1011
from upath.implementations.cloud import S3Path
1112

1213
from ..cases import BaseTests
@@ -53,9 +54,21 @@ def test_rmdir(self):
5354
self.path.joinpath("file1.txt").rmdir()
5455

5556
def test_relative_to(self):
56-
assert "s3://test_bucket/file.txt" == str(
57-
UPath("s3://test_bucket/file.txt").relative_to(UPath("s3://test_bucket"))
58-
)
57+
rel_path = UPath("s3:///test_bucket/file.txt").relative_to(UPath("s3:///test_bucket"))
58+
assert isinstance(rel_path, PosixUPath)
59+
assert not rel_path.is_absolute()
60+
assert 'file.txt' == rel_path.path
61+
62+
walk_path = UPath("s3:///test_bucket/file.txt").relative_to(UPath("s3:///other_test_bucket"), walk_up=True)
63+
assert isinstance(walk_path, PosixUPath)
64+
assert not walk_path.is_absolute()
65+
assert '../test_bucket/file.txt' == walk_path.path
66+
67+
with pytest.raises(ValueError):
68+
UPath("s3:///test_bucket/file.txt").relative_to(UPath("s3:///prod_bucket"))
69+
70+
with pytest.raises(ValueError):
71+
UPath("s3:///test_bucket/file.txt").relative_to(UPath("file:///test_bucket"))
5972

6073
def test_iterdir_root(self):
6174
client_kwargs = self.path.storage_options["client_kwargs"]

0 commit comments

Comments
 (0)