diff --git a/fsspec/asyn.py b/fsspec/asyn.py index d4e309790..4fce103b0 100644 --- a/fsspec/asyn.py +++ b/fsspec/asyn.py @@ -13,6 +13,12 @@ from .callbacks import _DEFAULT_CALLBACK from .exceptions import FSTimeoutError +from .implementations.local import ( + LocalFileSystem, + make_path_posix, + trailing_sep, + trailing_sep_maybe_asterisk, +) from .spec import AbstractBufferedFile, AbstractFileSystem from .utils import is_exception, other_paths @@ -336,15 +342,23 @@ async def _copy( elif on_error is None: on_error = "raise" + source_is_str = isinstance(path1, str) paths = await self._expand_path(path1, maxdepth=maxdepth, recursive=recursive) + if source_is_str and (not recursive or maxdepth is not None): + # Non-recursive glob does not copy directories + paths = [p for p in paths if not (trailing_sep(p) or await self._isdir(p))] + if not paths: + return + isdir = isinstance(path2, str) and ( - path2.endswith("/") or await self._isdir(path2) + trailing_sep(path2) or await self._isdir(path2) ) path2 = other_paths( paths, path2, - exists=isdir and isinstance(path1, str) and not path1.endswith("/"), + exists=isdir and source_is_str and not trailing_sep_maybe_asterisk(path1), is_dir=isdir, + flatten=not source_is_str, ) batch_size = batch_size or self.batch_size coros = [self._cp_file(p1, p2, **kwargs) for p1, p2 in zip(paths, path2)] @@ -466,6 +480,7 @@ async def _put( recursive=False, callback=_DEFAULT_CALLBACK, batch_size=None, + maxdepth=None, **kwargs, ): """Copy file(s) from local. @@ -481,21 +496,27 @@ async def _put( constructor, or for all instances by setting the "gather_batch_size" key in ``fsspec.config.conf``, falling back to 1/8th of the system limit . """ - from .implementations.local import LocalFileSystem, make_path_posix - - rpath = self._strip_protocol(rpath) - if isinstance(lpath, str): + source_is_str = isinstance(lpath, str) + if source_is_str: lpath = make_path_posix(lpath) fs = LocalFileSystem() - lpaths = fs.expand_path(lpath, recursive=recursive) + lpaths = fs.expand_path(lpath, recursive=recursive, maxdepth=maxdepth) + if source_is_str and (not recursive or maxdepth is not None): + # Non-recursive glob does not copy directories + lpaths = [p for p in lpaths if not (trailing_sep(p) or fs.isdir(p))] + if not lpaths: + return + isdir = isinstance(rpath, str) and ( - rpath.endswith("/") or await self._isdir(rpath) + trailing_sep(rpath) or await self._isdir(rpath) ) + rpath = self._strip_protocol(rpath) rpaths = other_paths( lpaths, rpath, - exists=isdir and isinstance(lpath, str) and not lpath.endswith("/"), + exists=isdir and source_is_str and not trailing_sep_maybe_asterisk(lpath), is_dir=isdir, + flatten=not source_is_str, ) is_dir = {l: os.path.isdir(l) for l in lpaths} @@ -519,7 +540,13 @@ async def _get_file(self, rpath, lpath, **kwargs): raise NotImplementedError async def _get( - self, rpath, lpath, recursive=False, callback=_DEFAULT_CALLBACK, **kwargs + self, + rpath, + lpath, + recursive=False, + callback=_DEFAULT_CALLBACK, + maxdepth=None, + **kwargs, ): """Copy file(s) to local. @@ -535,21 +562,31 @@ async def _get( constructor, or for all instances by setting the "gather_batch_size" key in ``fsspec.config.conf``, falling back to 1/8th of the system limit . """ - from fsspec.implementations.local import LocalFileSystem, make_path_posix - + source_is_str = isinstance(rpath, str) # First check for rpath trailing slash as _strip_protocol removes it. - rpath_trailing_slash = isinstance(rpath, str) and rpath.endswith("/") + source_not_trailing_sep = source_is_str and not trailing_sep_maybe_asterisk( + rpath + ) rpath = self._strip_protocol(rpath) - lpath = make_path_posix(lpath) rpaths = await self._expand_path(rpath, recursive=recursive) + if source_is_str and (not recursive or maxdepth is not None): + # Non-recursive glob does not copy directories + rpaths = [ + p for p in rpaths if not (trailing_sep(p) or await self._isdir(p)) + ] + if not rpaths: + return + + lpath = make_path_posix(lpath) isdir = isinstance(lpath, str) and ( - lpath.endswith("/") or LocalFileSystem().isdir(lpath) + trailing_sep(lpath) or LocalFileSystem().isdir(lpath) ) lpaths = other_paths( rpaths, lpath, - exists=isdir and not rpath_trailing_slash, + exists=isdir and source_not_trailing_sep, is_dir=isdir, + flatten=not source_is_str, ) [os.makedirs(os.path.dirname(lp), exist_ok=True) for lp in lpaths] batch_size = kwargs.pop("batch_size", self.batch_size) @@ -766,9 +803,16 @@ async def _expand_path(self, path, recursive=False, maxdepth=None): bit = set(await self._glob(p)) out |= bit if recursive: + # glob call above expanded one depth so if maxdepth is defined + # then decrement it in expand_path call below. If it is zero + # after decrementing then avoid expand_path call. + if maxdepth is not None and maxdepth <= 1: + continue out |= set( await self._expand_path( - list(bit), recursive=recursive, maxdepth=maxdepth + list(bit), + recursive=recursive, + maxdepth=maxdepth - 1 if maxdepth is not None else None, ) ) continue @@ -778,8 +822,6 @@ async def _expand_path(self, path, recursive=False, maxdepth=None): if p not in out and (recursive is False or (await self._exists(p))): # should only check once, for the root out.add(p) - # reduce depth on each recursion level unless None or 0 - maxdepth = maxdepth if not maxdepth else maxdepth - 1 if not out: raise FileNotFoundError(path) return list(sorted(out)) diff --git a/fsspec/implementations/tests/local/local_fixtures.py b/fsspec/implementations/tests/local/local_fixtures.py index aec1d2976..d850fcf5f 100644 --- a/fsspec/implementations/tests/local/local_fixtures.py +++ b/fsspec/implementations/tests/local/local_fixtures.py @@ -5,12 +5,10 @@ class LocalFixtures(AbstractFixtures): - @staticmethod - @pytest.fixture - def fs(): + @pytest.fixture(scope="class") + def fs(self): return LocalFileSystem(auto_mkdir=True) - @staticmethod @pytest.fixture - def fs_path(tmpdir): + def fs_path(self, tmpdir): return str(tmpdir) diff --git a/fsspec/implementations/tests/memory/memory_fixtures.py b/fsspec/implementations/tests/memory/memory_fixtures.py index d30521221..27d025267 100644 --- a/fsspec/implementations/tests/memory/memory_fixtures.py +++ b/fsspec/implementations/tests/memory/memory_fixtures.py @@ -5,9 +5,8 @@ class MemoryFixtures(AbstractFixtures): - @staticmethod - @pytest.fixture - def fs(): + @pytest.fixture(scope="class") + def fs(self): m = filesystem("memory") m.store.clear() m.pseudo_dirs.clear() @@ -19,12 +18,10 @@ def fs(): m.pseudo_dirs.clear() m.pseudo_dirs.append("") - @staticmethod @pytest.fixture - def fs_join(): + def fs_join(self): return lambda *args: "/".join(args) - @staticmethod @pytest.fixture - def fs_path(): + def fs_path(self): return "" diff --git a/fsspec/implementations/tests/memory/memory_test.py b/fsspec/implementations/tests/memory/memory_test.py index fd0ebaac8..98bf063ef 100644 --- a/fsspec/implementations/tests/memory/memory_test.py +++ b/fsspec/implementations/tests/memory/memory_test.py @@ -1,3 +1,5 @@ +import pytest + import fsspec.tests.abstract as abstract from fsspec.implementations.tests.memory.memory_fixtures import MemoryFixtures @@ -7,7 +9,21 @@ class TestMemoryCopy(abstract.AbstractCopyTests, MemoryFixtures): class TestMemoryGet(abstract.AbstractGetTests, MemoryFixtures): - pass + @pytest.mark.skip(reason="Bug: does not auto-create new directory") + def test_get_file_to_new_directory(self): + pass + + @pytest.mark.skip(reason="Bug: does not auto-create new directory") + def test_get_file_to_file_in_new_directory(self): + pass + + @pytest.mark.skip(reason="Bug: does not auto-create new directory") + def test_get_glob_to_new_directory(self): + pass + + @pytest.mark.skip(reason="Bug: does not auto-create new directory") + def test_get_list_of_files_to_new_directory(self): + pass class TestMemoryPut(abstract.AbstractPutTests, MemoryFixtures): diff --git a/fsspec/implementations/tests/test_memory.py b/fsspec/implementations/tests/test_memory.py index 7e734da5f..bb6fd4bfb 100644 --- a/fsspec/implementations/tests/test_memory.py +++ b/fsspec/implementations/tests/test_memory.py @@ -23,19 +23,6 @@ def test_strip(m): assert m._strip_protocol("/b/c/") == "/b/c" -def test_put_single(m, tmpdir): - fn = os.path.join(str(tmpdir), "dir") - os.mkdir(fn) - open(os.path.join(fn, "abc"), "w").write("text") - m.put(fn, "/test") # no-op, no files - assert m.isdir("/test") - assert not m.exists("/test/abc") - assert not m.exists("/test/dir") - m.put(fn, "/test", recursive=True) - assert m.isdir("/test/dir") - assert m.cat("/test/dir/abc") == b"text" - - def test_ls(m): m.mkdir("/dir") m.mkdir("/dir/dir1") diff --git a/fsspec/registry.py b/fsspec/registry.py index 7527f0a85..c6ef1b08f 100644 --- a/fsspec/registry.py +++ b/fsspec/registry.py @@ -197,7 +197,7 @@ def register_implementation(name, cls, clobber=False, errtxt=None): "box": { "class": "boxfs.BoxFileSystem", "err": "Please install boxfs to access BoxFileSystem", - } + }, } diff --git a/fsspec/spec.py b/fsspec/spec.py index 493faf4ee..b3918722b 100644 --- a/fsspec/spec.py +++ b/fsspec/spec.py @@ -976,23 +976,23 @@ def put( trailing_sep_maybe_asterisk, ) - if isinstance(lpath, str): + source_is_str = isinstance(lpath, str) + if source_is_str: lpath = make_path_posix(lpath) fs = LocalFileSystem() - source_is_str = isinstance(lpath, str) lpaths = fs.expand_path(lpath, recursive=recursive, maxdepth=maxdepth) if source_is_str and (not recursive or maxdepth is not None): # Non-recursive glob does not copy directories - lpaths = [p for p in lpaths if not (trailing_sep(p) or self.isdir(p))] + lpaths = [p for p in lpaths if not (trailing_sep(p) or fs.isdir(p))] if not lpaths: return + isdir = isinstance(rpath, str) and (trailing_sep(rpath) or self.isdir(rpath)) rpath = ( self._strip_protocol(rpath) if isinstance(rpath, str) else [self._strip_protocol(p) for p in rpath] ) - isdir = isinstance(rpath, str) and (trailing_sep(rpath) or self.isdir(rpath)) rpaths = other_paths( lpaths, rpath, diff --git a/fsspec/tests/abstract/__init__.py b/fsspec/tests/abstract/__init__.py index 4301ac0d7..d2bc1627d 100644 --- a/fsspec/tests/abstract/__init__.py +++ b/fsspec/tests/abstract/__init__.py @@ -8,23 +8,63 @@ from fsspec.tests.abstract.put import AbstractPutTests # noqa -class AbstractFixtures: - @staticmethod +class BaseAbstractFixtures: + """ + Abstract base class containing fixtures that are used by but never need to + be overridden in derived filesystem-specific classes to run the abstract + tests on such filesystems. + """ + @pytest.fixture - def fs_join(): + def fs_bulk_operations_scenario_0(self, fs, fs_join, fs_path): """ - Return a function that joins its arguments together into a path. + Scenario on remote filesystem that is used for many cp/get/put tests. - Most fsspec implementations join paths in a platform-dependent way, - but some will override this to always use a forward slash. + Cleans up at the end of each test it which it is used. """ - return os.path.join + source = self._bulk_operations_scenario_0(fs, fs_join, fs_path) + yield source + fs.rm(source, recursive=True) - @staticmethod @pytest.fixture - def fs_scenario_cp(fs, fs_join, fs_path): + def fs_target(self, fs, fs_join, fs_path): """ - Scenario on remote filesystem that is used for many cp/get/put tests. + Return name of remote directory that does not yet exist to copy into. + + Cleans up at the end of each test it which it is used. + """ + target = fs_join(fs_path, "target") + yield target + if fs.exists(target): + fs.rm(target, recursive=True) + + @pytest.fixture + def local_bulk_operations_scenario_0(self, local_fs, local_join, local_path): + """ + Scenario on local filesystem that is used for many cp/get/put tests. + + Cleans up at the end of each test it which it is used. + """ + source = self._bulk_operations_scenario_0(local_fs, local_join, local_path) + yield source + local_fs.rm(source, recursive=True) + + @pytest.fixture + def local_target(self, local_fs, local_join, local_path): + """ + Return name of local directory that does not yet exist to copy into. + + Cleans up at the end of each test it which it is used. + """ + target = local_join(local_path, "target") + yield target + if local_fs.exists(target): + local_fs.rm(target, recursive=True) + + def _bulk_operations_scenario_0(self, some_fs, some_join, some_path): + """ + Scenario that is used for many cp/get/put tests. Creates the following + directory and file structure: 📁 source ├── 📄 file1 @@ -35,36 +75,62 @@ def fs_scenario_cp(fs, fs_join, fs_path): └── 📁 nesteddir └── 📄 nestedfile """ - source = fs_join(fs_path, "source") - subdir = fs_join(source, "subdir") - nesteddir = fs_join(subdir, "nesteddir") - fs.makedirs(nesteddir) - fs.touch(fs_join(source, "file1")) - fs.touch(fs_join(source, "file2")) - fs.touch(fs_join(subdir, "subfile1")) - fs.touch(fs_join(subdir, "subfile2")) - fs.touch(fs_join(nesteddir, "nestedfile")) + source = some_join(some_path, "source") + subdir = some_join(source, "subdir") + nesteddir = some_join(subdir, "nesteddir") + some_fs.makedirs(nesteddir) + some_fs.touch(some_join(source, "file1")) + some_fs.touch(some_join(source, "file2")) + some_fs.touch(some_join(subdir, "subfile1")) + some_fs.touch(some_join(subdir, "subfile2")) + some_fs.touch(some_join(nesteddir, "nestedfile")) return source - @staticmethod + +class AbstractFixtures(BaseAbstractFixtures): + """ + Abstract base class containing fixtures that may be overridden in derived + filesystem-specific classes to run the abstract tests on such filesystems. + + For any particular filesystem some of these fixtures must be overridden, + such as ``fs`` and ``fs_path``, and others may be overridden if the + default functions here are not appropriate, such as ``fs_join``. + """ + + @pytest.fixture + def fs(self): + raise NotImplementedError("This function must be overridden in derived classes") + + @pytest.fixture + def fs_join(self): + """ + Return a function that joins its arguments together into a path. + + Most fsspec implementations join paths in a platform-dependent way, + but some will override this to always use a forward slash. + """ + return os.path.join + @pytest.fixture - def local_fs(): + def fs_path(self): + raise NotImplementedError("This function must be overridden in derived classes") + + @pytest.fixture(scope="class") + def local_fs(self): # Maybe need an option for auto_mkdir=False? This is only relevant # for certain implementations. return LocalFileSystem(auto_mkdir=True) - @staticmethod @pytest.fixture - def local_join(): + def local_join(self): """ Return a function that joins its arguments together into a path, on the local filesystem. """ return os.path.join - @staticmethod @pytest.fixture - def local_path(tmpdir): + def local_path(self, tmpdir): return tmpdir def supports_empty_directories(self): diff --git a/fsspec/tests/abstract/copy.py b/fsspec/tests/abstract/copy.py index eb283649b..6498fd215 100644 --- a/fsspec/tests/abstract/copy.py +++ b/fsspec/tests/abstract/copy.py @@ -1,13 +1,14 @@ class AbstractCopyTests: def test_copy_file_to_existing_directory( - self, fs, fs_join, fs_path, fs_scenario_cp + self, fs, fs_join, fs_bulk_operations_scenario_0, fs_target ): # Copy scenario 1a - source = fs_scenario_cp + source = fs_bulk_operations_scenario_0 - target = fs_join(fs_path, "target") + target = fs_target fs.mkdir(target) if not self.supports_empty_directories(): + # Force target directory to exist by adding a dummy file fs.touch(fs_join(target, "dummy")) assert fs.isdir(target) @@ -35,11 +36,13 @@ def test_copy_file_to_existing_directory( fs.cp(fs_join(source, "subdir", "subfile1"), target + "/") assert fs.isfile(target_subfile1) - def test_copy_file_to_new_directory(self, fs, fs_join, fs_path, fs_scenario_cp): + def test_copy_file_to_new_directory( + self, fs, fs_join, fs_bulk_operations_scenario_0, fs_target + ): # Copy scenario 1b - source = fs_scenario_cp + source = fs_bulk_operations_scenario_0 - target = fs_join(fs_path, "target") + target = fs_target fs.mkdir(target) fs.cp( @@ -50,24 +53,24 @@ def test_copy_file_to_new_directory(self, fs, fs_join, fs_path, fs_scenario_cp): assert fs.isfile(fs_join(target, "newdir", "subfile1")) def test_copy_file_to_file_in_existing_directory( - self, fs, fs_join, fs_path, fs_scenario_cp + self, fs, fs_join, fs_bulk_operations_scenario_0, fs_target ): # Copy scenario 1c - source = fs_scenario_cp + source = fs_bulk_operations_scenario_0 - target = fs_join(fs_path, "target") + target = fs_target fs.mkdir(target) fs.cp(fs_join(source, "subdir", "subfile1"), fs_join(target, "newfile")) assert fs.isfile(fs_join(target, "newfile")) def test_copy_file_to_file_in_new_directory( - self, fs, fs_join, fs_path, fs_scenario_cp + self, fs, fs_join, fs_bulk_operations_scenario_0, fs_target ): # Copy scenario 1d - source = fs_scenario_cp + source = fs_bulk_operations_scenario_0 - target = fs_join(fs_path, "target") + target = fs_target fs.mkdir(target) fs.cp( @@ -77,13 +80,18 @@ def test_copy_file_to_file_in_new_directory( assert fs.isfile(fs_join(target, "newdir", "newfile")) def test_copy_directory_to_existing_directory( - self, fs, fs_join, fs_path, fs_scenario_cp + self, fs, fs_join, fs_bulk_operations_scenario_0, fs_target ): # Copy scenario 1e - source = fs_scenario_cp + source = fs_bulk_operations_scenario_0 - target = fs_join(fs_path, "target") + target = fs_target fs.mkdir(target) + if not self.supports_empty_directories(): + # Force target directory to exist by adding a dummy file + dummy = fs_join(target, "dummy") + fs.touch(dummy) + assert fs.isdir(target) for source_slash, target_slash in zip([False, True], [False, True]): s = fs_join(source, "subdir") @@ -93,7 +101,7 @@ def test_copy_directory_to_existing_directory( # Without recursive does nothing fs.cp(s, t) - assert fs.ls(target) == [] + assert fs.ls(target) == [] if self.supports_empty_directories() else [dummy] # With recursive fs.cp(s, t, recursive=True) @@ -113,7 +121,7 @@ def test_copy_directory_to_existing_directory( assert fs.isfile(fs_join(target, "subdir", "nesteddir", "nestedfile")) fs.rm(fs_join(target, "subdir"), recursive=True) - assert fs.ls(target) == [] + assert fs.ls(target) == [] if self.supports_empty_directories() else [dummy] # Limit recursive by maxdepth fs.cp(s, t, recursive=True, maxdepth=1) @@ -131,15 +139,15 @@ def test_copy_directory_to_existing_directory( assert not fs.exists(fs_join(target, "subdir", "nesteddir")) fs.rm(fs_join(target, "subdir"), recursive=True) - assert fs.ls(target) == [] + assert fs.ls(target) == [] if self.supports_empty_directories() else [dummy] def test_copy_directory_to_new_directory( - self, fs, fs_join, fs_path, fs_scenario_cp + self, fs, fs_join, fs_bulk_operations_scenario_0, fs_target ): # Copy scenario 1f - source = fs_scenario_cp + source = fs_bulk_operations_scenario_0 - target = fs_join(fs_path, "target") + target = fs_target fs.mkdir(target) for source_slash, target_slash in zip([False, True], [False, True]): @@ -164,7 +172,7 @@ def test_copy_directory_to_new_directory( assert not fs.exists(fs_join(target, "subdir")) fs.rm(fs_join(target, "newdir"), recursive=True) - assert fs.ls(target) == [] + assert not fs.exists(fs_join(target, "newdir")) # Limit recursive by maxdepth fs.cp(s, t, recursive=True, maxdepth=1) @@ -175,15 +183,15 @@ def test_copy_directory_to_new_directory( assert not fs.exists(fs_join(target, "subdir")) fs.rm(fs_join(target, "newdir"), recursive=True) - assert fs.ls(target) == [] + assert not fs.exists(fs_join(target, "newdir")) def test_copy_glob_to_existing_directory( - self, fs, fs_join, fs_path, fs_scenario_cp + self, fs, fs_join, fs_bulk_operations_scenario_0, fs_target ): # Copy scenario 1g - source = fs_scenario_cp + source = fs_bulk_operations_scenario_0 - target = fs_join(fs_path, "target") + target = fs_target fs.mkdir(target) for target_slash in [False, True]: @@ -221,11 +229,13 @@ def test_copy_glob_to_existing_directory( fs.rm(fs.ls(target, detail=False), recursive=True) assert fs.ls(target) == [] - def test_copy_glob_to_new_directory(self, fs, fs_join, fs_path, fs_scenario_cp): + def test_copy_glob_to_new_directory( + self, fs, fs_join, fs_bulk_operations_scenario_0, fs_target + ): # Copy scenario 1h - source = fs_scenario_cp + source = fs_bulk_operations_scenario_0 - target = fs_join(fs_path, "target") + target = fs_target fs.mkdir(target) for target_slash in [False, True]: @@ -244,7 +254,7 @@ def test_copy_glob_to_new_directory(self, fs, fs_join, fs_path, fs_scenario_cp): assert not fs.exists(fs_join(target, "newdir", "subdir")) fs.rm(fs_join(target, "newdir"), recursive=True) - assert fs.ls(target) == [] + assert not fs.exists(fs_join(target, "newdir")) # With recursive fs.cp(fs_join(source, "subdir", "*"), t, recursive=True) @@ -257,7 +267,7 @@ def test_copy_glob_to_new_directory(self, fs, fs_join, fs_path, fs_scenario_cp): assert not fs.exists(fs_join(target, "newdir", "subdir")) fs.rm(fs_join(target, "newdir"), recursive=True) - assert fs.ls(target) == [] + assert not fs.exists(fs_join(target, "newdir")) # Limit recursive by maxdepth fs.cp(fs_join(source, "subdir", "*"), t, recursive=True, maxdepth=1) @@ -269,16 +279,21 @@ def test_copy_glob_to_new_directory(self, fs, fs_join, fs_path, fs_scenario_cp): assert not fs.exists(fs_join(target, "newdir", "subdir")) fs.rm(fs.ls(target, detail=False), recursive=True) - assert fs.ls(target) == [] + assert not fs.exists(fs_join(target, "newdir")) def test_copy_list_of_files_to_existing_directory( - self, fs, fs_join, fs_path, fs_scenario_cp + self, fs, fs_join, fs_bulk_operations_scenario_0, fs_target ): # Copy scenario 2a - source = fs_scenario_cp + source = fs_bulk_operations_scenario_0 - target = fs_join(fs_path, "target") + target = fs_target fs.mkdir(target) + if not self.supports_empty_directories(): + # Force target directory to exist by adding a dummy file + dummy = fs_join(target, "dummy") + fs.touch(dummy) + assert fs.isdir(target) source_files = [ fs_join(source, "file1"), @@ -295,15 +310,15 @@ def test_copy_list_of_files_to_existing_directory( assert fs.isfile(fs_join(target, "subfile1")) fs.rm(fs.find(target)) - assert fs.ls(target) == [] + assert fs.ls(target) == [] if self.supports_empty_directories() else [dummy] def test_copy_list_of_files_to_new_directory( - self, fs, fs_join, fs_path, fs_scenario_cp + self, fs, fs_join, fs_bulk_operations_scenario_0, fs_target ): # Copy scenario 2b - source = fs_scenario_cp + source = fs_bulk_operations_scenario_0 - target = fs_join(fs_path, "target") + target = fs_target fs.mkdir(target) source_files = [ @@ -318,12 +333,14 @@ def test_copy_list_of_files_to_new_directory( assert fs.isfile(fs_join(target, "newdir", "file2")) assert fs.isfile(fs_join(target, "newdir", "subfile1")) - def test_copy_two_files_new_directory(self, fs, fs_join, fs_path, fs_scenario_cp): + def test_copy_two_files_new_directory( + self, fs, fs_join, fs_bulk_operations_scenario_0, fs_target + ): # This is a duplicate of test_copy_list_of_files_to_new_directory and # can eventually be removed. - source = fs_scenario_cp + source = fs_bulk_operations_scenario_0 - target = fs_join(fs_path, "target") + target = fs_target assert not fs.exists(target) fs.cp([fs_join(source, "file1"), fs_join(source, "file2")], target) diff --git a/fsspec/tests/abstract/get.py b/fsspec/tests/abstract/get.py index 992e9c6ae..baa9aa4a9 100644 --- a/fsspec/tests/abstract/get.py +++ b/fsspec/tests/abstract/get.py @@ -1,6 +1,347 @@ class AbstractGetTests: + def test_get_file_to_existing_directory( + self, + fs, + fs_join, + fs_bulk_operations_scenario_0, + local_fs, + local_join, + local_target, + ): + # Copy scenario 1a + source = fs_bulk_operations_scenario_0 + + target = local_target + local_fs.mkdir(target) + assert local_fs.isdir(target) + + target_file2 = local_join(target, "file2") + target_subfile1 = local_join(target, "subfile1") + + # Copy from source directory + fs.get(fs_join(source, "file2"), target) + assert local_fs.isfile(target_file2) + + # Copy from sub directory + fs.get(fs_join(source, "subdir", "subfile1"), target) + assert local_fs.isfile(target_subfile1) + + # Remove copied files + local_fs.rm([target_file2, target_subfile1]) + assert not local_fs.exists(target_file2) + assert not local_fs.exists(target_subfile1) + + # Repeat with trailing slash on target + fs.get(fs_join(source, "file2"), target + "/") + assert local_fs.isdir(target) + assert local_fs.isfile(target_file2) + + fs.get(fs_join(source, "subdir", "subfile1"), target + "/") + assert local_fs.isfile(target_subfile1) + + def test_get_file_to_new_directory( + self, + fs, + fs_join, + fs_bulk_operations_scenario_0, + local_fs, + local_join, + local_target, + ): + # Copy scenario 1b + source = fs_bulk_operations_scenario_0 + + target = local_target + local_fs.mkdir(target) + + fs.get( + fs_join(source, "subdir", "subfile1"), local_join(target, "newdir/") + ) # Note trailing slash + + assert local_fs.isdir(target) + assert local_fs.isdir(local_join(target, "newdir")) + assert local_fs.isfile(local_join(target, "newdir", "subfile1")) + + def test_get_file_to_file_in_existing_directory( + self, + fs, + fs_join, + fs_path, + fs_bulk_operations_scenario_0, + local_fs, + local_join, + local_target, + ): + # Copy scenario 1c + source = fs_bulk_operations_scenario_0 + + target = local_target + local_fs.mkdir(target) + + fs.get(fs_join(source, "subdir", "subfile1"), local_join(target, "newfile")) + assert local_fs.isfile(local_join(target, "newfile")) + + def test_get_file_to_file_in_new_directory( + self, + fs, + fs_join, + fs_bulk_operations_scenario_0, + local_fs, + local_join, + local_target, + ): + # Copy scenario 1d + source = fs_bulk_operations_scenario_0 + + target = local_target + local_fs.mkdir(target) + + fs.get( + fs_join(source, "subdir", "subfile1"), + local_join(target, "newdir", "newfile"), + ) + assert local_fs.isdir(local_join(target, "newdir")) + assert local_fs.isfile(local_join(target, "newdir", "newfile")) + + def test_get_directory_to_existing_directory( + self, + fs, + fs_join, + fs_bulk_operations_scenario_0, + local_fs, + local_join, + local_target, + ): + # Copy scenario 1e + source = fs_bulk_operations_scenario_0 + + target = local_target + local_fs.mkdir(target) + + for source_slash, target_slash in zip([False, True], [False, True]): + s = fs_join(source, "subdir") + if source_slash: + s += "/" + t = target + "/" if target_slash else target + + # Without recursive does nothing + # ERROR: erroneously creates new directory + # fs.get(s, t) + # assert fs.ls(target) == [] + + # With recursive + fs.get(s, t, recursive=True) + if source_slash: + assert local_fs.isfile(local_join(target, "subfile1")) + assert local_fs.isfile(local_join(target, "subfile2")) + assert local_fs.isdir(local_join(target, "nesteddir")) + assert local_fs.isfile(local_join(target, "nesteddir", "nestedfile")) + + local_fs.rm( + [ + local_join(target, "subfile1"), + local_join(target, "subfile2"), + local_join(target, "nesteddir"), + ], + recursive=True, + ) + else: + assert local_fs.isdir(local_join(target, "subdir")) + assert local_fs.isfile(local_join(target, "subdir", "subfile1")) + assert local_fs.isfile(local_join(target, "subdir", "subfile2")) + assert local_fs.isdir(local_join(target, "subdir", "nesteddir")) + assert local_fs.isfile( + local_join(target, "subdir", "nesteddir", "nestedfile") + ) + + local_fs.rm(local_join(target, "subdir"), recursive=True) + assert local_fs.ls(target) == [] + + # Limit by maxdepth + # ERROR: maxdepth ignored here + + def test_get_directory_to_new_directory( + self, + fs, + fs_join, + fs_bulk_operations_scenario_0, + local_fs, + local_join, + local_target, + ): + # Copy scenario 1f + source = fs_bulk_operations_scenario_0 + + target = local_target + local_fs.mkdir(target) + + for source_slash, target_slash in zip([False, True], [False, True]): + s = fs_join(source, "subdir") + if source_slash: + s += "/" + t = local_join(target, "newdir") + if target_slash: + t += "/" + + # Without recursive does nothing + # ERROR: erroneously creates new directory + # fs.get(s, t) + # assert fs.ls(target) == [] + + # With recursive + fs.get(s, t, recursive=True) + assert local_fs.isdir(local_join(target, "newdir")) + assert local_fs.isfile(local_join(target, "newdir", "subfile1")) + assert local_fs.isfile(local_join(target, "newdir", "subfile2")) + assert local_fs.isdir(local_join(target, "newdir", "nesteddir")) + assert local_fs.isfile( + local_join(target, "newdir", "nesteddir", "nestedfile") + ) + + local_fs.rm(local_join(target, "newdir"), recursive=True) + assert local_fs.ls(target) == [] + + # Limit by maxdepth + # ERROR: maxdepth ignored here + + def test_get_glob_to_existing_directory( + self, + fs, + fs_join, + fs_bulk_operations_scenario_0, + local_fs, + local_join, + local_target, + ): + # Copy scenario 1g + source = fs_bulk_operations_scenario_0 + + target = local_target + local_fs.mkdir(target) + + # for target_slash in [False, True]: + for target_slash in [False]: + t = target + "/" if target_slash else target + + # Without recursive + fs.get(fs_join(source, "subdir", "*"), t) + assert local_fs.isfile(local_join(target, "subfile1")) + assert local_fs.isfile(local_join(target, "subfile2")) + # assert not local_fs.isdir(local_join(target, "nesteddir")) # ERROR + assert not local_fs.isdir(local_join(target, "subdir")) + + # With recursive + + # Limit by maxdepth + + def test_get_glob_to_new_directory( + self, + fs, + fs_join, + fs_bulk_operations_scenario_0, + local_fs, + local_join, + local_target, + ): + # Copy scenario 1h + source = fs_bulk_operations_scenario_0 + + target = local_target + local_fs.mkdir(target) + + for target_slash in [False, True]: + t = fs_join(target, "newdir") + if target_slash: + t += "/" + + # Without recursive + fs.get(fs_join(source, "subdir", "*"), t) + assert local_fs.isdir(local_join(target, "newdir")) + assert local_fs.isfile(local_join(target, "newdir", "subfile1")) + assert local_fs.isfile(local_join(target, "newdir", "subfile2")) + # ERROR - do not copy empty directory + # assert not local_fs.exists(local_join(target, "newdir", "nesteddir")) + + local_fs.rm(local_join(target, "newdir"), recursive=True) + assert local_fs.ls(target) == [] + + # With recursive + fs.get(fs_join(source, "subdir", "*"), t, recursive=True) + assert local_fs.isdir(local_join(target, "newdir")) + assert local_fs.isfile(local_join(target, "newdir", "subfile1")) + assert local_fs.isfile(local_join(target, "newdir", "subfile2")) + assert local_fs.isdir(local_join(target, "newdir", "nesteddir")) + assert local_fs.isfile( + local_join(target, "newdir", "nesteddir", "nestedfile") + ) + + local_fs.rm(local_join(target, "newdir"), recursive=True) + assert local_fs.ls(target) == [] + + # Limit by maxdepth + # ERROR: this is not correct + + def test_get_list_of_files_to_existing_directory( + self, + fs, + fs_join, + fs_bulk_operations_scenario_0, + local_fs, + local_join, + local_target, + ): + # Copy scenario 2a + source = fs_bulk_operations_scenario_0 + + target = local_target + local_fs.mkdir(target) + + source_files = [ + fs_join(source, "file1"), + fs_join(source, "file2"), + fs_join(source, "subdir", "subfile1"), + ] + + for target_slash in [False, True]: + t = target + "/" if target_slash else target + + fs.get(source_files, t) + assert local_fs.isfile(local_join(target, "file1")) + assert local_fs.isfile(local_join(target, "file2")) + assert local_fs.isfile(local_join(target, "subfile1")) + + local_fs.rm(local_fs.find(target)) + assert local_fs.ls(target) == [] + + def test_get_list_of_files_to_new_directory( + self, + fs, + fs_join, + fs_bulk_operations_scenario_0, + local_fs, + local_join, + local_target, + ): + # Copy scenario 2b + source = fs_bulk_operations_scenario_0 + + target = local_target + local_fs.mkdir(target) + + source_files = [ + fs_join(source, "file1"), + fs_join(source, "file2"), + fs_join(source, "subdir", "subfile1"), + ] + + fs.get(source_files, local_join(target, "newdir") + "/") # Note trailing slash + assert local_fs.isdir(local_join(target, "newdir")) + assert local_fs.isfile(local_join(target, "newdir", "file1")) + assert local_fs.isfile(local_join(target, "newdir", "file2")) + assert local_fs.isfile(local_join(target, "newdir", "subfile1")) + def test_get_directory_recursive( - self, fs, fs_join, fs_path, local_fs, local_join, local_path + self, fs, fs_join, fs_path, local_fs, local_join, local_target ): # https://github.com/fsspec/filesystem_spec/issues/1062 # Recursive cp/get/put of source directory into non-existent target directory. @@ -9,7 +350,7 @@ def test_get_directory_recursive( fs.mkdir(src) fs.touch(src_file) - target = local_join(local_path, "target") + target = local_target # get without slash assert not local_fs.exists(target) diff --git a/fsspec/tests/abstract/put.py b/fsspec/tests/abstract/put.py index cdb76a8b7..d06f9d9b5 100644 --- a/fsspec/tests/abstract/put.py +++ b/fsspec/tests/abstract/put.py @@ -1,6 +1,367 @@ class AbstractPutTests: + def test_put_file_to_existing_directory( + self, + fs, + fs_join, + fs_target, + local_join, + local_bulk_operations_scenario_0, + ): + # Copy scenario 1a + source = local_bulk_operations_scenario_0 + + target = fs_target + fs.mkdir(target) + if not self.supports_empty_directories(): + # Force target directory to exist by adding a dummy file + fs.touch(fs_join(target, "dummy")) + assert fs.isdir(target) + + target_file2 = fs_join(target, "file2") + target_subfile1 = fs_join(target, "subfile1") + + # Copy from source directory + fs.put(local_join(source, "file2"), target) + assert fs.isfile(target_file2) + + # Copy from sub directory + fs.put(local_join(source, "subdir", "subfile1"), target) + assert fs.isfile(target_subfile1) + + # Remove copied files + fs.rm([target_file2, target_subfile1]) + assert not fs.exists(target_file2) + assert not fs.exists(target_subfile1) + + # Repeat with trailing slash on target + fs.put(local_join(source, "file2"), target + "/") + assert fs.isdir(target) + assert fs.isfile(target_file2) + + fs.put(local_join(source, "subdir", "subfile1"), target + "/") + assert fs.isfile(target_subfile1) + + def test_put_file_to_new_directory( + self, fs, fs_join, fs_target, local_join, local_bulk_operations_scenario_0 + ): + # Copy scenario 1b + source = local_bulk_operations_scenario_0 + + target = fs_target + fs.mkdir(target) + + fs.put( + local_join(source, "subdir", "subfile1"), fs_join(target, "newdir/") + ) # Note trailing slash + assert fs.isdir(target) + assert fs.isdir(fs_join(target, "newdir")) + assert fs.isfile(fs_join(target, "newdir", "subfile1")) + + def test_put_file_to_file_in_existing_directory( + self, fs, fs_join, fs_target, local_join, local_bulk_operations_scenario_0 + ): + # Copy scenario 1c + source = local_bulk_operations_scenario_0 + + target = fs_target + fs.mkdir(target) + + fs.put(local_join(source, "subdir", "subfile1"), fs_join(target, "newfile")) + assert fs.isfile(fs_join(target, "newfile")) + + def test_put_file_to_file_in_new_directory( + self, fs, fs_join, fs_target, local_join, local_bulk_operations_scenario_0 + ): + # Copy scenario 1d + source = local_bulk_operations_scenario_0 + + target = fs_target + fs.mkdir(target) + + fs.put( + local_join(source, "subdir", "subfile1"), + fs_join(target, "newdir", "newfile"), + ) + assert fs.isdir(fs_join(target, "newdir")) + assert fs.isfile(fs_join(target, "newdir", "newfile")) + + def test_put_directory_to_existing_directory( + self, fs, fs_join, fs_target, local_bulk_operations_scenario_0 + ): + # Copy scenario 1e + source = local_bulk_operations_scenario_0 + + target = fs_target + fs.mkdir(target) + if not self.supports_empty_directories(): + # Force target directory to exist by adding a dummy file + dummy = fs_join(target, "dummy") + fs.touch(dummy) + assert fs.isdir(target) + + for source_slash, target_slash in zip([False, True], [False, True]): + s = fs_join(source, "subdir") + if source_slash: + s += "/" + t = target + "/" if target_slash else target + + # Without recursive does nothing + fs.put(s, t) + assert fs.ls(target) == [] if self.supports_empty_directories() else [dummy] + + # With recursive + fs.put(s, t, recursive=True) + if source_slash: + assert fs.isfile(fs_join(target, "subfile1")) + assert fs.isfile(fs_join(target, "subfile2")) + assert fs.isdir(fs_join(target, "nesteddir")) + assert fs.isfile(fs_join(target, "nesteddir", "nestedfile")) + assert not fs.exists(fs_join(target, "subdir")) + + fs.rm(fs.ls(target, detail=False), recursive=True) + else: + assert fs.isdir(fs_join(target, "subdir")) + assert fs.isfile(fs_join(target, "subdir", "subfile1")) + assert fs.isfile(fs_join(target, "subdir", "subfile2")) + assert fs.isdir(fs_join(target, "subdir", "nesteddir")) + assert fs.isfile(fs_join(target, "subdir", "nesteddir", "nestedfile")) + + fs.rm(fs_join(target, "subdir"), recursive=True) + assert fs.ls(target) == [] if self.supports_empty_directories() else [dummy] + + # Limit recursive by maxdepth + fs.put(s, t, recursive=True, maxdepth=1) + if source_slash: + assert fs.isfile(fs_join(target, "subfile1")) + assert fs.isfile(fs_join(target, "subfile2")) + assert not fs.exists(fs_join(target, "nesteddir")) + assert not fs.exists(fs_join(target, "subdir")) + + fs.rm(fs.ls(target, detail=False), recursive=True) + else: + assert fs.isdir(fs_join(target, "subdir")) + assert fs.isfile(fs_join(target, "subdir", "subfile1")) + assert fs.isfile(fs_join(target, "subdir", "subfile2")) + assert not fs.exists(fs_join(target, "subdir", "nesteddir")) + + fs.rm(fs_join(target, "subdir"), recursive=True) + assert fs.ls(target) == [] if self.supports_empty_directories() else [dummy] + + def test_put_directory_to_new_directory( + self, fs, fs_join, fs_target, local_bulk_operations_scenario_0 + ): + # Copy scenario 1f + source = local_bulk_operations_scenario_0 + + target = fs_target + fs.mkdir(target) + if not self.supports_empty_directories(): + # Force target directory to exist by adding a dummy file + dummy = fs_join(target, "dummy") + fs.touch(dummy) + assert fs.isdir(target) + + for source_slash, target_slash in zip([False, True], [False, True]): + s = fs_join(source, "subdir") + if source_slash: + s += "/" + t = fs_join(target, "newdir") + if target_slash: + t += "/" + + # Without recursive does nothing + fs.put(s, t) + assert fs.ls(target) == [] if self.supports_empty_directories() else [dummy] + + # With recursive + fs.put(s, t, recursive=True) + assert fs.isdir(fs_join(target, "newdir")) + assert fs.isfile(fs_join(target, "newdir", "subfile1")) + assert fs.isfile(fs_join(target, "newdir", "subfile2")) + assert fs.isdir(fs_join(target, "newdir", "nesteddir")) + assert fs.isfile(fs_join(target, "newdir", "nesteddir", "nestedfile")) + assert not fs.exists(fs_join(target, "subdir")) + + fs.rm(fs_join(target, "newdir"), recursive=True) + assert not fs.exists(fs_join(target, "newdir")) + + # Limit recursive by maxdepth + fs.put(s, t, recursive=True, maxdepth=1) + assert fs.isdir(fs_join(target, "newdir")) + assert fs.isfile(fs_join(target, "newdir", "subfile1")) + assert fs.isfile(fs_join(target, "newdir", "subfile2")) + assert not fs.exists(fs_join(target, "newdir", "nesteddir")) + assert not fs.exists(fs_join(target, "subdir")) + + fs.rm(fs_join(target, "newdir"), recursive=True) + assert not fs.exists(fs_join(target, "newdir")) + + def test_put_glob_to_existing_directory( + self, fs, fs_join, fs_target, local_join, local_bulk_operations_scenario_0 + ): + # Copy scenario 1g + source = local_bulk_operations_scenario_0 + + target = fs_target + fs.mkdir(target) + if not self.supports_empty_directories(): + # Force target directory to exist by adding a dummy file + dummy = fs_join(target, "dummy") + fs.touch(dummy) + assert fs.isdir(target) + + for target_slash in [False, True]: + t = target + "/" if target_slash else target + + # Without recursive + fs.put(local_join(source, "subdir", "*"), t) + assert fs.isfile(fs_join(target, "subfile1")) + assert fs.isfile(fs_join(target, "subfile2")) + assert not fs.isdir(fs_join(target, "nesteddir")) + assert not fs.exists(fs_join(target, "nesteddir", "nestedfile")) + assert not fs.exists(fs_join(target, "subdir")) + + fs.rm(fs.ls(target, detail=False), recursive=True) + assert fs.ls(target) == [] if self.supports_empty_directories() else [dummy] + + # With recursive + fs.put(local_join(source, "subdir", "*"), t, recursive=True) + assert fs.isfile(fs_join(target, "subfile1")) + assert fs.isfile(fs_join(target, "subfile2")) + assert fs.isdir(fs_join(target, "nesteddir")) + assert fs.isfile(fs_join(target, "nesteddir", "nestedfile")) + assert not fs.exists(fs_join(target, "subdir")) + + fs.rm(fs.ls(target, detail=False), recursive=True) + assert fs.ls(target) == [] if self.supports_empty_directories() else [dummy] + + # Limit recursive by maxdepth + fs.put(local_join(source, "subdir", "*"), t, recursive=True, maxdepth=1) + assert fs.isfile(fs_join(target, "subfile1")) + assert fs.isfile(fs_join(target, "subfile2")) + assert not fs.exists(fs_join(target, "nesteddir")) + assert not fs.exists(fs_join(target, "subdir")) + + fs.rm(fs.ls(target, detail=False), recursive=True) + assert fs.ls(target) == [] if self.supports_empty_directories() else [dummy] + + def test_put_glob_to_new_directory( + self, fs, fs_join, fs_target, local_join, local_bulk_operations_scenario_0 + ): + # Copy scenario 1h + source = local_bulk_operations_scenario_0 + + target = fs_target + fs.mkdir(target) + if not self.supports_empty_directories(): + # Force target directory to exist by adding a dummy file + dummy = fs_join(target, "dummy") + fs.touch(dummy) + assert fs.isdir(target) + + for target_slash in [False, True]: + t = fs_join(target, "newdir") + if target_slash: + t += "/" + + # Without recursive + fs.put(local_join(source, "subdir", "*"), t) + assert fs.isdir(fs_join(target, "newdir")) + assert fs.isfile(fs_join(target, "newdir", "subfile1")) + assert fs.isfile(fs_join(target, "newdir", "subfile2")) + assert not fs.exists(fs_join(target, "newdir", "nesteddir")) + assert not fs.exists(fs_join(target, "newdir", "nesteddir", "nestedfile")) + assert not fs.exists(fs_join(target, "subdir")) + assert not fs.exists(fs_join(target, "newdir", "subdir")) + + fs.rm(fs_join(target, "newdir"), recursive=True) + assert not fs.exists(fs_join(target, "newdir")) + + # With recursive + fs.put(local_join(source, "subdir", "*"), t, recursive=True) + assert fs.isdir(fs_join(target, "newdir")) + assert fs.isfile(fs_join(target, "newdir", "subfile1")) + assert fs.isfile(fs_join(target, "newdir", "subfile2")) + assert fs.isdir(fs_join(target, "newdir", "nesteddir")) + assert fs.isfile(fs_join(target, "newdir", "nesteddir", "nestedfile")) + assert not fs.exists(fs_join(target, "subdir")) + assert not fs.exists(fs_join(target, "newdir", "subdir")) + + fs.rm(fs_join(target, "newdir"), recursive=True) + assert not fs.exists(fs_join(target, "newdir")) + + # Limit recursive by maxdepth + fs.put(local_join(source, "subdir", "*"), t, recursive=True, maxdepth=1) + assert fs.isdir(fs_join(target, "newdir")) + assert fs.isfile(fs_join(target, "newdir", "subfile1")) + assert fs.isfile(fs_join(target, "newdir", "subfile2")) + assert not fs.exists(fs_join(target, "newdir", "nesteddir")) + assert not fs.exists(fs_join(target, "subdir")) + assert not fs.exists(fs_join(target, "newdir", "subdir")) + + fs.rm(fs_join(target, "newdir"), recursive=True) + assert not fs.exists(fs_join(target, "newdir")) + + def test_put_list_of_files_to_existing_directory( + self, + fs, + fs_join, + fs_target, + local_join, + local_bulk_operations_scenario_0, + fs_path, + ): + # Copy scenario 2a + source = local_bulk_operations_scenario_0 + + target = fs_target + fs.mkdir(target) + if not self.supports_empty_directories(): + # Force target directory to exist by adding a dummy file + dummy = fs_join(target, "dummy") + fs.touch(dummy) + assert fs.isdir(target) + + source_files = [ + local_join(source, "file1"), + local_join(source, "file2"), + local_join(source, "subdir", "subfile1"), + ] + + for target_slash in [False, True]: + t = target + "/" if target_slash else target + + fs.put(source_files, t) + assert fs.isfile(fs_join(target, "file1")) + assert fs.isfile(fs_join(target, "file2")) + assert fs.isfile(fs_join(target, "subfile1")) + + fs.rm(fs.find(target)) + assert fs.ls(target) == [] if self.supports_empty_directories() else [dummy] + + def test_put_list_of_files_to_new_directory( + self, fs, fs_join, fs_target, local_join, local_bulk_operations_scenario_0 + ): + # Copy scenario 2b + source = local_bulk_operations_scenario_0 + + target = fs_target + fs.mkdir(target) + + source_files = [ + local_join(source, "file1"), + local_join(source, "file2"), + local_join(source, "subdir", "subfile1"), + ] + + fs.put(source_files, fs_join(target, "newdir") + "/") # Note trailing slash + assert fs.isdir(fs_join(target, "newdir")) + assert fs.isfile(fs_join(target, "newdir", "file1")) + assert fs.isfile(fs_join(target, "newdir", "file2")) + assert fs.isfile(fs_join(target, "newdir", "subfile1")) + def test_put_directory_recursive( - self, fs, fs_join, fs_path, local_fs, local_join, local_path + self, fs, fs_join, fs_target, local_fs, local_join, local_path ): # https://github.com/fsspec/filesystem_spec/issues/1062 # Recursive cp/get/put of source directory into non-existent target directory. @@ -9,7 +370,7 @@ def test_put_directory_recursive( local_fs.mkdir(src) local_fs.touch(src_file) - target = fs_join(fs_path, "target") + target = fs_target # put without slash assert not fs.exists(target)