Skip to content

pythontk v0.8.79

Latest

Choose a tag to compare

@m3trik m3trik released this 07 Jul 03:18
  • SymbolRecord + structured HelpMixin output + python -m pythontk.help — a machine-readable introspection surface (2026-07-06). New pythontk/core_utils/symbol_record.py::SymbolRecord is a frozen-shape dataclass (name/qualname/kind/signature/summary/line/deprecated) describing one public symbol, deliberately sized to match the fields m3trik/scripts/generate_api_registry.py's static ast-walker already serializes into API_REGISTRY.json — promoting the generator's former private SymbolEntry onto this shared type changes zero committed bytes. HelpMixin now has a second, dynamic producer of the same shape: Cls.help(as_dict=True)/as_json=True, Cls.classify(as_dict=True), and Cls.list_members(as_dict=True) return SymbolRecords (enriched at the dict level with runtime-only flags/defined_in/source that the static side can't derive) instead of only formatted text, and HelpMixin.about(obj, as_dict=True) covers non-HelpMixin objects (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 scraping help() text. Also new pythontk/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 info HelpMixin exposes 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). New pythontk/str_utils/hotkey_utils.py::HotkeyUtils centralizes the on-disk hotkey-token convention ("ctl+sht+i", canonical modifier order ctl/alt/sht, single-character keys lowercased) that both mayatk's and blendertk's edit_utils.macros.MacroManager were 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"-style QKeySequence strings), and humanize_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 (no cmds/bpy), so it belongs at this DCC-agnostic layer rather than being copy-pasted twice; each MacroManager keeps 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_size match, 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 above sample_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) eigh returns 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_transform normals 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.

  • SSHClient PTY/streaming execution no longer truncates trailing output (2026-07-05). _execute_transport's read loop broke the moment channel.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 through codecs incremental 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 plain exec_command path (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-level IMAGE_EXTS tuple and near-identical sorted-os.listdir predicate — a format added in one place would silently diverge from the others. Both now live once on ImgUtils: IMAGE_EXTS (plain photographic formats — a deliberate semantic subset of the image_formats capability table) and list_image_files(directory, exts=None, full_paths=False) (sorted, case-insensitive, non-recursive; a caller-supplied exts accepts a bare string and any case — a raw tuple(".png") would have silently matched on single characters); all three modules delegate. Also routed VidUtils.compress_video's print()-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 returned None instead 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 three hierarchy_utils modules, 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.py did handler = logging.handlers = [], clobbering the stdlib logging.handlers submodule attribute process-wide for every test that ran after it; and _str_utils.truncate's middle-mode carried a leftover vis computation plus refactor-note comments. Also removed err.txt/out.txt — committed pytest-run redirects from February — and gitignored the pattern.

  • PointCloud.pca_transform gains 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 within tolerance AND 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's TestAutoInstancerNormals (partial-flip twin rejected, full-flip twin correctly matched with the shading-true rotation).

  • PointCloud.pca_transform rotation search vectorized + made deterministic; ExecutionMonitor spinner 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 one KDTree.query each — 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 the np.random.choice subsample (matching results varied run-to-run for >500-point clouds) with deterministic stride sampling. Separately, ExecutionMonitor._start_spinner_process passed --pos -122,-341 as 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-failing test_spinner_process_start_stop now 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 the pythontk → uitk → mayatk/blendertk → tentacle chain, 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 calling ptk.arrange_points_as_path/ptk.smooth_points (removed in the 2026-06-19 geo_utils clean break; now Polyline.order_points/Polyline.smooth) and a leftover empty duplicate heading. Also corrected the dependency story (numpy/Pillow are hard deps per pyproject.toml, not optional; FFmpeg/OpenCV/rembg/PyMeshLab are the feature-gated optionals), refreshed the stale pyproject.toml description to match, replaced the hand-maintained Version badge (45 patch releases stale: said 0.8.32, actual 0.8.77 — nothing regenerates it) with the dynamic img.shields.io/pypi/v/ shield, and fixed test/run_tests.py::update_readme_badge to compute the Tests-badge link target relative to the README it writes (it hardcoded ](test/), which would have re-broken docs/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 per m3trik/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 of test_execution_monitor_comprehensive.py's, and a no-op test_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 focused TestCase classes, 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 leftover DEBUG: print statements from run_tests.py and a module-level debug block (plus duplicate sys/inspect imports) from test_execution_monitor.py. Fixed two real Pillow deprecation warnings surfaced by the run: ImgUtils.are_identical called Image.getdata() (removed in Pillow 14) — switched to np.array(image) directly; and the test_img.py 16-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 unused ImageEnhance import from _img_utils.py and a dead total local from run_tests.py::update_readme_badge (pyflakes-clean now). No temp_tests/ sweep was needed — it was already empty and correctly gitignored. Verified: full suite green before and after, 1613 passed1609 passed, 18 skipped (net -4 from removing the subset/no-op tests), 0 failures/errors/warnings.