Changelog¶
[Unreleased]¶
[0.4.0] - 2026-07-27¶
Two things dominate this release.
js2max: a JavaScript counterpart to py2max that runs inside an open Max
patcher through the v8 object, so a patch can build objects into itself and
serialize itself back out. Its runtime ships in the wheel, so
p.add_v8_bridge() and p.save() are all it takes. Confirmed end to end
against Max, including a file js2max wrote being opened by Max.
Typed box properties: the Max property vocabulary reaches the emitted patch
through **kwds no longer. BoxProps / TextboxProps mean a misspelled
bgcolour or a wrongly-typed fontsize is rejected by mypy with a suggestion,
where both previously shipped straight into the file.
The rest is nine fixes, several of them long-standing and silent -- comments
written with their port counts backwards since the beginning, to_dict()
returning an empty patcher depending on call order, and nulls reaching the patch
one level down.
New: js2max -- a JavaScript counterpart that builds patches inside Max¶
-
js2max/is a second front end to the.maxpatformat, in TypeScript, doing the one thing this package cannot: Max embeds a JavaScript engine, andv8ships in current Max, so a script runs inside an open patcher and builds into it directly. py2max writes files Max later opens; js2max builds into the patch that is already open, from the same description. It serializes the other way too, and a file it wrote has been opened in Max. -
Nothing changes for Python users. js2max adds no dependency -- its bundles are inert data, and nothing in py2max imports or executes them; the package keeps its zero runtime dependencies. It is a sibling directory with its own toolchain (Bun) and its own CHANGELOG, where the detail lives. The two loadable bundles are committed, so a Max user needs no toolchain either.
-
The two packages share one description of the format, not two.
js2max/src/objects.tsis generated from py2max's maxref bundle byscripts/gen_js2max_objects.py, so the box class, port counts and outlet types of all 1098 known object classes come from one source; the Max version a written file declares is exported the same way, rather than restated. The verification patches underjs2max/max/are generated by py2max itself (scripts/gen_v8_harness.py) -- the Python package emits a patch that loads the JavaScript bundle that builds objects from the format the Python package writes. -
make js2maxbuilds it,make js2max-checktypechecks it, runs its 236 tests, and fails if any generated file or committed bundle has drifted from its source. CI runs the latter. -
One py2max bug came back the other way:
add_commenthad its port counts backwards, found by serializing a py2max patch out of Max and diffing it against the original. See theadd_commententry in this release.
New: the js2max runtime ships in the wheel; add_v8_bridge() writes it beside a patch¶
-
The built JavaScript now lives at
py2max/data/js2max/and ships as package data, sopip install py2maxis enough to use js2max. Until now the bundles existed only in the source repository, which meant the feature was unreachable for everyone who installs from PyPI. -
p.add_v8_bridge()adds a[v8 js2max.v8.js]box and marks the patcher;save()then writes the runtime next to the patch, because Max resolves a bare filename through the folder holding it. A patcher that never asked for the bridge writes only itself, so an ordinarysave()cannot leave a stray.jsfile behind, andadd_v8_bridge(bundle="my.js")installs nothing -- naming your own file means you placed it.py2max.js2max_runtime.path()/install()are the direct API. -
Shipping them together is a correctness guarantee, not a convenience.
js2max/src/objects.ts-- box classes, port counts and outlet types for 1098 object classes -- is generated from py2max's maxref bundle. A runtime built against one version of py2max and paired with another declares wrong port counts for whatever changed, and a box declaring a port it does not have silently loses the cord attached to it when Max opens the file. One artifact makes that skew impossible, andtests/test_js2max_runtime.pyasserts the two agree rather than leaving it as an intention. -
No new dependency: the bundles are inert data, like the maxref bundle, and nothing in py2max imports or executes them. They add 160 KB to a wheel whose maxref data is already 1.0 MB. The build writes both copies (
js2max/max/for Max,py2max/data/js2max/for the wheel) andmake js2max-checkfails if they differ, so the shipped runtime cannot lag the repository one. -
The single-file edition (
scripts/py2max.py) cannot carry 160 KB of bundled JavaScript, sojs2max_runtimeis stubbed there aslayout="graph:*"already is:add_v8_bridge()still builds the box, and saving raisesNotImplementedErrornaming the full package -- unless you passbundle=for a runtime you placed yourself, which works in both editions. -
Documented in the js2max guide.
New: box properties are typed -- a misspelled property is now an error, not a silent key¶
-
Max box properties reached the emitted
.maxpatthrough**kwds: Any, sop.add_textbox("cycle~ 440", bgcolour=[0,0,0,1])wrotebgcolourinto the patch andfontsize="twelve"wrote a string where Max wants a number. Neither was caught by anything:mypy --strictpasses onAny, andvalidate_attrs=Truewarns about unknown property names at runtime but says nothing about types and emits the key regardless. -
py2max/core/props.py(generated) definesBoxProps/TextboxProps, andBox.__init__,add_textbox,add_messageandadd_commentnow accept**kwds: Unpack[BoxProps]. Under themypy --strictthis project already runs, a misspelled property is rejected with a suggestion (did you mean "bgcolor", "bgcolor2", or "hbgcolor"?), as are a wrongly-typed value and an explicitNonefor an optional property. -
Zero runtime cost and no new dependency:
Unpackis imported underif TYPE_CHECKINGwith postponed annotations, so nothing is evaluated at runtime and the library keeps shipping zero runtime dependencies (verified by running the single-file build on a Python with notyping_extensionsinstalled).BoxPropsandTextboxPropsare exported from the top level for callers annotating their own helpers. -
Two TypedDicts rather than one, deliberately. A TypedDict key that collides with a named parameter makes mypy report
Overlap between argument names and ** TypedDict itemsand then stop checking calls to that function altogether -- a silent loss of coverage, which is the worst possible failure for a change whose entire purpose is coverage.BoxPropstherefore omitsBox.__init__'s five structural parameters andTextboxPropsadditionally omitstext/outlettype/comment/comment_pos/justify; the narrower flows into the wider when kwds are forwarded.tests/test_box_props.py::test_the_overlap_trap_is_absentguards against a regression. -
The 850-property vocabulary is generated from four sources by
scripts/gen_box_props.py(make box-props), but they are nowhere near equal partners. maxref attributes whosesavemeta-attribute is 1 -- exactly those Max persists into a file -- supply 830 names, 785 of them found nowhere else. A hand-written table adds 20 more and, more usefully, retypes 7 that maxref declares too widely: the generator unions an attribute's type across every object declaring it, which is correct where the key genuinely differs but costs real checking on the handful users actually pass (rangearrives asUnion[Sequence[float], Sequence[int], float, int]and is pinned toSequence[Atom]). Three keys are dropped for not being valid Python identifiers (one/column,one/matrix,one/row). -
The other two sources contribute one name each -- and that is why they exist. An AST scan of every keyword py2max itself passes to
Box(...)addsviewvisibility: maxref documentsbpatcherwith 12 attributes and omits this one, which the library writes and Max accepts, so a maxref-only vocabulary would have rejected py2max's own output. A sweep of the repository's.maxpatfixtures addscomment. Both scans are cheap, and they are the only defence against maxref being incomplete, which it demonstrably is. -
The result is typed rather than nominally typed: of the 850 properties only five are bare
Any(comment,data,outlettype,patcher,viewvisibility); the rest resolve toint(481),Sequence[float](154),str(92),float(68) and small unions. -
Known limitation: the vocabulary is a flat union across all 1175 objects, and the median maxref attribute is declared by exactly one of them (the most widely shared,
bgcolor, by 76). SoBoxPropscannot tell that a property is invalid for the maxclass it was passed to: 547 of the 850 belong to a single object, andp.add_textbox("cycle~ 440", activedialcolor=[1.0, 0.0, 0.0, 1.0])type-checks cleanly and writesactivedialcolorinto the patch even though onlylive.dialdeclares it. Misspelled and wrongly-typed properties are rejected; real properties on the wrong object are not. Recorded inTODO.md. -
add_floatparam/add_intparamnow passminimum/maximumthroughkwds_filterinstead of forwardingNone, so an unset bound is absent from the patch rather than present as null. -
tests/test_box_props.py(13 tests) runs mypy in subprocesses to assert each failure mode is rejected and that correct usage still checks -- a static guarantee is not observable at runtime, so an ordinary assertion cannot see it. A staleness guard fails if the generated file drifts from its sources. -
Those subprocesses pass
--no-color-output. mypy honours aFORCE_COLORinherited from the developer's shell, and the assertions match on message text, so without it the tests failed for anyone who has it set -- andtest_the_overlap_trap_is_absent, which asserts a fragment is absent, would instead have passed for the wrong reason.
New: scripts/py2max.py is now generated, not hand-maintained¶
-
scripts/py2max.py-- the single-file edition -- is now produced byscripts/build_single_file.py(make single-file) instead of being maintained by hand. It amalgamates the core object model, the grid/flow/columnar/matrix layout managers,lint(), connection and attribute validation,.amxdread/write and SVG export into one module, with an offline maxref table embedded (port types, method names and attribute names for all 1175 objects, compressed to ~30 KB; the ~8 MB of documentation prose is dropped, soBox.help()returns a pointer to the full package whileget_info()still returns structured data). Graph layouts (layout="graph:*"), the CLI and the SQLite maxref database are excluded;layout="graph:*"raisesNotImplementedErrornaming the package to install. -
Why: the hand-maintained version had drifted five releases behind and carried four real defects, including one that broke every edit-after-load (
widthreadself.rect.w, but a loaded patch keepsrectas a plain JSON list) and the operator-precedence bug inadd_coll/add_dict/add_tablethat discarded a caller-suppliedtext. Nothing in the repo imported it, so no test caught any of it. Generating the file makes drift structurally impossible: the single file now is the package's own code. -
tests/test_single_file.pyasserts equivalence rather than mere importability: 13 patch builders (layouts, containers, subpatchers, semantic ids, editing, theming) are built with both implementations and their emitted JSON must match exactly, plus per-object agreement on port counts, validation verdicts and messages,MAXCLASS_DEFAULTS, object coverage, lint findings and SVG output. A staleness guard runs the generator with--checkand fails if the committed file differs from a fresh build, so a stale copy can no longer rot unnoticed. -
The generator refuses to emit a broken file: it fails on top-level name collisions between amalgamated modules, and on any undefined name left behind when included code calls into an excluded module (which is how the missing SVG exporter was caught). Builds are byte-reproducible (the gzip header's timestamp is zeroed), so the staleness check is meaningful.
Fixed: add_comment had its port counts backwards¶
-
A comment box takes a
setmessage and emits nothing -- 1 inlet, 0 outlets.add_commentpassed neither toBox, so the constructor's defaults applied and produced 0 inlets and 1 outlet on every comment py2max has ever written. -
Found by round-tripping a py2max patch through Max: js2max serialized a patcher back to a
.maxpat, and diffing it against the py2max original showed Max had rewritten the values on save. Max is the authority, and it disagreed. -
Related, in the generated js2max object table:
Box.__init__defaultsnumoutletsto 1, and for the 73 objects maxref does not state it that default is simply wrong --printcame out with an outlet it does not have. The table now reads maxref directly and omits a count maxref is silent about, since Max derives ports from the instantiated object anyway.
Fixed: to_dict() returned an empty patcher until something else rendered¶
-
p.to_dict()["patcher"]["boxes"]was empty on a patcher full of boxes, and stayed empty untilto_json()orsave()happened to callrender()-- after which the same call on the same object started returning them. Order-dependent, and silent: a test asserting overto_dict()examined an empty patcher and passed for the wrong reason, which is how it was found. -
to_dict()now renders on its own behalf. Renaming it was the alternative, and would have preserved the trap under a new name across 104 call sites; rendering also removes the asymmetry withBox.to_dict(), which has always returned a populated box with no preparation required.to_json()no longer renders separately, sinceto_dict()does it. -
render()is now idempotent, which the above requires --save_as()renders for its own log line andto_dict()renders again underneath it.self.boxeswas appended to whileself.lineswas rebuilt, so a second render duplicated every box and no line. That path is reachable only throughreset_on_render=False, which nothing in the repository passes, so the asymmetry had never bitten; it made rendering order-dependent in exactly the wayto_dict()was. -
tests/test_serialization.py(11 tests) pins both properties: thatto_dict()is self-sufficient and repeatable, and that rendering, saving or serializing more than once cannot duplicate a box -- including for subpatchers, whose contents render one level down.
Fixed: box port counts -- explicit zeros, and subpatchers that track their contents¶
-
Box.__init__usednumoutlets or 1/numinlets or 0, so an explicitnumoutlets=0was silently promoted to 1: an object deliberately created with no outlets still claimed one, and could therefore be used as a connection source. Both now usex if x is not None else default, so a meaningful 0 survives. (Thenuminletsline was not actually defective -- its default is already 0 -- but is spelled the same way for clarity.) -
A subpatcher box now declares as many ports as it really has.
inlet/outletobjects are normally added to a nested patcher after the subpatcher box exists, so the counts fixed at construction went stale, and Max renders a box's declared count -- ap subholding threeoutletobjects but declaring one emitted a patch whose other two outlets could not be connected.Box.render()now syncs the counts (andoutlettype) from the nested patcher'sinlet/outletobjects, reusing the same derivationlint()already used to report the discrepancy. -
The sync only overrides a dimension it actually counted objects for, because a nested patcher can hold I/O that this cannot interpret:
gen~andrnbo~declare theirs within/outobjects, and an empty subpatcher is a stub the caller has yet to fill. Zeroing those boxes' ports would be worse than keeping the constructed default, so they are left alone. -
add_subpatchernow states its port defaults (1 inlet, 1 outlet) explicitly instead of passing a falsy0and relying onBox.__init__to promote it -- which is what the previousnumoutlets or 0did in practice. This keepsgen~/rnbo~boxes, which are created through this path, exactly as before. -
Regression tests in
tests/test_subpatch.pycover all four cases: explicit zeros, port tracking, empty-subpatcher defaults, andgen~/rnbo~ports surviving.
Fixed: nulls no longer reach the patch -- _remove_none_entries recurses¶
-
Box._remove_none_entriesdropped None-valued keys only at the top level, so anything one level down survived.add_intparamwritesparameter_mmaxunconditionally, which meant an unset maximum shipped as"parameter_mmax": nullinsidesaved_attribute_attributes-- and Max distinguishes an absent key from a null one. (add_floatparamomits the key entirely; the asymmetry was the tell, and the method's ownTODO: make recursivewas the same symptom.) Fixed by making the scrub recursive rather than special-casing the key, which closes the class instead of the instance. -
Lists are walked but not filtered: a None element is positional -- an
outlettypeslot, say -- so dropping it would change the arity. Tuples are left alone soRect, a NamedTuple, survives as itself. Loading is unaffected, sinceBox.from_dictbypasses__init__entirely. -
tests/test_param.pynow asserts that a representative patch contains no nulls at any depth. It reads backto_json()rather thanto_dict(), because only the former renders the boxes -- asserting overto_dict()inspects an empty patcher and passes for the wrong reason. That trap is recorded inTODO.md.
Fixed: add() raised TypeError when a keyword named the target's own parameter¶
-
Patcher.add()derives the first argument of its target method from the value it was handed (the text tail, the number) and then forwards**kwdsto the same call, so a caller naming that parameter passed it twice:p.add("cycle~ 440", text="saw~ 220"),p.add(5, initial=3)andp.add("coll x", name="y")all failed withgot multiple values for argument. An explicit keyword now wins over the derived value. -
This was a family, not a list of cases. The
_maxclass_methodsbranch fills every specialized method's first parameter positionally, soadd_coll(name=),add_dict(name=),add_table(name=),add_itable(name=),add_umenu(prefix=),add_bpatcher(name=),add_message(text=)andadd_comment(text=)collided as well, alongsideadd_textbox(text=),add_subpatcher(text=),add_gen_codebox(code=),add_rnbo(text=)andadd_floatparam/add_intparam'sinitial=. A single_dispatchhelper now applies one rule at every branch, and finds the parameter name by introspection rather than from a table, so adding an entry toPatcher._maxclass_methodscannot silently reintroduce the collision. The test drives off that table for the same reason. -
.add(<number>, name=...)no longer leaks a straynameproperty into the patch. The keyword names the parameter (it becomesparameter_longname), but it was read without being removed from**kwds, so it was also emitted as a box property. The positional form,.add(1.5, "freq"), is unchanged and still wins over aname=keyword. -
Fixed alongside:
add_umenu()crashed unlessitemswas given --TypeError: object of type 'NoneType' has no len()-- despite the parameter being optional, which also meantp.add("umenu")had never worked. Thecast(List[str], items)masking theOptionalfrom mypy was the tell.
Fixed: MaxRefDB.search() treated % and _ as wildcards¶
-
The query was interpolated into a
LIKEpattern unescaped, so SQL metacharacters in a search term were executed rather than matched.search("%")returned the entire database (1175 objects),search("_")likewise, andsearch("gain_")returned 16 unrelated objects because_matches any single character. Since Max object names routinely contain_(jit_kernel), this was reachable in ordinary use. The term is now escaped and the clause declaresESCAPE '\'. -
search()with no recognized field now raisesValueErrorinstead of buildingWHERE ORDER BYand failing inside sqlite withOperationalError: near "ORDER": syntax error. The recognized set is exposed asMaxRefDB.SEARCHABLE_FIELDS.
Fixed: matrix layout fragmented a signal chain when boxes were added in reverse order¶
-
MatrixLayoutManagertreated any object with at most one input as the start of a signal chain, which makes every mid-chain object a chain start. A chain stops as soon as it reaches an object another chain already claimed, so whichever mid-chain object came first in iteration order consumed the tail and stranded the real source:cycle~ -> gain~ -> ezdac~was detected as three chains -- and therefore laid out as three matrix columns -- purely because the boxes were added in reverse signal order. A chain start is now an object with no inputs at all. -
The recorded symptom for this was "fix cycle handling", but cycles were never the defect: pure cycles, cycles with an external feeder, and self-loops all traced correctly before and after. Regression tests in
tests/test_layout_matrix.pycover creation order, parallel chains meeting at a shared sink, all three cycle shapes, and disconnected objects.
Fixed: importing py2max no longer logs, nor hijacks the host's logging¶
-
import py2maxis now silent. DEBUG-level logging was previously the shipped default (log.py:DEBUG = getenv("DEBUG", default=True)), so simply constructing aPatcherprinted internal diagnostics to the console. Logging is now opt-in. -
Breaking (bad default removed): the library no longer calls
logging.basicConfig(..., force=True)at import. That call replaced the host application's logging configuration -- handlers, format and level -- as a side effect of importing py2max. A program that configuredbasicConfig(format="APP: %(message)s")and then imported py2max silently lost its own format. Thepy2maxlogger now carries aNullHandlerand nothing else is touched, which is the standard way for a library to participate in logging without imposing any. -
New
py2max.setup_logging(level=..., color=..., log_file=...)opts in to py2max's colored console output. It attaches handlers to thepy2maxlogger only, never to root, and is idempotent (repeat calls replace their own handlers rather than stacking duplicates). It deliberately leavespropagatealone: disabling it would be a global side effect that silently blinds anything capturing py2max records through an ancestor logger, including pytest'scaplog. -
Env vars are namespaced and default off:
PY2MAX_DEBUG=1(was the bare, extremely commonDEBUG, which meant any unrelatedDEBUG=1in the environment turned py2max verbose), plusPY2MAX_LOG_LEVEL,PY2MAX_LOG_FILEandPY2MAX_COLOR. Setting any of the first three enables logging at import; an explicitPY2MAX_DEBUG=0stays silent. -
The CLI, being an application rather than a library, now configures logging explicitly and gained
-v/-vv(INFO/DEBUG) and-qflags. It previously got its output purely as a side effect of importing the package. -
config()is retained as a no-op alias ofget_logger()for backwards compatibility. -
tests/test_logging.pycovers all of it, using subprocesses for the import-time behaviour that cannot be re-tested in an already-imported module. Atests/conftest.pyfixture now snapshots and restores thepy2maxlogger around every test:setup_logging()mutates process-global state, and without isolation a CLI test's configuration madecaplogblind in a later lint test -- a failure that passed in isolation and only appeared in the full run.
Fixed: the maxref cache no longer prints to stderr¶
- Building the one-time object cache announced itself with four
print(..., file=sys.stderr)calls. A library does not get to decide whether that is visible or where it goes; it is now logged at INFO on thepy2maxlogger, sopy2max.setup_logging("INFO")shows it and the default stays silent. The CLI still prints -- it is an application, and its output is the product.
Notes for upgraders¶
No API was removed and no call signature changed, but four things behave differently enough to mention.
- Every comment box changes shape.
add_commentwrote 0 inlets and 1 outlet; a comment has 1 and 0. Regenerating a patch that contains comments produces a different -- correct -- file, and Max was silently rewriting the values on save anyway. to_dict()renders, so it returns the patcher as it stands rather than as it stood after whatever last rendered it. It previously returned an empty patcher until something else calledrender(). If you were callingrender()first, you no longer need to; if you were relying on the empty result, you were relying on a bug.render()is idempotent.reset_on_render=Falseno longer accumulates boxes across renders. It also never accumulated lines, so what it did before was not coherent.save()may now write a second file -- but only for a patcher that calledadd_v8_bridge(), which is new in this release. Nothing that worked before writes anything extra.
Typed box properties are a static change: mypy --strict will now reject a
misspelled or wrongly-typed property that it previously accepted. That is the
point of them, and nothing changes at runtime.
[0.3.6]¶
Removed: incremental layout; optimize_layout() is batch-only again¶
-
Patcher.optimize_layout()no longer takes thechanged_objectsparameter added in 0.3.5 -- it takes no arguments and always performs a full, whole-patch layout. The incremental machinery in the layout managers was removed with it:LayoutManager.should_use_incremental,get_affected_objects,get_connected_objects,_incremental_layout,_find_non_overlapping_position, and theINCREMENTAL_THRESHOLDconstant (layout/base.py); theoptimize_layout(changed_objects)overrides inlayout/grid.pyandlayout/flow.py(they now implement_full_layoutand inherit the batch entry point, with flow's<2 objectsguard moved into_full_layout); andlayout/matrix.py's override (now_full_layout, withColumnarLayoutManagerinheriting it). -
Rationale (scope split): py2max owns batch layout -- arranging a whole patch once, typically at the end of programmatic creation. Interactive, per-edit ("live") relayout belongs to the editor that owns the editing session (
py2max-server), which handles it client-side. The incremental path existed only to serve that live case and was never exercised by a batch caller (batchoptimize_layout()always passedchanged_objects=None), so it was dead weight in the library. This reverses the 0.3.5 change, which had added the parameter as a prerequisite for a server-side auto-layout approach that was subsequently dropped. -
Breaking: calling
optimize_layout()with an argument (e.g.optimize_layout({obj.id})) now raisesTypeError; drop the argument. Thetest_optimize_layout_forwards_changed_objects/..._incremental_leaves_untouched_objects_fixedregression tests were replaced bytest_optimize_layout_is_batch_only.
[0.3.5]¶
Fixed: Patcher.optimize_layout() now reaches the incremental layout path¶
Patcher.optimize_layout()gained an optionalchanged_objects: Optional[Set[str]]parameter and forwards it to the layout manager. It previously called the manager with no arguments, silently discarding any change set, so the incremental layout engine (layout/base.pyoptimize_layout(changed_objects)/should_use_incremental/_incremental_layout) was unreachable through the public API -- every call forced a full relayout. Passing a set of object IDs now lets managers that support it (grid, flow) reposition only those objects and their patchline neighbours;None(the default) preserves the previous full-relayout behaviour, so the change is backward compatible. Matrix/columnar managers still recompute in full (they ignore the argument by design). This is the core prerequisite for server-side auto-layout, which lives inpy2max-server.
[0.3.4]¶
New: Param docking in layouts¶
Patcher(..., param_placement=True)docks value/UI "param" objects (flonum,number,message,toggle,slider,dial,live.*) next to the single object they drive, instead of spreading them through the signal graph. Duringoptimize_layout(), a param whose outgoing connections all go to one non-param target is placed perpendicular to the signal flow -- above the target for a horizontal flow, to its left for a vertical flow (and to the right / below when the flow hugs that edge) -- ordered by, and for a lone param aligned to, the inlet it feeds, then de-overlapped. Works across every built-in layout (grid, flow, columnar, matrix). Off by default; a control that fans out to more than one object is left in the flow. Implemented asLayoutManager.place_params().
New: Patch linting and message-type-aware connection validation¶
-
Added
Patcher.lint()(and thepy2max.lintmodule:lint(),Finding) -- a patch-level health check returning structured findings with aseverity, acode, and object/connection references. It covers invalid connections, out-of-range outlet/inlet indices, orphaned patchlines, duplicate IDs, overlapping objects, off-canvas objects, and unknown object classes. -
Linting runs automatically on
save(): error-severity findings (bad connections, out-of-range ports, orphaned lines, duplicate IDs) are logged. PassPatcher(strict=True)to raiseInvalidPatchErroron any error instead. Layout warnings (overlaps / off-canvas / unknown objects) are left to an explicitlint()orpy2max validateto keep normal saves quiet. This is on by default and non-breaking -- saves still succeed unlessstrict=True. -
Connection validation is now message-type aware and bidirectional. The previous check only rejected a signal outlet wired into a non-signal inlet; it now also catches a control outlet (a bang from
metro/loadbang/button) wired into an oscillator's signal inlet -- e.g.metro -> cycle~, which Max rejects. The rules are deliberately conservative (ambiguous cases and maxref-unknown objects are allowed) so on-by-default checking never rejects a valid patch; notably a bang intoadsr~(a legitimate envelope trigger) is not flagged. -
Port typing is now modeled in
py2max/maxref/porttypes.py, normalizing maxref's placeholder control types (OUTLET_TYPE/INLET_TYPE) into message kinds, and resolving argument-dependent port counts that maxref reports as the arg-less default -- both value-scaled (limi~ 2-> 2 in/out) and arg-count-scaled (select a b c-> 4 outlets,route,pack/unpack,selector~/switch) -- plus curated overrides for the handful of objects maxref mis-types. -
py2max validate(CLI) now reports the full lint result -- errors and warnings with codes -- and exits non-zero on any error. -
A corpus test re-lints every shipped layout example patch and fails on any error, so Max-invalid wiring can no longer ship unnoticed.
-
Subpatchers are handled: a subpatcher/bpatcher box's inlet/outlet count is derived from the
inlet/outletobjects it contains (not the maxref default), andlint()recurses into nested patchers -- findings inside a subpatcher are reported path-qualified (e.g.sub-box-id/obj-1). -
Inlet acceptance is now derived from each object's
<methodlist>-- its real message vocabulary in Max's own docs -- rather than the placeholder inlettype(Cycling '74 shipsINLET_TYPE/OUTLET_TYPEfor control ports, so the type attribute alone is useless). This is what distinguishes a bang intocycle~(nobangmethod -> rejected) from a bang intoadsr~(has ananythingwildcard method -> allowed), replacing hand-curation with data that generalizes to all ~1050 objects that carry method lists. The shippedbundle.json.gzwas regenerated so no-Max users get the same data (atest_bundle_method_data_qualityguard prevents a future regeneration from dropping it).
[0.3.3]¶
Removed: serve and repl CLI subcommands¶
- The
py2max serveandpy2max replsubcommands -- thin stubs that only pointed at the separatepy2max-serverpackage since v0.3.0 -- have been removed. The interactive live editor and remote REPL still live inpy2max-server(pip install py2max-server).
Fixed: Graph layouts (graph:*) are overlap-free and open on-screen¶
-
GraphLayoutManagernow runs the dimension-aware overlap sweep (prevent_overlaps) after placement, so constraint/force engines no longer leave large UI objects (e.g.scope~at 130x130) overlapping their neighbours -- matching the guarantee the grid/flow managers already gave. -
The patcher window is grown to enclose the laid-out graph plus a margin, so
optimize_layout()output opens with the whole graph visible instead of spilling past the default 640x480 canvas (the window never shrinks below the default).
Fixed: Clustered grid layout squashed object sizes¶
- The connection-aware clustering path of
GridLayoutManager(cluster_connected=True) still wrote the manager's uniform 66x22 size back onto every clustered object, squashing UI objects (scope~,ezdac~,dial, ...) to text-box size. It now preserves each object's real width/height, matching the already-fixed non-clustered path.
Fixed: Example patches use valid Max connections¶
- The layout example scripts (
tests/examples/layout/) and matrix-layout tests wired control objects (metro,loadbang) straight into oscillator signal inlets, which Max rejects ("error connecting outlet ... to ... inlet"). They now use the correct idiom -- a float number box sets the oscillator frequency,metrotriggers envelopes, and envelopes modulate amplitude through a*~VCA -- so the generated demo patches load cleanly.
New: kwds_filter utility¶
- Added
py2max.utils.kwds_filter(kwds, **elems): returnskwdsmerged with theelemswhose value is notNone(legitimate falsy values such as0/""are kept, and the input is not mutated). Lets a method keep an optional parameter in its signature but omit it from the forwarded**kwdswhen the caller leaves it unset.
Improved: Build and test-output hygiene¶
- Coverage HTML now writes to
build/coverage-htmland the test suite writes its artifacts underbuild/test-output/-- both within the git-ignoredbuild/tree -- instead of a trackedoutputs/directory. A newmake test-outputstarget writes all test artifacts flat intobuild/test-outputs/for quick inspection.
[0.3.2]¶
New: Patch editing and removal API¶
-
Loading a patch (
Patcher.from_dict/load) now restores all ID-generation state -- object, node, edge, and semantic-ID counters -- soadd_*calls made after a load no longer collide with existing object IDs. This fixes the headline "edit an existing patch" round-trip, which previously emitted duplicate IDs on the first post-load add. -
Added a removal / editing API with referential-integrity cleanup:
remove_line/disconnect,remove_box/remove, andreplace. Removing a box also prunes its dangling patchlines and clears the associated node/edge/index bookkeeping, so no orphaned lines or stale IDs are left behind.
New: Graph-layout engines as layout managers¶
-
Three optional graph-layout backends -- HOLA (
hola-graph), COLA plus force-directed / geometric layouts (graph-layout), and OGDF's layered / force-directed / planar layouts (ogdf-py) -- are now selectable as first-class layout managers vialayout="graph:<algo>"(e.g.graph:hola,graph:cola,graph:ogdf-sugiyama). Because these algorithms need the whole graph, positions are applied onoptimize_layout()rather than as each box is added, and each box's width/height is preserved so UI objects are not squashed. Eleven algorithms are available:hola,cola,sugiyama,fruchterman-reingold,kamada-kawai,spectral,circular,shell,ogdf-sugiyama,ogdf-fmmm,ogdf-planarization. -
The engines are lazy-imported inside the manager, so
import py2maxstill pulls zero runtime dependencies; a missing backend raises a clear error naming the package to install. Install withpip install "py2max[graph]"-- thegraphextra now also bundleshola-graphalongsidegraph-layoutandogdf-py. Implemented asGraphLayoutManagerinpy2max/layout/external.py.
New: Layout gallery generator and docs page¶
-
scripts/gen_layout_gallery.py(run viamake gallery) renders every supported graph layout over one shared sample patch to transparent SVGs underdocs/assets/imgs/, using py2max's own SVG exporter so the results are directly comparable. Backends that are not installed are skipped rather than failing the run. -
A new published Layout Gallery page (
docs/user_guide/layout_gallery.md, linked in the User Guide navigation) shows the rendered layouts and documents thegraph:<algo>layout-manager API. The older networkx / graphviz / tsmpy experiments were dropped in favor of the three maintained backends. -
The generator also renders py2max's built-in managers (grid, flow, columnar, matrix) via
optimize_layout()-- no external dependencies -- and the Layout Managers guide (docs/user_guide/layout_managers.md) now embeds these as inline visuals so each layout strategy can be seen, not just described.
Fixed: Generation correctness¶
-
add_coll/add_dict/add_tableno longer discard a caller-suppliedtextargument whennameisNone(an operator-precedence bug that let the fallback string win). -
add_beapstrips the.maxpatsuffix correctly; previouslyrstrip(".maxpat")could truncate names such asdrum.maxpatdown todru. -
Parallel patchlines between the same source and destination now receive incrementing
ordervalues, so they spread apart in Max instead of overlapping. -
Comments created with
comment=are emitted on every save path (save_as,to_json,to_dict), not onlysave()/optimize_layout(). -
Hand-typed objects that are not in the built-in defaults now get one outlet instead of zero, so they can act as a connection source.
-
Load/save round-trip no longer injects
autosave/dependency_cachedefaults into nested subpatchers the source patch did not have; patches containing subpatchers now round-trip faithfully.
Fixed: Layout managers¶
-
layout="columnar"now works; it previously raisedNotImplementedErrordespite being documented. -
Layout optimizers preserve each object's real width and height. UI objects (
scope~,dial,slider,function,live.*, comments) are no longer squashed to text-box size byoptimize_layout(). -
The managers now share a single directed-graph model (
PatchGraph) rather than re-deriving adjacency from patchlines in each manager, and object classification no longer carries contradictory category assignments (its context inference uses maxref signal typing with word-boundary matching).
Improved: maxref bundle loading (lazy and thread-safe)¶
-
In bundle mode (no local Max install), the shipped catalog is no longer eagerly materialized into the cache on first access. The name-to-source map still loads once, but each object is now built from the in-memory bundle on demand, so a single-object query no longer constructs all ~1175 entries. A bundle object whose cache slot is empty is re-materialized from the bundle rather than returning nothing.
-
The process-wide maxref cache is now thread-safe: an
RLockguards the lazy refdict / category-map initialization and cache population, so concurrentBox.help()/ validation lookups no longer race on first load or mutate the cache unsafely.
Fixed: maxref outlet digests and parser robustness¶
-
Outlet
<digest>text is now extracted symmetrically with inlets. A condition bug previously required the<outlet>element to have leading text and then stored that text instead of the digest, so outlet descriptions were dropped for almost every object (~2000 digests across ~1093 objects).Box.help()/get_info()now report outlet descriptions. The shipped offlinebundle.json.gzwas regenerated so bundle-mode users (Linux/Windows/no-Max) get the corrected data. -
A raw
&in reference prose no longer drops the whole object on parse. Ampersands that do not begin a valid XML entity are escaped before parsing, hardening against.maxref.xmlmarkup variations across Max versions (valid entities like&,",µare left intact).
Improved: Cross-platform maxref discovery¶
- Reference-page resolution now honors the
PY2MAX_MAX_REFPAGESenvironment override on all platforms and auto-discovers a Windowsrefpagesdirectory, so Windows users with Max installed read live reference data instead of always falling back to the bundled snapshot. A wheel-build test now asserts the offlinebundle.json.gzactually ships in the built artifact.
Improved: SVG preview fidelity¶
-
The SVG exporter (
Patcher.to_svg/py2max preview) now renders a more faithful preview instead of a uniform grey schematic: -
Box colors are honored. Colors set via
Box.set_color/apply_theme(bgcolor,bordercolor,textcolor) are drawn, converted from Max's[r, g, b, a]floats to CSS. -
UI objects get recognizable affordances rather than an identical rectangle: message boxes draw the right-edge flag notch,
togglean X,buttona circle, number boxes (flonum/number) the left triangle marker,diala circle with a pointer, andslidera thumb bar. -
Ports resolve from the box's own
numinlets/numoutlets(what is written to the.maxpat) rather than a maxref lookup. Ports therefore render correctly for objects maxref does not know and without a Max install, and patchline endpoints line up with the ports they connect to (both use the same counts). -
export_svg_stringnow builds the document in memory instead of round-tripping through a temporary file.
Fixed: Layout classification and overlap resolution¶
-
Object classification (matrix / columnar layouts) is now factored into a clear precedence -- curated functional intent, then maxref signal typing for the unknown audio tail, then name patterns -- with the rationale documented. Curated sets must win because functional categories do not map to raw signal I/O (even
cycle~exposes a signal inlet, so signal typing alone would call it a processor;adc~is an input though it is a signal source). The dead, never-effective_refine_column_assignments_by_flowno-op and its call site were removed. -
LayoutManager.prevent_overlapsnow converges. The previous version cached each object's rect before mutating it (so pushes stopped accumulating) and clamped boxes back inside the canvas (re-introducing the overlaps it had just removed), leaving dense layouts overlapping at the 50-iteration cap. It is replaced with a monotone sweep that pushes each object clear of already-placed ones along the axis of least penetration; it converges in a few passes, early-exits when nothing overlaps, and preserves each object's real size.
Improved: Patch transformers¶
-
run_pipelinenow delegates tocompose, removing a duplicated apply loop and giving the previously-unused (but exported)composehelper a real use. -
The
add-commenttransformer's position is now reachable from the CLI: prefix the value withabove|below|left|right:to place the comment (e.g.--apply "add-comment=below:tempo"), defaulting toabove. A leading token that is not a position is kept as comment text, so"note: hi"is left intact. -
Added two transformers backed by existing APIs:
apply-theme(apply a named color theme --light/dark/blue/high-contrast) andscale-positions(scale every object's x/y position by a factor, sizes unchanged).
Fixed: Converters module cleanup¶
- Importing
py2max.export.convertersno longer constructs aPatcheras an import-time side effect (which pulled in maxref, layout, etc.). The default-attribute set is now computed lazily on first use and cached. Also removed a dead_infer_categoryhelper and an unreachableNotImplementedErrorguard (the caller strips subpatcher dicts before that path).
Removed: Honest CLI help for the moved serve / repl subcommands¶
- The
serveandreplsubcommands (whose implementations moved topy2max-serverin 0.3.0) no longer advertise themselves as live in--help; they now removed.
Removed: MaxRefDB deprecated alias methods¶
-
Removed 12 long-deprecated
MaxRefDBaliases in favor of the canonical API:populate_from_maxref/populate_all_*->populate([category=...]),search_objects->search,get_objects_by_category->by_category,get_all_categories->.categories,get_object_count->.count, andexport_to_json/import_from_json->export/load. The database module is already off the import path (lazily loaded, stdlib-only), so this only trims its API surface. Callers, tests, and docs were updated to the canonical names. -
While updating the SQLite demo scripts, also fixed two pre-existing broken examples:
from py2max import MaxRefDB(correct:from py2max.maxref import MaxRefDB) andcreate_database(...)used as a free function (it isMaxRefDB.create_database(...)). Bothtests/examples/db/scripts now run.
Removed: Obsolete layout experiments and vendored editor assets¶
-
Deleted the old graph-layout experiment tests (
tests/test_layout_hola{1,2,3},test_layout_hola_graph,test_layout_networkx{1,2},test_layout_nx_graphviz,test_layout_nx_orthogonal,test_layout_nx_tsmpy). They exercised backends that are no longer supported (raw adaptagrams, networkx, pygraphviz, tsmpy) or duplicated the newGraphLayoutManagercoverage, and only ever skipped. The maintained path is covered bytests/test_layout_graph_manager.py. -
Removed the vendored browser libraries under
docs/js/(SVG.js, WebCola, D3) -- reference copies for the interactive editor that moved to the separatepy2max-serverpackage in 0.3.0 -- and the stale Sphinx build output underdocs/build/that predated the MkDocs migration and was being copied into the published site. -
Pruned 30 obsolete design notes from the
docs/notes/dev journal (REPL, SSE/WebSocket live-preview server, and interactive SVG-editor implementation notes), all for features that moved topy2max-server. The 13 still-relevant library/journal notes were kept.
[0.3.1]¶
New: Standalone gen.codebox~ Support¶
-
Patcher.add_gen_codebox(code)adds a self-containedgen.codebox~object -- a complete gen patch in a single box that lives directly in a regular Max patcher, distinct from the innercodebox~(emitted byadd_codebox) that belongs inside agen~/rnbo~subpatcher. This is the form emitted by gen transpilers. Code newlines are normalized to CRLF as Max expects, andfontname/fontsizedefault to the monospaced gen style. -
Inlet/outlet counts are derived automatically from the code (the highest
inN/outNreferences, floor of 1), matching gen's dynamic-I/O semantics. Explicitnuminlets/numoutletsstill override. -
Available via the
add()string shortcut too:p.add("gen.codebox~ out1 = in1 * 0.5;"). The shortcut suits single-line /;-terminated code; pass multi-line source toadd_gen_codebox()directly. -
Connection validation for
gen.codebox~(andcodebox/codebox~) now bound-checks against the box's own declared inlet/outlet counts rather than the static.maxref.xmlentry, since codebox I/O is code-dependent. This both allows valid connections to/from wider codeboxes (e.g. from a second outlet) and rejects genuinely out-of-range ones.
[0.3.0]¶
Removed: Interactive Server Split Into py2max-server (breaking)¶
The browser-based live editor and remote REPL have moved to a separate companion package, py2max-server, so the core library stays small, offline, and dependency-free.
-
Removed
Patcher.serve()and thepy2max serve/py2max replCLI commands; those CLI subcommands now print a pointer topy2max-server. -
Removed the
[server]optional-dependency extra (websockets,ptpython) and the bundled browser assets (py2max/static/). -
Install the server features with
pip install py2max-serverand usepy2max-server serve <patch>/py2max-server repl …. The remote REPL now requires token authentication (passed via--tokenorPY2MAX_REPL_TOKEN).
New: Patcher.encapsulate()¶
Patcher.encapsulate(boxes, text="p sub")wraps a selection of boxes into a subpatcher, auto-generatinginlet/outletobjects for any connections that cross the selection boundary and rewiring the parent through the new subpatcher box. Connections wholly inside the selection move into the subpatcher; connections wholly outside it are untouched. Ports are de-duplicated by source, matching how patches are built by hand. Returns the new subpatcherBox.
New: Preset / pattrstorage Scaffolding¶
-
Patcher.add_pattrstorage(name),Patcher.add_autopattr(), andPatcher.add_preset_system(name)(which adds both and wiresautopattr->pattrstorage) scaffold a Max preset system. Any object with a scripting name (varname) orparameter_enable=1participates. -
Patcher.enable_parameter(box, longname, shortname="", ptype=0, initial=None)turns an existing UI box into a Max parameter (setsparameter_enableand thesaved_attribute_attributes), so it participates in presets and, in a Max for Live device, appears as an automatable parameter.
New: Keyword-Attribute Validation (validate_attrs)¶
Patcher(validate_attrs=True)warns (UserWarning) when an object is given a keyword that is not a known attribute for its Max class -- catching typos likeinital=forinitial=. The known set is the object's maxref attributes plus a universal box-attribute whitelist; objects with no maxref entry are skipped. Off by default and warn-only, so it never changes generated output.
New: Multichannel (mc.) / Polyphony Helpers¶
-
Patcher.add_mc(text, chans=None)adds a multichannel object, prefixingmc.and appending@chans(e.g.add_mc("cycle~ 440", chans=4)->mc.cycle~ 440 @chans 4). -
Patcher.add_poly(target, voices=1)adds apoly~object hosting N voices of a target patch.
Improved: SVG Export (Max-faithful preview)¶
- The
preview/to_svgoutput now approximates Max's look: a light patcher background, signal vs message/control ports colored distinctly (signal green, control dark), signal cables drawn thicker and in a distinct color, and subpatcher boxes tinted so they stand out. Object text is intentionally not truncated, matching Max (objects size to their text).
Changed: Documentation moved to MkDocs¶
- Documentation migrated from Sphinx/reStructuredText to MkDocs + Material + mkdocstrings (all Markdown, matching the rest of the repo). The API reference is generated from the (now fully typed) docstrings, including
Patcher's mixin-provided methods. The changelog and contributing pages are single-source includes ofCHANGELOG.md/CONTRIBUTING.md. Build withmake docs, preview withmake docs-serve, publish withmake docs-deploy.docs/notes/is retained as a historical journal but excluded from the published site.
New: Color / Theme Helpers¶
-
Box.set_color(bg=..., text=..., border=...)sets a box'sbgcolor/textcolor/bordercolor; each accepts a named color (e.g."red"), a hex string ("#ff8800"), or an[r, g, b(, a)]float sequence. Returns the box for chaining. -
Patcher.apply_theme(theme)applies a color theme to every box (recursing into subpatchers). Built-in themes:"light","dark","blue","high-contrast"; or pass a dict ofbg/text/bordercolors. -
py2max.core.colorsexposes theMAX_COLORSnamed palette andresolve_color().
Security¶
- Removed a misleading path-traversal check in
Patcher.save_as(). The previous..//etcallowlist was trivially bypassable and gave a false sense of safety; for an offline file generator it provided no real protection. Genuinely unresolvable paths still raisePatcherIOError.
Typed: Full mypy --strict¶
- The entire package is now annotated and passes
mypy --strict, backing the shippedpy.typedmarker.[tool.mypy]enforcesstrict = true. Core has no runtime dependencies.
Changed: Lighter Core Imports¶
import py2maxno longer eagerly importssqlite3, the maxref database layer, orpy2max.m4l.MaxRefDBis now available lazily viafrom py2max.maxref import MaxRefDB(removed from the top-levelpy2maxnamespace).
Internal: Patcher Decomposition¶
- Split the ~1660-line
Patcherclass into focused mixins composed via inheritance: object creation (BoxFactoryMixinincore/factory.py) and serialization (SerializationMixinincore/serialization.py). The public API is unchanged; adding a new object type now means editingcore/factory.pyrather than the core class.
Fixed¶
-
Object-name resolution (used by connection validation and object classification) now reads the box
textproperty, so it resolves correctly for boxes loaded from a file. Previously it inspected only programmatic kwargs and returnednewobjfor loaded boxes. -
Box.oidnow returns the trailing numeric part of any id (e.g.cycle_1-> 1) instead of raisingValueErrorundersemantic_ids=True. -
The
py2maxCLI now reports allPy2MaxErrors (not justInvalidConnectionError) as a clean error message instead of leaking a traceback. -
Fixed an
inital->initialkeyword typo in the simple-synthesis tutorial.
Testing & Tooling¶
-
The test suite is now hermetic: a
conftest.pyautouse fixture isolates each test in a temporary working directory, so relativeoutputs/writes no longer accumulate in the repo. Fixture reads are anchored at the test file. -
Promoted the
.amxdbyte-for-byte fixtures from the gitignoredoutputs/into trackedtests/data/, so that verification runs in CI and on fresh checkouts instead of only on the author's machine. -
Repo-wide
rufflint and format cleanup.
New: Max for Live Support (py2max.m4l)¶
Implements issue #9. See docs/notes/amxd.md for the on-disk format, embedded-project block, and verification details.
-
.amxdread/write: byte-for-byte compatible with Max-exported devices; verified against real fixtures and end-to-end in Live 12. -
Device-type discrimination: Audio Effect / Instrument / MIDI Effect via
Patcher(device_type=...)or thepack_amxd/write_amxddevice_typeargument. -
Presentation-mode helpers:
Patcher.enable_presentation(devicewidth=...),Patcher.enforce_integer_coords(),Box.add_to_presentation([x, y, w, h])(rejects M4L infrastructure objects, rounds fractional coords with a warning). -
Patcher.save()/Patcher.from_file()auto-detect the.amxdextension;.maxpatpath is unchanged.
Changed: M4L Module Layout & Imports¶
-
All M4L code (binary format + presentation helpers) lives in a single module
py2max/m4l.py. Previously briefly split aspy2max/amxd.py. -
M4L symbols are reachable only via
from py2max.m4l import …; nothing is re-exported from the top-levelpy2maxnamespace.
New: Prebuilt MaxRef Bundle (Linux Support)¶
-
Ship
py2max/maxref/data/bundle.json.gzin the wheel (1175 objects, ~1 MiB compressed, ~7 MiB raw). -
MaxRefCache._get_refdict()falls back to the bundle when no local Max installation is found, pre-seeding the parser cache soBox.help(),get_inlet_count,get_outlet_count, and connection validation work identically on Linux. -
Regenerate with
uv run python scripts/build_maxref_bundle.pyon a machine with Max installed; commit the result. -
Bundle stores full parsed data (methods, attributes, inlets/outlets, digests, descriptions) — not a trimmed subset — so introspection parity with macOS/Windows is preserved.
[0.2.1] - 2026-01-11¶
New: Dagre Layout Algorithm¶
-
Added Dagre (Directed Acyclic Graph) as third layout algorithm option alongside WebCola and ELK
-
Integrated
dagre-bundle.jscombining graphlib with require shim for browser compatibility -
Added Dagre-specific controls: Ranker (network-simplex, longest-path, tight-tree) and Align options
-
Supports all flow directions: top-bottom, bottom-top, left-right, right-left
Improved: Interactive Editor Visualization¶
-
ViewBox Scaling: Dynamic padding (10% of content, min 30px, max 100px) with aspect ratio preservation
-
Port Position Safety: Added bounds checking with
safeIndexclamping to prevent invalid port positions -
Patchline Animation: Added
animatePatchlines()method for smooth patchline transitions during layout -
Layout Centering: Added
centerLayout()helper method - all three algorithms now center content within canvas -
Delta Updates: Position updates now send only changed box data instead of full patcher state
-
Added
updateBoxPosition()for efficient single-box DOM updates -
Added
updateConnectedLines()to update patchlines without full re-render -
Significantly reduces bandwidth during drag operations
Improved: FlowLayoutManager¶
-
Line Crossing Minimization: Added
_minimize_crossings()method using barycenter heuristic -
Objects within each level are reordered based on average position of connected objects in previous level
-
Reduces visual line crossings for cleaner layouts
-
Negative Position Prevention: Added bounds clamping and auto-scaling when content exceeds available space
-
Incremental Layout: Supports
optimize_layout(changed_objects)for efficient partial updates
Improved: GridLayoutManager¶
-
Fixed integer division to float division for consistent cluster positioning
-
Now uses consistent float spacing within clusters
-
Incremental Layout: Supports
optimize_layout(changed_objects)for efficient partial updates
Improved: WebSocket Server Security¶
-
Input Validation: Added comprehensive schema-based message validation
-
MESSAGE_SCHEMASdefines required fields and types for each message type -
MAX_STRING_LENGTHSprevents abuse (256 chars for IDs, 10000 for text, 4096 for filepaths) -
COORDINATE_BOUNDSvalidates positions (-100000 to 100000) -
Checks for control characters in strings
-
Validates optional fields (outlet/inlet indices 0-255)
-
Validation errors sent back to client as error messages
New: Save As Dialog¶
-
Added
save_as_requiredmessage type when patcher has no filepath -
Added
handle_save_as()handler for saving with specified filepath -
Added
showSaveAsDialog()in JavaScript with filename prompt -
Automatically adds
.maxpatextension if not provided
Fixed: ELK Layout¶
-
Fixed "Referenced shape does not exist" errors by validating edges before creating ports
-
Ports now created based on actual connections, not just declared counts
Fixed: Static File Paths¶
- Fixed 404 error for
interactive.htmlby correcting static file path resolution
Improved: Base LayoutManager¶
-
Added
prevent_overlaps()method for iterative overlap prevention -
Incremental Layout System: Added smart layout optimization that only repositions affected objects
-
optimize_layout(changed_objects)accepts optional set of changed object IDs -
should_use_incremental()determines when to use incremental vs full layout (30% threshold) -
get_affected_objects()finds changed objects plus their connected neighbors -
_incremental_layout()repositions only affected objects using spiral search -
_find_non_overlapping_position()finds nearby positions that don't overlap with fixed objects -
_full_layout()for complete layout recalculation (subclasses override)
[0.2.0]¶
Updated: Optional Layout Dependencies¶
-
Updated
pycoladependency tograph-layoutpackage (https://github.com/shakfu/graph-layout) -
Renamed test file from
test_layout_pycola.pytotest_layout_graph_layout.py -
Updated API to use
ColaLayoutAdapterfromgraph_layoutmodule -
Updated
pyholadependency tohola-graphpackage (https://github.com/shakfu/hola-graph) -
Renamed test file from
test_layout_pyhola.pytotest_layout_hola_graph.py -
Updated imports to use
hola_graph._coremodule -
Fixed
test_layout_networkx2.pyto properly check forpygraphvizdependency -
Test now correctly skips when pygraphviz is not installed
Simplified: Optional Dependencies¶
-
Consolidated optional dependencies in
pyproject.tomlto singleserveroption -
Removed
replandalloptions -
servernow includes bothwebsocketsandptpython -
Install with:
pip install py2max[server]
New: Interactive Editor - Advanced Layout with SVG.js, WebCola, and D3.js¶
-
Added complete SVG.js (v3.2.5) integration for all SVG manipulation and animation in the interactive editor
-
Added WebCola constraint-based force-directed graph layout engine with D3.js (v7) integration
-
Added interactive auto-layout controls panel with real-time parameter adjustment
-
Added 5 adjustable layout parameters via sliders and controls:
-
Link Distance (50-300): Controls spacing between connected objects
-
Iterations (10-200): Controls layout quality and convergence
-
Canvas Width (400-1600): Adjustable layout area width
-
Canvas Height (300-1200): Adjustable layout area height
-
Avoid Overlaps (checkbox): Toggle automatic overlap prevention
-
Added constraint-based layout system with 4 presets:
-
None: Natural force-directed layout without alignment constraints
-
Horizontal Flow: Aligns objects in horizontal rows (left-to-right signal flow)
-
Vertical Flow: Aligns objects in vertical columns (top-to-bottom signal flow)
-
Grid: Strict grid alignment with both row and column constraints
-
Added smooth SVG.js animations (500ms ease-in-out) for layout transitions
-
Added constraint generation algorithm that analyzes object positions and creates alignment constraints
-
Added collapsible controls panel with "Apply Layout" and "Hide" buttons
-
Added visual feedback showing active parameters and constraint count
SVG.js Implementation:
-
Refactored all SVG rendering to use SVG.js declarative API instead of native DOM manipulation
-
initializeSVG(): Creates SVG canvas and layer groups using SVG.js -
createBox(): Renders boxes with rectangles, text, and clipping paths using SVG.js -
createLine(): Renders connection lines with hitboxes using SVG.js -
addPorts(): Renders inlet/outlet circles using SVG.js -
autoLayout(): Animates box movements using SVG.js transforms
WebCola Integration:
-
Force-directed graph layout with configurable parameters
-
Constraint-based positioning using alignment constraints
-
Automatic overlap avoidance with adjustable node dimensions
-
Handles disconnected graph components gracefully
-
Jaccard link lengths for natural connection spacing
Constraint System:
-
Automatic constraint generation based on object proximity (50px threshold)
-
Alignment constraints for horizontal rows (Y-axis alignment)
-
Alignment constraints for vertical columns (X-axis alignment)
-
Grid constraints combining both row and column alignment
-
Real-time constraint application with visual feedback
Documentation:
-
Added comprehensive
docs/LIBRARIES_INTEGRATION.md(518 lines) -
Detailed parameter descriptions and effects
-
Constraint preset usage examples
-
Testing procedures and expected behavior
-
Code examples and API documentation
-
Performance considerations for different patch sizes
Demo Scripts:
-
Added
examples/auto_layout_demo.py: Complex synthesizer with randomized positions (13 objects, 16 connections) -
Hierarchical layout demo: Tree structure with multiple processing layers (12 objects)
Benefits:
-
Professional animated transitions for all layout operations
-
Interactive experimentation with layout parameters
-
Structured layouts matching typical Max patch patterns
-
Clean, maintainable SVG.js codebase
-
Four layout presets for different use cases
-
Real-time visual feedback
-
Minimal overhead (234KB total: D3 + SVG.js + WebCola, minified)
Example Usage:
# Start interactive editor
py2max serve outputs/auto_layout_demo.maxpat
# In browser:
# 1. Click "Auto-Layout" to show controls
# 2. Adjust Link Distance slider (50-300)
# 3. Select Constraint Preset (Grid/Horizontal/Vertical/None)
# 4. Adjust Iterations for convergence quality
# 5. Click "Apply Layout" to see smooth animations
# 6. Experiment with different parameter combinations
New: Interactive Editor - Nested Patcher Navigation¶
-
Added full nested patcher (subpatcher) navigation support in interactive editor
-
Double-click on subpatcher boxes (blue dashed border) to navigate into them
-
Navigate back using "Parent" button or ESC key
-
Breadcrumb navigation displays current location (e.g., "Main / Oscillator / Envelope")
-
Subpatcher boxes are fully interactive: draggable, connectable, deletable
-
Visual distinction: subpatcher boxes have blue dashed borders and bold blue text
-
Event delegation for reliable double-click detection even with dynamic DOM updates
-
Automatic parent reference restoration when loading patches from files
Server-Side Changes:
-
Modified
get_patcher_state_json()to includehas_subpatcherflag andpatcher_pathbreadcrumb -
Added
handle_navigate_to_subpatcher(),handle_navigate_to_parent(),handle_navigate_to_root()handlers -
Fixed inlet/outlet count detection to use
numinlets/numoutletsattributes from loaded files -
Handler now tracks both
root_patcher(for saving) andpatcher(current view)
Client-Side Changes:
-
Added breadcrumb UI showing patcher hierarchy
-
Implemented event delegation for double-click handling on dynamically created boxes
-
Fixed object positioning by flattening
patching_rectintox,y,width,height -
CSS styling for subpatcher boxes with distinct visual appearance
-
ESC key navigation support
Core Changes:
-
Modified
Patcher.from_dict()to set_parentreferences for nested subpatchers when loading from files -
Ensures bidirectional parent-child relationships for proper navigation
Tests:
-
Added 14 comprehensive tests in
tests/test_nested_patchers.py -
All tests passing (326 passed, 14 skipped)
Demo:
-
Added
examples/nested_patcher_demo.pywith three demonstration patches: -
Synthesizer with nested envelope subpatcher
-
Effects chain with parallel subpatchers
-
Deeply nested hierarchy (6 levels)
New: SVG Preview Feature¶
-
Added
py2max previewCLI command for offline visual validation of Max patches -
Added
py2max.svgmodule with complete SVG rendering engine (330 lines) -
Added
export_svg()andexport_svg_string()functions for programmatic SVG generation -
Added SVG rendering for boxes with type-specific styling:
-
Regular objects: Light gray fill
-
Comments: Yellow fill (#ffffd0)
-
Messages: Medium gray fill
-
Added patchline rendering with correct inlet/outlet connection points
-
Added optional inlet/outlet port visualization (blue inlets, orange outlets)
-
Added automatic port detection from MaxRef metadata via
get_inlet_count()andget_outlet_count() -
Added support for both Rect objects and list/tuple coordinate formats
-
Added proper XML text escaping for special characters
-
Added automatic viewBox calculation with padding
-
Added browser integration with
--openflag -
Added 17 comprehensive tests covering all SVG functionality
-
Added
tests/examples/preview/svg_preview_demo.pydemonstration script -
Added
docs/SVG_PREVIEW.mdcomplete documentation
CLI Usage:
# Basic preview (saves to /tmp)
py2max preview my-patch.maxpat
# Specify output path
py2max preview my-patch.maxpat -o output.svg
# Custom title
py2max preview my-patch.maxpat --title "My Synth"
# Hide inlet/outlet ports
py2max preview my-patch.maxpat --no-ports
# Open in browser automatically
py2max preview my-patch.maxpat --open
# Combine options
py2max preview synth.maxpat -o docs/synth.svg --title "Synth" --open
Python API:
from py2max import Patcher, export_svg, export_svg_string
# Create and export
p = Patcher('synth.maxpat', layout='grid')
osc = p.add_textbox('cycle~ 440')
dac = p.add_textbox('ezdac~')
p.add_line(osc, dac)
p.optimize_layout()
export_svg(p, 'synth.svg', title="Simple Synth", show_ports=True)
# Export to string
svg_content = export_svg_string(p, show_ports=True)
Benefits:
-
No Max installation required for visual validation
-
High-quality, scalable vector graphics
-
Works with all py2max layout managers
-
Perfect for CI/CD, documentation, and version control
-
Pure Python implementation with no binary dependencies
-
Viewable in any web browser
New: SQLite Database Support¶
-
Added
py2max.dbmodule with comprehensive SQLite database support for Max object reference data -
Added
MaxRefDBclass for creating, querying, and managing Max object databases -
Added 14 normalized database tables: objects, metadata, inlets, outlets, methods, method_args, attributes, attribute_enums, objargs, examples, seealso, misc, palette, parameter
-
Added support for both in-memory and file-based databases
-
Added database query API:
search_objects(),get_objects_by_category(),get_all_categories() -
Added bidirectional conversion: .maxref.xml → SQLite → JSON
-
Added
export_to_json()andimport_from_json()methods for database portability -
Added
create_database()convenience function for database creation and population -
Added category-based population methods:
populate_all_objects(),populate_all_max_objects(),populate_all_jit_objects(),populate_all_msp_objects(),populate_all_m4l_objects() -
Added maxref category helper functions:
get_all_max_objects(),get_all_jit_objects(),get_all_msp_objects(),get_all_m4l_objects(),get_objects_by_category() -
Added category tracking to maxref module (462 Max, 448 MSP, 210 Jitter, 37 M4L objects)
-
Added complete test suite with 17 test cases
-
Added
examples/maxref_db_demo.pydemonstration script -
Added
examples/category_db_demo.pycategory-specific examples -
Added
docs/database.mdAPI documentation
Improved: MaxRefDB API Enhancements¶
Python API Improvements:
-
Added Pythonic properties:
.count,.categories,.objectsfor cleaner access -
Added magic methods:
len(db),'obj' in db,db['obj'],repr(db)for natural Python usage -
Added simplified methods:
populate(),search(),by_category(),export(),load()with cleaner naming -
Added
summary()method for database statistics with category breakdown -
Maintained full backward compatibility with deprecated methods
-
All 18 database tests pass
CLI Improvements:
-
Added comprehensive
py2max dbsubcommand with 7 operations: -
db create- Create new databases with optional category filtering -
db populate- Add objects to existing databases -
db info- Show database information with summary and listing options -
db search- Search objects by text or category with verbose mode -
db query- Get detailed object information (JSON, dict, or human-readable) -
db export- Export database to JSON -
db import- Import JSON data into database -
Updated
convert maxref-to-sqliteto use MaxRefDB internally -
Added 7 new CLI tests covering all db subcommands
-
All 272 tests pass (258 passed, 14 skipped)
Example Usage:
# New Pythonic API
db = MaxRefDB('maxref.db')
db.populate(category='msp')
print(len(db)) # Total objects
if 'cycle~' in db:
cycle = db['cycle~']
results = db.search('filter')
db.export('backup.json')
# New CLI commands
py2max db create msp.db --category msp
py2max db info msp.db --summary
py2max db search msp.db "oscillator" -v
py2max db query msp.db cycle~ --json
py2max db export msp.db backup.json
# Cache management
py2max db cache location
py2max db cache init
py2max db cache clear
New: Automatic Cache System¶
Platform-Specific Cache:
MaxRefDB now automatically creates and populates a cache database on first use:
-
macOS:
~/Library/Caches/py2max/maxref.db -
Linux:
~/.cache/py2max/maxref.db -
Windows:
~/AppData/Local/py2max/Cache/maxref.db
Benefits:
-
One-time population of all 1157 Max objects
-
Instant access on subsequent use
-
No manual setup required
-
Platform-appropriate cache location
New Static Methods:
-
MaxRefDB.get_cache_dir()- Get platform-specific cache directory -
MaxRefDB.get_default_db_path()- Get default database path
Updated API:
-
MaxRefDB()- Now uses cache by default -
MaxRefDB(db_path, auto_populate=True)- Control auto-population -
MaxRefDB(':memory:')- In-memory database (no caching)
New CLI Commands:
-
py2max db cache location- Show cache location and status -
py2max db cache init- Manually initialize cache -
py2max db cache clear- Clear cache database
Example Usage:
# Automatic caching (default)
from py2max.db import MaxRefDB
db = MaxRefDB() # Auto-populates cache on first use
print(f"Objects: {len(db)}") # 1157
# Get cache location
print(f"Cache: {MaxRefDB.get_default_db_path()}")
[0.1.2]¶
Improvements in Type Safety¶
- Added type safety improvements via compliance with
mypychecks
Improvements in Layout¶
-
Added
optimize_layout()method for post-connection layout optimization -
Added
cluster_connectedparameter toGridLayoutManagerfor connection-aware object clustering -
Added
flow_directionparameter support for both horizontal and vertical layouts in all layout managers -
Added backward compatibility for legacy layout manager APIs
-
Enhanced layout performance with connection-aware clustering algorithms
-
Improved layout manager consistency with unified
GridLayoutManagerandFlowLayoutManagerAPIs -
Added
FlowLayoutManagerwith intelligent signal flow analysis and hierarchical positioning -
Added
GridLayoutManagerwith connection-aware clustering and configurable flow direction
Improvements in Max Object Introspection¶
-
Added optional connection validation system with inlet/outlet validation and
InvalidConnectionError. This is early stages, and may have some false positives, but planned improvements in handling of excepttions should make this accurate and useful. -
Added object introspection methods:
get_inlet_count(),get_outlet_count(),get_inlet_types(),get_outlet_types() -
Added
Box.help(),Box.help_text()andBox.get_info()methods for rich object documentation. -
Added
maxrefintegration system with dynamic help for 1157 Max objects using.maxref.xmlfiles
Bug Fixes¶
- Fixed
maxclassassignment bug that was preventing patchlines from connecting properly
Improvements in Project Management¶
- Converted to uv for project and dependency management.
[0.1.1]¶
-
Added
Makefilefrontend -
Changed package manager to
uv -
Improved compatibility with Python 3.7
-
Improved core Coverage: 99%
-
Added clean script:
./scripts/clean.sh -
Added coverage script and reporting:
./scripts/coverage.sh -
Moved
testsfolder frompy2max/py2max/teststopy2max/tests -
Added gradual types to
py2max/core, no errors withmypy -
Added
number_tildetest -
Fixed
commentpositioning -
Added
pyholalayout. -
Added
graphvizlayouts. -
Fixed
Adaptagramslayout. -
Added graph layout comparison and additional layouts.
-
Added vertical layout variant.
-
Added boolean
tildeparameter for objects which have a tilde sibling. -
Added preliminary support for
rnbo~include rnbo codebox
[0.1.0]¶
-
Added a generic
.addmethod toPatcherobjects which include some logic to to figure out to which specialized method to dispatch to. See:tests/test_add.pyfor examples of this. -
Major refactoring after
test_tree_builderdesign experiment, so we have now only one simple extendable Box class, and there is round trip conversion between .maxpat files and patchers. -
Added
test_tree_builder.pywhich shows that the json tree can be converted to a python object tree which corresponds to it on a one-on-one basis, which itself can be used to generate the json tree for round-trip conversion. -
Added
from_fileclassmethod toPatcherto populate object from.maxpatfile. -
Added
coll,dictandtableobjects and tests -
Added some tests which try to use generic layout algorithm in Networkx but the results are quite terrible using builtin algorithms so probably better to try to create something fit-for-purpose.
-
Added
gensubpatcher -
Moved
varnameto optional kwds instead of being an explicit parameter since it's optional and its inclusion when not populated is sometimes problematic. -
Renamed odb to maxclassdb since it only relates to defaults per
maxclass -
Added smarter textbox which uses odb to improve object creation.
-
Added separate test folder
-
Added
odb.pyin package with a number of default configs of objects -
Converted to package.
-
Added some notes on graph drawing and layout algorithms
-
Added comments keyword in box objects + PositionManager for easy documentation
-
Added Comments objects
-
Refactor: MaxPatch and Patcher objects are now one.
-
Initial release