Skip to content

Commit

Permalink
fea: return posixpath for relative_to
Browse files Browse the repository at this point in the history
  • Loading branch information
CompRhys committed Oct 30, 2024
1 parent 1a32d12 commit 3e83274
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 36 deletions.
31 changes: 3 additions & 28 deletions upath/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
29 changes: 28 additions & 1 deletion upath/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 1 addition & 2 deletions upath/implementations/cloud.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
1 change: 1 addition & 0 deletions upath/implementations/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions upath/tests/implementations/test_azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"))

20 changes: 19 additions & 1 deletion upath/tests/implementations/test_local.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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):
Expand Down
21 changes: 17 additions & 4 deletions upath/tests/implementations/test_s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]
Expand Down

0 comments on commit 3e83274

Please sign in to comment.