- 2026-07-25 — New
StrUtils.resolve_placeholders(text, **kwargs) -> dict: a verbose companion toreplace_placeholdersfor live previews / diagnostics. Wherereplace_placeholdersreturns only the substituted string, this returns{result, fields, resolved, unresolved}— the same substituted string, every distinct{token}base name in first-seen order (format specs like{n:03d}and attribute / index access{obj.attr}/{seq[0]}reduce to the base name; positional{}/{0}skipped), the{token: value}map for supplied keys, and the tokens left unresolved. RaisesValueErroron a malformed format string, matchingreplace_placeholders. Powers the ecosystem's new resolved-placeholder tooltips (uitkTooltipFormat.placeholder_preview). Additive;test_str.py+6. Release note: publish pythontk before uitk / mayatk / blendertk — the new tooltip depends on this. - 2026-07-25 —
SchemaSpecnow expresses list-of-nested sub-records vianested=[Spec](mirroringtyping.List[X]). The schema base could nest a single mapping (nested=Spec) but not a list of them —from_dictonly recursed when the value was adict,validateerrored "expected a mapping" on a list, andto_dictdeep-copied a list ofSchemaSpecinstances as opaque objects instead of serialising them. Any schema with a repeated sub-record (a rack's bays, a bay's occupants, a table's columns) had to drop to a hand-rolledvalidatecallable plus manualfrom_dictreconstruction at each call site — the exact per-schema duplication the base exists to remove. A one-elementnested=[Spec]now signals a list of that schema, resolved uniformly by new internal_nested_of(meta) -> (schema, is_list):from_dictbuilds a list of instances,to_dictserialises each element,validaterecurses per element with an indexed path (occupants[1].label),skeletonemits a one-element example list (still self-valid), andto_markdownmarks the field(list)and renders the element schema's table. Fully additive — barenested=Specis unchanged; a malformed multi-element[A, B]raisesSchemaError.FieldDocgainednested_is_list: bool = False.test_schema_spec.py+10 → 36/0. No other public API change; downstream (uitk/mayatk/blendertk/tentacle) unaffected. Release note: publish pythontk before mayatk (its newRackBuilderschema depends on this). - 2026-07-20 — New
CachedArtifact: the produce-once/reuse-forever lifecycle every headless-conversion pipeline was re-implementing. mayatk'sblender_bridgeand blendertk'smaya_bridgeeach carried a hand-rolled copy of the same ~40-line block — hash the inputs into a key, serve the cached file if present, else produce into a scoped scratch store and atomicallyos.replace-promote into the cache slot (plus sidecar promotion and stale-sidecar cleanup). Adding an FBX→native-scene bake stage to both would have made that four copies, so it moved topythontkbesideTempArtifacts(which it composes:"detached"owns the cache,"scoped"owns the scratch).CachedArtifact.key(*parts, files=…)folds each file's path+mtime+size in, so a template fix invalidates stale payloads rather than replaying the old bug on retry;get(key, produce, sidecars=…, use_cache=…)returns(path, hit, scratch). The scratch hop is load-bearing, not ceremony: producing straight into the slot lets a timeout-killed partial write poison every later run and lets two concurrent producers interleave into one file. Both bridges'_cache_keynow delegate to it (cache prefixes and on-disk layout unchanged).test_temp_artifacts.py+13 → 34/0. - 2026-07-19 —
bootstrap_packagenow derives__all__automatically from the resolved public surface, so it no longer has to be hand-listed or kept in sync withDEFAULT_INCLUDE(newset_allparam, default on). pythontk's root__init__.pycarried a 113-name__all__maintained by hand — and it had already drifted: 20 symbols were registered inDEFAULT_INCLUDEyet missing from__all__(Git,QcLog/QcGate/GateError,MapFactory/MapRegistry/MapType,OutputSpec/OutputTemplate/OutputTemplates,MatReport,Op,Workspace/WORKSPACE_MARKER/DEFAULT_FILE_RULES/RULE_NICE_NAMES,RpcClient,Call/Result,ImageFormat) — sofrom pythontk import *and every introspection tool that reads__all__under-reported the surface.bootstrap_packagenow populates__all__after resolver build + namespace-alias creation as the sorted union of any__all__the module declared before the call and the resolver'sclass_to_modulekeys — every module-level class/function/constant and created namespace alias reachable aspkg.Name. Flat method aliases (method_to_module, e.g.ptk.make_iterable) and bare submodule names are intentionally excluded — they still resolve lazily via__getattr__but aren't advertised, matching the prior convention. The pre-declared list is snapshotted once so a runtimeconfigure()/build_dictionaries()reconfigure re-derives cleanly without accumulating stale names. The derived pythontk__all__is a strict superset of the old curated list (113 → 133, zero regressions, every name resolves —test_all_names_resolveunchanged and green), and the importable surface is identical (those 20 names were always reachable via__getattr__;__all__now just reflects reality, so no registry regen). The manual__all__block is deleted frompythontk/__init__.py. Behavior is opt-out viabootstrap_package(..., set_all=False). Shared-infra change (core_utils/module_resolver.py): every ecosystem package (uitk, mayatk, blendertk, tentacle) that previously shipped no__all__now gets a clean auto-derived one for free — nofrom <pkg> import *exists anywhere in the monorepo, so the default is purely additive. Full suite green (2115 passed, 0 failed, 7 skipped); uitk verified (115-name__all__, all resolve).