Skip to content

Commit 9ceaf2f

Browse files
authored
Adjust behavior of UPath.copy and UPath.copy_into with str and Path targets (#530)
* tests: add a str tmpdir test case for copy_into * test: add dir copy/copy_into test * upath.core: fix copy / copy_into / move / move_into * tests: check copy and copy_into also for Path and UPath * upath.core: fix Path handling in copy and copy_into * tests: add exception cases for copy / copy_into * upath.core: raise IsADirectoryError in copy * tests: adjust test cases to OSError for windows cpython also only checks for OSError in the test suite...
1 parent 4ff77c7 commit 9ceaf2f

File tree

4 files changed

+124
-16
lines changed

4 files changed

+124
-16
lines changed

upath/core.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1221,10 +1221,17 @@ def copy(self, target: _WT | SupportsPathLike | str, **kwargs: Any) -> _WT | UPa
12211221
"""
12221222
Recursively copy this file or directory tree to the given destination.
12231223
"""
1224-
if not isinstance(target, UPath):
1225-
return super().copy(self.with_segments(target), **kwargs)
1226-
else:
1227-
return super().copy(target, **kwargs)
1224+
if isinstance(target, str):
1225+
proto = get_upath_protocol(target)
1226+
if proto != self.protocol:
1227+
target = UPath(target)
1228+
else:
1229+
target = self.with_segments(target)
1230+
elif not isinstance(target, UPath):
1231+
target = UPath(target)
1232+
if target.is_dir():
1233+
raise IsADirectoryError(str(target))
1234+
return super().copy(target, **kwargs)
12281235

12291236
@overload
12301237
def copy_into(self, target_dir: _WT, **kwargs: Any) -> _WT: ...
@@ -1238,10 +1245,19 @@ def copy_into(
12381245
"""
12391246
Copy this file or directory tree into the given existing directory.
12401247
"""
1241-
if not isinstance(target_dir, UPath):
1242-
return super().copy_into(self.with_segments(target_dir), **kwargs)
1243-
else:
1244-
return super().copy_into(target_dir, **kwargs)
1248+
if isinstance(target_dir, str):
1249+
proto = get_upath_protocol(target_dir)
1250+
if proto != self.protocol:
1251+
target_dir = UPath(target_dir)
1252+
else:
1253+
target_dir = self.with_segments(target_dir)
1254+
elif not isinstance(target_dir, UPath):
1255+
target_dir = UPath(target_dir)
1256+
if not target_dir.exists():
1257+
raise FileNotFoundError(str(target_dir))
1258+
if not target_dir.is_dir():
1259+
raise NotADirectoryError(str(target_dir))
1260+
return super().copy_into(target_dir, **kwargs)
12451261

12461262
@overload
12471263
def move(self, target: _WT, **kwargs: Any) -> _WT: ...
@@ -1274,8 +1290,15 @@ def move_into(
12741290
raise ValueError(f"{self!r} has an empty name")
12751291
elif hasattr(target_dir, "with_segments"):
12761292
target = target_dir.with_segments(target_dir, name) # type: ignore
1293+
elif isinstance(target_dir, PurePath):
1294+
target = UPath(target_dir, name)
12771295
else:
12781296
target = self.with_segments(target_dir, name)
1297+
td = target.parent
1298+
if not td.exists():
1299+
raise FileNotFoundError(str(td))
1300+
elif not td.is_dir():
1301+
raise NotADirectoryError(str(td))
12791302
return self.move(target)
12801303

12811304
def _copy_from(

upath/implementations/local.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from upath._chain import ChainSegment
2222
from upath._chain import FSSpecChainParser
2323
from upath._protocol import compatible_protocol
24+
from upath._protocol import get_upath_protocol
2425
from upath.core import UnsupportedOperation
2526
from upath.core import UPath
2627
from upath.core import _UPathMixin
@@ -377,10 +378,17 @@ def copy(
377378
# hacky workaround for missing pathlib.Path.copy in python < 3.14
378379
# todo: revisit
379380
_copy: Any = ReadablePath.copy.__get__(self)
380-
if not isinstance(target, UPath):
381-
return _copy(self.with_segments(str(target)), **kwargs)
382-
else:
383-
return _copy(target, **kwargs)
381+
if isinstance(target, str):
382+
proto = get_upath_protocol(target)
383+
if proto != self.protocol:
384+
target = UPath(target)
385+
else:
386+
target = self.with_segments(target)
387+
elif not isinstance(target, UPath):
388+
target = UPath(target)
389+
if target.is_dir():
390+
raise IsADirectoryError(str(target))
391+
return _copy(target, **kwargs)
384392

385393
@overload
386394
def copy_into(self, target_dir: _WT, **kwargs: Any) -> _WT: ...
@@ -398,10 +406,15 @@ def copy_into(
398406
# hacky workaround for missing pathlib.Path.copy_into in python < 3.14
399407
# todo: revisit
400408
_copy_into: Any = ReadablePath.copy_into.__get__(self)
401-
if not isinstance(target_dir, UPath):
402-
return _copy_into(self.with_segments(str(target_dir)), **kwargs)
403-
else:
404-
return _copy_into(target_dir, **kwargs)
409+
if isinstance(target_dir, str):
410+
proto = get_upath_protocol(target_dir)
411+
if proto != self.protocol:
412+
target_dir = UPath(target_dir)
413+
else:
414+
target_dir = self.with_segments(target_dir)
415+
elif not isinstance(target_dir, UPath):
416+
target_dir = UPath(target_dir)
417+
return _copy_into(target_dir, **kwargs)
405418

406419
@overload
407420
def move(self, target: _WT, **kwargs: Any) -> _WT: ...
@@ -432,8 +445,15 @@ def move_into(
432445
raise ValueError(f"{self!r} has an empty name")
433446
elif hasattr(target_dir, "with_segments"):
434447
target = target_dir.with_segments(str(target_dir), name) # type: ignore
448+
elif isinstance(target_dir, pathlib.PurePath):
449+
target = UPath(target_dir, name)
435450
else:
436451
target = self.with_segments(str(target_dir), name)
452+
td = target.parent
453+
if not td.exists():
454+
raise FileNotFoundError(str(td))
455+
elif not td.is_dir():
456+
raise NotADirectoryError(str(td))
437457
return self.move(target)
438458

439459
@property

upath/tests/cases.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -551,6 +551,40 @@ def test_copy_local(self, tmp_path: Path):
551551
assert target.exists()
552552
assert target.read_text() == content
553553

554+
@pytest.mark.parametrize("target_type", [str, Path, UPath])
555+
def test_copy_into__file_to_str_tempdir(self, tmp_path: Path, target_type):
556+
tmp_path = tmp_path.joinpath("somewhere")
557+
tmp_path.mkdir()
558+
target_dir = target_type(tmp_path)
559+
assert isinstance(target_dir, target_type)
560+
561+
source = self.path_file
562+
source.copy_into(target_dir)
563+
target = tmp_path.joinpath(source.name)
564+
565+
assert target.exists()
566+
assert target.read_text() == source.read_text()
567+
568+
@pytest.mark.parametrize("target_type", [str, Path, UPath])
569+
def test_copy_into__dir_to_str_tempdir(self, tmp_path: Path, target_type):
570+
tmp_path = tmp_path.joinpath("somewhere")
571+
tmp_path.mkdir()
572+
target_dir = target_type(tmp_path)
573+
assert isinstance(target_dir, target_type)
574+
575+
source_dir = self.path.joinpath("folder1")
576+
assert source_dir.is_dir()
577+
source_dir.copy_into(target_dir)
578+
target = tmp_path.joinpath(source_dir.name)
579+
580+
assert target.exists()
581+
assert target.is_dir()
582+
for item in source_dir.iterdir():
583+
target_item = target.joinpath(item.name)
584+
assert target_item.exists()
585+
if item.is_file():
586+
assert target_item.read_text() == item.read_text()
587+
554588
def test_copy_into_local(self, tmp_path: Path):
555589
target_dir = UPath(tmp_path) / "target-dir"
556590
target_dir.mkdir()
@@ -581,6 +615,32 @@ def test_copy_into_memory(self, clear_fsspec_memory_cache):
581615
assert target.exists()
582616
assert target.read_text() == content
583617

618+
def test_copy_exceptions(self, tmp_path: Path):
619+
source = self.path_file
620+
# target is a directory
621+
target = UPath(tmp_path) / "target-folder"
622+
target.mkdir()
623+
# FIXME: pytest.raises(IsADirectoryError) not working on Windows
624+
with pytest.raises(OSError):
625+
source.copy(target)
626+
# target parent does not exist
627+
target = UPath(tmp_path) / "nonexistent-dir" / "target-file1.txt"
628+
with pytest.raises(FileNotFoundError):
629+
source.copy(target)
630+
631+
def test_copy_into_exceptions(self, tmp_path: Path):
632+
source = self.path_file
633+
# target is not a directory
634+
target_file = UPath(tmp_path) / "target-file.txt"
635+
target_file.write_text("content")
636+
# FIXME: pytest.raises(NotADirectoryError) not working on Windows
637+
with pytest.raises(OSError):
638+
source.copy_into(target_file)
639+
# target dir does not exist
640+
target_dir = UPath(tmp_path) / "nonexistent-dir"
641+
with pytest.raises(FileNotFoundError):
642+
source.copy_into(target_dir)
643+
584644
def test_read_with_fsspec(self):
585645
p = self.path_file
586646

upath/tests/implementations/test_data.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,8 @@ def test_unlink(self):
253253
# DataPaths can't be deleted
254254
with pytest.raises(UnsupportedOperation):
255255
self.path_file.unlink()
256+
257+
@overrides_base
258+
def test_copy_into__dir_to_str_tempdir(self):
259+
# There are no directories in DataPath
260+
assert not self.path.is_dir()

0 commit comments

Comments
 (0)