py2max 0.2.1__tar.gz → 0.3.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.2.1 → py2max-0.3.0}/CHANGELOG.md +92 -0
- {py2max-0.2.1 → py2max-0.3.0}/PKG-INFO +32 -21
- {py2max-0.2.1 → py2max-0.3.0}/README.md +26 -13
- {py2max-0.2.1 → py2max-0.3.0}/py2max/__init__.py +5 -4
- {py2max-0.2.1 → py2max-0.3.0}/py2max/cli.py +38 -158
- {py2max-0.2.1 → py2max-0.3.0}/py2max/core/abstract.py +33 -5
- {py2max-0.2.1 → py2max-0.3.0}/py2max/core/box.py +80 -29
- py2max-0.3.0/py2max/core/colors.py +83 -0
- py2max-0.2.1/py2max/core/patcher.py → py2max-0.3.0/py2max/core/factory.py +423 -695
- py2max-0.3.0/py2max/core/patcher.py +596 -0
- {py2max-0.2.1 → py2max-0.3.0}/py2max/core/patchline.py +13 -10
- py2max-0.3.0/py2max/core/serialization.py +122 -0
- {py2max-0.2.1 → py2max-0.3.0}/py2max/export/converters.py +11 -8
- {py2max-0.2.1 → py2max-0.3.0}/py2max/export/svg.py +86 -23
- {py2max-0.2.1 → py2max-0.3.0}/py2max/layout/base.py +5 -5
- {py2max-0.2.1 → py2max-0.3.0}/py2max/layout/flow.py +25 -14
- {py2max-0.2.1 → py2max-0.3.0}/py2max/layout/grid.py +26 -18
- {py2max-0.2.1 → py2max-0.3.0}/py2max/layout/matrix.py +22 -17
- {py2max-0.2.1 → py2max-0.3.0}/py2max/log.py +4 -2
- py2max-0.3.0/py2max/m4l.py +626 -0
- {py2max-0.2.1 → py2max-0.3.0}/py2max/maxref/__init__.py +15 -3
- py2max-0.3.0/py2max/maxref/data/bundle.json.gz +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/py2max/maxref/db.py +42 -24
- {py2max-0.2.1 → py2max-0.3.0}/py2max/maxref/legacy.py +13 -5
- {py2max-0.2.1 → py2max-0.3.0}/py2max/maxref/parser.py +95 -26
- {py2max-0.2.1 → py2max-0.3.0}/py2max/utils.py +22 -1
- {py2max-0.2.1 → py2max-0.3.0}/pyproject.toml +13 -15
- py2max-0.3.0/tests/conftest.py +21 -0
- py2max-0.3.0/tests/data/mydevice.amxd +0 -0
- py2max-0.3.0/tests/data/mydevice2.amxd +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/advanced/data_containers.py +5 -5
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/api/box_api_examples.py +7 -7
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/api/patcher_api_examples.py +12 -12
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/auto_layout_demo.py +14 -14
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/layout/columnar_layout_examples.py +5 -5
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/layout/grid_layout_examples.py +1 -1
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/layout/matrix_layout_examples.py +2 -2
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/svg_preview_demo.py +7 -7
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/quickstart/layout_examples.py +1 -1
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/tutorial/generative_music.py +1 -1
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_abstract_coverage.py +1 -1
- py2max-0.3.0/tests/test_amxd.py +336 -0
- py2max-0.3.0/tests/test_basic.py +22 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_bpatcher.py +1 -1
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_cli.py +0 -1
- py2max-0.3.0/tests/test_colors_theme.py +94 -0
- py2max-0.3.0/tests/test_comment.py +8 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_connection_validation.py +2 -2
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_converters.py +1 -2
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_core_coverage.py +25 -11
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_db.py +0 -2
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_dict.py +1 -1
- py2max-0.3.0/tests/test_encapsulate.py +106 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_error_handling.py +14 -7
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout.py +0 -1
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_builtins.py +6 -6
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_coverage.py +26 -28
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_flow.py +4 -4
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_hola1.py +5 -2
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_hola_graph.py +1 -1
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_matrix.py +0 -1
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_networkx2.py +3 -3
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_nx_graphviz.py +3 -3
- py2max-0.3.0/tests/test_m4l.py +140 -0
- py2max-0.3.0/tests/test_maxref_bundle.py +91 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_mc_cycle.py +1 -1
- py2max-0.3.0/tests/test_mc_poly.py +40 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_nested.py +6 -2
- py2max-0.3.0/tests/test_nested_patchers.py +50 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_number_tilde.py +5 -5
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_param.py +2 -2
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_patcher.py +9 -5
- py2max-0.3.0/tests/test_presets.py +64 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_pydantic.py +6 -8
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_rnbo.py +1 -2
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_search.py +12 -12
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_svg.py +48 -10
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_table.py +6 -2
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_tree_builder.py +3 -1
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_tutorial_simple_synthesis.py +1 -1
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_two_sines.py +10 -0
- py2max-0.3.0/tests/test_validate_attrs.py +66 -0
- py2max-0.2.1/py2max/server/__init__.py +0 -54
- py2max-0.2.1/py2max/server/client.py +0 -295
- py2max-0.2.1/py2max/server/inline.py +0 -312
- py2max-0.2.1/py2max/server/repl.py +0 -561
- py2max-0.2.1/py2max/server/rpc.py +0 -240
- py2max-0.2.1/py2max/server/websocket.py +0 -997
- py2max-0.2.1/py2max/static/cola.min.js +0 -4
- py2max-0.2.1/py2max/static/d3.v7.min.js +0 -2
- py2max-0.2.1/py2max/static/dagre-bundle.js +0 -328
- py2max-0.2.1/py2max/static/elk.bundled.js +0 -6663
- py2max-0.2.1/py2max/static/index.html +0 -168
- py2max-0.2.1/py2max/static/interactive.html +0 -589
- py2max-0.2.1/py2max/static/interactive.js +0 -2111
- py2max-0.2.1/py2max/static/live-preview.js +0 -324
- py2max-0.2.1/py2max/static/svg.min.js +0 -13
- py2max-0.2.1/py2max/static/svg.min.js.map +0 -1
- py2max-0.2.1/tests/examples/info_command_demo.py +0 -106
- py2max-0.2.1/tests/examples/inline_repl_verification.py +0 -64
- py2max-0.2.1/tests/examples/interactive_demo.py +0 -228
- py2max-0.2.1/tests/examples/interactive_save_demo.py +0 -164
- py2max-0.2.1/tests/examples/live_preview_demo.py +0 -225
- py2max-0.2.1/tests/examples/refresh_function_verification.py +0 -54
- py2max-0.2.1/tests/examples/repl_client_server_demo.py +0 -171
- py2max-0.2.1/tests/examples/repl_quickstart.py +0 -140
- py2max-0.2.1/tests/test_basic.py +0 -11
- py2max-0.2.1/tests/test_comment.py +0 -8
- py2max-0.2.1/tests/test_nested_patchers.py +0 -279
- py2max-0.2.1/tests/test_repl.py +0 -309
- py2max-0.2.1/tests/test_repl_client.py +0 -348
- py2max-0.2.1/tests/test_repl_inline.py +0 -283
- py2max-0.2.1/tests/test_repl_server.py +0 -316
- py2max-0.2.1/tests/test_websocket.py +0 -232
- {py2max-0.2.1 → py2max-0.3.0}/LICENSE +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/py2max/__main__.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/py2max/core/__init__.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/py2max/core/common.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/py2max/exceptions.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/py2max/export/__init__.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/py2max/layout/__init__.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/py2max/maxref/category.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/py2max/py.typed +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/py2max/transformers.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/__init__.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/data/complex.maxpat +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/data/desc.maxpat +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/data/empty.maxpat +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/data/nested.maxpat +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/data/simple.maxpat +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/data/tabular.maxpat +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/data/umenu.maxref.xml +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/README.md +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/advanced/connection_patterns.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/advanced/custom_extensions.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/advanced/error_handling.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/advanced/performance_optimization.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/advanced/subpatchers.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/db/category_db_demo.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/db/maxref_db_demo.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/layout/flow_layout_examples.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/basic_synth.maxpat +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/basic_synth.svg +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/complex_synth.maxpat +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/complex_synth.svg +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/flow_layout.maxpat +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/flow_layout.svg +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/grid_layout.maxpat +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/grid_layout.svg +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/horizontal_layout.maxpat +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/horizontal_layout.svg +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/styled_no_ports.svg +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/styled_no_title.svg +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/styled_patch.maxpat +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/styled_with_ports.svg +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/vertical_layout.maxpat +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/vertical_layout.svg +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/workflow_demo.maxpat +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/workflow_demo.svg +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/quickstart/basic_patch.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/tutorial/interactive_controller.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/tutorial/signal_processing_chain.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/examples/tutorial/simple_synthesis.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/graphs/random/v30e33.tglf +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/registry.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/scratch.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_abstraction.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_add.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_attrui.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_beap.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_coll.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_colors.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_defaults.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_examples.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_ezdac.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_gen.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_group.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_itable.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_js.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_kwds_filter.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_graph_layout.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_hola2.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_hola3.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_networkx1.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_nx_orthogonal.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_nx_tsmpy.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_vertical.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_linking.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_maxref.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_message.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_mypatch.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_numbers.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_pitched_osc.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_rnbo_subpatcher.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_scripting_name.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_semantic_ids.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_subpatch.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_transformers.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_tree.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_umenu.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_utils.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_varname.py +0 -0
- {py2max-0.2.1 → py2max-0.3.0}/tests/test_zl_group.py +0 -0
|
@@ -2,6 +2,98 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.0]
|
|
6
|
+
|
|
7
|
+
### Removed: Interactive Server Split Into `py2max-server` (breaking)
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
- Removed `Patcher.serve()` and the `py2max serve` / `py2max repl` CLI commands; those CLI subcommands now print a pointer to `py2max-server`.
|
|
12
|
+
- Removed the `[server]` optional-dependency extra (`websockets`, `ptpython`) and the bundled browser assets (`py2max/static/`).
|
|
13
|
+
- 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`).
|
|
14
|
+
|
|
15
|
+
### New: `Patcher.encapsulate()`
|
|
16
|
+
|
|
17
|
+
- `Patcher.encapsulate(boxes, text="p sub")` wraps a selection of boxes into a subpatcher, auto-generating `inlet`/`outlet` objects for any connections that cross the selection boundary and rewiring the parent through the new subpatcher box. Connections wholly inside the selection move into the subpatcher; connections wholly outside it are untouched. Ports are de-duplicated by source, matching how patches are built by hand. Returns the new subpatcher `Box`.
|
|
18
|
+
|
|
19
|
+
### New: Preset / `pattrstorage` Scaffolding
|
|
20
|
+
|
|
21
|
+
- `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.
|
|
22
|
+
- `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.
|
|
23
|
+
|
|
24
|
+
### New: Keyword-Attribute Validation (`validate_attrs`)
|
|
25
|
+
|
|
26
|
+
- `Patcher(validate_attrs=True)` warns (`UserWarning`) when an object is given a keyword that is not a known attribute for its Max class -- catching typos like `inital=` for `initial=`. The known set is the object's maxref attributes plus a universal box-attribute whitelist; objects with no maxref entry are skipped. Off by default and warn-only, so it never changes generated output.
|
|
27
|
+
|
|
28
|
+
### New: Multichannel (`mc.`) / Polyphony Helpers
|
|
29
|
+
|
|
30
|
+
- `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`).
|
|
31
|
+
- `Patcher.add_poly(target, voices=1)` adds a `poly~` object hosting N voices of a target patch.
|
|
32
|
+
|
|
33
|
+
### Improved: SVG Export (Max-faithful preview)
|
|
34
|
+
|
|
35
|
+
- The `preview` / `to_svg` output now approximates Max's look: a light patcher background, signal vs message/control **ports colored distinctly** (signal green, control dark), signal **cables drawn thicker and in a distinct color**, and subpatcher boxes tinted so they stand out. Object text is intentionally not truncated, matching Max (objects size to their text).
|
|
36
|
+
|
|
37
|
+
### Changed: Documentation moved to MkDocs
|
|
38
|
+
|
|
39
|
+
- Documentation migrated from Sphinx/reStructuredText to **MkDocs + Material + mkdocstrings** (all Markdown, matching the rest of the repo). The API reference is generated from the (now fully typed) docstrings, including `Patcher`'s mixin-provided methods. The changelog and contributing pages are single-source includes of `CHANGELOG.md` / `CONTRIBUTING.md`. Build with `make docs`, preview with `make docs-serve`, publish with `make docs-deploy`. `docs/notes/` is retained as a historical journal but excluded from the published site.
|
|
40
|
+
|
|
41
|
+
### New: Color / Theme Helpers
|
|
42
|
+
|
|
43
|
+
- `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.
|
|
44
|
+
- `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.
|
|
45
|
+
- `py2max.core.colors` exposes the `MAX_COLORS` named palette and `resolve_color()`.
|
|
46
|
+
|
|
47
|
+
### Security
|
|
48
|
+
|
|
49
|
+
- **Removed a misleading path-traversal check** in `Patcher.save_as()`. The previous `..`/`/etc` allowlist was trivially bypassable and gave a false sense of safety; for an offline file generator it provided no real protection. Genuinely unresolvable paths still raise `PatcherIOError`.
|
|
50
|
+
|
|
51
|
+
### Typed: Full `mypy --strict`
|
|
52
|
+
|
|
53
|
+
- The entire package is now annotated and passes `mypy --strict`, backing the shipped `py.typed` marker. `[tool.mypy]` enforces `strict = true`. Core has **no runtime dependencies**.
|
|
54
|
+
|
|
55
|
+
### Changed: Lighter Core Imports
|
|
56
|
+
|
|
57
|
+
- `import py2max` no longer eagerly imports `sqlite3`, the maxref database layer, or `py2max.m4l`. `MaxRefDB` is now available lazily via `from py2max.maxref import MaxRefDB` (removed from the top-level `py2max` namespace).
|
|
58
|
+
|
|
59
|
+
### Internal: `Patcher` Decomposition
|
|
60
|
+
|
|
61
|
+
- Split the ~1660-line `Patcher` class into focused mixins composed via inheritance: object creation (`BoxFactoryMixin` in `core/factory.py`) and serialization (`SerializationMixin` in `core/serialization.py`). The public API is unchanged; adding a new object type now means editing `core/factory.py` rather than the core class.
|
|
62
|
+
|
|
63
|
+
### Fixed
|
|
64
|
+
|
|
65
|
+
- 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.
|
|
66
|
+
- `Box.oid` now returns the trailing numeric part of any id (e.g. `cycle_1` -> 1) instead of raising `ValueError` under `semantic_ids=True`.
|
|
67
|
+
- The `py2max` CLI now reports all `Py2MaxError`s (not just `InvalidConnectionError`) as a clean error message instead of leaking a traceback.
|
|
68
|
+
- Fixed an `inital` -> `initial` keyword typo in the simple-synthesis tutorial.
|
|
69
|
+
|
|
70
|
+
### Testing & Tooling
|
|
71
|
+
|
|
72
|
+
- 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.
|
|
73
|
+
- 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.
|
|
74
|
+
- Repo-wide `ruff` lint and format cleanup.
|
|
75
|
+
|
|
76
|
+
### New: Max for Live Support (`py2max.m4l`)
|
|
77
|
+
|
|
78
|
+
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.
|
|
79
|
+
|
|
80
|
+
- **`.amxd` read/write**: byte-for-byte compatible with Max-exported devices; verified against real fixtures and end-to-end in Live 12.
|
|
81
|
+
- **Device-type discrimination**: Audio Effect / Instrument / MIDI Effect via `Patcher(device_type=...)` or the `pack_amxd` / `write_amxd` `device_type` argument.
|
|
82
|
+
- **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).
|
|
83
|
+
- `Patcher.save()` / `Patcher.from_file()` auto-detect the `.amxd` extension; `.maxpat` path is unchanged.
|
|
84
|
+
|
|
85
|
+
### Changed: M4L Module Layout & Imports
|
|
86
|
+
|
|
87
|
+
- All M4L code (binary format + presentation helpers) lives in a single module `py2max/m4l.py`. Previously briefly split as `py2max/amxd.py`.
|
|
88
|
+
- M4L symbols are reachable only via `from py2max.m4l import …`; nothing is re-exported from the top-level `py2max` namespace.
|
|
89
|
+
|
|
90
|
+
### New: Prebuilt MaxRef Bundle (Linux Support)
|
|
91
|
+
|
|
92
|
+
- Ship `py2max/maxref/data/bundle.json.gz` in the wheel (1175 objects, ~1 MiB compressed, ~7 MiB raw).
|
|
93
|
+
- `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.
|
|
94
|
+
- Regenerate with `uv run python scripts/build_maxref_bundle.py` on a machine with Max installed; commit the result.
|
|
95
|
+
- Bundle stores full parsed data (methods, attributes, inlets/outlets, digests, descriptions) — not a trimmed subset — so introspection parity with macOS/Windows is preserved.
|
|
96
|
+
|
|
5
97
|
## [0.2.1] - 2026-01-11
|
|
6
98
|
|
|
7
99
|
### New: Dagre Layout Algorithm
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py2max
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -12,7 +12,6 @@ Classifier: Intended Audience :: Developers
|
|
|
12
12
|
Classifier: Intended Audience :: End Users/Desktop
|
|
13
13
|
Classifier: Topic :: Multimedia :: Sound/Audio
|
|
14
14
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
15
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
16
15
|
Classifier: Programming Language :: Python :: 3
|
|
17
16
|
Classifier: Programming Language :: Python :: 3.9
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
@@ -22,15 +21,14 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
22
21
|
Classifier: Programming Language :: Python :: 3.14
|
|
23
22
|
Classifier: Operating System :: OS Independent
|
|
24
23
|
Classifier: Typing :: Typed
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
Maintainer: Shakeeb Alireza
|
|
25
|
+
Maintainer-email: Shakeeb Alireza <shakfu@users.noreply.github.com>
|
|
27
26
|
Requires-Python: >=3.9
|
|
28
|
-
Project-URL: Changelog, https://github.com/shakfu/py2max/blob/main/CHANGELOG.md
|
|
29
|
-
Project-URL: Documentation, https://github.com/shakfu/py2max#readme
|
|
30
27
|
Project-URL: Homepage, https://github.com/shakfu/py2max
|
|
31
|
-
Project-URL:
|
|
28
|
+
Project-URL: Documentation, https://github.com/shakfu/py2max#readme
|
|
32
29
|
Project-URL: Repository, https://github.com/shakfu/py2max.git
|
|
33
|
-
|
|
30
|
+
Project-URL: Issues, https://github.com/shakfu/py2max/issues
|
|
31
|
+
Project-URL: Changelog, https://github.com/shakfu/py2max/blob/main/CHANGELOG.md
|
|
34
32
|
Description-Content-Type: text/markdown
|
|
35
33
|
|
|
36
34
|
# py2max
|
|
@@ -46,9 +44,13 @@ If you are looking for Python 3 externals for Max/MSP, check out the [py-js](htt
|
|
|
46
44
|
|
|
47
45
|
```bash
|
|
48
46
|
pip install py2max
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
For the browser-based live editor and remote REPL, install the companion
|
|
50
|
+
[`py2max-server`](https://github.com/shakfu/py2max-server) package:
|
|
49
51
|
|
|
50
|
-
|
|
51
|
-
pip install py2max
|
|
52
|
+
```bash
|
|
53
|
+
pip install py2max-server
|
|
52
54
|
```
|
|
53
55
|
|
|
54
56
|
For development:
|
|
@@ -85,12 +87,15 @@ That's it! Open `my-synth.maxpat` in Max to see your patch.
|
|
|
85
87
|
- **Universal Object Support** - Works with any Max/MSP/Jitter object
|
|
86
88
|
- **99% Test Coverage** - 418+ tests ensure reliability
|
|
87
89
|
|
|
88
|
-
### Interactive Server (
|
|
90
|
+
### Interactive Server (separate package)
|
|
89
91
|
|
|
90
|
-
Real-time browser-based patch editing with bidirectional sync
|
|
92
|
+
Real-time browser-based patch editing with bidirectional sync lives in the
|
|
93
|
+
companion [`py2max-server`](https://github.com/shakfu/py2max-server) package, so
|
|
94
|
+
the core library stays small and offline:
|
|
91
95
|
|
|
92
96
|
```bash
|
|
93
|
-
|
|
97
|
+
pip install py2max-server
|
|
98
|
+
py2max-server serve my-patch.maxpat
|
|
94
99
|
# Opens browser at http://localhost:8000
|
|
95
100
|
```
|
|
96
101
|
|
|
@@ -285,12 +290,15 @@ py2max validate demo.maxpat
|
|
|
285
290
|
|
|
286
291
|
### Interactive Server
|
|
287
292
|
|
|
293
|
+
Provided by the separate [`py2max-server`](https://github.com/shakfu/py2max-server)
|
|
294
|
+
package (`pip install py2max-server`):
|
|
295
|
+
|
|
288
296
|
```bash
|
|
289
297
|
# Start server with browser editing
|
|
290
|
-
py2max serve my-patch.maxpat
|
|
298
|
+
py2max-server serve my-patch.maxpat
|
|
291
299
|
|
|
292
300
|
# With REPL in same terminal
|
|
293
|
-
py2max serve my-patch.maxpat --repl
|
|
301
|
+
py2max-server serve my-patch.maxpat --repl
|
|
294
302
|
```
|
|
295
303
|
|
|
296
304
|
### MaxRef Database
|
|
@@ -350,18 +358,21 @@ All classes are extendable via `**kwargs`, allowing any Max object configuration
|
|
|
350
358
|
|
|
351
359
|
## Caveats
|
|
352
360
|
|
|
353
|
-
- Max doesn't refresh from file when open - close and reopen to see changes, or use `py2max serve` for live editing
|
|
361
|
+
- Max doesn't refresh from file when open - close and reopen to see changes, or use `py2max-server serve` (from the separate `py2max-server` package) for live editing
|
|
354
362
|
- For tilde variants, use the `_tilde` suffix: `p.add_gen()` vs `p.add_gen_tilde()`
|
|
355
363
|
- API docs in progress - see `CLAUDE.md` for comprehensive usage
|
|
356
364
|
|
|
357
365
|
## Examples
|
|
358
366
|
|
|
359
|
-
|
|
367
|
+
The [`tests/examples/`](tests/examples/) directory contains working, tested
|
|
368
|
+
examples organized by topic (see its [README](tests/examples/README.md)):
|
|
360
369
|
|
|
361
|
-
- `
|
|
362
|
-
- `
|
|
363
|
-
- `
|
|
364
|
-
- `
|
|
370
|
+
- `quickstart/basic_patch.py` - Simple oscillator patch
|
|
371
|
+
- `tutorial/signal_processing_chain.py` - Complex audio processing chain
|
|
372
|
+
- `tutorial/generative_music.py` - Generative music system with patterns
|
|
373
|
+
- `layout/grid_layout_examples.py` - Grid layout with clustering
|
|
374
|
+
- `advanced/data_containers.py` - Tables, collections, and dictionaries
|
|
375
|
+
- `api/patcher_api_examples.py` - Patcher API reference examples
|
|
365
376
|
|
|
366
377
|
External usage:
|
|
367
378
|
|
|
@@ -11,9 +11,13 @@ If you are looking for Python 3 externals for Max/MSP, check out the [py-js](htt
|
|
|
11
11
|
|
|
12
12
|
```bash
|
|
13
13
|
pip install py2max
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
For the browser-based live editor and remote REPL, install the companion
|
|
17
|
+
[`py2max-server`](https://github.com/shakfu/py2max-server) package:
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
pip install py2max
|
|
19
|
+
```bash
|
|
20
|
+
pip install py2max-server
|
|
17
21
|
```
|
|
18
22
|
|
|
19
23
|
For development:
|
|
@@ -50,12 +54,15 @@ That's it! Open `my-synth.maxpat` in Max to see your patch.
|
|
|
50
54
|
- **Universal Object Support** - Works with any Max/MSP/Jitter object
|
|
51
55
|
- **99% Test Coverage** - 418+ tests ensure reliability
|
|
52
56
|
|
|
53
|
-
### Interactive Server (
|
|
57
|
+
### Interactive Server (separate package)
|
|
54
58
|
|
|
55
|
-
Real-time browser-based patch editing with bidirectional sync
|
|
59
|
+
Real-time browser-based patch editing with bidirectional sync lives in the
|
|
60
|
+
companion [`py2max-server`](https://github.com/shakfu/py2max-server) package, so
|
|
61
|
+
the core library stays small and offline:
|
|
56
62
|
|
|
57
63
|
```bash
|
|
58
|
-
|
|
64
|
+
pip install py2max-server
|
|
65
|
+
py2max-server serve my-patch.maxpat
|
|
59
66
|
# Opens browser at http://localhost:8000
|
|
60
67
|
```
|
|
61
68
|
|
|
@@ -250,12 +257,15 @@ py2max validate demo.maxpat
|
|
|
250
257
|
|
|
251
258
|
### Interactive Server
|
|
252
259
|
|
|
260
|
+
Provided by the separate [`py2max-server`](https://github.com/shakfu/py2max-server)
|
|
261
|
+
package (`pip install py2max-server`):
|
|
262
|
+
|
|
253
263
|
```bash
|
|
254
264
|
# Start server with browser editing
|
|
255
|
-
py2max serve my-patch.maxpat
|
|
265
|
+
py2max-server serve my-patch.maxpat
|
|
256
266
|
|
|
257
267
|
# With REPL in same terminal
|
|
258
|
-
py2max serve my-patch.maxpat --repl
|
|
268
|
+
py2max-server serve my-patch.maxpat --repl
|
|
259
269
|
```
|
|
260
270
|
|
|
261
271
|
### MaxRef Database
|
|
@@ -315,18 +325,21 @@ All classes are extendable via `**kwargs`, allowing any Max object configuration
|
|
|
315
325
|
|
|
316
326
|
## Caveats
|
|
317
327
|
|
|
318
|
-
- Max doesn't refresh from file when open - close and reopen to see changes, or use `py2max serve` for live editing
|
|
328
|
+
- Max doesn't refresh from file when open - close and reopen to see changes, or use `py2max-server serve` (from the separate `py2max-server` package) for live editing
|
|
319
329
|
- For tilde variants, use the `_tilde` suffix: `p.add_gen()` vs `p.add_gen_tilde()`
|
|
320
330
|
- API docs in progress - see `CLAUDE.md` for comprehensive usage
|
|
321
331
|
|
|
322
332
|
## Examples
|
|
323
333
|
|
|
324
|
-
|
|
334
|
+
The [`tests/examples/`](tests/examples/) directory contains working, tested
|
|
335
|
+
examples organized by topic (see its [README](tests/examples/README.md)):
|
|
325
336
|
|
|
326
|
-
- `
|
|
327
|
-
- `
|
|
328
|
-
- `
|
|
329
|
-
- `
|
|
337
|
+
- `quickstart/basic_patch.py` - Simple oscillator patch
|
|
338
|
+
- `tutorial/signal_processing_chain.py` - Complex audio processing chain
|
|
339
|
+
- `tutorial/generative_music.py` - Generative music system with patterns
|
|
340
|
+
- `layout/grid_layout_examples.py` - Grid layout with clustering
|
|
341
|
+
- `advanced/data_containers.py` - Tables, collections, and dictionaries
|
|
342
|
+
- `api/patcher_api_examples.py` - Patcher API reference examples
|
|
330
343
|
|
|
331
344
|
External usage:
|
|
332
345
|
|
|
@@ -8,7 +8,10 @@ Main Classes:
|
|
|
8
8
|
Patcher: Core class for creating and managing Max patches
|
|
9
9
|
Box: Represents individual Max objects (oscillators, effects, etc.)
|
|
10
10
|
Patchline: Represents connections between objects
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
The SQLite Max-reference database is available as ``from py2max.maxref import
|
|
13
|
+
MaxRefDB`` -- kept out of the top-level import so ``import py2max`` does not pull
|
|
14
|
+
in ``sqlite3`` and the database layer.
|
|
12
15
|
|
|
13
16
|
Exceptions:
|
|
14
17
|
Py2MaxError: Base exception for all py2max errors
|
|
@@ -32,10 +35,9 @@ Example:
|
|
|
32
35
|
>>> p.save()
|
|
33
36
|
"""
|
|
34
37
|
|
|
35
|
-
__version__ = "0.
|
|
38
|
+
__version__ = "0.3.0"
|
|
36
39
|
|
|
37
40
|
from .core import Box, Patcher, Patchline
|
|
38
|
-
from .maxref import MaxRefDB
|
|
39
41
|
from .exceptions import (
|
|
40
42
|
DatabaseError,
|
|
41
43
|
InvalidConnectionError,
|
|
@@ -54,7 +56,6 @@ __all__ = [
|
|
|
54
56
|
"Patcher",
|
|
55
57
|
"Box",
|
|
56
58
|
"Patchline",
|
|
57
|
-
"MaxRefDB",
|
|
58
59
|
# Exceptions
|
|
59
60
|
"Py2MaxError",
|
|
60
61
|
"InvalidConnectionError",
|
|
@@ -17,7 +17,7 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
17
17
|
|
|
18
18
|
from .core import Patcher, Patchline
|
|
19
19
|
from .core.common import Rect
|
|
20
|
-
from .exceptions import
|
|
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
23
|
from .maxref import MaxRefCache, MaxRefDB, validate_connection
|
|
@@ -42,15 +42,13 @@ def _to_pascal_case(name: str) -> str:
|
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def _object_name(box) -> str:
|
|
46
|
-
|
|
47
|
-
text = getattr(box, "text", "") or ""
|
|
48
|
-
if maxclass == "newobj" and text:
|
|
49
|
-
return text.split()[0]
|
|
50
|
-
return maxclass
|
|
45
|
+
def _object_name(box: Any) -> str:
|
|
46
|
+
from .utils import object_name
|
|
51
47
|
|
|
48
|
+
return object_name(box)
|
|
52
49
|
|
|
53
|
-
|
|
50
|
+
|
|
51
|
+
def _unique_object_labels(boxes: Iterable[Any]) -> List[str]:
|
|
54
52
|
labels: set[str] = set()
|
|
55
53
|
for box in boxes:
|
|
56
54
|
labels.add(_object_name(box))
|
|
@@ -63,7 +61,7 @@ def _coerce_rect(patcher: Patcher) -> None:
|
|
|
63
61
|
patcher.rect = Rect(*rect)
|
|
64
62
|
|
|
65
63
|
|
|
66
|
-
def _format_args(method:
|
|
64
|
+
def _format_args(method: Dict[str, Any]) -> List[str]:
|
|
67
65
|
args: List[str] = []
|
|
68
66
|
for arg in method.get("args", []):
|
|
69
67
|
name = _sanitize_identifier(arg.get("name", "arg"))
|
|
@@ -75,7 +73,7 @@ def _format_args(method: dict) -> List[str]:
|
|
|
75
73
|
return args
|
|
76
74
|
|
|
77
75
|
|
|
78
|
-
def _dump_code(name: str, data:
|
|
76
|
+
def _dump_code(name: str, data: Dict[str, Any]) -> None:
|
|
79
77
|
class_name = _to_pascal_case(name)
|
|
80
78
|
digest = data.get("digest", "")
|
|
81
79
|
description = data.get("description", "")
|
|
@@ -112,7 +110,7 @@ def _dump_code(name: str, data: dict) -> None:
|
|
|
112
110
|
print(" raise NotImplementedError\n")
|
|
113
111
|
|
|
114
112
|
|
|
115
|
-
def _dump_tests(name: str, data:
|
|
113
|
+
def _dump_tests(name: str, data: Dict[str, Any]) -> None:
|
|
116
114
|
base = _sanitize_identifier(name)
|
|
117
115
|
for method_name, method in sorted(data.get("methods", {}).items()):
|
|
118
116
|
identifier = _sanitize_identifier(method_name)
|
|
@@ -123,7 +121,7 @@ def _dump_tests(name: str, data: dict) -> None:
|
|
|
123
121
|
print(" # TODO: implement test\n")
|
|
124
122
|
|
|
125
123
|
|
|
126
|
-
def _generate_test_source(name: str, data:
|
|
124
|
+
def _generate_test_source(name: str, data: Dict[str, Any]) -> str:
|
|
127
125
|
base = _sanitize_identifier(name)
|
|
128
126
|
lines = [
|
|
129
127
|
"from py2max.maxref import MaxRefCache",
|
|
@@ -639,108 +637,16 @@ def cmd_db_cache(args: argparse.Namespace) -> int:
|
|
|
639
637
|
|
|
640
638
|
|
|
641
639
|
def cmd_serve(args: argparse.Namespace) -> int:
|
|
642
|
-
"""
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
import importlib.util
|
|
653
|
-
|
|
654
|
-
if importlib.util.find_spec("websockets") is None:
|
|
655
|
-
print("Error: websockets package required for server.", file=sys.stderr)
|
|
656
|
-
print("Install with: pip install websockets", file=sys.stderr)
|
|
657
|
-
return 1
|
|
658
|
-
|
|
659
|
-
# Check if ptpython is installed (if --repl requested)
|
|
660
|
-
if args.repl:
|
|
661
|
-
if importlib.util.find_spec("ptpython") is None:
|
|
662
|
-
print("Error: ptpython package required for REPL.", file=sys.stderr)
|
|
663
|
-
print(
|
|
664
|
-
"Install with: pip install ptpython or uv add ptpython", file=sys.stderr
|
|
665
|
-
)
|
|
666
|
-
return 1
|
|
667
|
-
|
|
668
|
-
# Load patcher
|
|
669
|
-
patcher = Patcher.from_file(input_path)
|
|
670
|
-
_coerce_rect(patcher)
|
|
671
|
-
|
|
672
|
-
# Start interactive server
|
|
673
|
-
try:
|
|
674
|
-
print(f"Starting server for: {input_path}")
|
|
675
|
-
print(f"HTTP server: http://localhost:{args.port}")
|
|
676
|
-
print(f"WebSocket server: ws://localhost:{args.port + 1}")
|
|
677
|
-
print("Interactive editing enabled - changes sync bidirectionally")
|
|
678
|
-
if not args.no_save:
|
|
679
|
-
print(f"Auto-save enabled: changes will be saved to {input_path}")
|
|
680
|
-
if args.repl:
|
|
681
|
-
print("REPL mode enabled - type 'commands()' for help")
|
|
682
|
-
print("Press Ctrl+C to stop")
|
|
683
|
-
|
|
684
|
-
async def run_server():
|
|
685
|
-
# Check if using single-terminal mode (Option 2b)
|
|
686
|
-
if args.repl and args.log_file:
|
|
687
|
-
# Option 2b: Single terminal with log redirection
|
|
688
|
-
from .server.inline import start_background_server_repl
|
|
689
|
-
|
|
690
|
-
log_file_path = Path(args.log_file)
|
|
691
|
-
await start_background_server_repl(
|
|
692
|
-
patcher, port=args.port, log_file=log_file_path
|
|
693
|
-
)
|
|
694
|
-
return
|
|
695
|
-
|
|
696
|
-
# Option 2a: Client-server mode (default)
|
|
697
|
-
server = await patcher.serve(port=args.port, auto_open=not args.no_open)
|
|
698
|
-
|
|
699
|
-
# Start REPL server (always running, can connect remotely)
|
|
700
|
-
from .server.rpc import start_repl_server
|
|
701
|
-
|
|
702
|
-
repl_port = args.port + 2 # HTTP=8000, WS=8001, REPL=8002
|
|
703
|
-
repl_server = await start_repl_server(patcher, server, port=repl_port)
|
|
704
|
-
|
|
705
|
-
print()
|
|
706
|
-
print("=" * 70)
|
|
707
|
-
print("REPL server started")
|
|
708
|
-
print(f"Connect with: py2max repl localhost:{repl_port}")
|
|
709
|
-
print("=" * 70)
|
|
710
|
-
print()
|
|
711
|
-
|
|
712
|
-
if args.repl:
|
|
713
|
-
# Show deprecation warning if --repl used without --log-file
|
|
714
|
-
print("WARNING: --repl flag without --log-file is deprecated.")
|
|
715
|
-
print("For single-terminal mode, use: --repl --log-file server.log")
|
|
716
|
-
print(
|
|
717
|
-
"For client-server mode (recommended), in a separate terminal run:"
|
|
718
|
-
)
|
|
719
|
-
print(f" py2max repl localhost:{repl_port}")
|
|
720
|
-
print()
|
|
721
|
-
|
|
722
|
-
# Keep running until interrupted
|
|
723
|
-
try:
|
|
724
|
-
while True:
|
|
725
|
-
await asyncio.sleep(1)
|
|
726
|
-
except KeyboardInterrupt:
|
|
727
|
-
print("\nStopping server...")
|
|
728
|
-
repl_server.close()
|
|
729
|
-
await repl_server.wait_closed()
|
|
730
|
-
await server.stop()
|
|
731
|
-
|
|
732
|
-
asyncio.run(run_server())
|
|
733
|
-
return 0
|
|
734
|
-
|
|
735
|
-
except KeyboardInterrupt:
|
|
736
|
-
print("\nStopping server...")
|
|
737
|
-
return 0
|
|
738
|
-
except Exception as e:
|
|
739
|
-
print(f"Error starting server: {e}", file=sys.stderr)
|
|
740
|
-
import traceback
|
|
741
|
-
|
|
742
|
-
traceback.print_exc()
|
|
743
|
-
return 1
|
|
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
|
|
744
650
|
|
|
745
651
|
|
|
746
652
|
def cmd_preview(args: argparse.Namespace) -> int:
|
|
@@ -790,48 +696,16 @@ def cmd_preview(args: argparse.Namespace) -> int:
|
|
|
790
696
|
|
|
791
697
|
|
|
792
698
|
def cmd_repl(args: argparse.Namespace) -> int:
|
|
793
|
-
"""
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
print(f"Invalid port number: {port_str}", file=sys.stderr)
|
|
804
|
-
return 1
|
|
805
|
-
else:
|
|
806
|
-
host = server
|
|
807
|
-
port = 9000
|
|
808
|
-
|
|
809
|
-
# Check if websockets is installed
|
|
810
|
-
import importlib.util
|
|
811
|
-
|
|
812
|
-
if importlib.util.find_spec("websockets") is None:
|
|
813
|
-
print("Error: websockets package required for REPL client.", file=sys.stderr)
|
|
814
|
-
print("Install with: pip install websockets", file=sys.stderr)
|
|
815
|
-
return 1
|
|
816
|
-
|
|
817
|
-
# Check if ptpython is installed
|
|
818
|
-
if importlib.util.find_spec("ptpython") is None:
|
|
819
|
-
print("Error: ptpython package required for REPL.", file=sys.stderr)
|
|
820
|
-
print("Install with: pip install ptpython or uv add ptpython", file=sys.stderr)
|
|
821
|
-
return 1
|
|
822
|
-
|
|
823
|
-
# Start REPL client
|
|
824
|
-
try:
|
|
825
|
-
from .server.client import start_repl_client
|
|
826
|
-
|
|
827
|
-
return asyncio.run(start_repl_client(host, port))
|
|
828
|
-
|
|
829
|
-
except KeyboardInterrupt:
|
|
830
|
-
print("\nDisconnected.")
|
|
831
|
-
return 0
|
|
832
|
-
except Exception as e:
|
|
833
|
-
print(f"Error: {e}", file=sys.stderr)
|
|
834
|
-
return 1
|
|
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
|
|
835
709
|
|
|
836
710
|
|
|
837
711
|
def cmd_maxref(args: argparse.Namespace) -> int:
|
|
@@ -1228,6 +1102,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1228
1102
|
default="localhost:9000",
|
|
1229
1103
|
help="Server address (default: localhost:9000)",
|
|
1230
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
|
+
)
|
|
1231
1111
|
repl_parser.set_defaults(func=cmd_repl)
|
|
1232
1112
|
|
|
1233
1113
|
return parser
|
|
@@ -1242,8 +1122,8 @@ def main(argv: List[str] | None = None) -> int:
|
|
|
1242
1122
|
return 1
|
|
1243
1123
|
|
|
1244
1124
|
try:
|
|
1245
|
-
return args.func(args)
|
|
1246
|
-
except
|
|
1125
|
+
return cast(int, args.func(args))
|
|
1126
|
+
except Py2MaxError as exc:
|
|
1247
1127
|
print(f"Error: {exc}", file=sys.stderr)
|
|
1248
1128
|
return 1
|
|
1249
1129
|
|
|
@@ -6,7 +6,7 @@ between core.py and layout.py modules.
|
|
|
6
6
|
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import Any, Callable, Optional, Union
|
|
9
|
+
from typing import Any, Callable, Dict, Iterator, Optional, Union
|
|
10
10
|
|
|
11
11
|
from .common import Rect
|
|
12
12
|
|
|
@@ -60,7 +60,7 @@ class AbstractBox(ABC):
|
|
|
60
60
|
id: Optional[str]
|
|
61
61
|
maxclass: str
|
|
62
62
|
patching_rect: Rect
|
|
63
|
-
_kwds:
|
|
63
|
+
_kwds: Dict[str, Any]
|
|
64
64
|
|
|
65
65
|
@abstractmethod
|
|
66
66
|
def render(self) -> None:
|
|
@@ -68,12 +68,12 @@ class AbstractBox(ABC):
|
|
|
68
68
|
...
|
|
69
69
|
|
|
70
70
|
@abstractmethod
|
|
71
|
-
def to_dict(self) ->
|
|
71
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
72
72
|
"""Convert the box to a dictionary representation."""
|
|
73
73
|
...
|
|
74
74
|
|
|
75
75
|
@abstractmethod
|
|
76
|
-
def __iter__(self):
|
|
76
|
+
def __iter__(self) -> Iterator[Any]:
|
|
77
77
|
"""Make the box iterable."""
|
|
78
78
|
...
|
|
79
79
|
|
|
@@ -99,7 +99,7 @@ class AbstractPatchline(ABC):
|
|
|
99
99
|
...
|
|
100
100
|
|
|
101
101
|
@abstractmethod
|
|
102
|
-
def to_dict(self) ->
|
|
102
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
103
103
|
"""Convert the patchline to a dictionary representation."""
|
|
104
104
|
...
|
|
105
105
|
|
|
@@ -143,4 +143,32 @@ class AbstractPatcher(ABC):
|
|
|
143
143
|
_layout_mgr: AbstractLayoutManager
|
|
144
144
|
_auto_hints: bool
|
|
145
145
|
_validate_connections: bool
|
|
146
|
+
_validate_attrs: bool
|
|
146
147
|
_maxclass_methods: dict[str, Callable[..., Any]]
|
|
148
|
+
_semantic_ids: bool
|
|
149
|
+
_semantic_counters: dict[str, int]
|
|
150
|
+
_device_type: str
|
|
151
|
+
classnamespace: str
|
|
152
|
+
_pending_comments: list[tuple[str, str, Optional[str]]]
|
|
153
|
+
# Rendered (dict) forms, populated by render() and read by serialization.
|
|
154
|
+
boxes: list[dict[str, Any]]
|
|
155
|
+
lines: list[dict[str, Any]]
|
|
156
|
+
|
|
157
|
+
# Core methods implemented by Patcher and relied on by the BoxFactory and
|
|
158
|
+
# serialization mixins. Declared here (non-abstract) so each mixin can be
|
|
159
|
+
# type-checked in isolation; Patcher provides the real implementations.
|
|
160
|
+
def get_id(self, object_name: Optional[str] = None) -> str:
|
|
161
|
+
"""Generate an object id (implemented by Patcher)."""
|
|
162
|
+
raise NotImplementedError
|
|
163
|
+
|
|
164
|
+
def get_pos(self, maxclass: Optional[str] = None) -> Rect:
|
|
165
|
+
"""Get a box position from the layout manager (implemented by Patcher)."""
|
|
166
|
+
raise NotImplementedError
|
|
167
|
+
|
|
168
|
+
def render(self, reset: bool = False) -> None:
|
|
169
|
+
"""Render boxes/lines to dicts (implemented by Patcher)."""
|
|
170
|
+
raise NotImplementedError
|
|
171
|
+
|
|
172
|
+
def _process_pending_comments(self) -> None:
|
|
173
|
+
"""Position deferred associated comments (implemented by Patcher)."""
|
|
174
|
+
raise NotImplementedError
|