py2max 0.3.0__tar.gz → 0.3.3__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 (213) hide show
  1. {py2max-0.3.0 → py2max-0.3.3}/CHANGELOG.md +121 -0
  2. {py2max-0.3.0 → py2max-0.3.3}/PKG-INFO +67 -5
  3. {py2max-0.3.0 → py2max-0.3.3}/README.md +62 -4
  4. {py2max-0.3.0 → py2max-0.3.3}/py2max/__init__.py +1 -1
  5. {py2max-0.3.0 → py2max-0.3.3}/py2max/cli.py +9 -69
  6. {py2max-0.3.0 → py2max-0.3.3}/py2max/core/abstract.py +2 -2
  7. {py2max-0.3.0 → py2max-0.3.3}/py2max/core/factory.py +281 -22
  8. {py2max-0.3.0 → py2max-0.3.3}/py2max/core/patcher.py +69 -5
  9. {py2max-0.3.0 → py2max-0.3.3}/py2max/core/serialization.py +3 -6
  10. {py2max-0.3.0 → py2max-0.3.3}/py2max/export/converters.py +21 -38
  11. py2max-0.3.3/py2max/export/svg.py +565 -0
  12. {py2max-0.3.0 → py2max-0.3.3}/py2max/layout/__init__.py +4 -1
  13. {py2max-0.3.0 → py2max-0.3.3}/py2max/layout/base.py +68 -92
  14. py2max-0.3.3/py2max/layout/external.py +238 -0
  15. {py2max-0.3.0 → py2max-0.3.3}/py2max/layout/flow.py +12 -24
  16. py2max-0.3.3/py2max/layout/graph.py +173 -0
  17. {py2max-0.3.0 → py2max-0.3.3}/py2max/layout/grid.py +9 -53
  18. {py2max-0.3.0 → py2max-0.3.3}/py2max/layout/matrix.py +129 -146
  19. {py2max-0.3.0 → py2max-0.3.3}/py2max/maxref/category.py +5 -4
  20. py2max-0.3.3/py2max/maxref/data/bundle.json.gz +0 -0
  21. {py2max-0.3.0 → py2max-0.3.3}/py2max/maxref/db.py +2 -88
  22. {py2max-0.3.0 → py2max-0.3.3}/py2max/maxref/legacy.py +7 -0
  23. {py2max-0.3.0 → py2max-0.3.3}/py2max/maxref/parser.py +136 -54
  24. {py2max-0.3.0 → py2max-0.3.3}/py2max/transformers.py +69 -6
  25. {py2max-0.3.0 → py2max-0.3.3}/py2max/utils.py +17 -1
  26. {py2max-0.3.0 → py2max-0.3.3}/pyproject.toml +26 -1
  27. py2max-0.3.3/tests/conftest.py +98 -0
  28. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/auto_layout_demo.py +1 -1
  29. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/db/category_db_demo.py +16 -16
  30. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/db/maxref_db_demo.py +13 -13
  31. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/layout/columnar_layout_examples.py +24 -27
  32. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/layout/matrix_layout_examples.py +11 -14
  33. py2max-0.3.3/tests/test_abstraction.py +34 -0
  34. {py2max-0.3.0 → py2max-0.3.3}/tests/test_amxd.py +3 -1
  35. {py2max-0.3.0 → py2max-0.3.3}/tests/test_attrui.py +37 -0
  36. {py2max-0.3.0 → py2max-0.3.3}/tests/test_basic.py +4 -3
  37. {py2max-0.3.0 → py2max-0.3.3}/tests/test_beap.py +8 -1
  38. py2max-0.3.3/tests/test_bpatcher.py +44 -0
  39. py2max-0.3.3/tests/test_coll.py +28 -0
  40. py2max-0.3.3/tests/test_colors.py +44 -0
  41. py2max-0.3.3/tests/test_comment.py +20 -0
  42. {py2max-0.3.0 → py2max-0.3.3}/tests/test_db.py +15 -15
  43. py2max-0.3.3/tests/test_defaults.py +26 -0
  44. py2max-0.3.3/tests/test_dict.py +15 -0
  45. py2max-0.3.3/tests/test_edit_operations.py +362 -0
  46. {py2max-0.3.0 → py2max-0.3.3}/tests/test_examples.py +27 -0
  47. py2max-0.3.3/tests/test_ezdac.py +21 -0
  48. py2max-0.3.3/tests/test_gen.py +96 -0
  49. py2max-0.3.3/tests/test_itable.py +15 -0
  50. {py2max-0.3.0 → py2max-0.3.3}/tests/test_js.py +14 -2
  51. py2max-0.3.3/tests/test_kwds_filter.py +49 -0
  52. py2max-0.3.3/tests/test_layout_box_dims.py +81 -0
  53. {py2max-0.3.0 → py2max-0.3.3}/tests/test_layout_builtins.py +1 -1
  54. {py2max-0.3.0 → py2max-0.3.3}/tests/test_layout_coverage.py +33 -23
  55. py2max-0.3.3/tests/test_layout_graph.py +77 -0
  56. py2max-0.3.3/tests/test_layout_graph_layout.py +103 -0
  57. py2max-0.3.3/tests/test_layout_graph_manager.py +127 -0
  58. {py2max-0.3.0 → py2max-0.3.3}/tests/test_layout_matrix.py +145 -41
  59. py2max-0.3.3/tests/test_layout_overlaps.py +66 -0
  60. {py2max-0.3.0 → py2max-0.3.3}/tests/test_layout_vertical.py +21 -0
  61. py2max-0.3.3/tests/test_linking.py +76 -0
  62. {py2max-0.3.0 → py2max-0.3.3}/tests/test_m4l.py +0 -6
  63. {py2max-0.3.0 → py2max-0.3.3}/tests/test_maxref_bundle.py +8 -7
  64. py2max-0.3.3/tests/test_maxref_lazy.py +55 -0
  65. py2max-0.3.3/tests/test_maxref_parser.py +60 -0
  66. py2max-0.3.3/tests/test_maxref_refpages.py +85 -0
  67. py2max-0.3.3/tests/test_mc_cycle.py +19 -0
  68. py2max-0.3.3/tests/test_message.py +16 -0
  69. py2max-0.3.3/tests/test_mypatch.py +22 -0
  70. py2max-0.3.3/tests/test_number_tilde.py +87 -0
  71. py2max-0.3.3/tests/test_numbers.py +56 -0
  72. py2max-0.3.3/tests/test_param.py +36 -0
  73. {py2max-0.3.0 → py2max-0.3.3}/tests/test_pitched_osc.py +12 -0
  74. {py2max-0.3.0 → py2max-0.3.3}/tests/test_pydantic.py +29 -0
  75. {py2max-0.3.0 → py2max-0.3.3}/tests/test_rnbo.py +78 -1
  76. py2max-0.3.3/tests/test_rnbo_subpatcher.py +36 -0
  77. py2max-0.3.3/tests/test_scripting_name.py +18 -0
  78. py2max-0.3.3/tests/test_subpatch.py +34 -0
  79. py2max-0.3.3/tests/test_svg_fidelity.py +63 -0
  80. {py2max-0.3.0 → py2max-0.3.3}/tests/test_table.py +32 -2
  81. py2max-0.3.3/tests/test_transformers_e4.py +68 -0
  82. py2max-0.3.3/tests/test_tutorial_simple_synthesis.py +100 -0
  83. py2max-0.3.3/tests/test_umenu.py +18 -0
  84. py2max-0.3.3/tests/test_varname.py +26 -0
  85. py2max-0.3.3/tests/test_wheel_bundle.py +76 -0
  86. py2max-0.3.3/tests/test_zl_group.py +28 -0
  87. py2max-0.3.0/py2max/export/svg.py +0 -456
  88. py2max-0.3.0/py2max/maxref/data/bundle.json.gz +0 -0
  89. py2max-0.3.0/tests/conftest.py +0 -21
  90. py2max-0.3.0/tests/registry.py +0 -1526
  91. py2max-0.3.0/tests/scratch.py +0 -80
  92. py2max-0.3.0/tests/test_abstraction.py +0 -21
  93. py2max-0.3.0/tests/test_bpatcher.py +0 -20
  94. py2max-0.3.0/tests/test_coll.py +0 -16
  95. py2max-0.3.0/tests/test_colors.py +0 -9
  96. py2max-0.3.0/tests/test_comment.py +0 -8
  97. py2max-0.3.0/tests/test_defaults.py +0 -11
  98. py2max-0.3.0/tests/test_dict.py +0 -7
  99. py2max-0.3.0/tests/test_ezdac.py +0 -10
  100. py2max-0.3.0/tests/test_gen.py +0 -27
  101. py2max-0.3.0/tests/test_itable.py +0 -7
  102. py2max-0.3.0/tests/test_kwds_filter.py +0 -116
  103. py2max-0.3.0/tests/test_layout_graph_layout.py +0 -113
  104. py2max-0.3.0/tests/test_layout_hola1.py +0 -117
  105. py2max-0.3.0/tests/test_layout_hola2.py +0 -100
  106. py2max-0.3.0/tests/test_layout_hola3.py +0 -123
  107. py2max-0.3.0/tests/test_layout_hola_graph.py +0 -124
  108. py2max-0.3.0/tests/test_layout_networkx1.py +0 -128
  109. py2max-0.3.0/tests/test_layout_networkx2.py +0 -96
  110. py2max-0.3.0/tests/test_layout_nx_graphviz.py +0 -96
  111. py2max-0.3.0/tests/test_layout_nx_orthogonal.py +0 -120
  112. py2max-0.3.0/tests/test_layout_nx_tsmpy.py +0 -108
  113. py2max-0.3.0/tests/test_linking.py +0 -40
  114. py2max-0.3.0/tests/test_mc_cycle.py +0 -7
  115. py2max-0.3.0/tests/test_message.py +0 -7
  116. py2max-0.3.0/tests/test_mypatch.py +0 -12
  117. py2max-0.3.0/tests/test_number_tilde.py +0 -52
  118. py2max-0.3.0/tests/test_numbers.py +0 -34
  119. py2max-0.3.0/tests/test_param.py +0 -8
  120. py2max-0.3.0/tests/test_rnbo_subpatcher.py +0 -19
  121. py2max-0.3.0/tests/test_scripting_name.py +0 -7
  122. py2max-0.3.0/tests/test_subpatch.py +0 -17
  123. py2max-0.3.0/tests/test_tutorial_simple_synthesis.py +0 -44
  124. py2max-0.3.0/tests/test_umenu.py +0 -11
  125. py2max-0.3.0/tests/test_varname.py +0 -11
  126. py2max-0.3.0/tests/test_zl_group.py +0 -11
  127. {py2max-0.3.0 → py2max-0.3.3}/LICENSE +0 -0
  128. {py2max-0.3.0 → py2max-0.3.3}/py2max/__main__.py +0 -0
  129. {py2max-0.3.0 → py2max-0.3.3}/py2max/core/__init__.py +0 -0
  130. {py2max-0.3.0 → py2max-0.3.3}/py2max/core/box.py +0 -0
  131. {py2max-0.3.0 → py2max-0.3.3}/py2max/core/colors.py +0 -0
  132. {py2max-0.3.0 → py2max-0.3.3}/py2max/core/common.py +0 -0
  133. {py2max-0.3.0 → py2max-0.3.3}/py2max/core/patchline.py +0 -0
  134. {py2max-0.3.0 → py2max-0.3.3}/py2max/exceptions.py +0 -0
  135. {py2max-0.3.0 → py2max-0.3.3}/py2max/export/__init__.py +0 -0
  136. {py2max-0.3.0 → py2max-0.3.3}/py2max/log.py +0 -0
  137. {py2max-0.3.0 → py2max-0.3.3}/py2max/m4l.py +0 -0
  138. {py2max-0.3.0 → py2max-0.3.3}/py2max/maxref/__init__.py +0 -0
  139. {py2max-0.3.0 → py2max-0.3.3}/py2max/py.typed +0 -0
  140. {py2max-0.3.0 → py2max-0.3.3}/tests/__init__.py +0 -0
  141. {py2max-0.3.0 → py2max-0.3.3}/tests/data/complex.maxpat +0 -0
  142. {py2max-0.3.0 → py2max-0.3.3}/tests/data/desc.maxpat +0 -0
  143. {py2max-0.3.0 → py2max-0.3.3}/tests/data/empty.maxpat +0 -0
  144. {py2max-0.3.0 → py2max-0.3.3}/tests/data/mydevice.amxd +0 -0
  145. {py2max-0.3.0 → py2max-0.3.3}/tests/data/mydevice2.amxd +0 -0
  146. {py2max-0.3.0 → py2max-0.3.3}/tests/data/nested.maxpat +0 -0
  147. {py2max-0.3.0 → py2max-0.3.3}/tests/data/simple.maxpat +0 -0
  148. {py2max-0.3.0 → py2max-0.3.3}/tests/data/tabular.maxpat +0 -0
  149. {py2max-0.3.0 → py2max-0.3.3}/tests/data/umenu.maxref.xml +0 -0
  150. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/README.md +0 -0
  151. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/advanced/connection_patterns.py +0 -0
  152. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/advanced/custom_extensions.py +0 -0
  153. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/advanced/data_containers.py +0 -0
  154. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/advanced/error_handling.py +0 -0
  155. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/advanced/performance_optimization.py +0 -0
  156. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/advanced/subpatchers.py +0 -0
  157. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/api/box_api_examples.py +0 -0
  158. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/api/patcher_api_examples.py +0 -0
  159. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/layout/flow_layout_examples.py +0 -0
  160. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/layout/grid_layout_examples.py +0 -0
  161. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/basic_synth.maxpat +0 -0
  162. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/basic_synth.svg +0 -0
  163. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/complex_synth.maxpat +0 -0
  164. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/complex_synth.svg +0 -0
  165. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/flow_layout.maxpat +0 -0
  166. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/flow_layout.svg +0 -0
  167. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/grid_layout.maxpat +0 -0
  168. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/grid_layout.svg +0 -0
  169. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/horizontal_layout.maxpat +0 -0
  170. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/horizontal_layout.svg +0 -0
  171. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/styled_no_ports.svg +0 -0
  172. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/styled_no_title.svg +0 -0
  173. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/styled_patch.maxpat +0 -0
  174. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/styled_with_ports.svg +0 -0
  175. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/svg_preview_demo.py +0 -0
  176. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/vertical_layout.maxpat +0 -0
  177. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/vertical_layout.svg +0 -0
  178. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/workflow_demo.maxpat +0 -0
  179. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/preview/workflow_demo.svg +0 -0
  180. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/quickstart/basic_patch.py +0 -0
  181. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/quickstart/layout_examples.py +0 -0
  182. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/tutorial/generative_music.py +0 -0
  183. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/tutorial/interactive_controller.py +0 -0
  184. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/tutorial/signal_processing_chain.py +0 -0
  185. {py2max-0.3.0 → py2max-0.3.3}/tests/examples/tutorial/simple_synthesis.py +0 -0
  186. {py2max-0.3.0 → py2max-0.3.3}/tests/graphs/random/v30e33.tglf +0 -0
  187. {py2max-0.3.0 → py2max-0.3.3}/tests/test_abstract_coverage.py +0 -0
  188. {py2max-0.3.0 → py2max-0.3.3}/tests/test_add.py +0 -0
  189. {py2max-0.3.0 → py2max-0.3.3}/tests/test_cli.py +0 -0
  190. {py2max-0.3.0 → py2max-0.3.3}/tests/test_colors_theme.py +0 -0
  191. {py2max-0.3.0 → py2max-0.3.3}/tests/test_connection_validation.py +0 -0
  192. {py2max-0.3.0 → py2max-0.3.3}/tests/test_converters.py +0 -0
  193. {py2max-0.3.0 → py2max-0.3.3}/tests/test_core_coverage.py +0 -0
  194. {py2max-0.3.0 → py2max-0.3.3}/tests/test_encapsulate.py +0 -0
  195. {py2max-0.3.0 → py2max-0.3.3}/tests/test_error_handling.py +0 -0
  196. {py2max-0.3.0 → py2max-0.3.3}/tests/test_group.py +0 -0
  197. {py2max-0.3.0 → py2max-0.3.3}/tests/test_layout.py +0 -0
  198. {py2max-0.3.0 → py2max-0.3.3}/tests/test_layout_flow.py +0 -0
  199. {py2max-0.3.0 → py2max-0.3.3}/tests/test_maxref.py +0 -0
  200. {py2max-0.3.0 → py2max-0.3.3}/tests/test_mc_poly.py +0 -0
  201. {py2max-0.3.0 → py2max-0.3.3}/tests/test_nested.py +0 -0
  202. {py2max-0.3.0 → py2max-0.3.3}/tests/test_nested_patchers.py +0 -0
  203. {py2max-0.3.0 → py2max-0.3.3}/tests/test_patcher.py +0 -0
  204. {py2max-0.3.0 → py2max-0.3.3}/tests/test_presets.py +0 -0
  205. {py2max-0.3.0 → py2max-0.3.3}/tests/test_search.py +0 -0
  206. {py2max-0.3.0 → py2max-0.3.3}/tests/test_semantic_ids.py +0 -0
  207. {py2max-0.3.0 → py2max-0.3.3}/tests/test_svg.py +0 -0
  208. {py2max-0.3.0 → py2max-0.3.3}/tests/test_transformers.py +0 -0
  209. {py2max-0.3.0 → py2max-0.3.3}/tests/test_tree.py +0 -0
  210. {py2max-0.3.0 → py2max-0.3.3}/tests/test_tree_builder.py +0 -0
  211. {py2max-0.3.0 → py2max-0.3.3}/tests/test_two_sines.py +0 -0
  212. {py2max-0.3.0 → py2max-0.3.3}/tests/test_utils.py +0 -0
  213. {py2max-0.3.0 → py2max-0.3.3}/tests/test_validate_attrs.py +0 -0
@@ -2,6 +2,127 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.3.3]
6
+
7
+ ### Removed: `serve` and `repl` CLI subcommands
8
+
9
+ - 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`).
10
+
11
+ ### Fixed: Graph layouts (`graph:*`) are overlap-free and open on-screen
12
+
13
+ - `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.
14
+ - 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).
15
+
16
+ ### Fixed: Clustered grid layout squashed object sizes
17
+
18
+ - 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.
19
+
20
+ ### Fixed: Example patches use valid Max connections
21
+
22
+ - 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.
23
+
24
+ ### New: `kwds_filter` utility
25
+
26
+ - 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.
27
+
28
+ ### Improved: Build and test-output hygiene
29
+
30
+ - 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.
31
+
32
+ ## [0.3.2]
33
+
34
+ ### New: Patch editing and removal API
35
+
36
+ - 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.
37
+ - 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.
38
+
39
+ ### New: Graph-layout engines as layout managers
40
+
41
+ - 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`.
42
+ - 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`.
43
+
44
+ ### New: Layout gallery generator and docs page
45
+
46
+ - `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.
47
+ - 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.
48
+ - 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.
49
+
50
+ ### Fixed: Generation correctness
51
+
52
+ - `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).
53
+ - `add_beap` strips the `.maxpat` suffix correctly; previously `rstrip(".maxpat")` could truncate names such as `drum.maxpat` down to `dru`.
54
+ - Parallel patchlines between the same source and destination now receive incrementing `order` values, so they spread apart in Max instead of overlapping.
55
+ - Comments created with `comment=` are emitted on every save path (`save_as`, `to_json`, `to_dict`), not only `save()` / `optimize_layout()`.
56
+ - 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.
57
+ - 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.
58
+
59
+ ### Fixed: Layout managers
60
+
61
+ - `layout="columnar"` now works; it previously raised `NotImplementedError` despite being documented.
62
+ - 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()`.
63
+ - 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).
64
+
65
+ ### Improved: maxref bundle loading (lazy and thread-safe)
66
+
67
+ - 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.
68
+ - 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.
69
+
70
+ ### Fixed: maxref outlet digests and parser robustness
71
+
72
+ - 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.
73
+ - 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 `&amp;`, `&quot;`, `&#181;` are left intact).
74
+
75
+ ### Improved: Cross-platform maxref discovery
76
+
77
+ - 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.
78
+
79
+ ### Improved: SVG preview fidelity
80
+
81
+ - The SVG exporter (`Patcher.to_svg` / `py2max preview`) now renders a more faithful preview instead of a uniform grey schematic:
82
+ - **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.
83
+ - **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.
84
+ - **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).
85
+ - `export_svg_string` now builds the document in memory instead of round-tripping through a temporary file.
86
+
87
+ ### Fixed: Layout classification and overlap resolution
88
+
89
+ - 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.
90
+ - `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.
91
+
92
+ ### Improved: Patch transformers
93
+
94
+ - `run_pipeline` now delegates to `compose`, removing a duplicated apply loop and giving the previously-unused (but exported) `compose` helper a real use.
95
+ - 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.
96
+ - 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).
97
+
98
+ ### Fixed: Converters module cleanup
99
+
100
+ - 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).
101
+
102
+ ### Removed: Honest CLI help for the moved `serve` / `repl` subcommands
103
+
104
+ - 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.
105
+
106
+ ### Removed: MaxRefDB deprecated alias methods
107
+
108
+ - 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.
109
+ - 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.
110
+
111
+ ### Removed: Obsolete layout experiments and vendored editor assets
112
+
113
+ - 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`.
114
+ - 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.
115
+ - 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.
116
+
117
+ ## [0.3.1]
118
+
119
+ ### New: Standalone `gen.codebox~` Support
120
+
121
+ - `Patcher.add_gen_codebox(code)` adds a self-contained `gen.codebox~` object -- a complete gen patch in a single box that lives directly in a regular Max patcher, distinct from the inner `codebox~` (emitted by `add_codebox`) that belongs inside a `gen~`/`rnbo~` subpatcher. This is the form emitted by gen transpilers. Code newlines are normalized to CRLF as Max expects, and `fontname`/`fontsize` default to the monospaced gen style.
122
+ - Inlet/outlet counts are derived automatically from the code (the highest `inN` / `outN` references, floor of 1), matching gen's dynamic-I/O semantics. Explicit `numinlets` / `numoutlets` still override.
123
+ - Available via the `add()` string shortcut too: `p.add("gen.codebox~ out1 = in1 * 0.5;")`. The shortcut suits single-line / `;`-terminated code; pass multi-line source to `add_gen_codebox()` directly.
124
+ - Connection validation for `gen.codebox~` (and `codebox` / `codebox~`) now bound-checks against the box's own declared inlet/outlet counts rather than the static `.maxref.xml` entry, since codebox I/O is code-dependent. This both allows valid connections to/from wider codeboxes (e.g. from a second outlet) and rejects genuinely out-of-range ones.
125
+
5
126
  ## [0.3.0]
6
127
 
7
128
  ### Removed: Interactive Server Split Into `py2max-server` (breaking)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py2max
3
- Version: 0.3.0
3
+ Version: 0.3.3
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
@@ -84,8 +88,38 @@ That's it! Open `my-synth.maxpat` in Max to see your patch.
84
88
 
85
89
  - **Offline Patch Generation** - Create Max patches programmatically without Max running
86
90
  - **Round-trip Conversion** - Load, modify, and save existing `.maxpat` files
91
+ - **Max for Live (.amxd)** - Read/write binary `.amxd` device files with presentation-mode helpers
87
92
  - **Universal Object Support** - Works with any Max/MSP/Jitter object
88
- - **99% Test Coverage** - 418+ tests ensure reliability
93
+ - **Fully typed** - Passes `mypy --strict`; no runtime dependencies
94
+ - **High Test Coverage** - 420+ tests ensure reliability
95
+
96
+ ### Max for Live (.amxd)
97
+
98
+ Generate Max for Live devices directly. `Patcher.save()` / `Patcher.from_file()`
99
+ auto-detect the `.amxd` extension and read/write the binary device format,
100
+ byte-for-byte compatible with Max-exported devices.
101
+
102
+ ```python
103
+ from py2max import Patcher
104
+
105
+ # device_type: "audio_effect" (default), "instrument", or "midi_effect"
106
+ p = Patcher('gain.amxd', device_type='audio_effect')
107
+ p.enable_presentation(devicewidth=120) # render Ableton's device strip
108
+
109
+ plugin = p.add_textbox('plugin~') # audio in from Live
110
+ gain = p.add('live.gain~', maxclass='live.gain~')
111
+ plugout = p.add_textbox('plugout~') # audio back to Live
112
+ gain.add_to_presentation([20, 20, 60, 136]) # show the fader in the device
113
+
114
+ p.add_line(plugin, gain, outlet=0, inlet=0)
115
+ p.add_line(gain, plugout, outlet=0, inlet=0)
116
+ p.save() # writes a binary .amxd
117
+ ```
118
+
119
+ Helpers: `Patcher.enable_presentation(devicewidth=...)`,
120
+ `Box.add_to_presentation([x, y, w, h])` (rejects M4L infrastructure objects and
121
+ rounds fractional coordinates), and `Patcher.enforce_integer_coords()`. M4L
122
+ binary helpers live in `py2max.m4l`.
89
123
 
90
124
  ### Interactive Server (separate package)
91
125
 
@@ -142,7 +176,7 @@ p.save()
142
176
 
143
177
  ### MaxRef Integration
144
178
 
145
- Access documentation for 1157 Max objects:
179
+ Access documentation for 1175 Max objects:
146
180
 
147
181
  ```python
148
182
  p = Patcher('demo.maxpat')
@@ -186,10 +220,10 @@ osc = p.find_by_id('cycle_1')
186
220
  Query Max object metadata efficiently:
187
221
 
188
222
  ```python
189
- from py2max import MaxRefDB
223
+ from py2max.maxref import MaxRefDB
190
224
 
191
225
  db = MaxRefDB() # Auto-cached on first use
192
- print(len(db)) # 1157 objects
226
+ print(len(db)) # 1175 objects
193
227
 
194
228
  if 'cycle~' in db:
195
229
  info = db['cycle~']
@@ -252,6 +286,34 @@ p.link(sbox, dac)
252
286
  p.save()
253
287
  ```
254
288
 
289
+ ### Gen Codebox
290
+
291
+ `add_gen_codebox()` adds a standalone `gen.codebox~` object -- a complete gen
292
+ patch in a single box that sits directly in a regular Max patcher (unlike the
293
+ inner `codebox~` from `add_codebox()`, which belongs inside a `gen~`/`rnbo~`
294
+ subpatcher). Inlet/outlet counts are derived automatically from the highest
295
+ `inN`/`outN` references in the code:
296
+
297
+ ```python
298
+ p = Patcher('fbdelay.maxpat')
299
+
300
+ # 1 inlet (in1), 1 outlet (out1)
301
+ osc = p.add('cycle~ 440')
302
+ cb = p.add_gen_codebox('''
303
+ Param feedback(0.5, min=0.0, max=0.95);
304
+ History fb(0.0);
305
+ out1 = in1 + fb * feedback;
306
+ fb = out1;
307
+ ''')
308
+ dac = p.add('ezdac~')
309
+ p.link(osc, cb)
310
+ p.link(cb, dac)
311
+ p.save()
312
+
313
+ # Or via the add() string shortcut (single-line / `;`-terminated code)
314
+ cb = p.add('gen.codebox~ out1 = in1 * 0.5;')
315
+ ```
316
+
255
317
  ### Object Search
256
318
 
257
319
  ```python
@@ -51,8 +51,38 @@ That's it! Open `my-synth.maxpat` in Max to see your patch.
51
51
 
52
52
  - **Offline Patch Generation** - Create Max patches programmatically without Max running
53
53
  - **Round-trip Conversion** - Load, modify, and save existing `.maxpat` files
54
+ - **Max for Live (.amxd)** - Read/write binary `.amxd` device files with presentation-mode helpers
54
55
  - **Universal Object Support** - Works with any Max/MSP/Jitter object
55
- - **99% Test Coverage** - 418+ tests ensure reliability
56
+ - **Fully typed** - Passes `mypy --strict`; no runtime dependencies
57
+ - **High Test Coverage** - 420+ tests ensure reliability
58
+
59
+ ### Max for Live (.amxd)
60
+
61
+ Generate Max for Live devices directly. `Patcher.save()` / `Patcher.from_file()`
62
+ auto-detect the `.amxd` extension and read/write the binary device format,
63
+ byte-for-byte compatible with Max-exported devices.
64
+
65
+ ```python
66
+ from py2max import Patcher
67
+
68
+ # device_type: "audio_effect" (default), "instrument", or "midi_effect"
69
+ p = Patcher('gain.amxd', device_type='audio_effect')
70
+ p.enable_presentation(devicewidth=120) # render Ableton's device strip
71
+
72
+ plugin = p.add_textbox('plugin~') # audio in from Live
73
+ gain = p.add('live.gain~', maxclass='live.gain~')
74
+ plugout = p.add_textbox('plugout~') # audio back to Live
75
+ gain.add_to_presentation([20, 20, 60, 136]) # show the fader in the device
76
+
77
+ p.add_line(plugin, gain, outlet=0, inlet=0)
78
+ p.add_line(gain, plugout, outlet=0, inlet=0)
79
+ p.save() # writes a binary .amxd
80
+ ```
81
+
82
+ Helpers: `Patcher.enable_presentation(devicewidth=...)`,
83
+ `Box.add_to_presentation([x, y, w, h])` (rejects M4L infrastructure objects and
84
+ rounds fractional coordinates), and `Patcher.enforce_integer_coords()`. M4L
85
+ binary helpers live in `py2max.m4l`.
56
86
 
57
87
  ### Interactive Server (separate package)
58
88
 
@@ -109,7 +139,7 @@ p.save()
109
139
 
110
140
  ### MaxRef Integration
111
141
 
112
- Access documentation for 1157 Max objects:
142
+ Access documentation for 1175 Max objects:
113
143
 
114
144
  ```python
115
145
  p = Patcher('demo.maxpat')
@@ -153,10 +183,10 @@ osc = p.find_by_id('cycle_1')
153
183
  Query Max object metadata efficiently:
154
184
 
155
185
  ```python
156
- from py2max import MaxRefDB
186
+ from py2max.maxref import MaxRefDB
157
187
 
158
188
  db = MaxRefDB() # Auto-cached on first use
159
- print(len(db)) # 1157 objects
189
+ print(len(db)) # 1175 objects
160
190
 
161
191
  if 'cycle~' in db:
162
192
  info = db['cycle~']
@@ -219,6 +249,34 @@ p.link(sbox, dac)
219
249
  p.save()
220
250
  ```
221
251
 
252
+ ### Gen Codebox
253
+
254
+ `add_gen_codebox()` adds a standalone `gen.codebox~` object -- a complete gen
255
+ patch in a single box that sits directly in a regular Max patcher (unlike the
256
+ inner `codebox~` from `add_codebox()`, which belongs inside a `gen~`/`rnbo~`
257
+ subpatcher). Inlet/outlet counts are derived automatically from the highest
258
+ `inN`/`outN` references in the code:
259
+
260
+ ```python
261
+ p = Patcher('fbdelay.maxpat')
262
+
263
+ # 1 inlet (in1), 1 outlet (out1)
264
+ osc = p.add('cycle~ 440')
265
+ cb = p.add_gen_codebox('''
266
+ Param feedback(0.5, min=0.0, max=0.95);
267
+ History fb(0.0);
268
+ out1 = in1 + fb * feedback;
269
+ fb = out1;
270
+ ''')
271
+ dac = p.add('ezdac~')
272
+ p.link(osc, cb)
273
+ p.link(cb, dac)
274
+ p.save()
275
+
276
+ # Or via the add() string shortcut (single-line / `;`-terminated code)
277
+ cb = p.add('gen.codebox~ out1 = in1 * 0.5;')
278
+ ```
279
+
222
280
  ### Object Search
223
281
 
224
282
  ```python
@@ -35,7 +35,7 @@ Example:
35
35
  >>> p.save()
36
36
  """
37
37
 
38
- __version__ = "0.3.0"
38
+ __version__ = "0.3.3"
39
39
 
40
40
  from .core import Box, Patcher, Patchline
41
41
  from .exceptions import (
@@ -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,6 +225,7 @@ 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)
@@ -272,6 +276,7 @@ def _parse_transform_spec(spec: str) -> tuple[str, str | None]:
272
276
 
273
277
 
274
278
  def cmd_transform(args: argparse.Namespace) -> int:
279
+ """Apply one or more transformers to a patcher (or list available ones)."""
275
280
  if args.list_transformers:
276
281
  for name, desc in available_transformers().items():
277
282
  print(f"{name}: {desc}")
@@ -316,6 +321,7 @@ def cmd_transform(args: argparse.Namespace) -> int:
316
321
 
317
322
 
318
323
  def cmd_convert(args: argparse.Namespace) -> int:
324
+ """Convert between formats (e.g. .maxpat to Python, maxref to SQLite)."""
319
325
  if args.mode == "maxpat-to-python":
320
326
  if not args.input or not args.output:
321
327
  print(
@@ -636,19 +642,6 @@ def cmd_db_cache(args: argparse.Namespace) -> int:
636
642
  return 1
637
643
 
638
644
 
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
645
  def cmd_preview(args: argparse.Namespace) -> int:
653
646
  """Generate SVG preview of a patcher."""
654
647
  import tempfile
@@ -695,20 +688,8 @@ def cmd_preview(args: argparse.Namespace) -> int:
695
688
  return 1
696
689
 
697
690
 
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
691
  def cmd_maxref(args: argparse.Namespace) -> int:
692
+ """Query the Max object reference (list objects or show one's details)."""
712
693
  cache = MaxRefCache()
713
694
 
714
695
  if args.list:
@@ -783,6 +764,7 @@ def cmd_maxref(args: argparse.Namespace) -> int:
783
764
 
784
765
 
785
766
  def build_parser() -> argparse.ArgumentParser:
767
+ """Construct the top-level argument parser with all subcommands."""
786
768
  parser = argparse.ArgumentParser(
787
769
  prog="py2max", description="Utilities for working with Max patchers."
788
770
  )
@@ -836,33 +818,6 @@ def build_parser() -> argparse.ArgumentParser:
836
818
  val_parser.add_argument("path", help="Target .maxpat path")
837
819
  val_parser.set_defaults(func=cmd_validate)
838
820
 
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
821
  preview_parser = subparsers.add_parser(
867
822
  "preview", help="Generate SVG preview of a patcher"
868
823
  )
@@ -1094,26 +1049,11 @@ def build_parser() -> argparse.ArgumentParser:
1094
1049
  )
1095
1050
  cache_clear.set_defaults(func=cmd_db)
1096
1051
 
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
1052
  return parser
1114
1053
 
1115
1054
 
1116
1055
  def main(argv: List[str] | None = None) -> int:
1056
+ """CLI entry point: parse ``argv`` and dispatch to the subcommand handler."""
1117
1057
  parser = build_parser()
1118
1058
  args = parser.parse_args(argv)
1119
1059
 
@@ -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