-
Notifications
You must be signed in to change notification settings - Fork 116
Follow fsspec's get_file() contract for directories and file objects #990
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
2a68d34
a87a59e
e7d77a1
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 |
|---|---|---|
|
|
@@ -25,7 +25,7 @@ | |
| from fsspec import AbstractFileSystem | ||
| from fsspec.callbacks import _DEFAULT_CALLBACK | ||
| from fsspec.spec import AbstractBufferedFile | ||
| from fsspec.utils import tokenize | ||
| from fsspec.utils import isfilelike, tokenize | ||
|
|
||
| import pyathena | ||
| from pyathena.connection import Connection | ||
|
|
@@ -1839,32 +1839,50 @@ def put_file( | |
|
|
||
| self.invalidate_cache(rpath) | ||
|
|
||
| def get_file(self, rpath: str, lpath: str, callback=_DEFAULT_CALLBACK, outfile=None, **kwargs): | ||
| def get_file(self, rpath: str, lpath=None, callback=_DEFAULT_CALLBACK, outfile=None, **kwargs): | ||
| """Download an S3 file to local filesystem. | ||
|
|
||
| Downloads a file from S3 to the local filesystem with progress tracking. | ||
| Reads the file in chunks to handle large files efficiently. | ||
|
|
||
| As with fsspec's ``AbstractFileSystem.get_file()``, a directory | ||
| ``rpath`` creates the local directory ``lpath``, and the parent | ||
| directories of a local file ``lpath`` are created as needed. | ||
|
|
||
| Args: | ||
| rpath: S3 source path (s3://bucket/key). | ||
| lpath: Local destination file path. | ||
| lpath: Local destination path, or a file-like object to write to. | ||
| Not needed when ``outfile`` is given. | ||
| callback: Progress callback for tracking download progress. | ||
| outfile: Unused parameter for fsspec compatibility. | ||
| outfile: A file-like object to write to instead of ``lpath``. | ||
| **kwargs: Additional S3 parameters passed to open(). | ||
|
|
||
| Note: | ||
| If lpath is a directory, the method returns without performing | ||
| any operation. | ||
| """ | ||
| if os.path.isdir(lpath): | ||
| _, _, path_version_id = self.parse_path(self._strip_protocol(rpath)) | ||
| if outfile is None and isfilelike(lpath): | ||
| outfile = lpath | ||
|
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 (relayed): FINDINGS
Reviewer output (verbatim): Covered surfaces: the exact diff, metadata/cache/version handling, stream ownership and failure paths, sync FINDINGS
Other review results:
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. Repair in a87a59e, with both self-review perspectives applied Patch series
Round one (behavior):
Round two (claims):
Validation on a87a59e:
A Codex follow-up on
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 follow-up on the repair (relayed): CLEAN
Reviewer output (verbatim): Covered surfaces: the exact repair, fsspec 2026.9.0 compatibility, parent creation, relative paths, PathLike inputs, bare filenames, CLEAN — both prior defects are resolved; no introduced regression found.
Static review only; no tests run. Checkout remained clean at |
||
| elif ( | ||
| outfile is None | ||
| and not (path_version_id or kwargs.get("version_id")) | ||
| and self.isdir(rpath) | ||
| ): | ||
| # A requested version always names an object, while isdir() | ||
| # would look up the latest version, or the prefix of the same | ||
| # name when the version does not exist. | ||
| os.makedirs(lpath, exist_ok=True) | ||
|
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. Self-review round two (claims and operational behavior): FINDINGS (PR description only, corrected) Base
|
||
| return | ||
|
|
||
| # The remote file is opened first so that no local file is created | ||
|
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. Rebase record and self-review of the integration: 68f6499 rebased onto master 4950996 (after #986 rm(), #987 ranges/open(), #988 aio parity, #999), new head 156d460. Perspective one (behavior):
Perspective two (claims):
|
||
| # when open() finds no object at the path. | ||
| with self.open(rpath, "rb", **kwargs) as remote, open(lpath, "wb") as local: | ||
| with contextlib.ExitStack() as stack: | ||
| remote = stack.enter_context(self.open(rpath, "rb", **kwargs)) | ||
| if outfile is None: | ||
| # Not abspath(), which would resolve ".." before symlinks. | ||
| if parent := os.path.dirname(lpath): | ||
| os.makedirs(parent, exist_ok=True) | ||
| outfile = stack.enter_context(open(lpath, "wb")) | ||
| callback.set_size(remote.size) | ||
| while data := remote.read(remote.blocksize): | ||
| local.write(data) | ||
| outfile.write(data) | ||
| callback.relative_update(len(data)) | ||
|
|
||
| def checksum(self, path: str, **kwargs): | ||
|
|
||
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 one after the rewrite to
get_file()only (implementation behavior): CLEANBase
28826be7d37983cb54050e849b32a4fe2331ea7d, head2a68d3481253291aef5089c1d1c8e56f59d9da83. Full pass overgit diff 28826be7d37983cb54050e849b32a4fe2331ea7d..2a68d3481253291aef5089c1d1c8e56f59d9da83(2 files). The earlier review threads on this PR refer to the previous headc6782a50, which includedcp_file()andmv(). That part moved to #1008.Covered:
AbstractFileSystem.get_file()(spec.py:981-1007):lpathbecomesoutfile;outfileis given;makedirs(lpath)is called for a directory;lpathare created;outfilefrom the caller is not closed.isdir().rpathmakesisdir()returnFalse(OSErroris caught), andopen()then raisesFileNotFoundErrorbefore any local file or parent directory is created.rpathwith an existing local directory aslpathraisesIsADirectoryError(release-noted).AioS3FileSystem._get_file()passesrpath,lpathand**kwargs(includingoutfile) to the syncget_file().get()and async_get()callget_file()once for each expanded path, directories included.rpathadds anisdir()→info()lookup. When the listings cache is on, the HeadObject result is cached and reused byopen(). When it is off, a download sends 2 HeadObject requests, as fsspec's ownget_file()does.test_get_file_version_idcases guard the new version skip and pass on master. Master'stest_get_file_directory, which pinnedFileNotFoundError, is replaced.Pre-existing and out of scope:
lpathstays a required argument, while fsspec defaults it toNoneforoutfile-only calls.