py2max 0.3.1__tar.gz → 0.3.4__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.1 → py2max-0.3.4}/CHANGELOG.md +129 -0
- {py2max-0.3.1 → py2max-0.3.4}/PKG-INFO +8 -4
- {py2max-0.3.1 → py2max-0.3.4}/README.md +3 -3
- {py2max-0.3.1 → py2max-0.3.4}/py2max/__init__.py +5 -1
- {py2max-0.3.1 → py2max-0.3.4}/py2max/cli.py +22 -104
- {py2max-0.3.1 → py2max-0.3.4}/py2max/core/abstract.py +2 -2
- {py2max-0.3.1 → py2max-0.3.4}/py2max/core/factory.py +178 -20
- {py2max-0.3.1 → py2max-0.3.4}/py2max/core/patcher.py +102 -6
- {py2max-0.3.1 → py2max-0.3.4}/py2max/core/serialization.py +30 -6
- {py2max-0.3.1 → py2max-0.3.4}/py2max/export/converters.py +21 -38
- py2max-0.3.4/py2max/export/svg.py +565 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/layout/__init__.py +4 -1
- {py2max-0.3.1 → py2max-0.3.4}/py2max/layout/base.py +190 -93
- py2max-0.3.4/py2max/layout/external.py +238 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/layout/flow.py +12 -24
- py2max-0.3.4/py2max/layout/graph.py +173 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/layout/grid.py +9 -53
- {py2max-0.3.1 → py2max-0.3.4}/py2max/layout/matrix.py +129 -146
- py2max-0.3.4/py2max/lint.py +226 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/maxref/category.py +5 -4
- py2max-0.3.4/py2max/maxref/data/bundle.json.gz +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/maxref/db.py +2 -88
- {py2max-0.3.1 → py2max-0.3.4}/py2max/maxref/parser.py +185 -98
- py2max-0.3.4/py2max/maxref/porttypes.py +321 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/transformers.py +69 -6
- {py2max-0.3.1 → py2max-0.3.4}/py2max/utils.py +17 -1
- {py2max-0.3.1 → py2max-0.3.4}/pyproject.toml +26 -1
- py2max-0.3.4/tests/conftest.py +98 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/auto_layout_demo.py +1 -1
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/db/category_db_demo.py +16 -16
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/db/maxref_db_demo.py +13 -13
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/layout/columnar_layout_examples.py +24 -27
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/layout/matrix_layout_examples.py +11 -14
- py2max-0.3.4/tests/test_abstraction.py +34 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_amxd.py +3 -1
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_attrui.py +37 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_basic.py +4 -3
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_beap.py +8 -1
- py2max-0.3.4/tests/test_bpatcher.py +44 -0
- py2max-0.3.4/tests/test_coll.py +28 -0
- py2max-0.3.4/tests/test_colors.py +44 -0
- py2max-0.3.4/tests/test_comment.py +20 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_db.py +15 -15
- py2max-0.3.4/tests/test_defaults.py +26 -0
- py2max-0.3.4/tests/test_dict.py +15 -0
- py2max-0.3.4/tests/test_edit_operations.py +362 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_error_handling.py +5 -4
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_examples.py +87 -0
- py2max-0.3.4/tests/test_ezdac.py +21 -0
- py2max-0.3.4/tests/test_itable.py +15 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_js.py +14 -2
- py2max-0.3.4/tests/test_kwds_filter.py +49 -0
- py2max-0.3.4/tests/test_layout_box_dims.py +81 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_layout_builtins.py +1 -1
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_layout_coverage.py +33 -23
- py2max-0.3.4/tests/test_layout_graph.py +77 -0
- py2max-0.3.4/tests/test_layout_graph_layout.py +103 -0
- py2max-0.3.4/tests/test_layout_graph_manager.py +127 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_layout_matrix.py +145 -41
- py2max-0.3.4/tests/test_layout_overlaps.py +66 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_layout_vertical.py +21 -0
- py2max-0.3.4/tests/test_linking.py +76 -0
- py2max-0.3.4/tests/test_lint.py +180 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_m4l.py +0 -6
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_maxref_bundle.py +25 -7
- py2max-0.3.4/tests/test_maxref_lazy.py +55 -0
- py2max-0.3.4/tests/test_maxref_parser.py +60 -0
- py2max-0.3.4/tests/test_maxref_refpages.py +85 -0
- py2max-0.3.4/tests/test_mc_cycle.py +19 -0
- py2max-0.3.4/tests/test_message.py +16 -0
- py2max-0.3.4/tests/test_mypatch.py +22 -0
- py2max-0.3.4/tests/test_number_tilde.py +87 -0
- py2max-0.3.4/tests/test_numbers.py +56 -0
- py2max-0.3.4/tests/test_param.py +36 -0
- py2max-0.3.4/tests/test_param_placement.py +148 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_pitched_osc.py +12 -0
- py2max-0.3.4/tests/test_porttypes.py +85 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_pydantic.py +29 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_rnbo.py +78 -1
- py2max-0.3.4/tests/test_rnbo_subpatcher.py +36 -0
- py2max-0.3.4/tests/test_scripting_name.py +18 -0
- py2max-0.3.4/tests/test_subpatch.py +34 -0
- py2max-0.3.4/tests/test_svg_fidelity.py +63 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_table.py +32 -2
- py2max-0.3.4/tests/test_transformers_e4.py +68 -0
- py2max-0.3.4/tests/test_tutorial_simple_synthesis.py +100 -0
- py2max-0.3.4/tests/test_umenu.py +18 -0
- py2max-0.3.4/tests/test_validation.py +67 -0
- py2max-0.3.4/tests/test_varname.py +26 -0
- py2max-0.3.4/tests/test_wheel_bundle.py +76 -0
- py2max-0.3.4/tests/test_zl_group.py +28 -0
- py2max-0.3.1/py2max/export/svg.py +0 -456
- py2max-0.3.1/py2max/maxref/data/bundle.json.gz +0 -0
- py2max-0.3.1/tests/conftest.py +0 -21
- py2max-0.3.1/tests/registry.py +0 -1526
- py2max-0.3.1/tests/scratch.py +0 -80
- py2max-0.3.1/tests/test_abstraction.py +0 -21
- py2max-0.3.1/tests/test_bpatcher.py +0 -20
- py2max-0.3.1/tests/test_coll.py +0 -16
- py2max-0.3.1/tests/test_colors.py +0 -9
- py2max-0.3.1/tests/test_comment.py +0 -8
- py2max-0.3.1/tests/test_defaults.py +0 -11
- py2max-0.3.1/tests/test_dict.py +0 -7
- py2max-0.3.1/tests/test_ezdac.py +0 -10
- py2max-0.3.1/tests/test_itable.py +0 -7
- py2max-0.3.1/tests/test_kwds_filter.py +0 -116
- py2max-0.3.1/tests/test_layout_graph_layout.py +0 -113
- py2max-0.3.1/tests/test_layout_hola1.py +0 -117
- py2max-0.3.1/tests/test_layout_hola2.py +0 -100
- py2max-0.3.1/tests/test_layout_hola3.py +0 -123
- py2max-0.3.1/tests/test_layout_hola_graph.py +0 -124
- py2max-0.3.1/tests/test_layout_networkx1.py +0 -128
- py2max-0.3.1/tests/test_layout_networkx2.py +0 -96
- py2max-0.3.1/tests/test_layout_nx_graphviz.py +0 -96
- py2max-0.3.1/tests/test_layout_nx_orthogonal.py +0 -120
- py2max-0.3.1/tests/test_layout_nx_tsmpy.py +0 -108
- py2max-0.3.1/tests/test_linking.py +0 -40
- py2max-0.3.1/tests/test_mc_cycle.py +0 -7
- py2max-0.3.1/tests/test_message.py +0 -7
- py2max-0.3.1/tests/test_mypatch.py +0 -12
- py2max-0.3.1/tests/test_number_tilde.py +0 -52
- py2max-0.3.1/tests/test_numbers.py +0 -34
- py2max-0.3.1/tests/test_param.py +0 -8
- py2max-0.3.1/tests/test_rnbo_subpatcher.py +0 -19
- py2max-0.3.1/tests/test_scripting_name.py +0 -7
- py2max-0.3.1/tests/test_subpatch.py +0 -17
- py2max-0.3.1/tests/test_tutorial_simple_synthesis.py +0 -44
- py2max-0.3.1/tests/test_umenu.py +0 -11
- py2max-0.3.1/tests/test_varname.py +0 -11
- py2max-0.3.1/tests/test_zl_group.py +0 -11
- {py2max-0.3.1 → py2max-0.3.4}/LICENSE +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/__main__.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/core/__init__.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/core/box.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/core/colors.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/core/common.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/core/patchline.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/exceptions.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/export/__init__.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/log.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/m4l.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/maxref/__init__.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/maxref/legacy.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/py2max/py.typed +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/__init__.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/data/complex.maxpat +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/data/desc.maxpat +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/data/empty.maxpat +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/data/mydevice.amxd +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/data/mydevice2.amxd +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/data/nested.maxpat +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/data/simple.maxpat +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/data/tabular.maxpat +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/data/umenu.maxref.xml +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/README.md +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/advanced/connection_patterns.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/advanced/custom_extensions.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/advanced/data_containers.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/advanced/error_handling.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/advanced/performance_optimization.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/advanced/subpatchers.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/api/box_api_examples.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/api/patcher_api_examples.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/layout/flow_layout_examples.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/layout/grid_layout_examples.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/basic_synth.maxpat +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/basic_synth.svg +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/complex_synth.maxpat +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/complex_synth.svg +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/flow_layout.maxpat +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/flow_layout.svg +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/grid_layout.maxpat +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/grid_layout.svg +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/horizontal_layout.maxpat +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/horizontal_layout.svg +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/styled_no_ports.svg +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/styled_no_title.svg +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/styled_patch.maxpat +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/styled_with_ports.svg +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/svg_preview_demo.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/vertical_layout.maxpat +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/vertical_layout.svg +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/workflow_demo.maxpat +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/workflow_demo.svg +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/quickstart/basic_patch.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/quickstart/layout_examples.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/tutorial/generative_music.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/tutorial/interactive_controller.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/tutorial/signal_processing_chain.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/examples/tutorial/simple_synthesis.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/graphs/random/v30e33.tglf +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_abstract_coverage.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_add.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_cli.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_colors_theme.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_connection_validation.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_converters.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_core_coverage.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_encapsulate.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_gen.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_group.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_layout.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_layout_flow.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_maxref.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_mc_poly.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_nested.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_nested_patchers.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_patcher.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_presets.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_search.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_semantic_ids.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_svg.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_transformers.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_tree.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_tree_builder.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_two_sines.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_utils.py +0 -0
- {py2max-0.3.1 → py2max-0.3.4}/tests/test_validate_attrs.py +0 -0
|
@@ -2,6 +2,135 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.4]
|
|
6
|
+
|
|
7
|
+
### New: Param docking in layouts
|
|
8
|
+
|
|
9
|
+
- `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. During `optimize_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 as `LayoutManager.place_params()`.
|
|
10
|
+
|
|
11
|
+
### New: Patch linting and message-type-aware connection validation
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
- **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`.
|
|
15
|
+
- 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.
|
|
16
|
+
- 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.
|
|
17
|
+
- `py2max validate` (CLI) now reports the full lint result -- errors and warnings with codes -- and exits non-zero on any error.
|
|
18
|
+
- A corpus test re-lints every shipped layout example patch and fails on any error, so Max-invalid wiring can no longer ship unnoticed.
|
|
19
|
+
- 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`).
|
|
20
|
+
- 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).
|
|
21
|
+
|
|
22
|
+
## [0.3.3]
|
|
23
|
+
|
|
24
|
+
### Removed: `serve` and `repl` CLI subcommands
|
|
25
|
+
|
|
26
|
+
- The `py2max serve` and `py2max repl` subcommands -- thin stubs that only pointed at the separate `py2max-server` package since v0.3.0 -- have been removed. The interactive live editor and remote REPL still live in `py2max-server` (`pip install py2max-server`).
|
|
27
|
+
|
|
28
|
+
### Fixed: Graph layouts (`graph:*`) are overlap-free and open on-screen
|
|
29
|
+
|
|
30
|
+
- `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.
|
|
31
|
+
- 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).
|
|
32
|
+
|
|
33
|
+
### Fixed: Clustered grid layout squashed object sizes
|
|
34
|
+
|
|
35
|
+
- 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.
|
|
36
|
+
|
|
37
|
+
### Fixed: Example patches use valid Max connections
|
|
38
|
+
|
|
39
|
+
- 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, `metro` triggers envelopes, and envelopes modulate amplitude through a `*~` VCA -- so the generated demo patches load cleanly.
|
|
40
|
+
|
|
41
|
+
### New: `kwds_filter` utility
|
|
42
|
+
|
|
43
|
+
- Added `py2max.utils.kwds_filter(kwds, **elems)`: returns `kwds` merged with the `elems` whose value is not `None` (legitimate falsy values such as `0` / `""` are kept, and the input is not mutated). Lets a method keep an optional parameter in its signature but omit it from the forwarded `**kwds` when the caller leaves it unset.
|
|
44
|
+
|
|
45
|
+
### Improved: Build and test-output hygiene
|
|
46
|
+
|
|
47
|
+
- Coverage HTML now writes to `build/coverage-html` and the test suite writes its artifacts under `build/test-output/` -- both within the git-ignored `build/` tree -- instead of a tracked `outputs/` directory. A new `make test-outputs` target writes all test artifacts flat into `build/test-outputs/` for quick inspection.
|
|
48
|
+
|
|
49
|
+
## [0.3.2]
|
|
50
|
+
|
|
51
|
+
### New: Patch editing and removal API
|
|
52
|
+
|
|
53
|
+
- 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.
|
|
54
|
+
- 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.
|
|
55
|
+
|
|
56
|
+
### New: Graph-layout engines as layout managers
|
|
57
|
+
|
|
58
|
+
- 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`.
|
|
59
|
+
- 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`.
|
|
60
|
+
|
|
61
|
+
### New: Layout gallery generator and docs page
|
|
62
|
+
|
|
63
|
+
- `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.
|
|
64
|
+
- 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.
|
|
65
|
+
- 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.
|
|
66
|
+
|
|
67
|
+
### Fixed: Generation correctness
|
|
68
|
+
|
|
69
|
+
- `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).
|
|
70
|
+
- `add_beap` strips the `.maxpat` suffix correctly; previously `rstrip(".maxpat")` could truncate names such as `drum.maxpat` down to `dru`.
|
|
71
|
+
- Parallel patchlines between the same source and destination now receive incrementing `order` values, so they spread apart in Max instead of overlapping.
|
|
72
|
+
- Comments created with `comment=` are emitted on every save path (`save_as`, `to_json`, `to_dict`), not only `save()` / `optimize_layout()`.
|
|
73
|
+
- 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.
|
|
74
|
+
- 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.
|
|
75
|
+
|
|
76
|
+
### Fixed: Layout managers
|
|
77
|
+
|
|
78
|
+
- `layout="columnar"` now works; it previously raised `NotImplementedError` despite being documented.
|
|
79
|
+
- 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()`.
|
|
80
|
+
- 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).
|
|
81
|
+
|
|
82
|
+
### Improved: maxref bundle loading (lazy and thread-safe)
|
|
83
|
+
|
|
84
|
+
- 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.
|
|
85
|
+
- 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.
|
|
86
|
+
|
|
87
|
+
### Fixed: maxref outlet digests and parser robustness
|
|
88
|
+
|
|
89
|
+
- 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.
|
|
90
|
+
- 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).
|
|
91
|
+
|
|
92
|
+
### Improved: Cross-platform maxref discovery
|
|
93
|
+
|
|
94
|
+
- Reference-page resolution now honors the `PY2MAX_MAX_REFPAGES` environment override on all platforms and auto-discovers a Windows `refpages` directory, 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 offline `bundle.json.gz` actually ships in the built artifact.
|
|
95
|
+
|
|
96
|
+
### Improved: SVG preview fidelity
|
|
97
|
+
|
|
98
|
+
- The SVG exporter (`Patcher.to_svg` / `py2max preview`) now renders a more faithful preview instead of a uniform grey schematic:
|
|
99
|
+
- **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.
|
|
100
|
+
- **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.
|
|
101
|
+
- **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).
|
|
102
|
+
- `export_svg_string` now builds the document in memory instead of round-tripping through a temporary file.
|
|
103
|
+
|
|
104
|
+
### Fixed: Layout classification and overlap resolution
|
|
105
|
+
|
|
106
|
+
- 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.
|
|
107
|
+
- `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.
|
|
108
|
+
|
|
109
|
+
### Improved: Patch transformers
|
|
110
|
+
|
|
111
|
+
- `run_pipeline` now delegates to `compose`, removing a duplicated apply loop and giving the previously-unused (but exported) `compose` helper a real use.
|
|
112
|
+
- 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.
|
|
113
|
+
- 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).
|
|
114
|
+
|
|
115
|
+
### Fixed: Converters module cleanup
|
|
116
|
+
|
|
117
|
+
- Importing `py2max.export.converters` no longer constructs a `Patcher` as 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_category` helper and an unreachable `NotImplementedError` guard (the caller strips subpatcher dicts before that path).
|
|
118
|
+
|
|
119
|
+
### Removed: Honest CLI help for the moved `serve` / `repl` subcommands
|
|
120
|
+
|
|
121
|
+
- The `serve` and `repl` subcommands (whose implementations moved to `py2max-server` in 0.3.0) no longer advertise themselves as live in `--help`; they now removed.
|
|
122
|
+
|
|
123
|
+
### Removed: MaxRefDB deprecated alias methods
|
|
124
|
+
|
|
125
|
+
- 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.
|
|
126
|
+
- 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.
|
|
127
|
+
|
|
128
|
+
### Removed: Obsolete layout experiments and vendored editor assets
|
|
129
|
+
|
|
130
|
+
- 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`.
|
|
131
|
+
- 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.
|
|
132
|
+
- 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.
|
|
133
|
+
|
|
5
134
|
## [0.3.1]
|
|
6
135
|
|
|
7
136
|
### New: Standalone `gen.codebox~` Support
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py2max
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.4
|
|
4
4
|
Summary: A library for offline generation of Max/MSP patcher (.maxpat) files.
|
|
5
5
|
Keywords: Max,maxpat,offline
|
|
6
6
|
Author: Shakeeb Alireza
|
|
@@ -21,6 +21,9 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.14
|
|
22
22
|
Classifier: Operating System :: OS Independent
|
|
23
23
|
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Dist: graph-layout>=0.4 ; extra == 'graph'
|
|
25
|
+
Requires-Dist: ogdf-py>=0.3 ; python_full_version >= '3.10' and extra == 'graph'
|
|
26
|
+
Requires-Dist: hola-graph>=0.1 ; extra == 'graph'
|
|
24
27
|
Maintainer: Shakeeb Alireza
|
|
25
28
|
Maintainer-email: Shakeeb Alireza <shakfu@users.noreply.github.com>
|
|
26
29
|
Requires-Python: >=3.9
|
|
@@ -29,6 +32,7 @@ Project-URL: Documentation, https://github.com/shakfu/py2max#readme
|
|
|
29
32
|
Project-URL: Repository, https://github.com/shakfu/py2max.git
|
|
30
33
|
Project-URL: Issues, https://github.com/shakfu/py2max/issues
|
|
31
34
|
Project-URL: Changelog, https://github.com/shakfu/py2max/blob/main/CHANGELOG.md
|
|
35
|
+
Provides-Extra: graph
|
|
32
36
|
Description-Content-Type: text/markdown
|
|
33
37
|
|
|
34
38
|
# py2max
|
|
@@ -172,7 +176,7 @@ p.save()
|
|
|
172
176
|
|
|
173
177
|
### MaxRef Integration
|
|
174
178
|
|
|
175
|
-
Access documentation for
|
|
179
|
+
Access documentation for 1175 Max objects:
|
|
176
180
|
|
|
177
181
|
```python
|
|
178
182
|
p = Patcher('demo.maxpat')
|
|
@@ -216,10 +220,10 @@ osc = p.find_by_id('cycle_1')
|
|
|
216
220
|
Query Max object metadata efficiently:
|
|
217
221
|
|
|
218
222
|
```python
|
|
219
|
-
from py2max import MaxRefDB
|
|
223
|
+
from py2max.maxref import MaxRefDB
|
|
220
224
|
|
|
221
225
|
db = MaxRefDB() # Auto-cached on first use
|
|
222
|
-
print(len(db)) #
|
|
226
|
+
print(len(db)) # 1175 objects
|
|
223
227
|
|
|
224
228
|
if 'cycle~' in db:
|
|
225
229
|
info = db['cycle~']
|
|
@@ -139,7 +139,7 @@ p.save()
|
|
|
139
139
|
|
|
140
140
|
### MaxRef Integration
|
|
141
141
|
|
|
142
|
-
Access documentation for
|
|
142
|
+
Access documentation for 1175 Max objects:
|
|
143
143
|
|
|
144
144
|
```python
|
|
145
145
|
p = Patcher('demo.maxpat')
|
|
@@ -183,10 +183,10 @@ osc = p.find_by_id('cycle_1')
|
|
|
183
183
|
Query Max object metadata efficiently:
|
|
184
184
|
|
|
185
185
|
```python
|
|
186
|
-
from py2max import MaxRefDB
|
|
186
|
+
from py2max.maxref import MaxRefDB
|
|
187
187
|
|
|
188
188
|
db = MaxRefDB() # Auto-cached on first use
|
|
189
|
-
print(len(db)) #
|
|
189
|
+
print(len(db)) # 1175 objects
|
|
190
190
|
|
|
191
191
|
if 'cycle~' in db:
|
|
192
192
|
info = db['cycle~']
|
|
@@ -35,7 +35,7 @@ Example:
|
|
|
35
35
|
>>> p.save()
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
|
-
__version__ = "0.3.
|
|
38
|
+
__version__ = "0.3.4"
|
|
39
39
|
|
|
40
40
|
from .core import Box, Patcher, Patchline
|
|
41
41
|
from .exceptions import (
|
|
@@ -48,6 +48,7 @@ from .exceptions import (
|
|
|
48
48
|
PatcherIOError,
|
|
49
49
|
Py2MaxError,
|
|
50
50
|
)
|
|
51
|
+
from .lint import Finding, lint
|
|
51
52
|
|
|
52
53
|
__all__ = [
|
|
53
54
|
# Version
|
|
@@ -56,6 +57,9 @@ __all__ = [
|
|
|
56
57
|
"Patcher",
|
|
57
58
|
"Box",
|
|
58
59
|
"Patchline",
|
|
60
|
+
# Linting
|
|
61
|
+
"lint",
|
|
62
|
+
"Finding",
|
|
59
63
|
# Exceptions
|
|
60
64
|
"Py2MaxError",
|
|
61
65
|
"InvalidConnectionError",
|
|
@@ -15,12 +15,12 @@ try: # pragma: no cover - optional dependency
|
|
|
15
15
|
except Exception: # pragma: no cover - optional dependency
|
|
16
16
|
yaml = None
|
|
17
17
|
|
|
18
|
-
from .core import Patcher
|
|
18
|
+
from .core import Patcher
|
|
19
19
|
from .core.common import Rect
|
|
20
20
|
from .exceptions import Py2MaxError
|
|
21
21
|
from .export import export_svg
|
|
22
22
|
from .export.converters import maxpat_to_python, maxref_to_sqlite
|
|
23
|
-
from .maxref import MaxRefCache, MaxRefDB
|
|
23
|
+
from .maxref import MaxRefCache, MaxRefDB
|
|
24
24
|
from .transformers import available_transformers, create_transformer, run_pipeline
|
|
25
25
|
|
|
26
26
|
LAYOUT_CHOICES = ["horizontal", "vertical", "grid", "flow", "matrix"]
|
|
@@ -148,6 +148,7 @@ def _generate_test_source(name: str, data: Dict[str, Any]) -> str:
|
|
|
148
148
|
|
|
149
149
|
|
|
150
150
|
def cmd_new(args: argparse.Namespace) -> int:
|
|
151
|
+
"""Create a new, empty patcher file at the given path."""
|
|
151
152
|
path = Path(args.path)
|
|
152
153
|
if path.exists() and not args.force:
|
|
153
154
|
print(f"Refusing to overwrite existing file: {path}", file=sys.stderr)
|
|
@@ -179,6 +180,7 @@ def cmd_new(args: argparse.Namespace) -> int:
|
|
|
179
180
|
|
|
180
181
|
|
|
181
182
|
def cmd_info(args: argparse.Namespace) -> int:
|
|
183
|
+
"""Print a summary of a patcher file (object and connection counts)."""
|
|
182
184
|
path = Path(args.path)
|
|
183
185
|
patcher = Patcher.from_file(path)
|
|
184
186
|
_coerce_rect(patcher)
|
|
@@ -201,6 +203,7 @@ def cmd_info(args: argparse.Namespace) -> int:
|
|
|
201
203
|
|
|
202
204
|
|
|
203
205
|
def cmd_optimize(args: argparse.Namespace) -> int:
|
|
206
|
+
"""Re-run layout optimization on a patcher and write the result."""
|
|
204
207
|
input_path = Path(args.input)
|
|
205
208
|
save_path = Path(args.output) if args.output else input_path
|
|
206
209
|
|
|
@@ -222,44 +225,23 @@ def cmd_optimize(args: argparse.Namespace) -> int:
|
|
|
222
225
|
|
|
223
226
|
|
|
224
227
|
def cmd_validate(args: argparse.Namespace) -> int:
|
|
228
|
+
"""Validate a patcher's connections against maxref inlet/outlet metadata."""
|
|
225
229
|
path = Path(args.path)
|
|
226
230
|
patcher = Patcher.from_file(path)
|
|
227
231
|
_coerce_rect(patcher)
|
|
228
232
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
src_id, dst_id = patchline.src, patchline.dst
|
|
234
|
-
src_port = int(patchline.source[1]) if len(patchline.source) > 1 else 0
|
|
235
|
-
dst_port = (
|
|
236
|
-
int(patchline.destination[1]) if len(patchline.destination) > 1 else 0
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
src_obj = patcher._objects.get(src_id)
|
|
240
|
-
dst_obj = patcher._objects.get(dst_id)
|
|
241
|
-
|
|
242
|
-
if not src_obj or not dst_obj:
|
|
243
|
-
errors.append(f"Dangling connection: {src_id} -> {dst_id}")
|
|
244
|
-
continue
|
|
245
|
-
|
|
246
|
-
src_name = _object_name(src_obj)
|
|
247
|
-
dst_name = _object_name(dst_obj)
|
|
248
|
-
|
|
249
|
-
is_valid, message = validate_connection(src_name, src_port, dst_name, dst_port)
|
|
250
|
-
if not is_valid:
|
|
251
|
-
errors.append(
|
|
252
|
-
f"Invalid connection {src_name}[{src_port}] -> {dst_name}[{dst_port}]: {message}"
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
if errors:
|
|
256
|
-
print("Connection validation failed:", file=sys.stderr)
|
|
257
|
-
for error in errors:
|
|
258
|
-
print(f" - {error}", file=sys.stderr)
|
|
259
|
-
return 1
|
|
233
|
+
findings = patcher.lint()
|
|
234
|
+
if not findings:
|
|
235
|
+
print("Patch is clean: no lint findings.")
|
|
236
|
+
return 0
|
|
260
237
|
|
|
261
|
-
|
|
262
|
-
|
|
238
|
+
errors = [f for f in findings if f.severity == "error"]
|
|
239
|
+
warnings = [f for f in findings if f.severity == "warning"]
|
|
240
|
+
stream = sys.stderr if errors else sys.stdout
|
|
241
|
+
for finding in findings:
|
|
242
|
+
print(f" {finding}", file=stream)
|
|
243
|
+
print(f"{len(errors)} error(s), {len(warnings)} warning(s).", file=stream)
|
|
244
|
+
return 1 if errors else 0
|
|
263
245
|
|
|
264
246
|
|
|
265
247
|
def _parse_transform_spec(spec: str) -> tuple[str, str | None]:
|
|
@@ -272,6 +254,7 @@ def _parse_transform_spec(spec: str) -> tuple[str, str | None]:
|
|
|
272
254
|
|
|
273
255
|
|
|
274
256
|
def cmd_transform(args: argparse.Namespace) -> int:
|
|
257
|
+
"""Apply one or more transformers to a patcher (or list available ones)."""
|
|
275
258
|
if args.list_transformers:
|
|
276
259
|
for name, desc in available_transformers().items():
|
|
277
260
|
print(f"{name}: {desc}")
|
|
@@ -316,6 +299,7 @@ def cmd_transform(args: argparse.Namespace) -> int:
|
|
|
316
299
|
|
|
317
300
|
|
|
318
301
|
def cmd_convert(args: argparse.Namespace) -> int:
|
|
302
|
+
"""Convert between formats (e.g. .maxpat to Python, maxref to SQLite)."""
|
|
319
303
|
if args.mode == "maxpat-to-python":
|
|
320
304
|
if not args.input or not args.output:
|
|
321
305
|
print(
|
|
@@ -636,19 +620,6 @@ def cmd_db_cache(args: argparse.Namespace) -> int:
|
|
|
636
620
|
return 1
|
|
637
621
|
|
|
638
622
|
|
|
639
|
-
def cmd_serve(args: argparse.Namespace) -> int:
|
|
640
|
-
"""The interactive server moved to the separate py2max-server package."""
|
|
641
|
-
target = getattr(args, "input", None) or "<patch.maxpat>"
|
|
642
|
-
print(
|
|
643
|
-
"The interactive server has moved to the separate 'py2max-server' package.\n"
|
|
644
|
-
"Install it and use its CLI instead:\n"
|
|
645
|
-
" pip install py2max-server\n"
|
|
646
|
-
f" py2max-server serve {target}",
|
|
647
|
-
file=sys.stderr,
|
|
648
|
-
)
|
|
649
|
-
return 1
|
|
650
|
-
|
|
651
|
-
|
|
652
623
|
def cmd_preview(args: argparse.Namespace) -> int:
|
|
653
624
|
"""Generate SVG preview of a patcher."""
|
|
654
625
|
import tempfile
|
|
@@ -695,20 +666,8 @@ def cmd_preview(args: argparse.Namespace) -> int:
|
|
|
695
666
|
return 1
|
|
696
667
|
|
|
697
668
|
|
|
698
|
-
def cmd_repl(args: argparse.Namespace) -> int:
|
|
699
|
-
"""The remote REPL moved to the separate py2max-server package."""
|
|
700
|
-
target = getattr(args, "server", None) or "localhost:9000"
|
|
701
|
-
print(
|
|
702
|
-
"The remote REPL has moved to the separate 'py2max-server' package.\n"
|
|
703
|
-
"Install it and use its CLI instead:\n"
|
|
704
|
-
" pip install py2max-server\n"
|
|
705
|
-
f" py2max-server repl {target}",
|
|
706
|
-
file=sys.stderr,
|
|
707
|
-
)
|
|
708
|
-
return 1
|
|
709
|
-
|
|
710
|
-
|
|
711
669
|
def cmd_maxref(args: argparse.Namespace) -> int:
|
|
670
|
+
"""Query the Max object reference (list objects or show one's details)."""
|
|
712
671
|
cache = MaxRefCache()
|
|
713
672
|
|
|
714
673
|
if args.list:
|
|
@@ -783,6 +742,7 @@ def cmd_maxref(args: argparse.Namespace) -> int:
|
|
|
783
742
|
|
|
784
743
|
|
|
785
744
|
def build_parser() -> argparse.ArgumentParser:
|
|
745
|
+
"""Construct the top-level argument parser with all subcommands."""
|
|
786
746
|
parser = argparse.ArgumentParser(
|
|
787
747
|
prog="py2max", description="Utilities for working with Max patchers."
|
|
788
748
|
)
|
|
@@ -836,33 +796,6 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
836
796
|
val_parser.add_argument("path", help="Target .maxpat path")
|
|
837
797
|
val_parser.set_defaults(func=cmd_validate)
|
|
838
798
|
|
|
839
|
-
serve_parser = subparsers.add_parser(
|
|
840
|
-
"serve", help="Start interactive server with live preview"
|
|
841
|
-
)
|
|
842
|
-
serve_parser.add_argument("input", help="Input .maxpat file")
|
|
843
|
-
serve_parser.add_argument(
|
|
844
|
-
"--port",
|
|
845
|
-
type=int,
|
|
846
|
-
default=8000,
|
|
847
|
-
help="HTTP server port (default: 8000, WebSocket on port+1)",
|
|
848
|
-
)
|
|
849
|
-
serve_parser.add_argument(
|
|
850
|
-
"--no-open", action="store_true", help="Don't automatically open browser"
|
|
851
|
-
)
|
|
852
|
-
serve_parser.add_argument(
|
|
853
|
-
"--no-save", action="store_true", help="Disable auto-save on changes"
|
|
854
|
-
)
|
|
855
|
-
serve_parser.add_argument(
|
|
856
|
-
"--repl",
|
|
857
|
-
action="store_true",
|
|
858
|
-
help="Start interactive REPL for live patch editing",
|
|
859
|
-
)
|
|
860
|
-
serve_parser.add_argument(
|
|
861
|
-
"--log-file",
|
|
862
|
-
help="Redirect server logs to file (enables single-terminal REPL mode when used with --repl)",
|
|
863
|
-
)
|
|
864
|
-
serve_parser.set_defaults(func=cmd_serve)
|
|
865
|
-
|
|
866
799
|
preview_parser = subparsers.add_parser(
|
|
867
800
|
"preview", help="Generate SVG preview of a patcher"
|
|
868
801
|
)
|
|
@@ -1094,26 +1027,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1094
1027
|
)
|
|
1095
1028
|
cache_clear.set_defaults(func=cmd_db)
|
|
1096
1029
|
|
|
1097
|
-
# REPL client command
|
|
1098
|
-
repl_parser = subparsers.add_parser("repl", help="Connect to remote REPL server")
|
|
1099
|
-
repl_parser.add_argument(
|
|
1100
|
-
"server",
|
|
1101
|
-
nargs="?",
|
|
1102
|
-
default="localhost:9000",
|
|
1103
|
-
help="Server address (default: localhost:9000)",
|
|
1104
|
-
)
|
|
1105
|
-
repl_parser.add_argument(
|
|
1106
|
-
"--token",
|
|
1107
|
-
default=None,
|
|
1108
|
-
help="Session token printed by the server "
|
|
1109
|
-
"(or set PY2MAX_REPL_TOKEN). Required for authentication.",
|
|
1110
|
-
)
|
|
1111
|
-
repl_parser.set_defaults(func=cmd_repl)
|
|
1112
|
-
|
|
1113
1030
|
return parser
|
|
1114
1031
|
|
|
1115
1032
|
|
|
1116
1033
|
def main(argv: List[str] | None = None) -> int:
|
|
1034
|
+
"""CLI entry point: parse ``argv`` and dispatch to the subcommand handler."""
|
|
1117
1035
|
parser = build_parser()
|
|
1118
1036
|
args = parser.parse_args(argv)
|
|
1119
1037
|
|
|
@@ -60,6 +60,8 @@ class AbstractBox(ABC):
|
|
|
60
60
|
id: Optional[str]
|
|
61
61
|
maxclass: str
|
|
62
62
|
patching_rect: Rect
|
|
63
|
+
numinlets: int
|
|
64
|
+
numoutlets: int
|
|
63
65
|
_kwds: Dict[str, Any]
|
|
64
66
|
|
|
65
67
|
@abstractmethod
|
|
@@ -135,8 +137,6 @@ class AbstractPatcher(ABC):
|
|
|
135
137
|
_lines: list[AbstractPatchline]
|
|
136
138
|
_edge_ids: list[tuple[str, str]]
|
|
137
139
|
_id_counter: int = 0
|
|
138
|
-
_link_counter: int = 0
|
|
139
|
-
_last_link: Optional[tuple[str, str]]
|
|
140
140
|
_reset_on_render: bool
|
|
141
141
|
_flow_direction: str
|
|
142
142
|
_cluster_connected: bool
|