|
12 | 12 | ``` |
13 | 13 |
|
14 | 14 | Selection construction does not read source values: `result()`, `__array__`, |
15 | | -and eager `__getitem__` perform reads. Tokenization can also read or hash source |
16 | | -data, depending on the source and tokenization path. `.lazy` operations inspect |
| 15 | +and eager `__getitem__` perform reads. Source tokenization is delegated to |
| 16 | +Dask and may inspect source values. `.lazy` operations inspect |
17 | 17 | selection metadata and may copy or process supplied index arrays. Composition |
18 | 18 | does not accumulate wrapper layers: a view of a view is still a single transform |
19 | 19 | and retains its reader. |
|
138 | 138 | import json |
139 | 139 | import math |
140 | 140 | import operator |
141 | | -import uuid |
142 | 141 | from collections.abc import Sequence |
143 | 142 | from dataclasses import dataclass, field |
144 | 143 | from typing import TYPE_CHECKING, Any, Protocol, cast |
|
173 | 172 |
|
174 | 173 | __all__ = ["LazyArray", "Partition"] |
175 | 174 |
|
176 | | -# Above this declared byte count, the no-Dask token fallback adds a fresh UUID |
177 | | -# instead of digesting contents. See `_wrapped_token`. |
178 | | -_TOKEN_DIGEST_LIMIT = 1 << 20 |
179 | | - |
180 | 175 |
|
181 | 176 | def _invoke_reader( |
182 | 177 | reader: Reader, |
@@ -525,66 +520,6 @@ def _validate_prepared_parts(parts: Sequence[Partition], out_shape: tuple[int, . |
525 | 520 | raise ValueError("prepared parts do not tile the view exactly") |
526 | 521 |
|
527 | 522 |
|
528 | | -# --------------------------------------------------------------------------- # |
529 | | -# Tokenization |
530 | | -# --------------------------------------------------------------------------- # |
531 | | - |
532 | | - |
533 | | -def _wrapped_token(array: Any) -> Any: |
534 | | - """A token for the wrapped array. |
535 | | -
|
536 | | - In order of preference: the array's own `__dask_tokenize__`; |
537 | | - `dask.base.tokenize` when dask is importable (imported lazily — this package |
538 | | - never requires it); otherwise a local fallback that digests the contents of |
539 | | - a small array. |
540 | | -
|
541 | | - Tokens can differ depending on whether Dask is available and on the source |
542 | | - hook. This fallback does not provide a portable content identifier. |
543 | | -
|
544 | | - Above `_TOKEN_DIGEST_LIMIT`, or when conversion is unavailable, the local |
545 | | - fallback adds a fresh UUID on each call, so repeated calls normally differ. |
546 | | - The returned tuple is still equal to itself. Below the limit, conversion |
547 | | - can read the source, and the hash is of its NumPy buffer bytes. Object-array |
548 | | - buffer bytes contain object references, not a recursive content snapshot. |
549 | | - """ |
550 | | - hook = getattr(array, "__dask_tokenize__", None) |
551 | | - if hook is not None: |
552 | | - try: |
553 | | - return hook() |
554 | | - # A failing source hook falls through to the remaining tokenization paths. |
555 | | - except Exception: # pragma: no cover - a hook that refuses to run |
556 | | - pass |
557 | | - try: |
558 | | - # dask is an optional peer, never a dependency of this package, so it is |
559 | | - # imported here and its absence is ordinary. |
560 | | - from dask.base import tokenize # pyright: ignore[reportMissingImports] |
561 | | - except ImportError: |
562 | | - pass |
563 | | - else: |
564 | | - return tokenize(array) |
565 | | - |
566 | | - shape = tuple(int(s) for s in getattr(array, "shape", ())) |
567 | | - dtype = getattr(array, "dtype", None) |
568 | | - structural = (type(array).__qualname__, shape, str(dtype)) |
569 | | - # A fresh identifier per call when contents cannot be identified. It |
570 | | - # is the shape and dtype that would otherwise be mistaken for an identity, |
571 | | - # so they are kept alongside it for a reader looking at a graph. |
572 | | - unidentified = (*structural, "unidentified", uuid.uuid4().hex) |
573 | | - |
574 | | - # Decide whether to digest the contents from the *declared* size. Measuring |
575 | | - # it by converting first would read the whole array — a multi-gigabyte store |
576 | | - # pulled into memory by a token call, which is the opposite of the point. |
577 | | - itemsize = getattr(dtype, "itemsize", None) |
578 | | - if not isinstance(itemsize, int) or itemsize * math.prod(shape) > _TOKEN_DIGEST_LIMIT: |
579 | | - return unidentified |
580 | | - try: |
581 | | - contents = np.ascontiguousarray(array) |
582 | | - # Failed NumPy conversion leaves this source unidentified. |
583 | | - except Exception: |
584 | | - return unidentified |
585 | | - return (*structural, hashlib.sha256(contents.tobytes()).hexdigest()) |
586 | | - |
587 | | - |
588 | 523 | # --------------------------------------------------------------------------- # |
589 | 524 | # The wrapper |
590 | 525 | # --------------------------------------------------------------------------- # |
@@ -1249,15 +1184,18 @@ def __dask_tokenize__(self) -> Any: |
1249 | 1184 | tokens produce equal tokens; arbitrary semantically equivalent mappings |
1250 | 1185 | are not guaranteed to serialize identically. |
1251 | 1186 |
|
1252 | | - Source tokenization can read or hash data and need not be deterministic |
1253 | | - on every fallback path; see `_wrapped_token`. The reader and partitioning |
1254 | | - are omitted under the contract that they preserve values. Cache users |
1255 | | - must also account for source mutation and the source's token semantics. |
| 1187 | + Dask tokenizes the wrapped source using its normal dispatch and |
| 1188 | + determinism policy. This may read or hash source values. Dask is |
| 1189 | + imported only when this method is called and is otherwise optional. |
| 1190 | + The reader and partitioning are omitted because they must preserve |
| 1191 | + values. Mutating a source does not update keys in existing Dask graphs. |
1256 | 1192 | """ |
| 1193 | + from dask.base import tokenize # pyright: ignore[reportMissingImports] |
| 1194 | + |
1257 | 1195 | canonical = json.dumps(self._transform.to_json(), sort_keys=True) |
1258 | 1196 | return ( |
1259 | 1197 | type(self).__qualname__, |
1260 | | - _wrapped_token(self._array), |
| 1198 | + tokenize(self._array), |
1261 | 1199 | hashlib.sha256(canonical.encode()).hexdigest(), |
1262 | 1200 | ) |
1263 | 1201 |
|
|
0 commit comments