Repository navigation
Conversation
10cb73b to
3aa63ac
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4192 +/- ##
==========================================
+ Coverage 94.71% 94.77% +0.06%
==========================================
Files 94 96 +2
Lines 13663 13923 +260
==========================================
+ Hits 12941 13196 +255
- Misses 722 727 +5
🚀 New features to boost your workflow:
|
effd07c to
b0e7af9
Compare
b0e7af9 to
c744f95
Compare
787e785 to
6e6c2f5
Compare
|
This is great! Here's some stuff I found with Claude: 🤖 AI text below 🤖 Review of c744f95. The parser and adapter ABC look solid (the spec corpus passes, ruff/mypy are clean, no import cycles, test order doesn't leak registry state), but the store hooks introduce some regressions and there are a few contract questions worth settling before the Correctness / regressions
Spec conformance at the root
Design / API surface
Smaller things
|
|
a few more findings, this time from codex: 🤖 AI text below 🤖 A few additional findings on
For reference, the updated focused suite passes locally: |
6e6c2f5 to
d29f495
Compare
Implements URL pipeline support (https://github.com/jbms/url-pipeline): '|'-chained URLs resolve through pluggable adapters registered under the 'zarr.url_adapters' entry-point group (entry-point name = URL scheme). - zarr.abc.url_pipeline: PipelineSegment, AdapterResolution, PipelineContext, URLPipelineAdapter (single-classmethod contract) - zarr.storage._url_pipeline: parse_pipeline / resolve_pipeline; the root sub-URL delegates to make_store so existing file/memory/fsspec routing is unchanged - registry: register_url_adapter / get_url_adapter / list_url_adapter_schemes (name check only; no adapter imports) - make_store/make_store_path route strings containing '|' (or a registered root scheme) through the resolver; residual store paths combine with the user-supplied path - StorePath gains a zarr_format attribute (populated by format segments in a follow-up) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
d29f495 to
20f2ac8
Compare
…e-core # Conflicts: # src/zarr/errors.py # src/zarr/registry.py
…thoring - make_store validates the access mode before routing a URL pipeline string to adapters, matching every other StoreLike branch - make_store docstring lists URL pipeline strings; StorePath.open documents the invalid-mode ValueError - user guide: StoreLike bullet for pipeline strings and an adapter authoring section in extending.md (runnable example) - changelog: drop PR-relative wording, name the public parser entry points Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
get_url_adapter took a non-reentrant lock across entry_point.load(), so an adapter module that resolved another scheme at import time deadlocked the process. Matching entry points are now taken off the pending list under the lock and imported outside it; import failures are wrapped in URLPipelineError and leave the entry point discoverable for a retry. A same-named entry point is discarded with a ZarrUserWarning when the scheme is already registered (e.g. by a builtin), instead of staying pending forever; duplicate entry-point names warn and the first wins. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ent contracts - The resolver closes the store an adapter returned when it cannot be made read-only for mode 'r', instead of leaking it. - resolve_pipeline documents every error type it lets through; the resolve_preceding docstring states the local-file-root limitation. - AdapterResolution and PipelineContext compare and hash by identity, since a Store is not hashable and frozen=True implied otherwise. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… caller The zarr2:/zarr3: format carried on StorePath.zarr_format was merged only in zarr.api.asynchronous; create_array, from_array, Group.from_store, Group.open, Array.open and the deprecated AsyncArray._create dropped it and swallowed explicit conflicts. The merge now lives on StorePath.resolve_zarr_format and is applied at each site. To let the pipeline's format apply when the caller does not specify one, create_array (sync and async) and Group.from_store default zarr_format to None, resolving to the configured default (3) as before; Array.open gains a zarr_format parameter mirroring Group.open. Tests: registry deadlock/race/import-failure/shadowing, resolver leak, zarr_format merge at each core site, and pins for the documented memory:/ file: divergences and the local-file-root limitation (xfail, strict). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- The extending guide's wrapper example dropped the preceding residual path while its comment claimed the opposite; it now joins the two and a tested snippet shows root|a|b resolving to a/b. - Changelog and storage guide state the memory:// routing split with fsspec, the undecoded percent-escapes in file: roots, the local-file-root limitation, the entry-point collision warnings, and the zarr_format default changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ature/url-pipeline-core # Conflicts: # src/zarr/api/asynchronous.py # src/zarr/core/group.py
…leSystemWrapper fsspec < 2024.12.0 (the min_deps job) cannot open a plain memory:// URL at all, so the routing comparison only applies with fsspec>=2024.12.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… root scheme detection `URLPipeline.resolve(mode=..., storage_options=...)` replaces the module-level `resolve_pipeline`, and `parse_pipeline` leaves the public `zarr.storage` surface: the object is the one entry point for parsing and resolving a pipeline. Root scheme detection no longer consults `parse_store_url` for single-letter candidates. A letter followed by `:` is a URL scheme on every platform, so parsing is deterministic across operating systems; a Windows drive path is spelled `file:/C:/...` in a pipeline, as the root must carry a scheme anyway. Assisted-by: ClaudeCode:claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
what's the technical downside of this approach? I'm asking because I think it's worth exploring all options before trying to switch people to a new API. The value of url-pipeline is being able to have zarr just work simply across varied storage types. requiring code changes for use reduces that value. |
are you proposing changing the url pipeline spec itself? the basic premise of the pipeline is that there's a delimiter string (or sequence of strings) separating the store segments. using that delimiter string as a delimiter means it can't be used as part of a path any more. So I don't see a transformation of the spec that cleanly separates URL pipeline declarations from possible s3 objects or file paths. lmk if this is not what you had in mind.
why? new API for a new feature seems like a good starting point to me, especially when the existing API is problematic and has immediate conflicts with the new feature. see this PR against joe's fork that adds new API for the URL pipeline. by completely separating URL pipeline declarations from legacy string declarations, we can remove code that existed only the disambiguate the two. I think this is an easier starting point that managing the inherent conflict in |
|
and here's a second PR that covers array / group creation with strings interpreted as URL pipelines: jhamman#8. That could be a follow-up to the PR here. Since it adds new API, it's completely safe to merge without breaking existing code, unlike the proposed changes to |
|
Thanks @d-v-b and @maxrjones for working through this. I'd like us to agree on a plan we can merge. I don't have strong opinions on the exact API, with one exception: Xarray users have to be able to use pipelines through the existing store= argument, e.g. There's a lot I think we already agree on:
Here's a proposal that I think gets everyone what they need:
Concrete asks:
If we can settle on one this plan, I'll rework #4192 to match. I'll fold in d-v-b's PoC where it fits and keep the format-adapter and zip follow-ups (#4476, #4477) rebased on top. |
thanks, @jhamman. Your proposal sounds good to me. I don't have a strong opinion on StorePath.zarr_format, so no objection from me. We really need this feature for getting virtual Icechunk stores into CMR, so I'm really excited to see this move forward. |
…re-v2 # Conflicts: # src/zarr/core/array.py
So the central design requirement is something for xarray users, not zarr users. IMO that suggests the solution: xarray handles the URL pipeline inference, and branches internally on whether the user-supplied string is a url pipeline, or a plain path. When a user invokes to make this easy for xarray, this keeps |
|
to be clear, I'm not saying we should be slow to land this. I also want this feature out, and I'm happy to move quickly to land it. I'd be happy working on a 3.5.0 release next week with the |
|
hey @d-v-b, do you mind responding to Joe's question about whether an opt-in flag would resolve your concerns about breakages? FWIW I appreciate your concerns about not breaking things, even for super rare use-cases. I also generally prefer having a deliberate, forward progression in our existing APIs rather than asking people to move to new APIs for new features. |
can you sketch out how this would work? i might have missed that explanation in joe's proposal |
|
also, I should give some context for why I am deeply averse to increasing / encouraging use of
especially if we are talking about xarray, and not zarr users, we have even more flexibility here. |
|
I remember advocating that we deprecate I do also care about this as a feature within Zarr and I think this plan risks divergent behavior between I'm going to pursue the new api surface of |
Windows forbids '|' in file names, so the directory cannot be created there. Also use a test-only scheme name rather than 'zip', which a builtin adapter will claim. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| ] | ||
|
|
||
|
|
||
| class _Unset(enum.Enum): |
There was a problem hiding this comment.
we might even already have an unset sentinel value
| class AdapterResolution: | ||
| """ | ||
| The result of resolving a URL pipeline (or a prefix of one). | ||
|
|
||
| Instances are immutable but compare and hash by identity, since a | ||
| [`Store`][zarr.abc.store.Store] is not hashable. | ||
|
|
||
| Attributes | ||
| ---------- | ||
| store : Store | ||
| The resolved store. | ||
| path : str | ||
| Residual path *within* the store that the pipeline addresses | ||
| (e.g. `"path/to/node"` for `...|icechunk://tag.v1/path/to/node`). | ||
| Empty string when the pipeline addresses the store root. | ||
|
|
||
| A wrapper adapter that re-wraps a preceding resolution must carry every | ||
| field it does not change forward: prefer | ||
| `dataclasses.replace(preceding, store=..., path=...)` over | ||
| reconstructing, so fields added later are never silently dropped. | ||
| Format segments (`zarr2:`/`zarr3:`) are not adapters; they are consumed | ||
| by [`URLPipeline`][zarr.storage.URLPipeline] before resolution. | ||
| """ | ||
|
|
||
| store: Store | ||
| path: str = "" |
There was a problem hiding this comment.
code smell: this is just StorePath without the methods. Can we re-use StorePath?
...when we define the same (Store, path) accessory class twice, that's a sign that we should just put a path attribute on the Store itself. Obviously out of scope for this PR but I would love to stop paying tech debt in this area. Something to think about when we iterate the store API
| async def open_pipeline_segment( | ||
| cls, segment: PipelineSegment, context: PipelineContext | ||
| ) -> AdapterResolution: |
There was a problem hiding this comment.
we only need PipelineContext and AdapterResolution here for type checking, so can we conditionally import them from _url_pipeline.py under a if TYPE_CHECKING block? Because those two classes are not abstract base classes, and URLPipelineAdapter clearly belongs in _url_pipeline.py because it is importing _resolve from there in a naughty way
Following review on zarr-developers#4192: - Adapters return a StorePath instead of AdapterResolution, which had become a StorePath without the methods. AdapterResolution is removed. - zarr.abc.url_pipeline now holds only the URLPipelineAdapter ABC. PipelineSegment and PipelineContext are plain data classes and move next to the resolver; they are exported from zarr.storage. This also removes the lazy import of the resolver from zarr.abc. - PipelineContext.resolve_preceding() takes no arguments. An adapter that needs another mode or options resolves through a derived context, dataclasses.replace(context, mode="r").resolve_preceding(), so the _Unset sentinel is no longer needed. - Less public surface: URLPipeline keeps from_url and segments; its resolve(), store_segments, zarr_format, path and resolve_zarr_format become private, as do zarr.registry.get_url_adapter and list_url_adapter_schemes. register_url_adapter stays public. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
some more issues found by claude:
I'd be fine handling these in a follow-up PR, but we should make sure they are resolved before we release |
maxrjones
left a comment
There was a problem hiding this comment.
I understand not wanting to build on zarr.open(), but IMO the biggest weakness of the open* functions is the mode parameter, which is super confusing for users and developers. So, I'm a bit concerned about adding another function with a mode keyword argument.
Do others share this concern? If so, to get an MVP out, could zarr.open_url() be moved to a distinct PR such that the following would work immediately:
from zarr.storage import URLPipeline
pipeline = URLPipeline.from_url("memory://pipeline-demo|zarr2:")
group = zarr.open_group(pipeline, mode="w")
totally agree! |
|
@maxrjones and @d-v-b - to clarify, are you asking for the following scope changes:
Anything else? |
Summary
Core support for URL pipelines (https://github.com/jbms/url-pipeline, ZEP 8):
|-chained URLs that address zarr data through nested storage layers, e.g.s3://bucket/data.zip|zip:|zarr3:. Adapters are registered under thezarr.url_adaptersentry-point group (entry-point name = URL scheme).Following the review discussion below, this PR no longer changes what existing string store specifications mean. A plain string is never read as a pipeline. Pipelines reach zarr in two explicit ways:
zarr.open_url(url, *, mode="r" | "r+", storage_options=None)opens the existing group or array a pipeline string addresses. It never creates or overwrites anything.zarr.storage.URLPipeline.from_url(url)builds a parsed, immutable pipeline object. It can be passed anywhere aStoreLikeis accepted (open_group,create_array,xr.open_zarr(store=...), ...), which is how you create or write through a pipeline.zarr.open("data|x.zarr")still opens a local directory of that name, whatever adapters are installed.What's in the PR:
zarr.abc.url_pipeline: only theURLPipelineAdapterABC. Adapters receive azarr.storage.PipelineSegmentandzarr.storage.PipelineContextand return azarr.storage.StorePath. To resolve the preceding pipeline with another mode or options, an adapter derives a context:dataclasses.replace(context, mode="r").resolve_preceding().zarr.storage.URLPipeline: holds every parsed segment and resolves viaURLPipeline.resolve().memory:andfile:roots follow the spec's semantics. Every other root delegates to the existingmake_storerouting.zarr2:/zarr3:) are read by zarr from the pipeline itself, not by adapters, and must be the last segment. The format they select is merged with the caller'szarr_format; an explicit conflict raises.zarr.registry.register_url_adapterregisters an adapter without an entry point and warns when it replaces one. Lookup is lazy per scheme, thread-safe and case-insensitive, and is internal.StorePathis unchanged frommain. There is nozarr_formatattribute, and mode"a"on a read-only store raises as before.create_arrayandGroup.from_storekeep their default of 3.Behavior notes are in
changes/4192.feature.md, and the user guide has a URL pipelines section.Why the change in scope
The earlier version reserved
|in every string store spec. That is a breaking change for anyone with|in a local path or object key; see @d-v-b's review. Explicit entry points let the pipeline machinery ship now with no change to existing strings. The design here is @d-v-b's jhamman#7, merged with the current branch andmain.Planned stack
zarr.open_urlzip:adapter; drops its format pseudo-adapterszip:zarr.open("s3://bucket/repo|icechunk:"))#4476 (format adapters) will be closed as superseded, since format segments are now read by zarr itself.
Proposal for PR 5:
|names an installed adapter followed by:.FutureWarningthat they will be read as URL pipelines starting in 3.6.LocalStore,FsspecStore,pathlib.Path) or rename the path.|is not a valid URL character (RFC 3986), it's on AWS's "characters to avoid" list for S3 keys, GCS discourages it, and Windows forbids it in file names.Author attestation
TODO
docs/user-guide/*.mdchanges/🤖 Generated with Claude Code