diff --git a/upath/core.py b/upath/core.py index 7c28ba00..d572a4c9 100644 --- a/upath/core.py +++ b/upath/core.py @@ -1221,10 +1221,17 @@ def copy(self, target: _WT | SupportsPathLike | str, **kwargs: Any) -> _WT | UPa """ Recursively copy this file or directory tree to the given destination. """ - if not isinstance(target, UPath): - return super().copy(self.with_segments(target), **kwargs) - else: - return super().copy(target, **kwargs) + if isinstance(target, str): + proto = get_upath_protocol(target) + if proto != self.protocol: + target = UPath(target) + else: + target = self.with_segments(target) + elif not isinstance(target, UPath): + target = UPath(target) + if target.is_dir(): + raise IsADirectoryError(str(target)) + return super().copy(target, **kwargs) @overload def copy_into(self, target_dir: _WT, **kwargs: Any) -> _WT: ... @@ -1238,10 +1245,19 @@ def copy_into( """ Copy this file or directory tree into the given existing directory. """ - if not isinstance(target_dir, UPath): - return super().copy_into(self.with_segments(target_dir), **kwargs) - else: - return super().copy_into(target_dir, **kwargs) + if isinstance(target_dir, str): + proto = get_upath_protocol(target_dir) + if proto != self.protocol: + target_dir = UPath(target_dir) + else: + target_dir = self.with_segments(target_dir) + elif not isinstance(target_dir, UPath): + target_dir = UPath(target_dir) + if not target_dir.exists(): + raise FileNotFoundError(str(target_dir)) + if not target_dir.is_dir(): + raise NotADirectoryError(str(target_dir)) + return super().copy_into(target_dir, **kwargs) @overload def move(self, target: _WT, **kwargs: Any) -> _WT: ... @@ -1274,8 +1290,15 @@ def move_into( 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 + elif isinstance(target_dir, PurePath): + target = UPath(target_dir, name) else: target = self.with_segments(target_dir, name) + td = target.parent + if not td.exists(): + raise FileNotFoundError(str(td)) + elif not td.is_dir(): + raise NotADirectoryError(str(td)) return self.move(target) def _copy_from( diff --git a/upath/implementations/local.py b/upath/implementations/local.py index 739c78aa..3bf3ec76 100644 --- a/upath/implementations/local.py +++ b/upath/implementations/local.py @@ -21,6 +21,7 @@ from upath._chain import ChainSegment from upath._chain import FSSpecChainParser from upath._protocol import compatible_protocol +from upath._protocol import get_upath_protocol from upath.core import UnsupportedOperation from upath.core import UPath from upath.core import _UPathMixin @@ -377,10 +378,17 @@ def copy( # hacky workaround for missing pathlib.Path.copy in python < 3.14 # todo: revisit _copy: Any = ReadablePath.copy.__get__(self) - if not isinstance(target, UPath): - return _copy(self.with_segments(str(target)), **kwargs) - else: - return _copy(target, **kwargs) + if isinstance(target, str): + proto = get_upath_protocol(target) + if proto != self.protocol: + target = UPath(target) + else: + target = self.with_segments(target) + elif not isinstance(target, UPath): + target = UPath(target) + if target.is_dir(): + raise IsADirectoryError(str(target)) + return _copy(target, **kwargs) @overload def copy_into(self, target_dir: _WT, **kwargs: Any) -> _WT: ... @@ -398,10 +406,15 @@ def copy_into( # hacky workaround for missing pathlib.Path.copy_into in python < 3.14 # todo: revisit _copy_into: Any = ReadablePath.copy_into.__get__(self) - if not isinstance(target_dir, UPath): - return _copy_into(self.with_segments(str(target_dir)), **kwargs) - else: - return _copy_into(target_dir, **kwargs) + if isinstance(target_dir, str): + proto = get_upath_protocol(target_dir) + if proto != self.protocol: + target_dir = UPath(target_dir) + else: + target_dir = self.with_segments(target_dir) + elif not isinstance(target_dir, UPath): + target_dir = UPath(target_dir) + return _copy_into(target_dir, **kwargs) @overload def move(self, target: _WT, **kwargs: Any) -> _WT: ... @@ -432,8 +445,15 @@ def move_into( raise ValueError(f"{self!r} has an empty name") elif hasattr(target_dir, "with_segments"): target = target_dir.with_segments(str(target_dir), name) # type: ignore + elif isinstance(target_dir, pathlib.PurePath): + target = UPath(target_dir, name) else: target = self.with_segments(str(target_dir), name) + td = target.parent + if not td.exists(): + raise FileNotFoundError(str(td)) + elif not td.is_dir(): + raise NotADirectoryError(str(td)) return self.move(target) @property diff --git a/upath/tests/cases.py b/upath/tests/cases.py index dddb9852..22587fa9 100644 --- a/upath/tests/cases.py +++ b/upath/tests/cases.py @@ -551,6 +551,40 @@ def test_copy_local(self, tmp_path: Path): assert target.exists() assert target.read_text() == content + @pytest.mark.parametrize("target_type", [str, Path, UPath]) + def test_copy_into__file_to_str_tempdir(self, tmp_path: Path, target_type): + tmp_path = tmp_path.joinpath("somewhere") + tmp_path.mkdir() + target_dir = target_type(tmp_path) + assert isinstance(target_dir, target_type) + + source = self.path_file + source.copy_into(target_dir) + target = tmp_path.joinpath(source.name) + + assert target.exists() + assert target.read_text() == source.read_text() + + @pytest.mark.parametrize("target_type", [str, Path, UPath]) + def test_copy_into__dir_to_str_tempdir(self, tmp_path: Path, target_type): + tmp_path = tmp_path.joinpath("somewhere") + tmp_path.mkdir() + target_dir = target_type(tmp_path) + assert isinstance(target_dir, target_type) + + source_dir = self.path.joinpath("folder1") + assert source_dir.is_dir() + source_dir.copy_into(target_dir) + target = tmp_path.joinpath(source_dir.name) + + assert target.exists() + assert target.is_dir() + for item in source_dir.iterdir(): + target_item = target.joinpath(item.name) + assert target_item.exists() + if item.is_file(): + assert target_item.read_text() == item.read_text() + def test_copy_into_local(self, tmp_path: Path): target_dir = UPath(tmp_path) / "target-dir" target_dir.mkdir() @@ -581,6 +615,32 @@ def test_copy_into_memory(self, clear_fsspec_memory_cache): assert target.exists() assert target.read_text() == content + def test_copy_exceptions(self, tmp_path: Path): + source = self.path_file + # target is a directory + target = UPath(tmp_path) / "target-folder" + target.mkdir() + # FIXME: pytest.raises(IsADirectoryError) not working on Windows + with pytest.raises(OSError): + source.copy(target) + # target parent does not exist + target = UPath(tmp_path) / "nonexistent-dir" / "target-file1.txt" + with pytest.raises(FileNotFoundError): + source.copy(target) + + def test_copy_into_exceptions(self, tmp_path: Path): + source = self.path_file + # target is not a directory + target_file = UPath(tmp_path) / "target-file.txt" + target_file.write_text("content") + # FIXME: pytest.raises(NotADirectoryError) not working on Windows + with pytest.raises(OSError): + source.copy_into(target_file) + # target dir does not exist + target_dir = UPath(tmp_path) / "nonexistent-dir" + with pytest.raises(FileNotFoundError): + source.copy_into(target_dir) + def test_read_with_fsspec(self): p = self.path_file diff --git a/upath/tests/implementations/test_data.py b/upath/tests/implementations/test_data.py index a9e2f4b8..d478559f 100644 --- a/upath/tests/implementations/test_data.py +++ b/upath/tests/implementations/test_data.py @@ -253,3 +253,8 @@ def test_unlink(self): # DataPaths can't be deleted with pytest.raises(UnsupportedOperation): self.path_file.unlink() + + @overrides_base + def test_copy_into__dir_to_str_tempdir(self): + # There are no directories in DataPath + assert not self.path.is_dir()