From ca4664d875a435e79b4f6c78a21098089403e518 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 18:51:57 +0200 Subject: [PATCH 1/8] upath: implement move and add tests --- upath/core.py | 29 ++++++++++++++++ upath/tests/cases.py | 43 ++++++++++++++++++++++++ upath/tests/implementations/test_data.py | 16 +++++++++ 3 files changed, 88 insertions(+) diff --git a/upath/core.py b/upath/core.py index a4e1127c..36af6dd9 100644 --- a/upath/core.py +++ b/upath/core.py @@ -648,6 +648,35 @@ def copy_into( else: return super().copy_into(target_dir, **kwargs) + @overload + def move(self, target: _WT, **kwargs: Any) -> _WT: ... + + @overload + def move(self, target: SupportsPathLike | str, **kwargs: Any) -> Self: ... + + def move(self, target: _WT | SupportsPathLike | str, **kwargs: Any) -> _WT | UPath: + target = self.copy(target, **kwargs) + self.fs.rm(self.path, recursive=self.is_dir()) + return target + + @overload + def move_into(self, target_dir: _WT, **kwargs: Any) -> _WT: ... + + @overload + def move_into(self, target_dir: SupportsPathLike | str, **kwargs: Any) -> Self: ... + + def move_into( + self, target_dir: _WT | SupportsPathLike | str, **kwargs: Any + ) -> _WT | UPath: + name = self.name + if not name: + raise ValueError(f"{self!r} has an empty name") + elif hasattr(target_dir, "with_segments"): + target = target_dir.with_segments(target_dir, name) # type: ignore + else: + target = self.with_segments(target_dir, name) + return self.move(target) + # --- WritablePath attributes ------------------------------------- def symlink_to( diff --git a/upath/tests/cases.py b/upath/tests/cases.py index fb0e90b2..0d397df3 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -606,3 +606,46 @@ def test_copy_into_memory(self, clear_fsspec_memory_cache): target = target_dir / "file1.txt" assert target.exists() assert target.read_text() == content + + def test_move_local(self, tmp_path: Path): + target = UPath(tmp_path) / "target-file1.txt" + + source = self.path / "file1.txt" + content = source.read_text() + source.move(target) + assert target.exists() + assert target.read_text() == content + assert not source.exists() + + def test_move_into_local(self, tmp_path: Path): + target_dir = UPath(tmp_path) / "target-dir" + target_dir.mkdir() + + source = self.path / "file1.txt" + content = source.read_text() + source.move_into(target_dir) + target = target_dir / "file1.txt" + assert target.exists() + assert target.read_text() == content + assert not source.exists() + + def test_move_memory(self, clear_fsspec_memory_cache): + target = UPath("memory:///target-file1.txt") + source = self.path / "file1.txt" + content = source.read_text() + source.move(target) + assert target.exists() + assert target.read_text() == content + assert not source.exists() + + def test_move_into_memory(self, clear_fsspec_memory_cache): + target_dir = UPath("memory:///target-dir") + target_dir.mkdir() + + source = self.path / "file1.txt" + content = source.read_text() + source.move_into(target_dir) + target = target_dir / "file1.txt" + assert target.exists() + assert target.read_text() == content + assert not source.exists() diff --git a/upath/tests/implementations/test_data.py b/upath/tests/implementations/test_data.py index 840b93d1..9f2f0077 100644 --- a/upath/tests/implementations/test_data.py +++ b/upath/tests/implementations/test_data.py @@ -264,3 +264,19 @@ def test_copy_into_memory(self, clear_fsspec_memory_cache): target = target_dir / source.name assert target.exists() assert target.read_text() == content + + @pytest.mark.skip(reason="DataPath does not support unlink") + def test_move_local(self, tmp_path): + pass + + @pytest.mark.skip(reason="DataPath does not support unlink") + def test_move_into_local(self, tmp_path): + pass + + @pytest.mark.skip(reason="DataPath does not support unlink") + def test_move_memory(self, clear_fsspec_memory_cache): + pass + + @pytest.mark.skip(reason="DataPath does not support unlink") + def test_move_into_memory(self, clear_fsspec_memory_cache): + pass From 5c90cc47665c5f128b0089dc473374cd43482981 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 18:54:32 +0200 Subject: [PATCH 2/8] upath: fix required typing_extension import --- upath/implementations/data.py | 10 ++++++---- upath/implementations/github.py | 10 ++++++---- upath/implementations/hdfs.py | 10 ++++++---- upath/implementations/memory.py | 10 ++++++---- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/upath/implementations/data.py b/upath/implementations/data.py index 4197f2de..6ad50667 100644 --- a/upath/implementations/data.py +++ b/upath/implementations/data.py @@ -2,14 +2,16 @@ import sys from collections.abc import Sequence +from typing import TYPE_CHECKING from upath.core import UPath from upath.types import JoinablePathLike -if sys.version_info > (3, 11): - from typing import Self -else: - from typing_extensions import Self +if TYPE_CHECKING: + if sys.version_info > (3, 11): + from typing import Self + else: + from typing_extensions import Self class DataPath(UPath): diff --git a/upath/implementations/github.py b/upath/implementations/github.py index 5fd400d8..0c6572a8 100644 --- a/upath/implementations/github.py +++ b/upath/implementations/github.py @@ -5,13 +5,15 @@ import sys from collections.abc import Iterator from collections.abc import Sequence +from typing import TYPE_CHECKING import upath.core -if sys.version_info > (3, 11): - from typing import Self -else: - from typing_extensions import Self +if TYPE_CHECKING: + if sys.version_info > (3, 11): + from typing import Self + else: + from typing_extensions import Self class GitHubPath(upath.core.UPath): diff --git a/upath/implementations/hdfs.py b/upath/implementations/hdfs.py index 0cabb42c..dab26167 100644 --- a/upath/implementations/hdfs.py +++ b/upath/implementations/hdfs.py @@ -2,13 +2,15 @@ import sys from collections.abc import Iterator +from typing import TYPE_CHECKING from upath.core import UPath -if sys.version_info > (3, 11): - from typing import Self -else: - from typing_extensions import Self +if TYPE_CHECKING: + if sys.version_info > (3, 11): + from typing import Self + else: + from typing_extensions import Self __all__ = ["HDFSPath"] diff --git a/upath/implementations/memory.py b/upath/implementations/memory.py index 350e11c8..33aaa8c9 100644 --- a/upath/implementations/memory.py +++ b/upath/implementations/memory.py @@ -2,13 +2,15 @@ import sys from collections.abc import Iterator +from typing import TYPE_CHECKING from upath.core import UPath -if sys.version_info > (3, 11): - from typing import Self -else: - from typing_extensions import Self +if TYPE_CHECKING: + if sys.version_info > (3, 11): + from typing import Self + else: + from typing_extensions import Self __all__ = ["MemoryPath"] From 021b2fdbaaf23b848b4833b5e1257f94627e6512 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 18:56:46 +0200 Subject: [PATCH 3/8] typesafety: check move and move_into --- typesafety/test_upath_signatures.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/typesafety/test_upath_signatures.yml b/typesafety/test_upath_signatures.yml index 781a801a..94470af7 100644 --- a/typesafety/test_upath_signatures.yml +++ b/typesafety/test_upath_signatures.yml @@ -454,6 +454,24 @@ reveal_type(p.copy_into("target_dir")) # N: Revealed type is "upath.core.UPath" reveal_type(p.copy_into(UPath("target_dir"))) # N: Revealed type is "upath.core.UPath" +- case: upath_method_move + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.move("target")) # N: Revealed type is "upath.core.UPath" + reveal_type(p.move(UPath("target"))) # N: Revealed type is "upath.core.UPath" + +- case: upath_method_move_into + disable_cache: false + main: | + from upath import UPath + + p = UPath("") + reveal_type(p.move_into("target_dir")) # N: Revealed type is "upath.core.UPath" + reveal_type(p.move_into(UPath("target_dir"))) # N: Revealed type is "upath.core.UPath" + - case: upath_method_symlink_to disable_cache: false main: | @@ -1017,6 +1035,10 @@ reveal_type(p.copy({{ cls }}("target"))) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.copy_into("target_dir")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.copy_into({{ cls }}("target_dir"))) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.move("target")) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.move({{ cls }}("target"))) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.move_into("target_dir")) # N: Revealed type is "{{ module }}.{{ cls }}" + reveal_type(p.move_into({{ cls }}("target_dir"))) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.rename("new_name")) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.rename({{ cls }}("new_name"))) # N: Revealed type is "{{ module }}.{{ cls }}" reveal_type(p.replace("target")) # N: Revealed type is "{{ module }}.{{ cls }}" From e778d5739a111daa630529e327c17d36089de5c7 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 18:59:24 +0200 Subject: [PATCH 4/8] upath.implementations: add missing future imports --- upath/implementations/cached.py | 2 ++ upath/implementations/github.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/upath/implementations/cached.py b/upath/implementations/cached.py index 2f4071f3..dc8800e9 100644 --- a/upath/implementations/cached.py +++ b/upath/implementations/cached.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from upath.core import UPath diff --git a/upath/implementations/github.py b/upath/implementations/github.py index 0c6572a8..692ed5fe 100644 --- a/upath/implementations/github.py +++ b/upath/implementations/github.py @@ -2,6 +2,8 @@ GitHub file system implementation """ +from __future__ import annotations + import sys from collections.abc import Iterator from collections.abc import Sequence From 9f00a080f2735c195ad0a0816d68deb34e5db381 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 19:05:30 +0200 Subject: [PATCH 5/8] tests: fix test cases on readonly fs --- upath/tests/implementations/test_github.py | 16 ++++++++++++++++ upath/tests/implementations/test_http.py | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/upath/tests/implementations/test_github.py b/upath/tests/implementations/test_github.py index d6330f2a..92d77581 100644 --- a/upath/tests/implementations/test_github.py +++ b/upath/tests/implementations/test_github.py @@ -71,3 +71,19 @@ def test_write_text(self): @pytest.mark.skip(reason="GitHub filesystem is read-only") def test_fsspec_compat(self): pass + + @pytest.mark.skip(reason="Only testing read on GithubPath") + def test_move_local(self, tmp_path): + pass + + @pytest.mark.skip(reason="Only testing read on GithubPath") + def test_move_into_local(self, tmp_path): + pass + + @pytest.mark.skip(reason="Only testing read on GithubPath") + def test_move_memory(self, clear_fsspec_memory_cache): + pass + + @pytest.mark.skip(reason="Only testing read on GithubPath") + def test_move_into_memory(self, clear_fsspec_memory_cache): + pass diff --git a/upath/tests/implementations/test_http.py b/upath/tests/implementations/test_http.py index 170ad197..473bb95d 100644 --- a/upath/tests/implementations/test_http.py +++ b/upath/tests/implementations/test_http.py @@ -141,6 +141,22 @@ def test_info(self): assert p1.info.is_dir() is True assert p1.info.is_symlink() is False + @pytest.mark.skip(reason="HttpPath does not support unlink") + def test_move_local(self, tmp_path): + pass + + @pytest.mark.skip(reason="HttpPath does not support unlink") + def test_move_into_local(self, tmp_path): + pass + + @pytest.mark.skip(reason="HttpPath does not support unlink") + def test_move_memory(self, clear_fsspec_memory_cache): + pass + + @pytest.mark.skip(reason="HttpPath does not support unlink") + def test_move_into_memory(self, clear_fsspec_memory_cache): + pass + @pytest.mark.parametrize( "args,parts", From fe767bdc4c061565e80098a7a81203b7cc277d0f Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 19:05:55 +0200 Subject: [PATCH 6/8] upath.extensions: add move and move_into to ProxyUPath --- upath/extensions.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/upath/extensions.py b/upath/extensions.py index 715ba610..8c83873c 100644 --- a/upath/extensions.py +++ b/upath/extensions.py @@ -434,6 +434,12 @@ def copy(self, target: WritablePathLike, **kwargs: Any) -> Self: # type: ignore def copy_into(self, target_dir: WritablePathLike, **kwargs: Any) -> Self: # type: ignore[override] # noqa: E501 return self._from_upath(self.__wrapped__.copy_into(target_dir, **kwargs)) + def move(self, target: WritablePathLike, **kwargs: Any) -> Self: # type: ignore[override] # noqa: E501 + return self._from_upath(self.__wrapped__.move(target, **kwargs)) + + def move_into(self, target_dir: WritablePathLike, **kwargs: Any) -> Self: # type: ignore[override] # noqa: E501 + return self._from_upath(self.__wrapped__.move_into(target_dir, **kwargs)) + def write_bytes(self, data: bytes) -> int: return self.__wrapped__.write_bytes(data) From c7be99544a144696b780e99b9832f1c5fcc5c206 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 19:12:14 +0200 Subject: [PATCH 7/8] upath.implementations.local: provide move and move_into implementations on older pythons --- upath/implementations/local.py | 35 +++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/upath/implementations/local.py b/upath/implementations/local.py index a609202f..7119c742 100644 --- a/upath/implementations/local.py +++ b/upath/implementations/local.py @@ -310,7 +310,7 @@ def open( **fsspec_kwargs, ) - if sys.version_info < (3, 14): + if sys.version_info < (3, 14): # noqa: C901 @overload def copy(self, target: _WT, **kwargs: Any) -> _WT: ... @@ -350,6 +350,39 @@ def copy_into( else: return _copy_into(target_dir, **kwargs) + @overload + def move(self, target: _WT, **kwargs: Any) -> _WT: ... + + @overload + def move(self, target: SupportsPathLike | str, **kwargs: Any) -> Self: ... + + def move( + self, target: _WT | SupportsPathLike | str, **kwargs: Any + ) -> _WT | Self: + target = self.copy(target, **kwargs) + self.fs.rm(self.path, recursive=self.is_dir()) + return target + + @overload + def move_into(self, target_dir: _WT, **kwargs: Any) -> _WT: ... + + @overload + def move_into( + self, target_dir: SupportsPathLike | str, **kwargs: Any + ) -> Self: ... + + def move_into( + self, target_dir: _WT | SupportsPathLike | str, **kwargs: Any + ) -> _WT | Self: + name = self.name + if not name: + raise ValueError(f"{self!r} has an empty name") + elif hasattr(target_dir, "with_segments"): + target = target_dir.with_segments(target_dir, name) # type: ignore + else: + target = self.with_segments(target_dir, name) + return self.move(target) + @property def info(self) -> PathInfo: return _LocalPathInfo(self) From f9fb4f1ccc3fbb6081a60112a80a4889ed842e66 Mon Sep 17 00:00:00 2001 From: Andreas Poehlmann Date: Fri, 3 Oct 2025 19:17:58 +0200 Subject: [PATCH 8/8] upath.implementations.local: fix minor typing issue on 311 and 312 --- upath/implementations/local.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/upath/implementations/local.py b/upath/implementations/local.py index 7119c742..e6d4a27d 100644 --- a/upath/implementations/local.py +++ b/upath/implementations/local.py @@ -378,9 +378,9 @@ def move_into( if not name: raise ValueError(f"{self!r} has an empty name") elif hasattr(target_dir, "with_segments"): - target = target_dir.with_segments(target_dir, name) # type: ignore + target = target_dir.with_segments(str(target_dir), name) # type: ignore else: - target = self.with_segments(target_dir, name) + target = self.with_segments(str(target_dir), name) return self.move(target) @property