Repository navigation
Open
0 / 50 of 5 issues completedOpen
0 / 50 of 5 issues completed
Copy link
Labels
Description
Part of #1327 (design problems 4 and 5 and defects 22 and 23 of the audit report).
Summary
Settle the shape of the response models, of S3Object as fsspec's info entry, and of the listing cache.
Current state (766d79d)
- Two model styles in two modules: frozen dataclasses with
from_responseins3_core.py(S3ObjectSummary,S3CommonPrefix,S3Bucket, pages, delete models) and property classes over raw dicts ins3_object.py(S3ObjectVersion,S3MultipartUpload,S3Owner,S3PutObject,S3MultipartUploadPart,S3CompleteMultipartUpload), with a circular import.S3ObjectVersion.namedrops the version; checksum fields cover 11 algorithms on parts and 4 elsewhere. S3Objectis aMutableMapping, not adict: keys live in__dict__, so a key namedkeys,items,get,updateorpopbreaksdict(o)ando.keys()(defect 22); it has noreprand is not JSON-serializable;update()does not remap API names while__init__does; directory and bucket entries carryNone-valued keys that file entries omit. Building it costs 3.8 µs per listed key, against 1.1 µs for adictsubclass built from the typed page and 0.25 µs for a plain dict (about 3 s of CPU per million keys infind).S3Metadatais both the core's HeadObject result and the user-metadata mapping of fsspec'smetadata(): an existing object without user metadata is falsy, and results of different objects compare equal to each other and to{}.S3Coreresults: three**paramsprecedence rules (sent as given withTypeErroron duplicates; arguments win; params win over the path ingenerate_presigned_url);copy_object,delete_object,replace_object_metadatareturnNoneand discard version and ETag;to_dict()ofS3PutObjectandS3CompleteMultipartUploadexposes_etag(returned bytouch()).S3Pathround trips: access-point ARN buckets cannot be parsed though the core accepts them;key=""re-parses asNone; a listed key ending in a version-like query (data?versionId=1) parses as version1ofdataand cannot be addressed (defect 23).- The listing cache is an ad-hoc key scheme (path,
"",(path, "/"),(path, ""),(path, "lookups")) spread over about nine methods, andinvalidate_cachere-enumerates every delimiter and query spelling.info()andls(detail=True)return the cachedS3Objectby reference, so a caller's mutation changes the cache.
Proposal (for decision)
S3Objectas adictsubclass with attribute access, built directly from the typed page models (from_summary,from_version,from_prefix,root), which fixes defect 22,reprand JSON, and the per-entry cost.- One module and one style for the response models; typed results for copy and delete; a typed HeadObject result with a
user_metadatamapping, whilefs.metadata()keeps returning the mapping. - One
**paramsrule acrossS3Core. - An escape for keys that end in a version-like query, or a documented limit.
- A typed cache object over
fs.dircachethat owns the key scheme and invalidation, shared by both filesystems; return copies or read-only entries frominfo()/ls().
Risks: isinstance(entry, dict) becomes true and the pickled form changes; S3Core.head_object's return type changes; tests that inspect fs.dircache (#1328).
Activity
laughingman7743 commentedon Nov 7, 2026
Deferred from #1370 (recursive
chmod, GH-1335), pre-existing, found by its independent review; they concern how paths and listed names are identified, so they belong with the response model here:a/xanda/x?versionId=null(no versioning),find("bucket/a")names the secondbucket/a/x?versionId=null;chmod(recursive=True)andrm(recursive=True)reparse that name as thenullversion ofa/x, soa/xis targeted twice and the literal key not at all.bucket//is both the bucket and the key/.S3Path.parse("bucket//")keeps the key/, whilefind()strips it to the bucket.chmod("s3://bucket//", acl, recursive=True)updates the objects but skipsPutBucketAcl(as on master).PathLikepaths.chmod(Path("bucket/a"), ...)raisesTypeErrorbecauseS3Path.parsereceives thePathbefore_strip_protocolconverts it.laughingman7743 commentedon Nov 7, 2026
Design audit at 29cc1d3 (offline: stub client, no AWS), before deciding the proposal above. It re-checks the issue text and the three deferred items of the comment above, inventories the callers, and records the decisions. Codex reviewed a first draft; its factual corrections were reproduced and are folded in.
Decided (2026-11-07): the recommended option of each item below. The sub-issues follow in the next comment.
Current state (29cc1d3)
Reproduced with a stub client unless marked "read".
from_responseins3_core.py, property classes over raw dicts ins3_object.py. The import cycle exists only underTYPE_CHECKING(s3_object.py:14).S3ObjectVersion("b", False, {"Key": "k", "VersionId": "v1"}).nameisb/k.S3MultipartUploadPart, 4 onS3PutObjectandS3CompleteMultipartUpload, none onS3Metadata. The installed botocore defines the same 10 algorithms (plusChecksumType, except UploadPart) as optional output fields of PutObject, CompleteMultipartUpload, HeadObject, GetObject and theCopyObjectResultof CopyObject.S3Object.dict:isinstance(entry, dict)is False,repr()is the default<S3Object object at ...>,json.dumpsraises.entry["keys"] = 1,dict(entry)raisesTypeError: 'int' object is not callable; afterentry["get"] = 1,entry.get("size")raises the same.entry.update({"ContentType": "x"})storesContentType, while__init__maps it tocontent_type.content_type,etag,last_modifiedandversion_idas None; file entries omit the fields that the response lacks.S3Object.from_summary2.36 µs andcopy()1.20 µs; a prototypedictsubclass 0.23 µs and itsdict.copy()0.07 µs (not a type-preserving copy).dictsubclass is JSON-serializable only for directory entries; file entries holdlast_modifiedas adatetime, as s3fs's entries do.S3Metadata.bool(S3Metadata({"ContentLength": 5}))is False, and two objects without user metadata compare equal to each other and to{}. This is the documented mapping behavior offs.metadata(); no caller in the package tests its truth value (read).S3Coreparameters and results (read).TypeErroron a duplicate (head_object,copy_object,delete_object, tagging, ACL, listings); the method's fields win (get_object,put_object, the multipart methods,list_object_annotations);paramswin over retained fields butTypeErrorfor the fields the copy sets (replace_object_metadata);paramswin over the path (generate_presigned_url).call()addsrequest_kwargsunder the request's own parameters (s3_core.py:651); Send the authorization parameters of s3_additional_kwargs with every filesystem request #1372 extends which filesystem parameters reach it.copy_object,delete_objectandreplace_object_metadatareturn None.to_dict()ofS3PutObjectandS3CompleteMultipartUploadreturns the private attribute names (_etag, ...), andtouch()returns it (s3.py:1491; asserted intest_s3.py:1276). s3fs'stouchreturns the raw PutObject response.S3Path.ValueError); s3fs 2026.9.0 splitsarn:aws:s3:us-east-1:123456789012:accesspoint/ap/kinto the ARN andk.S3Path("b", "")printsband re-parses with key None.split_path("bucket/data?versionId=1")drops the query and gives the keydata.""(buckets),(path, "/")and(path, "")(listings),(path, "lookups")(lookups with parameters)._invalidate_path(s3.py:2586) evicts four keys for each of the four query spellings of each ancestor.info()andls(detail=True)return cached entries by reference: afterfs.info("bucket/a")["size"] = 999the nextinfo()returns 999; the same forls. fsspec'sfind/walk/dudo not mutate entries (read); s3fs's_lsdiralso returns its cached list.AioS3FileSystemshares the syncdircache(s3_async.py:161), and its_findcalls the sync_find(s3_async.py:788).Deferred items of the comment above:
a/xanda/x?versionId=nullin an unversioned bucket,find("bucket/a")namesbucket/a/x?versionId=null, and:chmod(recursive=True)updatesa/xtwice (once withVersionId=null) and never the literal key;rm(recursive=True)deletesa/xanda/xversionnull, and leaves the literal key;copy(recursive=True)raisesValueError: Cannot copy to a versioned file.after copyinga/x;mv(recursive=True)raisesValueError: Cannot move several paths to the same destination.before copying;du(fsspec's, throughinfo()of each name) countsa/xonce under one name;info("bucket/a/x?versionId=null")looks upa/xwithVersionId=null(defect 23).a/dir?versionId=v1/z,find("bucket/a")lists it butfind("bucket/a", maxdepth=2)does not, since_find_levels(s3.py:930) re-parses the directory name;_invalidate_path(s3.py:2594) can take part of such a key for a version and skip the parent's listing.bucket//).S3Path.parse("b//")is the key/withis_bucketTrue.find("bucket//")and recursivechmod("s3://bucket//")list the bucket; the recursivechmodsends no PutBucketAcl.rm_file("bucket//")deletes the key/. So the spelling is the key/for single-object operations and the bucket for listings.PathLike) withpathlib.Path("bucket/a"):chmod,cat_file,rm_file,touch,metadata,sign,getxattr,setxattr,get_tags,object_version_info,cp_file,list_multipart_uploadsandput_file(remote path) raiseTypeErrorfromS3Path.parse;rmraisesTypeError: 'PosixPath' object is not iterable;globraisesAttributeError(endswith).info,exists,ls,find,du,sizeandchecksumwork through fsspec's_strip_protocol.Callers and compatibility surface
S3Objectis returned byinfo,ls/find/glob/walkwithdetail=True, thedetailsof an open file and their aio counterparts, is stored infs.dircache, and is in the API reference (docs/api/filesystem.rst:113). Itsto_api_repr()feeds append mode (Let the caller's parameters take precedence in append mode #1373 changes that precedence), andcreated/modifiedand read-version/ETag pinning read its fields.S3Core,S3Path,S3PutObject,S3MultipartUploadand the page models are documented indocs/filesystem.md;S3Metadatais the documented return offs.metadata()/S3File.metadata()and theheadargument ofS3Core.replace_object_metadata, with a published{**head, ...}example (docs/filesystem.md:563).dircacheundertests/pyathena/filesystem; Audit the filesystem tests changed since the #1239 audit #1328 left the design-dependent cache/model test rewrites to this issue.to_api_repr()), Run the batch operations of S3FileSystem concurrently (GH-1322) #1344 (Run the batch operations of S3FileSystem concurrently #1322 step 4a: copy/mv pairing and therm/chmodfan-outs), Remove the duplication between S3FileSystem and AioS3FileSystem #1340 (sync/aio duplication).Decisions
S3Objectas adictsubclass. Recommended: yes, with attribute access for known fields (None for an absent known field,AttributeErrorotherwise), attribute set/delete mapped to the entries,copy()returning anS3Object,to_dict()/to_api_repr()unchanged, and theS3Object(init, **kwargs)constructor still mapping API names. Entries keep their current key sets (directories keep their None-valued keys), soentry["etag"]does not change toKeyError. Breaking:isinstance(entry, dict)becomes True and the pickled form changes (release note).info,ls/find/glob/walkdetails, filedetails, aio_find) returns copies that also copy the nestedmetadatamapping; the cost is measured on the type-preserving copy before merging.S3ObjectVersiongains a typedpath(itsnamestaysbucket/key). Whether pickles of the property classes must still load is part of this decision; recommended: no (release note).S3MetadataforS3Core.head_objectandfs.metadata()and document that its truth value and equality are those of the user metadata; no new type. A new record type would break the published{**head, ...}usage for little gain.copy_object/replace_object_metadatareturn anS3CopyObject(destination version ID, source version ID, ETag, last modified, checksums) anddelete_objectanS3DeleteObject(version ID, delete marker), in place of None.touch()options: (a) unchanged, documented; (b) the raw PutObject response, as s3fs; (c) public field names. Recommended: (b) in the release with the other breaking changes, since (c) is breaking without gaining s3fs parity.**paramsrule. Recommended principle: fields that a method derives from its arguments raiseTypeErrorwhen also given inparams; defaults the method fills in (retained metadata,request_kwargs) yield toparams. The PR first tabulates each method's derived fields (includingCopySourceRange,MultipartUpload, checksum and pagination fields), keeps documented fallbacks such asrange_=NonewithRange=, and normalizes the writer's internal calls (s3_writer.py:219) that forward checksum parameters._find_levels), pairing, the fan-outs, lookups and invalidation, using a private sequence of typed paths; publicfind()/expand_path()still return strings. Addressing such a key by a string path stays a documented limit, as in s3fs, withS3Coreand an explicitS3Pathas the way to reach it; no escape syntax, since any spelling is itself a possible key.bucket//(C2). Options: (a) the bucket for every operation (removesrm_file's access to the key/); (b) keep the current split and document it, including that a recursivechmodofbucket//does not change the bucket ACL. Recommended: (b).PathLike(C3). Recommended: every public method convertsos.PathLikewithfsspec.utils.stringify_pathbefore classifying the path (includingrm's iterable check,globand the pairing'sisinstance(..., str)), without stripping slashes.S3Pathdetails. Recommended:S3Path(bucket, "")normalizes the key to None. Access point ARNs as buckets become a separate enhancement issue, since parent, pairing and cache handling need ARN-aware buckets.S3ListingCacheoverfs.dircache(still fsspec'sDirCache, souse_listings_cache,listings_expiry_time,max_pathsand empty-cache pickling keep working), shared by both filesystems, that owns the key scheme. It keeps today's behavior: the four version-query spellings, explicitversion_id, trailing-slash and empty-segment variants, eviction of the bucket listing only when it lists the bucket, uncachednullversions, no caching for prefix/token listings, per-parameter lookups with their own timestamps and hashed SSE-C keys, and the_ls_from_cacheoverride. Tests that read dircache keys are rewritten in that PR, keeping their request/expiry/invalidation evidence.Split
One sub-issue of this issue per PR:
PathLike(item 9). Small and independent; first.key=""of 10). After Run the batch operations of S3FileSystem concurrently (GH-1322) #1344 merges, since both rewrite pairing and the fan-outs; by the owner of Run the batch operations of S3FileSystem concurrently (GH-1322) #1344.touch(), the**paramsrule) in the same release.laughingman7743 commentedon Nov 7, 2026
Sub-issues of the decisions above, one PR each:
os.PathLikepaths in every method (decision 9). First; small and independent.bucket//documented;key=""normalized (decisions 7, 8, 10). After Run the batch operations of S3FileSystem concurrently (GH-1322) #1344 merges.S3Objectas adictsubclass and copies of cached entries (decisions 1, 2).touch()and the**paramsrule (decisions 3–6).Access point ARNs (decision 10) are split out to #1385.