From 5addecda926178912127e3b6049cfbe5f8e91209 Mon Sep 17 00:00:00 2001 From: laughingman7743 Date: Tue, 27 Oct 2026 00:23:18 +0900 Subject: [PATCH 1/3] Document that copies of a delete-marker source version raise OSError(EINVAL) (GH-1346) CopyObject and UploadPartCopy whose source names a delete-marker version are answered with 400 InvalidRequest, without the x-amz-delete-marker header, so S3Core.copy_object() and upload_part_copy() raise OSError with EINVAL rather than FileNotFoundError. The filesystem's copies look up the source first and raise FileNotFoundError. Co-Authored-By: Claude Opus 5.5 --- docs/filesystem.md | 6 ++++-- pyathena/filesystem/s3_core.py | 9 ++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/filesystem.md b/docs/filesystem.md index a4e279500..4344d754d 100644 --- a/docs/filesystem.md +++ b/docs/filesystem.md @@ -399,8 +399,10 @@ operation sends one request (one per page for the iterators and described below, send several, and `generate_presigned_url()` signs a URL locally without a request. The requests are sent with the retry policy. An operation raises `FileNotFoundError` for a missing bucket or multipart upload, or for a missing object or -version that it reads, and caches nothing. Requests sent through `fs.core` do not -invalidate the filesystem's cache: call +version that it reads, and caches nothing. `copy_object()` and `upload_part_copy()` with +a source version that is a delete marker raise `OSError` with `EINVAL`; `cp_file()`, +`copy()` and `mv()` of the filesystem raise `FileNotFoundError` for it. Requests sent +through `fs.core` do not invalidate the filesystem's cache: call `fs.invalidate_cache()` after a change, or make it through the filesystem. ```python diff --git a/pyathena/filesystem/s3_core.py b/pyathena/filesystem/s3_core.py index 84b98d94b..e3ce23b02 100644 --- a/pyathena/filesystem/s3_core.py +++ b/pyathena/filesystem/s3_core.py @@ -501,7 +501,9 @@ class S3Core: :class:`~pyathena.filesystem.s3_errors.S3ClientError`): a missing bucket or multipart upload, or a missing object or version that an operation reads, raises ``FileNotFoundError``, and a denied request - ``PermissionError``. + ``PermissionError``. A copy whose source names a version that is a delete + marker raises ``OSError`` with ``EINVAL``, as S3 rejects it as an invalid + request. As in S3, deleting a missing key is not an error. Nothing is cached. Example: @@ -1019,6 +1021,8 @@ def upload_part_copy( Raises: ValueError: If the upload has no bucket, key, or upload ID, or the source has no key. + OSError: With ``EINVAL`` if the source names a version that is a + delete marker. """ request = self._multipart_upload_request(upload) if not source.key: @@ -1176,6 +1180,9 @@ def copy_object(self, source: S3Path, destination: S3Path, **params) -> None: Raises: ValueError: If the source or the destination has no key, or the destination has a version ID, which a write cannot replace. + FileNotFoundError: If the source object or version does not exist. + OSError: With ``EINVAL`` if the source names a version that is a + delete marker. """ if not source.key: raise ValueError(f"The source has no key: {source.uri}.") From 247bf10687ffcaca9f41d37350fea88cd78f8383 Mon Sep 17 00:00:00 2001 From: laughingman7743 Date: Tue, 27 Oct 2026 00:24:09 +0900 Subject: [PATCH 2/3] Name the two copy operations in the S3Core docstring (GH-1346) plan_multipart_copy() looks up the source with HeadObject first and raises FileNotFoundError for a delete-marker version, so the class docstring names copy_object() and upload_part_copy() instead of every copy. Co-Authored-By: Claude Opus 5.5 --- pyathena/filesystem/s3_core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyathena/filesystem/s3_core.py b/pyathena/filesystem/s3_core.py index e3ce23b02..330c62a29 100644 --- a/pyathena/filesystem/s3_core.py +++ b/pyathena/filesystem/s3_core.py @@ -501,9 +501,9 @@ class S3Core: :class:`~pyathena.filesystem.s3_errors.S3ClientError`): a missing bucket or multipart upload, or a missing object or version that an operation reads, raises ``FileNotFoundError``, and a denied request - ``PermissionError``. A copy whose source names a version that is a delete - marker raises ``OSError`` with ``EINVAL``, as S3 rejects it as an invalid - request. + ``PermissionError``. :meth:`copy_object` and :meth:`upload_part_copy` + with a source version that is a delete marker raise ``OSError`` with + ``EINVAL``, as S3 rejects the request as invalid. As in S3, deleting a missing key is not an error. Nothing is cached. Example: From d3ae3a9ce8b101673332a7af18bc68d49af5ea98 Mon Sep 17 00:00:00 2001 From: laughingman7743 Date: Tue, 27 Oct 2026 00:27:05 +0900 Subject: [PATCH 3/3] Narrow the copy docs to what the copies raise (GH-1346) - The filesystem paragraph names cp_file() only: copy() and mv() follow fsspec, which skips a missing source with on_error="ignore" (the recursive default) and drops a missing version from expand_path(). - copy_object() and upload_part_copy() list FileNotFoundError for every missing resource they can report, measured for a removed source version and a missing source key, and EINVAL for an invalid request, which a malformed version ID also gives. Co-Authored-By: Claude Opus 5.5 --- docs/filesystem.md | 6 +++--- pyathena/filesystem/s3_core.py | 15 +++++++++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/filesystem.md b/docs/filesystem.md index 4344d754d..307f4852b 100644 --- a/docs/filesystem.md +++ b/docs/filesystem.md @@ -400,9 +400,9 @@ described below, send several, and `generate_presigned_url()` signs a URL locall without a request. The requests are sent with the retry policy. An operation raises `FileNotFoundError` for a missing bucket or multipart upload, or for a missing object or version that it reads, and caches nothing. `copy_object()` and `upload_part_copy()` with -a source version that is a delete marker raise `OSError` with `EINVAL`; `cp_file()`, -`copy()` and `mv()` of the filesystem raise `FileNotFoundError` for it. Requests sent -through `fs.core` do not invalidate the filesystem's cache: call +a source version that is a delete marker raise `OSError` with `EINVAL`, while the +filesystem's `cp_file()` raises `FileNotFoundError` for it, as for a missing file. +Requests sent through `fs.core` do not invalidate the filesystem's cache: call `fs.invalidate_cache()` after a change, or make it through the filesystem. ```python diff --git a/pyathena/filesystem/s3_core.py b/pyathena/filesystem/s3_core.py index 330c62a29..802326eea 100644 --- a/pyathena/filesystem/s3_core.py +++ b/pyathena/filesystem/s3_core.py @@ -503,7 +503,7 @@ class S3Core: reads, raises ``FileNotFoundError``, and a denied request ``PermissionError``. :meth:`copy_object` and :meth:`upload_part_copy` with a source version that is a delete marker raise ``OSError`` with - ``EINVAL``, as S3 rejects the request as invalid. + ``EINVAL``. As in S3, deleting a missing key is not an error. Nothing is cached. Example: @@ -1021,8 +1021,10 @@ def upload_part_copy( Raises: ValueError: If the upload has no bucket, key, or upload ID, or the source has no key. - OSError: With ``EINVAL`` if the source names a version that is a - delete marker. + FileNotFoundError: If the upload, or the source object or version, + does not exist. + OSError: With ``EINVAL`` for an invalid request, such as a source + version that is a delete marker. """ request = self._multipart_upload_request(upload) if not source.key: @@ -1180,9 +1182,10 @@ def copy_object(self, source: S3Path, destination: S3Path, **params) -> None: Raises: ValueError: If the source or the destination has no key, or the destination has a version ID, which a write cannot replace. - FileNotFoundError: If the source object or version does not exist. - OSError: With ``EINVAL`` if the source names a version that is a - delete marker. + FileNotFoundError: If a bucket, or the source object or version, + does not exist. + OSError: With ``EINVAL`` for an invalid request, such as a source + version that is a delete marker. """ if not source.key: raise ValueError(f"The source has no key: {source.uri}.")