An authoritative, code-verified map of the
dollibrary, intended for (1) a redesign/refactoring effort and (2) authoring "dev skills" (AI-agent tooling for developers working on dol). All non-obvious claims citefile:lineagainst the current source (verified 2026-07, version 0.3.46, Python ≥ 3.10).
This document is the structural/mechanical reference for dol's Python source: every
module, the public API surface, the class hierarchy, and — in depth — the
wrap_kvs/store_decorator/codec machinery that is the heart of the library. It is
deliberately factual and line-cited so it can be trusted when refactoring or when writing
agent tooling that edits dol source.
It complements, does not repeat the existing misc/docs:
| For… | Read | This doc adds |
|---|---|---|
| Language-agnostic "what/why" (middleware, KV pipeline, Russian dolls) | general_design.md | — |
| Python architecture narrative + design critique | dol_design.md | Verifies it against code; flags staleness (§ below) |
| GitHub issues/discussions themes | issues_and_discussions.md | Corrects the Issue #9 root-cause (§5.4) |
| The split-store / content-metadata problem | dol_content_metadata_bifurcation.md | — |
| Dead code / coverage tracker | code-quality-improvements.md | Adds new debt found (§11) |
| Doc index | README.md | The master index; routes here for refactors |
issues_and_discussions.md§1a stated the wrong root cause for Issue #9 (now corrected in that doc). It saidwrap_kvs"checks whetherobj_of_datahas 1 or 2+ required args". The real (pre-fix) decision was the transform's first parameter name ∈{"self","store","mapping"}(self_names). Issue #9/#12 is now FIXED (name and ≥2 required params + theFirstArgIsMappingmarker) — see §5.4.dol_design.mdciteswrap_kvsattrans.py:1801andstore_decoratorattrans.py:130.store_decoratoris still:130;wrap_kvsis now:1813(decorator) /:1814(def).Codecis now:3374(doc says:3362).dol_design.md'scache_thisexamples are stale. The current signature iscache_this(func=None, *, cache, key, pre_cache, as_property, ignore, serialize, deserialize)(dol/caching.py:1055). The wholeKeyStrategy/CachedProperty/CachedMethodsubsystem (caching.py:126–1053) is undocumented in existing docs.code-quality-improvements.mdLOC figures are per-statement (coverage), not raw lines — it liststrans.pyas "789 lines" etc. Raw line counts are much larger (see §2). Its dead-code items were spot-checked and are still present (§11).dol_design.mdclaimsStore.__getattr__returnsgetattr(self.store, attr). The real implementation guards against pickling recursion viagetattr(object.__getattribute__(self, "store"), attr)(dol/base.py:617).
LOC = raw wc -l. "Internal deps" = other dol.* modules imported at module load
time (docstring-only and inline-function imports excluded).
| Module | LOC | Purpose | Key public exports | Internal deps |
|---|---|---|---|---|
__init__.py |
208 | Public API surface; deprecation shim via module __getattr__ |
(re-exports — see §3) | everything |
base.py |
1140 | Class hierarchy + hook protocol; kv_walk; delegation machinery |
Collection, KvReader, KvPersister, Store, MappingViewMixin, kv_walk, Stream |
errors, signatures, util |
trans.py |
3492 | The core. wrap_kvs, store_decorator, filt_iter, cached_keys, Codec/ValueCodec/KeyCodec, kv_wrap, path-access adders |
wrap_kvs, filt_iter, cached_keys, kv_wrap, store_decorator, flatten, add_path_get/access, mk_read_only, Codec* |
base, errors, signatures, util |
caching.py |
2675 | Caching layers: cache_this, cache_vals, store_cached, WriteBackChainMap, key-strategy protocol |
cache_this, cache_vals, store_cached, WriteBackChainMap, flush_on_exit, ensure_clear_to_kv_store |
base, trans, util |
paths.py |
2270 | Path/nested access: KeyTemplate, KeyPath, path_get/set/filter, relative-path stores |
KeyTemplate, KeyPath, path_get, path_set, path_filter, mk_relative_path_store, flatten_dict, leaf_paths, add_prefix_filtering |
base, dig, explicit, naming, trans, util |
naming.py |
1243 | Older parametrized-key system: StrTupleDict (str↔tuple↔dict). Overlaps KeyTemplate (§7) |
StrTupleDict, mk_store_from_path_format_store_cls |
base, errors, signatures, trans, util |
sources.py |
1172 | KV views over disparate sources; composite stores | FlatReader, FanoutReader, FanoutPersister, CascadedStores, MultiSource, SequenceKvReader, FuncReader, ObjReader, Attrs |
base, caching, signatures, trans, util |
filesys.py |
867 | Filesystem stores | Files, FilesReader, TextFiles, PickleFiles, JsonFiles, Jsons, DirReader, mk_dirs_if_missing, subfolder_stores |
base, dig, kv_codecs, naming, paths, trans, trash |
kv_codecs.py |
596 | Ready-made codec namespaces | ValueCodecs, KeyCodecs (KeyValueCodecs exists but unexported) |
paths, signatures, trans, util, zipfiledol |
signatures.py |
5403 | Sig signature-calculus engine (used everywhere) |
Sig, KO/PK/… kind aliases, Param, call_forgivingly |
(none) |
util.py |
2356 | Grab-bag utilities | Pipe, lazyprop, partialclass, groupby, regroupby, instance_checker, LiteralVal, chain_get, written_bytes |
(none — self-contained) |
zipfiledol.py |
994 | Zip/tar archive stores + compression codecs | ZipFiles, ZipReader, FilesOfZip, FlatZipFilesReader, zip_compress, tar_compress |
base, errors, explicit, filesys, paths, sources, trans, util |
appendable.py |
673 | Append semantics for stores | appendable, mk_item2kv_for, Extender |
trans, util |
explicit.py |
291 | Stores driven by explicit key data | ExplicitKeyMap, KeysReader, invertible_maps |
base, sources, trans, util |
tools.py |
585 | Misc store add-ons | store_aggregate, iSliceStore, Forest |
base, filesys, trans |
misc.py |
462 | Read/write misc sources (URLs, dropbox) | MiscGetter, MiscGetterAndSetter, get_obj |
filesys, trans, util, zipfiledol |
dig.py |
206 | Introspect wrapper layers | trace_getitem, inner_most, layers |
trans |
mixins.py |
248 | Legacy mixin classes (mostly superseded by wrap_kvs) |
ReadOnlyMixin, IdentityKvWrapMixin, OverWritesNotAllowedMixin |
errors, util |
errors.py |
170 | Exception hierarchy | KeyValidationError, OperationNotAllowed, WritesNotAllowed, … |
(none) |
trash.py |
237 | Cross-platform trash/recycle delete | get_platform_trash_func, make_safe_delete_func |
(none) |
recipes.py |
5 | Thin alias module | search_paths (= paths.path_filter) |
paths |
* Codec/ValueCodec/KeyCodec/KeyValueCodec live in trans.py but are not
top-level dol exports; import from dol.trans (see §3, §6).
Foundation layer (no internal deps): signatures, util, errors, trash.
graph TD
signatures[signatures]
util[util]
errors[errors]
trash[trash]
base --> errors & signatures & util
trans --> base & errors & signatures & util
caching --> base & trans & util
naming --> base & errors & signatures & trans & util
paths --> base & dig & explicit & naming & trans & util
dig --> trans
explicit --> base & sources & trans & util
sources --> base & caching & signatures & trans & util
kv_codecs --> paths & signatures & trans & util & zipfiledol
zipfiledol --> base & errors & explicit & filesys & paths & sources & trans & util
filesys --> base & dig & kv_codecs & naming & paths & trans & trash
appendable --> trans & util
mixins --> errors & util
tools --> base & filesys & trans
misc --> filesys & trans & util & zipfiledol
recipes --> paths
Notes on the graph:
baseandtransare the two hubs. Almost everything depends ontrans;transdepends only onbase+ the foundation. There is no circular import betweentransandpaths(thefrom dol.pathslines intrans.pyare inside doctests only —trans.py:2661,2793).paths↔explicitare mutually entangled (pathsimportsexplicit;explicitimportssourceswhich is independent) — butpathsalso importsnaminganddig, making it one of the most coupled modules.zipfiledolandfilesysare the heaviest leaves, each pulling in 6–8 internal modules.filesysimportskv_codecswhich importszipfiledol— a long chain, but acyclic.
Grouped by concern. All names below are importable as from dol import <name>.
Base classes / protocol (base.py): Collection, MappingViewMixin, KvReader,
KvPersister, Reader (alias of KvReader), Persister (alias of KvPersister),
Store, kv_walk, KT, VT, BaseKeysView, BaseValuesView, BaseItemsView.
Wrappers / transforms (trans.py): wrap_kvs, kv_wrap, filt_iter,
cached_keys, cache_iter (deprecating), add_decoder, add_ipython_key_completions,
insert_hash_method, add_path_get, add_path_access, flatten, disable_delitem,
disable_setitem, mk_read_only, add_aliases, insert_aliases,
add_missing_key_handling, store_decorator, redirect_getattr_to_getitem.
Codecs (kv_codecs.py): ValueCodecs, KeyCodecs.
Caching (caching.py): cache_this, add_extension, lru_cache_method,
WriteBackChainMap, mk_cached_store (old alias), cache_vals, store_cached,
store_cached_with_single_key, ensure_clear_to_kv_store, flush_on_exit,
mk_write_cached_store.
Paths / naming (paths.py, naming.py): flatten_dict, leaf_paths,
KeyTemplate, mk_relative_path_store, KeyPath, paths_getter, path_get,
path_set, path_filter, add_prefix_filtering, StrTupleDict,
mk_store_from_path_format_store_cls.
Sources / composite (sources.py): FlatReader, SequenceKvReader, FuncReader,
Attrs, ObjReader, FanoutReader, FanoutPersister, CascadedStores, MultiSource.
Filesystem (filesys.py): Files, FilesReader, TextFiles, PickleFiles,
JsonFiles, Jsons, DirReader, ensure_dir, mk_dirs_if_missing,
MakeMissingDirsStoreMixin, resolve_path, resolve_dir, temp_dir,
create_directories, process_path, subfolder_stores.
Zip (zipfiledol.py): ZipReader, ZipInfoReader, FilesOfZip,
FileStreamsOfZip, FlatZipFilesReader, ZipFiles, ZipStore (alias),
ZipFileStreamsReader, zip_compress, zip_decompress, to_zip_file,
remove_mac_junk_from_zip, tar_compress, tar_decompress.
Utils (util.py): Pipe, lazyprop, partialclass, groupby, regroupby,
igroupby, instance_checker, chain_get, non_colliding_key, get_app_folder,
get_app_config_folder, AttributeMapping, AttributeMutableMapping,
not_a_mac_junk_path, written_bytes, written_key, read_from_bytes.
Other: store_aggregate (tools.py), mk_item2kv_for, appendable
(appendable.py), trace_getitem (dig.py), ExplicitKeyMap, invertible_maps,
KeysReader (explicit.py).
Deprecation shim: __init__.py:197 defines a module-level __getattr__ that maps the
removed get_app_data_folder → get_app_config_folder with a DeprecationWarning.
- Documented / expected but NOT top-level exported:
Codec,ValueCodec,KeyCodec,KeyValueCodec(must usefrom dol.trans import …).llms.txtlists them under "Core API / trans.py" which reads as if they weredol-level. - Exists but unexported:
KeyValueCodecsnamespace (kv_codecs.py:575) — and its two factorieskey_based/extension_basedare unimplemented stubs (§6, §11). Stream(base.py:1021) — a layer-able stream interface — is public-quality but not exported anywhere.— wired in as of the Issue #9/#12 fix (2026-07): consumed viaFirstArgIsMappingis defined but unused_resolve_self_conventionat all four call sites and exported fromdol. (Historical: it was dead code with# TODO: Use this for it's intent!.)
collections.abc.Collection (ABC: __iter__, __contains__, __len__)
└─ dol.base.Collection base.py:86 + head(); default __len__/__contains__ by iteration
└─ dol.base.KvReader base.py:156 (MappingViewMixin, Collection, Mapping)
│ + __getitem__, keys/values/items; __reversed__ → NotImplementedError
└─ dol.base.KvPersister base.py:196 (KvReader, MutableMapping) + __setitem__/__delitem__; clear() DISABLED
└─ dol.base.Store base.py:469 wraps self.store; adds the 4 transform hooks
Aliases: Reader = KvReader (base.py:191), Persister = KvPersister (:239),
KvStore = Store (:737).
Mixins & helpers (base.py):
MappingViewMixin(:141) — swapKeysView/ValuesView/ItemsViewclass attributes to customize views instead of overriding.keys()/.values()/.items().DelegatedAttribute(:251) +delegate_to(:291) +delegator_wrap(:366) — the delegation engine.Store.wrap = classmethod(partial(delegator_wrap, delegation_attr="store"))(:611) lets any Store carry its own wrapping method.KeyValidationABC(:978),Stream(:1021),kv_walk(:768).
The hook protocol (the customization surface of Store):
Hook (default = static_identity_method) |
Direction | Called by |
|---|---|---|
_id_of_key(k) → _id |
ingoing key | __getitem__, __setitem__, __delitem__, __contains__ |
_key_of_id(_id) → k |
outgoing key | __iter__ |
_data_of_obj(obj) → data |
ingoing value | __setitem__ |
_obj_of_data(data) → obj |
outgoing value | __getitem__ |
Defaults assigned at base.py:600–603. __getitem__ (:631) also routes
_errors_that_trigger_missing (default (KeyError,), :607) to __missing__ if present.
Store.__getattr__ (:613) delegates everything else to self.store using
object.__getattribute__ to survive pickling; __getstate__/__setstate__
(:723/:730) persist only _state_attrs = ["store", "_class_wrapper"].
Three documented ways to inject hooks (Store docstring, base.py:469–578): subclass
& override; assign callables to _id_of_key etc. on class/instance; or use wrap_kvs
(preferred). Convention per .claude/rules/dol-conventions.md: prefer wrap_kvs over
subclassing Store.
This is the heart of dol and the root of Issues #9/#12/#18. Everything below is verified
against dol/trans.py.
store_decorator(func) takes a class-transforming func(store=None, *, **kw) and returns
a wrapper usable four ways: class-decorator, class-decorator-factory,
instance-decorator, instance-decorator-factory. Mechanism:
- It computes an enriched signature
wrapper_sig=Sig(func)merged with the "wrapper assignment" params__module__, __name__, __qualname__, __doc__, __annotations__, __defaults__, __kwdefaults__(all keyword-only, defaultNone) —trans.py:357. These let callers rename/re-doc the produced class. wrapper(store=None, **kwargs)(:363): ifstore is None→ returnpartial(_func_wrapping_store_in_cls_if_not_type, **kwargs)(the factory branch); else call it directly._func_wrapping_store_in_cls_if_not_type(:324) is where instance vs class is resolved: ifstoreis not a type (an instance), it buildsWrapperStore = func(Store, **kwargs)then returnsWrapperStore(store_instance)— i.e. the instance is first wrapped in a freshStoresubclass (:337–338). Ifstoreis a type, it asserts all-but-first args are keyword-only (:340) and callsfunc(store, **kwargs).- After producing
r, it copies over any explicitly-passed dunder specials (:346).
Consequence to know (documented in the docstring, :219): decorating an instance
returns an object whose type is dol.base.Store (or a subclass), not the original
type. b.store == a but isinstance(b, type(a)) is False.
double_up_as_factory (:36) is the simpler cousin (direct-or-factory, no
instance-wrapping) used for plain function decorators; it validates first-arg-defaults-
to-None and all-else-keyword-only (:90).
Decorated with @store_decorator, so it inherits the full 4-way behavior. Body is short
(:1968–1978): default name to the store's qualname, assemble kwargs, call
_handle_codecs(kwargs) to normalize codec/encoder/decoder aliases into the canonical
key_of_id/id_of_key/obj_of_data/data_of_obj (:1981), then delegate to
_wrap_store(_wrap_kvs, kwargs).
Parameters (canonical + aliases resolved by _handle_codecs):
- Canonical:
key_of_id,id_of_key,obj_of_data,data_of_obj,preset,postget. - Codec shortcuts:
key_codec/value_codec(aCodec→ its.encoder/.decoderare split into the canonical pair), and the four*_encoder/*_decoderaliases._handle_codecsraisesValueErrorif you pass both a codec and the canonical it maps to (:2000,:2007, …). - Advanced:
outcoming_key_methods,outcoming_value_methods,ingoing_key_methods,ingoing_value_methods— extra method names to also wrap alongside the defaults.
For each of the four canonical transforms it wraps the corresponding hook method
(plus any extra *_methods): _key_of_id/_obj_of_data via _wrap_outcoming
(:2141–2145); _id_of_key/_data_of_obj via _wrap_ingoing (:2147–2151).
postget/preset are special-cased (:2156–2190): they replace __getitem__/
__setitem__ directly so the transform receives (k, data).
This was the crux of Issue #9/#12. Status: RESOLVED (2026-07) — the mechanism below is the current (fixed) behavior; the historical bug is preserved for context.
(A) For the four canonical transforms (key_of_id, id_of_key, obj_of_data,
data_of_obj), _wrap_outcoming and _wrap_ingoing branch on a single resolver
_resolve_self_convention(trans_func) -> (func, wants_self):
wants_self == False→ the wrapped method callstrans_func(<value>).wants_self == True→ it callstrans_func(self, <value>).
_resolve_self_convention (a) unwraps an explicit FirstArgIsMapping marker and forces
wants_self=True, else (b) falls back to _has_unbound_self. The fixed rule is:
wants_self ⟺ first parameter name ∈ self_names = {"self","store","mapping"}AND_num_required_positional_params(params) >= 2.
The arity clause is the fix. Before, the decision was name-only — so a unary callable
whose first param merely happened to be named self (e.g. bytes.decode) was mis-called
as trans_func(store, data):
# OLD (buggy): _has_unbound_self(bytes.decode) -> True (first param named 'self')
wrap_kvs(dict, obj_of_data=bytes.decode)()['k']
# → TypeError: descriptor 'decode' for 'bytes' objects doesn't apply to a 'dict' object
# NOW (fixed): bytes.decode has only 1 required positional -> treated as unary -> works.
A genuine self-aware transform def f(self, data) (2 required) is unaffected. If
signature() raises ValueError (a signature-less builtin), _has_unbound_self returns
False. The old note that this was an "arg-count check" was wrong; it is now name and
arity.
(B) For postget/preset, _wrap_kvs resolves the marker first, then does its
num_of_args(...) < 2 arity validation on the unwrapped function, then branches on the
resolved wants_self.
The explicit escape hatch — now wired in: FirstArgIsMapping(f) (a LiteralVal
subclass in trans.py) forces the (self, data) convention regardless of names/arity.
It was dead code (# TODO: Use this for it's intent!); the fix consumes it via
_resolve_self_convention at all four call sites and exports it from dol. This realizes
Issue #12's proposed solution.
Backward-compat evidence: an AST scan of dol + all 76 dependents found 0
behavior-changing call sites (12 genuine self-convention transforms, all ≥2 required, are
preserved); a recall-gap scan of attribute/imported-name transforms across 13 heavy users
found 0 more; and a baseline-vs-modified dependents test-gate across 25 repos showed 0
pass→fail regressions. See dol_issues_report.md and the dol-dev-wrap-kvs skill.
Remaining redesign note: the name-based fallback is kept for compatibility; the clean
long-term direction is explicit-marker-only (drop self_names), a future breaking change.
_wrap_outcoming/_wrap_ingoing wrap methods on the generated class via
super(store_cls, self).<method>(...). When a method inside a wrap_kvs-decorated
class calls self[k], self is the inner (unwrapped) instance, so the transform pipeline
is bypassed. Workaround (per CLAUDE.md gotchas): re-apply the wrapper to self inside the
method, or route through the wrapped instance explicitly.
kv_wrap(trans_obj) builds a wrapper from an object carrying _key_of_id/_id_of_key/
_obj_of_data/_data_of_obj attributes (a "trans object"), with chainable
.outcoming_keys(...), .ingoing_keys(...), .outcoming_vals(...), .ingoing_vals(...)
attributes (_kv_wrap_* at :2198–2348). Same underlying _wrap_outcoming/_wrap_ingoing.
Defined at the end of trans.py (:3365–3451); ready-made namespaces in
kv_codecs.py.
Dataclasses (trans.py:3374):
@dataclass
class Codec(Generic[DecodedType, EncodedType]):
encoder: Callable # decoded → encoded (the "write"/ingoing side)
decoder: Callable # encoded → decoded (the "read"/outgoing side)
__iter__ = (encoder, decoder) # so `enc, dec = codec` works
compose_with(other) → Pipe both sides # __add__ (:3382)
invert() → swap encoder/decoder # __invert__ (:3389)Subclasses are callable store-wrappers (each just calls wrap_kvs):
ValueCodec(obj)→wrap_kvs(obj, data_of_obj=encoder, obj_of_data=decoder)(:3403)KeyCodec(obj)→wrap_kvs(obj, id_of_key=encoder, key_of_id=decoder)(:3408)KeyValueCodec(obj)→wrap_kvs(obj, preset=encoder, postget=decoder)(:3413)
Composition: codec_a + codec_b composes encoders left→right and decoders right→left
(compose_with, :3382) — order matters and is inverse on the two sides so round-trips
hold. Pipe (util.py:638) is the underlying left-to-right function composition;
ValueCodecs.pickle() + ValueCodecs.gzip() means "pickle then gzip on write, gunzip then
unpickle on read". Codecs can also be threaded through Pipe(...) as store wrappers.
kv_codecs.py namespaces (all classes subclass CodecCollection, which is
non-instantiable — :227; .default sub-namespace holds pre-called default codecs,
populated by @_add_default_codecs, :254):
ValueCodecs(:263):pickle,json,csv,csv_dict,gzip,bz2,lzma,zipfile,tarfile,base64,urlsafe_b64,codecs,quopri,plistlib,xml_etree,str_to_bytes,stringio,bytesio,single_nested_value,tuple_of_dict. Each is built byvalue_wrap(= codec_wrap(ValueCodec, …),:222) which usesSigto give the factory a signature merged from encoder+decoder params.KeyCodecs(:427):affixed,suffixed,prefixed,common_prefixed,mapped_keys. Affix codecs go throughaffix_key_codec(trans.py:3439).KeyValueCodecs(:575, unexported):key_based,extension_based— both are empty stubs (extension_based'sext_mappingis unused;:596–597). Dead/incomplete.
The CSV codecs use the "signature-template" pattern: @Sig-decorated no-op functions
(_csv_rw_sig etc., :36) exist only so their parameter lists can be composed onto the
real encode/decode functions. Vulture flags their params as unused — this is by design
(see code-quality-improvements.md).
There are two overlapping parametrized-key systems, and Discussion #21's concern is still valid:
| Capability | paths.KeyTemplate (paths.py:1658) |
naming.StrTupleDict (naming.py:432) |
|---|---|---|
| Template with named fields + per-field regex | yes (field_patterns) |
yes (format_dict) |
str_to_dict / dict_to_str |
yes | yes |
str_to_tuple / tuple_to_str |
yes | yes |
str_to_namedtuple / str_to_simple_str |
yes | partial |
per-field from_str/to_str casts |
yes (from_str_funcs/to_str_funcs) |
yes (process_info_dict) |
.key_codec(src, tgt) → a KeyCodec store-wrapper |
yes | no |
.filt_iter / .clone conveniences |
yes | no |
| Age / status | newer, richer, actively used | older; only StrTupleDict + mk_store_from_path_format_store_cls exported; 31% test coverage (lowest in the package) |
Verdict: KeyTemplate supersedes StrTupleDict for essentially all new use. naming.py
is a redesign-shrink candidate (deprecate StrTupleDict → thin adapter over KeyTemplate,
or move its still-used bits — mk_pattern_from_template_and_format_dict, used by
filesys.py:11 — into paths.py).
Other path machinery in paths.py:
KeyPath(:956) — a path object with a configurable separator; usable as akey_of_id/id_of_key.path_get(:405) /path_set(:712) /path_filter(:836) — nested get/set/ search. Note there is also a private_path_get(:216) andchain_get(util.py:298) doing overlapping "walk a path of getitems" work — three implementations of the same idea, exactly what Discussion #21 flagged.mk_relative_path_store(:1139),PrefixRelativizationMixin(:1036),add_prefix_filtering(:1336),prefixless_view(:1270) — the relative/prefix family.flatten_dict(:99, viaflattened_dict_items) andleaf_paths(:134, via_leaf_paths_recursive) — nested→flat. Note these use their own recursion, notkv_walk; onlypath_filter/search_pathsare built onkv_walk— another instance of the same-idea-implemented-N-times debt.
Also note trans.flatten (trans.py:2867) flattens a store of stores (levels-aware),
while paths.flatten_dict flattens a plain nested Mapping — two "flatten"s with
different semantics; a naming-collision trap for agents.
The subsystem is far larger than the existing docs suggest (2675 lines). Key pieces:
cache_this (:1055) — unified property/method cache. Signature:
cache_this(func=None, *, cache=None, key=None, pre_cache=False, as_property=None, ignore=None, serialize=None, deserialize=None). Auto-detects property vs method from the
signature unless as_property forces it. cache may be a MutableMapping, an attribute
name (string) resolved on the instance, or a callable self → MutableMapping
(enables per-instance persistent caches, e.g. cache=lambda self: Files(f'/cache/{self.id}')).
Backed by:
KeyStrategyprotocol (:126) + implementationsExplicitKey(:165),ApplyToMethodName(:188),InstanceProp(:211),ApplyToInstance(:233),FromMethodArgs(:255),CompositeKey(:284), registered via@register_key_strategy(:158). This is the pluggable key-generation layer that decides what key a cached value is stored under. None of this is in the existing docs.CachedProperty(:464) andCachedMethod(:807) — the two descriptor classescache_thisdispatches to.
Store-level caching:
cache_vals(:1902, aliasmk_cached_store) — read-through value cache in front of a slow store.mk_sourced_store(:2000) — cache-aside with asourcefallback.store_cached(store, key_func)(:2161) /store_cached_with_single_key(:2223) — function memoization into an arbitrary store.mk_write_cached_store(:2386) +flush_on_exit(:2355) — buffered writes flushed on context exit.WriteBackChainMap(:2528) —ChainMapwhere writes hit the first map and reads fall through.ensure_clear_to_kv_store(:2293) — re-enable the disabledclear().
Stacking behavior (Issue #50): cache_this decorators are descriptor-based and key on
the method name / arg-derived key; stacking multiple @cache_this on one method is not
robustly supported (key conflicts / invalidation). lru_cache_method (:1596) and
cached_method (:1541) are the lighter in-memory alternatives. For agents: prefer a
single cache_this with an explicit key/cache over stacking.
| Pattern | Mini-example | Why |
|---|---|---|
X_of_Y naming |
id_of_key(k)→_id, key_of_id(_id)→k |
Directionality is explicit; always paired. Outer=key/obj, inner=_id/data. |
Test with dict, swap backend |
S = wrap_kvs(dict, …); …; S = wrap_kvs(Files('/d'), …) |
Same transforms, real backend. Core testing convention. |
4-way store_decorator usage |
@filt_iter(filt=f) on a class / filt_iter(inst, filt=f) / filt_iter(filt=f) factory |
One decorator, class-or-instance, with-or-without params. |
Pipe composition |
Pipe(json.dumps, str.encode, gzip.compress) |
Left-to-right function chain (util.py:638). |
| Codec composition | ValueCodecs.pickle() + ValueCodecs.gzip() |
Encoders compose L→R, decoders R→L; round-trip safe. |
Stack wrap_kvs (Russian dolls) |
key layer, then value layer, then filter layer | Each layer independent/composable. |
postget/preset for key-conditioned values |
postget=lambda k,v: json.loads(v) if k.endswith('.json') else v |
Use only when the value transform depends on the key. |
Re-wrap self inside methods |
inside a wrap_kvs-class method, wrap self again before self[k] |
Works around Issue #18 (self is unwrapped). |
Avoid bytes.decode as a transform |
use lambda b: b.decode() |
bytes.decode's first param is named self → misclassified (§5.4, Issue #9). |
| Customize views via class attr | MyStore.KeysView = MyKeysView |
Don't override .keys(); override the view class (MappingViewMixin). |
Re-enable clear() |
ensure_clear_to_kv_store(store) |
clear() is disabled on KvPersister. |
- New store — preferred:
wrap_kvs(backend, …)(class or instance). Test withdictfirst. (.claude/rules/dol-conventions.md.) - New store — read-only: subclass
KvReaderimplementing__getitem__,__iter__(optionally__len__,__contains__). Read-write: subclassKvPersisteradding__setitem__,__delitem__. - New store — with hooks: subclass
Store, override_id_of_key/_key_of_id/_data_of_obj/_obj_of_data. (Discouraged vswrap_kvsunless the hook protocol is genuinely needed.) - New codec: build a
ValueCodec/KeyCodec/KeyValueCodec(encoder=…, decoder=…), or add a factory to aCodecCollectionnamespace usingvalue_wrap/key_wrap(kv_codecs.py:222). Compose with+. - New store decorator: write
func(store=None, *, **kw)and wrap with@store_decoratorfor free 4-way behavior. - New key-cache strategy: implement the
KeyStrategyprotocol and@register_key_strategy(caching.py:126,158). - "Hooks for optimized ops" (Discussion #24) — NOT yet implemented. The intended
design: dol tooling (
filt_iter,update, sort/paginate) would check for a backend-provided fast method (e.g._filter_, a fast-syncprotocol) before falling back to Python-level iteration — analogous to how__len__optimizeslen(). The plug point today would be in_filt_iter(trans.py:1403) andStore.update/__iter__. This is greenfield for the redesign.
- Signature-conditioning fragility (§5.4). The name-based
(data)vs(self, data)heuristic (self_names,_has_unbound_self) is the single highest-leverage fix. Wire in the already-definedFirstArgIsMappingmarker (trans.py:2113) as the explicit opt-in and make the name heuristic a deprecated fallback. Fixes Issue #9/#12, removes a class of silent bugs, and de-risks everywrap_kvscall. High impact, moderate effort. trans.pyis a 3492-line kitchen sink. It holds the core wrappers, filtering, codecs, path-access adders, alias inserters, missing-key handlers, and hashing. Split intotrans_core(wrap machinery),codecs(moveCodec/ValueCodec/… out — they're logicallykv_codecs'), andstore_addons(path-get/access, aliases, hash). High impact on maintainability + testability.naming.py↔paths.pyduplication (§7). DeprecateStrTupleDictin favor ofKeyTemplate; consolidate the three path-get impls (path_get,_path_get,chain_get).naming.pyhas the lowest coverage in the package (31%). Medium impact, removes ~1200 lines of parallel logic.signatures.py(5403 lines) andcaching.py(2675) module-size hotspots.Sigis load-bearing everywhere and a hard dependency to test around; consider carving the rarely-used arithmetic out.caching.pybundles the key-strategy protocol, two descriptor classes, and ~10 store-cache helpers — extractable. Medium.- Dead / incomplete code (confirmed present):
KeyValueCodecs.key_based/.extension_based— empty stubs (kv_codecs.py:580,589).FirstArgIsMapping— defined, never used (trans.py:2113).util.delegate_as— raisesNotImplementedErrorwith unreachable code after (util.py:1598).stream_util.skip_lines—n_lines_to_skipparam unused (base.py:1016).zipfiledol.py:267passafterraise; unuseddisable_deletes(trans.py:551).- Duplicate
identity/identity_funcdefined 3× (caching.py:87,121;sources.py:28;util.py:159) andHashableMixin/HashableDictduplicated acrossutil.py/caching.py/mixins.py. Low-effort cleanup; some already tracked incode-quality-improvements.md.
- LSP violation: disabled
clear()on a declaredMutableMapping(base.py:224).dict(store)/ anything calling.clear()breaks. Consider a read-only marker type in the hierarchy rather than method-nulling. Design-level; breaking. - No async, no generics, ABC-not-Protocol — the three additive modernizations from
dol_design.md's critique (§Design Critique) remain open and non-breaking if done as additions.
- Tests live in
dol/tests/(test_trans.py,test_caching.py,test_paths.py,test_filesys.py, …). Runpytest dol/tests/. Doctests are first-class — most functions document via runnable doctests;pytest --doctest-modules dol/matters. Never break a doctest silently. - The
(self, data)trap (§5.4). Any transform whose first parameter is namedself,store, ormappingwill be called asf(store_instance, value). This is the most common way to accidentally breakwrap_kvs. When writing or reviewing transforms, check the first parameter name. Preferlambda x: …over bound/unbound builtins. - The "self is unwrapped" trap (§5.5, Issue #18). Inside a
wrap_kvs-decorated class,self[k]bypasses the transforms. Don't assume in-classself[...]is transformed. - Decorating an instance changes its type to
Store(§5.1).wrap_kvs(some_dict, …)returns aStore, not adict;isinstancechecks andtype().__name__will surprise you. The original is at.store. store_decoratorrequires all-but-first args keyword-only and first arg defaulting toNone. Violating this asserts at decoration/first-call time (trans.py:340,:92). Follow the same rule for new decorators (matches CLAUDE.md's keyword-only convention).Codec/ValueCodec/KeyCodecare intrans.py, not exported at top level. Import fromdol.trans. Ready-made ones:dol.ValueCodecs/dol.KeyCodecs.- Keep core dependency-free.
dolcore imports nothing outside stdlib. New optional deps go behind lazy imports with a helpful error (see howkv_codecslazily importspickle/gzip/plistlibinside the class body,:315,356). Sig(signatures.py) is everywhere —store_decorator, codec factories, andwrap_kvsall rewrite signatures with it. If a wrapper's signature looks wrong, suspectSigmerge logic. Don't read all 5403 lines; useSig(func).names/.defaults/.kinds.- Two different "flatten"s and three "path get"s (§7). Disambiguate before touching:
trans.flatten(store-of-stores) vspaths.flatten_dict(nested Mapping);path_get/_path_get/chain_get. - Every module must keep its top-level docstring (all 20 currently have one — verified;
they are auto-extracted for docs). When adding a module or editing one, preserve/enhance
it (per CLAUDE.md). And update
misc/docs/README.md(the master index) if you add a doc here.
Generated as a structural companion to the misc/docs/ design set. Cross-links above are
intra-repo. Line numbers are current as of the verification date; re-check after any large
trans.py/caching.py refactor.