diff --git a/upath/extensions.py b/upath/extensions.py index d75dd43b..a60e7ca8 100644 --- a/upath/extensions.py +++ b/upath/extensions.py @@ -431,6 +431,8 @@ def relative_to( # type: ignore[override] ) def is_relative_to(self, other, /, *_deprecated) -> bool: # type: ignore[override] + if not isinstance(other, str): + other = vfspath(other) return self.__wrapped__.is_relative_to(other, *_deprecated) def hardlink_to(self, target: ReadablePathLike) -> None: diff --git a/upath/tests/cases.py b/upath/tests/cases.py index 8026b15a..309a8de2 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -9,6 +9,8 @@ from fsspec import __version__ as fsspec_version from fsspec import filesystem from packaging.version import Version +from pathlib_abc import PathParser +from pathlib_abc import vfspath from upath import UnsupportedOperation from upath import UPath @@ -30,6 +32,31 @@ class JoinablePathTests: path: UPath + def test_parser(self): + parser = self.path.parser + assert isinstance(parser, PathParser) + assert isinstance(parser.sep, str) + assert parser.altsep is None or isinstance(parser.altsep, str) + assert callable(parser.split) + assert callable(parser.splitext) + assert callable(parser.normcase) + + def test_with_segments(self): + p = self.path.with_segments(self.path.__vfspath__(), "folder", "file.txt") + assert p.parts[-2:] == ("folder", "file.txt") + assert type(p) is type(self.path) + + def test___vfspath__(self): + assert hasattr(self.path, "__vfspath__") + assert callable(self.path.__vfspath__) + str_path = vfspath(self.path) + assert isinstance(str_path, str) + + def test_anchor(self): + anchor = self.path.anchor + assert isinstance(anchor, str) + assert anchor == self.path.drive + self.path.root + def test_is_absolute(self): assert self.path.is_absolute() is True @@ -166,6 +193,21 @@ def test_relative_to(self): relative = child.relative_to(base) assert str(relative) == "folder1/file1.txt" + def test_is_relative_to(self): + base = self.path + child = self.path / "folder1" / "file1.txt" + other = UPath("/some/other/path") + + assert child.is_relative_to(base) is True + assert base.is_relative_to(child) is False + assert child.is_relative_to(other) is False + + def test_full_match(self): + p = self.path / "folder" / "file.txt" + assert p.full_match("**/*") is True + assert p.full_match("**/*.txt") is True + assert p.full_match("*.doesnotexist") is False + def test_trailing_slash_joinpath_is_identical(self): # setup cls = type(self.path) @@ -409,12 +451,52 @@ def test_read_text(self, local_testdir): upath.read_text() == Path(local_testdir).joinpath("file1.txt").read_text() ) + def test_read_text_encoding(self): + upath = self.path.joinpath("file1.txt") + content = upath.read_text(encoding="utf-8") + assert content == "hello world" + + def test_read_text_errors(self): + upath = self.path.joinpath("file1.txt") + content = upath.read_text(encoding="ascii", errors="strict") + assert content == "hello world" + def test_rglob(self, pathlib_base): pattern = "*.txt" result = [*self.path.rglob(pattern)] expected = [*pathlib_base.rglob(pattern)] assert len(result) == len(expected) + def test_walk(self, local_testdir): + # collect walk results from UPath + upath_walk = [] + for dirpath, dirnames, filenames in self.path.walk(): + rel_dirpath = dirpath.relative_to(self.path) + upath_walk.append((str(rel_dirpath), sorted(dirnames), sorted(filenames))) + upath_walk.sort() + + # collect walk results using os.walk (compatible with Python 3.9+) + os_walk = [] + for dirpath, dirnames, filenames in os.walk(local_testdir): + rel_dirpath = os.path.relpath(dirpath, local_testdir) + os_walk.append((rel_dirpath, sorted(dirnames), sorted(filenames))) + os_walk.sort() + + assert upath_walk == os_walk + + def test_walk_top_down_false(self): + # test walk with top_down=False returns directories after their contents + paths_seen = [] + for dirpath, _, _ in self.path.walk(top_down=False): + paths_seen.append(dirpath) + + # in bottom-up walk, parent directories should come after children + for i, path in enumerate(paths_seen): + for _, other in enumerate(paths_seen[i + 1 :], start=i + 1): + # if path is a parent of other, path should come after other + if other.is_relative_to(path) and other != path: + pytest.fail(f"In bottom-up walk, {path} should come after {other}") + def test_samefile(self): f1 = self.path.joinpath("file1.txt") f2 = self.path.joinpath("file2.txt") @@ -603,6 +685,20 @@ def test_write_text(self, pathlib_base): path.write_text(s) assert path.read_text() == s + def test_write_text_encoding(self): + fn = "test_write_text_enc.txt" + s = "hello_world" + path = self.path.joinpath(fn) + path.write_text(s, encoding="utf-8") + assert path.read_text(encoding="utf-8") == s + + def test_write_text_errors(self): + fn = "test_write_text_errors.txt" + s = "hello_world" + path = self.path.joinpath(fn) + path.write_text(s, encoding="ascii", errors="strict") + assert path.read_text(encoding="ascii") == s + def test_chmod(self): with pytest.raises(NotImplementedError): self.path.joinpath("file1.txt").chmod(777) diff --git a/upath/tests/implementations/test_data.py b/upath/tests/implementations/test_data.py index ac632a42..b869c268 100644 --- a/upath/tests/implementations/test_data.py +++ b/upath/tests/implementations/test_data.py @@ -3,6 +3,7 @@ import fsspec import pytest +from upath import UnsupportedOperation from upath import UPath from upath.implementations.data import DataPath from upath.tests.cases import BaseTests @@ -33,6 +34,14 @@ def test_is_DataPath(self): """ assert isinstance(self.path, DataPath) + def test_with_segments(self): + with pytest.raises(UnsupportedOperation): + super().test_with_segments() + + def test_is_relative_to(self): + with pytest.raises(UnsupportedOperation): + super().test_is_relative_to() + @pytest.mark.skip(reason="DataPath does not have directories") def test_stat_dir_st_mode(self): super().test_stat_dir_st_mode() @@ -304,3 +313,31 @@ def test_trailing_slash_is_stripped(self): @pytest.mark.skip(reason="DataPath does not support joins") def test_parents_are_absolute(self): pass + + @pytest.mark.skip(reason="DataPath does not support write_text") + def test_write_text_encoding(self): + pass + + @pytest.mark.skip(reason="DataPath does not support write_text") + def test_write_text_errors(self): + pass + + @pytest.mark.skip(reason="base test incompatible with DataPath") + def test_read_text_encoding(self): + pass + + @pytest.mark.skip(reason="base test incompatible with DataPath") + def test_read_text_errors(self): + pass + + @pytest.mark.skip(reason="DataPath does not support walk") + def test_walk(self, local_testdir): + pass + + @pytest.mark.skip(reason="DataPath does not support walk") + def test_walk_top_down_false(self): + pass + + @pytest.mark.skip(reason="DataPath does not support full_match") + def test_full_match(self): + pass diff --git a/upath/tests/implementations/test_github.py b/upath/tests/implementations/test_github.py index 4783b823..d132702c 100644 --- a/upath/tests/implementations/test_github.py +++ b/upath/tests/implementations/test_github.py @@ -132,3 +132,11 @@ def test_move_into_memory(self, clear_fsspec_memory_cache): @pytest.mark.skip(reason="Only testing read on GithubPath") def test_rename_with_target_absolute(self, target_factory): return super().test_rename_with_target_str_absolute(target_factory) + + @pytest.mark.skip(reason="Only testing read on GithubPath") + def test_write_text_encoding(self): + return super().test_write_text_encoding() + + @pytest.mark.skip(reason="Only testing read on GithubPath") + def test_write_text_errors(self): + return super().test_write_text_errors() diff --git a/upath/tests/implementations/test_http.py b/upath/tests/implementations/test_http.py index f52d8bf9..f3b22d43 100644 --- a/upath/tests/implementations/test_http.py +++ b/upath/tests/implementations/test_http.py @@ -177,6 +177,14 @@ def test_move_into_memory(self, clear_fsspec_memory_cache): def test_rename_with_target_absolute(self, target_factory): return super().test_rename_with_target_absolute(target_factory) + @pytest.mark.skip(reason="Only testing read on HttpPath") + def test_write_text_encoding(self): + return super().test_write_text_encoding() + + @pytest.mark.skip(reason="Only testing read on HttpPath") + def test_write_text_errors(self): + return super().test_write_text_errors() + @pytest.mark.parametrize( "args,parts", diff --git a/upath/tests/implementations/test_tar.py b/upath/tests/implementations/test_tar.py index 9b7326ce..c528a7e2 100644 --- a/upath/tests/implementations/test_tar.py +++ b/upath/tests/implementations/test_tar.py @@ -87,6 +87,14 @@ def test_move_into_memory(self, clear_fsspec_memory_cache): def test_rename_with_target_absolute(self, target_factory): return super().test_rename_with_target_str_absolute(target_factory) + @pytest.mark.skip(reason="Only testing read on TarPath") + def test_write_text_encoding(self): + return super().test_write_text_encoding() + + @pytest.mark.skip(reason="Only testing read on TarPath") + def test_write_text_errors(self): + return super().test_write_text_errors() + @pytest.fixture(scope="function") def tarred_testdir_file_in_memory(tarred_testdir_file, clear_fsspec_memory_cache): diff --git a/upath/tests/implementations/test_zip.py b/upath/tests/implementations/test_zip.py index 72956bb6..28779e32 100644 --- a/upath/tests/implementations/test_zip.py +++ b/upath/tests/implementations/test_zip.py @@ -149,6 +149,14 @@ def test_fsspec_compat(self): def test_rename_with_target_absolute(self, target_factory): return super().test_rename_with_target_absolute(target_factory) + @pytest.mark.skip(reason="fsspec zipfile filesystem is either read xor write mode") + def test_write_text_encoding(self): + return super().test_write_text_encoding() + + @pytest.mark.skip(reason="fsspec zipfile filesystem is either read xor write mode") + def test_write_text_errors(self): + return super().test_write_text_errors() + @pytest.fixture(scope="function") def zipped_testdir_file_in_memory(zipped_testdir_file, clear_fsspec_memory_cache):