Skip to content

Commit 4ef02a3

Browse files
authored
Upath add move into (#422)
* upath: implement move and add tests * upath: fix required typing_extension import * typesafety: check move and move_into * upath.implementations: add missing future imports * tests: fix test cases on readonly fs * upath.extensions: add move and move_into to ProxyUPath * upath.implementations.local: provide move and move_into implementations on older pythons * upath.implementations.local: fix minor typing issue on 311 and 312
1 parent 8c3f574 commit 4ef02a3

File tree

13 files changed

+210
-17
lines changed

13 files changed

+210
-17
lines changed

typesafety/test_upath_signatures.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,24 @@
454454
reveal_type(p.copy_into("target_dir")) # N: Revealed type is "upath.core.UPath"
455455
reveal_type(p.copy_into(UPath("target_dir"))) # N: Revealed type is "upath.core.UPath"
456456
457+
- case: upath_method_move
458+
disable_cache: false
459+
main: |
460+
from upath import UPath
461+
462+
p = UPath("")
463+
reveal_type(p.move("target")) # N: Revealed type is "upath.core.UPath"
464+
reveal_type(p.move(UPath("target"))) # N: Revealed type is "upath.core.UPath"
465+
466+
- case: upath_method_move_into
467+
disable_cache: false
468+
main: |
469+
from upath import UPath
470+
471+
p = UPath("")
472+
reveal_type(p.move_into("target_dir")) # N: Revealed type is "upath.core.UPath"
473+
reveal_type(p.move_into(UPath("target_dir"))) # N: Revealed type is "upath.core.UPath"
474+
457475
- case: upath_method_symlink_to
458476
disable_cache: false
459477
main: |
@@ -1017,6 +1035,10 @@
10171035
reveal_type(p.copy({{ cls }}("target"))) # N: Revealed type is "{{ module }}.{{ cls }}"
10181036
reveal_type(p.copy_into("target_dir")) # N: Revealed type is "{{ module }}.{{ cls }}"
10191037
reveal_type(p.copy_into({{ cls }}("target_dir"))) # N: Revealed type is "{{ module }}.{{ cls }}"
1038+
reveal_type(p.move("target")) # N: Revealed type is "{{ module }}.{{ cls }}"
1039+
reveal_type(p.move({{ cls }}("target"))) # N: Revealed type is "{{ module }}.{{ cls }}"
1040+
reveal_type(p.move_into("target_dir")) # N: Revealed type is "{{ module }}.{{ cls }}"
1041+
reveal_type(p.move_into({{ cls }}("target_dir"))) # N: Revealed type is "{{ module }}.{{ cls }}"
10201042
reveal_type(p.rename("new_name")) # N: Revealed type is "{{ module }}.{{ cls }}"
10211043
reveal_type(p.rename({{ cls }}("new_name"))) # N: Revealed type is "{{ module }}.{{ cls }}"
10221044
reveal_type(p.replace("target")) # N: Revealed type is "{{ module }}.{{ cls }}"

upath/core.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,35 @@ def copy_into(
648648
else:
649649
return super().copy_into(target_dir, **kwargs)
650650

651+
@overload
652+
def move(self, target: _WT, **kwargs: Any) -> _WT: ...
653+
654+
@overload
655+
def move(self, target: SupportsPathLike | str, **kwargs: Any) -> Self: ...
656+
657+
def move(self, target: _WT | SupportsPathLike | str, **kwargs: Any) -> _WT | UPath:
658+
target = self.copy(target, **kwargs)
659+
self.fs.rm(self.path, recursive=self.is_dir())
660+
return target
661+
662+
@overload
663+
def move_into(self, target_dir: _WT, **kwargs: Any) -> _WT: ...
664+
665+
@overload
666+
def move_into(self, target_dir: SupportsPathLike | str, **kwargs: Any) -> Self: ...
667+
668+
def move_into(
669+
self, target_dir: _WT | SupportsPathLike | str, **kwargs: Any
670+
) -> _WT | UPath:
671+
name = self.name
672+
if not name:
673+
raise ValueError(f"{self!r} has an empty name")
674+
elif hasattr(target_dir, "with_segments"):
675+
target = target_dir.with_segments(target_dir, name) # type: ignore
676+
else:
677+
target = self.with_segments(target_dir, name)
678+
return self.move(target)
679+
651680
# --- WritablePath attributes -------------------------------------
652681

653682
def symlink_to(

upath/extensions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,12 @@ def copy(self, target: WritablePathLike, **kwargs: Any) -> Self: # type: ignore
434434
def copy_into(self, target_dir: WritablePathLike, **kwargs: Any) -> Self: # type: ignore[override] # noqa: E501
435435
return self._from_upath(self.__wrapped__.copy_into(target_dir, **kwargs))
436436

437+
def move(self, target: WritablePathLike, **kwargs: Any) -> Self: # type: ignore[override] # noqa: E501
438+
return self._from_upath(self.__wrapped__.move(target, **kwargs))
439+
440+
def move_into(self, target_dir: WritablePathLike, **kwargs: Any) -> Self: # type: ignore[override] # noqa: E501
441+
return self._from_upath(self.__wrapped__.move_into(target_dir, **kwargs))
442+
437443
def write_bytes(self, data: bytes) -> int:
438444
return self.__wrapped__.write_bytes(data)
439445

upath/implementations/cached.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from __future__ import annotations
2+
13
from upath.core import UPath
24

35

upath/implementations/data.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
import sys
44
from collections.abc import Sequence
5+
from typing import TYPE_CHECKING
56

67
from upath.core import UPath
78
from upath.types import JoinablePathLike
89

9-
if sys.version_info > (3, 11):
10-
from typing import Self
11-
else:
12-
from typing_extensions import Self
10+
if TYPE_CHECKING:
11+
if sys.version_info > (3, 11):
12+
from typing import Self
13+
else:
14+
from typing_extensions import Self
1315

1416

1517
class DataPath(UPath):

upath/implementations/github.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22
GitHub file system implementation
33
"""
44

5+
from __future__ import annotations
6+
57
import sys
68
from collections.abc import Iterator
79
from collections.abc import Sequence
10+
from typing import TYPE_CHECKING
811

912
import upath.core
1013

11-
if sys.version_info > (3, 11):
12-
from typing import Self
13-
else:
14-
from typing_extensions import Self
14+
if TYPE_CHECKING:
15+
if sys.version_info > (3, 11):
16+
from typing import Self
17+
else:
18+
from typing_extensions import Self
1519

1620

1721
class GitHubPath(upath.core.UPath):

upath/implementations/hdfs.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
import sys
44
from collections.abc import Iterator
5+
from typing import TYPE_CHECKING
56

67
from upath.core import UPath
78

8-
if sys.version_info > (3, 11):
9-
from typing import Self
10-
else:
11-
from typing_extensions import Self
9+
if TYPE_CHECKING:
10+
if sys.version_info > (3, 11):
11+
from typing import Self
12+
else:
13+
from typing_extensions import Self
1214

1315
__all__ = ["HDFSPath"]
1416

upath/implementations/local.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ def open(
310310
**fsspec_kwargs,
311311
)
312312

313-
if sys.version_info < (3, 14):
313+
if sys.version_info < (3, 14): # noqa: C901
314314

315315
@overload
316316
def copy(self, target: _WT, **kwargs: Any) -> _WT: ...
@@ -350,6 +350,39 @@ def copy_into(
350350
else:
351351
return _copy_into(target_dir, **kwargs)
352352

353+
@overload
354+
def move(self, target: _WT, **kwargs: Any) -> _WT: ...
355+
356+
@overload
357+
def move(self, target: SupportsPathLike | str, **kwargs: Any) -> Self: ...
358+
359+
def move(
360+
self, target: _WT | SupportsPathLike | str, **kwargs: Any
361+
) -> _WT | Self:
362+
target = self.copy(target, **kwargs)
363+
self.fs.rm(self.path, recursive=self.is_dir())
364+
return target
365+
366+
@overload
367+
def move_into(self, target_dir: _WT, **kwargs: Any) -> _WT: ...
368+
369+
@overload
370+
def move_into(
371+
self, target_dir: SupportsPathLike | str, **kwargs: Any
372+
) -> Self: ...
373+
374+
def move_into(
375+
self, target_dir: _WT | SupportsPathLike | str, **kwargs: Any
376+
) -> _WT | Self:
377+
name = self.name
378+
if not name:
379+
raise ValueError(f"{self!r} has an empty name")
380+
elif hasattr(target_dir, "with_segments"):
381+
target = target_dir.with_segments(str(target_dir), name) # type: ignore
382+
else:
383+
target = self.with_segments(str(target_dir), name)
384+
return self.move(target)
385+
353386
@property
354387
def info(self) -> PathInfo:
355388
return _LocalPathInfo(self)

upath/implementations/memory.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
import sys
44
from collections.abc import Iterator
5+
from typing import TYPE_CHECKING
56

67
from upath.core import UPath
78

8-
if sys.version_info > (3, 11):
9-
from typing import Self
10-
else:
11-
from typing_extensions import Self
9+
if TYPE_CHECKING:
10+
if sys.version_info > (3, 11):
11+
from typing import Self
12+
else:
13+
from typing_extensions import Self
1214

1315
__all__ = ["MemoryPath"]
1416

upath/tests/cases.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -606,3 +606,46 @@ def test_copy_into_memory(self, clear_fsspec_memory_cache):
606606
target = target_dir / "file1.txt"
607607
assert target.exists()
608608
assert target.read_text() == content
609+
610+
def test_move_local(self, tmp_path: Path):
611+
target = UPath(tmp_path) / "target-file1.txt"
612+
613+
source = self.path / "file1.txt"
614+
content = source.read_text()
615+
source.move(target)
616+
assert target.exists()
617+
assert target.read_text() == content
618+
assert not source.exists()
619+
620+
def test_move_into_local(self, tmp_path: Path):
621+
target_dir = UPath(tmp_path) / "target-dir"
622+
target_dir.mkdir()
623+
624+
source = self.path / "file1.txt"
625+
content = source.read_text()
626+
source.move_into(target_dir)
627+
target = target_dir / "file1.txt"
628+
assert target.exists()
629+
assert target.read_text() == content
630+
assert not source.exists()
631+
632+
def test_move_memory(self, clear_fsspec_memory_cache):
633+
target = UPath("memory:///target-file1.txt")
634+
source = self.path / "file1.txt"
635+
content = source.read_text()
636+
source.move(target)
637+
assert target.exists()
638+
assert target.read_text() == content
639+
assert not source.exists()
640+
641+
def test_move_into_memory(self, clear_fsspec_memory_cache):
642+
target_dir = UPath("memory:///target-dir")
643+
target_dir.mkdir()
644+
645+
source = self.path / "file1.txt"
646+
content = source.read_text()
647+
source.move_into(target_dir)
648+
target = target_dir / "file1.txt"
649+
assert target.exists()
650+
assert target.read_text() == content
651+
assert not source.exists()

0 commit comments

Comments
 (0)