py2pd 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {py2pd-0.2.2 → py2pd-0.2.3}/PKG-INFO +2 -2
- {py2pd-0.2.2 → py2pd-0.2.3}/README.md +1 -1
- {py2pd-0.2.2 → py2pd-0.2.3}/pyproject.toml +1 -1
- {py2pd-0.2.2 → py2pd-0.2.3}/pyproject.toml.orig +1 -1
- {py2pd-0.2.2 → py2pd-0.2.3}/src/py2pd/__init__.py +1 -1
- {py2pd-0.2.2 → py2pd-0.2.3}/src/py2pd/api.py +230 -19
- {py2pd-0.2.2 → py2pd-0.2.3}/src/py2pd/ast.py +71 -37
- {py2pd-0.2.2 → py2pd-0.2.3}/src/py2pd/discover.py +5 -2
- {py2pd-0.2.2 → py2pd-0.2.3}/src/py2pd/integrations/cypd.py +4 -4
- {py2pd-0.2.2 → py2pd-0.2.3}/LICENSE +0 -0
- {py2pd-0.2.2 → py2pd-0.2.3}/src/py2pd/integrations/__init__.py +0 -0
- {py2pd-0.2.2 → py2pd-0.2.3}/src/py2pd/integrations/hvcc.py +0 -0
- {py2pd-0.2.2 → py2pd-0.2.3}/src/py2pd/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: py2pd
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Roundtrip parsing and generation of pure-data patches from python
|
|
5
5
|
Keywords: puredata,pd,audio,dsp,music,synthesis,patching
|
|
6
6
|
Author: Shakeeb Alireza
|
|
@@ -483,4 +483,4 @@ from py2pd import (
|
|
|
483
483
|
|
|
484
484
|
`PdConnectionError` is raised eagerly by `link()` when outlet or inlet indices exceed the node's known I/O counts. `Node.__getitem__` (e.g., `osc[2]`) raises `ValueError` for out-of-range outlet indices. Both checks are skipped for objects with unknown counts (`num_outlets=None` / `num_inlets=None`), and `link()` warns instead of raising when the patch was created with `validate_links=False`.
|
|
485
485
|
|
|
486
|
-
`to_builder()` issues `UnsupportedElementWarning` (from `py2pd.ast`) for
|
|
486
|
+
`to_builder()` issues `UnsupportedElementWarning` (from `py2pd.ast`) for a connection it cannot rebuild, rather than dropping patch content silently. Statements the Builder does not model are carried verbatim instead of warned about, so `parse -> to_builder -> from_builder -> serialize` returns the bytes it started with.
|
|
@@ -451,4 +451,4 @@ from py2pd import (
|
|
|
451
451
|
|
|
452
452
|
`PdConnectionError` is raised eagerly by `link()` when outlet or inlet indices exceed the node's known I/O counts. `Node.__getitem__` (e.g., `osc[2]`) raises `ValueError` for out-of-range outlet indices. Both checks are skipped for objects with unknown counts (`num_outlets=None` / `num_inlets=None`), and `link()` warns instead of raising when the patch was created with `validate_links=False`.
|
|
453
453
|
|
|
454
|
-
`to_builder()` issues `UnsupportedElementWarning` (from `py2pd.ast`) for
|
|
454
|
+
`to_builder()` issues `UnsupportedElementWarning` (from `py2pd.ast`) for a connection it cannot rebuild, rather than dropping patch content silently. Statements the Builder does not model are carried verbatim instead of warned about, so `parse -> to_builder -> from_builder -> serialize` returns the bytes it started with.
|
|
@@ -6,7 +6,7 @@ import warnings
|
|
|
6
6
|
|
|
7
7
|
# The builder writes the same number format as the AST serializer, so it uses
|
|
8
8
|
# the same helper. ast imports api only lazily, so this direction does not cycle.
|
|
9
|
-
from .ast import _fmt_num
|
|
9
|
+
from .ast import PdCoords, PdDeclare, _fmt_num
|
|
10
10
|
|
|
11
11
|
# Layout constants (pixels)
|
|
12
12
|
ROW_HEIGHT = 25
|
|
@@ -221,6 +221,20 @@ class Node:
|
|
|
221
221
|
hidden: bool = False
|
|
222
222
|
num_inlets: Optional[int] = None
|
|
223
223
|
num_outlets: Optional[int] = None
|
|
224
|
+
# A carried statement that followed the #X connect lines in the file it was
|
|
225
|
+
# read from, and so must be written back after them.
|
|
226
|
+
after_connections: bool = False
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def occupies_connect_index(self) -> bool:
|
|
230
|
+
"""Whether PureData counts this node when numbering ``#X connect``.
|
|
231
|
+
|
|
232
|
+
Almost everything does. ``#X declare``, ``#A`` array data and ``#X f``
|
|
233
|
+
box widths are statements rather than objects, so they sit in the node
|
|
234
|
+
list without taking an index; a node that returns False here shifts no
|
|
235
|
+
connection.
|
|
236
|
+
"""
|
|
237
|
+
return True
|
|
224
238
|
|
|
225
239
|
class Outlet:
|
|
226
240
|
"""Reference to a specific outlet of a Node, used for creating connections."""
|
|
@@ -463,6 +477,7 @@ class Float(Node):
|
|
|
463
477
|
send: str = "-",
|
|
464
478
|
num_inlets: Optional[int] = 1,
|
|
465
479
|
num_outlets: Optional[int] = 1,
|
|
480
|
+
font_size: Optional[int] = None,
|
|
466
481
|
) -> None:
|
|
467
482
|
self.parameters = {
|
|
468
483
|
"x_pos": x_pos,
|
|
@@ -474,16 +489,18 @@ class Float(Node):
|
|
|
474
489
|
"label": label,
|
|
475
490
|
"receive": receive,
|
|
476
491
|
"send": send,
|
|
492
|
+
"font_size": font_size,
|
|
477
493
|
}
|
|
478
494
|
self.num_inlets = num_inlets
|
|
479
495
|
self.num_outlets = num_outlets
|
|
480
496
|
|
|
481
497
|
def __str__(self) -> str:
|
|
482
498
|
p = self.parameters
|
|
499
|
+
tail = "" if p["font_size"] is None else f" {p['font_size']}"
|
|
483
500
|
return (
|
|
484
501
|
f"#X floatatom {p['x_pos']} {p['y_pos']} {p['width']} "
|
|
485
502
|
f"{_fmt_num(p['lower_limit'])} {_fmt_num(p['upper_limit'])} {p['label_pos']} "
|
|
486
|
-
f"{p['label']} {p['receive']} {p['send']};\n"
|
|
503
|
+
f"{p['label']} {p['receive']} {p['send']}{tail};\n"
|
|
487
504
|
)
|
|
488
505
|
|
|
489
506
|
@property
|
|
@@ -610,6 +627,12 @@ class Subpatch(Node):
|
|
|
610
627
|
is_graph: bool = False,
|
|
611
628
|
gop_rect: Tuple[float, float, float, float] = (0, 1, 1, 0),
|
|
612
629
|
gop_margins: Optional[Tuple[int, int]] = (0, 0),
|
|
630
|
+
canvas_x: int = 0,
|
|
631
|
+
canvas_y: int = 0,
|
|
632
|
+
canvas_name: str = "(subpatch)",
|
|
633
|
+
canvas_font_size: int = 10,
|
|
634
|
+
open_on_load: int = 0,
|
|
635
|
+
restore_kind: Optional[str] = None,
|
|
613
636
|
) -> None:
|
|
614
637
|
"""Create a subpatch node.
|
|
615
638
|
|
|
@@ -667,6 +690,12 @@ class Subpatch(Node):
|
|
|
667
690
|
"is_graph": is_graph,
|
|
668
691
|
"gop_rect": gop_rect,
|
|
669
692
|
"gop_margins": gop_margins,
|
|
693
|
+
"canvas_x": canvas_x,
|
|
694
|
+
"canvas_y": canvas_y,
|
|
695
|
+
"canvas_name": canvas_name,
|
|
696
|
+
"canvas_font_size": canvas_font_size,
|
|
697
|
+
"open_on_load": open_on_load,
|
|
698
|
+
"restore_kind": restore_kind,
|
|
670
699
|
}
|
|
671
700
|
self.num_inlets = num_inlets
|
|
672
701
|
self.num_outlets = num_outlets
|
|
@@ -674,7 +703,8 @@ class Subpatch(Node):
|
|
|
674
703
|
def __str__(self) -> str:
|
|
675
704
|
p = self.parameters
|
|
676
705
|
coords_line = ""
|
|
677
|
-
|
|
706
|
+
carries_own_coords = any(isinstance(n, Coords) for n in self.src.nodes)
|
|
707
|
+
if p["graph_on_parent"] and not carries_own_coords:
|
|
678
708
|
# PureData encodes "hide object name and arguments" in the
|
|
679
709
|
# graph-on-parent flag itself: 1 = shown, 2 = hidden. There is no
|
|
680
710
|
# separate field, and the two values after the flag are the
|
|
@@ -684,13 +714,17 @@ class Subpatch(Node):
|
|
|
684
714
|
margins = p.get("gop_margins", (0, 0))
|
|
685
715
|
tail = "" if margins is None else f" {margins[0]} {margins[1]}"
|
|
686
716
|
coords_line = f"#X coords {rect} {p['gop_width']} {p['gop_height']} {gop_flag}{tail};\n"
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
717
|
+
# A parsed subpatch keeps the kind PureData wrote ("page" and others
|
|
718
|
+
# exist); one the builder created derives it from is_graph. A graph
|
|
719
|
+
# canvas closes without a name.
|
|
720
|
+
kind = p.get("restore_kind") or ("graph" if p.get("is_graph") else "pd")
|
|
721
|
+
name = "" if kind == "graph" else p["name"]
|
|
722
|
+
suffix = f" {name}" if name else ""
|
|
723
|
+
restore = f"#X restore {p['x_pos']} {p['y_pos']} {kind}{suffix};\n"
|
|
692
724
|
return (
|
|
693
|
-
f"#N canvas
|
|
725
|
+
f"#N canvas {p['canvas_x']} {p['canvas_y']} "
|
|
726
|
+
f"{self.canvas_width} {self.canvas_height} "
|
|
727
|
+
f"{p['canvas_name']} {p['open_on_load']};\n"
|
|
694
728
|
f"{self.src._subpatch_str()}"
|
|
695
729
|
f"{coords_line}"
|
|
696
730
|
f"{restore}"
|
|
@@ -845,6 +879,89 @@ class Array(Node):
|
|
|
845
879
|
return f"Array({p['name']!r}, {p['length']})"
|
|
846
880
|
|
|
847
881
|
|
|
882
|
+
class Raw(Node):
|
|
883
|
+
"""A statement py2pd has no builder node for, carried verbatim.
|
|
884
|
+
|
|
885
|
+
``#X scalar``, ``#X listbox``, ``#A`` array data and ``#X f`` box widths
|
|
886
|
+
have no builder representation, but a patch read with ``to_builder()`` must
|
|
887
|
+
still write back what it read. The text is stored exactly as parsed.
|
|
888
|
+
|
|
889
|
+
Parameters
|
|
890
|
+
----------
|
|
891
|
+
text : str
|
|
892
|
+
The statement without its trailing semicolon.
|
|
893
|
+
is_object : bool
|
|
894
|
+
Whether PureData counts the statement as an object on the canvas, which
|
|
895
|
+
decides whether it takes a ``#X connect`` index.
|
|
896
|
+
"""
|
|
897
|
+
|
|
898
|
+
def __init__(self, text: str, is_object: bool = False) -> None:
|
|
899
|
+
self.hidden = True
|
|
900
|
+
self.parameters = {"text": text, "is_object": is_object}
|
|
901
|
+
self.num_inlets = None
|
|
902
|
+
self.num_outlets = None
|
|
903
|
+
|
|
904
|
+
@property
|
|
905
|
+
def occupies_connect_index(self) -> bool:
|
|
906
|
+
return bool(self.parameters["is_object"])
|
|
907
|
+
|
|
908
|
+
def __str__(self) -> str:
|
|
909
|
+
return f"{self.parameters['text']};\n"
|
|
910
|
+
|
|
911
|
+
def __repr__(self) -> str:
|
|
912
|
+
return f"Raw({self.parameters['text'][:40]!r})"
|
|
913
|
+
|
|
914
|
+
|
|
915
|
+
class Declare(Node):
|
|
916
|
+
"""A ``#X declare`` statement (search paths and libraries).
|
|
917
|
+
|
|
918
|
+
Wraps the AST's ``PdDeclare`` rather than re-modelling it, so the builder
|
|
919
|
+
writes back exactly what the parser read and ``extract_declare_paths()``
|
|
920
|
+
keeps working on a patch that went through the builder.
|
|
921
|
+
"""
|
|
922
|
+
|
|
923
|
+
def __init__(self, declare: "PdDeclare") -> None:
|
|
924
|
+
self.hidden = True
|
|
925
|
+
self.parameters = {"declare": declare}
|
|
926
|
+
self.num_inlets = 0
|
|
927
|
+
self.num_outlets = 0
|
|
928
|
+
|
|
929
|
+
@property
|
|
930
|
+
def occupies_connect_index(self) -> bool:
|
|
931
|
+
return False
|
|
932
|
+
|
|
933
|
+
def __str__(self) -> str:
|
|
934
|
+
return f"{self.parameters['declare']}\n"
|
|
935
|
+
|
|
936
|
+
def __repr__(self) -> str:
|
|
937
|
+
return f"Declare({self.parameters['declare']!r})"
|
|
938
|
+
|
|
939
|
+
|
|
940
|
+
class Coords(Node):
|
|
941
|
+
"""An ``#X coords`` statement that no subpatch consumed.
|
|
942
|
+
|
|
943
|
+
A subpatch folds its own ``#X coords`` into graph-on-parent parameters. One
|
|
944
|
+
on the top-level canvas, or a second one inside a subpatch, has nowhere to
|
|
945
|
+
go, so it is carried as its parsed ``PdCoords``.
|
|
946
|
+
"""
|
|
947
|
+
|
|
948
|
+
def __init__(self, coords: "PdCoords") -> None:
|
|
949
|
+
self.hidden = True
|
|
950
|
+
self.parameters = {"coords": coords}
|
|
951
|
+
self.num_inlets = 0
|
|
952
|
+
self.num_outlets = 0
|
|
953
|
+
|
|
954
|
+
@property
|
|
955
|
+
def occupies_connect_index(self) -> bool:
|
|
956
|
+
return False
|
|
957
|
+
|
|
958
|
+
def __str__(self) -> str:
|
|
959
|
+
return f"{self.parameters['coords']}\n"
|
|
960
|
+
|
|
961
|
+
def __repr__(self) -> str:
|
|
962
|
+
return f"Coords({self.parameters['coords']!r})"
|
|
963
|
+
|
|
964
|
+
|
|
848
965
|
# An IEM GUI colour is either a legacy packed negative integer (PureData < 0.47)
|
|
849
966
|
# or a hex string such as ``#fcfcfc`` (PureData >= 0.47). Both are accepted and
|
|
850
967
|
# written out unchanged.
|
|
@@ -1043,6 +1160,7 @@ class Symbol(Node):
|
|
|
1043
1160
|
label: str = "-",
|
|
1044
1161
|
receive: str = "-",
|
|
1045
1162
|
send: str = "-",
|
|
1163
|
+
font_size: Optional[int] = None,
|
|
1046
1164
|
) -> None:
|
|
1047
1165
|
self.parameters = {
|
|
1048
1166
|
"x_pos": x_pos,
|
|
@@ -1054,16 +1172,18 @@ class Symbol(Node):
|
|
|
1054
1172
|
"label": label,
|
|
1055
1173
|
"receive": receive,
|
|
1056
1174
|
"send": send,
|
|
1175
|
+
"font_size": font_size,
|
|
1057
1176
|
}
|
|
1058
1177
|
self.num_inlets = 1
|
|
1059
1178
|
self.num_outlets = 1
|
|
1060
1179
|
|
|
1061
1180
|
def __str__(self) -> str:
|
|
1062
1181
|
p = self.parameters
|
|
1182
|
+
tail = "" if p["font_size"] is None else f" {p['font_size']}"
|
|
1063
1183
|
return (
|
|
1064
1184
|
f"#X symbolatom {p['x_pos']} {p['y_pos']} {p['width']} "
|
|
1065
1185
|
f"{_fmt_num(p['lower_limit'])} {_fmt_num(p['upper_limit'])} {p['label_pos']} "
|
|
1066
|
-
f"{p['label']} {p['receive']} {p['send']};\n"
|
|
1186
|
+
f"{p['label']} {p['receive']} {p['send']}{tail};\n"
|
|
1067
1187
|
)
|
|
1068
1188
|
|
|
1069
1189
|
@property
|
|
@@ -2280,6 +2400,14 @@ class Patcher:
|
|
|
2280
2400
|
self.canvas_height = canvas_height
|
|
2281
2401
|
self.font_size = font_size
|
|
2282
2402
|
self._node_positions: Dict[int, int] = {}
|
|
2403
|
+
# Statements above the #N canvas line (#N struct and friends), carried
|
|
2404
|
+
# verbatim so a parsed patch writes back what it read.
|
|
2405
|
+
self.preamble: List[str] = []
|
|
2406
|
+
# Set once a node that takes no #X connect index is added. While it is
|
|
2407
|
+
# False the connect index is the list position, which is the common case.
|
|
2408
|
+
self._has_unindexed = False
|
|
2409
|
+
self._connect_prefix: List[int] = []
|
|
2410
|
+
self._connect_prefix_len = -1
|
|
2283
2411
|
# Nodes whose x the caller set. Subpatch I/O ordering may reposition
|
|
2284
2412
|
# the others; it must never move one the caller placed.
|
|
2285
2413
|
self._explicit_x: Set[int] = set()
|
|
@@ -2565,6 +2693,12 @@ class Patcher:
|
|
|
2565
2693
|
is_graph: bool = False,
|
|
2566
2694
|
gop_rect: Tuple[float, float, float, float] = (0, 1, 1, 0),
|
|
2567
2695
|
gop_margins: Optional[Tuple[int, int]] = (0, 0),
|
|
2696
|
+
canvas_x: int = 0,
|
|
2697
|
+
canvas_y: int = 0,
|
|
2698
|
+
canvas_name: str = "(subpatch)",
|
|
2699
|
+
canvas_font_size: int = 10,
|
|
2700
|
+
open_on_load: int = 0,
|
|
2701
|
+
restore_kind: Optional[str] = None,
|
|
2568
2702
|
) -> Subpatch:
|
|
2569
2703
|
"""Add a subpatch to the patch.
|
|
2570
2704
|
|
|
@@ -2669,6 +2803,12 @@ class Patcher:
|
|
|
2669
2803
|
is_graph=is_graph,
|
|
2670
2804
|
gop_rect=gop_rect,
|
|
2671
2805
|
gop_margins=gop_margins,
|
|
2806
|
+
canvas_x=canvas_x,
|
|
2807
|
+
canvas_y=canvas_y,
|
|
2808
|
+
canvas_name=canvas_name,
|
|
2809
|
+
canvas_font_size=canvas_font_size,
|
|
2810
|
+
open_on_load=open_on_load,
|
|
2811
|
+
restore_kind=restore_kind,
|
|
2672
2812
|
)
|
|
2673
2813
|
self._register(node, pos_update)
|
|
2674
2814
|
return node
|
|
@@ -2730,7 +2870,9 @@ class Patcher:
|
|
|
2730
2870
|
self._register(node, pos_update)
|
|
2731
2871
|
return node
|
|
2732
2872
|
|
|
2733
|
-
def add_array(
|
|
2873
|
+
def add_array(
|
|
2874
|
+
self, name: str, length: int, element_type: str = "float", save_flag: int = 0
|
|
2875
|
+
) -> Array:
|
|
2734
2876
|
"""Declare an array in the subpatch.
|
|
2735
2877
|
|
|
2736
2878
|
Parameters
|
|
@@ -2741,6 +2883,12 @@ class Patcher:
|
|
|
2741
2883
|
length : int
|
|
2742
2884
|
the array length
|
|
2743
2885
|
|
|
2886
|
+
element_type : str
|
|
2887
|
+
the array's data type (default ``'float'``)
|
|
2888
|
+
|
|
2889
|
+
save_flag : int
|
|
2890
|
+
whether PureData saves the contents with the patch (default 0)
|
|
2891
|
+
|
|
2744
2892
|
Returns
|
|
2745
2893
|
-------
|
|
2746
2894
|
node : Array
|
|
@@ -2748,9 +2896,9 @@ class Patcher:
|
|
|
2748
2896
|
|
|
2749
2897
|
Notes
|
|
2750
2898
|
-----
|
|
2751
|
-
The array will not have a graph.
|
|
2899
|
+
The array will not have a graph.
|
|
2752
2900
|
"""
|
|
2753
|
-
node = Array(name, length)
|
|
2901
|
+
node = Array(name, length, element_type, save_flag)
|
|
2754
2902
|
self._register(node)
|
|
2755
2903
|
return node
|
|
2756
2904
|
|
|
@@ -3435,8 +3583,8 @@ class Patcher:
|
|
|
3435
3583
|
outlet = source.index
|
|
3436
3584
|
source = source.owner
|
|
3437
3585
|
|
|
3438
|
-
source_index = self.
|
|
3439
|
-
sink_index = self.
|
|
3586
|
+
source_index = self._connect_index_of(source, "Source")
|
|
3587
|
+
sink_index = self._connect_index_of(sink, "Sink")
|
|
3440
3588
|
|
|
3441
3589
|
if outlet < 0:
|
|
3442
3590
|
raise PdConnectionError(f"Outlet index must be non-negative, got {outlet}")
|
|
@@ -3456,6 +3604,62 @@ class Patcher:
|
|
|
3456
3604
|
|
|
3457
3605
|
self.connections.append(Connection(source_index, outlet, sink_index, inlet))
|
|
3458
3606
|
|
|
3607
|
+
def _connect_index_of(self, node: Node, role: str) -> int:
|
|
3608
|
+
"""Return *node*'s ``#X connect`` index.
|
|
3609
|
+
|
|
3610
|
+
Equal to the list position until a node that takes no index is added,
|
|
3611
|
+
after which it is the count of index-taking nodes before it. The prefix
|
|
3612
|
+
sums are cached against the node count, so the link pass over a parsed
|
|
3613
|
+
patch rebuilds them once rather than per connection.
|
|
3614
|
+
"""
|
|
3615
|
+
position = self._index_of(node, role)
|
|
3616
|
+
if not self._has_unindexed:
|
|
3617
|
+
return position
|
|
3618
|
+
if self._connect_prefix_len != len(self.nodes):
|
|
3619
|
+
prefix: List[int] = []
|
|
3620
|
+
count = 0
|
|
3621
|
+
for existing in self.nodes:
|
|
3622
|
+
prefix.append(count)
|
|
3623
|
+
if existing.occupies_connect_index:
|
|
3624
|
+
count += 1
|
|
3625
|
+
self._connect_prefix = prefix
|
|
3626
|
+
self._connect_prefix_len = len(self.nodes)
|
|
3627
|
+
return self._connect_prefix[position]
|
|
3628
|
+
|
|
3629
|
+
def add_raw(self, text: str, is_object: bool = False, after_connections: bool = False) -> Raw:
|
|
3630
|
+
"""Append a statement the builder does not model, written back verbatim.
|
|
3631
|
+
|
|
3632
|
+
Parameters
|
|
3633
|
+
----------
|
|
3634
|
+
text : str
|
|
3635
|
+
The statement without its trailing semicolon.
|
|
3636
|
+
is_object : bool
|
|
3637
|
+
Whether PureData counts it as an object, and so whether it takes a
|
|
3638
|
+
``#X connect`` index.
|
|
3639
|
+
"""
|
|
3640
|
+
node = Raw(text, is_object)
|
|
3641
|
+
node.after_connections = after_connections
|
|
3642
|
+
if not node.occupies_connect_index:
|
|
3643
|
+
self._has_unindexed = True
|
|
3644
|
+
self._register(node)
|
|
3645
|
+
return node
|
|
3646
|
+
|
|
3647
|
+
def add_declare(self, declare: PdDeclare, after_connections: bool = False) -> Declare:
|
|
3648
|
+
"""Append a ``#X declare`` statement, written back verbatim."""
|
|
3649
|
+
node = Declare(declare)
|
|
3650
|
+
node.after_connections = after_connections
|
|
3651
|
+
self._has_unindexed = True
|
|
3652
|
+
self._register(node)
|
|
3653
|
+
return node
|
|
3654
|
+
|
|
3655
|
+
def add_coords(self, coords: PdCoords, after_connections: bool = False) -> Coords:
|
|
3656
|
+
"""Append an ``#X coords`` statement no subpatch consumed."""
|
|
3657
|
+
node = Coords(coords)
|
|
3658
|
+
node.after_connections = after_connections
|
|
3659
|
+
self._has_unindexed = True
|
|
3660
|
+
self._register(node)
|
|
3661
|
+
return node
|
|
3662
|
+
|
|
3459
3663
|
def _index_of(self, node: Node, role: str) -> int:
|
|
3460
3664
|
"""Return *node*'s position in ``self.nodes``.
|
|
3461
3665
|
|
|
@@ -3491,20 +3695,27 @@ class Patcher:
|
|
|
3491
3695
|
add_link = link
|
|
3492
3696
|
|
|
3493
3697
|
def __str__(self) -> str:
|
|
3698
|
+
preamble = "".join(f"{line};\n" for line in self.preamble)
|
|
3494
3699
|
canvas = (
|
|
3495
3700
|
f"#N canvas {self.canvas_x} {self.canvas_y} "
|
|
3496
3701
|
f"{self.canvas_width} {self.canvas_height} {self.font_size};\n"
|
|
3497
3702
|
)
|
|
3498
|
-
return f"{canvas}{self._subpatch_str().rstrip()}"
|
|
3703
|
+
return f"{preamble}{canvas}{self._subpatch_str().rstrip()}"
|
|
3499
3704
|
|
|
3500
3705
|
def __repr__(self) -> str:
|
|
3501
3706
|
return f"Patcher(nodes={len(self.nodes)}, connections={len(self.connections)})"
|
|
3502
3707
|
|
|
3503
3708
|
def _subpatch_str(self) -> str:
|
|
3504
|
-
"""Internal: generate string for patch contents.
|
|
3505
|
-
|
|
3709
|
+
"""Internal: generate string for patch contents.
|
|
3710
|
+
|
|
3711
|
+
A statement read from after the ``#X connect`` block goes back there.
|
|
3712
|
+
PureData writes canvas properties such as ``#X coords`` below the
|
|
3713
|
+
connections, and moving one above them changes the file it wrote.
|
|
3714
|
+
"""
|
|
3715
|
+
nodes_str = "".join(str(n) for n in self.nodes if not n.after_connections)
|
|
3506
3716
|
connections_str = "".join(str(c) for c in self.connections)
|
|
3507
|
-
|
|
3717
|
+
trailing_str = "".join(str(n) for n in self.nodes if n.after_connections)
|
|
3718
|
+
return f"{nodes_str}{connections_str}{trailing_str}"
|
|
3508
3719
|
|
|
3509
3720
|
def save(
|
|
3510
3721
|
self,
|
|
@@ -782,10 +782,10 @@ class ParseError(Exception):
|
|
|
782
782
|
class UnsupportedElementWarning(UserWarning):
|
|
783
783
|
"""Warning issued when converting to the builder API would lose an element.
|
|
784
784
|
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
785
|
+
Statements the builder does not model -- data structure templates, scalars,
|
|
786
|
+
array data, box widths -- are carried verbatim by ``Raw``, ``Declare`` and
|
|
787
|
+
``Coords`` nodes, so they no longer raise this. It is left for a connection
|
|
788
|
+
whose endpoint has no builder node at all.
|
|
789
789
|
"""
|
|
790
790
|
|
|
791
791
|
pass
|
|
@@ -1512,13 +1512,16 @@ def from_builder(patch: "api.Patcher") -> PdPatch:
|
|
|
1512
1512
|
patch.font_size,
|
|
1513
1513
|
)
|
|
1514
1514
|
elements: List[PdElement] = []
|
|
1515
|
+
# Carried statements PureData wrote below the #X connect block.
|
|
1516
|
+
trailing: List[PdElement] = []
|
|
1515
1517
|
|
|
1516
1518
|
for node in patch.nodes:
|
|
1517
1519
|
if isinstance(node, api.Obj):
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1520
|
+
# The parser's tokenizer, not str.split(): a plain whitespace split
|
|
1521
|
+
# eats an escaped trailing space, rewriting the object's last atom.
|
|
1522
|
+
tokens = _tokenize(node.parameters["text"])
|
|
1523
|
+
class_name = tokens[0] if tokens else ""
|
|
1524
|
+
args = tuple(tokens[1:])
|
|
1522
1525
|
pos = Position(node.parameters["x_pos"], node.parameters["y_pos"])
|
|
1523
1526
|
elements.append(PdObj(pos, class_name, args))
|
|
1524
1527
|
|
|
@@ -1539,13 +1542,23 @@ def from_builder(patch: "api.Patcher") -> PdPatch:
|
|
|
1539
1542
|
p["width"],
|
|
1540
1543
|
p["lower_limit"],
|
|
1541
1544
|
p["upper_limit"],
|
|
1542
|
-
|
|
1545
|
+
p["label_pos"],
|
|
1543
1546
|
p["label"],
|
|
1544
1547
|
p["receive"],
|
|
1545
1548
|
p["send"],
|
|
1549
|
+
p["font_size"],
|
|
1546
1550
|
)
|
|
1547
1551
|
)
|
|
1548
1552
|
|
|
1553
|
+
elif isinstance(node, (api.Raw, api.Declare, api.Coords)):
|
|
1554
|
+
if isinstance(node, api.Raw):
|
|
1555
|
+
carried: PdElement = PdRaw(node.parameters["text"], node.parameters["is_object"])
|
|
1556
|
+
elif isinstance(node, api.Declare):
|
|
1557
|
+
carried = node.parameters["declare"]
|
|
1558
|
+
else:
|
|
1559
|
+
carried = node.parameters["coords"]
|
|
1560
|
+
(trailing if node.after_connections else elements).append(carried)
|
|
1561
|
+
|
|
1549
1562
|
elif isinstance(node, api.Array):
|
|
1550
1563
|
p = node.parameters
|
|
1551
1564
|
elements.append(PdArray(p["name"], p["length"], p["element_type"], p["save_flag"]))
|
|
@@ -1556,15 +1569,19 @@ def from_builder(patch: "api.Patcher") -> PdPatch:
|
|
|
1556
1569
|
p = node.parameters
|
|
1557
1570
|
pos = Position(p["x_pos"], p["y_pos"])
|
|
1558
1571
|
subpatch_canvas = CanvasProperties(
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1572
|
+
p["canvas_x"],
|
|
1573
|
+
p["canvas_y"],
|
|
1574
|
+
node.canvas_width,
|
|
1575
|
+
node.canvas_height,
|
|
1576
|
+
p["canvas_font_size"],
|
|
1577
|
+
p["canvas_name"],
|
|
1578
|
+
p["open_on_load"],
|
|
1565
1579
|
)
|
|
1580
|
+
kind = p.get("restore_kind") or ("graph" if p.get("is_graph") else "pd")
|
|
1581
|
+
restore = PdRestore(pos, "" if kind == "graph" else p["name"], kind)
|
|
1566
1582
|
inner_elements = list(inner_ast.elements)
|
|
1567
|
-
|
|
1583
|
+
carries_own_coords = any(isinstance(e, PdCoords) for e in inner_elements)
|
|
1584
|
+
if p["graph_on_parent"] and not carries_own_coords:
|
|
1568
1585
|
rect = p.get("gop_rect", (0, 1, 1, 0))
|
|
1569
1586
|
margins = p.get("gop_margins", (0, 0))
|
|
1570
1587
|
inner_elements.append(
|
|
@@ -1641,6 +1658,7 @@ def from_builder(patch: "api.Patcher") -> PdPatch:
|
|
|
1641
1658
|
p["label"],
|
|
1642
1659
|
p["receive"],
|
|
1643
1660
|
p["send"],
|
|
1661
|
+
p["font_size"],
|
|
1644
1662
|
)
|
|
1645
1663
|
)
|
|
1646
1664
|
|
|
@@ -1817,8 +1835,9 @@ def from_builder(patch: "api.Patcher") -> PdPatch:
|
|
|
1817
1835
|
# Add connections
|
|
1818
1836
|
for conn in patch.connections:
|
|
1819
1837
|
elements.append(PdConnect(conn.source, conn.outlet_index, conn.sink, conn.inlet_index))
|
|
1838
|
+
elements.extend(trailing)
|
|
1820
1839
|
|
|
1821
|
-
return PdPatch(canvas, elements)
|
|
1840
|
+
return PdPatch(canvas, elements, [PdRaw(line) for line in patch.preamble])
|
|
1822
1841
|
|
|
1823
1842
|
|
|
1824
1843
|
def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
@@ -1853,9 +1872,14 @@ def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
|
1853
1872
|
font_size=ast.canvas.font_size,
|
|
1854
1873
|
)
|
|
1855
1874
|
|
|
1875
|
+
patch.preamble = [raw.text for raw in ast.preamble]
|
|
1876
|
+
|
|
1856
1877
|
# First pass: create all nodes (non-connections)
|
|
1857
1878
|
node_map: List[Optional[api.Node]] = [] # Track nodes for linking
|
|
1858
1879
|
node: api.Node
|
|
1880
|
+
# PureData writes canvas properties below the #X connect block. A carried
|
|
1881
|
+
# statement remembers which side of it that block it came from.
|
|
1882
|
+
seen_connect = False
|
|
1859
1883
|
for elem in ast.elements:
|
|
1860
1884
|
if isinstance(elem, PdObj):
|
|
1861
1885
|
# elem.text is already in PureData's escaped form; escaping it a
|
|
@@ -1881,6 +1905,7 @@ def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
|
1881
1905
|
label=elem.label,
|
|
1882
1906
|
receive=elem.receive,
|
|
1883
1907
|
send=elem.send,
|
|
1908
|
+
font_size=elem.font_size,
|
|
1884
1909
|
)
|
|
1885
1910
|
patch.nodes.append(node)
|
|
1886
1911
|
node_map.append(node)
|
|
@@ -1896,6 +1921,7 @@ def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
|
1896
1921
|
label=elem.label,
|
|
1897
1922
|
receive=elem.receive,
|
|
1898
1923
|
send=elem.send,
|
|
1924
|
+
font_size=elem.font_size,
|
|
1899
1925
|
)
|
|
1900
1926
|
patch.nodes.append(node)
|
|
1901
1927
|
node_map.append(node)
|
|
@@ -1906,12 +1932,10 @@ def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
|
1906
1932
|
node_map.append(node)
|
|
1907
1933
|
|
|
1908
1934
|
elif isinstance(elem, PdArray):
|
|
1909
|
-
node = patch.add_array(elem.name, elem.size)
|
|
1935
|
+
node = patch.add_array(elem.name, elem.size, elem.dtype, elem.save_flag)
|
|
1910
1936
|
node_map.append(node)
|
|
1911
1937
|
|
|
1912
1938
|
elif isinstance(elem, PdSubpatch):
|
|
1913
|
-
# Recursively convert subpatch
|
|
1914
|
-
inner_patch = to_builder(PdPatch(elem.canvas, elem.elements))
|
|
1915
1939
|
name = elem.restore.name if elem.restore else "subpatch"
|
|
1916
1940
|
pos = elem.restore.position if elem.restore else Position(0, 0)
|
|
1917
1941
|
# Extract GOP settings from PdCoords if present
|
|
@@ -1934,6 +1958,10 @@ def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
|
1934
1958
|
else (sub_elem.x_margin or 0, sub_elem.y_margin or 0)
|
|
1935
1959
|
)
|
|
1936
1960
|
break
|
|
1961
|
+
# The folded coords stays in the recursion as a carried node: the
|
|
1962
|
+
# writers below regenerate one only when the inner patch has none,
|
|
1963
|
+
# which keeps a parsed coords at the line PureData wrote it on.
|
|
1964
|
+
inner_patch = to_builder(PdPatch(elem.canvas, elem.elements))
|
|
1937
1965
|
if elem.restore is not None and elem.restore.is_graph:
|
|
1938
1966
|
gop_kwargs["is_graph"] = True
|
|
1939
1967
|
node = patch.add_subpatch(
|
|
@@ -1943,6 +1971,12 @@ def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
|
1943
1971
|
y_pos=pos.y,
|
|
1944
1972
|
canvas_width=elem.canvas.width,
|
|
1945
1973
|
canvas_height=elem.canvas.height,
|
|
1974
|
+
canvas_x=elem.canvas.x,
|
|
1975
|
+
canvas_y=elem.canvas.y,
|
|
1976
|
+
canvas_name=elem.canvas.name if elem.canvas.name is not None else "(subpatch)",
|
|
1977
|
+
canvas_font_size=elem.canvas.font_size,
|
|
1978
|
+
open_on_load=elem.canvas.open_on_load,
|
|
1979
|
+
restore_kind=elem.restore.kind if elem.restore is not None else None,
|
|
1946
1980
|
**gop_kwargs,
|
|
1947
1981
|
)
|
|
1948
1982
|
node_map.append(node)
|
|
@@ -2155,26 +2189,26 @@ def to_builder(ast: PdPatch) -> "api.Patcher":
|
|
|
2155
2189
|
node_map.append(node)
|
|
2156
2190
|
|
|
2157
2191
|
elif isinstance(elem, PdRaw):
|
|
2158
|
-
# The builder
|
|
2159
|
-
#
|
|
2160
|
-
|
|
2161
|
-
kind = " ".join(elem.text.split()[:2]) or "?"
|
|
2162
|
-
warnings.warn(
|
|
2163
|
-
f"to_builder() cannot represent {kind!r} statements; "
|
|
2164
|
-
f"dropping: {elem.text[:60]!r}. Use the AST API to preserve them.",
|
|
2165
|
-
UnsupportedElementWarning,
|
|
2166
|
-
stacklevel=2,
|
|
2167
|
-
)
|
|
2192
|
+
# The builder models no such statement, but it must still write back
|
|
2193
|
+
# what it read, so the text is carried verbatim.
|
|
2194
|
+
node = patch.add_raw(elem.text, elem.is_object, after_connections=seen_connect)
|
|
2168
2195
|
# Only occupy a connect index if Pd counts the statement as an
|
|
2169
2196
|
# object, otherwise every following index shifts.
|
|
2170
2197
|
if elem.is_object:
|
|
2171
|
-
node_map.append(
|
|
2198
|
+
node_map.append(node)
|
|
2199
|
+
|
|
2200
|
+
elif isinstance(elem, PdDeclare):
|
|
2201
|
+
# Not an object, so it consumes no connect index.
|
|
2202
|
+
patch.add_declare(elem, after_connections=seen_connect)
|
|
2203
|
+
|
|
2204
|
+
elif isinstance(elem, PdCoords):
|
|
2205
|
+
# A subpatch strips the coords it folds into its own parameters
|
|
2206
|
+
# before recursing, so anything reaching here belongs to this canvas.
|
|
2207
|
+
patch.add_coords(elem, after_connections=seen_connect)
|
|
2172
2208
|
|
|
2173
|
-
elif isinstance(elem,
|
|
2174
|
-
#
|
|
2175
|
-
|
|
2176
|
-
# the enclosing subpatch; declare has no builder equivalent.
|
|
2177
|
-
pass
|
|
2209
|
+
elif isinstance(elem, PdConnect):
|
|
2210
|
+
# Not an object, and handled in the second pass.
|
|
2211
|
+
seen_connect = True
|
|
2178
2212
|
|
|
2179
2213
|
else:
|
|
2180
2214
|
node_map.append(None) # Placeholder for unknown elements
|
|
@@ -2231,7 +2265,7 @@ def transform(patch: PdPatch, transformer: ElementTransformer) -> PdPatch:
|
|
|
2231
2265
|
if transformed is not None:
|
|
2232
2266
|
new_elements.append(transformed)
|
|
2233
2267
|
|
|
2234
|
-
return PdPatch(patch.canvas, new_elements)
|
|
2268
|
+
return PdPatch(patch.canvas, new_elements, patch.preamble)
|
|
2235
2269
|
|
|
2236
2270
|
|
|
2237
2271
|
def find_objects(patch: PdPatch, predicate: ElementPredicate) -> List[PdElement]:
|
|
@@ -12,7 +12,7 @@ import sys
|
|
|
12
12
|
from typing import Dict, List, Optional, Tuple
|
|
13
13
|
|
|
14
14
|
from .api import _infer_abstraction_io
|
|
15
|
-
from .ast import PdDeclare, PdElement, PdPatch, PdSubpatch
|
|
15
|
+
from .ast import ParseError, PdDeclare, PdElement, PdPatch, PdSubpatch
|
|
16
16
|
|
|
17
17
|
# Maps sys.platform prefix to recognized binary extensions for externals
|
|
18
18
|
_EXTERNAL_EXTENSIONS = {
|
|
@@ -98,6 +98,7 @@ def discover_externals(
|
|
|
98
98
|
Mapping of external name to (num_inlets, num_outlets).
|
|
99
99
|
Binary externals have (None, None) since I/O cannot be inferred.
|
|
100
100
|
First-found wins when the same name appears in multiple paths.
|
|
101
|
+
A ``.pd`` file that cannot be read or parsed is skipped.
|
|
101
102
|
"""
|
|
102
103
|
paths: List[str] = []
|
|
103
104
|
if search_paths:
|
|
@@ -130,7 +131,9 @@ def discover_externals(
|
|
|
130
131
|
try:
|
|
131
132
|
inlets, outlets = _infer_abstraction_io(full_path)
|
|
132
133
|
registry[name] = (inlets, outlets)
|
|
133
|
-
except OSError:
|
|
134
|
+
except (OSError, ParseError):
|
|
135
|
+
# An unreadable or unparseable candidate is an unusable
|
|
136
|
+
# candidate, not a reason to abandon the whole scan.
|
|
134
137
|
continue
|
|
135
138
|
continue
|
|
136
139
|
|
|
@@ -220,13 +220,13 @@ def validate_patch(
|
|
|
220
220
|
TypeError
|
|
221
221
|
If *patch* is not a ``Patcher`` or ``PdPatch``.
|
|
222
222
|
"""
|
|
223
|
-
_ensure_libpd()
|
|
224
|
-
|
|
225
|
-
import cypd
|
|
226
|
-
|
|
227
223
|
content = _serialize_input(patch)
|
|
228
224
|
|
|
229
225
|
with _libpd_lock:
|
|
226
|
+
_ensure_libpd()
|
|
227
|
+
|
|
228
|
+
import cypd
|
|
229
|
+
|
|
230
230
|
return _validate_locked(
|
|
231
231
|
cypd,
|
|
232
232
|
content,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|