td-python-extension · diff
git:20260613.00a53b8 to git:20260616.6317975
77 added, 32 removed. Audit A to A.
---
name: td-python-extension
- description: Python extensions — extension classes, ext0object, parameter callbacks, lifecycle, state/persistence, scriptTOP/numpy. Use when building Python-driven baseCOMPs.
+ description: Python extensions — ext0object, extension classes, parameter callbacks, scriptTOP/numpy, lifecycle. Use when building Python-driven baseCOMPs.
---
# Python Extensions
Patterns for Python extension components in TouchDesigner. Complements `td-comp-architecture` (structure/custom pars) and `td-dat-family` (callback mechanics). See `reference.md` for API signatures and `examples.md` for complete patterns.
- ## Extension Wiring
+ ## Create an extension — one pass
- Every extension needs three pieces on the baseCOMP:
+ The fixed choreography. Author the class (step 3) and verify (step 7); the rest is invariant.
+ Mirrors what TD's own Component Editor produces.
- 1. **textDAT** inside the COMP — extension class, `language=python`
- 2. **ext0object** — constant-mode string: `me.mod("ExtDAT").ClassName(me)`
- 3. **ext0promote** = `True` — exposes Capitalized methods externally
+ 1. **`baseCOMP base_<x>`** — name it now; binding and docked refs snapshot at creation.
+ 2. **`textDAT <Name>`** inside — DAT = module = class, one identifier (e.g. `SimpleExt`).
+ 3. **`set_dat_content`** the Skeleton (below) → `code/py/<comp-path>/<Name>.py`.
+ 4. **`language=python`** — set it explicitly; `set_dat_content` doesn't (see Sync to File).
+ 5. **`ext0object = op('./<Name>').module.<Name>(me)` + `ext0promote=True`** — leave `ext0name`
+ empty (promoted under the class name). `ext0` always exists → no sequence management.
+ 6. **pulse `reinitextensions`.**
+ 7. **Gate — not done until proven:** `get_errors` clean AND a promoted member called from outside
+ returns a value (`op('base_<x>').Hello()`). Print it.
- After editing extension code: `pulse_parameter` with `reinitextensions`, then check errors.
+ Inspect an existing one: `get_operator_info(path, include_extensions=true)` — class names, promoted
+ signatures, clone sources.
- To inspect an existing extension COMP: `get_operator_info(path, include_extensions=true)` — returns class names, promoted method signatures, and clone sources.
+ ## Naming & promotion
- ## Naming Conventions
+ - DAT == module == class — one identifier; `Ext`/`EXT` suffix optional. `me.mod("<Name>").<Name>(me)`
+ is an equivalent shorthand for the step-5 binding.
+ - **Capitalized** members = promoted (external via `comp.Member`); **lowercase/underscore** = internal.
+ - Promotion exposes Capitalized **methods**; use `@property` (or `TDF.createProperty`) for promoted data.
+ - Store `self.ownerComp` in `__init__`.
- - **Extension DAT**: `ComponentNameEXT` (e.g. `PongEXT`, `FluidEXT`)
- - **Class inside**: `ComponentName` (e.g. `Pong`, `Fluid`)
- - **ext0object**: `me.mod("PongEXT").Pong(me)`
- - **Capitalized** methods/properties = promoted, externally accessible via `comp.MethodName()`
- - **lowercase/underscore** = internal only
- - `ext0promote` only exposes **methods**, not attributes — use `@property` for data access
- - `self.ownerComp` — always store the parent baseCOMP reference
+ ## Skeleton
+ Bare MCP `create(textDAT)` gives you an empty DAT — author this skeleton (it mirrors TD's
+ Component Editor template). Going through the Component Editor harvests it for you.
+
+ ```python
+ from TDStoreTools import StorageManager
+ import TDFunctions as TDF
+
+ class CounterExt:
+ """CounterExt description"""
+ def __init__(self, ownerComp):
+ self.ownerComp = ownerComp
+
+ # Dependable property — cooks dependents when it changes
+ TDF.createProperty(self, 'MyProperty', value=0, dependable=True, readOnly=False)
+
+ self.a = 0 # internal attribute
+ self.B = 1 # promoted attribute (Capitalized)
+
+ # Persistent across saves AND re-initialization:
+ storedItems = [
+ {'name': 'StoredProperty', 'default': None, 'readOnly': False,
+ 'property': True, 'dependable': True},
+ ]
+ # self.stored = StorageManager(self, ownerComp, storedItems)
+
+ def myFunction(self, v): # internal
+ debug(v)
+
+ def PromotedFunction(self, v): # external (Capitalized)
+ debug(v)
+
+ # def onInitTD(self): # after all extensions attached — cross-ext deps
+ # debug("onInitTD called")
+ # def onDestroyTD(self): # cleanup; use instead of __del__
+ # debug("onDestroyTD called")
+ ```
+
+ ## Sync to File
+
+ The editor (and `create`) make the extension DAT **embedded**, not on disk. Getting it into the
+ code-on-disk workflow is a **follow-on step** — one `set_dat_content` call writes the file and sets
+ `file`/`syncfile` (it does NOT set `language` — set that yourself, step 4):
+
+ - Path: `code/py/<comp>/<subcomp>/<Name>.py` — see `td-dat-family` for the canonical flow
+ - After syncing, **edit on disk**; the DAT auto-re-inits on external edit (no `reinitextensions`).
+ - **Re-init wipes plain state.** It runs `__init__`, so `self.a`-style attributes reset every edit.
+ State that must survive re-init (and saves) belongs in `StorageManager` stored items (the
+ `self.stored` block above), not plain attributes.
+
## Parameter Callback Routing
Extension reacts to custom parameter changes via `parameterexecuteDAT`:
- `op` = `..`, `pars` = `*`, `builtin` = False
- Generic routing loops through `par.owner.extensions` calling `onParValueChange` / `onParPulse`
- Without this DAT, the extension is deaf to parameter changes
## Lifecycle
- `__init__(ownerComp)` — creation/reinit, store refs, declare slots as None
- `onInitTD()` — after all extensions attached, safe to access other exts
- `onDestroyTD()` — cleanup (use instead of `__del__`)
Use `onInitTD` when depending on other components' extensions being ready.
- ## State & Persistence
-
- - **State lives on `self`** — never on module globals. Module globals re-execute on reinit and lose state
- - **`TDF.createProperty`** (from `import TDFunctions as TDF`) for reactive/dependable state — list/panel callbacks reading `ext.PropertyName` auto-recook on change. Plain `self.x = ...` does NOT trigger downstream recooks. e.g. `TDF.createProperty(self, 'MyProperty', value=0, dependable=True, readOnly=False)`
- - **`StorageManager`** (from `from TDStoreTools import StorageManager`) for persistence across saves:
-
- ```python
- storedItems = [
- {'name': 'History', 'default': [], 'readOnly': False,
- 'property': True, 'dependable': True},
- ]
- self.stored = StorageManager(self, ownerComp, storedItems)
- ```
-
## scriptTOP + numpy
`copyNumpyArray()` **must** run inside `onCook` — calling it elsewhere raises `tdError`.
**Stage-and-cook**: extension stages `self._pixels`, calls `script_top.cook(force=True)`, scriptTOP `onCook` calls back into extension which runs `copyNumpyArray`.
## Safe Operator Resolution
Custom parameters referencing operators (TOP, CHOP, etc.) may be empty or invalid. Always resolve safely — check for None and validate before using.
## Internal Layout
Extension DAT and parameterexecuteDAT at X=-200 (left of origin). Main operator chain flows right from X=0. Output null at chain end.
## DAT Module Imports
- **Same COMP**: `mod.datName` or `from datName import X` (module-level only)
- **Cross COMP**: `op('comp').mod.datName` — the only reliable pattern
- - **External .py**: TD globals (`op`, `project`) are NOT available — only injected into DAT namespaces
+ - **Syncing a DAT to a file does NOT strip TD globals** — a file-synced DAT is still a DAT, so
+ `op`, `me`, `project`, `debug`, etc. are available (extensions sync to disk fine). The "no globals"
+ caveat applies only to a plain `.py` imported as a module *outside* any DAT namespace.
## Pitfalls
- **ext0object as expression** — must be constant-mode string, not expression
- **ext0promote only promotes methods** — not attributes; use `@property` for data
- **copyNumpyArray outside onCook** — stage data on extension, call from onCook only
- **Language not set on extension DAT** — always `language=python`
- **No parameterexecuteDAT** — extension won't react to custom parameter changes
- **Module-level heavy imports** — import torch/numpy in a setup method, not at module top
- **Name collision on create** — TD silently appends numbers; verify `.name` matches requested
- **HTTP/urllib in main thread** — blocks cook loop; use webClientDAT or `run()` with `delayFrames`
- - **Forgetting reinitextensions** — edited code doesn't reload until pulsed
+ - **Forgetting reinitextensions (in-DAT edits only)** — code edited *inside* the DAT doesn't reload
+ until pulsed. A **file-synced** DAT auto-re-inits on external disk edit. Either way, re-init wipes
+ plain attributes — persist anything that must survive via `StorageManager` (see Sync to File)
- **`findChildren(type=textDAT)` without `maxDepth=1`** — crawls into annotateCOMPs and other utility COMPs, triggering compilation of their internal DATs as Python. Always use `maxDepth=1` when loading sibling modules
- **Assuming op reference pars are valid** — always check for None before using