Skip to content

Commit 0753fa7

Browse files
FrancescAltedclaude
andcommitted
Give what a proxy reads from a module of its own
`c2array` imported `proxy` at module level, and had to: `C2NDSource` subclasses `ByteRangeNDSource`, which a class statement needs at import time. The suggested fix -- a function-local import -- could not work for that reason, and moving the import in `__init__` could not either: `embed_store` and `dict_store` are bound at lines 565-566 and both reach `c2array` for `C2Array`, while `lazyexpr` reaches `proxy` directly for `convert_dtype`. So `proxy.py` was dragged in ahead of `schunk`, `indexing` and `lazyexpr` by three independent paths, and could import none of them at module level without `import blosc2` failing outright. The half those modules actually want moves to `blosc2/proxy_source.py`: the `ProxySource`/`ProxyNDSource` protocol, the frame reading (header, offsets, chunk and block extents, splicing), `ByteRangeNDSource`, `FsspecNDSource`, `convert_dtype` and the block-granularity thresholds. It reaches into the package for nothing past what `blosc2_ext` binds first. `proxy.py` keeps the `Proxy` itself and imports the rest back, re-exporting the four classes so `blosc2.proxy.X` still answers -- as `blosc2.X` does, unchanged, which is what the docs autoclass. `.proxy` is now bound after `.schunk` and `.indexing`, and the proof is in the diff: `_reopen_cache`'s function-local `from blosc2.schunk import _set_default_dparams`, which was function-local for exactly this reason, is a module-level import again. What moved for good are the tuning constants -- `BLOCK_MIN_CBYTES` and its neighbours are `blosc2.proxy_source.BLOCK_MIN_CBYTES` now, since re-exporting a constant by value would leave anyone setting it on `blosc2.proxy` silently patching a copy nothing reads. The two benches and the tests that set them are updated to say where they live. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 4a1b927 commit 0753fa7

13 files changed

Lines changed: 929 additions & 854 deletions

‎RELEASE_NOTES.md‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@ XXX version-specific blurb XXX
5353
ranges (`multipart/byteranges`, which no object store offers). A dataset the
5454
subscriber *computes* — a lazy expression, an HDF5 leaf, a `.b2z` member — is
5555
fetched a whole chunk at a time as before; which it is costs at most one small
56-
request to find out, and is never asked twice. `blosc2.ByteRangeNDSource` is
56+
request to find out, and is not asked again -- unless the subscriber could not
57+
say, which a busy or unreachable one cannot: a 5xx or a connection that failed
58+
is asked again on the next fetch rather than written off, since neither
59+
downloaded anything to find out. `blosc2.ByteRangeNDSource` is
5760
the frame reader `FsspecNDSource` and the new `C2NDSource` share: subclass it
5861
with a `read_range(offset, size)` to give any transport the same treatment.
5962
Opening a remote frame through either of them now costs two requests instead
@@ -97,8 +100,22 @@ XXX version-specific blurb XXX
97100
bytes they read are held to that as well, so a remote array *replaced* while
98101
keeping its shape is noticed rather than served stale: `FsspecNDSource` uses
99102
fsspec's token and `C2Array` the subscriber's mtime, both free with metadata
100-
they already fetch. Caches from earlier 4.11.1 development builds are not
101-
adopted (the stamp moved to a `proxy-stamp` entry); pass `mode="w"` once.
103+
they already fetch. A proxy that `blosc2.open` rebuilds over its own cache
104+
gets the same treatment without the raise -- there is no `mode="w"` to offer
105+
it -- so a cache whose stamp no longer matches starts as though nothing had
106+
been fetched and fills again from the bytes served now; opened read-only there
107+
is nothing to empty, and every read falls through to the source. Caches from
108+
earlier 4.11.1 development builds are not adopted (the stamp moved to a
109+
`proxy-stamp` entry); pass `mode="w"` once.
110+
111+
* The source protocol moved to its own module, `blosc2.proxy_source`:
112+
`ProxySource`, `ProxyNDSource`, `ByteRangeNDSource`, `FsspecNDSource` and the
113+
frame reading behind them. They are still `blosc2.X` and still reachable as
114+
`blosc2.proxy.X`, so nothing outside need change; what moved for good are the
115+
block-granularity knobs, `blosc2.proxy_source.BLOCK_MIN_CBYTES` and its
116+
neighbours. This is what lets `proxy.py` be imported after `schunk` and
117+
`indexing`, rather than being dragged in ahead of them by every module that
118+
wants a source.
102119

103120
* Querying a `utf8()` column through its FULL index no longer materializes the
104121
index vocabulary. The query literal is turned into an alphabetical rank by

‎bench/ndarray/cat2-block-granularity.py‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@
8484
import blosc2
8585
from blosc2 import c2array
8686

87-
CHUNK_HEADER = blosc2.proxy._CHUNK_HEADER_LEN
87+
CHUNK_HEADER = blosc2.proxy_source._CHUNK_HEADER_LEN
8888

8989

9090
#
@@ -353,8 +353,8 @@ def timed_slice(open_array, item, mode, concurrency, latency, bandwidth):
353353
of what it used to do. The frame index is read before the clock starts: it
354354
costs four small requests once per C2Array, not once per slice.
355355
"""
356-
threshold = blosc2.proxy.BLOCK_MIN_CBYTES
357-
blosc2.proxy.BLOCK_MIN_CBYTES = 1 << 62 if mode == "chunks" else threshold
356+
threshold = blosc2.proxy_source.BLOCK_MIN_CBYTES
357+
blosc2.proxy_source.BLOCK_MIN_CBYTES = 1 << 62 if mode == "chunks" else threshold
358358
try:
359359
array = open_array()
360360
array.max_concurrency = concurrency
@@ -367,7 +367,7 @@ def timed_slice(open_array, item, mode, concurrency, latency, bandwidth):
367367
proxy[item]
368368
return time.perf_counter() - start, tally["requests"], tally["bytes"]
369369
finally:
370-
blosc2.proxy.BLOCK_MIN_CBYTES = threshold
370+
blosc2.proxy_source.BLOCK_MIN_CBYTES = threshold
371371

372372

373373
def connection_setup(urlbase, path, token, reps):
@@ -456,7 +456,7 @@ def open_array():
456456
found_out = (
457457
f"{opening['requests']} request found that out, and it is never asked again"
458458
if opening["requests"]
459-
else f"nothing was asked.\n Chunks under the {blosc2.proxy.BLOCK_MIN_CBYTES / 1e6:.0f} MB "
459+
else f"nothing was asked.\n Chunks under the {blosc2.proxy_source.BLOCK_MIN_CBYTES / 1e6:.0f} MB "
460460
"a round trip costs are not worth taking apart, and api/info\n tells a computed "
461461
"dataset from a stored one for free"
462462
)

‎bench/ndarray/fsspec-block-granularity.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,8 +223,8 @@ def timed_open(urlpath, item, concurrency, blocks, latency=0.0, bandwidth=0.0):
223223
*bandwidth* bytes per second across it. Both waits happen inside the
224224
proxy's thread pool, so they overlap the way real ones would.
225225
"""
226-
old = blosc2.proxy.BLOCK_MIN_CBYTES
227-
blosc2.proxy.BLOCK_MIN_CBYTES = old if blocks else 1 << 62
226+
old = blosc2.proxy_source.BLOCK_MIN_CBYTES
227+
blosc2.proxy_source.BLOCK_MIN_CBYTES = old if blocks else 1 << 62
228228
try:
229229
array = blosc2.open(urlpath, lazy=True, max_concurrency=concurrency)
230230
traffic = [0, 0]
@@ -246,7 +246,7 @@ def counted(*args, _o=original):
246246
array[item]
247247
return time.perf_counter() - t0, traffic[0], traffic[1]
248248
finally:
249-
blosc2.proxy.BLOCK_MIN_CBYTES = old
249+
blosc2.proxy_source.BLOCK_MIN_CBYTES = old
250250

251251

252252
def run_moto(local, concurrency, reps, latency, bandwidth):

‎src/blosc2/__init__.py‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -588,20 +588,25 @@ def _raise(exc):
588588
result_type,
589589
can_cast,
590590
)
591-
from .proxy import (
592-
Proxy,
591+
from .proxy_source import (
593592
ProxySource,
594593
ProxyNDSource,
595-
ProxyNDField,
596594
ByteRangeNDSource,
597595
FsspecNDSource,
598-
SimpleProxy,
599-
jit,
600-
as_simpleproxy,
601596
)
602597
from .indexing import Index
603598

604599
from .schunk import SChunk, load, open
600+
601+
# After .schunk and .indexing, which it may now import at module level: what a
602+
# proxy reads from lives in .proxy_source, which the early modules reach instead
603+
from .proxy import (
604+
Proxy,
605+
ProxyNDField,
606+
SimpleProxy,
607+
jit,
608+
as_simpleproxy,
609+
)
605610
from . import linalg
606611
from .linalg import tensordot, vecdot, permute_dims, matrix_transpose, matmul, transpose, diagonal, outer
607612
from .utils import linalg_funcs as linalg_funcs_list

‎src/blosc2/c2array.py‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,7 @@
2323
import blosc2
2424
from blosc2.b2objects import encode_b2object_payload, make_b2object_carrier, write_b2object_payload
2525
from blosc2.info import InfoReporter, format_nbytes_info
26-
27-
# blosc2/__init__ imports this module before blosc2.proxy, so this pulls proxy in
28-
# early; it is safe because proxy only reaches into the package at call time
29-
from blosc2.proxy import REMOTE_MAX_CONCURRENCY, ByteRangeNDSource
26+
from blosc2.proxy_source import REMOTE_MAX_CONCURRENCY, ByteRangeNDSource
3027

3128
_subscriber_data = {
3229
"urlbase": os.environ.get("BLOSC_C2URLBASE"),
@@ -782,7 +779,7 @@ def _open_block_source(self):
782779
# ever use a block, and the dataset keeps the behaviour it had before.
783780
# Inside the `try`, since `api/info` need not carry what these read.
784781
nchunks = math.prod(math.ceil(s / c) for s, c in zip(self.shape, self.chunks, strict=True))
785-
if not nchunks or self.cbytes / nchunks < blosc2.proxy.BLOCK_MIN_CBYTES:
782+
if not nchunks or self.cbytes / nchunks < blosc2.proxy_source.BLOCK_MIN_CBYTES:
786783
return None
787784
source = C2NDSource(self, max_concurrency=REMOTE_MAX_CONCURRENCY)
788785
source.adopt_index(self._pending_index)

‎src/blosc2/lazyexpr.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
write_b2object_user_vlmeta,
5555
)
5656
from .dsl_kernel import DSLKernel, DSLSyntaxError, DSLValidator, specialize_miniexpr_inputs
57-
from .proxy import convert_dtype
57+
from .proxy_source import convert_dtype
5858
from .utils import (
5959
check_smaller_shape,
6060
compute_smaller_slice,

‎src/blosc2/ndarray.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6880,8 +6880,8 @@ def asarray(
68806880
utf8_out, array = _asarray_string_dispatch(array, copy, kwargs)
68816881
if utf8_out is not None:
68826882
return utf8_out
6883-
dtype_ = blosc2.proxy.convert_dtype(array.dtype)
6884-
dtype = blosc2.proxy.convert_dtype(kwargs.pop("dtype", dtype_)) # check if dtype provided
6883+
dtype_ = blosc2.proxy_source.convert_dtype(array.dtype)
6884+
dtype = blosc2.proxy_source.convert_dtype(kwargs.pop("dtype", dtype_)) # check if dtype provided
68856885
kwargs = _check_ndarray_kwargs(**kwargs)
68866886
chunks = kwargs.pop("chunks", None)
68876887
blocks = kwargs.pop("blocks", None)

0 commit comments

Comments
 (0)