-
Notifications
You must be signed in to change notification settings - Fork 116
Read a negative cat_file() start without an end as a suffix range #998
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
0c00698
91d74a7
fbd2d0b
da5dedc
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 |
|---|---|---|
|
|
@@ -98,6 +98,17 @@ the existing object also count toward the limit. A write with `open` that reache | |
| limit raises `ValueError` and aborts its multipart upload. Multipart copies with `cp` | ||
| use parts large enough to stay within the limit. | ||
|
|
||
| Paths are normalized as in fsspec, which drops a trailing slash, so `info`, `isfile`, | ||
| and `open` treat `s3://YOUR_S3_BUCKET/dir/` as `s3://YOUR_S3_BUCKET/dir`: the object | ||
| `dir` if it exists, and otherwise the directory `dir`. An object whose key ends in a | ||
| slash, such as a folder marker, is therefore not a file for these methods. Opening | ||
| `dir/` for reading reads the object `dir` or raises `FileNotFoundError`, and opening it | ||
| for writing writes the object `dir`. A path with a `?versionId=` suffix keeps the slash | ||
| and refers to the object. `find`, and `ls` of the directory, list the object as a file | ||
| entry. `cat_file` uses the key as written. Without a `?versionId=` suffix, it reads | ||
|
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 review (relayed): Codex CLI 0.160.0, model Reviewer output, verbatim: Covered: the exact delta; path parsing, range branches, metadata/cache keys, version handling, prefixes, fsspec callers, async forwarding, FINDINGS — two documentation issues; no code regression found.
The implementation satisfies the requested guards. Ordinary keys, cached/version-aware entries, prefixes, suffix ranges, and non-empty non-negative ranges retain their prior behavior. Static tracing indicates all six new test cases fail before and pass after for the intended reasons: bucket cases previously reached No edits, builds, tests, or network access. HEAD remains Author verification on live S3:
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. Repaired in da5dedc (docs only):
|
||
| such an object without a range, with a non-empty range of non-negative offsets, or with | ||
| a negative `start` and no `end`, and raises `FileNotFoundError` for other ranges. | ||
|
|
||
| ## Error translation | ||
|
|
||
| S3 error responses are translated into standard Python exceptions, so filesystem | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1497,9 +1497,11 @@ def cat_file( | |
| ``start`` and ``end`` select bytes like a slice of the object: an | ||
| empty range, or one that starts at or past the end of the object, | ||
| returns ``b""``, and an end past the object reads up to its end. | ||
| Non-negative offsets are sent to S3 as they are; a negative offset is | ||
| resolved against the size from :meth:`info`, which also checks that | ||
| the object exists for an empty range. | ||
| Non-negative offsets are sent to S3 as they are, and so is a negative | ||
| ``start`` without an ``end``, as a suffix range of the last bytes. | ||
| Other negative offsets are resolved against the size from | ||
| :meth:`info`, which also checks that the object exists for an empty | ||
| range. | ||
|
|
||
| Args: | ||
| path: S3 path (s3://bucket/key) of the object. | ||
|
|
@@ -1515,36 +1517,44 @@ def cat_file( | |
| The bytes read from the object. | ||
|
|
||
| Raises: | ||
| FileNotFoundError: If the key does not exist. | ||
| FileNotFoundError: If the path has no key or the key does not | ||
| exist. | ||
| """ | ||
| bucket, key, path_version_id = self.parse_path(path) | ||
| if not key: | ||
| raise FileNotFoundError(path) | ||
| version_id = kwargs.pop("version_id", None) | ||
| if path_version_id: | ||
| version_id = path_version_id | ||
| if (start is not None and start < 0) or ( | ||
| end is not None and (end < 0 or (start or 0) >= end) | ||
| ): | ||
| # A negative offset needs the size of the object, and an empty | ||
| # range sends no GetObject request that would report a missing | ||
| # object. | ||
| info = self.info(path, version_id=version_id) | ||
| if info.get("type") == S3ObjectType.S3_OBJECT_TYPE_DIRECTORY: | ||
| # There is no object to read, as GetObject reports for the | ||
| # other ranges. | ||
| raise FileNotFoundError(path) | ||
| start, end, _ = slice(start, end).indices(info.get("size", 0)) | ||
|
|
||
| ranges: tuple[int, int | None] | None = None | ||
| if start is not None or end is not None: | ||
| start = start or 0 | ||
| if end is not None and start >= end: | ||
| # S3 would return the whole object for an empty range. | ||
| return b"" | ||
| ranges = (start, end) | ||
| if start is not None and start < 0 and end is None: | ||
|
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, callers, operations). Base Claims checked:
Callers and operations: a negative |
||
| # S3 returns the last bytes, or the whole object when it is | ||
| # shorter, without the size of the object. | ||
| ranges = (start, None) | ||
| else: | ||
| if (start is not None and start < 0) or ( | ||
| end is not None and (end < 0 or (start or 0) >= end) | ||
| ): | ||
| # A negative offset needs the size of the object, and an | ||
| # empty range sends no GetObject request that would report a | ||
| # missing object. | ||
| info = self.info(path, version_id=version_id) | ||
| if info.get("type") == S3ObjectType.S3_OBJECT_TYPE_DIRECTORY or info.key != key: | ||
|
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): Codex CLI 0.160.0, model Reviewer output, verbatim: Covered the exact range, FINDINGS
The suffix header itself matches RFC 9110 and the inspected fsspec/s3fs implementation. Version precedence and buffered-read/multipart-copy callers show no further regression. Ordinary marker entries are listed as files by The new tests would catch the old stale-size and slash-marker suffix failures. Mocking I read fsspec 2026.9.0 in the supplied Author verification on live S3 (master
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. Repaired in fbd2d0b:
Validation on fbd2d0b:
Self-review of this repair:
|
||
| # There is no object to read, as GetObject reports for | ||
| # the other ranges, or info() describes the key without | ||
| # the trailing slash of this one. | ||
| raise FileNotFoundError(path) | ||
| start, end, _ = slice(start, end).indices(info.get("size", 0)) | ||
| if start is not None or end is not None: | ||
| start = start or 0 | ||
| if end is not None and start >= end: | ||
| # S3 would return the whole object for an empty range. | ||
| return b"" | ||
| ranges = (start, end) | ||
| try: | ||
| return self._get_object( | ||
| bucket=bucket, | ||
| key=cast(str, key), | ||
| key=key, | ||
| ranges=ranges, | ||
| version_id=version_id, | ||
| **kwargs, | ||
|
|
@@ -2154,14 +2164,15 @@ def _get_object( | |
| bucket: The bucket name. | ||
| key: The object key. | ||
| ranges: The ``(start, end)`` byte range to read, with an exclusive | ||
| end or ``None`` to read to the end of the object, or ``None`` | ||
| to read the whole object. | ||
| end or ``None`` to read to the end of the object (the last | ||
| ``-start`` bytes for a negative start), or ``None`` to read | ||
| the whole object. | ||
| version_id: The version ID to read, or ``None`` for the latest. | ||
| **kwargs: Additional parameters passed to the GetObject API. | ||
|
|
||
| Returns: | ||
| Tuple of the start of the range (0 for the whole object) and the | ||
| bytes read. | ||
| Tuple of the start of the range as given (0 for the whole | ||
| object) and the bytes read. | ||
|
|
||
| Raises: | ||
| ValueError: If the range is empty. S3 ignores a range whose last | ||
|
|
@@ -2775,13 +2786,16 @@ def _format_ranges(ranges: tuple[int, int | None]) -> str: | |
|
|
||
| Args: | ||
| ranges: The ``(start, end)`` byte range, with an exclusive end or | ||
| ``None`` for the end of the object. | ||
| ``None`` for the end of the object. A negative start with no | ||
| end selects the last ``-start`` bytes. | ||
|
|
||
| Returns: | ||
| The range, such as ``bytes=0-99`` or ``bytes=100-``. | ||
| The range, such as ``bytes=0-99``, ``bytes=100-`` or ``bytes=-8``. | ||
| """ | ||
| start, end = ranges | ||
| return f"bytes={start}-" if end is None else f"bytes={start}-{end - 1}" | ||
| if end is None: | ||
| return f"bytes={start}" if start < 0 else f"bytes={start}-" | ||
| return f"bytes={start}-{end - 1}" | ||
|
|
||
| @staticmethod | ||
| def _get_ranges( | ||
|
|
||
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.
Independent narrow follow-up review (relayed): Codex CLI 0.160.0, model
gpt-6-astra, reasoning effortmax,codex exec -s read-only, session01a1012b-b219-74d0-8553-c9578ca54d61. The scope was the docs deltafbd2d0bc3aafa1185a7b3150879fe3ee6111e0a7..da5dedcfabdb057511dae7b935f28e17b6d50d57(the base92c9e3e67180eb3d52ef7b85579a24540a30fd64did not move), with each statement checked against the code paths. This is a static review. The snapshot and the PR worktree were unchanged. Result: CLEAN.Reviewer output, verbatim:
Surfaces covered, assuming
dir/exists and checking both with and without an object atdir:info,isfiledir/resolves todir: file/Truewhen that object exists; directory/Falseotherwise.opendiror raisesFileNotFoundError; writing targetsdir.?versionId=VValueError.find,lsof the directorydiralso exists.cat_file, no suffixstartwithoutendread the slash key directly.end, and negativestartwith an explicitendraiseFileNotFoundError. The key-mismatch guard also rejects an existingdirobject.cat_fileCLEAN — no actionable findings in the rewritten paragraph.
Static source review at
da5dedcf, including inherited methods in local fsspec 2026.9.0, matching the lockfile. No edits, builds, tests, or network access.