Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/filesystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,23 @@ for version in versions:
Version-aware operations require the `s3:GetObjectVersion` and
`s3:ListBucketVersions` permissions.

Moving a `?versionId=null` source onto its own key copies that version and
deletes the source version when bucket versioning is enabled. If versioning has
never been enabled or is suspended, the move leaves the source in place: copying
onto the key would replace the null version that the subsequent deletion removes.
Comment thread
laughingman7743 marked this conversation as resolved.
The same distinction applies to move conflict checks.

A move compares a null version with its unversioned key using `GetBucketVersioning`,
which requires `s3:GetBucketVersioning`. Each relevant bucket is looked up once
during planning; other moves do not make this request. The result is not cached
between moves. A failed lookup stops the move before any copy or deletion.
Directory buckets do not support versioning, so their null versions are treated
as the key itself without a versioning lookup.

The opt-in S3 versioning integration tests create dedicated temporary buckets and
check unversioned, enabled, and suspended states. See [Testing](testing.md) for the
command and required test permissions.

## Bucket lifecycle

Bucket creation and deletion are infrastructure-level changes and are disabled by
Expand Down Expand Up @@ -387,6 +404,13 @@ paths, and pass the results to `copy_pairs()` and `move_pairs()`. A `sources`,
`destination_is_dir` or `missing` that a rule needs and that is not passed raises
`ValueError`.

For moves that compare a null version with its unversioned key, the caller also
looks up bucket versioning and passes the names of the versioning-enabled buckets
as `versioning_enabled_buckets` to both `conflict_candidates()` and `move_pairs()`.
Pass a collection of bucket names, such as a set, rather than a single string.
The default empty collection treats null versions as their keys, as in unversioned
or suspended buckets. The model makes no AWS requests.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Compatibility, operational and claim repair self-review: CLEAN. Covered all six repair files separately from the implementation pass. Existing positional arguments, valid collection contexts, default no-op semantics and pure model/no-I/O behavior remain compatible. Both public methods now clearly raise TypeError for a bucket-name string; OSError accurately includes S3Core-translated permission failures through sync/async planners and wrapper. Public documentation states that callers supply the same enabled-bucket collection to candidate discovery and final checks; default-empty behavior and opt-in permissions are explicit. Tests verify both method entry points. Format/lint/docs checks and 226 offline tests passed; live results started at the earlier head will be identified by that revision, while current CI is still pending. No new AWS calls, permissions or runtime claims are introduced by this repair.

Frozen repair: ca44c77..4a3edf6; PR base e41fe33. Patch series compared as e41fe33..ca44c77 versus e41fe33..4a3edf6.

```python
from pyathena.filesystem.s3_path_pairing import S3PathPairing

Expand Down
31 changes: 31 additions & 0 deletions docs/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,37 @@ The offline-only `.env` example disables EC2 metadata to prevent implicit creden
`--noconftest` excludes the AWS session hooks and fixtures; disabling the rerun plugin also avoids its local socket setup in restricted environments.
Use this invocation only for self-contained modules; integration tests need their normal fixtures and a real AWS environment.

### S3 null-version moves

The null-version move regression tests create three dedicated temporary buckets:
unversioned, versioning-enabled, and versioning-suspended. They run only when
explicitly enabled, because provisioning requires additional permissions and a
15-minute wait after first enabling versioning. Run them serially:

```bash
AWS_ATHENA_S3_VERSIONING_TESTS=1 uv run --env-file .env pytest -n 1 \
tests/pyathena/filesystem/test_s3.py \
tests/pyathena/filesystem/test_s3_async.py -k move_null_version_onto_key -v
```

These nine cases belong to `TestS3FileSystem` and `TestAioS3FileSystem` and use
their existing filesystem fixtures. The shared session-scoped bucket fixture in
`tests/pyathena/filesystem/conftest.py` provisions the three buckets once per
worker, so a serial run shares one setup and wait across both test modules.
The synchronous API, asynchronous API, and asynchronous filesystem's synchronous
wrapper each have a separate test method.

The test identity needs `s3:CreateBucket`, `s3:GetBucketVersioning`,
`s3:PutBucketVersioning`, `s3:PutObject`, `s3:GetObject`, `s3:GetObjectVersion`,
`s3:ListBucketVersions`, `s3:DeleteObjectVersion`, and `s3:DeleteBucket` on the
temporary `pyathena-mv-null-*` buckets and their objects, in addition to the normal
test session permissions. The fixture deletes all versions and delete markers,
then removes its buckets, including when a test fails. An interrupted process may
leave buckets behind; remove only the buckets created by that run.

Ordinary CI does not enable these tests. Its state-specific mock regression tests
and existing AWS suite still run; report the opt-in results separately.

### SQLAlchemy suites

The SQLAlchemy compliance suites under `tests/sqlalchemy/` run with different configurations: use `sqla` for synchronous dialects and `sqla-async` for native asyncio dialects.
Expand Down
39 changes: 34 additions & 5 deletions pyathena/filesystem/s3.py
Original file line number Diff line number Diff line change
Expand Up @@ -1414,9 +1414,17 @@ def mv(self, path1, path2, recursive=False, maxdepth=None, **kwargs) -> None:
``AbstractFileSystem.mv()`` instead removes ``path1`` by expanding it
again, which also deletes copies placed where ``path1`` matches them
and files that ``maxdepth`` kept from being copied. A file whose
destination is the file itself, or the ``null`` version of a file
moved to the file, is left in place, and directories, which S3 does
not store as objects, are not copied.
destination is the file itself is left in place, and directories,
which S3 does not store as objects, are not copied. A ``null`` version
moved onto its key is copied and deleted only when bucket versioning
is enabled. Without versioning, or with versioning suspended, that
move is left in place because the copy would replace the version
that the deletion removes.

Comparing a ``null`` version with its unversioned key calls
GetBucketVersioning once per bucket during the move's planning and
requires ``s3:GetBucketVersioning``. The result is not cached across
moves. Directory buckets do not support versioning and need no lookup.

Args:
path1: Source S3 path, glob pattern, or list of paths.
Expand All @@ -1431,6 +1439,7 @@ def mv(self, path1, path2, recursive=False, maxdepth=None, **kwargs) -> None:
destination is another source, including one left in place,
which is checked before anything is copied. A directory with
no object at its key, which is not copied, does not conflict.
OSError: If bucket versioning cannot be read.
"""
if path1 == path2:
return
Expand Down Expand Up @@ -1462,15 +1471,35 @@ def _move_pairs(

Raises:
ValueError: If the move has conflicting paths.
OSError: If bucket versioning cannot be read.
"""
pairing = S3PathPairing(path1, path2, recursive=recursive, maxdepth=maxdepth)
pairs = self._copy_pairs(pairing)
paths = {p: S3Path.parse(p) for pair in pairs for p in pair}
unversioned = {p.name for p in paths.values() if not p.version_id}
buckets = {
p.bucket
for p in paths.values()
if p.version_id == "null"
and p.name in unversioned
and not self.core._is_directory_bucket(p.bucket)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review round one, bounded repair follow-up: CLEAN. Base: 0c19c8d; old head: 570ed72; new head: d680586. Verified the old commit objects and git range-diff: the initial patch is unchanged, with one repair commit added. Covered all five repaired files and traced the shared planner and existing S3Core._is_directory_bucket predicate through both sync and async callers. Directory null versions retain the previous identity, without an unsupported lookup, while general-purpose bucket handling is unchanged. Added regressions assert both same-key no-op and separate-destination moves without lookup, plus async no-op behavior. just format, just lint, docs lint, and the current-tree Sphinx build passed; the repaired move selection passed 54 cases and intentionally skipped the nine opt-in cases. The initial reviewed general-purpose bucket run passed all 60 cases, including the nine real AWS cases, with cleanup completed.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relayed independent bounded static follow-up: CLEAN. Reviewer: Claude Code claude-opus-5-5, verified first-party Max (not Enterprise), effort high; session 77be42ea-ce2d-4566-9889-c92c73f3db39. Old/new base: 0c19c8d; old head: 570ed72; new head: d680586. Supplied the bounded patch-series comparison without commit subjects, the literal repair diff, and a tracked-source snapshot. Only Read/Grep/Glob were enabled; no commands, edits, validation, network, PR context, commit messages, or memory access were authorized. Snapshot hashes and author worktree were unchanged afterward.

Coverage: lookup selection; the existing directory-bucket predicate and core property; null/key identity and conflicts; both shared-planner callers; affected docstrings; all three added directory-bucket test cases; filesystem documentation; testing-guide headings and related anchors. The reviewer confirmed the unsupported directory-bucket lookup is removed without changing general-purpose bucket lookups, and the general tox/reporting guidance has its own section. Both prior findings are resolved. No actionable defects found within the repaired scope.

Static review only: the reviewer ran no tests or builds. Directory-bucket preservation has mock coverage, without a real S3 Express runtime claim.

}
versioned_buckets = {
bucket
for bucket in buckets
if self._call(self._client.get_bucket_versioning, Bucket=bucket).get("Status")

Copy link
Copy Markdown
Member Author

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, and AWS operation): CLEAN.

Frozen scope: 0c19c8d..570ed72; all six changed files. Audited the PR explanation and changed documentation/comments before tracing the corresponding implementation and tests.

Confirmed unchanged public signatures and version_aware defaults, version-query/protocol normalization, and preservation of original copy/delete paths. The planner observes bucket state per move; it does not introduce persistent configuration caching. Each relevant bucket has one logical lookup through the existing S3Core request/retry/error-translation path, so service retries may send additional requests. Permission failures remain visible before copying or deleting. S3 moves remain copy/delete operations, without an atomicity guarantee.

Checked AWS's documented Enabled, Suspended, and absent-Status behavior and GetBucketVersioning permission, the first-enable propagation wait, local fixture permissions and cleanup, the ordinary-CI skip gate, and Test workflow path selection (PyAthena suite, with unrelated SQLAlchemy/Spark suites skipped by conditions). The targeted tests cover aliases, multiple keys/buckets, state freshness, conflicts, and lookup/copy failures. No actionable compatibility or operational regression found.

Corrected the PR validation record to name the full published SHA, dependency versions, and completed documentation builds, including the 205 current-tree Sphinx warnings; none names the changed Markdown or move docstrings. The active real AWS versioning cases remain pending and are not represented as passed.

Comment thread
laughingman7743 marked this conversation as resolved.
== "Enabled"
}
missing = {
source
for source in pairing.conflict_candidates(pairs)
for source in pairing.conflict_candidates(
pairs, versioning_enabled_buckets=versioned_buckets

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation complete at published/reviewed head 4a3edf6: Ready-triggered Test run 37208194062 passed against merge candidate eec166a (head plus master 855d4a7). Python 3.14: 2572 passed, 10 skipped, 13 warnings. All applicable current-head checks are green; SQLAlchemy jobs and Spark coverage were excluded by unchanged-path conditions, and nine temporary-bucket tests remain intentionally opt-in. Local opt-in run at ca44c77 passed all 66 cases, including nine real AWS versioning cases, and cleanup completed. The final repair preserves that valid set-context AWS path; current-head offline tests passed 226 cases. Required self-reviews and Opus 5.5 / first-party Max / high follow-up are complete and CLEAN; all verified findings are repaired and resolved. PR state rechecked: Ready, MERGEABLE, CLEAN.

CI: https://github.com/pyathena-dev/PyAthena/actions/runs/37208194062

)
if self._head_object(source) is None
}
return pairing.move_pairs(pairs, missing=missing)
return pairing.move_pairs(
pairs, missing=missing, versioning_enabled_buckets=versioned_buckets

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional upstream integration check: master advanced from e41fe33 to 855d4a7 while validation ran. Inspected that literal upstream diff: it extracts S3File buffered multipart writing and updates its tests/docs; S3Core copy/delete, S3PathPairing, sync/async move planners and versioning lookups are unchanged. No move-contract repair or branch rewrite is needed. The frozen review merge-base remains e41fe33, the published/reviewed head is 4a3edf6, offline checks pass and GitHub reports a clean merge. Ready CI will validate integration with the new base.

)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased implementation self-review (round one): CLEAN.
Frozen full diff: e41fe33..ca44c77; all nine changed files covered. The old base/head objects were verified and the patch-series comparison inspected. This is a full pass because the upstream refactor changes the planner's contract.
Checked selective bucket-state lookup, scheme/version normalization, per-call freshness, directory-bucket exclusion, enabled/suspended/unversioned identities, overlap and duplicate-destination guards, missing-directory candidates, and exact-version copy/delete. Traced both synchronous mv and async _mv/wrapper into the extracted S3PathPairing and shared core. Bucket status is passed identically to candidate discovery and final conflict checks; defaults and the pure model's lack of I/O remain compatible. Both lookup and copy failures prevent source deletion.
Regression coverage includes real-client Stubber assertions, status/error/request-count cases, pure-model bucket isolation and same-version no-op cases, and opt-in live coverage with exact-key version comparisons and exhaustive fixture cleanup. Preserved #1082's du tests and offline/docs organization.
Validation: just format, just lint, just docs lint and a build of the current Sphinx source passed; 224 self-contained path/pairing/pandas/Polars tests passed. Real AWS tests and current CI are still in progress. No S3 Express runtime claim. Static review does not replace those pending results.

def _copy_pairs(
self, pairing: S3PathPairing, isdir: Callable[[str], bool] | None = None
Expand Down
5 changes: 5 additions & 0 deletions pyathena/filesystem/s3_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ async def _mv(self, path1, path2, recursive=False, maxdepth=None, **kwargs) -> N
destination is another source, including one left in place,
which is checked before anything is copied. A directory with
no object at its key, which is not copied, does not conflict.
OSError: If bucket versioning cannot be read.
"""
if path1 == path2:
return
Expand Down Expand Up @@ -400,6 +401,10 @@ def mv(self, path1, path2, recursive=False, maxdepth=None, **kwargs) -> None:
recursive: Whether to move the directories with their contents.
maxdepth: Maximum depth of a recursive move.
**kwargs: Additional S3 copy parameters.

Raises:
ValueError: If the move has conflicting paths.
OSError: If bucket versioning cannot be read.
"""
sync(self.loop, self._mv, path1, path2, recursive=recursive, maxdepth=maxdepth, **kwargs)

Expand Down
6 changes: 3 additions & 3 deletions pyathena/filesystem/s3_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,13 +135,13 @@ def uri(self) -> str:

@property
def target(self) -> S3Path:
"""What the path names when paths are compared: a ``null`` version names its key.
"""The default move target, assuming bucket versioning is not enabled.

A ``null`` version is taken to name the key itself: in a bucket without
versioning, or with versioning suspended, a write to the key replaces
its ``null`` version. With versioning enabled, a write adds a new
version instead, but the ``null`` version is still taken to name the
key. Any other path is its own target.
version instead, so callers with that state must keep the ``null``
version distinct from its key. Any other path is its own target.
"""
return self.with_version_id(None) if self.version_id == "null" else self

Expand Down
63 changes: 54 additions & 9 deletions pyathena/filesystem/s3_path_pairing.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ class S3PathPairing:
pairs them, except that a path with a version ID names that version of an
object: it is not a glob pattern, and its destination is named after its
key without the version. A move compares the paths by what they name
(see :attr:`~pyathena.filesystem.s3_path.S3Path.target`).
(see :attr:`~pyathena.filesystem.s3_path.S3Path.target`), keeping ``null``
versions distinct from their keys in the caller's versioning-enabled
buckets.
:meth:`delete_paths` splits the paths of an ``rm()``.

The pairing is a frozen dataclass of the paths as given, and holds no
Expand Down Expand Up @@ -138,7 +140,12 @@ def copy_pairs(
destinations = other_paths(names, path2, exists=exists, flatten=not source_is_str)
return list(zip(sources, destinations, strict=True))

def conflict_candidates(self, pairs: Sequence[tuple[str, str]]) -> list[str]:
def conflict_candidates(
self,
pairs: Sequence[tuple[str, str]],
*,
versioning_enabled_buckets: Collection[str] = (),
) -> list[str]:
"""Return the sources of a move whose conflicts depend on an object at their key.

A source with another source below it may be a directory. If no object
Expand All @@ -149,18 +156,28 @@ def conflict_candidates(self, pairs: Sequence[tuple[str, str]]) -> list[str]:
Args:
pairs: The sources and destinations of the move, as
:meth:`copy_pairs` of this pairing returns them.
versioning_enabled_buckets: Buckets whose versioning is enabled.
Their ``null`` versions are distinct from their unversioned
keys. Use the same collection for :meth:`move_pairs`.

Returns:
The sources, in ``bucket/key`` form (their
:attr:`~pyathena.filesystem.s3_path.S3Path.target`), that have
another source below them and a destination that conflicts, one
per pair in the order of the pairs; empty if nothing needs to be
looked up.

Raises:
TypeError: If ``versioning_enabled_buckets`` is a string.
"""
return self._moves(pairs)[2]
return self._moves(pairs, versioning_enabled_buckets)[2]

def move_pairs(
self, pairs: Sequence[tuple[str, str]], missing: Collection[str] | None = None
self,
pairs: Sequence[tuple[str, str]],
missing: Collection[str] | None = None,
*,
versioning_enabled_buckets: Collection[str] = (),
) -> list[tuple[str, str]]:
"""Check the pairs of a move and leave out the sources that stay in place.

Expand All @@ -170,26 +187,31 @@ def move_pairs(
missing: The :meth:`conflict_candidates` without an object at their
key, in any form that names them; needed when there are
candidates.
versioning_enabled_buckets: Buckets whose versioning is enabled.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebased compatibility, operational and claim self-review (round two): CLEAN.
Frozen full diff: e41fe33..ca44c77; all nine changed files and upstream contract changes inspected separately from round one.
Claims checked: enabled null-version moves preserve the selected copy and delete only that version; never-enabled/suspended cases remain no-ops; conflict checks share the same identity; only null-versus-bare-key comparisons need status; state is shared within a move and fresh on the next call; failed planning cannot copy/delete; directory buckets skip the unsupported operation; the test fixture waits after first enablement and cleans only its own generated buckets. Traced public sync/async signatures, default pure-model arguments, version forwarding in CopySource and delete batches, S3Core request filtering/error translation and existing SDK/application retries. Logical per-bucket lookup counts are mock evidence, not a measurement of retry attempts.
Preserved upstream copy/get/rm pairing and #1082's offline command, parameter cases, du cleanup, SQLAlchemy/tox/reporting headings. Checked unchanged CI IAM and path filters: temporary-bucket provisioning remains opt-in; SQLAlchemy/Spark suites are outside the changed contracts. Existing static API docs and the explicit Enabled context agree.
Current evidence: format/lint/docs lint passed, current-source Sphinx build passed with 209 API/reference warnings (none on the changed Markdown, move methods, or target docstring), and 224 offline path/pairing/pandas/Polars tests passed. AWS regression and current CI are pending; prior-head runtime results will not be represented as results on this rebased head. No S3 Express live test or concurrent versioning-change guarantee is claimed.

Their ``null`` versions are distinct from their unversioned
keys. Use the same collection for :meth:`conflict_candidates`.

Returns:
The pairs, except those whose destination is the source itself or,
for a ``null`` version, the key of the source.
for a ``null`` version in a bucket without enabled versioning,
the key of the source.

Raises:
ValueError: If two sources have the same destination, or a
destination is another source, including one left in place,
except for a directory with no object at its key, which is not
copied. Also if ``missing`` is needed and None.
TypeError: If ``missing`` is a string instead of a collection.
Also if ``versioning_enabled_buckets`` is a string.
"""
if isinstance(missing, str):
raise TypeError("missing is a collection of paths, not a path.")
named, sources, candidates = self._moves(pairs)
named, sources, candidates = self._moves(pairs, versioning_enabled_buckets)
if missing is None and candidates:
raise ValueError("missing is needed to check the pairs.")
# A directory without an object at its key writes no destination.
skipped = set(candidates).intersection(
str(S3Path.parse(path).target) for path in missing or ()
self._target(S3Path.parse(path), versioning_enabled_buckets) for path in missing or ()
)
# A path with a version always names an object, so it writes its
# destination even when the key has no current object.
Expand Down Expand Up @@ -244,21 +266,44 @@ def _is_glob(path: str) -> bool:
"""
return has_magic(path) and not S3Path.has_version_id(path)

@staticmethod
def _target(path: S3Path, versioning_enabled_buckets: Collection[str]) -> str:
"""Return a move target using the caller's bucket versioning state.

Args:
path: The path of the source or destination.
versioning_enabled_buckets: Buckets whose versioning is enabled.

Returns:
The normalized path, retaining a ``null`` version only when its
bucket has versioning enabled.
"""
return str(path if path.bucket in versioning_enabled_buckets else path.target)
Comment thread
laughingman7743 marked this conversation as resolved.

@staticmethod
def _moves(
pairs: Sequence[tuple[str, str]],
versioning_enabled_buckets: Collection[str] = (),
) -> tuple[list[tuple[str, str, bool, str, str]], set[str], list[str]]:
"""Compare the paths of a move by what they name.

Args:
pairs: The sources and destinations of the move.
versioning_enabled_buckets: Buckets whose versioning is enabled.

Returns:
Each pair with whether its source has a version and the targets
of its source and destination; the targets of all sources,
including those left in place; and the conflict candidates (see
:meth:`conflict_candidates`).

Raises:
TypeError: If ``versioning_enabled_buckets`` is a string.
"""
if isinstance(versioning_enabled_buckets, str):
raise TypeError(

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementation repair self-review: CLEAN. Covered all six repair files plus shared callers and contracts using the verified old objects and patch-series comparison. The shared _moves entry rejects a string bucket context before target membership in both public methods; tuple/set/default behavior and internal set calls are preserved. The two regressions exercise the misleading logs/logs-archive substring case. The public model docs describe both required inputs and their default; sync/async exception and private helper docstrings match actual propagation. The valid AWS move path is unchanged by this guard; the opt-in run started on the old head remains in progress. Format/lint/docs lint/build passed and 226 self-contained tests passed.

Frozen repair: ca44c77..4a3edf6; PR base e41fe33. Patch series compared as e41fe33..ca44c77 versus e41fe33..4a3edf6.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Relayed independent repair review: CLEAN (no defects found). Claude Code claude-opus-5-5, verified first-party Max profile, high effort; session 180b3bea-3d55-4627-bdaa-8fe4c0d8090e, completed in 94257 ms. Model metadata confirms claude-opus-5-5 / firstParty; no permission denials.

Verified old/new series: e41fe33..ca44c77 versus e41fe33..4a3edf6. All six repair files and affected callers/contracts covered using the subject-free range-diff and literal repair diff. Read/Grep/Glob only; no edits, commands, builds/tests, network/GitHub or memory access. The exported snapshot remained unchanged, and the author worktree is clean.

The reviewer confirms the shared _moves guard rejects strings before any membership lookup in both public entry points, including empty pairs; valid collection and default behavior is preserved. Public docs describe the same collection for candidate discovery/final checks and the empty default. Private helper Args/Returns, sync/async OSError propagation, and the unversioned mock comment are accurate. The two tests exercise the substring failure scenario. No further actionable findings.

Static review only; runtime/lint/docs results are author validation. On the repaired head, format/lint/docs checks and 226 offline tests passed. At ca44c77, the serial real AWS selection passed all 66 tests, including nine opt-in cases, and temporary-bucket cleanup completed. The repair only rejects invalid string context and updates prose; the internal AWS caller continues to pass a set. S3 Express remains mock-only coverage. Current-head AWS CI will follow Ready.

"versioning_enabled_buckets is a collection of bucket names, not a bucket name."
)
named = []
for p1, p2 in pairs:
source_path = S3Path.parse(p1)
Expand All @@ -267,8 +312,8 @@ def _moves(
p1,
p2,
bool(source_path.version_id),
str(source_path.target),
str(S3Path.parse(p2).target),
S3PathPairing._target(source_path, versioning_enabled_buckets),
S3PathPairing._target(S3Path.parse(p2), versioning_enabled_buckets),
)
)
# The sources left in place count too; a copy onto one overwrites it.
Expand Down
Loading
Loading