diff --git a/upath/_compat.py b/upath/_compat.py index 334888f3..ff9f9e7a 100644 --- a/upath/_compat.py +++ b/upath/_compat.py @@ -253,34 +253,9 @@ def with_suffix(self, suffix): self.drive, self.root, self._tail[:-1] + [name] ) - def relative_to(self, other, /, *_deprecated, walk_up=False): - 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( - f"pathlib.PurePath.relative_to(*args) {msg}", - DeprecationWarning, - stacklevel=2, - ) - other = self.with_segments(other, *_deprecated) - for step, path in enumerate([other] + list(other.parents)): # noqa: B007 - if self.is_relative_to(path): - break - elif not walk_up: - raise ValueError( - f"{str(self)!r} is not in the subpath of {str(other)!r}" - ) - elif path.name == "..": - raise ValueError(f"'..' segment in {str(other)!r} cannot be walked") - else: - raise ValueError( - f"{str(self)!r} and {str(other)!r} have different anchors" - ) - parts = [".."] * step + self._tail[len(path._tail) :] - return self.with_segments(*parts) + # NOTE relative_to was elevated to UPath as otherwise this would + # cause a circular dependency + # def relative_to(self, other, /, *_deprecated, walk_up=False): def is_relative_to(self, other, /, *_deprecated): if _deprecated: diff --git a/upath/core.py b/upath/core.py index 49d997ca..2649b87e 100644 --- a/upath/core.py +++ b/upath/core.py @@ -721,7 +721,34 @@ def relative_to( # type: ignore[override] "paths have different storage_options:" f" {self.storage_options!r} != {other.storage_options!r}" ) - return super().relative_to(other, *_deprecated, walk_up=walk_up) + + 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( + f"pathlib.PurePath.relative_to(*args) {msg}", + DeprecationWarning, + stacklevel=2, + ) + other = self.with_segments(other, *_deprecated) + for step, path in enumerate([other] + list(other.parents)): # noqa: B007 + if self.is_relative_to(path): + break + elif not walk_up: + raise ValueError( + f"{str(self)!r} is not in the subpath of {str(other)!r}" + ) + elif path.name == "..": + raise ValueError(f"'..' segment in {str(other)!r} cannot be walked") + else: + raise ValueError( + f"{str(self)!r} and {str(other)!r} have different anchors" + ) + parts = [".."] * step + self._tail[len(path._tail) :] + return UPath(*parts, **self._storage_options) def is_relative_to(self, other, /, *_deprecated) -> bool: # type: ignore[override] if isinstance(other, UPath) and self.storage_options != other.storage_options: diff --git a/upath/implementations/cloud.py b/upath/implementations/cloud.py index 36f4029f..103bad52 100644 --- a/upath/implementations/cloud.py +++ b/upath/implementations/cloud.py @@ -57,8 +57,7 @@ def iterdir(self): def relative_to(self, other, /, *_deprecated, walk_up=False): # use the parent implementation for the ValueError logic - super().relative_to(other, *_deprecated, walk_up=False) - return self + return super().relative_to(other, *_deprecated, walk_up=walk_up) class GCSPath(CloudPath): diff --git a/upath/implementations/local.py b/upath/implementations/local.py index a0961cea..de0c31c5 100644 --- a/upath/implementations/local.py +++ b/upath/implementations/local.py @@ -11,6 +11,7 @@ from typing import Collection from typing import MutableMapping from urllib.parse import SplitResult +import warnings from upath._protocol import compatible_protocol from upath.core import UPath diff --git a/upath/tests/implementations/test_azure.py b/upath/tests/implementations/test_azure.py index ee38a917..82f88e9e 100644 --- a/upath/tests/implementations/test_azure.py +++ b/upath/tests/implementations/test_azure.py @@ -2,6 +2,7 @@ from upath import UPath from upath.implementations.cloud import AzurePath +from upath.implementations.local import PosixUPath from ..cases import BaseTests from ..utils import skip_on_windows @@ -61,3 +62,21 @@ def test_broken_mkdir(self): (path / "file").write_text("foo") assert path.exists() + + def test_relative_to(self): + rel_path = UPath("az:///test_bucket/file.txt").relative_to(UPath("az:///test_bucket")) + assert isinstance(rel_path, PosixUPath) + assert not rel_path.is_absolute() + assert 'file.txt' == rel_path.path + + walk_path = UPath("az:///test_bucket/file.txt").relative_to(UPath("az:///other_test_bucket"), walk_up=True) + assert isinstance(walk_path, PosixUPath) + assert not walk_path.is_absolute() + assert '../test_bucket/file.txt' == walk_path.path + + with pytest.raises(ValueError): + UPath("az:///test_bucket/file.txt").relative_to(UPath("az:///prod_bucket")) + + with pytest.raises(ValueError): + UPath("az:///test_bucket/file.txt").relative_to(UPath("file:///test_bucket")) + diff --git a/upath/tests/implementations/test_local.py b/upath/tests/implementations/test_local.py index e3f59d48..d6778f77 100644 --- a/upath/tests/implementations/test_local.py +++ b/upath/tests/implementations/test_local.py @@ -1,7 +1,7 @@ import pytest from upath import UPath -from upath.implementations.local import LocalPath +from upath.implementations.local import LocalPath, PosixUPath from upath.tests.cases import BaseTests from upath.tests.utils import xfail_if_version @@ -15,6 +15,24 @@ def path(self, local_testdir): def test_is_LocalPath(self): assert isinstance(self.path, LocalPath) + def test_relative_to(self): + rel_path = UPath("file:///test_bucket/file.txt").relative_to(UPath("file:///test_bucket")) + assert isinstance(rel_path, PosixUPath) + assert not rel_path.is_absolute() + assert 'file.txt' == rel_path.path + + walk_path = UPath("file:///test_bucket/file.txt").relative_to(UPath("file:///other_test_bucket"), walk_up=True) + assert isinstance(walk_path, PosixUPath) + assert not walk_path.is_absolute() + assert '../test_bucket/file.txt' == walk_path.path + + with pytest.raises(ValueError): + UPath("file:///test_bucket/file.txt").relative_to(UPath("file:///prod_bucket")) + + with pytest.raises(ValueError): + UPath("file:///test_bucket/file.txt").relative_to(UPath("s3:///test_bucket")) + + @xfail_if_version("fsspec", lt="2023.10.0", reason="requires fsspec>=2023.10.0") class TestRayIOFSSpecLocal(BaseTests): diff --git a/upath/tests/implementations/test_s3.py b/upath/tests/implementations/test_s3.py index f9cd3974..8218eec0 100644 --- a/upath/tests/implementations/test_s3.py +++ b/upath/tests/implementations/test_s3.py @@ -6,7 +6,8 @@ import fsspec import pytest # noqa: F401 -from upath import UPath +from upath.core import UPath +from upath.implementations.local import PosixUPath from upath.implementations.cloud import S3Path from ..cases import BaseTests @@ -53,9 +54,21 @@ def test_rmdir(self): self.path.joinpath("file1.txt").rmdir() def test_relative_to(self): - assert "s3://test_bucket/file.txt" == str( - UPath("s3://test_bucket/file.txt").relative_to(UPath("s3://test_bucket")) - ) + rel_path = UPath("s3:///test_bucket/file.txt").relative_to(UPath("s3:///test_bucket")) + assert isinstance(rel_path, PosixUPath) + assert not rel_path.is_absolute() + assert 'file.txt' == rel_path.path + + walk_path = UPath("s3:///test_bucket/file.txt").relative_to(UPath("s3:///other_test_bucket"), walk_up=True) + assert isinstance(walk_path, PosixUPath) + assert not walk_path.is_absolute() + assert '../test_bucket/file.txt' == walk_path.path + + with pytest.raises(ValueError): + UPath("s3:///test_bucket/file.txt").relative_to(UPath("s3:///prod_bucket")) + + with pytest.raises(ValueError): + UPath("s3:///test_bucket/file.txt").relative_to(UPath("file:///test_bucket")) def test_iterdir_root(self): client_kwargs = self.path.storage_options["client_kwargs"]