py2max 0.3.0__tar.gz → 0.3.1__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 (172) hide show
  1. {py2max-0.3.0 → py2max-0.3.1}/CHANGELOG.md +9 -0
  2. {py2max-0.3.0 → py2max-0.3.1}/PKG-INFO +60 -2
  3. {py2max-0.3.0 → py2max-0.3.1}/README.md +59 -1
  4. {py2max-0.3.0 → py2max-0.3.1}/py2max/__init__.py +1 -1
  5. {py2max-0.3.0 → py2max-0.3.1}/py2max/core/factory.py +109 -3
  6. {py2max-0.3.0 → py2max-0.3.1}/py2max/maxref/legacy.py +7 -0
  7. {py2max-0.3.0 → py2max-0.3.1}/pyproject.toml +1 -1
  8. py2max-0.3.1/tests/test_gen.py +96 -0
  9. py2max-0.3.0/tests/test_gen.py +0 -27
  10. {py2max-0.3.0 → py2max-0.3.1}/LICENSE +0 -0
  11. {py2max-0.3.0 → py2max-0.3.1}/py2max/__main__.py +0 -0
  12. {py2max-0.3.0 → py2max-0.3.1}/py2max/cli.py +0 -0
  13. {py2max-0.3.0 → py2max-0.3.1}/py2max/core/__init__.py +0 -0
  14. {py2max-0.3.0 → py2max-0.3.1}/py2max/core/abstract.py +0 -0
  15. {py2max-0.3.0 → py2max-0.3.1}/py2max/core/box.py +0 -0
  16. {py2max-0.3.0 → py2max-0.3.1}/py2max/core/colors.py +0 -0
  17. {py2max-0.3.0 → py2max-0.3.1}/py2max/core/common.py +0 -0
  18. {py2max-0.3.0 → py2max-0.3.1}/py2max/core/patcher.py +0 -0
  19. {py2max-0.3.0 → py2max-0.3.1}/py2max/core/patchline.py +0 -0
  20. {py2max-0.3.0 → py2max-0.3.1}/py2max/core/serialization.py +0 -0
  21. {py2max-0.3.0 → py2max-0.3.1}/py2max/exceptions.py +0 -0
  22. {py2max-0.3.0 → py2max-0.3.1}/py2max/export/__init__.py +0 -0
  23. {py2max-0.3.0 → py2max-0.3.1}/py2max/export/converters.py +0 -0
  24. {py2max-0.3.0 → py2max-0.3.1}/py2max/export/svg.py +0 -0
  25. {py2max-0.3.0 → py2max-0.3.1}/py2max/layout/__init__.py +0 -0
  26. {py2max-0.3.0 → py2max-0.3.1}/py2max/layout/base.py +0 -0
  27. {py2max-0.3.0 → py2max-0.3.1}/py2max/layout/flow.py +0 -0
  28. {py2max-0.3.0 → py2max-0.3.1}/py2max/layout/grid.py +0 -0
  29. {py2max-0.3.0 → py2max-0.3.1}/py2max/layout/matrix.py +0 -0
  30. {py2max-0.3.0 → py2max-0.3.1}/py2max/log.py +0 -0
  31. {py2max-0.3.0 → py2max-0.3.1}/py2max/m4l.py +0 -0
  32. {py2max-0.3.0 → py2max-0.3.1}/py2max/maxref/__init__.py +0 -0
  33. {py2max-0.3.0 → py2max-0.3.1}/py2max/maxref/category.py +0 -0
  34. {py2max-0.3.0 → py2max-0.3.1}/py2max/maxref/data/bundle.json.gz +0 -0
  35. {py2max-0.3.0 → py2max-0.3.1}/py2max/maxref/db.py +0 -0
  36. {py2max-0.3.0 → py2max-0.3.1}/py2max/maxref/parser.py +0 -0
  37. {py2max-0.3.0 → py2max-0.3.1}/py2max/py.typed +0 -0
  38. {py2max-0.3.0 → py2max-0.3.1}/py2max/transformers.py +0 -0
  39. {py2max-0.3.0 → py2max-0.3.1}/py2max/utils.py +0 -0
  40. {py2max-0.3.0 → py2max-0.3.1}/tests/__init__.py +0 -0
  41. {py2max-0.3.0 → py2max-0.3.1}/tests/conftest.py +0 -0
  42. {py2max-0.3.0 → py2max-0.3.1}/tests/data/complex.maxpat +0 -0
  43. {py2max-0.3.0 → py2max-0.3.1}/tests/data/desc.maxpat +0 -0
  44. {py2max-0.3.0 → py2max-0.3.1}/tests/data/empty.maxpat +0 -0
  45. {py2max-0.3.0 → py2max-0.3.1}/tests/data/mydevice.amxd +0 -0
  46. {py2max-0.3.0 → py2max-0.3.1}/tests/data/mydevice2.amxd +0 -0
  47. {py2max-0.3.0 → py2max-0.3.1}/tests/data/nested.maxpat +0 -0
  48. {py2max-0.3.0 → py2max-0.3.1}/tests/data/simple.maxpat +0 -0
  49. {py2max-0.3.0 → py2max-0.3.1}/tests/data/tabular.maxpat +0 -0
  50. {py2max-0.3.0 → py2max-0.3.1}/tests/data/umenu.maxref.xml +0 -0
  51. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/README.md +0 -0
  52. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/advanced/connection_patterns.py +0 -0
  53. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/advanced/custom_extensions.py +0 -0
  54. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/advanced/data_containers.py +0 -0
  55. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/advanced/error_handling.py +0 -0
  56. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/advanced/performance_optimization.py +0 -0
  57. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/advanced/subpatchers.py +0 -0
  58. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/api/box_api_examples.py +0 -0
  59. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/api/patcher_api_examples.py +0 -0
  60. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/auto_layout_demo.py +0 -0
  61. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/db/category_db_demo.py +0 -0
  62. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/db/maxref_db_demo.py +0 -0
  63. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/layout/columnar_layout_examples.py +0 -0
  64. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/layout/flow_layout_examples.py +0 -0
  65. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/layout/grid_layout_examples.py +0 -0
  66. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/layout/matrix_layout_examples.py +0 -0
  67. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/basic_synth.maxpat +0 -0
  68. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/basic_synth.svg +0 -0
  69. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/complex_synth.maxpat +0 -0
  70. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/complex_synth.svg +0 -0
  71. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/flow_layout.maxpat +0 -0
  72. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/flow_layout.svg +0 -0
  73. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/grid_layout.maxpat +0 -0
  74. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/grid_layout.svg +0 -0
  75. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/horizontal_layout.maxpat +0 -0
  76. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/horizontal_layout.svg +0 -0
  77. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/styled_no_ports.svg +0 -0
  78. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/styled_no_title.svg +0 -0
  79. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/styled_patch.maxpat +0 -0
  80. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/styled_with_ports.svg +0 -0
  81. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/svg_preview_demo.py +0 -0
  82. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/vertical_layout.maxpat +0 -0
  83. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/vertical_layout.svg +0 -0
  84. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/workflow_demo.maxpat +0 -0
  85. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/preview/workflow_demo.svg +0 -0
  86. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/quickstart/basic_patch.py +0 -0
  87. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/quickstart/layout_examples.py +0 -0
  88. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/tutorial/generative_music.py +0 -0
  89. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/tutorial/interactive_controller.py +0 -0
  90. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/tutorial/signal_processing_chain.py +0 -0
  91. {py2max-0.3.0 → py2max-0.3.1}/tests/examples/tutorial/simple_synthesis.py +0 -0
  92. {py2max-0.3.0 → py2max-0.3.1}/tests/graphs/random/v30e33.tglf +0 -0
  93. {py2max-0.3.0 → py2max-0.3.1}/tests/registry.py +0 -0
  94. {py2max-0.3.0 → py2max-0.3.1}/tests/scratch.py +0 -0
  95. {py2max-0.3.0 → py2max-0.3.1}/tests/test_abstract_coverage.py +0 -0
  96. {py2max-0.3.0 → py2max-0.3.1}/tests/test_abstraction.py +0 -0
  97. {py2max-0.3.0 → py2max-0.3.1}/tests/test_add.py +0 -0
  98. {py2max-0.3.0 → py2max-0.3.1}/tests/test_amxd.py +0 -0
  99. {py2max-0.3.0 → py2max-0.3.1}/tests/test_attrui.py +0 -0
  100. {py2max-0.3.0 → py2max-0.3.1}/tests/test_basic.py +0 -0
  101. {py2max-0.3.0 → py2max-0.3.1}/tests/test_beap.py +0 -0
  102. {py2max-0.3.0 → py2max-0.3.1}/tests/test_bpatcher.py +0 -0
  103. {py2max-0.3.0 → py2max-0.3.1}/tests/test_cli.py +0 -0
  104. {py2max-0.3.0 → py2max-0.3.1}/tests/test_coll.py +0 -0
  105. {py2max-0.3.0 → py2max-0.3.1}/tests/test_colors.py +0 -0
  106. {py2max-0.3.0 → py2max-0.3.1}/tests/test_colors_theme.py +0 -0
  107. {py2max-0.3.0 → py2max-0.3.1}/tests/test_comment.py +0 -0
  108. {py2max-0.3.0 → py2max-0.3.1}/tests/test_connection_validation.py +0 -0
  109. {py2max-0.3.0 → py2max-0.3.1}/tests/test_converters.py +0 -0
  110. {py2max-0.3.0 → py2max-0.3.1}/tests/test_core_coverage.py +0 -0
  111. {py2max-0.3.0 → py2max-0.3.1}/tests/test_db.py +0 -0
  112. {py2max-0.3.0 → py2max-0.3.1}/tests/test_defaults.py +0 -0
  113. {py2max-0.3.0 → py2max-0.3.1}/tests/test_dict.py +0 -0
  114. {py2max-0.3.0 → py2max-0.3.1}/tests/test_encapsulate.py +0 -0
  115. {py2max-0.3.0 → py2max-0.3.1}/tests/test_error_handling.py +0 -0
  116. {py2max-0.3.0 → py2max-0.3.1}/tests/test_examples.py +0 -0
  117. {py2max-0.3.0 → py2max-0.3.1}/tests/test_ezdac.py +0 -0
  118. {py2max-0.3.0 → py2max-0.3.1}/tests/test_group.py +0 -0
  119. {py2max-0.3.0 → py2max-0.3.1}/tests/test_itable.py +0 -0
  120. {py2max-0.3.0 → py2max-0.3.1}/tests/test_js.py +0 -0
  121. {py2max-0.3.0 → py2max-0.3.1}/tests/test_kwds_filter.py +0 -0
  122. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout.py +0 -0
  123. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_builtins.py +0 -0
  124. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_coverage.py +0 -0
  125. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_flow.py +0 -0
  126. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_graph_layout.py +0 -0
  127. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_hola1.py +0 -0
  128. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_hola2.py +0 -0
  129. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_hola3.py +0 -0
  130. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_hola_graph.py +0 -0
  131. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_matrix.py +0 -0
  132. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_networkx1.py +0 -0
  133. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_networkx2.py +0 -0
  134. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_nx_graphviz.py +0 -0
  135. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_nx_orthogonal.py +0 -0
  136. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_nx_tsmpy.py +0 -0
  137. {py2max-0.3.0 → py2max-0.3.1}/tests/test_layout_vertical.py +0 -0
  138. {py2max-0.3.0 → py2max-0.3.1}/tests/test_linking.py +0 -0
  139. {py2max-0.3.0 → py2max-0.3.1}/tests/test_m4l.py +0 -0
  140. {py2max-0.3.0 → py2max-0.3.1}/tests/test_maxref.py +0 -0
  141. {py2max-0.3.0 → py2max-0.3.1}/tests/test_maxref_bundle.py +0 -0
  142. {py2max-0.3.0 → py2max-0.3.1}/tests/test_mc_cycle.py +0 -0
  143. {py2max-0.3.0 → py2max-0.3.1}/tests/test_mc_poly.py +0 -0
  144. {py2max-0.3.0 → py2max-0.3.1}/tests/test_message.py +0 -0
  145. {py2max-0.3.0 → py2max-0.3.1}/tests/test_mypatch.py +0 -0
  146. {py2max-0.3.0 → py2max-0.3.1}/tests/test_nested.py +0 -0
  147. {py2max-0.3.0 → py2max-0.3.1}/tests/test_nested_patchers.py +0 -0
  148. {py2max-0.3.0 → py2max-0.3.1}/tests/test_number_tilde.py +0 -0
  149. {py2max-0.3.0 → py2max-0.3.1}/tests/test_numbers.py +0 -0
  150. {py2max-0.3.0 → py2max-0.3.1}/tests/test_param.py +0 -0
  151. {py2max-0.3.0 → py2max-0.3.1}/tests/test_patcher.py +0 -0
  152. {py2max-0.3.0 → py2max-0.3.1}/tests/test_pitched_osc.py +0 -0
  153. {py2max-0.3.0 → py2max-0.3.1}/tests/test_presets.py +0 -0
  154. {py2max-0.3.0 → py2max-0.3.1}/tests/test_pydantic.py +0 -0
  155. {py2max-0.3.0 → py2max-0.3.1}/tests/test_rnbo.py +0 -0
  156. {py2max-0.3.0 → py2max-0.3.1}/tests/test_rnbo_subpatcher.py +0 -0
  157. {py2max-0.3.0 → py2max-0.3.1}/tests/test_scripting_name.py +0 -0
  158. {py2max-0.3.0 → py2max-0.3.1}/tests/test_search.py +0 -0
  159. {py2max-0.3.0 → py2max-0.3.1}/tests/test_semantic_ids.py +0 -0
  160. {py2max-0.3.0 → py2max-0.3.1}/tests/test_subpatch.py +0 -0
  161. {py2max-0.3.0 → py2max-0.3.1}/tests/test_svg.py +0 -0
  162. {py2max-0.3.0 → py2max-0.3.1}/tests/test_table.py +0 -0
  163. {py2max-0.3.0 → py2max-0.3.1}/tests/test_transformers.py +0 -0
  164. {py2max-0.3.0 → py2max-0.3.1}/tests/test_tree.py +0 -0
  165. {py2max-0.3.0 → py2max-0.3.1}/tests/test_tree_builder.py +0 -0
  166. {py2max-0.3.0 → py2max-0.3.1}/tests/test_tutorial_simple_synthesis.py +0 -0
  167. {py2max-0.3.0 → py2max-0.3.1}/tests/test_two_sines.py +0 -0
  168. {py2max-0.3.0 → py2max-0.3.1}/tests/test_umenu.py +0 -0
  169. {py2max-0.3.0 → py2max-0.3.1}/tests/test_utils.py +0 -0
  170. {py2max-0.3.0 → py2max-0.3.1}/tests/test_validate_attrs.py +0 -0
  171. {py2max-0.3.0 → py2max-0.3.1}/tests/test_varname.py +0 -0
  172. {py2max-0.3.0 → py2max-0.3.1}/tests/test_zl_group.py +0 -0
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.3.1]
6
+
7
+ ### New: Standalone `gen.codebox~` Support
8
+
9
+ - `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.
10
+ - 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.
11
+ - 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.
12
+ - 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.
13
+
5
14
  ## [0.3.0]
6
15
 
7
16
  ### 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.1
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
@@ -84,8 +84,38 @@ That's it! Open `my-synth.maxpat` in Max to see your patch.
84
84
 
85
85
  - **Offline Patch Generation** - Create Max patches programmatically without Max running
86
86
  - **Round-trip Conversion** - Load, modify, and save existing `.maxpat` files
87
+ - **Max for Live (.amxd)** - Read/write binary `.amxd` device files with presentation-mode helpers
87
88
  - **Universal Object Support** - Works with any Max/MSP/Jitter object
88
- - **99% Test Coverage** - 418+ tests ensure reliability
89
+ - **Fully typed** - Passes `mypy --strict`; no runtime dependencies
90
+ - **High Test Coverage** - 420+ tests ensure reliability
91
+
92
+ ### Max for Live (.amxd)
93
+
94
+ Generate Max for Live devices directly. `Patcher.save()` / `Patcher.from_file()`
95
+ auto-detect the `.amxd` extension and read/write the binary device format,
96
+ byte-for-byte compatible with Max-exported devices.
97
+
98
+ ```python
99
+ from py2max import Patcher
100
+
101
+ # device_type: "audio_effect" (default), "instrument", or "midi_effect"
102
+ p = Patcher('gain.amxd', device_type='audio_effect')
103
+ p.enable_presentation(devicewidth=120) # render Ableton's device strip
104
+
105
+ plugin = p.add_textbox('plugin~') # audio in from Live
106
+ gain = p.add('live.gain~', maxclass='live.gain~')
107
+ plugout = p.add_textbox('plugout~') # audio back to Live
108
+ gain.add_to_presentation([20, 20, 60, 136]) # show the fader in the device
109
+
110
+ p.add_line(plugin, gain, outlet=0, inlet=0)
111
+ p.add_line(gain, plugout, outlet=0, inlet=0)
112
+ p.save() # writes a binary .amxd
113
+ ```
114
+
115
+ Helpers: `Patcher.enable_presentation(devicewidth=...)`,
116
+ `Box.add_to_presentation([x, y, w, h])` (rejects M4L infrastructure objects and
117
+ rounds fractional coordinates), and `Patcher.enforce_integer_coords()`. M4L
118
+ binary helpers live in `py2max.m4l`.
89
119
 
90
120
  ### Interactive Server (separate package)
91
121
 
@@ -252,6 +282,34 @@ p.link(sbox, dac)
252
282
  p.save()
253
283
  ```
254
284
 
285
+ ### Gen Codebox
286
+
287
+ `add_gen_codebox()` adds a standalone `gen.codebox~` object -- a complete gen
288
+ patch in a single box that sits directly in a regular Max patcher (unlike the
289
+ inner `codebox~` from `add_codebox()`, which belongs inside a `gen~`/`rnbo~`
290
+ subpatcher). Inlet/outlet counts are derived automatically from the highest
291
+ `inN`/`outN` references in the code:
292
+
293
+ ```python
294
+ p = Patcher('fbdelay.maxpat')
295
+
296
+ # 1 inlet (in1), 1 outlet (out1)
297
+ osc = p.add('cycle~ 440')
298
+ cb = p.add_gen_codebox('''
299
+ Param feedback(0.5, min=0.0, max=0.95);
300
+ History fb(0.0);
301
+ out1 = in1 + fb * feedback;
302
+ fb = out1;
303
+ ''')
304
+ dac = p.add('ezdac~')
305
+ p.link(osc, cb)
306
+ p.link(cb, dac)
307
+ p.save()
308
+
309
+ # Or via the add() string shortcut (single-line / `;`-terminated code)
310
+ cb = p.add('gen.codebox~ out1 = in1 * 0.5;')
311
+ ```
312
+
255
313
  ### Object Search
256
314
 
257
315
  ```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
 
@@ -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.1"
39
39
 
40
40
  from .core import Box, Patcher, Patchline
41
41
  from .exceptions import (
@@ -8,6 +8,7 @@ no API change -- adding a new object type means editing this file, not the
8
8
  core class.
9
9
  """
10
10
 
11
+ import re
11
12
  import warnings
12
13
  from typing import (
13
14
  TYPE_CHECKING,
@@ -35,6 +36,23 @@ if TYPE_CHECKING:
35
36
 
36
37
  logger = get_logger(__name__)
37
38
 
39
+ # Max objects whose inlet/outlet counts are determined by their code rather
40
+ # than by a fixed maxref entry. Connection validation for these consults the
41
+ # box's own declared numinlets/numoutlets instead of the static maxref data.
42
+ DYNAMIC_IO_MAXCLASSES = frozenset({"gen.codebox~", "codebox", "codebox~"})
43
+
44
+
45
+ def _max_gen_io_index(code: str, kind: str) -> int:
46
+ """Highest ``in<N>`` / ``out<N>`` index referenced in gen ``code``.
47
+
48
+ gen codeboxes derive their inlet/outlet count from the code: ``in1``,
49
+ ``in2``, ... and ``out1``, ``out2``, .... Returns at least 1 since a gen
50
+ codebox always has one signal inlet and one signal outlet.
51
+ """
52
+ indices = [int(m) for m in re.findall(rf"\b{kind}(\d+)\b", code)]
53
+ return max([1, *indices])
54
+
55
+
38
56
  # Box attributes valid on (nearly) every Max object, independent of an object's
39
57
  # own maxref attribute set. Used by add_box when validate_attrs is enabled to
40
58
  # whitelist universal/jbox attributes plus the structural keys py2max emits, so
@@ -260,9 +278,41 @@ class BoxFactoryMixin(AbstractPatcher):
260
278
  src_name = self._get_object_name(src_obj)
261
279
  dst_name = self._get_object_name(dst_obj)
262
280
 
263
- is_valid, error_msg = maxref.validate_connection(
264
- src_name, src_outlet, dst_name, dst_inlet
265
- )
281
+ src_dynamic = src_obj.maxclass in DYNAMIC_IO_MAXCLASSES
282
+ dst_dynamic = dst_obj.maxclass in DYNAMIC_IO_MAXCLASSES
283
+
284
+ if src_dynamic or dst_dynamic:
285
+ # Codeboxes derive their inlet/outlet counts from their code,
286
+ # so bound-check indices against the box's own declared counts
287
+ # rather than the fixed maxref entry. Type checking is skipped
288
+ # because codebox I/O is always signal.
289
+ src_outlets = (
290
+ src_obj.numoutlets
291
+ if src_dynamic
292
+ else maxref.get_outlet_count(src_name)
293
+ )
294
+ dst_inlets = (
295
+ dst_obj.numinlets
296
+ if dst_dynamic
297
+ else maxref.get_inlet_count(dst_name)
298
+ )
299
+ error_msg = ""
300
+ if src_outlets is not None and src_outlet >= src_outlets:
301
+ error_msg = (
302
+ f"Object '{src_name}' only has {src_outlets} outlet(s), "
303
+ f"cannot connect from outlet {src_outlet}"
304
+ )
305
+ elif dst_inlets is not None and dst_inlet >= dst_inlets:
306
+ error_msg = (
307
+ f"Object '{dst_name}' only has {dst_inlets} inlet(s), "
308
+ f"cannot connect to inlet {dst_inlet}"
309
+ )
310
+ is_valid = not error_msg
311
+ else:
312
+ is_valid, error_msg = maxref.validate_connection(
313
+ src_name, src_outlet, dst_name, dst_inlet
314
+ )
315
+
266
316
  if not is_valid:
267
317
  logger.warning(
268
318
  f"Connection validation failed: {src_name}[{src_outlet}] -> {dst_name}[{dst_inlet}]: {error_msg}"
@@ -473,6 +523,11 @@ class BoxFactoryMixin(AbstractPatcher):
473
523
  return self.add_subpatcher(value, **kwds)
474
524
  if maxclass == "gen~":
475
525
  return self.add_gen_tilde(**kwds)
526
+ if maxclass == "gen.codebox~":
527
+ # Tail is the gen code. value.split() collapses whitespace, so this
528
+ # shortcut suits single-line/`;`-terminated code; for multi-line
529
+ # source pass it to add_gen_codebox() directly.
530
+ return self.add_gen_codebox(txt, **kwds)
476
531
  if maxclass == "rnbo~":
477
532
  return self.add_rnbo(value, **kwds)
478
533
  return self.add_textbox(text=value, **kwds)
@@ -542,6 +597,57 @@ class BoxFactoryMixin(AbstractPatcher):
542
597
  code, patching_rect, id, comment, comment_pos, tilde=True, **kwds
543
598
  )
544
599
 
600
+ def add_gen_codebox(
601
+ self,
602
+ code: str,
603
+ patching_rect: Optional[Rect] = None,
604
+ id: Optional[str] = None,
605
+ numinlets: Optional[int] = None,
606
+ numoutlets: Optional[int] = None,
607
+ outlettype: Optional[List[str]] = None,
608
+ comment: Optional[str] = None,
609
+ comment_pos: Optional[str] = None,
610
+ **kwds: Any,
611
+ ) -> "Box":
612
+ """Add a standalone ``gen.codebox~`` object.
613
+
614
+ Unlike :meth:`add_codebox` (which emits the ``codebox~`` object meant to
615
+ live *inside* a ``gen~`` or ``rnbo~`` subpatcher), this creates the
616
+ self-contained ``gen.codebox~`` object that lives directly in a regular
617
+ Max patcher -- a complete gen patch in a single box, with no subpatcher
618
+ wrapper. This is the form emitted by gen transpilers.
619
+
620
+ A gen codebox's inlet/outlet counts are dynamic: they are determined by
621
+ the highest ``inN`` / ``outN`` references in the code. They are derived
622
+ automatically when ``numinlets`` / ``numoutlets`` are not given (each
623
+ defaults to at least 1).
624
+ """
625
+ if "\r" not in code:
626
+ code = code.replace("\n", "\r\n")
627
+
628
+ if numinlets is None:
629
+ numinlets = _max_gen_io_index(code, "in")
630
+ if numoutlets is None:
631
+ numoutlets = _max_gen_io_index(code, "out")
632
+
633
+ kwds.setdefault("fontname", "<Monospaced>")
634
+ kwds.setdefault("fontsize", 12.0)
635
+
636
+ return self.add_box(
637
+ Box(
638
+ id=id or self.get_id("gen.codebox~"),
639
+ code=code,
640
+ maxclass="gen.codebox~",
641
+ numinlets=numinlets,
642
+ numoutlets=numoutlets,
643
+ outlettype=outlettype or ["signal"] * numoutlets,
644
+ patching_rect=patching_rect or self.get_pos(),
645
+ **kwds,
646
+ ),
647
+ comment,
648
+ comment_pos,
649
+ )
650
+
545
651
  def add_message(
546
652
  self,
547
653
  text: Optional[str] = None,
@@ -41,6 +41,13 @@ MAXCLASS_DEFAULTS: Dict[str, Dict[str, Any]] = {
41
41
  "outlettype": [""],
42
42
  "patching_rect": Rect(x=191.0, y=118.0, w=200.0, h=200.0),
43
43
  },
44
+ "gen.codebox~": {
45
+ "maxclass": "gen.codebox~",
46
+ "numinlets": 1,
47
+ "numoutlets": 1,
48
+ "outlettype": ["signal"],
49
+ "patching_rect": Rect(x=60.0, y=107.0, w=688.0, h=471.0),
50
+ },
44
51
  "dial": {
45
52
  "maxclass": "dial",
46
53
  "numinlets": 1,
@@ -7,7 +7,7 @@ authors = [
7
7
  maintainers = [
8
8
  { name = "Shakeeb Alireza", email = "shakfu@users.noreply.github.com" }
9
9
  ]
10
- version = "0.3.0"
10
+ version = "0.3.1"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.9"
13
13
  keywords = ["Max", "maxpat", "offline"]
@@ -0,0 +1,96 @@
1
+ import pytest
2
+
3
+ from py2max import InvalidConnectionError, Patcher
4
+
5
+
6
+ def test_gen():
7
+ p = Patcher("outputs/test_gen.maxpat")
8
+ sbox = p.add_gen("@title windowSync")
9
+ sp = sbox.subpatcher
10
+ i3 = sp.add_textbox("in 3")
11
+ i4 = sp.add_textbox("in 4")
12
+ plus = sp.add_textbox("+")
13
+ sp.add_line(i3, plus)
14
+ sp.add_line(i4, plus)
15
+ p.save()
16
+
17
+
18
+ def test_gen_tilde():
19
+ p = Patcher("outputs/test_gen_tilde.maxpat")
20
+ sbox = p.add_gen_tilde("@nocache 0") # also p.add_gen(tilde=True)
21
+ sp = sbox.subpatcher
22
+ i1 = sp.add_textbox("in 1")
23
+ i2 = sp.add_textbox("in 2")
24
+ o1 = sp.add_textbox("out 1")
25
+ plus = sp.add_textbox("+")
26
+ sp.add_line(i1, plus)
27
+ sp.add_line(i2, plus, inlet=1)
28
+ sp.add_line(plus, o1)
29
+ p.save()
30
+
31
+
32
+ def test_gen_codebox():
33
+ code = "Param fb(0.5, min=0.0, max=0.95);\nout1 = in1 * fb;"
34
+ p = Patcher("outputs/test_gen_codebox.maxpat")
35
+ box = p.add_gen_codebox(code)
36
+ d = box.to_dict()["box"]
37
+
38
+ # standalone gen.codebox~ lives directly in a regular ("box") patcher,
39
+ # unlike the inner "codebox~" emitted by add_codebox.
40
+ assert box.maxclass == "gen.codebox~"
41
+ assert p.classnamespace == "box"
42
+ assert box.numinlets == 1
43
+ assert box.numoutlets == 1
44
+ assert d["outlettype"] == ["signal"]
45
+ assert d["fontname"] == "<Monospaced>"
46
+ # newlines are normalized to CRLF as Max expects
47
+ assert "\r\n" in d["code"]
48
+ p.save()
49
+
50
+
51
+ def test_gen_codebox_multi_io():
52
+ p = Patcher("outputs/test_gen_codebox_multi.maxpat")
53
+ box = p.add_gen_codebox("out1 = in1;\nout2 = in2;", numinlets=2, numoutlets=2)
54
+ assert box.numinlets == 2
55
+ assert box.numoutlets == 2
56
+ d = box.to_dict()["box"]
57
+ assert d["outlettype"] == ["signal", "signal"]
58
+ p.save()
59
+
60
+
61
+ def test_gen_codebox_io_autoderived_from_code():
62
+ # inlet/outlet counts follow the highest in<N>/out<N> referenced in the code
63
+ p = Patcher("outputs/test_gen_codebox_auto.maxpat")
64
+ box = p.add_gen_codebox("out1 = in1 + in3;\nout2 = in2;")
65
+ assert box.numinlets == 3
66
+ assert box.numoutlets == 2
67
+ # always at least one signal inlet and outlet, even with no references
68
+ bare = p.add_gen_codebox("history h(0.);\nout1 = h;")
69
+ assert bare.numinlets == 1
70
+ assert bare.numoutlets == 1
71
+
72
+
73
+ def test_gen_codebox_string_dispatch():
74
+ p = Patcher("outputs/test_gen_codebox_dispatch.maxpat")
75
+ box = p.add("gen.codebox~ out1 = in1 * 0.5;")
76
+ assert box.maxclass == "gen.codebox~"
77
+ assert box.to_dict()["box"]["code"].startswith("out1 = in1 * 0.5;")
78
+
79
+
80
+ def test_gen_codebox_connection_validation():
81
+ # validation uses the codebox's declared (dynamic) I/O, not static maxref
82
+ p = Patcher("outputs/test_gen_codebox_validate.maxpat", validate_connections=True)
83
+ box = p.add_gen_codebox("out1 = in1 + in3;\nout2 = in2;") # 3 in, 2 out
84
+ dac = p.add("ezdac~")
85
+ src = p.add("cycle~ 440")
86
+
87
+ # connecting from the second outlet is valid (codebox has 2 outlets)
88
+ p.add_line(box, dac, outlet=1)
89
+ # connecting into the third inlet is valid (codebox has 3 inlets)
90
+ p.add_line(src, box, inlet=2)
91
+
92
+ # out-of-range outlet/inlet are rejected against the box's own counts
93
+ with pytest.raises(InvalidConnectionError):
94
+ p.add_line(box, dac, outlet=5)
95
+ with pytest.raises(InvalidConnectionError):
96
+ p.add_line(src, box, inlet=9)
@@ -1,27 +0,0 @@
1
- from py2max import Patcher
2
-
3
-
4
- def test_gen():
5
- p = Patcher("outputs/test_gen.maxpat")
6
- sbox = p.add_gen("@title windowSync")
7
- sp = sbox.subpatcher
8
- i3 = sp.add_textbox("in 3")
9
- i4 = sp.add_textbox("in 4")
10
- plus = sp.add_textbox("+")
11
- sp.add_line(i3, plus)
12
- sp.add_line(i4, plus)
13
- p.save()
14
-
15
-
16
- def test_gen_tilde():
17
- p = Patcher("outputs/test_gen_tilde.maxpat")
18
- sbox = p.add_gen_tilde("@nocache 0") # also p.add_gen(tilde=True)
19
- sp = sbox.subpatcher
20
- i1 = sp.add_textbox("in 1")
21
- i2 = sp.add_textbox("in 2")
22
- o1 = sp.add_textbox("out 1")
23
- plus = sp.add_textbox("+")
24
- sp.add_line(i1, plus)
25
- sp.add_line(i2, plus, inlet=1)
26
- sp.add_line(plus, o1)
27
- p.save()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes