py2max 0.3.1__tar.gz → 0.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. {py2max-0.3.1 → py2max-0.3.4}/CHANGELOG.md +129 -0
  2. {py2max-0.3.1 → py2max-0.3.4}/PKG-INFO +8 -4
  3. {py2max-0.3.1 → py2max-0.3.4}/README.md +3 -3
  4. {py2max-0.3.1 → py2max-0.3.4}/py2max/__init__.py +5 -1
  5. {py2max-0.3.1 → py2max-0.3.4}/py2max/cli.py +22 -104
  6. {py2max-0.3.1 → py2max-0.3.4}/py2max/core/abstract.py +2 -2
  7. {py2max-0.3.1 → py2max-0.3.4}/py2max/core/factory.py +178 -20
  8. {py2max-0.3.1 → py2max-0.3.4}/py2max/core/patcher.py +102 -6
  9. {py2max-0.3.1 → py2max-0.3.4}/py2max/core/serialization.py +30 -6
  10. {py2max-0.3.1 → py2max-0.3.4}/py2max/export/converters.py +21 -38
  11. py2max-0.3.4/py2max/export/svg.py +565 -0
  12. {py2max-0.3.1 → py2max-0.3.4}/py2max/layout/__init__.py +4 -1
  13. {py2max-0.3.1 → py2max-0.3.4}/py2max/layout/base.py +190 -93
  14. py2max-0.3.4/py2max/layout/external.py +238 -0
  15. {py2max-0.3.1 → py2max-0.3.4}/py2max/layout/flow.py +12 -24
  16. py2max-0.3.4/py2max/layout/graph.py +173 -0
  17. {py2max-0.3.1 → py2max-0.3.4}/py2max/layout/grid.py +9 -53
  18. {py2max-0.3.1 → py2max-0.3.4}/py2max/layout/matrix.py +129 -146
  19. py2max-0.3.4/py2max/lint.py +226 -0
  20. {py2max-0.3.1 → py2max-0.3.4}/py2max/maxref/category.py +5 -4
  21. py2max-0.3.4/py2max/maxref/data/bundle.json.gz +0 -0
  22. {py2max-0.3.1 → py2max-0.3.4}/py2max/maxref/db.py +2 -88
  23. {py2max-0.3.1 → py2max-0.3.4}/py2max/maxref/parser.py +185 -98
  24. py2max-0.3.4/py2max/maxref/porttypes.py +321 -0
  25. {py2max-0.3.1 → py2max-0.3.4}/py2max/transformers.py +69 -6
  26. {py2max-0.3.1 → py2max-0.3.4}/py2max/utils.py +17 -1
  27. {py2max-0.3.1 → py2max-0.3.4}/pyproject.toml +26 -1
  28. py2max-0.3.4/tests/conftest.py +98 -0
  29. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/auto_layout_demo.py +1 -1
  30. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/db/category_db_demo.py +16 -16
  31. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/db/maxref_db_demo.py +13 -13
  32. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/layout/columnar_layout_examples.py +24 -27
  33. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/layout/matrix_layout_examples.py +11 -14
  34. py2max-0.3.4/tests/test_abstraction.py +34 -0
  35. {py2max-0.3.1 → py2max-0.3.4}/tests/test_amxd.py +3 -1
  36. {py2max-0.3.1 → py2max-0.3.4}/tests/test_attrui.py +37 -0
  37. {py2max-0.3.1 → py2max-0.3.4}/tests/test_basic.py +4 -3
  38. {py2max-0.3.1 → py2max-0.3.4}/tests/test_beap.py +8 -1
  39. py2max-0.3.4/tests/test_bpatcher.py +44 -0
  40. py2max-0.3.4/tests/test_coll.py +28 -0
  41. py2max-0.3.4/tests/test_colors.py +44 -0
  42. py2max-0.3.4/tests/test_comment.py +20 -0
  43. {py2max-0.3.1 → py2max-0.3.4}/tests/test_db.py +15 -15
  44. py2max-0.3.4/tests/test_defaults.py +26 -0
  45. py2max-0.3.4/tests/test_dict.py +15 -0
  46. py2max-0.3.4/tests/test_edit_operations.py +362 -0
  47. {py2max-0.3.1 → py2max-0.3.4}/tests/test_error_handling.py +5 -4
  48. {py2max-0.3.1 → py2max-0.3.4}/tests/test_examples.py +87 -0
  49. py2max-0.3.4/tests/test_ezdac.py +21 -0
  50. py2max-0.3.4/tests/test_itable.py +15 -0
  51. {py2max-0.3.1 → py2max-0.3.4}/tests/test_js.py +14 -2
  52. py2max-0.3.4/tests/test_kwds_filter.py +49 -0
  53. py2max-0.3.4/tests/test_layout_box_dims.py +81 -0
  54. {py2max-0.3.1 → py2max-0.3.4}/tests/test_layout_builtins.py +1 -1
  55. {py2max-0.3.1 → py2max-0.3.4}/tests/test_layout_coverage.py +33 -23
  56. py2max-0.3.4/tests/test_layout_graph.py +77 -0
  57. py2max-0.3.4/tests/test_layout_graph_layout.py +103 -0
  58. py2max-0.3.4/tests/test_layout_graph_manager.py +127 -0
  59. {py2max-0.3.1 → py2max-0.3.4}/tests/test_layout_matrix.py +145 -41
  60. py2max-0.3.4/tests/test_layout_overlaps.py +66 -0
  61. {py2max-0.3.1 → py2max-0.3.4}/tests/test_layout_vertical.py +21 -0
  62. py2max-0.3.4/tests/test_linking.py +76 -0
  63. py2max-0.3.4/tests/test_lint.py +180 -0
  64. {py2max-0.3.1 → py2max-0.3.4}/tests/test_m4l.py +0 -6
  65. {py2max-0.3.1 → py2max-0.3.4}/tests/test_maxref_bundle.py +25 -7
  66. py2max-0.3.4/tests/test_maxref_lazy.py +55 -0
  67. py2max-0.3.4/tests/test_maxref_parser.py +60 -0
  68. py2max-0.3.4/tests/test_maxref_refpages.py +85 -0
  69. py2max-0.3.4/tests/test_mc_cycle.py +19 -0
  70. py2max-0.3.4/tests/test_message.py +16 -0
  71. py2max-0.3.4/tests/test_mypatch.py +22 -0
  72. py2max-0.3.4/tests/test_number_tilde.py +87 -0
  73. py2max-0.3.4/tests/test_numbers.py +56 -0
  74. py2max-0.3.4/tests/test_param.py +36 -0
  75. py2max-0.3.4/tests/test_param_placement.py +148 -0
  76. {py2max-0.3.1 → py2max-0.3.4}/tests/test_pitched_osc.py +12 -0
  77. py2max-0.3.4/tests/test_porttypes.py +85 -0
  78. {py2max-0.3.1 → py2max-0.3.4}/tests/test_pydantic.py +29 -0
  79. {py2max-0.3.1 → py2max-0.3.4}/tests/test_rnbo.py +78 -1
  80. py2max-0.3.4/tests/test_rnbo_subpatcher.py +36 -0
  81. py2max-0.3.4/tests/test_scripting_name.py +18 -0
  82. py2max-0.3.4/tests/test_subpatch.py +34 -0
  83. py2max-0.3.4/tests/test_svg_fidelity.py +63 -0
  84. {py2max-0.3.1 → py2max-0.3.4}/tests/test_table.py +32 -2
  85. py2max-0.3.4/tests/test_transformers_e4.py +68 -0
  86. py2max-0.3.4/tests/test_tutorial_simple_synthesis.py +100 -0
  87. py2max-0.3.4/tests/test_umenu.py +18 -0
  88. py2max-0.3.4/tests/test_validation.py +67 -0
  89. py2max-0.3.4/tests/test_varname.py +26 -0
  90. py2max-0.3.4/tests/test_wheel_bundle.py +76 -0
  91. py2max-0.3.4/tests/test_zl_group.py +28 -0
  92. py2max-0.3.1/py2max/export/svg.py +0 -456
  93. py2max-0.3.1/py2max/maxref/data/bundle.json.gz +0 -0
  94. py2max-0.3.1/tests/conftest.py +0 -21
  95. py2max-0.3.1/tests/registry.py +0 -1526
  96. py2max-0.3.1/tests/scratch.py +0 -80
  97. py2max-0.3.1/tests/test_abstraction.py +0 -21
  98. py2max-0.3.1/tests/test_bpatcher.py +0 -20
  99. py2max-0.3.1/tests/test_coll.py +0 -16
  100. py2max-0.3.1/tests/test_colors.py +0 -9
  101. py2max-0.3.1/tests/test_comment.py +0 -8
  102. py2max-0.3.1/tests/test_defaults.py +0 -11
  103. py2max-0.3.1/tests/test_dict.py +0 -7
  104. py2max-0.3.1/tests/test_ezdac.py +0 -10
  105. py2max-0.3.1/tests/test_itable.py +0 -7
  106. py2max-0.3.1/tests/test_kwds_filter.py +0 -116
  107. py2max-0.3.1/tests/test_layout_graph_layout.py +0 -113
  108. py2max-0.3.1/tests/test_layout_hola1.py +0 -117
  109. py2max-0.3.1/tests/test_layout_hola2.py +0 -100
  110. py2max-0.3.1/tests/test_layout_hola3.py +0 -123
  111. py2max-0.3.1/tests/test_layout_hola_graph.py +0 -124
  112. py2max-0.3.1/tests/test_layout_networkx1.py +0 -128
  113. py2max-0.3.1/tests/test_layout_networkx2.py +0 -96
  114. py2max-0.3.1/tests/test_layout_nx_graphviz.py +0 -96
  115. py2max-0.3.1/tests/test_layout_nx_orthogonal.py +0 -120
  116. py2max-0.3.1/tests/test_layout_nx_tsmpy.py +0 -108
  117. py2max-0.3.1/tests/test_linking.py +0 -40
  118. py2max-0.3.1/tests/test_mc_cycle.py +0 -7
  119. py2max-0.3.1/tests/test_message.py +0 -7
  120. py2max-0.3.1/tests/test_mypatch.py +0 -12
  121. py2max-0.3.1/tests/test_number_tilde.py +0 -52
  122. py2max-0.3.1/tests/test_numbers.py +0 -34
  123. py2max-0.3.1/tests/test_param.py +0 -8
  124. py2max-0.3.1/tests/test_rnbo_subpatcher.py +0 -19
  125. py2max-0.3.1/tests/test_scripting_name.py +0 -7
  126. py2max-0.3.1/tests/test_subpatch.py +0 -17
  127. py2max-0.3.1/tests/test_tutorial_simple_synthesis.py +0 -44
  128. py2max-0.3.1/tests/test_umenu.py +0 -11
  129. py2max-0.3.1/tests/test_varname.py +0 -11
  130. py2max-0.3.1/tests/test_zl_group.py +0 -11
  131. {py2max-0.3.1 → py2max-0.3.4}/LICENSE +0 -0
  132. {py2max-0.3.1 → py2max-0.3.4}/py2max/__main__.py +0 -0
  133. {py2max-0.3.1 → py2max-0.3.4}/py2max/core/__init__.py +0 -0
  134. {py2max-0.3.1 → py2max-0.3.4}/py2max/core/box.py +0 -0
  135. {py2max-0.3.1 → py2max-0.3.4}/py2max/core/colors.py +0 -0
  136. {py2max-0.3.1 → py2max-0.3.4}/py2max/core/common.py +0 -0
  137. {py2max-0.3.1 → py2max-0.3.4}/py2max/core/patchline.py +0 -0
  138. {py2max-0.3.1 → py2max-0.3.4}/py2max/exceptions.py +0 -0
  139. {py2max-0.3.1 → py2max-0.3.4}/py2max/export/__init__.py +0 -0
  140. {py2max-0.3.1 → py2max-0.3.4}/py2max/log.py +0 -0
  141. {py2max-0.3.1 → py2max-0.3.4}/py2max/m4l.py +0 -0
  142. {py2max-0.3.1 → py2max-0.3.4}/py2max/maxref/__init__.py +0 -0
  143. {py2max-0.3.1 → py2max-0.3.4}/py2max/maxref/legacy.py +0 -0
  144. {py2max-0.3.1 → py2max-0.3.4}/py2max/py.typed +0 -0
  145. {py2max-0.3.1 → py2max-0.3.4}/tests/__init__.py +0 -0
  146. {py2max-0.3.1 → py2max-0.3.4}/tests/data/complex.maxpat +0 -0
  147. {py2max-0.3.1 → py2max-0.3.4}/tests/data/desc.maxpat +0 -0
  148. {py2max-0.3.1 → py2max-0.3.4}/tests/data/empty.maxpat +0 -0
  149. {py2max-0.3.1 → py2max-0.3.4}/tests/data/mydevice.amxd +0 -0
  150. {py2max-0.3.1 → py2max-0.3.4}/tests/data/mydevice2.amxd +0 -0
  151. {py2max-0.3.1 → py2max-0.3.4}/tests/data/nested.maxpat +0 -0
  152. {py2max-0.3.1 → py2max-0.3.4}/tests/data/simple.maxpat +0 -0
  153. {py2max-0.3.1 → py2max-0.3.4}/tests/data/tabular.maxpat +0 -0
  154. {py2max-0.3.1 → py2max-0.3.4}/tests/data/umenu.maxref.xml +0 -0
  155. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/README.md +0 -0
  156. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/advanced/connection_patterns.py +0 -0
  157. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/advanced/custom_extensions.py +0 -0
  158. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/advanced/data_containers.py +0 -0
  159. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/advanced/error_handling.py +0 -0
  160. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/advanced/performance_optimization.py +0 -0
  161. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/advanced/subpatchers.py +0 -0
  162. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/api/box_api_examples.py +0 -0
  163. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/api/patcher_api_examples.py +0 -0
  164. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/layout/flow_layout_examples.py +0 -0
  165. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/layout/grid_layout_examples.py +0 -0
  166. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/basic_synth.maxpat +0 -0
  167. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/basic_synth.svg +0 -0
  168. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/complex_synth.maxpat +0 -0
  169. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/complex_synth.svg +0 -0
  170. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/flow_layout.maxpat +0 -0
  171. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/flow_layout.svg +0 -0
  172. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/grid_layout.maxpat +0 -0
  173. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/grid_layout.svg +0 -0
  174. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/horizontal_layout.maxpat +0 -0
  175. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/horizontal_layout.svg +0 -0
  176. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/styled_no_ports.svg +0 -0
  177. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/styled_no_title.svg +0 -0
  178. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/styled_patch.maxpat +0 -0
  179. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/styled_with_ports.svg +0 -0
  180. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/svg_preview_demo.py +0 -0
  181. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/vertical_layout.maxpat +0 -0
  182. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/vertical_layout.svg +0 -0
  183. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/workflow_demo.maxpat +0 -0
  184. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/preview/workflow_demo.svg +0 -0
  185. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/quickstart/basic_patch.py +0 -0
  186. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/quickstart/layout_examples.py +0 -0
  187. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/tutorial/generative_music.py +0 -0
  188. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/tutorial/interactive_controller.py +0 -0
  189. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/tutorial/signal_processing_chain.py +0 -0
  190. {py2max-0.3.1 → py2max-0.3.4}/tests/examples/tutorial/simple_synthesis.py +0 -0
  191. {py2max-0.3.1 → py2max-0.3.4}/tests/graphs/random/v30e33.tglf +0 -0
  192. {py2max-0.3.1 → py2max-0.3.4}/tests/test_abstract_coverage.py +0 -0
  193. {py2max-0.3.1 → py2max-0.3.4}/tests/test_add.py +0 -0
  194. {py2max-0.3.1 → py2max-0.3.4}/tests/test_cli.py +0 -0
  195. {py2max-0.3.1 → py2max-0.3.4}/tests/test_colors_theme.py +0 -0
  196. {py2max-0.3.1 → py2max-0.3.4}/tests/test_connection_validation.py +0 -0
  197. {py2max-0.3.1 → py2max-0.3.4}/tests/test_converters.py +0 -0
  198. {py2max-0.3.1 → py2max-0.3.4}/tests/test_core_coverage.py +0 -0
  199. {py2max-0.3.1 → py2max-0.3.4}/tests/test_encapsulate.py +0 -0
  200. {py2max-0.3.1 → py2max-0.3.4}/tests/test_gen.py +0 -0
  201. {py2max-0.3.1 → py2max-0.3.4}/tests/test_group.py +0 -0
  202. {py2max-0.3.1 → py2max-0.3.4}/tests/test_layout.py +0 -0
  203. {py2max-0.3.1 → py2max-0.3.4}/tests/test_layout_flow.py +0 -0
  204. {py2max-0.3.1 → py2max-0.3.4}/tests/test_maxref.py +0 -0
  205. {py2max-0.3.1 → py2max-0.3.4}/tests/test_mc_poly.py +0 -0
  206. {py2max-0.3.1 → py2max-0.3.4}/tests/test_nested.py +0 -0
  207. {py2max-0.3.1 → py2max-0.3.4}/tests/test_nested_patchers.py +0 -0
  208. {py2max-0.3.1 → py2max-0.3.4}/tests/test_patcher.py +0 -0
  209. {py2max-0.3.1 → py2max-0.3.4}/tests/test_presets.py +0 -0
  210. {py2max-0.3.1 → py2max-0.3.4}/tests/test_search.py +0 -0
  211. {py2max-0.3.1 → py2max-0.3.4}/tests/test_semantic_ids.py +0 -0
  212. {py2max-0.3.1 → py2max-0.3.4}/tests/test_svg.py +0 -0
  213. {py2max-0.3.1 → py2max-0.3.4}/tests/test_transformers.py +0 -0
  214. {py2max-0.3.1 → py2max-0.3.4}/tests/test_tree.py +0 -0
  215. {py2max-0.3.1 → py2max-0.3.4}/tests/test_tree_builder.py +0 -0
  216. {py2max-0.3.1 → py2max-0.3.4}/tests/test_two_sines.py +0 -0
  217. {py2max-0.3.1 → py2max-0.3.4}/tests/test_utils.py +0 -0
  218. {py2max-0.3.1 → py2max-0.3.4}/tests/test_validate_attrs.py +0 -0
@@ -2,6 +2,135 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.3.4]
6
+
7
+ ### New: Param docking in layouts
8
+
9
+ - `Patcher(..., param_placement=True)` docks value/UI "param" objects (`flonum`, `number`, `message`, `toggle`, `slider`, `dial`, `live.*`) next to the single object they drive, instead of spreading them through the signal graph. During `optimize_layout()`, a param whose outgoing connections all go to one non-param target is placed perpendicular to the signal flow -- above the target for a horizontal flow, to its left for a vertical flow (and to the right / below when the flow hugs that edge) -- ordered by, and for a lone param aligned to, the inlet it feeds, then de-overlapped. Works across every built-in layout (grid, flow, columnar, matrix). Off by default; a control that fans out to more than one object is left in the flow. Implemented as `LayoutManager.place_params()`.
10
+
11
+ ### New: Patch linting and message-type-aware connection validation
12
+
13
+ - Added `Patcher.lint()` (and the `py2max.lint` module: `lint()`, `Finding`) -- a patch-level health check returning structured findings with a `severity`, a `code`, and object/connection references. It covers invalid connections, out-of-range outlet/inlet indices, orphaned patchlines, duplicate IDs, overlapping objects, off-canvas objects, and unknown object classes.
14
+ - **Linting runs automatically on `save()`**: error-severity findings (bad connections, out-of-range ports, orphaned lines, duplicate IDs) are logged. Pass `Patcher(strict=True)` to raise `InvalidPatchError` on any error instead. Layout warnings (overlaps / off-canvas / unknown objects) are left to an explicit `lint()` or `py2max validate` to keep normal saves quiet. This is on by default and non-breaking -- saves still succeed unless `strict=True`.
15
+ - Connection validation is now **message-type aware and bidirectional**. The previous check only rejected a signal outlet wired into a non-signal inlet; it now also catches a control outlet (a bang from `metro` / `loadbang` / `button`) wired into an oscillator's signal inlet -- e.g. `metro -> cycle~`, which Max rejects. The rules are deliberately conservative (ambiguous cases and maxref-unknown objects are allowed) so on-by-default checking never rejects a valid patch; notably a bang into `adsr~` (a legitimate envelope trigger) is *not* flagged.
16
+ - Port typing is now modeled in `py2max/maxref/porttypes.py`, normalizing maxref's placeholder control types (`OUTLET_TYPE` / `INLET_TYPE`) into message kinds, and resolving **argument-dependent port counts** that maxref reports as the arg-less default -- both value-scaled (`limi~ 2` -> 2 in/out) and arg-count-scaled (`select a b c` -> 4 outlets, `route`, `pack`/`unpack`, `selector~`/`switch`) -- plus curated overrides for the handful of objects maxref mis-types.
17
+ - `py2max validate` (CLI) now reports the full lint result -- errors and warnings with codes -- and exits non-zero on any error.
18
+ - A corpus test re-lints every shipped layout example patch and fails on any error, so Max-invalid wiring can no longer ship unnoticed.
19
+ - Subpatchers are handled: a subpatcher/bpatcher box's inlet/outlet count is derived from the `inlet` / `outlet` objects it contains (not the maxref default), and `lint()` recurses into nested patchers -- findings inside a subpatcher are reported path-qualified (e.g. `sub-box-id/obj-1`).
20
+ - Inlet acceptance is now derived from each object's `<methodlist>` -- its real message vocabulary in Max's own docs -- rather than the placeholder inlet `type` (Cycling '74 ships `INLET_TYPE`/`OUTLET_TYPE` for control ports, so the type attribute alone is useless). This is what distinguishes a bang into `cycle~` (no `bang` method -> rejected) from a bang into `adsr~` (has an `anything` wildcard method -> allowed), replacing hand-curation with data that generalizes to all ~1050 objects that carry method lists. The shipped `bundle.json.gz` was regenerated so no-Max users get the same data (a `test_bundle_method_data_quality` guard prevents a future regeneration from dropping it).
21
+
22
+ ## [0.3.3]
23
+
24
+ ### Removed: `serve` and `repl` CLI subcommands
25
+
26
+ - The `py2max serve` and `py2max repl` subcommands -- thin stubs that only pointed at the separate `py2max-server` package since v0.3.0 -- have been removed. The interactive live editor and remote REPL still live in `py2max-server` (`pip install py2max-server`).
27
+
28
+ ### Fixed: Graph layouts (`graph:*`) are overlap-free and open on-screen
29
+
30
+ - `GraphLayoutManager` now runs the dimension-aware overlap sweep (`prevent_overlaps`) after placement, so constraint/force engines no longer leave large UI objects (e.g. `scope~` at 130x130) overlapping their neighbours -- matching the guarantee the grid/flow managers already gave.
31
+ - The patcher window is grown to enclose the laid-out graph plus a margin, so `optimize_layout()` output opens with the whole graph visible instead of spilling past the default 640x480 canvas (the window never shrinks below the default).
32
+
33
+ ### Fixed: Clustered grid layout squashed object sizes
34
+
35
+ - The connection-aware clustering path of `GridLayoutManager` (`cluster_connected=True`) still wrote the manager's uniform 66x22 size back onto every clustered object, squashing UI objects (`scope~`, `ezdac~`, `dial`, ...) to text-box size. It now preserves each object's real width/height, matching the already-fixed non-clustered path.
36
+
37
+ ### Fixed: Example patches use valid Max connections
38
+
39
+ - The layout example scripts (`tests/examples/layout/`) and matrix-layout tests wired control objects (`metro`, `loadbang`) straight into oscillator signal inlets, which Max rejects ("error connecting outlet ... to ... inlet"). They now use the correct idiom -- a float number box sets the oscillator frequency, `metro` triggers envelopes, and envelopes modulate amplitude through a `*~` VCA -- so the generated demo patches load cleanly.
40
+
41
+ ### New: `kwds_filter` utility
42
+
43
+ - Added `py2max.utils.kwds_filter(kwds, **elems)`: returns `kwds` merged with the `elems` whose value is not `None` (legitimate falsy values such as `0` / `""` are kept, and the input is not mutated). Lets a method keep an optional parameter in its signature but omit it from the forwarded `**kwds` when the caller leaves it unset.
44
+
45
+ ### Improved: Build and test-output hygiene
46
+
47
+ - Coverage HTML now writes to `build/coverage-html` and the test suite writes its artifacts under `build/test-output/` -- both within the git-ignored `build/` tree -- instead of a tracked `outputs/` directory. A new `make test-outputs` target writes all test artifacts flat into `build/test-outputs/` for quick inspection.
48
+
49
+ ## [0.3.2]
50
+
51
+ ### New: Patch editing and removal API
52
+
53
+ - Loading a patch (`Patcher.from_dict` / `load`) now restores all ID-generation state -- object, node, edge, and semantic-ID counters -- so `add_*` calls made after a load no longer collide with existing object IDs. This fixes the headline "edit an existing patch" round-trip, which previously emitted duplicate IDs on the first post-load add.
54
+ - Added a removal / editing API with referential-integrity cleanup: `remove_line` / `disconnect`, `remove_box` / `remove`, and `replace`. Removing a box also prunes its dangling patchlines and clears the associated node/edge/index bookkeeping, so no orphaned lines or stale IDs are left behind.
55
+
56
+ ### New: Graph-layout engines as layout managers
57
+
58
+ - Three optional graph-layout backends -- HOLA (`hola-graph`), COLA plus force-directed / geometric layouts (`graph-layout`), and OGDF's layered / force-directed / planar layouts (`ogdf-py`) -- are now selectable as first-class layout managers via `layout="graph:<algo>"` (e.g. `graph:hola`, `graph:cola`, `graph:ogdf-sugiyama`). Because these algorithms need the whole graph, positions are applied on `optimize_layout()` rather than as each box is added, and each box's width/height is preserved so UI objects are not squashed. Eleven algorithms are available: `hola`, `cola`, `sugiyama`, `fruchterman-reingold`, `kamada-kawai`, `spectral`, `circular`, `shell`, `ogdf-sugiyama`, `ogdf-fmmm`, `ogdf-planarization`.
59
+ - The engines are lazy-imported inside the manager, so `import py2max` still pulls **zero runtime dependencies**; a missing backend raises a clear error naming the package to install. Install with `pip install "py2max[graph]"` -- the `graph` extra now also bundles `hola-graph` alongside `graph-layout` and `ogdf-py`. Implemented as `GraphLayoutManager` in `py2max/layout/external.py`.
60
+
61
+ ### New: Layout gallery generator and docs page
62
+
63
+ - `scripts/gen_layout_gallery.py` (run via `make gallery`) renders every supported graph layout over one shared sample patch to transparent SVGs under `docs/assets/imgs/`, using py2max's own SVG exporter so the results are directly comparable. Backends that are not installed are skipped rather than failing the run.
64
+ - A new published **Layout Gallery** page (`docs/user_guide/layout_gallery.md`, linked in the User Guide navigation) shows the rendered layouts and documents the `graph:<algo>` layout-manager API. The older networkx / graphviz / tsmpy experiments were dropped in favor of the three maintained backends.
65
+ - The generator also renders py2max's **built-in** managers (grid, flow, columnar, matrix) via `optimize_layout()` -- no external dependencies -- and the **Layout Managers** guide (`docs/user_guide/layout_managers.md`) now embeds these as inline visuals so each layout strategy can be seen, not just described.
66
+
67
+ ### Fixed: Generation correctness
68
+
69
+ - `add_coll` / `add_dict` / `add_table` no longer discard a caller-supplied `text` argument when `name` is `None` (an operator-precedence bug that let the fallback string win).
70
+ - `add_beap` strips the `.maxpat` suffix correctly; previously `rstrip(".maxpat")` could truncate names such as `drum.maxpat` down to `dru`.
71
+ - Parallel patchlines between the same source and destination now receive incrementing `order` values, so they spread apart in Max instead of overlapping.
72
+ - Comments created with `comment=` are emitted on every save path (`save_as`, `to_json`, `to_dict`), not only `save()` / `optimize_layout()`.
73
+ - Hand-typed objects that are not in the built-in defaults now get one outlet instead of zero, so they can act as a connection source.
74
+ - Load/save round-trip no longer injects `autosave` / `dependency_cache` defaults into nested subpatchers the source patch did not have; patches containing subpatchers now round-trip faithfully.
75
+
76
+ ### Fixed: Layout managers
77
+
78
+ - `layout="columnar"` now works; it previously raised `NotImplementedError` despite being documented.
79
+ - Layout optimizers preserve each object's real width and height. UI objects (`scope~`, `dial`, `slider`, `function`, `live.*`, comments) are no longer squashed to text-box size by `optimize_layout()`.
80
+ - The managers now share a single directed-graph model (`PatchGraph`) rather than re-deriving adjacency from patchlines in each manager, and object classification no longer carries contradictory category assignments (its context inference uses maxref signal typing with word-boundary matching).
81
+
82
+ ### Improved: maxref bundle loading (lazy and thread-safe)
83
+
84
+ - In bundle mode (no local Max install), the shipped catalog is no longer eagerly materialized into the cache on first access. The name-to-source map still loads once, but each object is now built from the in-memory bundle on demand, so a single-object query no longer constructs all ~1175 entries. A bundle object whose cache slot is empty is re-materialized from the bundle rather than returning nothing.
85
+ - The process-wide maxref cache is now thread-safe: an `RLock` guards the lazy refdict / category-map initialization and cache population, so concurrent `Box.help()` / validation lookups no longer race on first load or mutate the cache unsafely.
86
+
87
+ ### Fixed: maxref outlet digests and parser robustness
88
+
89
+ - Outlet `<digest>` text is now extracted symmetrically with inlets. A condition bug previously required the `<outlet>` element to have leading text and then stored that text instead of the digest, so outlet descriptions were dropped for almost every object (~2000 digests across ~1093 objects). `Box.help()` / `get_info()` now report outlet descriptions. The shipped offline `bundle.json.gz` was regenerated so bundle-mode users (Linux/Windows/no-Max) get the corrected data.
90
+ - A raw `&` in reference prose no longer drops the whole object on parse. Ampersands that do not begin a valid XML entity are escaped before parsing, hardening against `.maxref.xml` markup variations across Max versions (valid entities like `&amp;`, `&quot;`, `&#181;` are left intact).
91
+
92
+ ### Improved: Cross-platform maxref discovery
93
+
94
+ - Reference-page resolution now honors the `PY2MAX_MAX_REFPAGES` environment override on all platforms and auto-discovers a Windows `refpages` directory, so Windows users with Max installed read live reference data instead of always falling back to the bundled snapshot. A wheel-build test now asserts the offline `bundle.json.gz` actually ships in the built artifact.
95
+
96
+ ### Improved: SVG preview fidelity
97
+
98
+ - The SVG exporter (`Patcher.to_svg` / `py2max preview`) now renders a more faithful preview instead of a uniform grey schematic:
99
+ - **Box colors are honored.** Colors set via `Box.set_color` / `apply_theme` (`bgcolor`, `bordercolor`, `textcolor`) are drawn, converted from Max's `[r, g, b, a]` floats to CSS.
100
+ - **UI objects get recognizable affordances** rather than an identical rectangle: message boxes draw the right-edge flag notch, `toggle` an X, `button` a circle, number boxes (`flonum` / `number`) the left triangle marker, `dial` a circle with a pointer, and `slider` a thumb bar.
101
+ - **Ports resolve from the box's own `numinlets` / `numoutlets`** (what is written to the `.maxpat`) rather than a maxref lookup. Ports therefore render correctly for objects maxref does not know and without a Max install, and patchline endpoints line up with the ports they connect to (both use the same counts).
102
+ - `export_svg_string` now builds the document in memory instead of round-tripping through a temporary file.
103
+
104
+ ### Fixed: Layout classification and overlap resolution
105
+
106
+ - Object classification (matrix / columnar layouts) is now factored into a clear precedence -- curated functional intent, then maxref signal typing for the unknown audio tail, then name patterns -- with the rationale documented. Curated sets must win because functional categories do not map to raw signal I/O (even `cycle~` exposes a signal inlet, so signal typing alone would call it a processor; `adc~` is an input though it is a signal source). The dead, never-effective `_refine_column_assignments_by_flow` no-op and its call site were removed.
107
+ - `LayoutManager.prevent_overlaps` now converges. The previous version cached each object's rect before mutating it (so pushes stopped accumulating) and clamped boxes back inside the canvas (re-introducing the overlaps it had just removed), leaving dense layouts overlapping at the 50-iteration cap. It is replaced with a monotone sweep that pushes each object clear of already-placed ones along the axis of least penetration; it converges in a few passes, early-exits when nothing overlaps, and preserves each object's real size.
108
+
109
+ ### Improved: Patch transformers
110
+
111
+ - `run_pipeline` now delegates to `compose`, removing a duplicated apply loop and giving the previously-unused (but exported) `compose` helper a real use.
112
+ - The `add-comment` transformer's position is now reachable from the CLI: prefix the value with `above|below|left|right:` to place the comment (e.g. `--apply "add-comment=below:tempo"`), defaulting to `above`. A leading token that is not a position is kept as comment text, so `"note: hi"` is left intact.
113
+ - Added two transformers backed by existing APIs: `apply-theme` (apply a named color theme -- `light` / `dark` / `blue` / `high-contrast`) and `scale-positions` (scale every object's x/y position by a factor, sizes unchanged).
114
+
115
+ ### Fixed: Converters module cleanup
116
+
117
+ - Importing `py2max.export.converters` no longer constructs a `Patcher` as an import-time side effect (which pulled in maxref, layout, etc.). The default-attribute set is now computed lazily on first use and cached. Also removed a dead `_infer_category` helper and an unreachable `NotImplementedError` guard (the caller strips subpatcher dicts before that path).
118
+
119
+ ### Removed: Honest CLI help for the moved `serve` / `repl` subcommands
120
+
121
+ - The `serve` and `repl` subcommands (whose implementations moved to `py2max-server` in 0.3.0) no longer advertise themselves as live in `--help`; they now removed.
122
+
123
+ ### Removed: MaxRefDB deprecated alias methods
124
+
125
+ - Removed 12 long-deprecated `MaxRefDB` aliases in favor of the canonical API: `populate_from_maxref` / `populate_all_*` -> `populate([category=...])`, `search_objects` -> `search`, `get_objects_by_category` -> `by_category`, `get_all_categories` -> `.categories`, `get_object_count` -> `.count`, and `export_to_json` / `import_from_json` -> `export` / `load`. The database module is already off the import path (lazily loaded, stdlib-only), so this only trims its API surface. Callers, tests, and docs were updated to the canonical names.
126
+ - While updating the SQLite demo scripts, also fixed two pre-existing broken examples: `from py2max import MaxRefDB` (correct: `from py2max.maxref import MaxRefDB`) and `create_database(...)` used as a free function (it is `MaxRefDB.create_database(...)`). Both `tests/examples/db/` scripts now run.
127
+
128
+ ### Removed: Obsolete layout experiments and vendored editor assets
129
+
130
+ - Deleted the old graph-layout experiment tests (`tests/test_layout_hola{1,2,3}`, `test_layout_hola_graph`, `test_layout_networkx{1,2}`, `test_layout_nx_graphviz`, `test_layout_nx_orthogonal`, `test_layout_nx_tsmpy`). They exercised backends that are no longer supported (raw adaptagrams, networkx, pygraphviz, tsmpy) or duplicated the new `GraphLayoutManager` coverage, and only ever skipped. The maintained path is covered by `tests/test_layout_graph_manager.py`.
131
+ - Removed the vendored browser libraries under `docs/js/` (SVG.js, WebCola, D3) -- reference copies for the interactive editor that moved to the separate `py2max-server` package in 0.3.0 -- and the stale Sphinx build output under `docs/build/` that predated the MkDocs migration and was being copied into the published site.
132
+ - Pruned 30 obsolete design notes from the `docs/notes/` dev journal (REPL, SSE/WebSocket live-preview server, and interactive SVG-editor implementation notes), all for features that moved to `py2max-server`. The 13 still-relevant library/journal notes were kept.
133
+
5
134
  ## [0.3.1]
6
135
 
7
136
  ### New: Standalone `gen.codebox~` Support
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py2max
3
- Version: 0.3.1
3
+ Version: 0.3.4
4
4
  Summary: A library for offline generation of Max/MSP patcher (.maxpat) files.
5
5
  Keywords: Max,maxpat,offline
6
6
  Author: Shakeeb Alireza
@@ -21,6 +21,9 @@ Classifier: Programming Language :: Python :: 3.13
21
21
  Classifier: Programming Language :: Python :: 3.14
22
22
  Classifier: Operating System :: OS Independent
23
23
  Classifier: Typing :: Typed
24
+ Requires-Dist: graph-layout>=0.4 ; extra == 'graph'
25
+ Requires-Dist: ogdf-py>=0.3 ; python_full_version >= '3.10' and extra == 'graph'
26
+ Requires-Dist: hola-graph>=0.1 ; extra == 'graph'
24
27
  Maintainer: Shakeeb Alireza
25
28
  Maintainer-email: Shakeeb Alireza <shakfu@users.noreply.github.com>
26
29
  Requires-Python: >=3.9
@@ -29,6 +32,7 @@ Project-URL: Documentation, https://github.com/shakfu/py2max#readme
29
32
  Project-URL: Repository, https://github.com/shakfu/py2max.git
30
33
  Project-URL: Issues, https://github.com/shakfu/py2max/issues
31
34
  Project-URL: Changelog, https://github.com/shakfu/py2max/blob/main/CHANGELOG.md
35
+ Provides-Extra: graph
32
36
  Description-Content-Type: text/markdown
33
37
 
34
38
  # py2max
@@ -172,7 +176,7 @@ p.save()
172
176
 
173
177
  ### MaxRef Integration
174
178
 
175
- Access documentation for 1157 Max objects:
179
+ Access documentation for 1175 Max objects:
176
180
 
177
181
  ```python
178
182
  p = Patcher('demo.maxpat')
@@ -216,10 +220,10 @@ osc = p.find_by_id('cycle_1')
216
220
  Query Max object metadata efficiently:
217
221
 
218
222
  ```python
219
- from py2max import MaxRefDB
223
+ from py2max.maxref import MaxRefDB
220
224
 
221
225
  db = MaxRefDB() # Auto-cached on first use
222
- print(len(db)) # 1157 objects
226
+ print(len(db)) # 1175 objects
223
227
 
224
228
  if 'cycle~' in db:
225
229
  info = db['cycle~']
@@ -139,7 +139,7 @@ p.save()
139
139
 
140
140
  ### MaxRef Integration
141
141
 
142
- Access documentation for 1157 Max objects:
142
+ Access documentation for 1175 Max objects:
143
143
 
144
144
  ```python
145
145
  p = Patcher('demo.maxpat')
@@ -183,10 +183,10 @@ osc = p.find_by_id('cycle_1')
183
183
  Query Max object metadata efficiently:
184
184
 
185
185
  ```python
186
- from py2max import MaxRefDB
186
+ from py2max.maxref import MaxRefDB
187
187
 
188
188
  db = MaxRefDB() # Auto-cached on first use
189
- print(len(db)) # 1157 objects
189
+ print(len(db)) # 1175 objects
190
190
 
191
191
  if 'cycle~' in db:
192
192
  info = db['cycle~']
@@ -35,7 +35,7 @@ Example:
35
35
  >>> p.save()
36
36
  """
37
37
 
38
- __version__ = "0.3.1"
38
+ __version__ = "0.3.4"
39
39
 
40
40
  from .core import Box, Patcher, Patchline
41
41
  from .exceptions import (
@@ -48,6 +48,7 @@ from .exceptions import (
48
48
  PatcherIOError,
49
49
  Py2MaxError,
50
50
  )
51
+ from .lint import Finding, lint
51
52
 
52
53
  __all__ = [
53
54
  # Version
@@ -56,6 +57,9 @@ __all__ = [
56
57
  "Patcher",
57
58
  "Box",
58
59
  "Patchline",
60
+ # Linting
61
+ "lint",
62
+ "Finding",
59
63
  # Exceptions
60
64
  "Py2MaxError",
61
65
  "InvalidConnectionError",
@@ -15,12 +15,12 @@ try: # pragma: no cover - optional dependency
15
15
  except Exception: # pragma: no cover - optional dependency
16
16
  yaml = None
17
17
 
18
- from .core import Patcher, Patchline
18
+ from .core import Patcher
19
19
  from .core.common import Rect
20
20
  from .exceptions import Py2MaxError
21
21
  from .export import export_svg
22
22
  from .export.converters import maxpat_to_python, maxref_to_sqlite
23
- from .maxref import MaxRefCache, MaxRefDB, validate_connection
23
+ from .maxref import MaxRefCache, MaxRefDB
24
24
  from .transformers import available_transformers, create_transformer, run_pipeline
25
25
 
26
26
  LAYOUT_CHOICES = ["horizontal", "vertical", "grid", "flow", "matrix"]
@@ -148,6 +148,7 @@ def _generate_test_source(name: str, data: Dict[str, Any]) -> str:
148
148
 
149
149
 
150
150
  def cmd_new(args: argparse.Namespace) -> int:
151
+ """Create a new, empty patcher file at the given path."""
151
152
  path = Path(args.path)
152
153
  if path.exists() and not args.force:
153
154
  print(f"Refusing to overwrite existing file: {path}", file=sys.stderr)
@@ -179,6 +180,7 @@ def cmd_new(args: argparse.Namespace) -> int:
179
180
 
180
181
 
181
182
  def cmd_info(args: argparse.Namespace) -> int:
183
+ """Print a summary of a patcher file (object and connection counts)."""
182
184
  path = Path(args.path)
183
185
  patcher = Patcher.from_file(path)
184
186
  _coerce_rect(patcher)
@@ -201,6 +203,7 @@ def cmd_info(args: argparse.Namespace) -> int:
201
203
 
202
204
 
203
205
  def cmd_optimize(args: argparse.Namespace) -> int:
206
+ """Re-run layout optimization on a patcher and write the result."""
204
207
  input_path = Path(args.input)
205
208
  save_path = Path(args.output) if args.output else input_path
206
209
 
@@ -222,44 +225,23 @@ def cmd_optimize(args: argparse.Namespace) -> int:
222
225
 
223
226
 
224
227
  def cmd_validate(args: argparse.Namespace) -> int:
228
+ """Validate a patcher's connections against maxref inlet/outlet metadata."""
225
229
  path = Path(args.path)
226
230
  patcher = Patcher.from_file(path)
227
231
  _coerce_rect(patcher)
228
232
 
229
- errors: List[str] = []
230
-
231
- for line in patcher._lines:
232
- patchline = cast(Patchline, line)
233
- src_id, dst_id = patchline.src, patchline.dst
234
- src_port = int(patchline.source[1]) if len(patchline.source) > 1 else 0
235
- dst_port = (
236
- int(patchline.destination[1]) if len(patchline.destination) > 1 else 0
237
- )
238
-
239
- src_obj = patcher._objects.get(src_id)
240
- dst_obj = patcher._objects.get(dst_id)
241
-
242
- if not src_obj or not dst_obj:
243
- errors.append(f"Dangling connection: {src_id} -> {dst_id}")
244
- continue
245
-
246
- src_name = _object_name(src_obj)
247
- dst_name = _object_name(dst_obj)
248
-
249
- is_valid, message = validate_connection(src_name, src_port, dst_name, dst_port)
250
- if not is_valid:
251
- errors.append(
252
- f"Invalid connection {src_name}[{src_port}] -> {dst_name}[{dst_port}]: {message}"
253
- )
254
-
255
- if errors:
256
- print("Connection validation failed:", file=sys.stderr)
257
- for error in errors:
258
- print(f" - {error}", file=sys.stderr)
259
- return 1
233
+ findings = patcher.lint()
234
+ if not findings:
235
+ print("Patch is clean: no lint findings.")
236
+ return 0
260
237
 
261
- print("All connections are valid.")
262
- return 0
238
+ errors = [f for f in findings if f.severity == "error"]
239
+ warnings = [f for f in findings if f.severity == "warning"]
240
+ stream = sys.stderr if errors else sys.stdout
241
+ for finding in findings:
242
+ print(f" {finding}", file=stream)
243
+ print(f"{len(errors)} error(s), {len(warnings)} warning(s).", file=stream)
244
+ return 1 if errors else 0
263
245
 
264
246
 
265
247
  def _parse_transform_spec(spec: str) -> tuple[str, str | None]:
@@ -272,6 +254,7 @@ def _parse_transform_spec(spec: str) -> tuple[str, str | None]:
272
254
 
273
255
 
274
256
  def cmd_transform(args: argparse.Namespace) -> int:
257
+ """Apply one or more transformers to a patcher (or list available ones)."""
275
258
  if args.list_transformers:
276
259
  for name, desc in available_transformers().items():
277
260
  print(f"{name}: {desc}")
@@ -316,6 +299,7 @@ def cmd_transform(args: argparse.Namespace) -> int:
316
299
 
317
300
 
318
301
  def cmd_convert(args: argparse.Namespace) -> int:
302
+ """Convert between formats (e.g. .maxpat to Python, maxref to SQLite)."""
319
303
  if args.mode == "maxpat-to-python":
320
304
  if not args.input or not args.output:
321
305
  print(
@@ -636,19 +620,6 @@ def cmd_db_cache(args: argparse.Namespace) -> int:
636
620
  return 1
637
621
 
638
622
 
639
- def cmd_serve(args: argparse.Namespace) -> int:
640
- """The interactive server moved to the separate py2max-server package."""
641
- target = getattr(args, "input", None) or "<patch.maxpat>"
642
- print(
643
- "The interactive server has moved to the separate 'py2max-server' package.\n"
644
- "Install it and use its CLI instead:\n"
645
- " pip install py2max-server\n"
646
- f" py2max-server serve {target}",
647
- file=sys.stderr,
648
- )
649
- return 1
650
-
651
-
652
623
  def cmd_preview(args: argparse.Namespace) -> int:
653
624
  """Generate SVG preview of a patcher."""
654
625
  import tempfile
@@ -695,20 +666,8 @@ def cmd_preview(args: argparse.Namespace) -> int:
695
666
  return 1
696
667
 
697
668
 
698
- def cmd_repl(args: argparse.Namespace) -> int:
699
- """The remote REPL moved to the separate py2max-server package."""
700
- target = getattr(args, "server", None) or "localhost:9000"
701
- print(
702
- "The remote REPL has moved to the separate 'py2max-server' package.\n"
703
- "Install it and use its CLI instead:\n"
704
- " pip install py2max-server\n"
705
- f" py2max-server repl {target}",
706
- file=sys.stderr,
707
- )
708
- return 1
709
-
710
-
711
669
  def cmd_maxref(args: argparse.Namespace) -> int:
670
+ """Query the Max object reference (list objects or show one's details)."""
712
671
  cache = MaxRefCache()
713
672
 
714
673
  if args.list:
@@ -783,6 +742,7 @@ def cmd_maxref(args: argparse.Namespace) -> int:
783
742
 
784
743
 
785
744
  def build_parser() -> argparse.ArgumentParser:
745
+ """Construct the top-level argument parser with all subcommands."""
786
746
  parser = argparse.ArgumentParser(
787
747
  prog="py2max", description="Utilities for working with Max patchers."
788
748
  )
@@ -836,33 +796,6 @@ def build_parser() -> argparse.ArgumentParser:
836
796
  val_parser.add_argument("path", help="Target .maxpat path")
837
797
  val_parser.set_defaults(func=cmd_validate)
838
798
 
839
- serve_parser = subparsers.add_parser(
840
- "serve", help="Start interactive server with live preview"
841
- )
842
- serve_parser.add_argument("input", help="Input .maxpat file")
843
- serve_parser.add_argument(
844
- "--port",
845
- type=int,
846
- default=8000,
847
- help="HTTP server port (default: 8000, WebSocket on port+1)",
848
- )
849
- serve_parser.add_argument(
850
- "--no-open", action="store_true", help="Don't automatically open browser"
851
- )
852
- serve_parser.add_argument(
853
- "--no-save", action="store_true", help="Disable auto-save on changes"
854
- )
855
- serve_parser.add_argument(
856
- "--repl",
857
- action="store_true",
858
- help="Start interactive REPL for live patch editing",
859
- )
860
- serve_parser.add_argument(
861
- "--log-file",
862
- help="Redirect server logs to file (enables single-terminal REPL mode when used with --repl)",
863
- )
864
- serve_parser.set_defaults(func=cmd_serve)
865
-
866
799
  preview_parser = subparsers.add_parser(
867
800
  "preview", help="Generate SVG preview of a patcher"
868
801
  )
@@ -1094,26 +1027,11 @@ def build_parser() -> argparse.ArgumentParser:
1094
1027
  )
1095
1028
  cache_clear.set_defaults(func=cmd_db)
1096
1029
 
1097
- # REPL client command
1098
- repl_parser = subparsers.add_parser("repl", help="Connect to remote REPL server")
1099
- repl_parser.add_argument(
1100
- "server",
1101
- nargs="?",
1102
- default="localhost:9000",
1103
- help="Server address (default: localhost:9000)",
1104
- )
1105
- repl_parser.add_argument(
1106
- "--token",
1107
- default=None,
1108
- help="Session token printed by the server "
1109
- "(or set PY2MAX_REPL_TOKEN). Required for authentication.",
1110
- )
1111
- repl_parser.set_defaults(func=cmd_repl)
1112
-
1113
1030
  return parser
1114
1031
 
1115
1032
 
1116
1033
  def main(argv: List[str] | None = None) -> int:
1034
+ """CLI entry point: parse ``argv`` and dispatch to the subcommand handler."""
1117
1035
  parser = build_parser()
1118
1036
  args = parser.parse_args(argv)
1119
1037
 
@@ -60,6 +60,8 @@ class AbstractBox(ABC):
60
60
  id: Optional[str]
61
61
  maxclass: str
62
62
  patching_rect: Rect
63
+ numinlets: int
64
+ numoutlets: int
63
65
  _kwds: Dict[str, Any]
64
66
 
65
67
  @abstractmethod
@@ -135,8 +137,6 @@ class AbstractPatcher(ABC):
135
137
  _lines: list[AbstractPatchline]
136
138
  _edge_ids: list[tuple[str, str]]
137
139
  _id_counter: int = 0
138
- _link_counter: int = 0
139
- _last_link: Optional[tuple[str, str]]
140
140
  _reset_on_render: bool
141
141
  _flow_direction: str
142
142
  _cluster_connected: bool