py2pd 0.2.1__tar.gz → 0.2.2__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.
- {py2pd-0.2.1 → py2pd-0.2.2}/PKG-INFO +1 -1
- {py2pd-0.2.1 → py2pd-0.2.2}/pyproject.toml +1 -1
- {py2pd-0.2.1 → py2pd-0.2.2}/pyproject.toml.orig +1 -1
- {py2pd-0.2.1 → py2pd-0.2.2}/src/py2pd/__init__.py +94 -92
- {py2pd-0.2.1 → py2pd-0.2.2}/src/py2pd/api.py +90 -14
- {py2pd-0.2.1 → py2pd-0.2.2}/LICENSE +0 -0
- {py2pd-0.2.1 → py2pd-0.2.2}/README.md +0 -0
- {py2pd-0.2.1 → py2pd-0.2.2}/src/py2pd/ast.py +0 -0
- {py2pd-0.2.1 → py2pd-0.2.2}/src/py2pd/discover.py +0 -0
- {py2pd-0.2.1 → py2pd-0.2.2}/src/py2pd/integrations/__init__.py +0 -0
- {py2pd-0.2.1 → py2pd-0.2.2}/src/py2pd/integrations/cypd.py +0 -0
- {py2pd-0.2.1 → py2pd-0.2.2}/src/py2pd/integrations/hvcc.py +0 -0
- {py2pd-0.2.1 → py2pd-0.2.2}/src/py2pd/py.typed +0 -0
|
@@ -1,92 +1,94 @@
|
|
|
1
|
-
"""
|
|
2
|
-
py2pd - Python to PureData
|
|
3
|
-
==========================
|
|
4
|
-
|
|
5
|
-
Write PureData patches as Python programs.
|
|
6
|
-
|
|
7
|
-
Builder API example:
|
|
8
|
-
>>> from py2pd import Patcher
|
|
9
|
-
>>> p = Patcher('patch.pd')
|
|
10
|
-
>>> osc = p.add('osc~ 440')
|
|
11
|
-
>>> dac = p.add('dac~')
|
|
12
|
-
>>> p.link(osc, dac)
|
|
13
|
-
>>> p.link(osc, dac, inlet=1) # stereo
|
|
14
|
-
>>> p.save()
|
|
15
|
-
|
|
16
|
-
AST API example (round-trip):
|
|
17
|
-
>>> from py2pd import parse_file, serialize
|
|
18
|
-
>>> ast = parse_file('input.pd')
|
|
19
|
-
>>> # Modify the AST...
|
|
20
|
-
>>> with open('output.pd', 'w') as f:
|
|
21
|
-
... f.write(serialize(ast))
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
from .api import (
|
|
25
|
-
COLUMN_WIDTH,
|
|
26
|
-
DEFAULT_MARGIN,
|
|
27
|
-
ROW_HEIGHT,
|
|
28
|
-
SUBPATCH_CANVAS_HEIGHT,
|
|
29
|
-
SUBPATCH_CANVAS_WIDTH,
|
|
30
|
-
Abstraction,
|
|
31
|
-
CycleWarning,
|
|
32
|
-
GridLayoutManager,
|
|
33
|
-
InvalidConnectionError,
|
|
34
|
-
LayoutManager,
|
|
35
|
-
NodeNotFoundError,
|
|
36
|
-
Patcher,
|
|
37
|
-
PdConnectionError,
|
|
38
|
-
PdConnectionWarning,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
"
|
|
73
|
-
#
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
"
|
|
77
|
-
"
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
1
|
+
"""
|
|
2
|
+
py2pd - Python to PureData
|
|
3
|
+
==========================
|
|
4
|
+
|
|
5
|
+
Write PureData patches as Python programs.
|
|
6
|
+
|
|
7
|
+
Builder API example:
|
|
8
|
+
>>> from py2pd import Patcher
|
|
9
|
+
>>> p = Patcher('patch.pd')
|
|
10
|
+
>>> osc = p.add('osc~ 440')
|
|
11
|
+
>>> dac = p.add('dac~')
|
|
12
|
+
>>> p.link(osc, dac)
|
|
13
|
+
>>> p.link(osc, dac, inlet=1) # stereo
|
|
14
|
+
>>> p.save()
|
|
15
|
+
|
|
16
|
+
AST API example (round-trip):
|
|
17
|
+
>>> from py2pd import parse_file, serialize
|
|
18
|
+
>>> ast = parse_file('input.pd')
|
|
19
|
+
>>> # Modify the AST...
|
|
20
|
+
>>> with open('output.pd', 'w') as f:
|
|
21
|
+
... f.write(serialize(ast))
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from .api import (
|
|
25
|
+
COLUMN_WIDTH,
|
|
26
|
+
DEFAULT_MARGIN,
|
|
27
|
+
ROW_HEIGHT,
|
|
28
|
+
SUBPATCH_CANVAS_HEIGHT,
|
|
29
|
+
SUBPATCH_CANVAS_WIDTH,
|
|
30
|
+
Abstraction,
|
|
31
|
+
CycleWarning,
|
|
32
|
+
GridLayoutManager,
|
|
33
|
+
InvalidConnectionError,
|
|
34
|
+
LayoutManager,
|
|
35
|
+
NodeNotFoundError,
|
|
36
|
+
Patcher,
|
|
37
|
+
PdConnectionError,
|
|
38
|
+
PdConnectionWarning,
|
|
39
|
+
SubpatchIOOrderWarning,
|
|
40
|
+
lookup_object_io,
|
|
41
|
+
)
|
|
42
|
+
from .ast import (
|
|
43
|
+
ParseError,
|
|
44
|
+
UnsupportedElementWarning,
|
|
45
|
+
from_builder,
|
|
46
|
+
parse,
|
|
47
|
+
parse_file,
|
|
48
|
+
serialize,
|
|
49
|
+
serialize_to_file,
|
|
50
|
+
to_builder,
|
|
51
|
+
)
|
|
52
|
+
from .discover import (
|
|
53
|
+
default_search_paths,
|
|
54
|
+
discover_externals,
|
|
55
|
+
extract_declare_paths,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
# Builder API
|
|
60
|
+
"Patcher",
|
|
61
|
+
"Abstraction",
|
|
62
|
+
"LayoutManager",
|
|
63
|
+
"GridLayoutManager",
|
|
64
|
+
# Exceptions
|
|
65
|
+
"PdConnectionError",
|
|
66
|
+
"NodeNotFoundError",
|
|
67
|
+
"InvalidConnectionError",
|
|
68
|
+
"CycleWarning",
|
|
69
|
+
"PdConnectionWarning",
|
|
70
|
+
"SubpatchIOOrderWarning",
|
|
71
|
+
"UnsupportedElementWarning",
|
|
72
|
+
"ParseError",
|
|
73
|
+
# Object registry
|
|
74
|
+
"lookup_object_io",
|
|
75
|
+
# Layout constants
|
|
76
|
+
"ROW_HEIGHT",
|
|
77
|
+
"COLUMN_WIDTH",
|
|
78
|
+
"DEFAULT_MARGIN",
|
|
79
|
+
"SUBPATCH_CANVAS_WIDTH",
|
|
80
|
+
"SUBPATCH_CANVAS_HEIGHT",
|
|
81
|
+
# AST functions
|
|
82
|
+
"parse",
|
|
83
|
+
"parse_file",
|
|
84
|
+
"serialize",
|
|
85
|
+
"serialize_to_file",
|
|
86
|
+
"from_builder",
|
|
87
|
+
"to_builder",
|
|
88
|
+
# Discovery
|
|
89
|
+
"discover_externals",
|
|
90
|
+
"default_search_paths",
|
|
91
|
+
"extract_declare_paths",
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
__version__ = "0.2.2"
|
|
@@ -4,6 +4,10 @@ import re
|
|
|
4
4
|
from typing import Any, Callable, Dict, FrozenSet, List, Optional, Sequence, Set, Tuple, Union
|
|
5
5
|
import warnings
|
|
6
6
|
|
|
7
|
+
# The builder writes the same number format as the AST serializer, so it uses
|
|
8
|
+
# the same helper. ast imports api only lazily, so this direction does not cycle.
|
|
9
|
+
from .ast import _fmt_num
|
|
10
|
+
|
|
7
11
|
# Layout constants (pixels)
|
|
8
12
|
ROW_HEIGHT = 25
|
|
9
13
|
COLUMN_WIDTH = 50
|
|
@@ -53,6 +57,12 @@ class CycleWarning(UserWarning):
|
|
|
53
57
|
pass
|
|
54
58
|
|
|
55
59
|
|
|
60
|
+
class SubpatchIOOrderWarning(UserWarning):
|
|
61
|
+
"""A subpatch's inlet or outlet order will not match creation order."""
|
|
62
|
+
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
56
66
|
class PdConnectionWarning(UserWarning):
|
|
57
67
|
"""Warning raised for an out-of-range connection when validation is advisory.
|
|
58
68
|
|
|
@@ -64,11 +74,65 @@ class PdConnectionWarning(UserWarning):
|
|
|
64
74
|
pass
|
|
65
75
|
|
|
66
76
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
77
|
+
_IO_KINDS = (("inlet", ("inlet", "inlet~")), ("outlet", ("outlet", "outlet~")))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _subpatch_io_nodes(src: "Patcher", names: Tuple[str, str]) -> List["Obj"]:
|
|
81
|
+
"""The inlet or outlet objects of *src*, in creation order."""
|
|
82
|
+
return [
|
|
83
|
+
n
|
|
84
|
+
for n in src.nodes
|
|
85
|
+
if isinstance(n, Obj) and n.parameters["text"].split()[:1] in ([names[0]], [names[1]])
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _order_subpatch_io(src: "Patcher") -> None:
|
|
90
|
+
"""Spread a subpatch's inlet and outlet objects so PureData orders them by creation.
|
|
91
|
+
|
|
92
|
+
PureData derives a subpatch's inlet and outlet order from the x position of
|
|
93
|
+
the ``inlet`` / ``outlet`` objects, breaking a tie in reverse file order.
|
|
94
|
+
The default layout stacks every node at one x, so two inlets created in
|
|
95
|
+
order reach the parent reversed -- a patch that loads without complaint and
|
|
96
|
+
is wired backwards.
|
|
97
|
+
|
|
98
|
+
Only a tie is repaired, and only among objects the caller never positioned:
|
|
99
|
+
distinct positions already give an unambiguous order, and an explicit
|
|
100
|
+
``x_pos`` is a decision to respect. Repositioning is by creation order, so
|
|
101
|
+
``link(..., inlet=n)`` means the n-th ``inlet`` object created.
|
|
102
|
+
"""
|
|
103
|
+
for _, names in _IO_KINDS:
|
|
104
|
+
nodes = _subpatch_io_nodes(src, names)
|
|
105
|
+
if len(nodes) < 2:
|
|
106
|
+
continue
|
|
107
|
+
xs = [n.parameters["x_pos"] for n in nodes]
|
|
108
|
+
if len(set(xs)) == len(xs):
|
|
109
|
+
continue # already unambiguous
|
|
110
|
+
if any(id(n) in src._explicit_x for n in nodes):
|
|
111
|
+
continue # the caller placed at least one; leave the layout alone
|
|
112
|
+
base = min(xs)
|
|
113
|
+
for i, node in enumerate(nodes):
|
|
114
|
+
node.parameters["x_pos"] = base + i * SUBPATCH_IO_SPACING
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _warn_on_ambiguous_subpatch_io(src: "Patcher", name: str) -> None:
|
|
118
|
+
"""Warn when a subpatch's inlet or outlet order will not match creation order.
|
|
119
|
+
|
|
120
|
+
PureData orders a subpatch's inlets and outlets by the x position of the
|
|
121
|
+
``inlet`` / ``outlet`` objects inside it, and breaks a tie in reverse file
|
|
122
|
+
order. The default layout stacks every node at the same x, so two inlets
|
|
123
|
+
created in order arrive at the parent reversed -- a patch that loads
|
|
124
|
+
without complaint and is wired wrong.
|
|
125
|
+
"""
|
|
126
|
+
for kind, names in _IO_KINDS:
|
|
127
|
+
xs = [n.parameters["x_pos"] for n in _subpatch_io_nodes(src, names)]
|
|
128
|
+
if len(xs) > 1 and len(set(xs)) != len(xs):
|
|
129
|
+
warnings.warn(
|
|
130
|
+
f"subpatch {name!r} has {len(xs)} {kind}s sharing an x position; "
|
|
131
|
+
f"PureData orders them by x and will not use creation order. "
|
|
132
|
+
f"Give each {kind} a distinct x_pos to fix the order.",
|
|
133
|
+
SubpatchIOOrderWarning,
|
|
134
|
+
stacklevel=3,
|
|
135
|
+
)
|
|
72
136
|
|
|
73
137
|
|
|
74
138
|
def escape(text: str) -> str:
|
|
@@ -418,7 +482,7 @@ class Float(Node):
|
|
|
418
482
|
p = self.parameters
|
|
419
483
|
return (
|
|
420
484
|
f"#X floatatom {p['x_pos']} {p['y_pos']} {p['width']} "
|
|
421
|
-
f"{p['lower_limit']} {p['upper_limit']} {p['label_pos']} "
|
|
485
|
+
f"{_fmt_num(p['lower_limit'])} {_fmt_num(p['upper_limit'])} {p['label_pos']} "
|
|
422
486
|
f"{p['label']} {p['receive']} {p['send']};\n"
|
|
423
487
|
)
|
|
424
488
|
|
|
@@ -469,6 +533,9 @@ class Comment(Node):
|
|
|
469
533
|
|
|
470
534
|
|
|
471
535
|
# Default subpatch canvas dimensions (pixels)
|
|
536
|
+
# Horizontal gap applied when spreading a subpatch's inlet/outlet objects so
|
|
537
|
+
# PureData orders them by creation rather than by an arbitrary tie-break.
|
|
538
|
+
SUBPATCH_IO_SPACING = 70
|
|
472
539
|
SUBPATCH_CANVAS_WIDTH = 300
|
|
473
540
|
SUBPATCH_CANVAS_HEIGHT = 180
|
|
474
541
|
|
|
@@ -613,7 +680,7 @@ class Subpatch(Node):
|
|
|
613
680
|
# separate field, and the two values after the flag are the
|
|
614
681
|
# viewport margins.
|
|
615
682
|
gop_flag = 2 if p["hide_name"] else 1
|
|
616
|
-
rect = " ".join(
|
|
683
|
+
rect = " ".join(_fmt_num(v) for v in p.get("gop_rect", (0, 1, 1, 0)))
|
|
617
684
|
margins = p.get("gop_margins", (0, 0))
|
|
618
685
|
tail = "" if margins is None else f" {margins[0]} {margins[1]}"
|
|
619
686
|
coords_line = f"#X coords {rect} {p['gop_width']} {p['gop_height']} {gop_flag}{tail};\n"
|
|
@@ -995,7 +1062,7 @@ class Symbol(Node):
|
|
|
995
1062
|
p = self.parameters
|
|
996
1063
|
return (
|
|
997
1064
|
f"#X symbolatom {p['x_pos']} {p['y_pos']} {p['width']} "
|
|
998
|
-
f"{p['lower_limit']} {p['upper_limit']} {p['label_pos']} "
|
|
1065
|
+
f"{_fmt_num(p['lower_limit'])} {_fmt_num(p['upper_limit'])} {p['label_pos']} "
|
|
999
1066
|
f"{p['label']} {p['receive']} {p['send']};\n"
|
|
1000
1067
|
)
|
|
1001
1068
|
|
|
@@ -1073,11 +1140,11 @@ class NumberBox(Node):
|
|
|
1073
1140
|
p = self.parameters
|
|
1074
1141
|
return (
|
|
1075
1142
|
f"#X obj {p['x_pos']} {p['y_pos']} nbx {p['width']} {p['height']} "
|
|
1076
|
-
f"{p['min_val']} {p['max_val']} {p['log_flag']} {p['init']} "
|
|
1143
|
+
f"{_fmt_num(p['min_val'])} {_fmt_num(p['max_val'])} {p['log_flag']} {p['init']} "
|
|
1077
1144
|
f"{p['send']} {p['receive']} {p['label']} "
|
|
1078
1145
|
f"{p['label_x']} {p['label_y']} {p['font']} {p['font_size']} "
|
|
1079
1146
|
f"{p['bg_color']} {p['fg_color']} {p['label_color']} "
|
|
1080
|
-
f"{p['init_value']} {p['log_height']};\n"
|
|
1147
|
+
f"{_fmt_num(p['init_value'])} {p['log_height']};\n"
|
|
1081
1148
|
)
|
|
1082
1149
|
|
|
1083
1150
|
@property
|
|
@@ -1151,11 +1218,11 @@ class VSlider(Node):
|
|
|
1151
1218
|
p = self.parameters
|
|
1152
1219
|
return (
|
|
1153
1220
|
f"#X obj {p['x_pos']} {p['y_pos']} vsl {p['width']} {p['height']} "
|
|
1154
|
-
f"{p['min_val']} {p['max_val']} {p['log_flag']} {p['init']} "
|
|
1221
|
+
f"{_fmt_num(p['min_val'])} {_fmt_num(p['max_val'])} {p['log_flag']} {p['init']} "
|
|
1155
1222
|
f"{p['send']} {p['receive']} {p['label']} "
|
|
1156
1223
|
f"{p['label_x']} {p['label_y']} {p['font']} {p['font_size']} "
|
|
1157
1224
|
f"{p['bg_color']} {p['fg_color']} {p['label_color']} "
|
|
1158
|
-
f"{p['init_value']} {p['steady']};\n"
|
|
1225
|
+
f"{_fmt_num(p['init_value'])} {p['steady']};\n"
|
|
1159
1226
|
)
|
|
1160
1227
|
|
|
1161
1228
|
@property
|
|
@@ -1226,11 +1293,11 @@ class HSlider(Node):
|
|
|
1226
1293
|
p = self.parameters
|
|
1227
1294
|
return (
|
|
1228
1295
|
f"#X obj {p['x_pos']} {p['y_pos']} hsl {p['width']} {p['height']} "
|
|
1229
|
-
f"{p['min_val']} {p['max_val']} {p['log_flag']} {p['init']} "
|
|
1296
|
+
f"{_fmt_num(p['min_val'])} {_fmt_num(p['max_val'])} {p['log_flag']} {p['init']} "
|
|
1230
1297
|
f"{p['send']} {p['receive']} {p['label']} "
|
|
1231
1298
|
f"{p['label_x']} {p['label_y']} {p['font']} {p['font_size']} "
|
|
1232
1299
|
f"{p['bg_color']} {p['fg_color']} {p['label_color']} "
|
|
1233
|
-
f"{p['init_value']} {p['steady']};\n"
|
|
1300
|
+
f"{_fmt_num(p['init_value'])} {p['steady']};\n"
|
|
1234
1301
|
)
|
|
1235
1302
|
|
|
1236
1303
|
@property
|
|
@@ -2213,6 +2280,9 @@ class Patcher:
|
|
|
2213
2280
|
self.canvas_height = canvas_height
|
|
2214
2281
|
self.font_size = font_size
|
|
2215
2282
|
self._node_positions: Dict[int, int] = {}
|
|
2283
|
+
# Nodes whose x the caller set. Subpatch I/O ordering may reposition
|
|
2284
|
+
# the others; it must never move one the caller placed.
|
|
2285
|
+
self._explicit_x: Set[int] = set()
|
|
2216
2286
|
|
|
2217
2287
|
@property
|
|
2218
2288
|
def row_head(self) -> Optional[Node]:
|
|
@@ -2258,10 +2328,13 @@ class Patcher:
|
|
|
2258
2328
|
) -> Tuple[int, int, Callable[[Node], None]]:
|
|
2259
2329
|
"""Resolve position for a new element."""
|
|
2260
2330
|
was_absolute = x_pos >= 0 and y_pos >= 0
|
|
2331
|
+
x_was_given = x_pos >= 0
|
|
2261
2332
|
computed_x, computed_y = self.layout.compute_position(new_row, new_col, x_pos, y_pos)
|
|
2262
2333
|
|
|
2263
2334
|
def position_update(node: Node) -> None:
|
|
2264
2335
|
self.layout.register_node(node, new_row, new_col, was_absolute)
|
|
2336
|
+
if x_was_given:
|
|
2337
|
+
self._explicit_x.add(id(node))
|
|
2265
2338
|
|
|
2266
2339
|
return (computed_x, computed_y, position_update)
|
|
2267
2340
|
|
|
@@ -2576,6 +2649,9 @@ class Patcher:
|
|
|
2576
2649
|
and n.parameters["text"].split()[:1] in (["outlet"], ["outlet~"])
|
|
2577
2650
|
)
|
|
2578
2651
|
|
|
2652
|
+
_order_subpatch_io(src)
|
|
2653
|
+
_warn_on_ambiguous_subpatch_io(src, name)
|
|
2654
|
+
|
|
2579
2655
|
x_pos, y_pos, pos_update = self._resolve_position(x_pos, y_pos, new_row, new_col)
|
|
2580
2656
|
node = Subpatch(
|
|
2581
2657
|
x_pos,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|