dol 0.3.76__tar.gz → 0.3.78__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dol might be problematic. Click here for more details.
- {dol-0.3.76 → dol-0.3.78}/.claude/skills/dol-dev-wrap-kvs/SKILL.md +4 -3
- {dol-0.3.76 → dol-0.3.78}/CLAUDE.md +1 -9
- {dol-0.3.76 → dol-0.3.78}/PKG-INFO +1 -1
- {dol-0.3.76 → dol-0.3.78}/dol/base.py +21 -4
- {dol-0.3.76 → dol-0.3.78}/dol/errors.py +33 -2
- {dol-0.3.76 → dol-0.3.78}/dol/filesys.py +54 -2
- {dol-0.3.76 → dol-0.3.78}/dol/misc.py +10 -25
- {dol-0.3.76 → dol-0.3.78}/dol/paths.py +15 -16
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_filesys.py +136 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_paths.py +119 -0
- {dol-0.3.76 → dol-0.3.78}/dol/trans.py +11 -2
- {dol-0.3.76 → dol-0.3.78}/dol/util.py +3 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/README.md +2 -8
- {dol-0.3.76 → dol-0.3.78}/misc/docs/dol_roadmap.md +2 -2
- dol-0.3.78/misc/fleet_ledger/README.md +72 -0
- dol-0.3.78/misc/fleet_ledger/__init__.py +24 -0
- dol-0.3.78/misc/fleet_ledger/__main__.py +152 -0
- dol-0.3.78/misc/fleet_ledger/astscan.py +535 -0
- dol-0.3.78/misc/fleet_ledger/baseline.py +333 -0
- dol-0.3.78/misc/fleet_ledger/fleet.py +106 -0
- dol-0.3.78/misc/fleet_ledger/pins.py +205 -0
- dol-0.3.78/misc/fleet_ledger/report.py +265 -0
- dol-0.3.78/misc/fleet_ledger/scan.py +286 -0
- dol-0.3.78/misc/fleet_ledger/store.py +220 -0
- dol-0.3.78/misc/fleet_ledger/surface.py +196 -0
- dol-0.3.78/misc/fleet_ledger/tests/__init__.py +0 -0
- dol-0.3.78/misc/fleet_ledger/tests/test_astscan.py +192 -0
- dol-0.3.78/misc/fleet_ledger/tests/test_ledger.py +242 -0
- {dol-0.3.76 → dol-0.3.78}/pyproject.toml +4 -3
- {dol-0.3.76 → dol-0.3.78}/.claude/commands/add-codec.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/.claude/commands/explain-store.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/.claude/commands/new-store.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/.claude/rules/dol-conventions.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/.claude/skills/dol-dev-portability/SKILL.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/.claude/skills/dol-store-building/SKILL.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/.gitattributes +0 -0
- {dol-0.3.76 → dol-0.3.78}/.github/workflows/ci.yml +0 -0
- {dol-0.3.76 → dol-0.3.78}/.gitignore +0 -0
- {dol-0.3.76 → dol-0.3.78}/LICENSE +0 -0
- {dol-0.3.76 → dol-0.3.78}/README.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/__init__.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/_interface_wrap.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/_paths_core.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/appendable.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/caching.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/content.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/dig.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/explicit.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/kv_codecs.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/mixins.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/naming.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/py.typed +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/recipes.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/scrap/__init__.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/scrap/new_store_wrap.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/scrap/store_factories.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/signatures.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/sources.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/__init__.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/base_test.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/pickability_test.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/scrap.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_appendable.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_caching.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_confirm_overwrite.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_content.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_dig.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_dol_tools.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_edge_cases.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_errors.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_fanout_stores.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_interface_wrap.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_kv_codecs.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_mk_dirs_if_missing.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_packaging_hygiene.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_path_writethrough.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_trans.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/test_walk_permissions.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tests/utils_for_tests.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/tools.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/trash.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/dol/zipfiledol.py +0 -0
- {dol-0.3.76 → dol-0.3.78}/llms-full.txt +0 -0
- {dol-0.3.76 → dol-0.3.78}/llms.txt +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/TIPS.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/CHANGELOG.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/code-quality-improvements.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/dol_architecture_map.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/dol_content_metadata_bifurcation.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/dol_design.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/dol_flat_model.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/dol_issue10_design.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/dol_issue16_design.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/dol_issue18_design.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/dol_issue83_design.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/dol_issue86_design.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/dol_issues_report.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/frontend_dol_ideas.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/general_design.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/generate_llms_txt_instruction.md +0 -0
- {dol-0.3.76 → dol-0.3.78}/misc/docs/issues_and_discussions.md +0 -0
|
@@ -136,12 +136,13 @@ wrappers, and test all four forms (see `tests/test_trans.py::test_redirect_getat
|
|
|
136
136
|
dol is editably installed across the local ecosystem, so **dependents already import your
|
|
137
137
|
working-tree dol** — no reinstall needed. Before landing a wrap_kvs/Store.wrap change:
|
|
138
138
|
1. Measure blast radius statically: AST-scan dependents for transforms passed to
|
|
139
|
-
wrap_kvs-family whose first param ∈ self_names (
|
|
139
|
+
wrap_kvs-family whose first param ∈ self_names (the `misc/fleet_ledger` census lists every call site with its kwargs and their kinds;
|
|
140
140
|
the reusable pattern found 0 breaking + 12 safe self-convention sites).
|
|
141
141
|
2. Run the dependents test-gate **baseline vs modified**: run each key dependent's suite
|
|
142
142
|
with your change, then `git stash` your dol edits, run again (baseline), `git stash pop`;
|
|
143
|
-
a `PASS→FAIL` delta is a real regression (env `FAIL→FAIL` is not).
|
|
144
|
-
|
|
143
|
+
a `PASS→FAIL` delta is a real regression (env `FAIL→FAIL` is not). The ledger does this
|
|
144
|
+
per test id: `python -m misc.fleet_ledger baseline` before and after, then `diff`
|
|
145
|
+
(see `misc/fleet_ledger/README.md`); `priv test-dependents dol` is the quick gate. Prioritize `config2py → graze → ju →
|
|
145
146
|
tabled → oa` (highest transitive fan-out) plus the self-convention users
|
|
146
147
|
(`py2store, sqldol, tec, unbox, mongodol`).
|
|
147
148
|
|
|
@@ -227,15 +227,7 @@ and the full doc index with status labels. The table below is the quick version.
|
|
|
227
227
|
| [dol_content_metadata_bifurcation.md](misc/docs/dol_content_metadata_bifurcation.md) | The content/metadata split-store problem (feeds issue #80) |
|
|
228
228
|
| [code-quality-improvements.md](misc/docs/code-quality-improvements.md) | Tech-debt tracker: dead code, coverage gaps (feeds issue #94) |
|
|
229
229
|
|
|
230
|
-
> The **fleet usage ledger** ([#91](https://github.com/i2mint/dol/issues/91)) lives
|
|
231
|
-
> in gitignored `misc/data/` — start at its [README](misc/data/README.md).
|
|
232
|
-
> `python -m fleet_ledger scan` records one immutable, timestamped scan: dol's **89
|
|
233
|
-
> direct dependents** pinned at full 40-char commits, their imports, AST-verified
|
|
234
|
-
> subclasses and call-site kwargs (all `file:line`), a generated zero-usage report,
|
|
235
|
-
> and the pre-PR test-gate order. `baseline` captures every dependent's suite result
|
|
236
|
-
> so a candidate's damage can be told apart from pre-existing red; `pin restore`
|
|
237
|
-
> freezes the fleet at a scan and `pin undo` releases it. Scans accumulate — nothing
|
|
238
|
-
> is overwritten. Figures quoted in these docs are from the 2026-08-21 scan.
|
|
230
|
+
> The **fleet usage ledger** ([#91](https://github.com/i2mint/dol/issues/91)) is committed code in `misc/fleet_ledger/` — start at its [README](misc/fleet_ledger/README.md). Its *output* is never committed (it is derived from the fleet, some of it private): it lives in `~/.local/share/dol/fleet_ledger/dol/`. `python -m misc.fleet_ledger scan` records one immutable, timestamped scan: every direct dependent (from priv's `dep_graph`) pinned at its full commit, its imports, AST-verified subclasses and call-site kwargs (all `file:line`), and a generated zero-usage report (`zero-usage`). `baseline` captures every dependent's suite result with its failing test ids, so `diff` tells a candidate's damage apart from pre-existing red; `pin restore` freezes the fleet at a scan and `pin undo` releases it. Scans accumulate — nothing is overwritten. Figures quoted in these docs are from the 2026-08-21 scan; rescan for current ones.
|
|
239
231
|
|
|
240
232
|
---
|
|
241
233
|
|
|
@@ -51,7 +51,7 @@ from collections.abc import (
|
|
|
51
51
|
from typing import Any, Tuple, Union, Optional
|
|
52
52
|
from collections.abc import Iterable, Callable
|
|
53
53
|
|
|
54
|
-
from dol.errors import OutOfScopeKeyError
|
|
54
|
+
from dol.errors import OutOfScopeKeyError, may_raise_out_of_scope
|
|
55
55
|
from dol.util import (
|
|
56
56
|
wraps,
|
|
57
57
|
_disabled_clear_method,
|
|
@@ -814,8 +814,8 @@ class Store(KvPersister):
|
|
|
814
814
|
lacks the prefix/suffix the layer strips) has no outer key, so it is skipped:
|
|
815
815
|
the store is a *view* of the in-scope ids. Reading or writing such an id through
|
|
816
816
|
an outer key is still impossible, and calling ``_key_of_id`` directly still
|
|
817
|
-
raises (i2mint/dol#82, #114).
|
|
818
|
-
|
|
817
|
+
raises (i2mint/dol#82, #114). ``len`` and ``bool`` agree with this iteration
|
|
818
|
+
(see ``__len__``).
|
|
819
819
|
"""
|
|
820
820
|
key_of_id = self._key_of_id
|
|
821
821
|
for _id in self.store:
|
|
@@ -826,7 +826,24 @@ class Store(KvPersister):
|
|
|
826
826
|
yield k
|
|
827
827
|
|
|
828
828
|
def __len__(self) -> int:
|
|
829
|
-
|
|
829
|
+
"""The inner store's ``len``, or, when the key layer is scoped (its
|
|
830
|
+
``_key_of_id`` is marked with ``raises_out_of_scope``), the number of in-scope
|
|
831
|
+
ids, which takes a scan (i2mint/dol#123).
|
|
832
|
+
|
|
833
|
+
The inner ``len`` is always asked first, so an inner store that refuses ``len``
|
|
834
|
+
(e.g. a remote listing, to spare ``list()`` a second scan) still refuses it
|
|
835
|
+
here, and an empty one costs no scan. Otherwise note that ``list(store)`` of a
|
|
836
|
+
scoped layer scans twice: once to size the list, once to fill it.
|
|
837
|
+
"""
|
|
838
|
+
n = len(self.store)
|
|
839
|
+
if n and may_raise_out_of_scope(self._key_of_id):
|
|
840
|
+
return sum(1 for _ in self)
|
|
841
|
+
return n
|
|
842
|
+
|
|
843
|
+
def __bool__(self) -> bool:
|
|
844
|
+
if may_raise_out_of_scope(self._key_of_id):
|
|
845
|
+
return any(True for _ in self) # stops at the first in-scope id
|
|
846
|
+
return len(self) > 0
|
|
830
847
|
|
|
831
848
|
def __contains__(self, k) -> bool:
|
|
832
849
|
return self._id_of_key(k) in self.store
|
|
@@ -145,11 +145,42 @@ class OutOfScopeKeyError(KeyError, ValueError):
|
|
|
145
145
|
__str__ = Exception.__str__ # plain message, not KeyError's repr of it
|
|
146
146
|
|
|
147
147
|
|
|
148
|
+
_RAISES_OUT_OF_SCOPE = "_dol_raises_out_of_scope"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def raises_out_of_scope(func):
|
|
152
|
+
"""Mark ``func``, a ``key_of_id``, as one that may raise ``OutOfScopeKeyError``.
|
|
153
|
+
|
|
154
|
+
A ``Store`` layer whose ``_key_of_id`` is so marked is a *view* of the in-scope
|
|
155
|
+
ids, so its ``len`` counts them (a scan) instead of returning ``len`` of the inner
|
|
156
|
+
store, which would overcount (i2mint/dol#123). Unmarked key mappings keep the inner
|
|
157
|
+
store's (often O(1)) ``len``. Returns ``func``, so it works as a decorator.
|
|
158
|
+
|
|
159
|
+
``wrap_kvs`` (and so the codecs), ``Pipe`` and subclasses made by class wrapping
|
|
160
|
+
carry the mark over; a ``_key_of_id`` you override by hand (even one calling
|
|
161
|
+
``super()``) does not, so mark it too if it can raise.
|
|
162
|
+
|
|
163
|
+
>>> @raises_out_of_scope
|
|
164
|
+
... def key_of_id(_id):
|
|
165
|
+
... return _id
|
|
166
|
+
>>> may_raise_out_of_scope(key_of_id), may_raise_out_of_scope(str.upper)
|
|
167
|
+
(True, False)
|
|
168
|
+
"""
|
|
169
|
+
setattr(func, _RAISES_OUT_OF_SCOPE, True)
|
|
170
|
+
return func
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def may_raise_out_of_scope(func) -> bool:
|
|
174
|
+
"""Whether ``func`` was marked with :func:`raises_out_of_scope` (a bound method
|
|
175
|
+
reads the mark off its function)."""
|
|
176
|
+
return getattr(func, _RAISES_OUT_OF_SCOPE, False) is True
|
|
177
|
+
|
|
178
|
+
|
|
148
179
|
def out_of_scope_prefix_error(_id, prefix):
|
|
149
180
|
"""The ``OutOfScopeKeyError`` for an ``_id`` that doesn't start with ``prefix``."""
|
|
150
181
|
return OutOfScopeKeyError(
|
|
151
182
|
f"{_id!r} does not start with the prefix {prefix!r}, so it is outside this "
|
|
152
|
-
"store's scope
|
|
183
|
+
"store's scope: iterating the store skips it, and it can't be read or "
|
|
153
184
|
"written through an outer key (see i2mint/dol#82)."
|
|
154
185
|
)
|
|
155
186
|
|
|
@@ -158,7 +189,7 @@ def out_of_scope_suffix_error(_id, suffix):
|
|
|
158
189
|
"""The ``OutOfScopeKeyError`` for an ``_id`` that doesn't end with ``suffix``."""
|
|
159
190
|
return OutOfScopeKeyError(
|
|
160
191
|
f"{_id!r} does not end with the suffix {suffix!r}, so it is outside this "
|
|
161
|
-
"store's scope
|
|
192
|
+
"store's scope: iterating the store skips it, and it can't be read or "
|
|
162
193
|
"written through an outer key (see i2mint/dol#114)."
|
|
163
194
|
)
|
|
164
195
|
|
|
@@ -448,6 +448,46 @@ def path_is_under(path: str, rootdir: str) -> bool:
|
|
|
448
448
|
return False
|
|
449
449
|
|
|
450
450
|
|
|
451
|
+
def dotdot_resolves_lexically(path: str, *, base: str = "") -> bool:
|
|
452
|
+
"""Whether every ``..`` in ``path`` undoes the segment before it, as
|
|
453
|
+
``os.path.normpath`` assumes.
|
|
454
|
+
|
|
455
|
+
It doesn't when a symlink comes before a ``..``: the OS then climbs out of the
|
|
456
|
+
link's target, so ``root/lnk/../x`` with ``root/lnk -> /elsewhere/deep`` addresses
|
|
457
|
+
``/elsewhere/x``, not ``root/x`` (i2mint/dol#123). Paths without a ``..`` segment
|
|
458
|
+
are not touched (no file-system access); otherwise the part up to the last ``..``
|
|
459
|
+
is resolved both ways and compared.
|
|
460
|
+
|
|
461
|
+
With ``base`` (a string prefix of ``path``, e.g. a store's ``rootdir``), only the
|
|
462
|
+
``..`` segments after it count: ``base`` itself is taken as the OS resolves it,
|
|
463
|
+
whatever ``..`` it holds. Conservative by design: a ``..`` after a symlink is
|
|
464
|
+
refused even when the link points back inside ``base``.
|
|
465
|
+
|
|
466
|
+
>>> dotdot_resolves_lexically('/data/root/a/b.txt')
|
|
467
|
+
True
|
|
468
|
+
>>> dotdot_resolves_lexically('/no/such/dir/../file') # nothing to follow: lexical
|
|
469
|
+
True
|
|
470
|
+
"""
|
|
471
|
+
if base and path.startswith(base):
|
|
472
|
+
base, path = path[: len(base)], path[len(base) :]
|
|
473
|
+
else:
|
|
474
|
+
base = ""
|
|
475
|
+
if os.pardir not in path: # fast path: the common case, e.g. every listed key
|
|
476
|
+
return True
|
|
477
|
+
if os.altsep:
|
|
478
|
+
path = path.replace(os.altsep, os.sep)
|
|
479
|
+
segments = path.split(os.sep)
|
|
480
|
+
if os.pardir not in segments:
|
|
481
|
+
return True
|
|
482
|
+
last_pardir = len(segments) - 1 - segments[::-1].index(os.pardir)
|
|
483
|
+
head = os.sep.join(segments[: last_pardir + 1])
|
|
484
|
+
try:
|
|
485
|
+
physical = os.path.realpath(base + head)
|
|
486
|
+
return physical == os.path.realpath(base + os.path.normpath(head))
|
|
487
|
+
except (OSError, ValueError): # unresolvable: don't vouch for it
|
|
488
|
+
return False
|
|
489
|
+
|
|
490
|
+
|
|
451
491
|
class FileSysCollection(Collection):
|
|
452
492
|
# rootdir = None # mentioning here so that the attribute is seen as an attribute before instantiation.
|
|
453
493
|
|
|
@@ -503,8 +543,20 @@ class FileSysCollection(Collection):
|
|
|
503
543
|
|
|
504
544
|
def is_in_root(self, k):
|
|
505
545
|
"""Whether ``k`` stays under ``rootdir`` (always true with
|
|
506
|
-
``allow_escape=True`` or an empty ``rootdir``).
|
|
507
|
-
|
|
546
|
+
``allow_escape=True`` or an empty ``rootdir``).
|
|
547
|
+
|
|
548
|
+
The containment test is lexical (:func:`path_is_under`), so a key whose ``..``
|
|
549
|
+
follows a symlink -- where the OS climbs out of the link's *target*, not back
|
|
550
|
+
to the link's directory -- is refused too (i2mint/dol#123).
|
|
551
|
+
"""
|
|
552
|
+
return (
|
|
553
|
+
self.allow_escape
|
|
554
|
+
or not self.rootdir
|
|
555
|
+
or (
|
|
556
|
+
path_is_under(k, self.rootdir)
|
|
557
|
+
and dotdot_resolves_lexically(k, base=self.rootdir)
|
|
558
|
+
)
|
|
559
|
+
)
|
|
508
560
|
|
|
509
561
|
def validate_in_root(self, k):
|
|
510
562
|
"""Raise ``KeyValidationError`` if ``k`` leaves ``rootdir`` (i2mint/dol#107).
|
|
@@ -117,11 +117,8 @@ class MiscReaderMixin:
|
|
|
117
117
|
Warning: If used as a subclass, this mixin should (in general) be placed before the store
|
|
118
118
|
|
|
119
119
|
Security:
|
|
120
|
-
``.pkl``/``.pickle`` keys are unpickled by default
|
|
121
|
-
|
|
122
|
-
``get_obj`` and the getters this includes URLs: ``get_obj('https://.../x.pkl')``
|
|
123
|
-
downloads and unpickles. Give it an ``incoming_val_trans_for_key`` without the
|
|
124
|
-
pickle extensions and such keys come back as raw bytes instead (i2mint/dol#113).
|
|
120
|
+
``.pkl``/``.pickle`` keys are unpickled by default, which runs code from the
|
|
121
|
+
data: see the warning in the ``dol.misc`` module docstring (i2mint/dol#113).
|
|
125
122
|
|
|
126
123
|
|
|
127
124
|
>>> # make a reader that will wrap a dict
|
|
@@ -253,11 +250,8 @@ def get_obj(
|
|
|
253
250
|
"""A quick way to get an object, with default... everything (but the key, you know, a clue of what you want)
|
|
254
251
|
|
|
255
252
|
Security:
|
|
256
|
-
``.pkl``/``.pickle`` keys are unpickled by default
|
|
257
|
-
|
|
258
|
-
``get_obj`` and the getters this includes URLs: ``get_obj('https://.../x.pkl')``
|
|
259
|
-
downloads and unpickles. Give it an ``incoming_val_trans_for_key`` without the
|
|
260
|
-
pickle extensions and such keys come back as raw bytes instead (i2mint/dol#113).
|
|
253
|
+
``.pkl``/``.pickle`` keys are unpickled by default, which runs code from the
|
|
254
|
+
data: see the warning in the ``dol.misc`` module docstring (i2mint/dol#113).
|
|
261
255
|
"""
|
|
262
256
|
if k.startswith("http://") or k.startswith("https://"):
|
|
263
257
|
v = url_to_bytes(k)
|
|
@@ -285,11 +279,8 @@ class MiscGetter:
|
|
|
285
279
|
according to a property of the key (default: file extension).
|
|
286
280
|
|
|
287
281
|
Security:
|
|
288
|
-
``.pkl``/``.pickle`` keys are unpickled by default
|
|
289
|
-
|
|
290
|
-
``get_obj`` and the getters this includes URLs: ``get_obj('https://.../x.pkl')``
|
|
291
|
-
downloads and unpickles. Give it an ``incoming_val_trans_for_key`` without the
|
|
292
|
-
pickle extensions and such keys come back as raw bytes instead (i2mint/dol#113).
|
|
282
|
+
``.pkl``/``.pickle`` keys are unpickled by default, which runs code from the
|
|
283
|
+
data: see the warning in the ``dol.misc`` module docstring (i2mint/dol#113).
|
|
293
284
|
|
|
294
285
|
>>> from dol.misc import get_obj, misc_objs_get
|
|
295
286
|
>>> import os
|
|
@@ -356,11 +347,8 @@ class MiscStoreMixin(MiscReaderMixin):
|
|
|
356
347
|
If used as a subclass, this mixin should (in general) be placed before the store
|
|
357
348
|
|
|
358
349
|
Security:
|
|
359
|
-
``.pkl``/``.pickle`` keys are unpickled by default
|
|
360
|
-
|
|
361
|
-
``get_obj`` and the getters this includes URLs: ``get_obj('https://.../x.pkl')``
|
|
362
|
-
downloads and unpickles. Give it an ``incoming_val_trans_for_key`` without the
|
|
363
|
-
pickle extensions and such keys come back as raw bytes instead (i2mint/dol#113).
|
|
350
|
+
``.pkl``/``.pickle`` keys are unpickled by default, which runs code from the
|
|
351
|
+
data: see the warning in the ``dol.misc`` module docstring (i2mint/dol#113).
|
|
364
352
|
|
|
365
353
|
See also:
|
|
366
354
|
preset and postget args from wrap_kvs decorator from dol.trans.
|
|
@@ -465,11 +453,8 @@ class MiscGetterAndSetter(MiscGetter):
|
|
|
465
453
|
according to a property of the key (default: file extension).
|
|
466
454
|
|
|
467
455
|
Security:
|
|
468
|
-
``.pkl``/``.pickle`` keys are unpickled by default
|
|
469
|
-
|
|
470
|
-
``get_obj`` and the getters this includes URLs: ``get_obj('https://.../x.pkl')``
|
|
471
|
-
downloads and unpickles. Give it an ``incoming_val_trans_for_key`` without the
|
|
472
|
-
pickle extensions and such keys come back as raw bytes instead (i2mint/dol#113).
|
|
456
|
+
``.pkl``/``.pickle`` keys are unpickled by default, which runs code from the
|
|
457
|
+
data: see the warning in the ``dol.misc`` module docstring (i2mint/dol#113).
|
|
473
458
|
|
|
474
459
|
>>> from dol.misc import set_obj, misc_objs # the function and the object
|
|
475
460
|
>>> import json
|
|
@@ -36,7 +36,7 @@ from operator import getitem
|
|
|
36
36
|
import os
|
|
37
37
|
|
|
38
38
|
from dol.base import Store
|
|
39
|
-
from dol.util import
|
|
39
|
+
from dol.util import add_as_attribute_of, max_common_prefix
|
|
40
40
|
from dol.trans import (
|
|
41
41
|
store_decorator,
|
|
42
42
|
kv_wrap,
|
|
@@ -46,7 +46,7 @@ from dol.trans import (
|
|
|
46
46
|
add_missing_key_handling,
|
|
47
47
|
)
|
|
48
48
|
from dol.dig import recursive_get_attr
|
|
49
|
-
from dol.errors import out_of_scope_prefix_error
|
|
49
|
+
from dol.errors import out_of_scope_prefix_error, raises_out_of_scope
|
|
50
50
|
from dol._paths_core import (
|
|
51
51
|
PathContext,
|
|
52
52
|
PathCreationError,
|
|
@@ -1005,6 +1005,14 @@ class KeyPath:
|
|
|
1005
1005
|
# ------------------------------------------------------------------------------
|
|
1006
1006
|
|
|
1007
1007
|
|
|
1008
|
+
def _strip_prefix(_id, prefix):
|
|
1009
|
+
"""``_id`` without ``prefix``; ``OutOfScopeKeyError`` if it doesn't start with it
|
|
1010
|
+
(slicing anyway would yield a key addressing a different object, i2mint/dol#82)."""
|
|
1011
|
+
if prefix and _id[: len(prefix)] != prefix:
|
|
1012
|
+
raise out_of_scope_prefix_error(_id, prefix)
|
|
1013
|
+
return _id[len(prefix) :]
|
|
1014
|
+
|
|
1015
|
+
|
|
1008
1016
|
class PrefixRelativizationMixin:
|
|
1009
1017
|
"""
|
|
1010
1018
|
Mixin that adds a intercepts the _id_of_key an _key_of_id methods, transforming absolute keys to relative ones.
|
|
@@ -1048,20 +1056,13 @@ class PrefixRelativizationMixin:
|
|
|
1048
1056
|
|
|
1049
1057
|
_prefix_attr_name = "_prefix"
|
|
1050
1058
|
|
|
1051
|
-
@lazyprop
|
|
1052
|
-
def _prefix_length(self):
|
|
1053
|
-
return len(getattr(self, self._prefix_attr_name))
|
|
1054
|
-
|
|
1055
1059
|
def _id_of_key(self, k):
|
|
1056
1060
|
return getattr(self, self._prefix_attr_name) + k
|
|
1057
1061
|
|
|
1062
|
+
@raises_out_of_scope
|
|
1058
1063
|
def _key_of_id(self, _id):
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
raise out_of_scope_prefix_error(_id, prefix)
|
|
1062
|
-
# slice by the live prefix's length: the cached ``_prefix_length`` goes stale
|
|
1063
|
-
# if the prefix is reassigned after first use
|
|
1064
|
-
return _id[len(prefix) :]
|
|
1064
|
+
# the live prefix, not a cached length: it may be reassigned after first use
|
|
1065
|
+
return _strip_prefix(_id, getattr(self, self._prefix_attr_name))
|
|
1065
1066
|
|
|
1066
1067
|
|
|
1067
1068
|
class PrefixRelativization(PrefixRelativizationMixin):
|
|
@@ -1259,15 +1260,13 @@ class RelativePathKeyMapper:
|
|
|
1259
1260
|
|
|
1260
1261
|
def __init__(self, prefix):
|
|
1261
1262
|
self._prefix = prefix
|
|
1262
|
-
self._prefix_length = len(self._prefix)
|
|
1263
1263
|
|
|
1264
1264
|
def _id_of_key(self, k):
|
|
1265
1265
|
return self._prefix + k
|
|
1266
1266
|
|
|
1267
|
+
@raises_out_of_scope
|
|
1267
1268
|
def _key_of_id(self, _id):
|
|
1268
|
-
|
|
1269
|
-
raise out_of_scope_prefix_error(_id, self._prefix)
|
|
1270
|
-
return _id[self._prefix_length :]
|
|
1269
|
+
return _strip_prefix(_id, self._prefix)
|
|
1271
1270
|
|
|
1272
1271
|
|
|
1273
1272
|
@store_decorator
|
|
@@ -373,3 +373,139 @@ def test_rootdir_spellings_still_list_their_keys(tmp_path, monkeypatch):
|
|
|
373
373
|
assert list(Files("/" + str(root))) == [key] # '//...' rootdir
|
|
374
374
|
monkeypatch.chdir(root)
|
|
375
375
|
assert list(Files(os.path.join("a", ".."))) == [key] # rootdir normalising to '.'
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
# --- sprint review (i2mint/dol#123): coverage of the #107 containment surface --------
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def test_path_is_under_mixed_absolute_and_relative_is_false():
|
|
382
|
+
from dol.filesys import path_is_under
|
|
383
|
+
|
|
384
|
+
# commonpath raises ValueError on a mix of absolute and relative paths
|
|
385
|
+
assert path_is_under(os.path.abspath("x"), "rel/root") is False
|
|
386
|
+
assert path_is_under("rel/root/x", os.path.abspath("root")) is False
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def test_zip_readers_read_and_write_keys_inside_rootdir(tmp_path):
|
|
390
|
+
import zipfile
|
|
391
|
+
from dol.zipfiledol import ZipFilesReader, ZipFilesReaderAndBytesWriter
|
|
392
|
+
|
|
393
|
+
if os.name == "nt": # ZipFilesReader's default subpath regex is POSIX-only
|
|
394
|
+
return
|
|
395
|
+
root = tmp_path / "root"
|
|
396
|
+
root.mkdir()
|
|
397
|
+
zip_path = str(root / "in.zip")
|
|
398
|
+
with zipfile.ZipFile(zip_path, "w") as zf:
|
|
399
|
+
zf.writestr("member.txt", b"hello")
|
|
400
|
+
reader = ZipFilesReader(str(root))
|
|
401
|
+
assert reader[zip_path]["member.txt"] == b"hello"
|
|
402
|
+
with pytest.raises(KeyError): # inside the root, but no such file
|
|
403
|
+
reader[str(root / "missing.zip")]
|
|
404
|
+
writer = ZipFilesReaderAndBytesWriter(str(root))
|
|
405
|
+
copy_path = str(root / "copy.zip")
|
|
406
|
+
writer[copy_path] = (root / "in.zip").read_bytes()
|
|
407
|
+
assert writer[copy_path]["member.txt"] == b"hello"
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def test_make_missing_dirs_mixin_reraises_out_of_root_keys(tmp_path):
|
|
411
|
+
from dol.filesys import FileBytesPersister, MakeMissingDirsStoreMixin
|
|
412
|
+
|
|
413
|
+
class S(MakeMissingDirsStoreMixin, FileBytesPersister):
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
root = _sandbox(tmp_path)
|
|
417
|
+
s = S(str(root))
|
|
418
|
+
with pytest.raises(KeyError):
|
|
419
|
+
s[str(root) + "/../escaped/x.txt"] = b"pwned"
|
|
420
|
+
assert not (tmp_path / "escaped").exists()
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
# POSIX follows a symlink before applying the next '..'; Windows normalizes '..'
|
|
424
|
+
# lexically first, so there 'lnk/../x' IS 'root/x' and nothing escapes to refuse.
|
|
425
|
+
_DOTDOT_FOLLOWS_SYMLINKS = os.name != "nt"
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _symlink_sandbox(tmp_path):
|
|
429
|
+
"""``root/lnk`` -> ``outside/deep``; ``outside/secret.txt`` is outside the root."""
|
|
430
|
+
root = _sandbox(tmp_path)
|
|
431
|
+
deep = tmp_path / "outside" / "deep"
|
|
432
|
+
deep.mkdir(parents=True)
|
|
433
|
+
(tmp_path / "outside" / "secret.txt").write_bytes(b"secret")
|
|
434
|
+
try:
|
|
435
|
+
os.symlink(str(deep), str(root / "lnk"), target_is_directory=True)
|
|
436
|
+
except (OSError, NotImplementedError): # e.g. Windows without the privilege
|
|
437
|
+
pytest.skip("cannot create symlinks here")
|
|
438
|
+
return root
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def test_files_symlink_then_dotdot_does_not_escape_rootdir(tmp_path):
|
|
442
|
+
from dol.filesys import Files
|
|
443
|
+
|
|
444
|
+
root = _symlink_sandbox(tmp_path)
|
|
445
|
+
s = Files(str(root))
|
|
446
|
+
escaping_key = os.path.join("lnk", "..", "secret.txt")
|
|
447
|
+
assert escaping_key not in s
|
|
448
|
+
assert s.get(escaping_key) is None
|
|
449
|
+
pwn_key = os.path.join("lnk", "..", "pwned.txt")
|
|
450
|
+
if _DOTDOT_FOLLOWS_SYMLINKS:
|
|
451
|
+
with pytest.raises(KeyError):
|
|
452
|
+
s[pwn_key] = b"x"
|
|
453
|
+
else:
|
|
454
|
+
s[pwn_key] = b"x"
|
|
455
|
+
assert (root / "pwned.txt").read_bytes() == b"x" # contained in the root
|
|
456
|
+
assert not (tmp_path / "outside" / "pwned.txt").exists()
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def test_symlinked_subdir_of_rootdir_stays_addressable(tmp_path):
|
|
460
|
+
# the design #107 kept: a symlink inside the root is followed, and a '..' that
|
|
461
|
+
# does not come after a symlink still resolves lexically
|
|
462
|
+
from dol.filesys import Files
|
|
463
|
+
|
|
464
|
+
root = _symlink_sandbox(tmp_path)
|
|
465
|
+
s = Files(str(root))
|
|
466
|
+
s[os.path.join("lnk", "f.txt")] = b"via link"
|
|
467
|
+
assert (tmp_path / "outside" / "deep" / "f.txt").read_bytes() == b"via link"
|
|
468
|
+
assert s[os.path.join("lnk", "f.txt")] == b"via link"
|
|
469
|
+
assert s[os.path.join("a", "..", "lnk", "f.txt")] == b"via link"
|
|
470
|
+
assert s[os.path.join("a", "..", "a", "in.txt")] == b"inside"
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def test_symlink_then_dotdot_is_refused_by_every_containment_user(tmp_path):
|
|
474
|
+
from dol.filesys import DirReader, Files, mk_dirs_if_missing
|
|
475
|
+
|
|
476
|
+
root = _symlink_sandbox(tmp_path)
|
|
477
|
+
if not _DOTDOT_FOLLOWS_SYMLINKS:
|
|
478
|
+
pytest.skip("'..' never follows a symlink here: nothing escapes to refuse")
|
|
479
|
+
(tmp_path / "outside" / "sib").mkdir()
|
|
480
|
+
with pytest.raises(KeyError):
|
|
481
|
+
DirReader(str(root))[os.path.join(str(root), "lnk", "..", "sib")]
|
|
482
|
+
s = mk_dirs_if_missing(Files(str(root)))
|
|
483
|
+
with pytest.raises(KeyError):
|
|
484
|
+
s[os.path.join("lnk", "..", "made", "x.txt")] = b"pwned"
|
|
485
|
+
assert not (tmp_path / "outside" / "made").exists()
|
|
486
|
+
# allow_escape=True is the explicit opt-out
|
|
487
|
+
assert Files(str(root), allow_escape=True)[
|
|
488
|
+
os.path.join("lnk", "..", "secret.txt")
|
|
489
|
+
] == b"secret"
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def test_rootdir_with_dotdot_after_a_symlink_lists_and_reads(tmp_path):
|
|
493
|
+
# e.g. join(dirname(__file__), '..', 'data') in a symlink-installed package: the
|
|
494
|
+
# rootdir's own '..' is the OS's business, only the key's part beyond it is checked
|
|
495
|
+
from dol.filesys import Files
|
|
496
|
+
|
|
497
|
+
src = tmp_path / "src"
|
|
498
|
+
(src / "pkg").mkdir(parents=True)
|
|
499
|
+
(src / "data" / "sub").mkdir(parents=True)
|
|
500
|
+
(src / "data" / "a.txt").write_bytes(b"A")
|
|
501
|
+
site = tmp_path / "site"
|
|
502
|
+
site.mkdir()
|
|
503
|
+
try:
|
|
504
|
+
os.symlink(str(src / "pkg"), str(site / "pkg"), target_is_directory=True)
|
|
505
|
+
except (OSError, NotImplementedError):
|
|
506
|
+
pytest.skip("cannot create symlinks here")
|
|
507
|
+
s = Files(os.path.join(str(site), "pkg", "..", "data"))
|
|
508
|
+
if os.name != "nt": # Windows resolves '..' lexically: site/data doesn't exist
|
|
509
|
+
assert list(s) == ["a.txt"]
|
|
510
|
+
assert s["a.txt"] == b"A"
|
|
511
|
+
assert s[os.path.join("sub", "..", "a.txt")] == b"A"
|
|
@@ -233,3 +233,122 @@ def test_head_skips_out_of_scope_ids():
|
|
|
233
233
|
|
|
234
234
|
s = KeyCodecs.prefixed("p/")({"stray": 0, "p/a": 1})
|
|
235
235
|
assert s.head() == ("a", 1)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# --- sprint review (i2mint/dol#123): len/bool vs the in-scope iteration of #119 -------
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@pytest.mark.parametrize("name, store", list(_prefix_stores()))
|
|
242
|
+
def test_len_counts_only_in_scope_ids(name, store):
|
|
243
|
+
assert len(store) == len(list(store)) == 2
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_store_of_only_out_of_scope_ids_is_falsy():
|
|
247
|
+
from dol import KeyCodecs
|
|
248
|
+
|
|
249
|
+
s = KeyCodecs.prefixed("p/")({"stray": 0})
|
|
250
|
+
assert list(s) == []
|
|
251
|
+
assert not s
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def test_len_of_unscoped_store_is_the_inner_len():
|
|
255
|
+
from dol import wrap_kvs
|
|
256
|
+
|
|
257
|
+
s = wrap_kvs({"a": 1, "b": 2}, key_of_id=str.upper, id_of_key=str.lower)
|
|
258
|
+
assert len(s) == 2 and bool(s)
|
|
259
|
+
assert len(wrap_kvs({}, key_of_id=str.upper)) == 0
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_head_falls_back_to_inner_ids_when_getitem_fails():
|
|
263
|
+
from dol import wrap_kvs
|
|
264
|
+
|
|
265
|
+
# an asymmetric key mapping: iterated keys can't be read back
|
|
266
|
+
s = wrap_kvs({"p/stray": 0, "p/a": 1}, key_of_id=lambda _id: _id.upper())
|
|
267
|
+
with pytest.warns(UserWarning, match="Couldn't get data for the key"):
|
|
268
|
+
assert s.head() == ("P/STRAY", 0)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_composed_and_stacked_scoped_layers_count_in_scope_ids():
|
|
272
|
+
from dol import KeyCodecs, wrap_kvs
|
|
273
|
+
|
|
274
|
+
base = {"a/x.txt": 1, "a/y.json": 2, "b/z.txt": 3}
|
|
275
|
+
composed = (KeyCodecs.prefixed("a/") + KeyCodecs.suffixed(".txt"))(dict(base))
|
|
276
|
+
assert list(composed) == ["x"] and len(composed) == 1
|
|
277
|
+
# an unscoped layer over a scoped one gets its len from the scoped inner layer
|
|
278
|
+
upper = wrap_kvs(KeyCodecs.prefixed("a/")(dict(base)), key_of_id=str.upper)
|
|
279
|
+
assert sorted(upper) == ["X.TXT", "Y.JSON"] and len(upper) == 2
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def test_subclass_of_scoped_class_stays_scoped():
|
|
283
|
+
from dol import KeyCodecs, wrap_kvs
|
|
284
|
+
|
|
285
|
+
Scoped = KeyCodecs.prefixed("p/")(dict)
|
|
286
|
+
Sub = wrap_kvs(Scoped, key_of_id=str.upper, id_of_key=str.lower)
|
|
287
|
+
s = Sub({"stray": 0, "p/a": 1})
|
|
288
|
+
assert list(s) == ["A"] and len(s) == 1 and s["A"] == 1
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_empty_affix_codec_keeps_the_inner_len():
|
|
292
|
+
from dol import KeyCodecs
|
|
293
|
+
from dol.errors import may_raise_out_of_scope
|
|
294
|
+
|
|
295
|
+
codec = KeyCodecs.affixed()
|
|
296
|
+
assert not may_raise_out_of_scope(codec.decoder)
|
|
297
|
+
assert len(codec({"a": 1})) == 1
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_scoped_len_keeps_the_inner_stores_refusal_and_empty_shortcut():
|
|
301
|
+
from collections.abc import Mapping
|
|
302
|
+
from dol import KeyCodecs
|
|
303
|
+
|
|
304
|
+
class NoLenListing(Mapping):
|
|
305
|
+
"""A remote-like store refusing len, so list() lists it only once."""
|
|
306
|
+
|
|
307
|
+
def __init__(self, d):
|
|
308
|
+
self.d, self.listings = d, 0
|
|
309
|
+
|
|
310
|
+
def __getitem__(self, k):
|
|
311
|
+
return self.d[k]
|
|
312
|
+
|
|
313
|
+
def __iter__(self):
|
|
314
|
+
self.listings += 1
|
|
315
|
+
return iter(self.d)
|
|
316
|
+
|
|
317
|
+
def __len__(self):
|
|
318
|
+
raise TypeError("no len: it would take a full listing")
|
|
319
|
+
|
|
320
|
+
inner = NoLenListing({"p/a": 1, "q/b": 2})
|
|
321
|
+
s = KeyCodecs.prefixed("p/")(inner)
|
|
322
|
+
assert list(s) == ["a"] and inner.listings == 1
|
|
323
|
+
with pytest.raises(TypeError):
|
|
324
|
+
len(s)
|
|
325
|
+
|
|
326
|
+
class CountingDict(dict):
|
|
327
|
+
def __iter__(self):
|
|
328
|
+
raise AssertionError("an empty inner store needs no scan")
|
|
329
|
+
|
|
330
|
+
assert len(KeyCodecs.prefixed("p/")(CountingDict())) == 0
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_first_arg_is_mapping_keeps_the_scope_mark():
|
|
334
|
+
from dol import wrap_kvs, FirstArgIsMapping
|
|
335
|
+
from dol.errors import raises_out_of_scope, OutOfScopeKeyError
|
|
336
|
+
|
|
337
|
+
@raises_out_of_scope
|
|
338
|
+
def key_of_id(self, _id):
|
|
339
|
+
if not _id.startswith("p/"):
|
|
340
|
+
raise OutOfScopeKeyError(_id)
|
|
341
|
+
return _id[2:]
|
|
342
|
+
|
|
343
|
+
s = wrap_kvs({"p/a": 1, "q/b": 2}, key_of_id=FirstArgIsMapping(key_of_id))
|
|
344
|
+
assert list(s) == ["a"] and len(s) == 1
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_may_raise_out_of_scope_is_a_real_bool():
|
|
348
|
+
from dol.errors import may_raise_out_of_scope
|
|
349
|
+
|
|
350
|
+
class AnyAttr:
|
|
351
|
+
def __getattr__(self, name):
|
|
352
|
+
return "truthy"
|
|
353
|
+
|
|
354
|
+
assert may_raise_out_of_scope(AnyAttr()) is False
|
|
@@ -41,6 +41,8 @@ from dol.errors import (
|
|
|
41
41
|
SetattrNotAllowed,
|
|
42
42
|
out_of_scope_prefix_error,
|
|
43
43
|
out_of_scope_suffix_error,
|
|
44
|
+
raises_out_of_scope,
|
|
45
|
+
may_raise_out_of_scope,
|
|
44
46
|
)
|
|
45
47
|
from dol.base import Store, KvReader, AttrNames, kv_walk
|
|
46
48
|
from dol._paths_core import (
|
|
@@ -1966,7 +1968,10 @@ def _wrap_outcoming(
|
|
|
1966
1968
|
[7, 14]
|
|
1967
1969
|
"""
|
|
1968
1970
|
if trans_func is not None:
|
|
1971
|
+
marked = trans_func
|
|
1969
1972
|
trans_func, wants_self = _resolve_self_convention(trans_func)
|
|
1973
|
+
# read the mark off the transform itself, not a FirstArgIsMapping around it
|
|
1974
|
+
is_scoped = may_raise_out_of_scope(marked) or may_raise_out_of_scope(trans_func)
|
|
1970
1975
|
wrapped_func = getattr(store_cls, wrapped_method)
|
|
1971
1976
|
|
|
1972
1977
|
if not wants_self:
|
|
@@ -1993,6 +1998,8 @@ def _wrap_outcoming(
|
|
|
1993
1998
|
self, getattr(super(store_cls, self), wrapped_method)(x)
|
|
1994
1999
|
)
|
|
1995
2000
|
|
|
2001
|
+
if is_scoped: # (``wraps`` already carried over a scoped wrapped_func's mark)
|
|
2002
|
+
raises_out_of_scope(new_method)
|
|
1996
2003
|
setattr(store_cls, wrapped_method, new_method)
|
|
1997
2004
|
|
|
1998
2005
|
|
|
@@ -3862,9 +3869,11 @@ def affix_key_codec(prefix: str = "", suffix: str = ""):
|
|
|
3862
3869
|
>>> codec.decoder('/folder/name.txt')
|
|
3863
3870
|
'name'
|
|
3864
3871
|
"""
|
|
3872
|
+
decoder = partial(_affix_decoder, prefix=prefix, suffix=suffix)
|
|
3873
|
+
if prefix or suffix:
|
|
3874
|
+
raises_out_of_scope(decoder)
|
|
3865
3875
|
return KeyCodec(
|
|
3866
|
-
encoder=partial(_affix_encoder, prefix=prefix, suffix=suffix),
|
|
3867
|
-
decoder=partial(_affix_decoder, prefix=prefix, suffix=suffix),
|
|
3876
|
+
encoder=partial(_affix_encoder, prefix=prefix, suffix=suffix), decoder=decoder
|
|
3868
3877
|
)
|
|
3869
3878
|
|
|
3870
3879
|
|