py2max 0.3.6__tar.gz → 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {py2max-0.3.6 → py2max-0.4.0}/CHANGELOG.md +366 -0
- {py2max-0.3.6 → py2max-0.4.0}/PKG-INFO +54 -8
- {py2max-0.3.6 → py2max-0.4.0}/README.md +53 -7
- {py2max-0.3.6 → py2max-0.4.0}/py2max/__init__.py +18 -2
- {py2max-0.3.6 → py2max-0.4.0}/py2max/cli.py +17 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/core/abstract.py +1 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/core/box.py +74 -8
- {py2max-0.3.6 → py2max-0.4.0}/py2max/core/factory.py +182 -48
- {py2max-0.3.6 → py2max-0.4.0}/py2max/core/patcher.py +20 -4
- py2max-0.4.0/py2max/core/props.py +1748 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/core/serialization.py +25 -2
- py2max-0.4.0/py2max/data/js2max/js2max.js +2017 -0
- py2max-0.4.0/py2max/data/js2max/js2max.v8.js +2620 -0
- py2max-0.4.0/py2max/js2max_runtime.py +123 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/layout/matrix.py +10 -2
- {py2max-0.3.6 → py2max-0.4.0}/py2max/log.py +124 -52
- {py2max-0.3.6 → py2max-0.4.0}/py2max/maxref/db.py +44 -13
- {py2max-0.3.6 → py2max-0.4.0}/pyproject.toml +1 -1
- {py2max-0.3.6 → py2max-0.4.0}/tests/conftest.py +25 -2
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/README.md +10 -1
- py2max-0.4.0/tests/test_add.py +176 -0
- py2max-0.4.0/tests/test_box_props.py +186 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_comment.py +15 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_db.py +76 -0
- py2max-0.4.0/tests/test_js2max_runtime.py +258 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_matrix.py +62 -0
- py2max-0.4.0/tests/test_logging.py +189 -0
- py2max-0.4.0/tests/test_param.py +107 -0
- py2max-0.4.0/tests/test_serialization.py +130 -0
- py2max-0.4.0/tests/test_single_file.py +369 -0
- py2max-0.4.0/tests/test_subpatch.py +98 -0
- py2max-0.3.6/tests/test_add.py +0 -65
- py2max-0.3.6/tests/test_param.py +0 -36
- py2max-0.3.6/tests/test_subpatch.py +0 -34
- {py2max-0.3.6 → py2max-0.4.0}/LICENSE +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/__main__.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/core/__init__.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/core/colors.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/core/common.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/core/patchline.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/exceptions.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/export/__init__.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/export/converters.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/export/svg.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/layout/__init__.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/layout/base.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/layout/external.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/layout/flow.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/layout/graph.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/layout/grid.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/lint.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/m4l.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/maxref/__init__.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/maxref/category.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/maxref/data/bundle.json.gz +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/maxref/legacy.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/maxref/parser.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/maxref/porttypes.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/py.typed +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/transformers.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/py2max/utils.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/__init__.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/data/complex.maxpat +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/data/desc.maxpat +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/data/empty.maxpat +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/data/mydevice.amxd +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/data/mydevice2.amxd +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/data/nested.maxpat +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/data/simple.maxpat +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/data/tabular.maxpat +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/data/umenu.maxref.xml +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/advanced/connection_patterns.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/advanced/custom_extensions.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/advanced/data_containers.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/advanced/error_handling.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/advanced/performance_optimization.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/advanced/subpatchers.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/api/box_api_examples.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/api/patcher_api_examples.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/auto_layout_demo.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/db/category_db_demo.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/db/maxref_db_demo.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/layout/columnar_layout_examples.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/layout/flow_layout_examples.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/layout/grid_layout_examples.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/layout/matrix_layout_examples.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/basic_synth.maxpat +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/basic_synth.svg +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/complex_synth.maxpat +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/complex_synth.svg +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/flow_layout.maxpat +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/flow_layout.svg +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/grid_layout.maxpat +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/grid_layout.svg +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/horizontal_layout.maxpat +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/horizontal_layout.svg +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/styled_no_ports.svg +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/styled_no_title.svg +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/styled_patch.maxpat +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/styled_with_ports.svg +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/svg_preview_demo.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/vertical_layout.maxpat +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/vertical_layout.svg +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/workflow_demo.maxpat +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/preview/workflow_demo.svg +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/quickstart/basic_patch.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/quickstart/layout_examples.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/tutorial/generative_music.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/tutorial/interactive_controller.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/tutorial/signal_processing_chain.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/examples/tutorial/simple_synthesis.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/graphs/random/v30e33.tglf +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_abstract_coverage.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_abstraction.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_amxd.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_attrui.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_basic.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_beap.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_bpatcher.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_cli.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_coll.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_colors.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_colors_theme.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_connection_validation.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_converters.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_core_coverage.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_defaults.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_dict.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_edit_operations.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_encapsulate.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_error_handling.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_examples.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_ezdac.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_gen.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_group.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_itable.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_js.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_kwds_filter.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_box_dims.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_builtins.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_coverage.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_flow.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_graph.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_graph_layout.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_graph_manager.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_overlaps.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_layout_vertical.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_linking.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_lint.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_m4l.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_maxref.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_maxref_bundle.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_maxref_lazy.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_maxref_parser.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_maxref_refpages.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_mc_cycle.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_mc_poly.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_message.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_mypatch.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_nested.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_nested_patchers.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_number_tilde.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_numbers.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_param_placement.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_patcher.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_pitched_osc.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_porttypes.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_presets.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_pydantic.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_rnbo.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_rnbo_subpatcher.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_scripting_name.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_search.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_semantic_ids.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_svg.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_svg_fidelity.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_table.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_transformers.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_transformers_e4.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_tree.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_tree_builder.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_tutorial_simple_synthesis.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_two_sines.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_umenu.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_utils.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_validate_attrs.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_validation.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_varname.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_wheel_bundle.py +0 -0
- {py2max-0.3.6 → py2max-0.4.0}/tests/test_zl_group.py +0 -0
|
@@ -2,12 +2,199 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.4.0] - 2026-07-27
|
|
6
|
+
|
|
7
|
+
Two things dominate this release.
|
|
8
|
+
|
|
9
|
+
**js2max**: a JavaScript counterpart to py2max that runs *inside* an open Max
|
|
10
|
+
patcher through the `v8` object, so a patch can build objects into itself and
|
|
11
|
+
serialize itself back out. Its runtime ships in the wheel, so
|
|
12
|
+
`p.add_v8_bridge()` and `p.save()` are all it takes. Confirmed end to end
|
|
13
|
+
against Max, including a file js2max wrote being opened by Max.
|
|
14
|
+
|
|
15
|
+
**Typed box properties**: the Max property vocabulary reaches the emitted patch
|
|
16
|
+
through `**kwds` no longer. `BoxProps` / `TextboxProps` mean a misspelled
|
|
17
|
+
`bgcolour` or a wrongly-typed `fontsize` is rejected by mypy with a suggestion,
|
|
18
|
+
where both previously shipped straight into the file.
|
|
19
|
+
|
|
20
|
+
The rest is nine fixes, several of them long-standing and silent -- comments
|
|
21
|
+
written with their port counts backwards since the beginning, `to_dict()`
|
|
22
|
+
returning an empty patcher depending on call order, and nulls reaching the patch
|
|
23
|
+
one level down.
|
|
24
|
+
|
|
25
|
+
### New: js2max -- a JavaScript counterpart that builds patches inside Max
|
|
26
|
+
|
|
27
|
+
- **[`js2max/`](js2max/) is a second front end to the `.maxpat` format**, in TypeScript, doing the one thing this package cannot: Max embeds a JavaScript engine, and `v8` ships 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.
|
|
28
|
+
|
|
29
|
+
- **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](https://bun.sh)) and its own [CHANGELOG](https://github.com/shakfu/py2max/blob/main/js2max/CHANGELOG.md), where the detail lives. The two loadable bundles are committed, so a Max user needs no toolchain either.
|
|
30
|
+
|
|
31
|
+
- **The two packages share one description of the format, not two.** `js2max/src/objects.ts` is generated from py2max's maxref bundle by `scripts/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 under `js2max/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.
|
|
32
|
+
|
|
33
|
+
- `make js2max` builds it, `make js2max-check` typechecks it, runs its 236 tests, and fails if any generated file or committed bundle has drifted from its source. CI runs the latter.
|
|
34
|
+
|
|
35
|
+
- One py2max bug came back the other way: `add_comment` had its port counts backwards, found by serializing a py2max patch out of Max and diffing it against the original. See the `add_comment` entry in this release.
|
|
36
|
+
|
|
37
|
+
### New: the js2max runtime ships in the wheel; `add_v8_bridge()` writes it beside a patch
|
|
38
|
+
|
|
39
|
+
- The built JavaScript now lives at `py2max/data/js2max/` and ships as package data, so **`pip install py2max` is 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.
|
|
40
|
+
|
|
41
|
+
- `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 ordinary `save()` cannot leave a stray `.js` file behind, and `add_v8_bridge(bundle="my.js")` installs nothing -- naming your own file means you placed it. `py2max.js2max_runtime.path()` / `install()` are the direct API.
|
|
42
|
+
|
|
43
|
+
- **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, and `tests/test_js2max_runtime.py` asserts the two agree rather than leaving it as an intention.
|
|
44
|
+
|
|
45
|
+
- 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) and `make js2max-check` fails if they differ, so the shipped runtime cannot lag the repository one.
|
|
46
|
+
|
|
47
|
+
- The single-file edition (`scripts/py2max.py`) cannot carry 160 KB of bundled JavaScript, so `js2max_runtime` is stubbed there as `layout="graph:*"` already is: `add_v8_bridge()` still builds the box, and saving raises `NotImplementedError` naming the full package -- unless you pass `bundle=` for a runtime you placed yourself, which works in both editions.
|
|
48
|
+
|
|
49
|
+
- Documented in [the js2max guide](https://github.com/shakfu/py2max/blob/main/docs/user_guide/js2max.md).
|
|
50
|
+
|
|
51
|
+
### New: box properties are typed -- a misspelled property is now an error, not a silent key
|
|
52
|
+
|
|
53
|
+
- Max box properties reached the emitted `.maxpat` through `**kwds: Any`, so `p.add_textbox("cycle~ 440", bgcolour=[0,0,0,1])` wrote `bgcolour` into the patch and `fontsize="twelve"` wrote a string where Max wants a number. Neither was caught by anything: `mypy --strict` passes on `Any`, and `validate_attrs=True` warns about unknown property *names* at runtime but says nothing about *types* and emits the key regardless.
|
|
54
|
+
|
|
55
|
+
- **`py2max/core/props.py` (generated) defines `BoxProps` / `TextboxProps`**, and `Box.__init__`, `add_textbox`, `add_message` and `add_comment` now accept `**kwds: Unpack[BoxProps]`. Under the `mypy --strict` this 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 explicit `None` for an optional property.
|
|
56
|
+
|
|
57
|
+
- Zero runtime cost and no new dependency: `Unpack` is imported under `if TYPE_CHECKING` with 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 no `typing_extensions` installed). `BoxProps` and `TextboxProps` are exported from the top level for callers annotating their own helpers.
|
|
58
|
+
|
|
59
|
+
- **Two TypedDicts rather than one, deliberately.** A TypedDict key that collides with a named parameter makes mypy report `Overlap between argument names and ** TypedDict items` **and 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. `BoxProps` therefore omits `Box.__init__`'s five structural parameters and `TextboxProps` additionally omits `text`/`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_absent` guards against a regression.
|
|
60
|
+
|
|
61
|
+
- **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 whose `save` meta-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 (`range` arrives as `Union[Sequence[float], Sequence[int], float, int]` and is pinned to `Sequence[Atom]`). Three keys are dropped for not being valid Python identifiers (`one/column`, `one/matrix`, `one/row`).
|
|
62
|
+
|
|
63
|
+
- **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(...)` adds `viewvisibility`: maxref documents `bpatcher` with 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 `.maxpat` fixtures adds `comment`. Both scans are cheap, and they are the only defence against maxref being incomplete, which it demonstrably is.
|
|
64
|
+
|
|
65
|
+
- 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 to `int` (481), `Sequence[float]` (154), `str` (92), `float` (68) and small unions.
|
|
66
|
+
|
|
67
|
+
- **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). So `BoxProps` cannot tell that a property is invalid *for the maxclass it was passed to*: 547 of the 850 belong to a single object, and `p.add_textbox("cycle~ 440", activedialcolor=[1.0, 0.0, 0.0, 1.0])` type-checks cleanly and writes `activedialcolor` into the patch even though only `live.dial` declares it. Misspelled and wrongly-typed properties are rejected; real properties on the wrong object are not. Recorded in `TODO.md`.
|
|
68
|
+
|
|
69
|
+
- `add_floatparam` / `add_intparam` now pass `minimum`/`maximum` through `kwds_filter` instead of forwarding `None`, so an unset bound is absent from the patch rather than present as null.
|
|
70
|
+
|
|
71
|
+
- `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.
|
|
72
|
+
|
|
73
|
+
- Those subprocesses pass `--no-color-output`. mypy honours a `FORCE_COLOR` inherited from the developer's shell, and the assertions match on message text, so without it the tests failed for anyone who has it set -- and `test_the_overlap_trap_is_absent`, which asserts a fragment is *absent*, would instead have passed for the wrong reason.
|
|
74
|
+
|
|
75
|
+
### New: `scripts/py2max.py` is now generated, not hand-maintained
|
|
76
|
+
|
|
77
|
+
- `scripts/py2max.py` -- the single-file edition -- is now produced by `scripts/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, `.amxd` read/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, so `Box.help()` returns a pointer to the full package while `get_info()` still returns structured data). Graph layouts (`layout="graph:*"`), the CLI and the SQLite maxref database are excluded; `layout="graph:*"` raises `NotImplementedError` naming the package to install.
|
|
78
|
+
|
|
79
|
+
- **Why:** the hand-maintained version had drifted five releases behind and carried four real defects, including one that broke *every* edit-after-load (`width` read `self.rect.w`, but a loaded patch keeps `rect` as a plain JSON list) and the operator-precedence bug in `add_coll`/`add_dict`/`add_table` that discarded a caller-supplied `text`. 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.
|
|
80
|
+
|
|
81
|
+
- `tests/test_single_file.py` asserts 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 `--check` and fails if the committed file differs from a fresh build, so a stale copy can no longer rot unnoticed.
|
|
82
|
+
|
|
83
|
+
- 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.
|
|
84
|
+
|
|
85
|
+
### Fixed: `add_comment` had its port counts backwards
|
|
86
|
+
|
|
87
|
+
- A comment box takes a `set` message and emits nothing -- 1 inlet, 0 outlets. `add_comment` passed neither to `Box`, so the constructor's defaults applied and produced 0 inlets and 1 outlet on every comment py2max has ever written.
|
|
88
|
+
|
|
89
|
+
- **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.
|
|
90
|
+
|
|
91
|
+
- Related, in the generated js2max object table: `Box.__init__` defaults `numoutlets` to 1, and for the **73 objects maxref does not state it** that default is simply wrong -- `print` came 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.
|
|
92
|
+
|
|
93
|
+
### Fixed: `to_dict()` returned an empty patcher until something else rendered
|
|
94
|
+
|
|
95
|
+
- `p.to_dict()["patcher"]["boxes"]` was empty on a patcher full of boxes, and stayed empty until `to_json()` or `save()` happened to call `render()` -- after which the *same call on the same object* started returning them. Order-dependent, and silent: a test asserting over `to_dict()` examined an empty patcher and passed for the wrong reason, which is how it was found.
|
|
96
|
+
|
|
97
|
+
- **`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 with `Box.to_dict()`, which has always returned a populated box with no preparation required. `to_json()` no longer renders separately, since `to_dict()` does it.
|
|
98
|
+
|
|
99
|
+
- **`render()` is now idempotent**, which the above requires -- `save_as()` renders for its own log line and `to_dict()` renders again underneath it. `self.boxes` was *appended* to while `self.lines` was rebuilt, so a second render duplicated every box and no line. That path is reachable only through `reset_on_render=False`, which nothing in the repository passes, so the asymmetry had never bitten; it made rendering order-dependent in exactly the way `to_dict()` was.
|
|
100
|
+
|
|
101
|
+
- `tests/test_serialization.py` (11 tests) pins both properties: that `to_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.
|
|
102
|
+
|
|
103
|
+
### Fixed: box port counts -- explicit zeros, and subpatchers that track their contents
|
|
104
|
+
|
|
105
|
+
- `Box.__init__` used `numoutlets or 1` / `numinlets or 0`, so an explicit `numoutlets=0` was 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 use `x if x is not None else default`, so a meaningful 0 survives. (The `numinlets` line was not actually defective -- its default is already 0 -- but is spelled the same way for clarity.)
|
|
106
|
+
|
|
107
|
+
- **A subpatcher box now declares as many ports as it really has.** `inlet` / `outlet` objects 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 -- a `p sub` holding three `outlet` objects but declaring one emitted a patch whose other two outlets could not be connected. `Box.render()` now syncs the counts (and `outlettype`) from the nested patcher's `inlet`/`outlet` objects, reusing the same derivation `lint()` already used to report the discrepancy.
|
|
108
|
+
|
|
109
|
+
- The sync only overrides a dimension it actually counted objects for, because a nested patcher can hold I/O that this cannot interpret: `gen~` and `rnbo~` declare theirs with `in`/`out` objects, 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.
|
|
110
|
+
|
|
111
|
+
- `add_subpatcher` now states its port defaults (1 inlet, 1 outlet) explicitly instead of passing a falsy `0` and relying on `Box.__init__` to promote it -- which is what the previous `numoutlets or 0` did in practice. This keeps `gen~`/`rnbo~` boxes, which are created through this path, exactly as before.
|
|
112
|
+
|
|
113
|
+
- Regression tests in `tests/test_subpatch.py` cover all four cases: explicit zeros, port tracking, empty-subpatcher defaults, and `gen~`/`rnbo~` ports surviving.
|
|
114
|
+
|
|
115
|
+
### Fixed: nulls no longer reach the patch -- `_remove_none_entries` recurses
|
|
116
|
+
|
|
117
|
+
- `Box._remove_none_entries` dropped None-valued keys only at the top level, so anything one level down survived. `add_intparam` writes `parameter_mmax` unconditionally, which meant an unset maximum shipped as `"parameter_mmax": null` inside `saved_attribute_attributes` -- and Max distinguishes an absent key from a null one. (`add_floatparam` omits the key entirely; the asymmetry was the tell, and the method's own `TODO: make recursive` was the same symptom.) Fixed by making the scrub recursive rather than special-casing the key, which closes the class instead of the instance.
|
|
118
|
+
|
|
119
|
+
- Lists are walked but **not** filtered: a None *element* is positional -- an `outlettype` slot, say -- so dropping it would change the arity. Tuples are left alone so `Rect`, a NamedTuple, survives as itself. Loading is unaffected, since `Box.from_dict` bypasses `__init__` entirely.
|
|
120
|
+
|
|
121
|
+
- `tests/test_param.py` now asserts that a representative patch contains no nulls at any depth. It reads back `to_json()` rather than `to_dict()`, because only the former renders the boxes -- asserting over `to_dict()` inspects an empty patcher and passes for the wrong reason. That trap is recorded in `TODO.md`.
|
|
122
|
+
|
|
123
|
+
### Fixed: `add()` raised `TypeError` when a keyword named the target's own parameter
|
|
124
|
+
|
|
125
|
+
- `Patcher.add()` derives the first argument of its target method from the value it was handed (the text tail, the number) and then forwards `**kwds` to the same call, so a caller naming that parameter passed it twice: `p.add("cycle~ 440", text="saw~ 220")`, `p.add(5, initial=3)` and `p.add("coll x", name="y")` all failed with `got multiple values for argument`. An explicit keyword now wins over the derived value.
|
|
126
|
+
|
|
127
|
+
- **This was a family, not a list of cases.** The `_maxclass_methods` branch fills *every* specialized method's first parameter positionally, so `add_coll(name=)`, `add_dict(name=)`, `add_table(name=)`, `add_itable(name=)`, `add_umenu(prefix=)`, `add_bpatcher(name=)`, `add_message(text=)` and `add_comment(text=)` collided as well, alongside `add_textbox(text=)`, `add_subpatcher(text=)`, `add_gen_codebox(code=)`, `add_rnbo(text=)` and `add_floatparam`/`add_intparam`'s `initial=`. A single `_dispatch` helper now applies one rule at every branch, and finds the parameter name by introspection rather than from a table, so adding an entry to `Patcher._maxclass_methods` cannot silently reintroduce the collision. The test drives off that table for the same reason.
|
|
128
|
+
|
|
129
|
+
- **`.add(<number>, name=...)` no longer leaks a stray `name` property into the patch.** The keyword names the *parameter* (it becomes `parameter_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 a `name=` keyword.
|
|
130
|
+
|
|
131
|
+
- **Fixed alongside: `add_umenu()` crashed unless `items` was given** -- `TypeError: object of type 'NoneType' has no len()` -- despite the parameter being optional, which also meant `p.add("umenu")` had never worked. The `cast(List[str], items)` masking the `Optional` from mypy was the tell.
|
|
132
|
+
|
|
133
|
+
### Fixed: `MaxRefDB.search()` treated `%` and `_` as wildcards
|
|
134
|
+
|
|
135
|
+
- The query was interpolated into a `LIKE` pattern unescaped, so SQL metacharacters in a search term were executed rather than matched. `search("%")` returned the entire database (1175 objects), `search("_")` likewise, and `search("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 declares `ESCAPE '\'`.
|
|
136
|
+
|
|
137
|
+
- **`search()` with no recognized field now raises `ValueError`** instead of building `WHERE ORDER BY` and failing inside sqlite with `OperationalError: near "ORDER": syntax error`. The recognized set is exposed as `MaxRefDB.SEARCHABLE_FIELDS`.
|
|
138
|
+
|
|
139
|
+
### Fixed: matrix layout fragmented a signal chain when boxes were added in reverse order
|
|
140
|
+
|
|
141
|
+
- `MatrixLayoutManager` treated 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.
|
|
142
|
+
|
|
143
|
+
- 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.py` cover creation order, parallel chains meeting at a shared sink, all three cycle shapes, and disconnected objects.
|
|
144
|
+
|
|
145
|
+
### Fixed: importing py2max no longer logs, nor hijacks the host's logging
|
|
146
|
+
|
|
147
|
+
- **`import py2max` is now silent.** DEBUG-level logging was previously the shipped default (`log.py`: `DEBUG = getenv("DEBUG", default=True)`), so simply constructing a `Patcher` printed internal diagnostics to the console. Logging is now opt-in.
|
|
148
|
+
|
|
149
|
+
- **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 configured `basicConfig(format="APP: %(message)s")` and then imported py2max silently lost its own format. The `py2max` logger now carries a `NullHandler` and nothing else is touched, which is the standard way for a library to participate in logging without imposing any.
|
|
150
|
+
|
|
151
|
+
- **New `py2max.setup_logging(level=..., color=..., log_file=...)`** opts in to py2max's colored console output. It attaches handlers to the `py2max` logger only, never to root, and is idempotent (repeat calls replace their own handlers rather than stacking duplicates). It deliberately leaves `propagate` alone: disabling it would be a global side effect that silently blinds anything capturing py2max records through an ancestor logger, including pytest's `caplog`.
|
|
152
|
+
|
|
153
|
+
- **Env vars are namespaced and default off:** `PY2MAX_DEBUG=1` (was the bare, extremely common `DEBUG`, which meant any unrelated `DEBUG=1` in the environment turned py2max verbose), plus `PY2MAX_LOG_LEVEL`, `PY2MAX_LOG_FILE` and `PY2MAX_COLOR`. Setting any of the first three enables logging at import; an explicit `PY2MAX_DEBUG=0` stays silent.
|
|
154
|
+
|
|
155
|
+
- **The CLI, being an application rather than a library, now configures logging explicitly** and gained `-v`/`-vv` (INFO/DEBUG) and `-q` flags. It previously got its output purely as a side effect of importing the package.
|
|
156
|
+
|
|
157
|
+
- `config()` is retained as a no-op alias of `get_logger()` for backwards compatibility.
|
|
158
|
+
|
|
159
|
+
- `tests/test_logging.py` covers all of it, using subprocesses for the import-time behaviour that cannot be re-tested in an already-imported module. A `tests/conftest.py` fixture now snapshots and restores the `py2max` logger around every test: `setup_logging()` mutates process-global state, and without isolation a CLI test's configuration made `caplog` blind in a later lint test -- a failure that passed in isolation and only appeared in the full run.
|
|
160
|
+
|
|
161
|
+
### Fixed: the maxref cache no longer prints to stderr
|
|
162
|
+
|
|
163
|
+
- 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 the `py2max` logger, so `py2max.setup_logging("INFO")` shows it and the default stays silent. The CLI still prints -- it is an application, and its output is the product.
|
|
164
|
+
|
|
165
|
+
### Notes for upgraders
|
|
166
|
+
|
|
167
|
+
No API was removed and no call signature changed, but four things behave
|
|
168
|
+
differently enough to mention.
|
|
169
|
+
|
|
170
|
+
- **Every comment box changes shape.** `add_comment` wrote 0 inlets and 1 outlet;
|
|
171
|
+
a comment has 1 and 0. Regenerating a patch that contains comments produces a
|
|
172
|
+
different -- correct -- file, and Max was silently rewriting the values on save
|
|
173
|
+
anyway.
|
|
174
|
+
- **`to_dict()` renders**, so it returns the patcher as it stands rather than as
|
|
175
|
+
it stood after whatever last rendered it. It previously returned an empty
|
|
176
|
+
patcher until something else called `render()`. If you were calling
|
|
177
|
+
`render()` first, you no longer need to; if you were relying on the empty
|
|
178
|
+
result, you were relying on a bug.
|
|
179
|
+
- **`render()` is idempotent.** `reset_on_render=False` no longer accumulates
|
|
180
|
+
boxes across renders. It also never accumulated *lines*, so what it did before
|
|
181
|
+
was not coherent.
|
|
182
|
+
- **`save()` may now write a second file** -- but only for a patcher that called
|
|
183
|
+
`add_v8_bridge()`, which is new in this release. Nothing that worked before
|
|
184
|
+
writes anything extra.
|
|
185
|
+
|
|
186
|
+
Typed box properties are a static change: `mypy --strict` will now reject a
|
|
187
|
+
misspelled or wrongly-typed property that it previously accepted. That is the
|
|
188
|
+
point of them, and nothing changes at runtime.
|
|
189
|
+
|
|
5
190
|
## [0.3.6]
|
|
6
191
|
|
|
7
192
|
### Removed: incremental layout; `optimize_layout()` is batch-only again
|
|
8
193
|
|
|
9
194
|
- `Patcher.optimize_layout()` no longer takes the `changed_objects` parameter 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 the `INCREMENTAL_THRESHOLD` constant (`layout/base.py`); the `optimize_layout(changed_objects)` overrides in `layout/grid.py` and `layout/flow.py` (they now implement `_full_layout` and inherit the batch entry point, with flow's `<2 objects` guard moved into `_full_layout`); and `layout/matrix.py`'s override (now `_full_layout`, with `ColumnarLayoutManager` inheriting it).
|
|
195
|
+
|
|
10
196
|
- **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 (batch `optimize_layout()` always passed `changed_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.
|
|
197
|
+
|
|
11
198
|
- **Breaking:** calling `optimize_layout()` with an argument (e.g. `optimize_layout({obj.id})`) now raises `TypeError`; drop the argument. The `test_optimize_layout_forwards_changed_objects` / `..._incremental_leaves_untouched_objects_fixed` regression tests were replaced by `test_optimize_layout_is_batch_only`.
|
|
12
199
|
|
|
13
200
|
## [0.3.5]
|
|
@@ -25,12 +212,19 @@
|
|
|
25
212
|
### New: Patch linting and message-type-aware connection validation
|
|
26
213
|
|
|
27
214
|
- Added `Patcher.lint()` (and the `py2max.lint` module: `lint()`, `Finding`) -- a patch-level health check returning structured findings with a `severity`, a `code`, 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.
|
|
215
|
+
|
|
28
216
|
- **Linting runs automatically on `save()`**: error-severity findings (bad connections, out-of-range ports, orphaned lines, duplicate IDs) are logged. Pass `Patcher(strict=True)` to raise `InvalidPatchError` on any error instead. Layout warnings (overlaps / off-canvas / unknown objects) are left to an explicit `lint()` or `py2max validate` to keep normal saves quiet. This is on by default and non-breaking -- saves still succeed unless `strict=True`.
|
|
217
|
+
|
|
29
218
|
- 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 into `adsr~` (a legitimate envelope trigger) is *not* flagged.
|
|
219
|
+
|
|
30
220
|
- 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.
|
|
221
|
+
|
|
31
222
|
- `py2max validate` (CLI) now reports the full lint result -- errors and warnings with codes -- and exits non-zero on any error.
|
|
223
|
+
|
|
32
224
|
- A corpus test re-lints every shipped layout example patch and fails on any error, so Max-invalid wiring can no longer ship unnoticed.
|
|
225
|
+
|
|
33
226
|
- Subpatchers are handled: a subpatcher/bpatcher box's inlet/outlet count is derived from the `inlet` / `outlet` objects it contains (not the maxref default), and `lint()` recurses into nested patchers -- findings inside a subpatcher are reported path-qualified (e.g. `sub-box-id/obj-1`).
|
|
227
|
+
|
|
34
228
|
- Inlet acceptance is now derived from each object's `<methodlist>` -- its real message vocabulary in Max's own docs -- rather than the placeholder inlet `type` (Cycling '74 ships `INLET_TYPE`/`OUTLET_TYPE` for control ports, so the type attribute alone is useless). This is what distinguishes a bang into `cycle~` (no `bang` method -> rejected) from a bang into `adsr~` (has an `anything` wildcard method -> allowed), replacing hand-curation with data that generalizes to all ~1050 objects that carry method lists. The shipped `bundle.json.gz` was regenerated so no-Max users get the same data (a `test_bundle_method_data_quality` guard prevents a future regeneration from dropping it).
|
|
35
229
|
|
|
36
230
|
## [0.3.3]
|
|
@@ -42,6 +236,7 @@
|
|
|
42
236
|
### Fixed: Graph layouts (`graph:*`) are overlap-free and open on-screen
|
|
43
237
|
|
|
44
238
|
- `GraphLayoutManager` now 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.
|
|
239
|
+
|
|
45
240
|
- 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).
|
|
46
241
|
|
|
47
242
|
### Fixed: Clustered grid layout squashed object sizes
|
|
@@ -65,42 +260,55 @@
|
|
|
65
260
|
### New: Patch editing and removal API
|
|
66
261
|
|
|
67
262
|
- Loading a patch (`Patcher.from_dict` / `load`) now restores all ID-generation state -- object, node, edge, and semantic-ID counters -- so `add_*` 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.
|
|
263
|
+
|
|
68
264
|
- Added a removal / editing API with referential-integrity cleanup: `remove_line` / `disconnect`, `remove_box` / `remove`, and `replace`. 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.
|
|
69
265
|
|
|
70
266
|
### New: Graph-layout engines as layout managers
|
|
71
267
|
|
|
72
268
|
- 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 via `layout="graph:<algo>"` (e.g. `graph:hola`, `graph:cola`, `graph:ogdf-sugiyama`). Because these algorithms need the whole graph, positions are applied on `optimize_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`.
|
|
269
|
+
|
|
73
270
|
- The engines are lazy-imported inside the manager, so `import py2max` still pulls **zero runtime dependencies**; a missing backend raises a clear error naming the package to install. Install with `pip install "py2max[graph]"` -- the `graph` extra now also bundles `hola-graph` alongside `graph-layout` and `ogdf-py`. Implemented as `GraphLayoutManager` in `py2max/layout/external.py`.
|
|
74
271
|
|
|
75
272
|
### New: Layout gallery generator and docs page
|
|
76
273
|
|
|
77
274
|
- `scripts/gen_layout_gallery.py` (run via `make gallery`) renders every supported graph layout over one shared sample patch to transparent SVGs under `docs/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.
|
|
275
|
+
|
|
78
276
|
- A new published **Layout Gallery** page (`docs/user_guide/layout_gallery.md`, linked in the User Guide navigation) shows the rendered layouts and documents the `graph:<algo>` layout-manager API. The older networkx / graphviz / tsmpy experiments were dropped in favor of the three maintained backends.
|
|
277
|
+
|
|
79
278
|
- 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.
|
|
80
279
|
|
|
81
280
|
### Fixed: Generation correctness
|
|
82
281
|
|
|
83
282
|
- `add_coll` / `add_dict` / `add_table` no longer discard a caller-supplied `text` argument when `name` is `None` (an operator-precedence bug that let the fallback string win).
|
|
283
|
+
|
|
84
284
|
- `add_beap` strips the `.maxpat` suffix correctly; previously `rstrip(".maxpat")` could truncate names such as `drum.maxpat` down to `dru`.
|
|
285
|
+
|
|
85
286
|
- Parallel patchlines between the same source and destination now receive incrementing `order` values, so they spread apart in Max instead of overlapping.
|
|
287
|
+
|
|
86
288
|
- Comments created with `comment=` are emitted on every save path (`save_as`, `to_json`, `to_dict`), not only `save()` / `optimize_layout()`.
|
|
289
|
+
|
|
87
290
|
- 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.
|
|
291
|
+
|
|
88
292
|
- Load/save round-trip no longer injects `autosave` / `dependency_cache` defaults into nested subpatchers the source patch did not have; patches containing subpatchers now round-trip faithfully.
|
|
89
293
|
|
|
90
294
|
### Fixed: Layout managers
|
|
91
295
|
|
|
92
296
|
- `layout="columnar"` now works; it previously raised `NotImplementedError` despite being documented.
|
|
297
|
+
|
|
93
298
|
- 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 by `optimize_layout()`.
|
|
299
|
+
|
|
94
300
|
- 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).
|
|
95
301
|
|
|
96
302
|
### Improved: maxref bundle loading (lazy and thread-safe)
|
|
97
303
|
|
|
98
304
|
- 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.
|
|
305
|
+
|
|
99
306
|
- The process-wide maxref cache is now thread-safe: an `RLock` guards the lazy refdict / category-map initialization and cache population, so concurrent `Box.help()` / validation lookups no longer race on first load or mutate the cache unsafely.
|
|
100
307
|
|
|
101
308
|
### Fixed: maxref outlet digests and parser robustness
|
|
102
309
|
|
|
103
310
|
- 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 offline `bundle.json.gz` was regenerated so bundle-mode users (Linux/Windows/no-Max) get the corrected data.
|
|
311
|
+
|
|
104
312
|
- 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.xml` markup variations across Max versions (valid entities like `&`, `"`, `µ` are left intact).
|
|
105
313
|
|
|
106
314
|
### Improved: Cross-platform maxref discovery
|
|
@@ -110,20 +318,27 @@
|
|
|
110
318
|
### Improved: SVG preview fidelity
|
|
111
319
|
|
|
112
320
|
- The SVG exporter (`Patcher.to_svg` / `py2max preview`) now renders a more faithful preview instead of a uniform grey schematic:
|
|
321
|
+
|
|
113
322
|
- **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.
|
|
323
|
+
|
|
114
324
|
- **UI objects get recognizable affordances** rather than an identical rectangle: message boxes draw the right-edge flag notch, `toggle` an X, `button` a circle, number boxes (`flonum` / `number`) the left triangle marker, `dial` a circle with a pointer, and `slider` a thumb bar.
|
|
325
|
+
|
|
115
326
|
- **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).
|
|
327
|
+
|
|
116
328
|
- `export_svg_string` now builds the document in memory instead of round-tripping through a temporary file.
|
|
117
329
|
|
|
118
330
|
### Fixed: Layout classification and overlap resolution
|
|
119
331
|
|
|
120
332
|
- 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_flow` no-op and its call site were removed.
|
|
333
|
+
|
|
121
334
|
- `LayoutManager.prevent_overlaps` now 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.
|
|
122
335
|
|
|
123
336
|
### Improved: Patch transformers
|
|
124
337
|
|
|
125
338
|
- `run_pipeline` now delegates to `compose`, removing a duplicated apply loop and giving the previously-unused (but exported) `compose` helper a real use.
|
|
339
|
+
|
|
126
340
|
- The `add-comment` transformer's position is now reachable from the CLI: prefix the value with `above|below|left|right:` to place the comment (e.g. `--apply "add-comment=below:tempo"`), defaulting to `above`. A leading token that is not a position is kept as comment text, so `"note: hi"` is left intact.
|
|
341
|
+
|
|
127
342
|
- Added two transformers backed by existing APIs: `apply-theme` (apply a named color theme -- `light` / `dark` / `blue` / `high-contrast`) and `scale-positions` (scale every object's x/y position by a factor, sizes unchanged).
|
|
128
343
|
|
|
129
344
|
### Fixed: Converters module cleanup
|
|
@@ -137,12 +352,15 @@
|
|
|
137
352
|
### Removed: MaxRefDB deprecated alias methods
|
|
138
353
|
|
|
139
354
|
- Removed 12 long-deprecated `MaxRefDB` aliases 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`, and `export_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.
|
|
355
|
+
|
|
140
356
|
- While updating the SQLite demo scripts, also fixed two pre-existing broken examples: `from py2max import MaxRefDB` (correct: `from py2max.maxref import MaxRefDB`) and `create_database(...)` used as a free function (it is `MaxRefDB.create_database(...)`). Both `tests/examples/db/` scripts now run.
|
|
141
357
|
|
|
142
358
|
### Removed: Obsolete layout experiments and vendored editor assets
|
|
143
359
|
|
|
144
360
|
- 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 new `GraphLayoutManager` coverage, and only ever skipped. The maintained path is covered by `tests/test_layout_graph_manager.py`.
|
|
361
|
+
|
|
145
362
|
- Removed the vendored browser libraries under `docs/js/` (SVG.js, WebCola, D3) -- reference copies for the interactive editor that moved to the separate `py2max-server` package in 0.3.0 -- and the stale Sphinx build output under `docs/build/` that predated the MkDocs migration and was being copied into the published site.
|
|
363
|
+
|
|
146
364
|
- 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 to `py2max-server`. The 13 still-relevant library/journal notes were kept.
|
|
147
365
|
|
|
148
366
|
## [0.3.1]
|
|
@@ -150,8 +368,11 @@
|
|
|
150
368
|
### New: Standalone `gen.codebox~` Support
|
|
151
369
|
|
|
152
370
|
- `Patcher.add_gen_codebox(code)` adds a self-contained `gen.codebox~` object -- a complete gen patch in a single box that lives directly in a regular Max patcher, distinct from the inner `codebox~` (emitted by `add_codebox`) that belongs inside a `gen~`/`rnbo~` subpatcher. This is the form emitted by gen transpilers. Code newlines are normalized to CRLF as Max expects, and `fontname`/`fontsize` default to the monospaced gen style.
|
|
371
|
+
|
|
153
372
|
- Inlet/outlet counts are derived automatically from the code (the highest `inN` / `outN` references, floor of 1), matching gen's dynamic-I/O semantics. Explicit `numinlets` / `numoutlets` still override.
|
|
373
|
+
|
|
154
374
|
- 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 to `add_gen_codebox()` directly.
|
|
375
|
+
|
|
155
376
|
- Connection validation for `gen.codebox~` (and `codebox` / `codebox~`) now bound-checks against the box's own declared inlet/outlet counts rather than the static `.maxref.xml` entry, 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.
|
|
156
377
|
|
|
157
378
|
## [0.3.0]
|
|
@@ -161,7 +382,9 @@
|
|
|
161
382
|
The browser-based live editor and remote REPL have moved to a separate companion package, [`py2max-server`](https://github.com/shakfu/py2max-server), so the core library stays small, offline, and dependency-free.
|
|
162
383
|
|
|
163
384
|
- Removed `Patcher.serve()` and the `py2max serve` / `py2max repl` CLI commands; those CLI subcommands now print a pointer to `py2max-server`.
|
|
385
|
+
|
|
164
386
|
- Removed the `[server]` optional-dependency extra (`websockets`, `ptpython`) and the bundled browser assets (`py2max/static/`).
|
|
387
|
+
|
|
165
388
|
- Install the server features with `pip install py2max-server` and use `py2max-server serve <patch>` / `py2max-server repl …`. The remote REPL now requires token authentication (passed via `--token` or `PY2MAX_REPL_TOKEN`).
|
|
166
389
|
|
|
167
390
|
### New: `Patcher.encapsulate()`
|
|
@@ -171,6 +394,7 @@ The browser-based live editor and remote REPL have moved to a separate companion
|
|
|
171
394
|
### New: Preset / `pattrstorage` Scaffolding
|
|
172
395
|
|
|
173
396
|
- `Patcher.add_pattrstorage(name)`, `Patcher.add_autopattr()`, and `Patcher.add_preset_system(name)` (which adds both and wires `autopattr` -> `pattrstorage`) scaffold a Max preset system. Any object with a scripting name (`varname`) or `parameter_enable=1` participates.
|
|
397
|
+
|
|
174
398
|
- `Patcher.enable_parameter(box, longname, shortname="", ptype=0, initial=None)` turns an existing UI box into a Max parameter (sets `parameter_enable` and the `saved_attribute_attributes`), so it participates in presets and, in a Max for Live device, appears as an automatable parameter.
|
|
175
399
|
|
|
176
400
|
### New: Keyword-Attribute Validation (`validate_attrs`)
|
|
@@ -180,6 +404,7 @@ The browser-based live editor and remote REPL have moved to a separate companion
|
|
|
180
404
|
### New: Multichannel (`mc.`) / Polyphony Helpers
|
|
181
405
|
|
|
182
406
|
- `Patcher.add_mc(text, chans=None)` adds a multichannel object, prefixing `mc.` and appending `@chans` (e.g. `add_mc("cycle~ 440", chans=4)` -> `mc.cycle~ 440 @chans 4`).
|
|
407
|
+
|
|
183
408
|
- `Patcher.add_poly(target, voices=1)` adds a `poly~` object hosting N voices of a target patch.
|
|
184
409
|
|
|
185
410
|
### Improved: SVG Export (Max-faithful preview)
|
|
@@ -193,7 +418,9 @@ The browser-based live editor and remote REPL have moved to a separate companion
|
|
|
193
418
|
### New: Color / Theme Helpers
|
|
194
419
|
|
|
195
420
|
- `Box.set_color(bg=..., text=..., border=...)` sets a box's `bgcolor`/`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.
|
|
421
|
+
|
|
196
422
|
- `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 of `bg`/`text`/`border` colors.
|
|
423
|
+
|
|
197
424
|
- `py2max.core.colors` exposes the `MAX_COLORS` named palette and `resolve_color()`.
|
|
198
425
|
|
|
199
426
|
### Security
|
|
@@ -215,14 +442,19 @@ The browser-based live editor and remote REPL have moved to a separate companion
|
|
|
215
442
|
### Fixed
|
|
216
443
|
|
|
217
444
|
- Object-name resolution (used by connection validation and object classification) now reads the box `text` property, so it resolves correctly for boxes loaded from a file. Previously it inspected only programmatic kwargs and returned `newobj` for loaded boxes.
|
|
445
|
+
|
|
218
446
|
- `Box.oid` now returns the trailing numeric part of any id (e.g. `cycle_1` -> 1) instead of raising `ValueError` under `semantic_ids=True`.
|
|
447
|
+
|
|
219
448
|
- The `py2max` CLI now reports all `Py2MaxError`s (not just `InvalidConnectionError`) as a clean error message instead of leaking a traceback.
|
|
449
|
+
|
|
220
450
|
- Fixed an `inital` -> `initial` keyword typo in the simple-synthesis tutorial.
|
|
221
451
|
|
|
222
452
|
### Testing & Tooling
|
|
223
453
|
|
|
224
454
|
- The test suite is now hermetic: a `conftest.py` autouse fixture isolates each test in a temporary working directory, so relative `outputs/` writes no longer accumulate in the repo. Fixture reads are anchored at the test file.
|
|
455
|
+
|
|
225
456
|
- Promoted the `.amxd` byte-for-byte fixtures from the gitignored `outputs/` into tracked `tests/data/`, so that verification runs in CI and on fresh checkouts instead of only on the author's machine.
|
|
457
|
+
|
|
226
458
|
- Repo-wide `ruff` lint and format cleanup.
|
|
227
459
|
|
|
228
460
|
### New: Max for Live Support (`py2max.m4l`)
|
|
@@ -230,20 +462,27 @@ The browser-based live editor and remote REPL have moved to a separate companion
|
|
|
230
462
|
Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/notes/amxd.md`](https://github.com/shakfu/py2max/blob/main/docs/notes/amxd.md) for the on-disk format, embedded-project block, and verification details.
|
|
231
463
|
|
|
232
464
|
- **`.amxd` read/write**: byte-for-byte compatible with Max-exported devices; verified against real fixtures and end-to-end in Live 12.
|
|
465
|
+
|
|
233
466
|
- **Device-type discrimination**: Audio Effect / Instrument / MIDI Effect via `Patcher(device_type=...)` or the `pack_amxd` / `write_amxd` `device_type` argument.
|
|
467
|
+
|
|
234
468
|
- **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).
|
|
469
|
+
|
|
235
470
|
- `Patcher.save()` / `Patcher.from_file()` auto-detect the `.amxd` extension; `.maxpat` path is unchanged.
|
|
236
471
|
|
|
237
472
|
### Changed: M4L Module Layout & Imports
|
|
238
473
|
|
|
239
474
|
- All M4L code (binary format + presentation helpers) lives in a single module `py2max/m4l.py`. Previously briefly split as `py2max/amxd.py`.
|
|
475
|
+
|
|
240
476
|
- M4L symbols are reachable only via `from py2max.m4l import …`; nothing is re-exported from the top-level `py2max` namespace.
|
|
241
477
|
|
|
242
478
|
### New: Prebuilt MaxRef Bundle (Linux Support)
|
|
243
479
|
|
|
244
480
|
- Ship `py2max/maxref/data/bundle.json.gz` in the wheel (1175 objects, ~1 MiB compressed, ~7 MiB raw).
|
|
481
|
+
|
|
245
482
|
- `MaxRefCache._get_refdict()` falls back to the bundle when no local Max installation is found, pre-seeding the parser cache so `Box.help()`, `get_inlet_count`, `get_outlet_count`, and connection validation work identically on Linux.
|
|
483
|
+
|
|
246
484
|
- Regenerate with `uv run python scripts/build_maxref_bundle.py` on a machine with Max installed; commit the result.
|
|
485
|
+
|
|
247
486
|
- Bundle stores full parsed data (methods, attributes, inlets/outlets, digests, descriptions) — not a trimmed subset — so introspection parity with macOS/Windows is preserved.
|
|
248
487
|
|
|
249
488
|
## [0.2.1] - 2026-01-11
|
|
@@ -251,55 +490,81 @@ Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/not
|
|
|
251
490
|
### New: Dagre Layout Algorithm
|
|
252
491
|
|
|
253
492
|
- Added Dagre (Directed Acyclic Graph) as third layout algorithm option alongside WebCola and ELK
|
|
493
|
+
|
|
254
494
|
- Integrated `dagre-bundle.js` combining graphlib with require shim for browser compatibility
|
|
495
|
+
|
|
255
496
|
- Added Dagre-specific controls: Ranker (network-simplex, longest-path, tight-tree) and Align options
|
|
497
|
+
|
|
256
498
|
- Supports all flow directions: top-bottom, bottom-top, left-right, right-left
|
|
257
499
|
|
|
258
500
|
### Improved: Interactive Editor Visualization
|
|
259
501
|
|
|
260
502
|
- **ViewBox Scaling**: Dynamic padding (10% of content, min 30px, max 100px) with aspect ratio preservation
|
|
503
|
+
|
|
261
504
|
- **Port Position Safety**: Added bounds checking with `safeIndex` clamping to prevent invalid port positions
|
|
505
|
+
|
|
262
506
|
- **Patchline Animation**: Added `animatePatchlines()` method for smooth patchline transitions during layout
|
|
507
|
+
|
|
263
508
|
- **Layout Centering**: Added `centerLayout()` helper method - all three algorithms now center content within canvas
|
|
509
|
+
|
|
264
510
|
- **Delta Updates**: Position updates now send only changed box data instead of full patcher state
|
|
511
|
+
|
|
265
512
|
- Added `updateBoxPosition()` for efficient single-box DOM updates
|
|
513
|
+
|
|
266
514
|
- Added `updateConnectedLines()` to update patchlines without full re-render
|
|
515
|
+
|
|
267
516
|
- Significantly reduces bandwidth during drag operations
|
|
268
517
|
|
|
269
518
|
### Improved: FlowLayoutManager
|
|
270
519
|
|
|
271
520
|
- **Line Crossing Minimization**: Added `_minimize_crossings()` method using barycenter heuristic
|
|
521
|
+
|
|
272
522
|
- Objects within each level are reordered based on average position of connected objects in previous level
|
|
523
|
+
|
|
273
524
|
- Reduces visual line crossings for cleaner layouts
|
|
525
|
+
|
|
274
526
|
- **Negative Position Prevention**: Added bounds clamping and auto-scaling when content exceeds available space
|
|
527
|
+
|
|
275
528
|
- **Incremental Layout**: Supports `optimize_layout(changed_objects)` for efficient partial updates
|
|
276
529
|
|
|
277
530
|
### Improved: GridLayoutManager
|
|
278
531
|
|
|
279
532
|
- Fixed integer division to float division for consistent cluster positioning
|
|
533
|
+
|
|
280
534
|
- Now uses consistent float spacing within clusters
|
|
535
|
+
|
|
281
536
|
- **Incremental Layout**: Supports `optimize_layout(changed_objects)` for efficient partial updates
|
|
282
537
|
|
|
283
538
|
### Improved: WebSocket Server Security
|
|
284
539
|
|
|
285
540
|
- **Input Validation**: Added comprehensive schema-based message validation
|
|
541
|
+
|
|
286
542
|
- `MESSAGE_SCHEMAS` defines required fields and types for each message type
|
|
543
|
+
|
|
287
544
|
- `MAX_STRING_LENGTHS` prevents abuse (256 chars for IDs, 10000 for text, 4096 for filepaths)
|
|
545
|
+
|
|
288
546
|
- `COORDINATE_BOUNDS` validates positions (-100000 to 100000)
|
|
547
|
+
|
|
289
548
|
- Checks for control characters in strings
|
|
549
|
+
|
|
290
550
|
- Validates optional fields (outlet/inlet indices 0-255)
|
|
551
|
+
|
|
291
552
|
- Validation errors sent back to client as error messages
|
|
292
553
|
|
|
293
554
|
### New: Save As Dialog
|
|
294
555
|
|
|
295
556
|
- Added `save_as_required` message type when patcher has no filepath
|
|
557
|
+
|
|
296
558
|
- Added `handle_save_as()` handler for saving with specified filepath
|
|
559
|
+
|
|
297
560
|
- Added `showSaveAsDialog()` in JavaScript with filename prompt
|
|
561
|
+
|
|
298
562
|
- Automatically adds `.maxpat` extension if not provided
|
|
299
563
|
|
|
300
564
|
### Fixed: ELK Layout
|
|
301
565
|
|
|
302
566
|
- Fixed "Referenced shape does not exist" errors by validating edges before creating ports
|
|
567
|
+
|
|
303
568
|
- Ports now created based on actual connections, not just declared counts
|
|
304
569
|
|
|
305
570
|
### Fixed: Static File Paths
|
|
@@ -309,12 +574,19 @@ Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/not
|
|
|
309
574
|
### Improved: Base LayoutManager
|
|
310
575
|
|
|
311
576
|
- Added `prevent_overlaps()` method for iterative overlap prevention
|
|
577
|
+
|
|
312
578
|
- **Incremental Layout System**: Added smart layout optimization that only repositions affected objects
|
|
579
|
+
|
|
313
580
|
- `optimize_layout(changed_objects)` accepts optional set of changed object IDs
|
|
581
|
+
|
|
314
582
|
- `should_use_incremental()` determines when to use incremental vs full layout (30% threshold)
|
|
583
|
+
|
|
315
584
|
- `get_affected_objects()` finds changed objects plus their connected neighbors
|
|
585
|
+
|
|
316
586
|
- `_incremental_layout()` repositions only affected objects using spiral search
|
|
587
|
+
|
|
317
588
|
- `_find_non_overlapping_position()` finds nearby positions that don't overlap with fixed objects
|
|
589
|
+
|
|
318
590
|
- `_full_layout()` for complete layout recalculation (subclasses override)
|
|
319
591
|
|
|
320
592
|
## [0.2.0]
|
|
@@ -322,21 +594,29 @@ Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/not
|
|
|
322
594
|
### Updated: Optional Layout Dependencies
|
|
323
595
|
|
|
324
596
|
- Updated `pycola` dependency to `graph-layout` package (<https://github.com/shakfu/graph-layout>)
|
|
597
|
+
|
|
325
598
|
- Renamed test file from `test_layout_pycola.py` to `test_layout_graph_layout.py`
|
|
599
|
+
|
|
326
600
|
- Updated API to use `ColaLayoutAdapter` from `graph_layout` module
|
|
327
601
|
|
|
328
602
|
- Updated `pyhola` dependency to `hola-graph` package (<https://github.com/shakfu/hola-graph>)
|
|
603
|
+
|
|
329
604
|
- Renamed test file from `test_layout_pyhola.py` to `test_layout_hola_graph.py`
|
|
605
|
+
|
|
330
606
|
- Updated imports to use `hola_graph._core` module
|
|
331
607
|
|
|
332
608
|
- Fixed `test_layout_networkx2.py` to properly check for `pygraphviz` dependency
|
|
609
|
+
|
|
333
610
|
- Test now correctly skips when pygraphviz is not installed
|
|
334
611
|
|
|
335
612
|
### Simplified: Optional Dependencies
|
|
336
613
|
|
|
337
614
|
- Consolidated optional dependencies in `pyproject.toml` to single `server` option
|
|
615
|
+
|
|
338
616
|
- Removed `repl` and `all` options
|
|
617
|
+
|
|
339
618
|
- `server` now includes both `websockets` and `ptpython`
|
|
619
|
+
|
|
340
620
|
- Install with: `pip install py2max[server]`
|
|
341
621
|
|
|
342
622
|
### New: Interactive Editor - Advanced Layout with SVG.js, WebCola, and D3.js
|
|
@@ -348,16 +628,25 @@ Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/not
|
|
|
348
628
|
- Added interactive auto-layout controls panel with real-time parameter adjustment
|
|
349
629
|
|
|
350
630
|
- Added 5 adjustable layout parameters via sliders and controls:
|
|
631
|
+
|
|
351
632
|
- **Link Distance** (50-300): Controls spacing between connected objects
|
|
633
|
+
|
|
352
634
|
- **Iterations** (10-200): Controls layout quality and convergence
|
|
635
|
+
|
|
353
636
|
- **Canvas Width** (400-1600): Adjustable layout area width
|
|
637
|
+
|
|
354
638
|
- **Canvas Height** (300-1200): Adjustable layout area height
|
|
639
|
+
|
|
355
640
|
- **Avoid Overlaps** (checkbox): Toggle automatic overlap prevention
|
|
356
641
|
|
|
357
642
|
- Added constraint-based layout system with 4 presets:
|
|
643
|
+
|
|
358
644
|
- **None**: Natural force-directed layout without alignment constraints
|
|
645
|
+
|
|
359
646
|
- **Horizontal Flow**: Aligns objects in horizontal rows (left-to-right signal flow)
|
|
647
|
+
|
|
360
648
|
- **Vertical Flow**: Aligns objects in vertical columns (top-to-bottom signal flow)
|
|
649
|
+
|
|
361
650
|
- **Grid**: Strict grid alignment with both row and column constraints
|
|
362
651
|
|
|
363
652
|
- Added smooth SVG.js animations (500ms ease-in-out) for layout transitions
|
|
@@ -371,50 +660,75 @@ Implements [issue #9](https://github.com/shakfu/py2max/issues/9). See [`docs/not
|
|
|
371
660
|
**SVG.js Implementation:**
|
|
372
661
|
|
|
373
662
|
- Refactored all SVG rendering to use SVG.js declarative API instead of native DOM manipulation
|
|
663
|
+
|
|
374
664
|
- `initializeSVG()`: Creates SVG canvas and layer groups using SVG.js
|
|
665
|
+
|
|
375
666
|
- `createBox()`: Renders boxes with rectangles, text, and clipping paths using SVG.js
|
|
667
|
+
|
|
376
668
|
- `createLine()`: Renders connection lines with hitboxes using SVG.js
|
|
669
|
+
|
|
377
670
|
- `addPorts()`: Renders inlet/outlet circles using SVG.js
|
|
671
|
+
|
|
378
672
|
- `autoLayout()`: Animates box movements using SVG.js transforms
|
|
379
673
|
|
|
380
674
|
**WebCola Integration:**
|
|
381
675
|
|
|
382
676
|
- Force-directed graph layout with configurable parameters
|
|
677
|
+
|
|
383
678
|
- Constraint-based positioning using alignment constraints
|
|
679
|
+
|
|
384
680
|
- Automatic overlap avoidance with adjustable node dimensions
|
|
681
|
+
|
|
385
682
|
- Handles disconnected graph components gracefully
|
|
683
|
+
|
|
386
684
|
- Jaccard link lengths for natural connection spacing
|
|
387
685
|
|
|
388
686
|
**Constraint System:**
|
|
389
687
|
|
|
390
688
|
- Automatic constraint generation based on object proximity (50px threshold)
|
|
689
|
+
|
|
391
690
|
- Alignment constraints for horizontal rows (Y-axis alignment)
|
|
691
|
+
|
|
392
692
|
- Alignment constraints for vertical columns (X-axis alignment)
|
|
693
|
+
|
|
393
694
|
- Grid constraints combining both row and column alignment
|
|
695
|
+
|
|
394
696
|
- Real-time constraint application with visual feedback
|
|
395
697
|
|
|
396
698
|
**Documentation:**
|
|
397
699
|
|
|
398
700
|
- Added comprehensive `docs/LIBRARIES_INTEGRATION.md` (518 lines)
|
|
701
|
+
|
|
399
702
|
- Detailed parameter descriptions and effects
|
|
703
|
+
|
|
400
704
|
- Constraint preset usage examples
|
|
705
|
+
|
|
401
706
|
- Testing procedures and expected behavior
|
|
707
|
+
|
|
402
708
|
- Code examples and API documentation
|
|
709
|
+
|
|
403
710
|
- Performance considerations for different patch sizes
|
|
404
711
|
|
|
405
712
|
**Demo Scripts:**
|
|
406
713
|
|
|
407
714
|
- Added `examples/auto_layout_demo.py`: Complex synthesizer with randomized positions (13 objects, 16 connections)
|
|
715
|
+
|
|
408
716
|
- Hierarchical layout demo: Tree structure with multiple processing layers (12 objects)
|
|
409
717
|
|
|
410
718
|
**Benefits:**
|
|
411
719
|
|
|
412
720
|
- Professional animated transitions for all layout operations
|
|
721
|
+
|
|
413
722
|
- Interactive experimentation with layout parameters
|
|
723
|
+
|
|
414
724
|
- Structured layouts matching typical Max patch patterns
|
|
725
|
+
|
|
415
726
|
- Clean, maintainable SVG.js codebase
|
|
727
|
+
|
|
416
728
|
- Four layout presets for different use cases
|
|
729
|
+
|
|
417
730
|
- Real-time visual feedback
|
|
731
|
+
|
|
418
732
|
- Minimal overhead (234KB total: D3 + SVG.js + WebCola, minified)
|
|
419
733
|
|
|
420
734
|
**Example Usage:**
|
|
@@ -435,44 +749,63 @@ py2max serve outputs/auto_layout_demo.maxpat
|
|
|
435
749
|
### New: Interactive Editor - Nested Patcher Navigation
|
|
436
750
|
|
|
437
751
|
- Added full nested patcher (subpatcher) navigation support in interactive editor
|
|
752
|
+
|
|
438
753
|
- Double-click on subpatcher boxes (blue dashed border) to navigate into them
|
|
754
|
+
|
|
439
755
|
- Navigate back using "Parent" button or ESC key
|
|
756
|
+
|
|
440
757
|
- Breadcrumb navigation displays current location (e.g., "Main / Oscillator / Envelope")
|
|
758
|
+
|
|
441
759
|
- Subpatcher boxes are fully interactive: draggable, connectable, deletable
|
|
760
|
+
|
|
442
761
|
- Visual distinction: subpatcher boxes have blue dashed borders and bold blue text
|
|
762
|
+
|
|
443
763
|
- Event delegation for reliable double-click detection even with dynamic DOM updates
|
|
764
|
+
|
|
444
765
|
- Automatic parent reference restoration when loading patches from files
|
|
445
766
|
|
|
446
767
|
**Server-Side Changes:**
|
|
447
768
|
|
|
448
769
|
- Modified `get_patcher_state_json()` to include `has_subpatcher` flag and `patcher_path` breadcrumb
|
|
770
|
+
|
|
449
771
|
- Added `handle_navigate_to_subpatcher()`, `handle_navigate_to_parent()`, `handle_navigate_to_root()` handlers
|
|
772
|
+
|
|
450
773
|
- Fixed inlet/outlet count detection to use `numinlets`/`numoutlets` attributes from loaded files
|
|
774
|
+
|
|
451
775
|
- Handler now tracks both `root_patcher` (for saving) and `patcher` (current view)
|
|
452
776
|
|
|
453
777
|
**Client-Side Changes:**
|
|
454
778
|
|
|
455
779
|
- Added breadcrumb UI showing patcher hierarchy
|
|
780
|
+
|
|
456
781
|
- Implemented event delegation for double-click handling on dynamically created boxes
|
|
782
|
+
|
|
457
783
|
- Fixed object positioning by flattening `patching_rect` into `x`, `y`, `width`, `height`
|
|
784
|
+
|
|
458
785
|
- CSS styling for subpatcher boxes with distinct visual appearance
|
|
786
|
+
|
|
459
787
|
- ESC key navigation support
|
|
460
788
|
|
|
461
789
|
**Core Changes:**
|
|
462
790
|
|
|
463
791
|
- Modified `Patcher.from_dict()` to set `_parent` references for nested subpatchers when loading from files
|
|
792
|
+
|
|
464
793
|
- Ensures bidirectional parent-child relationships for proper navigation
|
|
465
794
|
|
|
466
795
|
**Tests:**
|
|
467
796
|
|
|
468
797
|
- Added 14 comprehensive tests in `tests/test_nested_patchers.py`
|
|
798
|
+
|
|
469
799
|
- All tests passing (326 passed, 14 skipped)
|
|
470
800
|
|
|
471
801
|
**Demo:**
|
|
472
802
|
|
|
473
803
|
- Added `examples/nested_patcher_demo.py` with three demonstration patches:
|
|
804
|
+
|
|
474
805
|
- Synthesizer with nested envelope subpatcher
|
|
806
|
+
|
|
475
807
|
- Effects chain with parallel subpatchers
|
|
808
|
+
|
|
476
809
|
- Deeply nested hierarchy (6 levels)
|
|
477
810
|
|
|
478
811
|
### New: SVG Preview Feature
|
|
@@ -484,8 +817,11 @@ py2max serve outputs/auto_layout_demo.maxpat
|
|
|
484
817
|
- Added `export_svg()` and `export_svg_string()` functions for programmatic SVG generation
|
|
485
818
|
|
|
486
819
|
- Added SVG rendering for boxes with type-specific styling:
|
|
820
|
+
|
|
487
821
|
- Regular objects: Light gray fill
|
|
822
|
+
|
|
488
823
|
- Comments: Yellow fill (#ffffd0)
|
|
824
|
+
|
|
489
825
|
- Messages: Medium gray fill
|
|
490
826
|
|
|
491
827
|
- Added patchline rendering with correct inlet/outlet connection points
|
|
@@ -550,10 +886,15 @@ svg_content = export_svg_string(p, show_ports=True)
|
|
|
550
886
|
**Benefits:**
|
|
551
887
|
|
|
552
888
|
- No Max installation required for visual validation
|
|
889
|
+
|
|
553
890
|
- High-quality, scalable vector graphics
|
|
891
|
+
|
|
554
892
|
- Works with all py2max layout managers
|
|
893
|
+
|
|
555
894
|
- Perfect for CI/CD, documentation, and version control
|
|
895
|
+
|
|
556
896
|
- Pure Python implementation with no binary dependencies
|
|
897
|
+
|
|
557
898
|
- Viewable in any web browser
|
|
558
899
|
|
|
559
900
|
### New: SQLite Database Support
|
|
@@ -593,24 +934,39 @@ svg_content = export_svg_string(p, show_ports=True)
|
|
|
593
934
|
**Python API Improvements:**
|
|
594
935
|
|
|
595
936
|
- Added Pythonic properties: `.count`, `.categories`, `.objects` for cleaner access
|
|
937
|
+
|
|
596
938
|
- Added magic methods: `len(db)`, `'obj' in db`, `db['obj']`, `repr(db)` for natural Python usage
|
|
939
|
+
|
|
597
940
|
- Added simplified methods: `populate()`, `search()`, `by_category()`, `export()`, `load()` with cleaner naming
|
|
941
|
+
|
|
598
942
|
- Added `summary()` method for database statistics with category breakdown
|
|
943
|
+
|
|
599
944
|
- Maintained full backward compatibility with deprecated methods
|
|
945
|
+
|
|
600
946
|
- All 18 database tests pass
|
|
601
947
|
|
|
602
948
|
**CLI Improvements:**
|
|
603
949
|
|
|
604
950
|
- Added comprehensive `py2max db` subcommand with 7 operations:
|
|
951
|
+
|
|
605
952
|
- `db create` - Create new databases with optional category filtering
|
|
953
|
+
|
|
606
954
|
- `db populate` - Add objects to existing databases
|
|
955
|
+
|
|
607
956
|
- `db info` - Show database information with summary and listing options
|
|
957
|
+
|
|
608
958
|
- `db search` - Search objects by text or category with verbose mode
|
|
959
|
+
|
|
609
960
|
- `db query` - Get detailed object information (JSON, dict, or human-readable)
|
|
961
|
+
|
|
610
962
|
- `db export` - Export database to JSON
|
|
963
|
+
|
|
611
964
|
- `db import` - Import JSON data into database
|
|
965
|
+
|
|
612
966
|
- Updated `convert maxref-to-sqlite` to use MaxRefDB internally
|
|
967
|
+
|
|
613
968
|
- Added 7 new CLI tests covering all db subcommands
|
|
969
|
+
|
|
614
970
|
- All 272 tests pass (258 passed, 14 skipped)
|
|
615
971
|
|
|
616
972
|
**Example Usage:**
|
|
@@ -647,31 +1003,41 @@ py2max db cache clear
|
|
|
647
1003
|
MaxRefDB now automatically creates and populates a cache database on first use:
|
|
648
1004
|
|
|
649
1005
|
- **macOS**: `~/Library/Caches/py2max/maxref.db`
|
|
1006
|
+
|
|
650
1007
|
- **Linux**: `~/.cache/py2max/maxref.db`
|
|
1008
|
+
|
|
651
1009
|
- **Windows**: `~/AppData/Local/py2max/Cache/maxref.db`
|
|
652
1010
|
|
|
653
1011
|
**Benefits:**
|
|
654
1012
|
|
|
655
1013
|
- One-time population of all 1157 Max objects
|
|
1014
|
+
|
|
656
1015
|
- Instant access on subsequent use
|
|
1016
|
+
|
|
657
1017
|
- No manual setup required
|
|
1018
|
+
|
|
658
1019
|
- Platform-appropriate cache location
|
|
659
1020
|
|
|
660
1021
|
**New Static Methods:**
|
|
661
1022
|
|
|
662
1023
|
- `MaxRefDB.get_cache_dir()` - Get platform-specific cache directory
|
|
1024
|
+
|
|
663
1025
|
- `MaxRefDB.get_default_db_path()` - Get default database path
|
|
664
1026
|
|
|
665
1027
|
**Updated API:**
|
|
666
1028
|
|
|
667
1029
|
- `MaxRefDB()` - Now uses cache by default
|
|
1030
|
+
|
|
668
1031
|
- `MaxRefDB(db_path, auto_populate=True)` - Control auto-population
|
|
1032
|
+
|
|
669
1033
|
- `MaxRefDB(':memory:')` - In-memory database (no caching)
|
|
670
1034
|
|
|
671
1035
|
**New CLI Commands:**
|
|
672
1036
|
|
|
673
1037
|
- `py2max db cache location` - Show cache location and status
|
|
1038
|
+
|
|
674
1039
|
- `py2max db cache init` - Manually initialize cache
|
|
1040
|
+
|
|
675
1041
|
- `py2max db cache clear` - Clear cache database
|
|
676
1042
|
|
|
677
1043
|
**Example Usage:**
|