py2max 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- py2max/__init__.py +67 -0
- py2max/__main__.py +6 -0
- py2max/cli.py +1251 -0
- py2max/core/__init__.py +39 -0
- py2max/core/abstract.py +146 -0
- py2max/core/box.py +231 -0
- py2max/core/common.py +19 -0
- py2max/core/patcher.py +1658 -0
- py2max/core/patchline.py +68 -0
- py2max/exceptions.py +385 -0
- py2max/export/__init__.py +20 -0
- py2max/export/converters.py +345 -0
- py2max/export/svg.py +393 -0
- py2max/layout/__init__.py +26 -0
- py2max/layout/base.py +463 -0
- py2max/layout/flow.py +405 -0
- py2max/layout/grid.py +374 -0
- py2max/layout/matrix.py +628 -0
- py2max/log.py +338 -0
- py2max/maxref/__init__.py +78 -0
- py2max/maxref/category.py +163 -0
- py2max/maxref/db.py +1082 -0
- py2max/maxref/legacy.py +324 -0
- py2max/maxref/parser.py +703 -0
- py2max/py.typed +0 -0
- py2max/server/__init__.py +54 -0
- py2max/server/client.py +295 -0
- py2max/server/inline.py +312 -0
- py2max/server/repl.py +561 -0
- py2max/server/rpc.py +240 -0
- py2max/server/websocket.py +997 -0
- py2max/static/cola.min.js +4 -0
- py2max/static/d3.v7.min.js +2 -0
- py2max/static/dagre-bundle.js +328 -0
- py2max/static/elk.bundled.js +6663 -0
- py2max/static/index.html +168 -0
- py2max/static/interactive.html +589 -0
- py2max/static/interactive.js +2111 -0
- py2max/static/live-preview.js +324 -0
- py2max/static/svg.min.js +13 -0
- py2max/static/svg.min.js.map +1 -0
- py2max/transformers.py +168 -0
- py2max/utils.py +83 -0
- py2max-0.2.1.dist-info/METADATA +390 -0
- py2max-0.2.1.dist-info/RECORD +48 -0
- py2max-0.2.1.dist-info/WHEEL +4 -0
- py2max-0.2.1.dist-info/entry_points.txt +3 -0
- py2max-0.2.1.dist-info/licenses/LICENSE +19 -0
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""Utilities for converting between Max patch formats."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import textwrap
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Iterable, Optional
|
|
9
|
+
|
|
10
|
+
from ..core import Patcher
|
|
11
|
+
from ..maxref.db import MaxRefDB
|
|
12
|
+
|
|
13
|
+
PY_TEMPLATE = '''"""Auto-generated by py2max. Recreates {source_name} using py2max."""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from py2max import Box, Patcher
|
|
17
|
+
from py2max.core.common import Rect
|
|
18
|
+
|
|
19
|
+
{helper_functions_block}
|
|
20
|
+
|
|
21
|
+
def build(output_path: str = {default_output_repr}) -> Patcher:
|
|
22
|
+
"""Rebuild the patcher and write it to ``output_path``."""
|
|
23
|
+
p = Patcher(output_path)
|
|
24
|
+
{patcher_attr_block}
|
|
25
|
+
{box_block}
|
|
26
|
+
{line_block}
|
|
27
|
+
p.save_as(output_path)
|
|
28
|
+
return p
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
build()
|
|
33
|
+
'''
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _format_rect(rect: object) -> str:
|
|
37
|
+
if isinstance(rect, dict):
|
|
38
|
+
x = rect.get("x", 0.0)
|
|
39
|
+
y = rect.get("y", 0.0)
|
|
40
|
+
w = rect.get("w", 0.0)
|
|
41
|
+
h = rect.get("h", 0.0)
|
|
42
|
+
elif isinstance(rect, (list, tuple)) and len(rect) == 4:
|
|
43
|
+
x, y, w, h = rect
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError(f"Unsupported rect value: {rect}")
|
|
46
|
+
return f"Rect({x}, {y}, {w}, {h})"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _format_value(value: object) -> str:
|
|
50
|
+
if isinstance(value, str):
|
|
51
|
+
return repr(value)
|
|
52
|
+
if isinstance(value, (int, float, bool)) or value is None:
|
|
53
|
+
return repr(value)
|
|
54
|
+
if isinstance(value, (list, tuple, dict)):
|
|
55
|
+
return repr(value)
|
|
56
|
+
return repr(value)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_subpatch_function(
|
|
60
|
+
name: str, patcher_dict: dict, ctx: _CodeGenContext
|
|
61
|
+
) -> str:
|
|
62
|
+
attr_block, box_block, line_block, _ = _build_patcher_sections(
|
|
63
|
+
patcher_dict, ctx, "sp", f"{name}_obj", " "
|
|
64
|
+
)
|
|
65
|
+
body = (
|
|
66
|
+
f"def {name}() -> Patcher:\n"
|
|
67
|
+
f" sp = Patcher()\n"
|
|
68
|
+
f"{attr_block}{box_block}{line_block}"
|
|
69
|
+
" return sp\n"
|
|
70
|
+
)
|
|
71
|
+
return body
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _build_patcher_attribute_lines(
|
|
75
|
+
patcher_dict: dict, patcher_var: str, indent: str
|
|
76
|
+
) -> str:
|
|
77
|
+
lines: list[str] = []
|
|
78
|
+
keys_present: set[str] = set()
|
|
79
|
+
for key, value in patcher_dict.items():
|
|
80
|
+
if key in {"boxes", "lines"}:
|
|
81
|
+
continue
|
|
82
|
+
if key.startswith("_"):
|
|
83
|
+
continue
|
|
84
|
+
if key == "rect":
|
|
85
|
+
formatted = _format_rect(value)
|
|
86
|
+
else:
|
|
87
|
+
formatted = _format_value(value)
|
|
88
|
+
line = f"{indent}{patcher_var}.{key} = {formatted}"
|
|
89
|
+
lines.append(line)
|
|
90
|
+
keys_present.add(key)
|
|
91
|
+
missing = sorted(_DEFAULT_PUBLIC_ATTRS - keys_present)
|
|
92
|
+
for key in missing:
|
|
93
|
+
lines.append(f"{indent}if hasattr({patcher_var}, '{key}'):")
|
|
94
|
+
lines.append(f"{indent} delattr({patcher_var}, '{key}')")
|
|
95
|
+
return "\n".join(lines) + ("\n" if lines else "")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _format_box(box: dict) -> tuple[str, dict[str, object]]:
|
|
99
|
+
if "patcher" in box:
|
|
100
|
+
raise NotImplementedError("Subpatcher conversion is not yet supported")
|
|
101
|
+
|
|
102
|
+
key_order = [
|
|
103
|
+
"id",
|
|
104
|
+
"maxclass",
|
|
105
|
+
"text",
|
|
106
|
+
"numinlets",
|
|
107
|
+
"numoutlets",
|
|
108
|
+
"outlettype",
|
|
109
|
+
"patching_rect",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
parts: list[str] = []
|
|
113
|
+
handled = set()
|
|
114
|
+
post_assign: dict[str, object] = {}
|
|
115
|
+
for key in key_order:
|
|
116
|
+
if key not in box:
|
|
117
|
+
continue
|
|
118
|
+
value = box[key]
|
|
119
|
+
if key == "patching_rect":
|
|
120
|
+
formatted = _format_rect(value)
|
|
121
|
+
elif key == "numoutlets" and value == 0:
|
|
122
|
+
post_assign[key] = value
|
|
123
|
+
handled.add(key)
|
|
124
|
+
continue
|
|
125
|
+
else:
|
|
126
|
+
formatted = _format_value(value)
|
|
127
|
+
parts.append(f"{key}={formatted}")
|
|
128
|
+
handled.add(key)
|
|
129
|
+
|
|
130
|
+
for key in sorted(box.keys()):
|
|
131
|
+
if key in handled or key == "patcher":
|
|
132
|
+
continue
|
|
133
|
+
parts.append(f"{key}={_format_value(box[key])}")
|
|
134
|
+
|
|
135
|
+
return f"Box({', '.join(parts)})", post_assign
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class _CodeGenContext:
|
|
139
|
+
def __init__(self) -> None:
|
|
140
|
+
self.counter = 0
|
|
141
|
+
self.helper_functions: list[str] = []
|
|
142
|
+
|
|
143
|
+
def new_subpatch_name(self) -> str:
|
|
144
|
+
name = f"_build_subpatch_{self.counter}"
|
|
145
|
+
self.counter += 1
|
|
146
|
+
return name
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _build_patcher_sections(
|
|
150
|
+
patcher_dict: dict,
|
|
151
|
+
ctx: _CodeGenContext,
|
|
152
|
+
patcher_var: str,
|
|
153
|
+
obj_prefix: str,
|
|
154
|
+
indent: str,
|
|
155
|
+
) -> tuple[str, str, str, dict[str, str]]:
|
|
156
|
+
attr_block = _build_patcher_attribute_lines(patcher_dict, patcher_var, indent)
|
|
157
|
+
box_block, id_map = _build_boxes_block(
|
|
158
|
+
patcher_dict.get("boxes", []), ctx, patcher_var, obj_prefix, indent
|
|
159
|
+
)
|
|
160
|
+
line_block = _build_lines_block(
|
|
161
|
+
patcher_dict.get("lines", []), id_map, patcher_var, indent
|
|
162
|
+
)
|
|
163
|
+
return attr_block, box_block, line_block, id_map
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _build_boxes_block(
|
|
167
|
+
box_entries: list,
|
|
168
|
+
ctx: _CodeGenContext,
|
|
169
|
+
patcher_var: str,
|
|
170
|
+
obj_prefix: str,
|
|
171
|
+
indent: str,
|
|
172
|
+
) -> tuple[str, dict[str, str]]:
|
|
173
|
+
lines: list[str] = []
|
|
174
|
+
id_map: dict[str, str] = {}
|
|
175
|
+
for idx, entry in enumerate(box_entries, start=1):
|
|
176
|
+
box_data = dict(entry.get("box", {}))
|
|
177
|
+
box_id = box_data.get("id", f"obj-{idx}")
|
|
178
|
+
var_name = f"{obj_prefix}_{idx}"
|
|
179
|
+
id_map[box_id] = var_name
|
|
180
|
+
subpatch_dict = box_data.pop("patcher", None)
|
|
181
|
+
box_repr, post_assign = _format_box(box_data)
|
|
182
|
+
lines.append(f"{indent}{var_name} = {patcher_var}.add_box({box_repr})")
|
|
183
|
+
for attr, value in post_assign.items():
|
|
184
|
+
lines.append(f"{indent}{var_name}.{attr} = {_format_value(value)}")
|
|
185
|
+
if subpatch_dict:
|
|
186
|
+
func_name = ctx.new_subpatch_name()
|
|
187
|
+
helper = _build_subpatch_function(func_name, subpatch_dict, ctx)
|
|
188
|
+
ctx.helper_functions.append(helper)
|
|
189
|
+
lines.append(f"{indent}{var_name}._patcher = {func_name}()")
|
|
190
|
+
lines.append(f"{indent}{var_name}._patcher._parent = {patcher_var}")
|
|
191
|
+
return ("\n".join(lines) + ("\n" if lines else ""), id_map)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _build_lines_block(
|
|
195
|
+
lines_data: list, id_var_map: dict[str, str], patcher_var: str, indent: str
|
|
196
|
+
) -> str:
|
|
197
|
+
lines_out: list[str] = []
|
|
198
|
+
for idx, entry in enumerate(lines_data, start=1):
|
|
199
|
+
line_data = entry.get("patchline", {})
|
|
200
|
+
source = line_data.get("source", [None, 0])
|
|
201
|
+
dest = line_data.get("destination", [None, 0])
|
|
202
|
+
src_id = source[0]
|
|
203
|
+
src_outlet = source[1] if len(source) > 1 else 0
|
|
204
|
+
dst_id = dest[0]
|
|
205
|
+
dst_inlet = dest[1] if len(dest) > 1 else 0
|
|
206
|
+
|
|
207
|
+
var_name = f"line_{idx}"
|
|
208
|
+
lines_out.append(
|
|
209
|
+
f"{indent}{var_name} = {patcher_var}.add_patchline('{src_id}', {src_outlet}, '{dst_id}', {dst_inlet})"
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
for key, value in line_data.items():
|
|
213
|
+
if key in {"source", "destination"}:
|
|
214
|
+
continue
|
|
215
|
+
formatted = _format_value(value)
|
|
216
|
+
lines_out.append(f"{indent}{var_name}.{key} = {formatted}")
|
|
217
|
+
|
|
218
|
+
return "\n".join(lines_out) + ("\n" if lines_out else "")
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def maxpat_to_python(
|
|
222
|
+
source: str | Path,
|
|
223
|
+
destination: str | Path,
|
|
224
|
+
default_output: Optional[str] = None,
|
|
225
|
+
) -> Path:
|
|
226
|
+
"""Convert a ``.maxpat`` file into a standalone Python script.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
source: Path to the source `.maxpat` file.
|
|
230
|
+
destination: Path where the Python script will be written.
|
|
231
|
+
default_output: Default output path baked into the generated script.
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Path to the generated Python script.
|
|
235
|
+
"""
|
|
236
|
+
|
|
237
|
+
source_path = Path(source)
|
|
238
|
+
dest_path = Path(destination)
|
|
239
|
+
|
|
240
|
+
with source_path.open(encoding="utf8") as fh:
|
|
241
|
+
payload = json.load(fh)
|
|
242
|
+
|
|
243
|
+
if "patcher" not in payload:
|
|
244
|
+
raise ValueError(f"File {source_path} does not appear to be a .maxpat file")
|
|
245
|
+
|
|
246
|
+
patcher_dict = payload["patcher"]
|
|
247
|
+
|
|
248
|
+
default_output_repr = repr(default_output or f"{source_path.stem}.maxpat")
|
|
249
|
+
|
|
250
|
+
ctx = _CodeGenContext()
|
|
251
|
+
patcher_attr_block, box_block, line_block, _ = _build_patcher_sections(
|
|
252
|
+
patcher_dict, ctx, "p", "obj", " "
|
|
253
|
+
)
|
|
254
|
+
helper_functions_block = "\n".join(ctx.helper_functions)
|
|
255
|
+
if helper_functions_block:
|
|
256
|
+
helper_functions_block += "\n\n"
|
|
257
|
+
|
|
258
|
+
module_text = PY_TEMPLATE.format(
|
|
259
|
+
source_name=source_path.name,
|
|
260
|
+
default_output_repr=default_output_repr,
|
|
261
|
+
patcher_attr_block=patcher_attr_block,
|
|
262
|
+
box_block=box_block,
|
|
263
|
+
line_block=line_block,
|
|
264
|
+
helper_functions_block=helper_functions_block,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
module_text = textwrap.dedent(module_text)
|
|
268
|
+
|
|
269
|
+
if dest_path.parent:
|
|
270
|
+
dest_path.parent.mkdir(parents=True, exist_ok=True)
|
|
271
|
+
|
|
272
|
+
dest_path.write_text(module_text, encoding="utf8")
|
|
273
|
+
return dest_path
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
__all__ = ["maxpat_to_python", "maxref_to_sqlite"]
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _infer_category(ref_path: Path, data: dict) -> str:
|
|
280
|
+
parts = {part.lower() for part in ref_path.parts}
|
|
281
|
+
if "jit-ref" in parts or "jit" in parts:
|
|
282
|
+
return "jit"
|
|
283
|
+
if "msp-ref" in parts or "msp" in parts:
|
|
284
|
+
return "msp"
|
|
285
|
+
if "m4l-ref" in parts or "m4l" in parts:
|
|
286
|
+
return "m4l"
|
|
287
|
+
if "max-ref" in parts or "max" in parts:
|
|
288
|
+
return "max"
|
|
289
|
+
|
|
290
|
+
module = (data.get("module") or "").lower()
|
|
291
|
+
if module in {"jit", "msp", "m4l", "max"}:
|
|
292
|
+
return module
|
|
293
|
+
|
|
294
|
+
tags = data.get("metadata", {}).get("tag")
|
|
295
|
+
if isinstance(tags, list):
|
|
296
|
+
for tag in tags:
|
|
297
|
+
lowered = str(tag).lower()
|
|
298
|
+
if lowered in {"jit", "msp", "m4l", "max"}:
|
|
299
|
+
return lowered
|
|
300
|
+
elif isinstance(tags, str):
|
|
301
|
+
lowered = tags.lower()
|
|
302
|
+
if lowered in {"jit", "msp", "m4l", "max"}:
|
|
303
|
+
return lowered
|
|
304
|
+
|
|
305
|
+
return "unknown"
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def maxref_to_sqlite(
|
|
309
|
+
sqlite_path: str | Path,
|
|
310
|
+
names: Optional[Iterable[str]] = None,
|
|
311
|
+
overwrite: bool = False,
|
|
312
|
+
) -> int:
|
|
313
|
+
"""Populate an SQLite database with maxref metadata using MaxRefDB.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
sqlite_path: Destination SQLite file path.
|
|
317
|
+
names: Optional iterable of object names to include. Defaults to all available.
|
|
318
|
+
overwrite: Whether to remove any existing database before writing.
|
|
319
|
+
|
|
320
|
+
Returns:
|
|
321
|
+
Number of maxref records written.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
path = Path(sqlite_path)
|
|
325
|
+
if overwrite and path.exists():
|
|
326
|
+
path.unlink()
|
|
327
|
+
|
|
328
|
+
# Use MaxRefDB for database operations
|
|
329
|
+
db = MaxRefDB(path)
|
|
330
|
+
|
|
331
|
+
if names is None:
|
|
332
|
+
# Populate with all objects
|
|
333
|
+
db.populate()
|
|
334
|
+
else:
|
|
335
|
+
# Populate with specific objects
|
|
336
|
+
db.populate(list(names))
|
|
337
|
+
|
|
338
|
+
return db.count
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
_DEFAULT_PUBLIC_ATTRS = {
|
|
342
|
+
name
|
|
343
|
+
for name in vars(Patcher()).keys()
|
|
344
|
+
if not name.startswith("_") and name not in {"boxes", "lines"}
|
|
345
|
+
}
|