-
SymbolRecord+ structuredHelpMixinoutput +python -m pythontk.help— a machine-readable introspection surface (2026-07-06). Newpythontk/core_utils/symbol_record.py::SymbolRecordis a frozen-shape dataclass (name/qualname/kind/signature/summary/line/deprecated) describing one public symbol, deliberately sized to match the fieldsm3trik/scripts/generate_api_registry.py's staticast-walker already serializes intoAPI_REGISTRY.json— promoting the generator's former privateSymbolEntryonto this shared type changes zero committed bytes.HelpMixinnow has a second, dynamic producer of the same shape:Cls.help(as_dict=True)/as_json=True,Cls.classify(as_dict=True), andCls.list_members(as_dict=True)returnSymbolRecords (enriched at the dict level with runtime-onlyflags/defined_in/sourcethat the static side can't derive) instead of only formatted text, andHelpMixin.about(obj, as_dict=True)covers non-HelpMixinobjects (modules, functions, plain classes) with{name, signature, module, doc, source}. The point is a common record both the committed static registry and any live class can hand an agent or RPC caller — no more scrapinghelp()text. Also newpythontk/help.py, a shell-level introspection CLI:python -m pythontk.help <dotted.path> [member] [--json|--source|--where|--signature|--brief]resolves a dotted path (importing the longest importable module prefix, then walking remaining attrs) and prints the same infoHelpMixinexposes in-process — deliberately excluded from the public registry walker (it's a CLI shell, not API surface). Tests:test_symbol_record.py(11),test_help_mixin.py(+13). -
HotkeyUtils— portable Maya-style hotkey-token <-> Qt-key-sequence conversion, pulled down as the shared SSoT (2026-07-06). Newpythontk/str_utils/hotkey_utils.py::HotkeyUtilscentralizes the on-disk hotkey-token convention ("ctl+sht+i", canonical modifier orderctl/alt/sht, single-character keys lowercased) that both mayatk's and blendertk'sedit_utils.macros.MacroManagerwere each hand-maintaining independently:parse_key(token ->(ctl, alt, sht, key)),qt_sequence_to_key/key_to_qt_sequence(round-trip against Qt's"Ctrl+Shift+I"-styleQKeySequencestrings), andhumanize_label(snake_case -> Title Case for macro display names, with an acronym map and already-uppercase-word passthrough). Pure string/dict manipulation with no DCC import (nocmds/bpy), so it belongs at this DCC-agnostic layer rather than being copy-pasted twice; eachMacroManagerkeeps only its own DCC-specific hotkey registration and MRO-walking macro discovery. Tests:test_hotkey_utils.py(13). -
PointCloud.pca_transform: fixed the subsampling floor that silently broke every >sample_sizematch, plus an identity candidate and a Kabsch/ICP refinement rescue (2026-07-06). Three robust-mode fixes, each surfaced by mayatk's AutoInstancer against a ground-truth CAD scene: (1) robust mode subsampled BOTH clouds independently, so a rotated query point's true twin was usually absent from the KDTree target set and even an exact copy at the exact rotation scored a nearest-neighbor floor around the inter-vertex spacing — no candidate could pass a tight tolerance for any mesh abovesample_size(674-vertex assembly copies never matched; ≤500-vertex ones did, hiding the bug). Only the query side is subsampled now; the target stays dense. (2) The identity rotation is always included as a candidate: for DEGENERATE covariances (near-equal eigenvalues — a cube, a sphere)eighreturns an arbitrary basis per cloud, so no eigenvector-derived candidate is guaranteed to align two already-aligned clouds (a scaled duplicate in the same orientation failed to match depending on the numpy build's eigenvector choice). (3) New_refine_rotation: when no discrete candidate passes, the best coarse candidates are refined by nearest-neighbor Kabsch iterations (proper rotations only — det +1, so a reflected twin can never slip through) and re-gated at the strict tolerance + flip-free normals check; the 15°-quantized spin search around a symmetric axis can never reach a 0.001 tolerance for an arbitrary-angle copy (measured: refinement converges a rotated 674-point canister copy to mean 4e-5 with normals dot 1.0). Consumer-verified end-to-end in mayatk (5 rotated canister copies now share one shape). -
PointCloud.pca_transformnormals gate no longer vetoes true rigid copies over coincident-vertex twins (2026-07-05). Hard-edged (CAD) meshes duplicate a position with different normals; the gate paired each rotated point with its single nearest neighbor, which could pick the wrong coincident twin (measured on production data: true twin at the same distance with dot=1.0 vs the picked twin at dot=-0.46) — one such phantom "flipped normal" rejected the whole candidate rotation. Each point now scores by its best-agreeing neighbor among the K=4 nearest that sit within the positional tolerance (the nearest always counts, so meshes without duplicated positions behave exactly as before, and a genuinely flipped vertex still has no agreeing twin — partial-flip rejection is unaffected). Applied to both the KDTree and brute-force scoring paths. Surfaced by mayatk's AutoInstancer failing to instance combined CAD assembly copies; behavior verified there end-to-end. -
SSHClientPTY/streaming execution no longer truncates trailing output (2026-07-05)._execute_transport's read loop broke the momentchannel.exit_status_ready()returned True, after at most one 4096-byte read per stream — but Paramiko can report the exit status while unread bytes still sit in the receive buffer, so the final chunk(s) of a command's output were silently dropped in both captured (use_pty=True) and streamed modes. The loop now only stops once the exit status is known AND both stdout/stderr buffers are drained. Also fixed a second latent decode defect on the same path: each chunk was decoded independently (errors="replace"), so a multi-byte UTF-8 character straddling a 4096-byte chunk boundary became two replacement characters; both streams now go throughcodecsincremental decoders (flushed at exit, so a genuinely truncated trailing sequence still renders as a replacement character). Regression tests:test_net_utils.py::test_execute_transport_drains_output_buffered_at_exit(exit status ready from the first poll with two chunks buffered — old code returned only the first) and::test_execute_transport_multibyte_char_split_across_chunks("café" split mid-é). The plainexec_commandpath (stdout.read()) was never affected. -
ImgUtils.list_image_files+ImgUtils.IMAGE_EXTS— the SfM-ingest directory-scan SSoT (2026-07-05). The photogrammetry ingest cluster (ExposureEqualizer/ImageCurator/MaskGenerator) each carried an identical module-levelIMAGE_EXTStuple and near-identical sorted-os.listdirpredicate — a format added in one place would silently diverge from the others. Both now live once onImgUtils:IMAGE_EXTS(plain photographic formats — a deliberate semantic subset of theimage_formatscapability table) andlist_image_files(directory, exts=None, full_paths=False)(sorted, case-insensitive, non-recursive; a caller-suppliedextsaccepts a bare string and any case — a rawtuple(".png")would have silently matched on single characters); all three modules delegate. Also routedVidUtils.compress_video'sprint()-based progress/error reporting through the module logger (logging.getLogger(__name__)) to match the rest of the ingest cluster — a batch caller can now capture why a compression returnedNoneinstead of losing the reason to stdout. Tests:test_img.py::ListImageFilesTest(+4). -
Dead-code sweep — package + test tree now pyflakes-clean (2026-07-05). Removed unused imports/locals across 15 package modules (
class_property,git,help_mixin,logging_mixin,module_resolver,_gif_viewer, the threehierarchy_utilsmodules,metadata,_file_utils,map_optimizer,progression,ssh_client,_str_utils) and 16 test files (intentional availability probes —cv2/KDTree/pythontk.net_utils— kept). Two of the "dead locals" were live hazards:test_audio_utils.pydidhandler = logging.handlers = [], clobbering the stdliblogging.handlerssubmodule attribute process-wide for every test that ran after it; and_str_utils.truncate's middle-mode carried a leftoverviscomputation plus refactor-note comments. Also removederr.txt/out.txt— committed pytest-run redirects from February — and gitignored the pattern. -
PointCloud.pca_transformgains optional normal-consistency verification (normals_a/normals_b/normal_threshold) (2026-07-05). Point positions alone cannot distinguish a symmetric shape from a shading-variant twin — a flat plate maps onto itself under a 180° flip while its normals invert, so a purely positional best-fit can return a rotation that flips the visible faces of whatever consumes it (found via mayatk's AutoInstancer replacing symmetric CAD parts with flip-shaded instances). When both normal arrays are supplied (unit vectors paired with the point arrays), candidate rotations must also align the normals: a candidate is valid only when its point fit is withintoleranceAND its rotated normals have zero flipped pairings (a true rigid copy aligns every normal — float noise cannot drive a ~1 dot below zero) AND mean dot ≥normal_threshold(default 0.8); among valid candidates the best normal agreement wins, so a legitimately flipped symmetric twin still matches via the rotation that maps its shading too. Backward compatible — omitting the normals reproduces the previous purely positional behavior exactly. Both the KDTree path (vectorized stacked scoring) and the no-scipy brute-force fallback implement the gate. Verified: full pythontk suite 1648/1648; consumer-side coverage in mayatk'sTestAutoInstancerNormals(partial-flip twin rejected, full-flip twin correctly matched with the shading-true rotation). -
PointCloud.pca_transformrotation search vectorized + made deterministic;ExecutionMonitorspinner fixed for negative cursor coordinates (2026-07-05). The robust-mode alignment scored up to 576 candidate rotations (24 bases × 24 symmetry spins) in a Python loop with oneKDTree.queryeach — profiled at 31s of a 32s mayatk auto-instancing run (393k queries). All candidate rotations are now scored in a single stacked query (einsum-rotated(K·N, 3)batch, one C call); the no-scipy brute-force fallback keeps its per-rotation loop to bound memory. Behavior-equivalent-or-better: the old near-perfect early-exit is replaced by a global argmin over all candidates. Also replaced thenp.random.choicesubsample (matching results varied run-to-run for >500-point clouds) with deterministic stride sampling. Separately,ExecutionMonitor._start_spinner_processpassed--pos -122,-341as two tokens when the cursor sits on a monitor left/above the primary — argparse reads the negative value as an option flag, the spinner exits code 2, and no indicator ever appears; now uses the--pos=form (regression test:test_spinner_accepts_negative_position, plus the previously machine-dependent-failingtest_spinner_process_start_stopnow passes cursor-anywhere). -
Docs front door rewritten to convey the package's actual role (2026-07-04).
docs/README.md(the PyPI long-description and GitHub landing) now opens with a "Why" section — pythontk as the bottom of thepythontk → uitk → mayatk/blendertk → tentaclechain, its two organizing rules (primitives placed by data type not domain; shared code moves down to become the SSoT) — plus a package→coverage table with the lazy-root usage forms, an "Infrastructure the ecosystem is built on" section (bootstrap_package, preset/template stores,AppLauncher/HandoffBridge, QC gates), and a Links section matching the other ecosystem landings. The example tour was de-duplicated and every API it references verified to resolve against the installed package — which caught and fixed a stale example still callingptk.arrange_points_as_path/ptk.smooth_points(removed in the 2026-06-19 geo_utils clean break; nowPolyline.order_points/Polyline.smooth) and a leftover empty duplicate heading. Also corrected the dependency story (numpy/Pillow are hard deps perpyproject.toml, not optional; FFmpeg/OpenCV/rembg/PyMeshLab are the feature-gated optionals), refreshed the stalepyproject.tomldescriptionto match, replaced the hand-maintained Version badge (45 patch releases stale: said 0.8.32, actual 0.8.77 — nothing regenerates it) with the dynamicimg.shields.io/pypi/v/shield, and fixedtest/run_tests.py::update_readme_badgeto compute the Tests-badge link target relative to the README it writes (it hardcoded](test/), which would have re-brokendocs/README.md's../test/link on the next run). Follows the uitk front-door pattern (see uitk CHANGELOG 2026-07-04); pythontk stays hub-wired tier perm3trik/docs/DOCS_STANDARD.md(one hand-written doc — no DOCMAP ledger warranted). -
Test-suite cleanup + two Pillow forward-compat fixes (2026-07-01). Consolidated
test_execution_monitor*from 4 files down to 1 (test_execution_monitor.py) —test_execution_monitor_maya.py's 3 cases were a strict subset oftest_execution_monitor_comprehensive.py's, and a no-optest_houdini(body was just comments +pass) asserted nothing; the remaining, genuinely distinct coverage (DCC-python-executable resolution, spinner subprocess lifecycle, public re-export surface) was merged in as two focusedTestCaseclasses, restoring the repo's "one test file per module" rule. Removed two tracked-by-mistake manual/smoke scripts with no assertions (manual_test_execution_monitor_integration.py,temp_smoke_img_utils.py) and two orphaned empty scratch dirs (test/bench,test/test_output) not referenced by any test. Dropped leftoverDEBUG:print statements fromrun_tests.pyand a module-level debug block (plus duplicatesys/inspectimports) fromtest_execution_monitor.py. Fixed two real Pillow deprecation warnings surfaced by the run:ImgUtils.are_identicalcalledImage.getdata()(removed in Pillow 14) — switched tonp.array(image)directly; and thetest_img.py16-bit-height-map fixture saved an"I"-mode (32-bit) image as PNG, which Pillow 13 will reject — switched to the explicit"I;16"mode the fixture actually means. Also dropped an unusedImageEnhanceimport from_img_utils.pyand a deadtotallocal fromrun_tests.py::update_readme_badge(pyflakes-clean now). Notemp_tests/sweep was needed — it was already empty and correctly gitignored. Verified: full suite green before and after,1613 passed→1609 passed, 18 skipped(net -4 from removing the subset/no-op tests), 0 failures/errors/warnings.