Repository navigation
Commit 01fa6d9
feat: URL pipeline core — URLPipeline objects, adapter ABC and registry (zarr-developers#4192)
* feat: url-pipeline core — parser, adapter ABC, registry, store hooks
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>
* polish: validate mode before pipeline resolution, document adapter authoring
- 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>
* fix: load URL adapter entry points outside the registry lock
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>
* fix: close the adapter's store when read-only conversion fails; document 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>
* fix: honor the pipeline-selected zarr format at every make_store_path 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>
* docs: fix wrapper example path handling; document review follow-ups
- 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>
* test: skip fsspec memory:// divergence test when fsspec lacks AsyncFileSystemWrapper
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>
* refactor(url-pipeline): resolve pipelines from a URLPipeline object, never from plain strings
Strings passed as a StoreLike keep their pre-existing meaning: a local
path containing `|` still names a directory, whatever adapters are
installed. A pipeline is opened either through the new `zarr.open_url`,
or by passing a `zarr.storage.URLPipeline` (built with `from_url`) to
any function that accepts a StoreLike.
- `URLPipeline` holds every parsed segment; a trailing `zarr2:`/`zarr3:`
format segment is read from it (`zarr_format`, `path`, `store_segments`)
instead of through pseudo-adapters, so `AdapterResolution.zarr_format`
is gone.
- `StorePath` is back to its state on main: no `zarr_format` attribute,
and mode "a" on a read-only store raises again. The open-or-create
downgrade applies only to pipelines that resolve read-only.
- The format selected by a pipeline is merged with the caller's
`zarr_format` by `resolve_zarr_format(store, zarr_format)` at each
call site, before the store is resolved. `create_array` and
`Group.from_store` default to 3 again; `Array.open` keeps its new
`zarr_format` parameter.
- `is_url_pipeline` and the `pathlib.Path` escape hatch are removed;
`resolve_pipeline` accepts a string or a `URLPipeline`, and a single
URL is a trivial pipeline.
Assisted-by: ClaudeCode:claude-fable-5-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(url-pipeline): make open_url open existing nodes only
`zarr.open_url(url, *, mode="r" | "r+", storage_options=None)` no longer
mirrors `zarr.open`: it never creates or overwrites, takes no `path`
(the node is addressed by the URL, via the format segment's body), no
`zarr_format` (taken from the pipeline or inferred), and no `**kwargs`.
Creating a node at a pipeline-addressed location is done by passing a
`URLPipeline` to `create_array` / `create_group`.
Assisted-by: ClaudeCode:claude-fable-5-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(url-pipeline): drop the string-routing leftovers from the resolver
With strings never routed as pipelines, four pieces of the resolver only
served the old design:
- The schemeless-root extension (`data.zip|zip:`). The root of a pipeline
now carries a URL scheme, as the specification requires; a bare path is
rejected with a message pointing at `file:` URLs and `%7C`. This also
removes the one place where `?` and `#` were treated as filename
characters.
- The negative lookahead that kept fsspec's `zip::...` chained URLs out of
root-scheme detection; it protected `zarr.open` strings that no longer
reach the resolver.
- The mode "a" downgrade for read-only pipelines in `make_store_path`;
`open_url` defaults to "r", and a read-only pipeline now raises on "a"
exactly like a plain read-only store.
- The "non-dict `storage_options` reserved for per-segment configuration"
reservation in docstrings and the user guide.
Assisted-by: ClaudeCode:claude-fable-5-1
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* refactor(url-pipeline): resolve through URLPipeline.resolve; simplify 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>
* test: skip the '|'-in-local-path test on Windows
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>
* refactor(url-pipeline): shrink the adapter API to an ABC plus StorePath
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>
* refactor(url-pipeline): drop open_url; open_* accept URLPipeline objects only
Scope agreed on zarr-developers#4192: `zarr.open_url` moves to a follow-on PR, and the
open functions accept `URLPipeline` objects but never read a plain string
as a pipeline.
- Remove `zarr.open_url` / `zarr.api.{a,}synchronous.open_url`, its API
page and tests; docs and docstrings point at `URLPipeline` instead.
- Revert the public signature changes the core does not need:
`Array.open` no longer gains a `zarr_format` parameter and
`Group.from_store` keeps `zarr_format: ZarrFormat = 3`. A conflicting
default raises, as with any other explicit format.
- Validate the dtype in `AsyncArray._create` before creating the store,
as before this PR.
- Test `open`, `open_group`, `open_array` and `open_consolidated` with a
`URLPipeline`, and that the same text passed as a string is a path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(url-pipeline): validate URLPipeline.from_url input
`from_url` failed with AttributeError on non-strings and silently
accepted input no URL should contain.
- Raise TypeError for anything but a `str`, and for a hand-built
`URLPipeline` whose segments are not a tuple of `PipelineSegment`.
- Raise URLPipelineError for ASCII control characters, whitespace at
either end of a sub-URL, and a bare Windows drive root (`C:\x`,
`C:/x`), which used to parse as scheme `c`. Spaces inside a sub-URL
stay allowed, since zarr does not percent-decode.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(url-pipeline): address review findings on resolution and validation
Validation and resolution:
- make_store_path rejects an invalid mode before anything is resolved,
so adapters never see one, and closes the adapter's store when opening
the StorePath fails (read-only store in mode "a", "w-" on data, ...).
- consolidate_metadata honors the pipeline's format segment.
- A format scheme cannot be the root, and a format segment's node path
is validated at construction: '.'/'..' components are rejected before
any I/O.
- Support the spec's `zarr:` segment (node path, format detected).
- file: roots: reject '//' paths, which urlparse misread as an
authority; reject '.'/'..' components; resolve via Path rather than a
reconstructed URL; drive letters are special only on Windows.
- Non-native roots go to fsspec with the lowercased scheme.
- memory: roots follow the spec: one in-memory store, body is a path.
- PipelineSegment validates field types and requires a lowercase,
valid scheme; register_url_adapter rejects invalid schemes and the
ones zarr handles itself (file, memory, zarr, zarr2, zarr3).
Messages: no percent-encoding advice (nothing is decoded), a readable
URLPipeline repr, and "invalid URL scheme" for e.g. `a_b://x`.
Tests in test_open_path.py exercise these through zarr.open_*,
make_store_path, make_store and consolidate_metadata.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(url-pipeline): harden parsing and resolution; trim down-scope leftovers
Security (round-2 review):
- Redact query strings and URL userinfo in every error message and in the
URLPipeline/PipelineSegment reprs; PipelineContext no longer reprs its
storage_options. str() stays lossless.
- zarr.url_adapters entry points with invalid or reserved names (file,
memory, zarr, zarr2, zarr3) are ignored with a warning, and file:/memory:
can no longer be adapter segments.
- At most 32 segments, which bounds resolution recursion.
- Single-letter root schemes are rejected (they read as drive paths on
Windows). On Windows, file: roots are checked with '\' as a separator,
so UNC paths are rejected, and ':' is allowed only in the drive.
- Schemes are validated with fullmatch and before lowercasing, so a
trailing newline or a non-ASCII look-alike cannot alias another scheme.
- file:// authorities compare case-insensitively; adapter-not-found
errors no longer list installed schemes.
- Docs: every adapter sees all storage_options; memory: is process-wide.
Simplification and leftovers:
- One scheme check, one format-merge function (resolve_zarr_format), and
inlined single-use helpers; parse_pipeline is private.
- make_store keeps its URLPipeline branch but checks mode first only
there; save/create functions keep their original defaulting order.
- Drop PipelineSegment.__str__, tests of removed behavior and duplicate
tests; fix the ABC docstring's TOML and the PipelineContext.mode text.
- Cut the changelog and the user-guide sections to what they need.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(url-pipeline): polish wording, comments and test names
Shorter module docstring, changelog and adapter-authoring notes; clearer
error-message wording; comments that restated the code trimmed. Tests:
names and docstrings that match what they check, sync tests that never
await, related tests grouped, a dead registration removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(url-pipeline): fix two tests on Windows and minimum fsspec
- Spell the LOCALHOST authority test's path URL-style, so a Windows
drive path gets its leading slash.
- Check the fsspec store's URL rather than `fs.protocol`, which older
fsspec reports as a plain string.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(url-pipeline): note current string behavior and explicit store creation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(url-pipeline): drop PipelineSegment.raw; add a canonical url property
Keeping the original text next to its parsed parts allowed the two to
disagree (e.g. scheme="file", body="/a", raw="file:/b"), with some code
paths reading one and some the other. The parts are now the only source
of truth. `PipelineSegment.url` compiles them into the canonical sub-URL
(lowercase scheme, `scheme:` always written), and `str(URLPipeline)` and
`PipelineContext.preceding_url` join those, so
`URLPipeline.from_url(str(p)) == p`. The only text no longer preserved is
the scheme's case and an omitted optional colon, both equivalent per the
spec.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Validate hand-built pipeline segments like parsed ones
A hand-built PipelineSegment skipped the parser's rules, so its URL could
re-parse as a different pipeline: a body of "/tmp/a|evil:" became a file
root plus an 'evil' adapter. PipelineSegment now rejects '|', '#', a '?'
in the body, control characters and surrounding whitespace, and
URLPipeline rejects single-letter roots, so `from_url(str(p)) == p` holds
for every pipeline. PipelineSegment fields are keyword-only. Property
tests check the round trip for hand-built and parsed pipelines.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Davis Bennett <davis.v.bennett@gmail.com>1 parent 1276f71 commit 01fa6d9
24 files changed
Lines changed: 2916 additions & 9 deletions
File tree
- changes
- docs
- api/zarr/abc
- user-guide
- src/zarr
- abc
- api
- core
- storage
- tests
- package_with_entrypoint-0.1.dist-info
- package_with_entrypoint
- test_url_pipeline
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
11 | 11 | | |
12 | 12 | | |
13 | 13 | | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
85 | 85 | | |
86 | 86 | | |
87 | 87 | | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
88 | 154 | | |
89 | 155 | | |
90 | 156 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
103 | 107 | | |
104 | 108 | | |
105 | 109 | | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
106 | 153 | | |
107 | 154 | | |
108 | 155 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
| 46 | + | |
46 | 47 | | |
47 | 48 | | |
48 | 49 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
51 | | - | |
| 51 | + | |
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
| |||
234 | 234 | | |
235 | 235 | | |
236 | 236 | | |
| 237 | + | |
237 | 238 | | |
238 | 239 | | |
239 | 240 | | |
| |||
409 | 410 | | |
410 | 411 | | |
411 | 412 | | |
| 413 | + | |
412 | 414 | | |
413 | 415 | | |
414 | 416 | | |
| |||
541 | 543 | | |
542 | 544 | | |
543 | 545 | | |
| 546 | + | |
544 | 547 | | |
545 | 548 | | |
546 | 549 | | |
| |||
594 | 597 | | |
595 | 598 | | |
596 | 599 | | |
| 600 | + | |
597 | 601 | | |
598 | 602 | | |
599 | 603 | | |
| |||
809 | 813 | | |
810 | 814 | | |
811 | 815 | | |
| 816 | + | |
812 | 817 | | |
813 | 818 | | |
814 | 819 | | |
| |||
904 | 909 | | |
905 | 910 | | |
906 | 911 | | |
| 912 | + | |
907 | 913 | | |
908 | 914 | | |
909 | 915 | | |
| |||
1118 | 1124 | | |
1119 | 1125 | | |
1120 | 1126 | | |
| 1127 | + | |
1121 | 1128 | | |
1122 | 1129 | | |
1123 | 1130 | | |
| |||
1357 | 1364 | | |
1358 | 1365 | | |
1359 | 1366 | | |
| 1367 | + | |
1360 | 1368 | | |
1361 | 1369 | | |
1362 | 1370 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
156 | 156 | | |
157 | 157 | | |
158 | 158 | | |
159 | | - | |
| 159 | + | |
160 | 160 | | |
161 | 161 | | |
162 | 162 | | |
| |||
469 | 469 | | |
470 | 470 | | |
471 | 471 | | |
| 472 | + | |
472 | 473 | | |
473 | 474 | | |
474 | 475 | | |
| |||
826 | 827 | | |
827 | 828 | | |
828 | 829 | | |
| 830 | + | |
829 | 831 | | |
830 | 832 | | |
831 | 833 | | |
| |||
4409 | 4411 | | |
4410 | 4412 | | |
4411 | 4413 | | |
| 4414 | + | |
4412 | 4415 | | |
4413 | 4416 | | |
4414 | 4417 | | |
| |||
4917 | 4920 | | |
4918 | 4921 | | |
4919 | 4922 | | |
| 4923 | + | |
4920 | 4924 | | |
4921 | 4925 | | |
4922 | 4926 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
67 | 67 | | |
68 | 68 | | |
69 | 69 | | |
70 | | - | |
| 70 | + | |
71 | 71 | | |
72 | 72 | | |
73 | 73 | | |
| |||
532 | 532 | | |
533 | 533 | | |
534 | 534 | | |
| 535 | + | |
535 | 536 | | |
536 | 537 | | |
537 | 538 | | |
| |||
574 | 575 | | |
575 | 576 | | |
576 | 577 | | |
| 578 | + | |
577 | 579 | | |
578 | 580 | | |
579 | 581 | | |
| |||
0 commit comments