-
2026-09-27 --
FileUtils.get_dir_contentslists in one order on every file system: each directory's entries as NTFS sorts them, upper-cased then as written (file_utils/_file_utils.py). It returned the file system's own order -- sorted on NTFS, none in particular on ext4 -- so on Linux a listing, and whatever a caller keeps by position, changed with the disk: of two FBX presets with one name, the Scene Exporter keeps the last one listed. The threaded walk (num_threads) took results as they completed, in any order on any platform; it keeps walk order now. Windows results are unchanged. Found by CI's first Linux run (test_get_dir_contents_dirpath,test_get_dir_contents_inc_files). Test:test_file.py(ascandirthat lists in reverse; serial and threaded). ItsFileTestclass now puts the committedtest_files/file1.txtback (a partial run left it rewritten, which a release commit sweeps up) and makes thesub-directoryits listing tests read, which they found only whentest_create_directoryhad run first. -
2026-09-27 --
FileUtils.atomic_write_textgives the file the mode any write would: an existing file keeps its own, a new one gets0o666less the umask (file_utils/_file_utils.py). It wrote throughtempfile.NamedTemporaryFile, which creates0o600, andos.replacekeeps the temp's mode -- so on POSIX every rewrite of a shared file left it readable by its owner alone: the scene-data sidecar beside a delivery (each exporter's own writer, before they shared this one, gave0o644), presets, the naming-convention doc. Newlines are unchanged (text mode,\nasos.linesep). Tests:test_atomic_write.py(+3; the two mode cases run on the Linux leg). -
2026-09-27 --
AppLauncher.find_appon Windows answers the newest install its Program Files scan finds, andscan_for_executablesranks versions naturally (core_utils/app_launcher/_discovery.py). The scan (apps with no App Paths entry) returned glob's first hit, which NTFS yields by name -- the OLDEST install (Blender 3.6 over 5.1, Toolbag 4 over 5) -- and it runs atresolve_app_path'sapp_namesstage, ahead of the newest-firstscan_globs, so blendertk'sfind_blender(highest version before it delegated) and mayatk's Blender bridge inherited it. Each depth now ranks throughscan_install_dirs(4.10over4.9);scan_for_executables' reverse string sort put4.9over4.10. Tests:test_app_launcher.py::test_find_app_program_files_fallback_takes_the_newest_install,::test_scan_for_executables_ranks_versions_naturally. -
2026-09-27 --
UserConfig.xdg_home(kind): the one reading of$XDG_CONFIG_HOME/$XDG_DATA_HOME, anduser_config_root()ignores a relative one (core_utils/user_config.py). An absolute value, else the spec's default (~/.config/~/.local/share): the XDG spec calls a relative value invalid and Qt ignores it, but used as given the root moved with the working directory (relcfg/uitk) -- and, with uitk'sPresetManager.get_presets_root()now delegating here (it resolved through Qt before), every preset store moved with it. The same rule was hand-read elsewhere, and those readers use it now:AppLauncher's.desktopsearch dirs, unitytk's Unity Hub config dir, and mayatk's / blendertk's Substance installer (Documents viauser-dirs.dirs) and Painter log path. Tests:test_user_config.py(test_a_relative_xdg_config_home_is_ignored,test_xdg_home_takes_only_an_absolute_value). -
2026-09-27 --
Metadata.seton Windows: a value or clear the property store takes drops the sidecar's parked copy of that key (file_utils/metadata.py). Withenable_sidecar, a commit the store refused (a cloud placeholder) parks the value in the sidecar, whichgetoverlays on every read, so once the store took writes again a later clear or new value was still shadowed. The sidecar keeps only what a store refused (MetadataInternal._sync_sidecar, shared with the POSIX path); a file with nothing parked gets no sidecar. Tests:test_metadata.py::TestMetadataSidecarWindows. -
2026-09-27 --
PresetStore.activeholds the preset's file stem, so a rename or delete by either spelling moves or clears it (core_utils/presets/store.py). A name with punctuation is stored under a stem without it ("m3trik (laptop)"ism3trik _laptop_.json), but the pointer kept the name as typed whilelist(), panel combos and the Preset Editor use the stem, sorename/deletecompared the two and missed: renaming the activem3trik (laptop)in the Preset Editor left.activenaming a gone file, and tentacle's launch-timeMacros.apply_saved_macros()hit aKeyErrorand fell back to the default hotkeys. The setter stores the stem,rename/deletecompare stems, and a pointer an older release wrote (the name as typed) reads back as its stem. Tests:test_preset_store.py(4),test_preset_library.py::EditTest::test_renaming_the_active_preset_moves_the_pointer_whatever_its_name. -
2026-09-27 -- A hot reload re-runs
bootstrap_packageandDeprecation.attributescleanly (core_utils/module_resolver.py,core_utils/deprecation.py).importlib.reloadre-executes a module in the SAME globals. The bootstrap handle took the__all__the previous run derived for a hand-declared one, so a name dropped from the include stayed advertised andfrom pkg import *raised on it; and everyDeprecation.attributesre-run chained onto the hooks the previous run installed -- one more__getattr__/__dir__layer per reload (two perreload_package), each still serving aliases the edited module no longer declares. Module hooks are now tagged with the__spec__they were installed under (a reload binds a fresh one): a re-run chains past stale ones while two installers in one run still compose (_ModuleHook, shared withlazy_exports). Tests:test_module_resolver.py,test_deprecation.py. -
2026-09-27 --
FileUtils.canonical_module_path(filepath): the dotted module path a.pyfile has by its location (file_utils/_file_utils.py). Public now (was_canonical_module_path, used byget_classes_from_path): extapps' panel launcher needs the package alauncher.pyrun as__main__belongs to, and had grown its own__init__.pywalk-up for it. Tests:test_file.py(3, renamed). -
2026-09-26 --
PresetLibraryscans a scope:domains(inc=, exc=),entries(..., inc=, exc=)andPresetLibrary.in_scope(keys, inc, exc)(core_utils/presets/library.py). A pattern names a FOLDER under the root and takes every store under it -- matched shell-style, ignoring case, against a key and each of its parent folders, so"mayatk"is every mayatk store and"mayatk/scene_*"one tool's (a bare prefix such as"maya"matches nothing). Matching isIterUtils.filter_list(nested_as_unit=True)over the folder chain. An out-of-scope store's sidecars are never read. uitk's Preset Editor focuses on it.PresetLibrary.collection_named(name, exclude=)is the public form of the taken-name lookup (was_named), so a form checks a name by the library's own rule. Tests:test_a_pattern_names_a_folder_and_takes_its_stores_with_it,test_entries_are_scoped_the_same_way, red first;test_a_name_is_taken_ignoring_case_and_edge_spaces. -
2026-09-26 --
core_utils/app_launcher.pyis a package, split by job behind the unchangedAppLauncherfacade:app_launcher/(_app_launcher.pythe facade --launch/run/spawn/write_batch_script;_discovery.pyinstall and interpreter discovery;_environment.pythe live process environment and the persisted PATH;_desktop.pyWindows sessions and window lookup;_processes.pythe process table and a spawned child's lifetime binding). A 1,660-line module with five jobs (CODE_STANDARD section 3's god-module trigger). Every member, public and private, still resolves onptk.AppLauncherwith its kind, signature and docstring, and cross-calls are still spelledAppLauncher.<name>, sopatch.object(AppLauncher, ...)reaches them. Thepythontk.core_utils.app_launcherpath still resolvesAppLauncher(lazy_exports; the Rizom bridge and the vendored Marmoset engines import the root name now), its loggers arepythontk.core_utils.app_launcher.*, andprocess_stream/x11stay flat as primitives with users of their own. -
2026-09-26 --
PreviewServeris split by concept insidenet_utils/preview/(server.pykeeps the facade -- lifecycle, manifest and viewer liveness,publishand its delivery dials, the browser; newroutes.pythe route names and the HTTP handler; private mixins_serve_root.pythe page, viewer scripts and atomic writes,_sharing.pythe guest listener and its tunnel,_page_outputs.pyrecordings and stills).server.pywas 2,145 lines holding the HTTP layer and four jobs of the server.PreviewServer's members are unchanged and it stays at its path and root name; the route constants (VIEWER_CLOSED_PATH,SETTINGS_PATH,PLAYBLAST_PATH,PLAYBLAST_ACTIONS,SNAPSHOT_PATH) are still importable fromserver.py. -
2026-09-26 --
AppLauncher._posix_environanswers None where no libc loads, instead of raising (core_utils/app_launcher/).ctypes.CDLL(None)raises TypeError, not OSError, on a non-POSIX host, sodesktop_env-- andFileUtils.open_explorerthrough it -- failed silently there. Found by an extapps test that simulates Linux on Windows. -
2026-09-26 --
HandoffScope: the Scope words (selected/all/visible) and their precedence, in one place (core_utils/handoff/handoff_scope.py,net_utils/preview/bridge.py).HandoffScope.resolve(scope, selected=..., all=..., visible=...)resolves a word through lookups the host supplies (a lookup, or a chain tried in order): an unknown word isselected, and a widening word whose lookups all answerNonenarrows to the selection, never to anything wider. Four resolvers had hand-kept that rule (mayatk's and blendertk's bridge-slots bases, their Lightmap Bakers) besidePreviewBridge.scope_objects, which now resolves through it too;HandoffScope.wordmaps a label'sscenetoall. Registered at the root. -
2026-09-26 --
core_utils/logging_mixin.pyis a package, split by job:logging_mixin/(_logging_mixin.pyLoggingMixin,logger_ext.pyLoggerExtwith its formatters and handlers,table_mixin.pyTableMixin, internal_text_layout.pyTextLayout). A 2,038-line module with four jobs, whose layout helpers wereLoggerExtstaticmethods reading and writingLoggerExt._wcwidth_fnby name: the column math (display widths, wrapping, box and table geometry) is now oneTextLayoutinstance that owns its state, held atLoggerExt._layoutand shared byTableMixin, with an injectable width function (TextLayout(wcwidth=False)forces the heuristic without mutating shared state). Root names and thepythontk.core_utils.logging_mixinpath are unchanged (lazy_exportsresolves all seven former names, so uitk's and extapps' deep imports keep working); the privateLoggerExt._char_width/_display_width/_pad/_truncate/_wrap_text/_split_lines/_wcwidth_fnare gone, and output is byte-identical (LayoutCharacterizationTestpins every box, divider, group and table, written before the split;test_text_layout.py). -
2026-09-26 -- The preset store and library are one subpackage:
core_utils/presets/(store.py,library.py).PresetLibraryis built onPresetStoreand landed as one more flat sibling of it, the case CODE_STANDARD section 3's subpackage rule was written from. Root names (ptk.PresetStore,ptk.PresetLibrary,ptk.Codec...) are unchanged; no other package imported the old paths. -
2026-09-26 --
ptk.TiledPath: the one tile / frame token vocabulary of a path (file_utils/tiled_path.py;MapRegistry.split_tile_token,MapFactory.get_tile_paths).<UDIM>,<uvtile>,<u>_<v>,<f>,<frame>in one(stand-in, glob)table with the operations every caller asked privately:has_token,is_frame_sequence,scheme,wildcard,spell(a UDIM number in each tile vocabulary),tiles(expand to the set) andrepresentative(collapse to one file). Four vocabularies had grown (mayatk six tokens, blendertk two, blendertk's exporter three,MapFactorytwo); the texture taxonomy now also splits and globs a<u>_<v>tile set, which it read as part of the map name. -
2026-09-26 --
HierarchyAnalyzer.detect_reparented/detect_fuzzy_renames/detect_suffix_flattening: the Hierarchy Sync pairing passes (core_utils/hierarchy_utils/hierarchy_analyzer.py). Plain missing/extra path lists in,(pairs, remaining_missing, remaining_extra)out, so the passes chain; a host vetoes a reparent pairing through acompatible(reference, current)predicate. mayatk and blendertk each carried the three passes (0.86-0.96 similar); both now call these. -
2026-09-26 --
ptk.SceneExporterBaseandptk.SceneDataSidecarBase: the Scene Exporter's DCC-free shell and the scene-data sidecar, written once (core_utils/engines/scene_export/scene_exporter.py,scene_data_sidecar.py). The exporter base holds the progress stream, the check-override consent and resume, the run config and the log-file plumbing (hooks:TASK_MANAGER_CLASS,_saved_scene_path,KEPT_EDITS_ADVICE); the sidecar base holds the v3 format, naming, legacy migration, manifest I/O, diff report and comparison (hooks:expand_to_descendants,drop_intermediate,build_clean_path_set,_close). mayatk and blendertk each carried the same 20 exporter and 24 sidecar methods (a few differing only in a comment or a host word); their classes now subclass these and keep only the scene I/O. -
2026-09-26 -- The scene-export engine:
scene_records,export_profileandhierarchy_baselinemove intocore_utils/engines/scene_export/, andscene_records.pysplits by concept (scene_records.pydeclarations,scene_store.pySceneStoreBase,export_snapshot.pyExportContext/ExportSnapshot,record_transfer.pyTransferContext/RecordTransfer). The three meet the engines bar (model + planner, shared by both DCC exporters;shot_modelalready imported them), and the 2,273-line module held four jobs. Root names are unchanged; the old deep pathspythontk.core_utils.scene_records/.export_profile/.hierarchy_baselinewarn and resolve until 0.13.0. The engine's loggers arepythontk.core_utils.engines.scene_export.*. -
2026-09-26 --
AppLauncher.companion_python(executable=None)/looks_like_python(path): the Python a host binary pairs with (core_utils/app_launcher/,core_utils/execution_monitor/_execution_monitor.py). A DCC's GUI binary runs-cas its own language, but the interpreter it ships (mayapybeside it, or Blender's undersys.prefix) shares its site-packages.ExecutionMonitorresolved this privately and uitk kept a product-name table; both now ask this one, which names no product. -
2026-09-26 --
StrUtils.natural_sort_key(text, ignore_case=False): the one natural-sort key (str_utils/_str_utils.py). Embedded integers rank by value (Cube2<Cube10,4.10>4.9). mayatk's and blendertk'sAutoInstancerandAppLauncher.scan_install_dirseach carried a copy (_natural_key); all three call this now, and the private copies are gone. It marks numbers by split parity rather thanstr.isdigit, so a superscript run ("x²"), which the copies passed toint()and raised on, stays text. Tests:test_str.py(5). -
2026-09-26 --
ptk.InstancingStrategy/StrategyConfig/StrategyType: the auto-instancer's strategy decision tree (core_utils/engines/instancing/instancing_strategy.py). Identical in mayatk and blendertk except for the one scene read; the triangle count is now a hook (_get_triangle_count) each DCC binding overrides, and a caller that knows the count passestriangle_count=and needs no binding. Test:test_instancing_strategy.py(19), pinned against both DCC copies before the hoist. -
2026-09-26 --
ptk.CylinderSeams: band-based UV seam placement for cylinder / tube / turned meshes, moved down from mayatk (geo_utils/uv/cylinder_seams.py). The seamer was already a numpy-only core over plain arrays (points, faces, edge table, hard-edge ids) with one Maya-facing reader; the core lives here now and mayatk keeps the reader. Test:test_cylinder_seams.py(9, built meshes: capped / open / stepped / chamfered / torus), pinned against the mayatk copy. -
2026-09-26 --
HierarchyBaselineStore: the scene-stored hierarchy baseline's storage, shared by both DCC exporters (core_utils/engines/scene_export/hierarchy_baseline.py).read/inherited_from/is_unreadable/compare/write/adopt_sidecarwere byte-identical in mayatk's and blendertk'sHierarchyBaseline, and the multi-deliverable adoption fix of 2026-09-26 had to land twice. A DCC subclass now setsSTORE(itsSceneStoreBase) andSIDECARand, where its exporter ships more than the selected paths, overrides_close(mayatk closes under ancestors). -
2026-09-26 -- The Shot Manifest and Shots panels' domain rules move into the shots engine (
engines/shots/manifest/range_resolver.py,manifest_model.py,mapping/_mapping.py, newengines/shots/shot_report.py).RangeResolvergainsparse_range_edit,previous_end,find_collisions,cascade_from,all_ranges_complete,step_indexandgaps_from_regions;ManifestModel.describe_read_failure(+LOW_DISK_BYTES),StepStatus.find_object,Mapping.seed_user_folder, andShotReport(summary,moved,delta_summary) for the Shots footer. They were controller code copied into both DCC panels; now each rule has one owner and a test. -
2026-09-26 --
FileUtils.move_file(overwrite=True)no longer deletes the destination before the move lands (file_utils/_file_utils.py). It removed the old file and then moved, so a move that failed (a full disk, a folder the user cannot write) lost both; and a cross-volume move copied straight onto the destination, leaving it truncated on failure. A move onto an existing file, or across volumes, is now staged beside the destination and swapped in (a same-volume move onto a free name stays a plain rename); a refused swap puts the source back. The stage is a hidden per-call sibling (.<name>.<pid>.<id>.moving) that noTempArtifactssweep matches: a moved file keeps its source's mtime, so staged as anatomic_write_*temp a week-old source read as stale to the sweep a concurrentatomic_writeinto the same folder runs, and a thread-pooled texture archive (MapOptimizer.optimize_maps) lost textures mid-move (test_file_transfer.py: a sibling sweep between stage and swap, 8 threads overwriting 32 week-old files). A failed cross-volume DIRECTORY move keeps its stage and logs where it is: the source may be half removed, so the stage can hold the only whole copy (test_an_interrupted_directory_move_keeps_the_whole_copy). mayatk's and blendertk's lightmap bakers carried a hand-written copy of this (_move_into_place) and now call it. -
2026-09-26 --
ImgUtils,MapFactory,MathUtilsandStrUtilsare split by job behind unchanged facades (img_utils/_codecs.py,_image_header.py,_channels.py,_filters.py,_atlas.py,_rasterize.py,_color_space.py;core_utils/engines/textures/map_factory/_texture_sets.py,_map_inventory.py,_converters.py,_channel_packing.py;math_utils/_clustering.py,_curve_fit.py;str_utils/_name_pattern.py,_search.py,_affix.py). CODE_STANDARD section 3's god-module trigger:_img_utils.py4,879 -> 2,718 lines,_map_factory.py3,927 -> 2,020,_math_utils.py2,379 -> 1,878,_str_utils.py2,513 -> 1,811. Every public method keeps its signature and docstring on the facade and delegates to an internal base of the same class (super().<name>(...)), so the flatptk.<method>surface,cls-bound helpers, subclass overrides (extapps'ConverterSlots(ImgUtils)) andmock.patch.object(ImgUtils, ...)all behave as before; private helpers moved whole and still resolve asImgUtils._<name>through the MRO. A layout test per facade pins that no public method lives only on a base. Left whole:logging_mixin.py(see the ledger: its split is a subpackage promotion plus an untangling ofLoggerExtclass state). -
2026-09-26 --
MapFactory.get_base_texture_nameowns the base-name rule;ImgUtils.get_base_texture_nameis a facade over it (core_utils/engines/textures/map_factory/_map_factory.py,img_utils/_img_utils.py). What counts as a map suffix is the texture engine's taxonomy, yet the one implementation sat in the generic*_utilslayer and reached back up intoMapRegistry, so the engine called down into ImgUtils which called up into the engine. The engine now holds it; ImgUtils keeps the public method (extapps'ConverterSlotsinherits it, mayatk calls it) through a deferred import, andset_bit_depth's map-type -> mode lookup stays a deferredMapRegistryread.test_img.pypins that noimg_utilsmodule imports an engine at module level. -
2026-09-26 -- UV-layout primitives are one subpackage,
geo_utils/uv/(pack.py,budget.py,transfer.py);file_utils/uv_unwrap/is a module (file_utils/uv_unwrap.py). Threeuv_*siblings shared a concept prefix their neighbours did not (CODE_STANDARD section 3, trigger 4), anduv_unwrap/was a package holding one module. Root names are unchanged (ptk.UvPack,ptk.UvBudget,ptk.UvTransfer,ptk.UvUnwrap, ...); no other package imported the old paths, so there is no alias. -
2026-09-26 --
ptk.ShotBoundaryConflict,ptk.GapRetimeandShotPlanner.UNBOUNDED(core_utils/engines/shots/shot_plan.py). blendertk reached the conflict class through the module path (11 imports) and took the private_INFsentinel; mayatk's_shot_planshim imported_INFand_EPSthe same way. The names are root-registered and the sentinel has a public spelling on the planner (theenv_endof an envelope no shot bounds), so both DCC packages import from the root. -
2026-09-26 --
UserConfig.CONFIG_ROOT_ENV_VAR: the config-root env var's public spelling (core_utils/user_config.py). Seven files in uitk, mayatk and extapps imported the constant from the module path; they now read it off the root class. The module-level name stays for pythontk's own code. -
2026-09-26 --
AppLauncher.write_batch_script(path, lines, shell=None): the one writer of a script a shell runs (core_utils/app_launcher/).cmdgets the console OEM codepage and exact CRLF (a text-mode write of"\r\n"doubled every CR; a character cmd cannot spell raises instead of becoming?);bashgets UTF-8, LF, a shebang and the executable bit. extapps' RealityScan and SuGaR runners each hand-wrote theirs.linesmay be one pre-joined string (it was written a character per line). Test:test_app_launcher.py::test_a_pre_joined_block_may_be_one_string. -
2026-09-26 --
ptk.TooltipFormat: the rich-text tooltip DSL, moved down from uitk (str_utils/tooltip_format.py).fmt/kbd/hl/placeholder_preview/stored_itemsand the layout rules (wrap,display_ms) are pure string work; headless engine code (scene-exporter task definitions under a DCC's background mode) imported them from a uitk Qt module. The plain/rich decision is the Qt-free port ofQt.mightBeRichText, now public asTooltipFormat.is_rich.test_tooltip_format(69 tests, moved from uitk). -
2026-09-26 --
ptk.ShotSequencer: the shot sequencer's ripple-editing orchestration, written once (core_utils/engines/shots/shot_sequencer.py). mayatk and blendertk each carried a copy of the same ~50 timeline operations (define, expand, resize, slide, ripple, insert, delete, merge, split, pad, respace, apply a gap, move sequences between shots, fit to content) -- 36 methods at >=0.8 AST similarity, most differing only in which scene call they made. The DCC classes now subclass this one and supply narrow scene hooks (_apply_plan,_move_content_keys,_content_batch,_key_extent, ...; the table is in the module docstring). Used on its own it is a complete bounds-only sequencer. Tests:test_shot_sequencer.py(66), its bounds expectations recorded from mayatk's sequencer before the hoist. -
2026-09-26 --
ptk.InstanceGrouping: auto-instancing's signature-bucket merge and run summary (core_utils/engines/instancing/instance_grouping.py). Hand-copied verbatim in mayatk's and blendertk'sAutoInstancer(_merge_similar_signatures,default_summary,format_summary); both delegate here now. Test:test_instance_grouping.py(12), expectations recorded from the DCC copies. -
2026-09-26 --
ShotDetection.cluster_spans: the one key-timing overlap grouping (core_utils/engines/shots/shot_detection.py). Strict overlap, touch, an epsilon seam and a frame gap are one sweep with a different join threshold, and it was written four times (cluster_segments_by_gap, mayatkAnimUtils._group_overlapping_keyframes/SegmentKeys._group_by_overlap, blendertkStaggerKeys._group_units) plus two interval merges. All six route through it;span=reads plain(start, end)tuples. Tests:test_shots_core.TestClusterSpans(9; one pins the exact-threshold seam:start - end <= gap, shot detection's form, splits a seam that float subtraction rounds one ULP abovegap). -
2026-09-26 -- The CSV-mapping schema and resolve round-trip tests live with the engine (
test/test_shots_manifest_core.py). Moved from mayatk'stest_shot_manifest_mapping.py, which ran only under mayapy yet exercised onlypythontk.core_utils.engines.shots.manifest.mapping. -
2026-09-26 --
SingletonMixinkeeps each subclass's__init__signature, name and docstring (core_utils/singleton_mixin.py). The once-only wrapper it installs was bare, soinspect.signatureanswered(*args, **kwargs)for every singleton class; it isfunctools.wraps-ed now. uitk's launch-code rendering asks a handler's signature whether itsswitchboardis optional. Test:test_singleton_mixin+1 (red first). -
2026-09-26 --
mesh_convert/is split by job:MeshConvertis a facade over one private mixin per job, and the GLB family is one subpackage,glb/(file_utils/mesh_convert/_mesh_convert.py->_fbx2gltf.py,_sidecar.py,_lightmaps.py,_shadow_rigs.py,_animation.py,_visibility.py,_materials.py,_textures.py,_images.py;glb_reader/glb_pipeline/glb_clips/glb_fades/glb_key_reduction/glb_tangents->glb/reader.py...glb/tangents.py; newglb/edit.py)._mesh_convert.pywas 9,889 lines doing nine jobs, and the sixglb_*siblings imported the facade only to reach its private container members. The container is nowGlbEdit(glb/edit.py), whoseopen/read/write/append_bin_views/compact_bin/drop_orphaned_accessors/release_animation_payloadthe siblings call directly. EveryMeshConvertattribute resolves as before --MeshConvert.GlbEdit,open_glband the private buffer helpers are bound toGlbEdit's -- andptk.GlbReader/ptk.GlbPipelineare unchanged at the root. Loggers follow their modules (pythontk.file_utils.mesh_convert.<module>): listen onpythontk.file_utils.mesh_convertfor the whole family. -
2026-09-26 --
ptk.GlbFadesis a root name, and its channel table isGlbFades.CHANNELS(file_utils/mesh_convert/glb/fades.py;glb_fades.pyis a one-release stub). mayatk, blendertk and uitk deep-importedmesh_convert.glb_fades.CHANNELSto join their channel rows to the glTF half; they readptk.GlbFades.CHANNELSnow. The old path servesCHANNELS,GlbFadesandPointerChannelwith aDeprecationWarninguntil 0.13.0. -
2026-09-26 --
lazy_exports: one declaration publishes a subpackage's names, each loaded on first use (core_utils/module_resolver.py). The root'sbootstrap_packagescan imports every subpackage__init__, so an eager re-export there was paid by everyimport pythontk(the map-factory, mapping and behaviors packages now load nothing until a name is read), and uitk carried four hand-written PEP 562 loaders. CODE_STANDARD section 4 now makes lazy loading the default. It is reload-safe:importlib.reloadre-runs the call in the same globals, and the re-run drops the names the last run cached (afterptk.reload_package("pythontk"),ptk.LoggingMixinwas still the old class), chains past the last run's__getattr__instead of stacking on it, and unions only a hand-declared__all__. A declared name whose submodule raises AttributeError while importing, or lacks the name, raises ImportError naming the submodule with the AttributeError as__cause__--from pkg import Xsaid only "cannot import name 'X'", andhasattrread it as absent. Tests:test_module_resolver.py(LazyExportsTests). -
2026-09-26 --
Deprecation.attributesresolves targets past the module, onto a class (core_utils/deprecation.py). A retired module function usually lands as a method (pkg.launch->pkg.slots.Slots.launch); the alias path can now say so. A target module that exists but fails inside its own import raises instead of reading as a missing attribute -- including when the missing dependency's name is a string prefix of the target's package (depinsidedeppkg.mod), which a barestartswithwalked past. Test:test_deprecation.py. -
2026-09-26 -- The app-handoff kit is one subpackage:
core_utils/handoff/(app_handoff,script_template,script_run,manifest(washandoff_manifest),manifest_plan). Five flat modules of one family, per CODE_STANDARD section 3. Every public name is unchanged at the root (ptk.HandoffBridge,ptk.ScriptTemplate,ptk.SEND_TO...); the two paths other packages imported,core_utils.app_handoffandcore_utils.script_template, warn and forward until 0.13.0. -
2026-09-26 --
TRANSFORM_CHANNELS: the channel vocabulary in display order;MeshConvert.FBX2GLTF_VERSION(core_utils/engines/shots/shot_detection.py,file_utils/mesh_convert/_mesh_convert.py).STANDARD_TRANSFORM_ATTRSis now derived from it. uitk's sequencer no longer carries Maya's channel names; hosts pass these in. The pinned FBX2glTF version was reachable only through a private module path. That path keeps re-exporting it (andFBX2GLTF_PLATFORMS) for published extapps 0.2.2, which imports them there. -
2026-09-26 -- Saving over a file a sync client or virus scanner has just opened no longer fails (
file_utils/_file_utils.py). Windows refuses to rename onto or away from a file another process holds for a moment after it changes ([WinError 5] / [WinError 32]); on a synced drive that is routinely the second quick rewrite of the same small file -- captured live asAccess is denied: '.x.tmp' -> '.Unity.preset'. NewFileUtils.replace_file(src, dst)isos.replacethat retries those two codes for about a second (pip's budget for the same cause) and raises anything else at once.atomic_write_text,atomic_write(so every preset, sidecar and bundle write) and pythontk's other promotions (app_handoff,fbx_media,temp_artifacts, the preview server and playblast) go through it. -
2026-09-26 -- A workspace template's
_-prefixed keys are annotations, never file rules (file_utils/workspace.py).WorkspaceTemplates.rulesdropped only uitk's_metablock, so a hand-shared template's_commentbecame a rule written intoworkspace.mel. NewWorkspaceTemplates.rules_from(data)is the one filter (every_key dropped, the SchemaSpec convention);rulesand blendertk's Workspace Editor both use it. -
2026-09-26 -- Presets can be locked, collected, backed up and shared:
PresetLibrary,PresetStoremetadata sidecars andPresetReadOnlyError(core_utils/presets/library.py,core_utils/presets/store.py). Each user preset now gets a sidecar.<name>.presetbeside it holding an id (it survives renames), the name as typed (file names lose punctuation), a lock, a collection tag, tags and the author; the payload file is never touched, because tools splat it into keyword arguments and older installs read the same folders.PresetStore.saverefuses a locked preset (PresetReadOnlyError, aPermissionError;force=Trueoverrides),delete/renamereturnFalsefor one,renamecarries the sidecar, and a preset created under a stale sidecar's name starts with a fresh one;unique_name(base)gives the first free"<base> N". A store announces itself with a.domainmarker (payload extension plus a host-independent built-in location) the first time it lists an existing folder.PresetLibraryworks over every store under the root without importing a tool: scan, lock, tag, duplicate, named collections (at most one per preset; creating or renaming one refuses a name already in use, and ids carry a random suffix so installing someone else's same-named collection never reads as an update of yours -- the review warns and installs it beside), and one bundle format (a.zip) for backups and shared collections.plan_importclassifies each preset as new / identical / update / conflict / removed beforeapplywrites anything, andapplyvalidates every action before writing and backs up first (the newest 10 automatic backups are kept); where the destination folder has no.domainmarker yet (a fresh root, a tool never run there) a payload keeps its bundle member's extension, so a YAML store restores as YAML rather than as a*.jsonits own store never lists (test_a_restore_into_a_fresh_root_keeps_a_non_json_payload_format). Bundles are treated as untrusted: member paths that could leave the root, and collection ids that aren't slugs, are refused. -
2026-09-26 -- Linux is a supported platform: the suite runs on ubuntu-latest beside windows-latest (
.github/workflows/tests.yml). Measured in WSL Ubuntu 24.04 on a case-sensitive filesystem: every module imports, and the 48 Linux-only failures were 24 Windows-shaped tests (ungatedctypes.windllpatches,C:\literals,.exezip fixtures, a case-insensitive filesystem assumed) and 24 missing system packages -- none a product bug. The tests now assert each OS's own behavior (the Windows branches patchctypes.windllin and run everywhere; the resolver fixtures are host-shaped, so Linux covers Maya'smaya.bin->mayapy). CI's Linux leg installslibopengl0and runs underxvfb-run: a private X server, never a desktop. The legs are one matrix job,suite, behind thetestcheck branch protection requires, which is green only when every leg is: a matrix reports one check per leg (<job> (<os>)), never the bare job name, so the requiredtestwould never have attached and every release PR would have sat BLOCKED. PyPI listsOperating System :: Microsoft :: Windowsand:: POSIX :: Linuxinstead ofOS Independent(pyproject.toml), anddocs/README.mdcarries a Platform badge. -
2026-09-26 --
X11: a small, fully typed ctypes Xlib client -- window titles by PID, the focused window's PID, the pointer, compositor presence, key state (core_utils/x11.py). Works on X11 and XWayland (Maya is an xcb app under Wayland too); every query answers None where no X server exists (Windows, macOS, pure Wayland, SSH). Every function is typed before the handle is published and every call holds one lock: the old Linux Esc probe inExecutionMonitorcalledXKeysymToKeycodewith noargtypes, so ctypes passed theDisplay*as a 32-bit int and the host SEGFAULTED on its worker thread -- every uitk slot with a timeout (measured, exit -11) -- and a second polling thread could open the display untyped. Xlib's default error handler EXITS the process, so ours absorbs errors on this connection during each call (a window vanishing mid-query) and hands others' to the handler it replaced. A failed load or open is cached, not retried at the polling rate.ExecutionMonitor.is_escape_pressed,is_foreground_process(Esc in another app no longer cancels) and the spinner's cursor position now ask it. Tests:test_x11(any OS against a recording libX11; a real server under Xvfb, with a raw-Xlib window fixture -- Tk 9 sets no_NET_WM_PID). -
2026-09-26 --
AppLauncheron Linux: process identity, windows, lifetime and discovery (core_utils/app_launcher/).get_running_processesmatches the program's NAME from/proc(mayafindsmaya.bin) --pgrep -fmatched any command line containing it, so MayaConnection's "is this PID still a Maya?" guard matchedtail maya.log.get_window_titles/wait_for_readysee X11 windows of the PID and its descendants (a Linux launcher is often a script that forks the real binary); they returned nothing / "ready" at once.spawn(bind_lifetime=True)binds on POSIX too: the child leads its own process group and a/bin/shwatcher in its own session kills that group within a second of the host going away, crash included (notPR_SET_PDEATHSIG: it needs apreexec_fnand fires when the spawning THREAD ends).scan_install_dirstakes{exe}(.exeon Windows only) and~, skips{program_files}layouts off Windows and ranks versions naturally (Blender 4.10over4.9-- a string sort had it backwards on every OS);location_env_varssuffixes take{exe};find_appreads XDG.desktopentries, taking an entry's program only when the entry is that program's own: no launcher (flatpak, snap,env, Steam, Lutris, Heroic,gtk-launch,xdg-open,sh/bash,python*,wine*,java) and nothing after it but options and field codes -- Steam'sName=Blenderentry (steam steam://rungameid/365670) had answered with Steam; a relative$XDG_DATA_HOME/$XDG_DATA_DIRSentry is ignored, per the spec (test_the_application_menu_never_answers_with_a_launcher,test_a_relative_xdg_data_dir_is_ignored).process_environreads libc's liveenvironon Linux;desktop_env()(new) is that minus a host's loader overrides (LD_LIBRARY_PATH,PYTHONHOME,QT_PLUGIN_PATH, ...) for desktop helpers.append_to_path(user_scope=True)persists one marked line in~/.profile(was a TODO),is_path_persistedfinds it, and the process PATH compares withnormcase.is_interactive_sessionis False on Linux without a display server. -
2026-09-26 --
NetUtils.listening_ports():[(port, pid)]for every listening TCP socket --netstaton Windows (read in the console's OEM codepage, errors replaced: under the ANSI codec a localized header -- French "État", cp850 0x90 -- raised UnicodeDecodeError;test_listening_ports_reads_a_localized_netstat),/proc/net/tcp{,6}plus socket inodes on Linux (net_utils/_net_utils.py). The owner check a launcher needs before trusting a port (MayaConnection parsed Windowsnetstatitself, which matched nothing on Linux).is_port_bindablebinds as the server will on POSIX (SO_REUSEADDR), so a quick restart no longer reads the port as taken and moves the preview server off its URL (red first on Linux). -
2026-09-26 -- Linux fixes across the file, install and config utilities.
AppInstaller.ensurepicks a download by CPU as well as OS: alinux-arm64key wins, and a Linux entry declared for anotherarchis refused rather than installed to fail "Exec format error" (ffmpeg and cloudflared gain arm64 builds; FBX2glTF has none); zip extraction keeps members' Unix permission bits, masked to0o755as the tar path'sfilter="data"masks (never setuid, never group/other-writable;test_extract_zip_never_grants_group_or_other_write).UvUnwrapno longer asks consent to download BFF where no build exists.FileUtils.reveal_in_file_managerresolves the path absolute first (a relative one raised ValueError building the file URI; a bare file name, dirname"", raised FileNotFoundError on every OS;test_reveal_in_file_manager_resolves_a_relative_path) and selects the file on Linux through freedesktopFileManager1(D-Bus), else opens the folder; it andopen_explorerstartxdg-openunderdesktop_env.remap_file_pathstests "under base" withis_under(a separator boundary:/proj2is not in/proj, on every OS).Metadata.seton POSIX: a mount that refuses xattrs (NFS, exFAT, FUSE) sends the value to the sidecar when enabled and raises otherwise -- it was printed and lost -- andgetoverlays the sidecar. The sidecar keeps only what the mount refused: a clear (None, refused too) or a value the xattrs take drops its parked copy, which had kept winning the read (test_a_cleared_key_leaves_the_sidecar_too,test_a_value_the_mount_takes_retires_the_parked_copy).UserConfig.expandreads${VAR},$VARand%VAR%on every OS -- names with parentheses (%ProgramFiles(x86)%) and the%%escape as Windows'os.path.expandvarsread them -- and gives${TEMP}a value where the OS sets none (a Windows-written profile kept%VAR%literal on Linux); a quote is plain text (Windows'expandvarsexpanded nothing after an apostrophe;test_expand_reads_windows_names_and_the_percent_escape).MeshOps.available()probes that pymeshlab can WRITE a mesh (its IO plug-ins needlibOpenGL.so.0; without it every op failed whileavailable()said True).StrUtils.name_pattern_contextno longer raises wheregetpass.getuser()has no account to name. -
2026-09-26 --
ImgUtils.get_image_sizesizes OpenEXR and Radiance HDR from the header;ImgUtils.is_equirectangular/is_environment_maptell an environment map from a lightmap (img_utils/_img_utils.py). Pillow reads neither HDR format, so an EXR or.hdrhad no size at all; the header parse walks EXR attributes todataWindow(seeking past large ones) and reads a Radiance resolution line by axis, through the same_radiance_resolutionwalkvalidate_image_integrityuses -- so a header the integrity check refuses (a CRLF one) never sizes.is_equirectangularanswers 2:1 (5% slack, for stitched panoramas like Maya's 4096x2004skyDome.hdr) cached per file version, and returnsNoneWITHOUT reading an online-only cloud placeholder -- a header read makes the sync client download the whole image.is_environment_mapadds the naming convention'slightmapaffix; it is what the Maya and Blender HDR managers now list through. -
2026-09-26 --
NamingConvention.matches/AffixRule.matches,NamingConvention.as_dict,NamingConvention.preset_store(core_utils/naming_convention.py).matchesreads back exactly whatapplywrites (a delimited affix brings its own word boundary; a bareLMmatchesroomLM), through a trailing index and dotted sub-names (room_Lightmap_12,desk_Lightmap.LIGHT_A_areaLight), case-insensitive by default (production files still carry a pre-convention_LightMap).as_dictis the whole table inupdate's shape;preset_storekeeps named snapshots of it under<user_config_root>/pythontk/naming_convention/, one store for every host. -
2026-09-26 --
NamingConvention.updateno longer writes a stale table over another host's edit, nor a preset's_metainto the doc (core_utils/naming_convention.py). It merged into the process's CACHED table and wrote the whole of it back, so with Maya and Blender both open, one host's nextseterased what the other had changed since. It now re-reads the doc first, and skips_-prefixed keys -- apreset_storefile carries_metabeside its entries, andupdate(store.load(name))stored it as a convention key. -
2026-09-26 --
TestSandbox.user_config: a test's view of the naming convention and preset stores is the shipped one, and its writes stay off the developer's doc (core_utils/test_sandbox.py). A context manager pointing$UITK_PRESETS_ROOTat a scoped temp dir, hiding$PYTHONTK_NAMING_CONVENTION, reloading the convention both ways; opt-in per test, since a suite asserting the default root must still see it. Replaces three hand-rolled copies (pythontk's and mayatk's convention tests, mayatk's naming-panel preset test).test_img'sImgTestno longer regenerates the TRACKEDtest/test_files/imgtk_test/on every run -- its fixtures are generated into aTempArtifactsdir.