Repository navigation
Align AioS3FileSystem with S3FileSystem for cancellation and arguments (GH-1337) #1395
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1a08eb1
df64393
511ffe4
1bf31e1
727a1dd
6894974
440b652
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1127,6 +1127,31 @@ def find( | |
| return {f.name: f for f in files} | ||
| return [f.name for f in files] | ||
|
|
||
| def glob(self, path, maxdepth=None, **kwargs): | ||
| """Find the paths that match a glob pattern. | ||
|
|
||
| As fsspec's ``glob()``, except that only the keys that start with the | ||
| part of the last path component before the first wildcard are listed, | ||
| as with fsspec's asynchronous ``_glob()``. | ||
|
|
||
| Args: | ||
| path: S3 glob pattern. | ||
| maxdepth: Maximum depth of the paths below the literal part of the | ||
| pattern, at least 1. | ||
| **kwargs: Additional arguments passed to ``find``, such as | ||
| ``detail``. | ||
|
|
||
| Returns: | ||
| The matching paths, or a dictionary of them and their objects with | ||
| ``detail=True``. | ||
| """ | ||
| stripped = self._strip_protocol(path) | ||
| wildcards = [i for i in (stripped.find(c) for c in "*?[") if i >= 0] | ||
| stem = stripped[: min(wildcards)].rpartition("/")[2] if wildcards else "" | ||
| if stem and "prefix" not in kwargs: | ||
| kwargs["prefix"] = stem | ||
| return super().glob(path, maxdepth=maxdepth, **kwargs) | ||
|
|
||
| def expand_path(self, path, recursive=False, maxdepth=None, **kwargs) -> list[str]: | ||
| """Expand glob patterns and directories into the paths they match. | ||
|
|
||
|
|
@@ -1288,18 +1313,35 @@ def rm(self, path, recursive=False, maxdepth=None, batch_size=None, **kwargs) -> | |
| TypeError: If both ``batch_size`` and ``max_workers`` are given. | ||
| OSError: If S3 could not delete some of the objects. | ||
| """ | ||
| max_workers = self._delete_batch_limit(batch_size, kwargs) | ||
| paths = self._delete_paths(path, recursive=recursive, maxdepth=maxdepth) | ||
| self._delete_objects(paths, max_workers=max_workers, **kwargs) | ||
|
|
||
| def _delete_batch_limit(self, batch_size: int | None, kwargs: dict[str, Any]) -> int: | ||
| """Return the number of DeleteObjects requests of ``rm()`` to send at once. | ||
|
|
||
| Args: | ||
| batch_size: The ``batch_size`` argument of ``rm()``. | ||
| kwargs: The other arguments of ``rm()``, from which its deprecated | ||
| alias ``max_workers`` is removed. | ||
|
|
||
| Returns: | ||
| The number of requests. | ||
|
|
||
| Raises: | ||
| ValueError: If the number is neither positive, -1, nor None. | ||
| TypeError: If both ``batch_size`` and ``max_workers`` are given. | ||
| """ | ||
| if "max_workers" in kwargs: | ||
| if batch_size is not None: | ||
| raise TypeError("rm() got both batch_size and its alias max_workers.") | ||
| warnings.warn( | ||
| "The max_workers argument of rm() is deprecated; use batch_size.", | ||
| DeprecationWarning, | ||
| stacklevel=2, | ||
| stacklevel=3, | ||
| ) | ||
| batch_size = kwargs.pop("max_workers") | ||
| max_workers = self._batch_limit(batch_size) | ||
| paths = self._delete_paths(path, recursive=recursive, maxdepth=maxdepth) | ||
| self._delete_objects(paths, max_workers=max_workers, **kwargs) | ||
| return self._batch_limit(batch_size) | ||
|
|
||
| def _delete_paths( | ||
| self, path: str | list[str], recursive: bool = False, maxdepth: int | None = None | ||
|
|
@@ -2671,32 +2713,7 @@ def put( | |
| ``batch_size`` is neither positive, -1, nor None. | ||
| """ | ||
| limit = self._batch_limit(batch_size) | ||
| if isinstance(lpath, list) and isinstance(rpath, list): | ||
| pairs = [(lp, self._strip_protocol(rp)) for lp, rp in zip(lpath, rpath, strict=False)] | ||
| else: | ||
| # As fsspec's put() pairs them. | ||
| source_is_str = isinstance(lpath, str) | ||
| if source_is_str: | ||
| lpath = make_path_posix(lpath) | ||
| local = LocalFileSystem() | ||
| lpaths = local.expand_path(lpath, recursive=recursive, maxdepth=maxdepth, **kwargs) | ||
| if source_is_str and (not recursive or maxdepth is not None): | ||
| # A string source without recursion does not copy directories. | ||
| lpaths = [p for p in lpaths if not (trailing_sep(p) or local.isdir(p))] | ||
| if not lpaths: | ||
| return | ||
| dest_is_dir = 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] | ||
| ) | ||
| exists = source_is_str and ( | ||
| (has_magic(lpath) and len(lpaths) == 1) | ||
| or (not has_magic(lpath) and dest_is_dir and not trailing_sep(lpath)) | ||
| ) | ||
| rpaths = other_paths(lpaths, rpath, exists=exists, flatten=not source_is_str) | ||
| pairs = list(zip(lpaths, rpaths, strict=True)) | ||
| pairs = self._put_pairs(lpath, rpath, recursive=recursive, maxdepth=maxdepth, **kwargs) | ||
| if not pairs: | ||
| return | ||
| if len({rp for _, rp in pairs}) != len(pairs): | ||
|
|
@@ -2719,6 +2736,46 @@ def put_pair(pair: tuple[str, str]) -> None: | |
| locked.set_size(len(pairs)) | ||
| self._run_batch(put_pair, pairs, concurrency) | ||
|
|
||
| def _put_pairs( | ||
| self, lpath, rpath, recursive=False, maxdepth=None, **kwargs | ||
| ) -> list[tuple[str, str]]: | ||
| """Pair the local sources of ``put()`` with their S3 destinations as fsspec does. | ||
|
|
||
| Args: | ||
| lpath: Local source path, glob pattern, or list of them. | ||
| rpath: S3 destination path, or list of paths when ``lpath`` is a | ||
| list. | ||
| recursive: Whether to include the directories with their contents. | ||
| maxdepth: Maximum depth of a recursive copy. | ||
| **kwargs: Additional arguments passed to the ``expand_path()`` of | ||
| the local filesystem. | ||
|
|
||
| Returns: | ||
| The local paths, including the directories of a recursive copy, | ||
| with their destinations without the protocol. | ||
| """ | ||
| if isinstance(lpath, list) and isinstance(rpath, list): | ||
| return [(lp, self._strip_protocol(rp)) for lp, rp in zip(lpath, rpath, strict=False)] | ||
| source_is_str = isinstance(lpath, str) | ||
| if source_is_str: | ||
| lpath = make_path_posix(lpath) | ||
| local = LocalFileSystem() | ||
| lpaths = local.expand_path(lpath, recursive=recursive, maxdepth=maxdepth, **kwargs) | ||
| if source_is_str and (not recursive or maxdepth is not None): | ||
| # A string source without recursion does not copy directories. | ||
| lpaths = [p for p in lpaths if not (trailing_sep(p) or local.isdir(p))] | ||
| if not lpaths: | ||
| return [] | ||
| dest_is_dir = isinstance(rpath, str) and (trailing_sep(rpath) or self.isdir(rpath)) | ||
| # A list of destinations or one path, also path-like, as in fsspec. | ||
| rpath = self._strip_protocol(rpath) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Independent review 3. P2 —
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verified and fixed in 511ffe4: |
||
| exists = source_is_str and ( | ||
| (has_magic(lpath) and len(lpaths) == 1) | ||
| or (not has_magic(lpath) and dest_is_dir and not trailing_sep(lpath)) | ||
| ) | ||
| rpaths = other_paths(lpaths, rpath, exists=exists, flatten=not source_is_str) | ||
| return list(zip(lpaths, rpaths, strict=True)) | ||
|
|
||
| def put_file( | ||
| self, | ||
| lpath: str, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Self-review round two (claims, compatibility, operations): base
6589eb6efeeddbd83e9faf07b879f6bd4270afa1, headdf64393031089f3bb95cee1f5758d24eed52632d.Claims checked:
prefix=, the synchronousglob()does not": read in fsspec 2026.9.0 (asyn.py_glob,spec.pyglob); 2026.9.0 is the floor inpyproject.toml, so it holds for the whole supported range. Matches unchanged:test_glob_matches_fsspec(incl.dir/s*) passes, and a bucket-level wildcard still raises in_find("")as before.test_put_file_cancelled_stops_upload(requests are exactly Create, one UploadPart, Abort, and the task is not done before the abort); the small-object and download checks are those of Run the batch operations of S3FileSystem concurrently (GH-1322) #1344, whichrun()now reaches through its stop scope.fsspec/asyn.py:380and:391-396; seen failing withrun()or_S3BatchTasksnot draining (mutants).docs/aio.md: the table row, the cancellation paragraph, and the batch paragraph match the code;_mv()runs all copies at once (gather), the othersbatch_sizeat a time.test_request_pool_size[None-...]checksmax_workers=Noneresolves to the client pool.Existing callers:
run()keeps its signature and return/exception; only the moment a cancellation is raised moves._rm,_cp_file,_put,_expand_pathaccept a superset of their old arguments, except_rm(batch_size=0)and other invalid values, nowValueError.S3FileSystem.put()/rm()behavior is unchanged by the helper extraction (their #1344 tests pass).Operator: fewer ListObjectsV2 keys for a glob with a stem, also for the synchronous filesystem. A cancellation, including an
asyncio.wait_for()timeout, now returns only when the in-flight request ends, e.g. after the whole body read of a large_cat_file(). That is the decided B6 behavior; added to the PR body's release notes with the other observable changes.Evidence: local AWS filesystem suite and
just benchmark testat 1a08eb1; df64393 changes tests only (rerun with their mutants). AWS CI not run yet (Draft).FINDINGS: the PR body lacked the observable changes; corrected.