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.
Files changed (203) hide show
  1. {py2max-0.2.1 → py2max-0.3.0}/CHANGELOG.md +92 -0
  2. {py2max-0.2.1 → py2max-0.3.0}/PKG-INFO +32 -21
  3. {py2max-0.2.1 → py2max-0.3.0}/README.md +26 -13
  4. {py2max-0.2.1 → py2max-0.3.0}/py2max/__init__.py +5 -4
  5. {py2max-0.2.1 → py2max-0.3.0}/py2max/cli.py +38 -158
  6. {py2max-0.2.1 → py2max-0.3.0}/py2max/core/abstract.py +33 -5
  7. {py2max-0.2.1 → py2max-0.3.0}/py2max/core/box.py +80 -29
  8. py2max-0.3.0/py2max/core/colors.py +83 -0
  9. py2max-0.2.1/py2max/core/patcher.py → py2max-0.3.0/py2max/core/factory.py +423 -695
  10. py2max-0.3.0/py2max/core/patcher.py +596 -0
  11. {py2max-0.2.1 → py2max-0.3.0}/py2max/core/patchline.py +13 -10
  12. py2max-0.3.0/py2max/core/serialization.py +122 -0
  13. {py2max-0.2.1 → py2max-0.3.0}/py2max/export/converters.py +11 -8
  14. {py2max-0.2.1 → py2max-0.3.0}/py2max/export/svg.py +86 -23
  15. {py2max-0.2.1 → py2max-0.3.0}/py2max/layout/base.py +5 -5
  16. {py2max-0.2.1 → py2max-0.3.0}/py2max/layout/flow.py +25 -14
  17. {py2max-0.2.1 → py2max-0.3.0}/py2max/layout/grid.py +26 -18
  18. {py2max-0.2.1 → py2max-0.3.0}/py2max/layout/matrix.py +22 -17
  19. {py2max-0.2.1 → py2max-0.3.0}/py2max/log.py +4 -2
  20. py2max-0.3.0/py2max/m4l.py +626 -0
  21. {py2max-0.2.1 → py2max-0.3.0}/py2max/maxref/__init__.py +15 -3
  22. py2max-0.3.0/py2max/maxref/data/bundle.json.gz +0 -0
  23. {py2max-0.2.1 → py2max-0.3.0}/py2max/maxref/db.py +42 -24
  24. {py2max-0.2.1 → py2max-0.3.0}/py2max/maxref/legacy.py +13 -5
  25. {py2max-0.2.1 → py2max-0.3.0}/py2max/maxref/parser.py +95 -26
  26. {py2max-0.2.1 → py2max-0.3.0}/py2max/utils.py +22 -1
  27. {py2max-0.2.1 → py2max-0.3.0}/pyproject.toml +13 -15
  28. py2max-0.3.0/tests/conftest.py +21 -0
  29. py2max-0.3.0/tests/data/mydevice.amxd +0 -0
  30. py2max-0.3.0/tests/data/mydevice2.amxd +0 -0
  31. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/advanced/data_containers.py +5 -5
  32. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/api/box_api_examples.py +7 -7
  33. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/api/patcher_api_examples.py +12 -12
  34. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/auto_layout_demo.py +14 -14
  35. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/layout/columnar_layout_examples.py +5 -5
  36. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/layout/grid_layout_examples.py +1 -1
  37. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/layout/matrix_layout_examples.py +2 -2
  38. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/svg_preview_demo.py +7 -7
  39. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/quickstart/layout_examples.py +1 -1
  40. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/tutorial/generative_music.py +1 -1
  41. {py2max-0.2.1 → py2max-0.3.0}/tests/test_abstract_coverage.py +1 -1
  42. py2max-0.3.0/tests/test_amxd.py +336 -0
  43. py2max-0.3.0/tests/test_basic.py +22 -0
  44. {py2max-0.2.1 → py2max-0.3.0}/tests/test_bpatcher.py +1 -1
  45. {py2max-0.2.1 → py2max-0.3.0}/tests/test_cli.py +0 -1
  46. py2max-0.3.0/tests/test_colors_theme.py +94 -0
  47. py2max-0.3.0/tests/test_comment.py +8 -0
  48. {py2max-0.2.1 → py2max-0.3.0}/tests/test_connection_validation.py +2 -2
  49. {py2max-0.2.1 → py2max-0.3.0}/tests/test_converters.py +1 -2
  50. {py2max-0.2.1 → py2max-0.3.0}/tests/test_core_coverage.py +25 -11
  51. {py2max-0.2.1 → py2max-0.3.0}/tests/test_db.py +0 -2
  52. {py2max-0.2.1 → py2max-0.3.0}/tests/test_dict.py +1 -1
  53. py2max-0.3.0/tests/test_encapsulate.py +106 -0
  54. {py2max-0.2.1 → py2max-0.3.0}/tests/test_error_handling.py +14 -7
  55. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout.py +0 -1
  56. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_builtins.py +6 -6
  57. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_coverage.py +26 -28
  58. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_flow.py +4 -4
  59. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_hola1.py +5 -2
  60. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_hola_graph.py +1 -1
  61. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_matrix.py +0 -1
  62. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_networkx2.py +3 -3
  63. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_nx_graphviz.py +3 -3
  64. py2max-0.3.0/tests/test_m4l.py +140 -0
  65. py2max-0.3.0/tests/test_maxref_bundle.py +91 -0
  66. {py2max-0.2.1 → py2max-0.3.0}/tests/test_mc_cycle.py +1 -1
  67. py2max-0.3.0/tests/test_mc_poly.py +40 -0
  68. {py2max-0.2.1 → py2max-0.3.0}/tests/test_nested.py +6 -2
  69. py2max-0.3.0/tests/test_nested_patchers.py +50 -0
  70. {py2max-0.2.1 → py2max-0.3.0}/tests/test_number_tilde.py +5 -5
  71. {py2max-0.2.1 → py2max-0.3.0}/tests/test_param.py +2 -2
  72. {py2max-0.2.1 → py2max-0.3.0}/tests/test_patcher.py +9 -5
  73. py2max-0.3.0/tests/test_presets.py +64 -0
  74. {py2max-0.2.1 → py2max-0.3.0}/tests/test_pydantic.py +6 -8
  75. {py2max-0.2.1 → py2max-0.3.0}/tests/test_rnbo.py +1 -2
  76. {py2max-0.2.1 → py2max-0.3.0}/tests/test_search.py +12 -12
  77. {py2max-0.2.1 → py2max-0.3.0}/tests/test_svg.py +48 -10
  78. {py2max-0.2.1 → py2max-0.3.0}/tests/test_table.py +6 -2
  79. {py2max-0.2.1 → py2max-0.3.0}/tests/test_tree_builder.py +3 -1
  80. {py2max-0.2.1 → py2max-0.3.0}/tests/test_tutorial_simple_synthesis.py +1 -1
  81. {py2max-0.2.1 → py2max-0.3.0}/tests/test_two_sines.py +10 -0
  82. py2max-0.3.0/tests/test_validate_attrs.py +66 -0
  83. py2max-0.2.1/py2max/server/__init__.py +0 -54
  84. py2max-0.2.1/py2max/server/client.py +0 -295
  85. py2max-0.2.1/py2max/server/inline.py +0 -312
  86. py2max-0.2.1/py2max/server/repl.py +0 -561
  87. py2max-0.2.1/py2max/server/rpc.py +0 -240
  88. py2max-0.2.1/py2max/server/websocket.py +0 -997
  89. py2max-0.2.1/py2max/static/cola.min.js +0 -4
  90. py2max-0.2.1/py2max/static/d3.v7.min.js +0 -2
  91. py2max-0.2.1/py2max/static/dagre-bundle.js +0 -328
  92. py2max-0.2.1/py2max/static/elk.bundled.js +0 -6663
  93. py2max-0.2.1/py2max/static/index.html +0 -168
  94. py2max-0.2.1/py2max/static/interactive.html +0 -589
  95. py2max-0.2.1/py2max/static/interactive.js +0 -2111
  96. py2max-0.2.1/py2max/static/live-preview.js +0 -324
  97. py2max-0.2.1/py2max/static/svg.min.js +0 -13
  98. py2max-0.2.1/py2max/static/svg.min.js.map +0 -1
  99. py2max-0.2.1/tests/examples/info_command_demo.py +0 -106
  100. py2max-0.2.1/tests/examples/inline_repl_verification.py +0 -64
  101. py2max-0.2.1/tests/examples/interactive_demo.py +0 -228
  102. py2max-0.2.1/tests/examples/interactive_save_demo.py +0 -164
  103. py2max-0.2.1/tests/examples/live_preview_demo.py +0 -225
  104. py2max-0.2.1/tests/examples/refresh_function_verification.py +0 -54
  105. py2max-0.2.1/tests/examples/repl_client_server_demo.py +0 -171
  106. py2max-0.2.1/tests/examples/repl_quickstart.py +0 -140
  107. py2max-0.2.1/tests/test_basic.py +0 -11
  108. py2max-0.2.1/tests/test_comment.py +0 -8
  109. py2max-0.2.1/tests/test_nested_patchers.py +0 -279
  110. py2max-0.2.1/tests/test_repl.py +0 -309
  111. py2max-0.2.1/tests/test_repl_client.py +0 -348
  112. py2max-0.2.1/tests/test_repl_inline.py +0 -283
  113. py2max-0.2.1/tests/test_repl_server.py +0 -316
  114. py2max-0.2.1/tests/test_websocket.py +0 -232
  115. {py2max-0.2.1 → py2max-0.3.0}/LICENSE +0 -0
  116. {py2max-0.2.1 → py2max-0.3.0}/py2max/__main__.py +0 -0
  117. {py2max-0.2.1 → py2max-0.3.0}/py2max/core/__init__.py +0 -0
  118. {py2max-0.2.1 → py2max-0.3.0}/py2max/core/common.py +0 -0
  119. {py2max-0.2.1 → py2max-0.3.0}/py2max/exceptions.py +0 -0
  120. {py2max-0.2.1 → py2max-0.3.0}/py2max/export/__init__.py +0 -0
  121. {py2max-0.2.1 → py2max-0.3.0}/py2max/layout/__init__.py +0 -0
  122. {py2max-0.2.1 → py2max-0.3.0}/py2max/maxref/category.py +0 -0
  123. {py2max-0.2.1 → py2max-0.3.0}/py2max/py.typed +0 -0
  124. {py2max-0.2.1 → py2max-0.3.0}/py2max/transformers.py +0 -0
  125. {py2max-0.2.1 → py2max-0.3.0}/tests/__init__.py +0 -0
  126. {py2max-0.2.1 → py2max-0.3.0}/tests/data/complex.maxpat +0 -0
  127. {py2max-0.2.1 → py2max-0.3.0}/tests/data/desc.maxpat +0 -0
  128. {py2max-0.2.1 → py2max-0.3.0}/tests/data/empty.maxpat +0 -0
  129. {py2max-0.2.1 → py2max-0.3.0}/tests/data/nested.maxpat +0 -0
  130. {py2max-0.2.1 → py2max-0.3.0}/tests/data/simple.maxpat +0 -0
  131. {py2max-0.2.1 → py2max-0.3.0}/tests/data/tabular.maxpat +0 -0
  132. {py2max-0.2.1 → py2max-0.3.0}/tests/data/umenu.maxref.xml +0 -0
  133. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/README.md +0 -0
  134. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/advanced/connection_patterns.py +0 -0
  135. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/advanced/custom_extensions.py +0 -0
  136. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/advanced/error_handling.py +0 -0
  137. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/advanced/performance_optimization.py +0 -0
  138. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/advanced/subpatchers.py +0 -0
  139. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/db/category_db_demo.py +0 -0
  140. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/db/maxref_db_demo.py +0 -0
  141. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/layout/flow_layout_examples.py +0 -0
  142. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/basic_synth.maxpat +0 -0
  143. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/basic_synth.svg +0 -0
  144. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/complex_synth.maxpat +0 -0
  145. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/complex_synth.svg +0 -0
  146. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/flow_layout.maxpat +0 -0
  147. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/flow_layout.svg +0 -0
  148. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/grid_layout.maxpat +0 -0
  149. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/grid_layout.svg +0 -0
  150. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/horizontal_layout.maxpat +0 -0
  151. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/horizontal_layout.svg +0 -0
  152. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/styled_no_ports.svg +0 -0
  153. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/styled_no_title.svg +0 -0
  154. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/styled_patch.maxpat +0 -0
  155. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/styled_with_ports.svg +0 -0
  156. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/vertical_layout.maxpat +0 -0
  157. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/vertical_layout.svg +0 -0
  158. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/workflow_demo.maxpat +0 -0
  159. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/preview/workflow_demo.svg +0 -0
  160. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/quickstart/basic_patch.py +0 -0
  161. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/tutorial/interactive_controller.py +0 -0
  162. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/tutorial/signal_processing_chain.py +0 -0
  163. {py2max-0.2.1 → py2max-0.3.0}/tests/examples/tutorial/simple_synthesis.py +0 -0
  164. {py2max-0.2.1 → py2max-0.3.0}/tests/graphs/random/v30e33.tglf +0 -0
  165. {py2max-0.2.1 → py2max-0.3.0}/tests/registry.py +0 -0
  166. {py2max-0.2.1 → py2max-0.3.0}/tests/scratch.py +0 -0
  167. {py2max-0.2.1 → py2max-0.3.0}/tests/test_abstraction.py +0 -0
  168. {py2max-0.2.1 → py2max-0.3.0}/tests/test_add.py +0 -0
  169. {py2max-0.2.1 → py2max-0.3.0}/tests/test_attrui.py +0 -0
  170. {py2max-0.2.1 → py2max-0.3.0}/tests/test_beap.py +0 -0
  171. {py2max-0.2.1 → py2max-0.3.0}/tests/test_coll.py +0 -0
  172. {py2max-0.2.1 → py2max-0.3.0}/tests/test_colors.py +0 -0
  173. {py2max-0.2.1 → py2max-0.3.0}/tests/test_defaults.py +0 -0
  174. {py2max-0.2.1 → py2max-0.3.0}/tests/test_examples.py +0 -0
  175. {py2max-0.2.1 → py2max-0.3.0}/tests/test_ezdac.py +0 -0
  176. {py2max-0.2.1 → py2max-0.3.0}/tests/test_gen.py +0 -0
  177. {py2max-0.2.1 → py2max-0.3.0}/tests/test_group.py +0 -0
  178. {py2max-0.2.1 → py2max-0.3.0}/tests/test_itable.py +0 -0
  179. {py2max-0.2.1 → py2max-0.3.0}/tests/test_js.py +0 -0
  180. {py2max-0.2.1 → py2max-0.3.0}/tests/test_kwds_filter.py +0 -0
  181. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_graph_layout.py +0 -0
  182. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_hola2.py +0 -0
  183. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_hola3.py +0 -0
  184. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_networkx1.py +0 -0
  185. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_nx_orthogonal.py +0 -0
  186. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_nx_tsmpy.py +0 -0
  187. {py2max-0.2.1 → py2max-0.3.0}/tests/test_layout_vertical.py +0 -0
  188. {py2max-0.2.1 → py2max-0.3.0}/tests/test_linking.py +0 -0
  189. {py2max-0.2.1 → py2max-0.3.0}/tests/test_maxref.py +0 -0
  190. {py2max-0.2.1 → py2max-0.3.0}/tests/test_message.py +0 -0
  191. {py2max-0.2.1 → py2max-0.3.0}/tests/test_mypatch.py +0 -0
  192. {py2max-0.2.1 → py2max-0.3.0}/tests/test_numbers.py +0 -0
  193. {py2max-0.2.1 → py2max-0.3.0}/tests/test_pitched_osc.py +0 -0
  194. {py2max-0.2.1 → py2max-0.3.0}/tests/test_rnbo_subpatcher.py +0 -0
  195. {py2max-0.2.1 → py2max-0.3.0}/tests/test_scripting_name.py +0 -0
  196. {py2max-0.2.1 → py2max-0.3.0}/tests/test_semantic_ids.py +0 -0
  197. {py2max-0.2.1 → py2max-0.3.0}/tests/test_subpatch.py +0 -0
  198. {py2max-0.2.1 → py2max-0.3.0}/tests/test_transformers.py +0 -0
  199. {py2max-0.2.1 → py2max-0.3.0}/tests/test_tree.py +0 -0
  200. {py2max-0.2.1 → py2max-0.3.0}/tests/test_umenu.py +0 -0
  201. {py2max-0.2.1 → py2max-0.3.0}/tests/test_utils.py +0 -0
  202. {py2max-0.2.1 → py2max-0.3.0}/tests/test_varname.py +0 -0
  203. {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.2.1
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
- Requires-Dist: websockets>=12.0 ; extra == 'server'
26
- Requires-Dist: ptpython>=3.0.0 ; extra == 'server'
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: Issues, https://github.com/shakfu/py2max/issues
28
+ Project-URL: Documentation, https://github.com/shakfu/py2max#readme
32
29
  Project-URL: Repository, https://github.com/shakfu/py2max.git
33
- Provides-Extra: server
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
- # With interactive server support
51
- pip install py2max[server]
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 (New in 0.2.x)
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
- py2max serve my-patch.maxpat
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
- See the `examples/` directory for demonstrations:
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
- - `auto_layout_demo.py` - Complex synthesizer with layout optimization
362
- - `nested_patcher_demo.py` - Subpatcher navigation
363
- - `columnar_layout_demo.py` - Functional column organization
364
- - `matrix_layout_demo.py` - Signal chain matrix layout
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
- # With interactive server support
16
- pip install py2max[server]
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 (New in 0.2.x)
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
- py2max serve my-patch.maxpat
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
- See the `examples/` directory for demonstrations:
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
- - `auto_layout_demo.py` - Complex synthesizer with layout optimization
327
- - `nested_patcher_demo.py` - Subpatcher navigation
328
- - `columnar_layout_demo.py` - Functional column organization
329
- - `matrix_layout_demo.py` - Signal chain matrix layout
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
- MaxRefDB: SQLite database for Max object reference data
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.2.1"
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 InvalidConnectionError
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
- maxclass = getattr(box, "maxclass", "newobj")
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
- def _unique_object_labels(boxes: Iterable) -> List[str]:
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: dict) -> List[str]:
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: dict) -> None:
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: dict) -> None:
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: dict) -> str:
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
- """Start interactive WebSocket server for a patcher."""
643
- import asyncio
644
-
645
- input_path = Path(args.input)
646
-
647
- if not input_path.exists():
648
- print(f"Input file not found: {input_path}", file=sys.stderr)
649
- return 1
650
-
651
- # Check if websockets is installed
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
- """Connect to remote REPL server."""
794
- import asyncio
795
-
796
- # Parse server address
797
- server = args.server
798
- if ":" in server:
799
- host, port_str = server.rsplit(":", 1)
800
- try:
801
- port = int(port_str)
802
- except ValueError:
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 InvalidConnectionError as exc:
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: dict
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) -> dict:
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) -> dict:
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