Skip to content

feat(array): add ArrayIO and AsyncArrayIO, the explicit-IO layer under Array and AsyncArray - #419

Draft
d-v-b wants to merge 49 commits into
mainfrom
claude/arrayio-lazy-metadata-48d8e5
Draft

d-v-b wants to merge 49 commits into
mainfrom
claude/arrayio-lazy-metadata-48d8e5

Conversation

@d-v-b

@d-v-b d-v-b commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

🤖 AI text below 🤖

Added zarr.ArrayIO and zarr.AsyncArrayIO, an explicit-IO layer over Zarr arrays. Constructing one performs no IO against storage: storing metadata (store_metadata), storing or reading chunks (setitem / getitem), and their erasing inverses (erase_metadata, erase_chunk, erase_chunks) are explicit method calls that run in whatever order the caller chooses — so chunks can be written before metadata, as in the array API of zarrs. open auto-detects the Zarr format by default, as zarrs does, and verify reads the stored metadata and raises, naming each differing field, when it does not match the bound metadata.

The binding holds the metadata document as a zarr-metadata model — zarr-python now depends on zarr-metadata>=0.6.0, its first sibling-package dependency — reachable as ArrayIO.document. Document operations (store_metadata, erase_metadata, erase_chunk, erase_chunks, verify, equality) never resolve codec or data type names: an array whose codecs this environment lacks still opens for document work and refuses only when chunks are read or written. The strict model read refuses a stored document zarr can only read as repaired (AsyncArray.open remains the repairing reader), and instances built from resolved metadata — every AsyncArray — keep deriving their chunk grid, chunk keys and equality from the resolved form, because zarr-python writes some documents the strict model refuses (Zarr format 2 generic time units).

AsyncArray is now an array-like layer over this one: it holds a single wrapped AsyncArrayIO, exposes metadata / store_path / config / codec_pipeline as properties over it, and delegates its metadata and selection IO to it, with resize rebinding the wrapped state so the two layers cannot drift. AsyncArrayIO.to_array() returns an AsyncArray sharing the same binding. AsyncArrayIO defines value equality over the full binding, preserving AsyncArray's previous dataclass equality.

The normalization half of init_array is extracted into the pure build_array_metadata, which init_array and AsyncArrayIO.build share; init_array's behavior is unchanged.

Merge posture: this PR introduces no new configuration concepts. ArrayIO meets the global configuration exactly as Array always has — the codec pipeline implementation is captured when a binding is made, the runtime configuration defaults from zarr.config, build_v2's order defaults from array.order, and build_v3's default codecs come from the v3_default_* keys. Scoping resolution per binding (and the path to deprecating the global state) is the follow-up, #420.

Design notes:

  • Declaration never hides the v2 / v3 difference: build_v2 and build_v3 each take the actual fields of their format's document (per-format ArrayV2DeclarationKwargs / ArrayV3DeclarationKwargs TypedDicts), so a format mistake — shards on a v2 array, order on a v3 one — is inexpressible instead of guarded. The documents themselves are built by module-level build_array_metadata_v2 / build_array_metadata_v3, lifted from the AsyncArray._create_metadata_* staticmethods (the module-level home misplaced metadata creation methods on AsyncArray class zarr-developers/zarr-python#4175 asks for). The declaring constructors are synchronous: binding is pure, so they take a Store or StorePath (coercing path and URL strings opens stores, which stays create_array's job), and stores open themselves on their first IO. build_v3's chunk_grid accepts the grid metadata, its JSON, or a chunk shape declaring a regular grid, as ArrayBuilder does in zarrs.
  • Every write has an erasing inverse (store_metadata ↔ erase_metadata, chunk writes ↔ erase_chunk / erase_chunks), each succeeding when the target is already absent, copying zarrs.
  • store_metadata() writes unconditionally, like store_metadata in zarrs; guarded creation (raising when a node exists) remains create_array's job, and chunks of a previous array at the location are kept — erase_chunks is the broom.
  • The bound metadata is assumed, never verified against the store (TensorStore's assume_metadata hazard); this is documented on the class. verify is the explicit check.
  • The classes live in zarr.core.array and are exported from the zarr namespace, next to the classes built on them; the wrapped layer is reachable as Array.io / AsyncArray.io, the inverse of to_array().

Issues this layer addresses or advances: zarr-developers#4217 (the 2026-10-29 comment: avoiding implicit IO, writing chunks before metadata — the Array-over-engine layering that issue describes now exists at the array layer), zarr-developers#1435 (idempotent, non-deleting parallel creation), zarr-developers#4175 (build_array_metadata moves metadata creation off AsyncArray), zarr-developers#4399 / zarr-developers#1754 (storage-free declaration with caller-scheduled IO), zarr-developers#3017 (metadata IO as explicit free-standing operations), and zarr-developers#4027 / zarr-developers#3782 (the Array / AsyncArray relationship: the sync ArrayIO still routes through sync(), so the event-loop issues — zarr-developers#2878, zarr-developers#3487, zarr-developers#3859 — need a sync engine under zarr-developers#4217 before they close).

Stack

Base of the stack. #420 (Context scoping) stacks on this branch.

🤖 Generated with Claude Code

d-v-b added 4 commits October 29, 2026 11:53
…y layer

Declaring an array today eagerly stores its metadata, so chunks cannot be
written first. ArrayIO and AsyncArrayIO bind an array metadata document to
a store location without performing any IO on construction; storing
metadata (store_metadata) and storing or reading chunks (setitem/getitem)
are explicit method calls that run in whatever order the caller chooses,
mirroring the array APIs of zarrs and zarrita.

The normalization half of init_array is extracted into the pure
build_array_metadata, which init_array and AsyncArrayIO.build share;
init_array's behavior is unchanged.

Refs zarr-developers#4217

Assisted-by: ClaudeCode:claude-fable-5
Assisted-by: ClaudeCode:claude-fable-5
…ing open

Every write now has an inverse, copying zarrs: store_metadata ↔
erase_metadata, chunk writes ↔ erase_chunk / erase_chunks, each
succeeding when the target is already absent. open's zarr_format now
defaults to None (read format 3, fall back to format 2), matching
zarrs' open, instead of requiring format 3.

Adversarial review against zarrs and TensorStore also found contract
gaps now documented: the bound metadata is assumed, never verified
(TensorStore's assume_metadata hazard); store_metadata keeps any
previous array's chunks; a repaired document is stored before the
first chunk write; and the module docstring no longer cites zarrita,
whose create is eager.

Assisted-by: ClaudeCode:claude-fable-5
… store

A writer that declared an array independently (the distributed
chunks-first pattern: one coordinator stores metadata, N workers build
the same declaration) can assert its assumption before writing chunks.
verify() reads the stored document, favoring format 3 when both exist,
and raises naming each differing field — the explicit counterpart of
TensorStore's spec-constraint validation on open.

Assisted-by: ClaudeCode:claude-fable-5
d-v-b added 3 commits October 29, 2026 13:04
The explicit-IO layer is the lower layer, so it moves into
zarr.core.array as a self-contained class holding its own (metadata,
store path, config, codec pipeline, chunk grid) state, with
zarr.experimental.array_io re-exporting it. AsyncArray now holds one
field, the wrapped AsyncArrayIO, exposes metadata/store_path/config/
codec_pipeline/_chunk_grid as properties over it, and delegates its
metadata and selection IO to it; resize rebinds the wrapped state, so
the two layers cannot drift. AsyncArrayIO defines value equality over
the full binding, preserving AsyncArray's previous dataclass equality,
and to_array() returns an AsyncArray sharing the same binding. The
repair-before-chunk-write hook and the stored-document mark reset
become module functions used by the one remaining implementation.

Refs zarr-developers#4217

Assisted-by: ClaudeCode:claude-fable-5
ArrayIO and AsyncArrayIO move out of zarr.experimental into the zarr
namespace, next to Array and AsyncArray, which are built on them: the
wrapped layer is reachable as Array.io / AsyncArray.io, the inverse of
to_array(). The zarr.experimental.array_io shim is removed and the
layer is documented on the array API page.

Assisted-by: ClaudeCode:claude-fable-5
@d-v-b d-v-b changed the title feat(experimental): add ArrayIO and AsyncArrayIO, an explicit-IO array layer feat(array): add ArrayIO and AsyncArrayIO, the explicit-IO layer under Array and AsyncArray Oct 29, 2026
d-v-b added 3 commits October 29, 2026 14:18
zarr-python now depends on zarr-metadata (>=0.6.0): AsyncArrayIO holds
one source — the document, a zarr_metadata model, when constructed by
open; the resolved metadata when constructed by build or by AsyncArray
— and derives the other form, the chunk grid, the chunk key encoding
and the codec pipeline as caches. Document operations (store_metadata,
erase_metadata, erase_chunk, erase_chunks, verify, equality) never
resolve codec or data type names, so an array whose codecs this
environment lacks still opens for document work and refuses only when
the data path is built; a document zarr can only read as repaired is
refused by the strict model read, with AsyncArray.open remaining the
repairing reader. Grid derivation goes through the same grid-metadata
objects ChunkGrid.from_metadata dispatches on, so both derivations lay
out chunks identically, and model JSON is normalized through JSON text
because the model returns read-only forms.

Resolved-seeded instances keep deriving their grid, chunk keys and
equality from the resolved metadata: zarr-python writes some documents
the strict model refuses (Zarr format 2 generic time units), and the
array API's behavior must not depend on the model accepting them.

Refs zarr-developers#4217

Assisted-by: ClaudeCode:claude-fable-5
…ce once

Binding is pure, so build stops being awaitable: it takes a Store or a
StorePath and binds with StorePath construction, never make_store_path,
whose coercion of path and URL strings opens stores — the IO the method
promises not to do. Stores open themselves on their first IO, so an
unopened store binds safely. The declaration keyword surface is declared
once, as the ArrayDeclarationKwargs TypedDict that both build methods
unpack, instead of being copied into each; zarr_format on build no
longer admits None, whose config-default meaning collided with open's
auto-detect meaning one method away. The constructors return Self, and
build_array_metadata's fill_value drops a redundant None from its type.

Assisted-by: ClaudeCode:claude-fable-5
…outine

Nothing hides the v2 / v3 difference any more: build_array_metadata is
replaced by module-level build_array_metadata_v2 and
build_array_metadata_v3, each taking the actual fields of its document
(.zarray plus .zattrs, and zarr.json), with the bodies lifted from the
AsyncArray._create_metadata_* staticmethods, which now delegate (the
module-level home zarr-developers#4175 asks for). init_array regains its normalization
and calls the per-format builders. ArrayIO and AsyncArrayIO declare with
build_v2 and build_v3 over per-format TypedDicts, so a format mistake —
shards on a v2 array, order on a v3 one — is inexpressible instead of
guarded, and build_v3's chunk_grid accepts the grid metadata, its JSON,
or a chunk shape declaring a regular grid, as ArrayBuilder does in
zarrs.

Refs zarr-developers#4175

Assisted-by: ClaudeCode:claude-fable-5
Comment thread src/zarr/core/array.py Outdated
)

@property
def document(self) -> ArrayDocumentModel:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

rename to metadata_document

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🤖 AI text below 🤖

Superseded by the follow-up naming: the property is metadata (the zarr-metadata model) and the legacy resolved view is metadata_legacy (8956b96).

Comment thread src/zarr/core/array.py Outdated

def __init__(
self,
document: ArrayDocumentModel | ArrayMetadata | ArrayMetadataDict,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

the bare name "document" is confusing. imo we should just call it metadata.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

🤖 AI text below 🤖

The agent removed the term: the constructor parameter and the model property are both metadata, and the resolved ArrayV2Metadata / ArrayV3Metadata view is metadata_legacy, per the follow-up instruction (ee8b2d1, 8956b96).

d-v-b and others added 7 commits October 31, 2026 15:57
…ta_legacy

ArrayIO's metadata is the zarr_metadata model, full stop: the property
is named metadata, the resolved ArrayV2Metadata / ArrayV3Metadata view
— which the data path runs on and AsyncArray.metadata still returns —
is metadata_legacy, and the term document leaves the surface. The
with_metadata, with_store and with_config methods return new bindings
with one part replaced, with with_config merging partial configuration
the way AsyncArray.with_config does.

Assisted-by: ClaudeCode:claude-fable-5
Assisted-by: ClaudeCode:claude-fable-5
…does

The codec pipeline implementation is captured when the binding is made —
the capture discipline AsyncArray has always had — through a new
pipeline_class parameter on create_codec_pipeline, while the pipeline
itself still builds on first use, so metadata-only work resolves
nothing. build_v2's order defaults from the global array.order instead
of a static C. Nothing about ArrayIO requires new configuration
concepts; scoping resolution per binding stays in the follow-up that
deprecates the global state.

Assisted-by: ClaudeCode:claude-fable-5
The zarr.json field is data_type, so build_array_metadata_v3 and the
build_v3 declaration take data_type; the v2 builder keeps dtype, which
is the .zarray field's own name.

Assisted-by: ClaudeCode:claude-fable-5
…data

Addresses the roborev review of this branch (job 1359). with_metadata,
with_store and with_config copy a legacy source object, so the
_stored_document repair mark and in-place attribute updates never leak
between bindings, and they carry the captured pipeline implementation
instead of re-reading the global configuration at call time;
AsyncArray.with_config routes through the binding and inherits all of
that. Storing a v2 model without attributes now deletes a stale
.zattrs, matching the unconditional-replacement contract, and the
model branch honors json_indent. erase_chunks reads async.concurrency
with an explicit None check instead of a falsy or-default. Behavioral
tests cover array-to-binding state propagation, derivative
independence, and the stale-.zattrs case.

Assisted-by: ClaudeCode:claude-fable-5
`copy.copy` on a legacy metadata object shared its `attributes` dict, so an
in-place attribute update still reached bindings derived before it. Derived
sources now go through `_copied_legacy`, which copies the dict and keeps the
repair mark. `_derive` fills the fields of a new binding directly instead of
re-running `__init__` (which re-read the global pipeline choice), and
`AsyncArray.with_config` builds the derived pipeline eagerly and constructs
through `type(self)`, keeping subclass identity and warning timing.

Assisted-by: ClaudeCode:claude-fable-5
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
d-v-b and others added 8 commits November 1, 2026 12:09
… on metadata

Whether a stored array document needs a repair that moves chunks is a fact
about the store, so it now lives in `zarr.core.metadata.stored`: a cache per
store object, keyed by array path and Zarr format, filled by metadata reads
(once the document has been read as metadata) and writes, and consulted
before chunk writes, which read the store on a miss. Consolidated copies of
such documents are recorded apart, so a group write keeps them as stored.

This removes the `_stored_document` attribute from `ArrayV2Metadata` and
`ArrayV3Metadata`, and every place that had to carry or clear it: derived
bindings, re-resolution and resize no longer manage store state.
`mark_repaired` becomes `warn_repairs`, and each `from_dict` is a private
`_read_stored` reader plus its warning, so the cache can read stored
documents without the rectilinear flag that gates storing them.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…quires

zarr-metadata 0.6.0 requires typing_extensions>=4.16, so zarr's effective
minimum was already 4.16 while it declared >=4.14, and the min_deps test
environment's typing_extensions==4.14.* pin could not resolve. Declare the
real floor and pin min_deps to 4.16.*.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A store's cache does not pickle with the store, so every unpickled binding
(each task of a distributed write) read its metadata document again before
its first chunk write. A pickled AsyncArrayIO now carries what its store's
cache knew of its own document, restored into the new store object's cache
unless that one already knows it, as the removed mark used to pickle with the
metadata. The StoredDocuments docstring now says that a chunk write's own
read is recorded as read.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`__init__` and `_derive` each listed every field of an AsyncArrayIO; both now
go through `_bind`, so a field added later cannot be missed by one of them.
`_copied_legacy` drops a guard that never fired and says what its copy
isolates: top-level attribute updates. Tests cover with_store and
with_metadata from a legacy source, a derived binding keeping its captured
pipeline while the global configuration names one that cannot load, and
StoredDocuments.restore keeping what the cache learned itself. The
installation guide gives the new typing_extensions floor.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ds it

with_metadata copied a legacy object without the reading parse_array_metadata
gives it, so ArrayV2Metadata with a chunk size of 0 bound unrepaired there
while the constructor bound it repaired. A test binds such metadata through
the constructor, build_v2 and with_metadata.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The repair check before a chunk write is only as fresh as the store
object's cache: once another binding of that store object has read or
stored a newer document, an array opened before then writes its chunks
under its own metadata without the check, as any stale array handle
does. This is the accepted behavior of the per-store cache; the removed
metadata mark had re-read the store for such handles.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ject codecs

A v2 binding read as a zarr-metadata model copied only the top level of the
model's read-only attributes into its legacy metadata, so nested lists and
objects stayed tuples and mapping proxies and every metadata write through it
(update_attributes, resize, append) failed to encode. The attributes now come
from the model's own `.zattrs` document.

build_array_metadata_v2 (and build_v2) accepted a variable-length string
data type with no object codec, storing metadata nothing can read; it now
makes the check create_array makes, shared as `_check_object_codec_v2`.
store_metadata's docstring says what it replaces: the documents of its own
format, leaving the other format's, a group's and created parents in place.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AsyncArray's only dataclass field had become the wrapped binding, so
dataclasses.fields/asdict/replace, the generated equality and hashing, the
pickled state and dask tokens (dask tokenizes dataclasses by their fields)
all changed, and copy.copy shared one binding. AsyncArray now declares the
fields it always had (metadata, store_path, codec_pipeline, _chunk_grid,
config), each a property over the binding that object.__setattr__ can still
set, and pickles as that field state, so pickles load across versions in
both directions and copies are independent. _save_metadata keeps its
signature, and with_config rebuilds the array as it always did, re-reading
the global pipeline choice.

A chunk write no longer reads a document the store's cache has not seen:
unknown means nothing is known to repair, as unmarked metadata always did,
so writes through consolidated members, code-built arrays and their
unpickled copies make no new metadata reads. A pickled binding carries a
consolidated copy's entry as well as its document's.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d-v-b and others added 24 commits November 1, 2026 15:01
…natures

AsyncArrayIO and ArrayIO read a metadata dict as a zarr-metadata model when
it is valid JSON metadata, as zarrista's from_metadata does, so an extension
zarr-python lacks fails only when chunks are read or written; metadata built
in code, or an invalid document zarr reads as repaired, is still read into
the legacy classes when bound. AsyncArray keeps reading dicts the legacy way.
erase_chunk takes grid coordinates positionally, as any sequence, and
setitem takes its prototype by keyword, matching zarrista before either
signature is released.

AsyncArray's pickled state carries the pipeline class its binding chose,
which may differ from its pipeline instance's class. The object-codec check
applies to build_array_metadata_v2 only, so zarr.create(zarr_format=2)
behaves as before. Also: drop an unpickling shim for a state no release
wrote, a dead read guard, and a duplicate attribute encode.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…els lazily

A pickled or deep-copied consolidated group lost what its store's cache knew
of its members, so a group write through the copy re-encoded a member read
from a repaired copy instead of keeping it as stored (and raised for a
rectilinear one). AsyncGroup now carries those entries in its pickled state.
An AsyncArray built from a stored document given as a dict records it, so its
first chunk write stores the repair, as before.

The cache records a valid document only where it replaces one that needs a
repair or a consolidated copy that does, so it no longer grows with every
array a store object sees; group writes are not re-parsed to record them,
and zarr_metadata is imported when a binding first holds a model, so
`import zarr` costs what it did. A legacy-sourced binding derives its model
on each access, so an in-place change through a handle sharing its metadata
is never stale. ArrayIO.open opens a path writable without creating it.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…antics

A metadata dict with storage transformers bound as a model and failed only at
the first chunk operation; it is now rejected when bound, as AsyncArray
always rejected it. The cache's `knows` and the repair check's docstring say
that a valid document is recorded only where it replaces a broken one, and a
test pins that a member read valid and then from a broken consolidated copy
is kept as the copy stored, as before. Tests also cover a pickled subgroup
of a consolidated parent, a binding derived from an unpickled array building
the pipeline class it chose, and `import zarr` leaving zarr_metadata
unimported.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ipelines

ArrayIO and AsyncArrayIO type their metadata argument to take
zarr_metadata's JSON types (what zarrista's arrays hold), and with_metadata
reads a metadata dict as the constructor does instead of binding the dict
itself. A pickled AsyncArrayIO no longer carries its built codec pipeline,
which is bound to the pickling process's store object and may not pickle;
it is rebuilt on first use. The class docstring words name resolution in the
data path as what today's pipelines do.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… fields

erase_metadata deleted the array documents of its format at its location
even when they were a group's (a v3 group's zarr.json, a v2 group's
.zattrs); it now deletes only an array's, and storing an attribute-less v2
model leaves a group's .zattrs in place. The AsyncArrayIO constructor copies
a legacy object it is given, as with_metadata and the derivations do, while
AsyncArray keeps binding the object itself through _from_resolved. A
metadata dict whose reading moves chunks binds as repaired legacy metadata
and is recorded as the stored document, also where the model accepts it
(a v2 chunk size of 0). verify names the fields that differ as the model
compares them. build_array_metadata_v3 keeps an empty dimension_names list
(the creation paths still read it as none, as they always have),
erase_chunk takes integer coordinates only, and binding equality no longer
depends on what has been resolved.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… before

zarr 3.4.1 and 3.4.2 released the repair mark on metadata objects
(`_stored_document`), so pickles cross versions: an array pickled by them
now has its mark read into the store's cache, a consolidated group's
members likewise, and an array pickled here marks a copy of its metadata
the same way for those versions to read. A v3 group write no longer encodes
its consolidated metadata twice (30% of every write on a large consolidated
group), and the check for whether a document's repair moves chunks returns
early for documents whose regular chunk sizes are all positive, so reads of
consolidated metadata cost what they did. AsyncArray and AsyncArrayIO raise
TypeError for metadata that is not a mapping, as AsyncArray always did.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…documents

ArrayIO.open and a model given to the constructor bound metadata declaring
storage transformers and failed only at the first chunk operation; every
model is now checked when bound, as dicts and AsyncArray are. erase_metadata
no longer parses documents before deleting them, so a corrupt or
node_type-less array document is removed rather than raising or surviving;
only a group's documents are left (a v2 `.zattrs` beside a `.zgroup`, a v3
group's `zarr.json`). Pickles no longer carry a stale 3.4.x mark, and a
consolidated group pickled here marks its members for 3.4.1 and 3.4.2 to
read. Binding equality is always the models' canonical equality, the
rectilinear grid skips the repair scan it never needed, AsyncArrayIO names
the type it was given when it cannot bind it, and with_metadata takes any
mapping. Tests pin these and check may_move_chunks against the repairs.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… chunk size 0 on strict reads

- verify builds both sides as plain JSON, so nested attributes (read-only
  mapping proxies in the model) no longer raise TypeError for v2 arrays.
- The strict read (`open`, `verify`) rejects a v2 chunk size zarr reads as
  repaired, as it does for v3, though the v2 model accepts it.
- with_metadata raises TypeError for anything that is not array metadata.

Assisted-by: ClaudeCode:claude-opus-5-5
…ubclass state in pickles

- A metadata write records its documents in the store's cache only when the
  cache knows the path or the written metadata might move chunks, judged from
  its chunk sizes; writes of large attributes cost what they did on main.
  store_documents takes the written array metadata (default: none, a group's),
  so its two-argument form works again.
- AsyncArray pickles keep a subclass's own instance attributes, and a shallow
  copy shares the metadata object again, also for a repaired array.
- Two threads first touching one store object share one cache.
- zarr.core.group re-exports Store and set_or_delete, as 3.4.2 did.

Assisted-by: ClaudeCode:claude-opus-5-5
…model's chunks

- A change to an AsyncArray's state (resize, append, attribute updates, a
  field set with object.__setattr__) gives it a binding of its own, so an
  array copied by its __dict__, or a binding taken from `io` before the
  change, keeps the state it had, as when the fields were its only state.
- An AsyncArray over a model-backed binding makes the metadata it hands out
  the binding's source, so the binding's model never disagrees with it.
- A zarr_metadata v2 model bound through the constructor or with_metadata is
  checked as the strict read checks it (a chunk size of 0 is rejected), and
  a model with storage transformers is rejected by with_metadata too; a
  tuple-valued mapping with chunk size 0 binds as repaired, as AsyncArray
  reads it.
- Bindings of different classes that compare equal hash equally.

Assisted-by: ClaudeCode:claude-opus-5-5
- An array made from a binding (`to_array`) works on a copy of it, so
  nothing done through the array (an attribute update, a resize) changes the
  binding it came from, and reading the array no longer replaces the
  binding's model with its legacy round trip, which lost NaN payloads, codec
  configuration and v2 float fill values.
- A model-backed binding whose resolved metadata an array changed in place
  (attribute updates through copies sharing it) reports those attributes in
  its model, and stores them.
- Copies and pickles of an array keep the class of its binding.
- The dataclass fields of AsyncArray are no longer attributes of the class,
  so a @DataClass subclass may annotate one again or add a field without a
  default, as before.
- The strict v2 read reports the model's problems together with its chunk
  sizes; binding is the one place a model is checked; a metadata mapping
  holding tuples is read once, as its JSON; parse_array_metadata's TypeError
  names what it got, and the storage-transformer error has its space back.

Assisted-by: ClaudeCode:claude-opus-5-5
Assisted-by: ClaudeCode:claude-opus-5-5
…ough rebuilds

- Every way a binding is made or changed goes through `_bind`, including the
  in-place rebind of a resize or attribute update, so a binding subclass that
  derives state of its own there (a native handle) never goes stale.
- An array over a model binding keeps the model when rebuilt by `with_config`,
  pickling or a deep copy, so its binding still verifies against an unchanged
  store and stores the document as it was (NaN payloads, codec
  configuration); `with_config` also keeps the binding's class.
- Binding equality no longer raises when attributes were changed in place to
  values JSON cannot hold, and reading the model names such a value.

Assisted-by: ClaudeCode:claude-opus-5-5
…NG on the class

- AsyncArray pickles and copies carry the values of slots a subclass
  declares, and a subclass that defines its own reduction (`__reduce_ex__`,
  `__reduce__`, `__getstate__`, `__setstate__`) is copied its way.
- The dataclass fields read as `dataclasses.MISSING` on the class instead of
  raising, so a loop over `dir(AsyncArray)` works while a `@dataclass`
  subclass still finds no default.

Assisted-by: ClaudeCode:claude-opus-5-5
…els portably

- AsyncArray.with_config keeps the metadata a subclass __init__ built,
  binding the model only when the constructor bound this array's own
  metadata.
- Pickles carry slots as the default reduction does (the instance dict and the
  slots' values, name-mangled ones included), so a slotted subclass's pickle
  loads in either version, and carry a model binding's documents as bytes, so
  a zarr without zarr-metadata loads it.
- Unpickling a binding, and setting an array's store_path or config, go
  through `_bind`; the docstring says attributes alone change in place.

Assisted-by: ClaudeCode:claude-opus-5-5
…each problem once

- AsyncArray pickles carry the binding's own state (pipeline class, binding
  class, a model's documents, the store cache's entry) under one key, so a
  subclass attribute of any other name pickles as it always did.
- The strict v2 read reports a chunk size the model already rejected once.
- A model binding compares changed attributes as JSON text and keeps the
  model it made for them, so NaN attributes are stable and reads are cheap;
  equality without JSON attributes compares documents without them, then the
  resolved attributes.
- A subclass that defines its own reduction is copied without private
  `copy` helpers; `__getstate__` is annotated with the tuple it may return; the
  docstring says a binding whose derived state does not pickle leaves it out.

Assisted-by: ClaudeCode:claude-opus-5-5
…ubclass reductions

- A pickled array's model is bound again only when the pickle still holds the
  metadata it was pickled with: a zarr that keeps the binding's state but
  changes the metadata (3.4.2 resizing an unpickled array) no longer brings
  back a stale model.
- A subclass whose own reduction carries a state of its own, with no
  `__setstate__`, copies and pickles as the default protocol would.
- Attributes changed in place are written in their own order; the strict v2
  read reports a chunk size of 0 beside the model's other problems.

Assisted-by: ClaudeCode:claude-opus-5-5
…d chunk size once

- A model binding compares attributes changed in place as JSON makes them
  (keys as strings), so attributes holding an int key no longer raise.
- The strict v2 read adds only an int chunk size of 0 to the model's
  problems; `False` and `0.0`, which the model rejects itself, are reported
  once.

Assisted-by: ClaudeCode:claude-opus-5-5
`AsyncArray.with_config` carries the pipeline its constructor built into the
rebound binding only when that binding runs the same pipeline class, so a
binding subclass whose `_bind` chooses its own pipeline (a backend) runs it.

Assisted-by: ClaudeCode:claude-opus-5-5
Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A binding over a model resolves its legacy metadata lazily. Threads that
resolved it at once each kept their own object, so an attribute update made
through one was lost when another stored its resolution afterwards.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…bute order

AsyncArray.__setstate__ took any state holding a `metadata` entry for its own
field state, and raised on a subclass reduction's `(None, slots)` state; it now
recognizes its own state by both `metadata` and `codec_pipeline`. AsyncArrayIO's
pickle carries its store-cache entry under one key of its own, so a subclass
attribute of the same name survives, and leaves out the attributes' derived
model. Attributes reordered in place now count as changed, so the written order
reaches the model. _rebound_like says a binding's pipeline class wins over one
a subclass __init__ assigned, and a test pins it.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ery field

AsyncArray.__setstate__ took a state with `metadata` and `codec_pipeline` for
its own field state and then raised KeyError on the fields it lacked, so a
subclass reduction holding just those two could not load or copy. It now
requires all five fields.

Assisted-by: ClaudeCode:claude-opus-5-5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant