From 99cf3f1abb87cb56eb310872e53a96fd116f8e15 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sun, 21 Dec 2025 17:37:58 +0100 Subject: [PATCH 1/9] tests: add JoinablePath.parser --- upath/tests/cases.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/upath/tests/cases.py b/upath/tests/cases.py index 8026b15a..74edab7b 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -9,6 +9,7 @@ from fsspec import __version__ as fsspec_version from fsspec import filesystem from packaging.version import Version +from pathlib_abc import PathParser from upath import UnsupportedOperation from upath import UPath @@ -30,6 +31,15 @@ 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_is_absolute(self): assert self.path.is_absolute() is True From 9a9e43b8ce0dcc4e46cb7b20671a580e86ce346a Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sun, 21 Dec 2025 17:42:18 +0100 Subject: [PATCH 2/9] tests: add JoinablePath.with_segments --- upath/tests/cases.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/upath/tests/cases.py b/upath/tests/cases.py index 74edab7b..914bb559 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -40,6 +40,11 @@ def test_parser(self): 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_is_absolute(self): assert self.path.is_absolute() is True From 1b582068bebcfa4469048df728fdddbecdfdbc6e Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sun, 21 Dec 2025 17:45:53 +0100 Subject: [PATCH 3/9] tests: DataPath.with_segments raises --- upath/tests/implementations/test_data.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/upath/tests/implementations/test_data.py b/upath/tests/implementations/test_data.py index ac632a42..e87ef420 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,10 @@ def test_is_DataPath(self): """ assert isinstance(self.path, DataPath) + def test_with_segments(self): + with pytest.raises(UnsupportedOperation): + super().test_with_segments() + @pytest.mark.skip(reason="DataPath does not have directories") def test_stat_dir_st_mode(self): super().test_stat_dir_st_mode() From 01523049382304643b9b7a04eab280ec9ad6a6d8 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sun, 21 Dec 2025 18:18:59 +0100 Subject: [PATCH 4/9] tests: add JoinablePath.__vfspath__ --- upath/tests/cases.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/upath/tests/cases.py b/upath/tests/cases.py index 914bb559..6ec9a8ad 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -10,6 +10,7 @@ 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 @@ -45,6 +46,12 @@ def test_with_segments(self): 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_is_absolute(self): assert self.path.is_absolute() is True From e4f148a513f8658cca62f1e6b39375453c62b95f Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sun, 21 Dec 2025 18:27:07 +0100 Subject: [PATCH 5/9] tests: add JoinablePath.anchor --- upath/tests/cases.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/upath/tests/cases.py b/upath/tests/cases.py index 6ec9a8ad..a4003c7a 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -52,6 +52,11 @@ def test___vfspath__(self): 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 From 06e0be76fa03dbb86dff3b54d271746a3ec485bd Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sun, 21 Dec 2025 20:25:39 +0100 Subject: [PATCH 6/9] tests: add JoinablePath.is_relative_to --- upath/tests/cases.py | 9 +++++++++ upath/tests/implementations/test_data.py | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/upath/tests/cases.py b/upath/tests/cases.py index a4003c7a..7ccb3ad6 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -193,6 +193,15 @@ 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_trailing_slash_joinpath_is_identical(self): # setup cls = type(self.path) diff --git a/upath/tests/implementations/test_data.py b/upath/tests/implementations/test_data.py index e87ef420..eab0ada8 100644 --- a/upath/tests/implementations/test_data.py +++ b/upath/tests/implementations/test_data.py @@ -38,6 +38,10 @@ 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() From 8a4c3af269d357e4402b5084deeede25e03658bc Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sun, 21 Dec 2025 20:26:44 +0100 Subject: [PATCH 7/9] upath.extensions: fix is_relative_to for extensions --- upath/extensions.py | 2 ++ 1 file changed, 2 insertions(+) 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: From fbc57c143858c6de2ee9bde203afe32b00c6c1f2 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sun, 21 Dec 2025 20:33:56 +0100 Subject: [PATCH 8/9] tests: add JoinablePath.full_match --- upath/tests/cases.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/upath/tests/cases.py b/upath/tests/cases.py index 7ccb3ad6..a9f13dc3 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -202,6 +202,12 @@ def test_is_relative_to(self): 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) From ef321a4b39de16e1963531567881a1b90112b96d Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Sun, 21 Dec 2025 21:14:54 +0100 Subject: [PATCH 9/9] tests: add missing tests from pathlib_abc api --- upath/tests/cases.py | 54 ++++++++++++++++++++++ upath/tests/implementations/test_data.py | 28 +++++++++++ upath/tests/implementations/test_github.py | 8 ++++ upath/tests/implementations/test_http.py | 8 ++++ upath/tests/implementations/test_tar.py | 8 ++++ upath/tests/implementations/test_zip.py | 8 ++++ 6 files changed, 114 insertions(+) diff --git a/upath/tests/cases.py b/upath/tests/cases.py index a9f13dc3..309a8de2 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -451,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") @@ -645,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 eab0ada8..b869c268 100644 --- a/upath/tests/implementations/test_data.py +++ b/upath/tests/implementations/test_data.py @@ -313,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):