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
py2max/core/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Core patcher functionality for py2max.
|
|
2
|
+
|
|
3
|
+
This subpackage contains the core classes for creating and managing Max/MSP patches:
|
|
4
|
+
|
|
5
|
+
- Patcher: Core class for creating and managing Max patches
|
|
6
|
+
- Box: Represents individual Max objects
|
|
7
|
+
- Patchline: Represents connections between objects
|
|
8
|
+
- Rect: Rectangle data structure for positioning
|
|
9
|
+
|
|
10
|
+
Abstract base classes are also provided for type checking and interface definitions.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from py2max.exceptions import InvalidConnectionError
|
|
14
|
+
|
|
15
|
+
from .abstract import (
|
|
16
|
+
AbstractBox,
|
|
17
|
+
AbstractLayoutManager,
|
|
18
|
+
AbstractPatcher,
|
|
19
|
+
AbstractPatchline,
|
|
20
|
+
)
|
|
21
|
+
from .box import Box
|
|
22
|
+
from .common import Rect
|
|
23
|
+
from .patcher import Patcher
|
|
24
|
+
from .patchline import Patchline
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
# Core classes
|
|
28
|
+
"Patcher",
|
|
29
|
+
"Box",
|
|
30
|
+
"Patchline",
|
|
31
|
+
"Rect",
|
|
32
|
+
# Abstract base classes
|
|
33
|
+
"AbstractPatcher",
|
|
34
|
+
"AbstractBox",
|
|
35
|
+
"AbstractPatchline",
|
|
36
|
+
"AbstractLayoutManager",
|
|
37
|
+
# Backward compatibility - exceptions re-exported
|
|
38
|
+
"InvalidConnectionError",
|
|
39
|
+
]
|
py2max/core/abstract.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Abstract base classes for py2max core objects.
|
|
2
|
+
|
|
3
|
+
This module defines abstract base classes to break circular dependencies
|
|
4
|
+
between core.py and layout.py modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Callable, Optional, Union
|
|
10
|
+
|
|
11
|
+
from .common import Rect
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AbstractLayoutManager(ABC):
|
|
15
|
+
"""Abstract base class for LayoutManager objects.
|
|
16
|
+
|
|
17
|
+
This class defines the interface that layout managers expect from
|
|
18
|
+
a LayoutManager object, allowing layout.py to reference LayoutManager without
|
|
19
|
+
creating circular imports.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Required attributes
|
|
23
|
+
box_height: float
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def get_rect_from_maxclass(self, maxclass: str) -> Optional[Rect]:
|
|
27
|
+
"""retrieves default patching_rect from defaults dictionary."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def get_relative_pos(self, rect: Rect) -> Rect:
|
|
32
|
+
"""returns a relative position for the object"""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def get_absolute_pos(self, rect: Rect) -> Rect:
|
|
37
|
+
"""returns an absolute position for the object"""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def get_pos(self, maxclass: Optional[str] = None) -> Rect:
|
|
42
|
+
"""get box rect (position) via maxclass or layout_manager"""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
@abstractmethod
|
|
46
|
+
def above(self, rect: Rect) -> Rect:
|
|
47
|
+
"""Return a position of a comment above the object"""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class AbstractBox(ABC):
|
|
52
|
+
"""Abstract base class for Box objects.
|
|
53
|
+
|
|
54
|
+
This class defines the interface that layout managers expect from
|
|
55
|
+
a Box object, allowing layout.py to reference Box without
|
|
56
|
+
creating circular imports.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
# These are instance attributes, not properties
|
|
60
|
+
id: Optional[str]
|
|
61
|
+
maxclass: str
|
|
62
|
+
patching_rect: Rect
|
|
63
|
+
_kwds: dict
|
|
64
|
+
|
|
65
|
+
@abstractmethod
|
|
66
|
+
def render(self) -> None:
|
|
67
|
+
"""Render the box object."""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def to_dict(self) -> dict:
|
|
72
|
+
"""Convert the box to a dictionary representation."""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def __iter__(self):
|
|
77
|
+
"""Make the box iterable."""
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class AbstractPatchline(ABC):
|
|
82
|
+
"""Abstract base class for Patchline objects.
|
|
83
|
+
|
|
84
|
+
This class defines the interface that layout managers expect from
|
|
85
|
+
a Patchline object, allowing layout.py to reference Patchline without
|
|
86
|
+
creating circular imports.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def src(self) -> str:
|
|
92
|
+
"""Source object identifier."""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def dst(self) -> str:
|
|
98
|
+
"""Destination object identifier."""
|
|
99
|
+
...
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def to_dict(self) -> dict:
|
|
103
|
+
"""Convert the patchline to a dictionary representation."""
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class AbstractPatcher(ABC):
|
|
108
|
+
"""Abstract base class for Patcher objects.
|
|
109
|
+
|
|
110
|
+
This class defines the interface that layout managers expect from
|
|
111
|
+
a Patcher object, allowing layout.py to reference Patcher without
|
|
112
|
+
creating circular imports.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def width(self) -> float:
|
|
118
|
+
"""Width of patcher window."""
|
|
119
|
+
...
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
@abstractmethod
|
|
123
|
+
def height(self) -> float:
|
|
124
|
+
"""Height of patcher window."""
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
# rect is an instance attribute, not a property
|
|
128
|
+
rect: Rect
|
|
129
|
+
|
|
130
|
+
_path: Optional[Union[str, Path]]
|
|
131
|
+
_parent: Optional["AbstractPatcher"]
|
|
132
|
+
_node_ids: list[str]
|
|
133
|
+
_objects: dict[str, AbstractBox]
|
|
134
|
+
_boxes: list[AbstractBox]
|
|
135
|
+
_lines: list[AbstractPatchline]
|
|
136
|
+
_edge_ids: list[tuple[str, str]]
|
|
137
|
+
_id_counter: int = 0
|
|
138
|
+
_link_counter: int = 0
|
|
139
|
+
_last_link: Optional[tuple[str, str]]
|
|
140
|
+
_reset_on_render: bool
|
|
141
|
+
_flow_direction: str
|
|
142
|
+
_cluster_connected: bool
|
|
143
|
+
_layout_mgr: AbstractLayoutManager
|
|
144
|
+
_auto_hints: bool
|
|
145
|
+
_validate_connections: bool
|
|
146
|
+
_maxclass_methods: dict[str, Callable[..., Any]]
|
py2max/core/box.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Box class for representing Max objects in a patch."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
4
|
+
|
|
5
|
+
from .abstract import AbstractBox
|
|
6
|
+
from .common import Rect
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .patcher import Patcher
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Box(AbstractBox):
|
|
13
|
+
"""Represents a Max object in a patch.
|
|
14
|
+
|
|
15
|
+
The Box class encapsulates a single Max object with its properties,
|
|
16
|
+
position, and connections. It provides methods for introspection
|
|
17
|
+
and help information.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
maxclass: Max object class name (e.g., 'newobj', 'flonum').
|
|
21
|
+
numinlets: Number of input connections.
|
|
22
|
+
numoutlets: Number of output connections.
|
|
23
|
+
id: Unique identifier for the object.
|
|
24
|
+
patching_rect: Position and size rectangle.
|
|
25
|
+
**kwds: Additional Max object properties.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
id: Unique identifier for the object.
|
|
29
|
+
maxclass: Max object class name.
|
|
30
|
+
numinlets: Number of input connections.
|
|
31
|
+
numoutlets: Number of output connections.
|
|
32
|
+
patching_rect: Position and size as Rect.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
maxclass: Optional[str] = None,
|
|
38
|
+
numinlets: Optional[int] = None,
|
|
39
|
+
numoutlets: Optional[int] = None,
|
|
40
|
+
id: Optional[str] = None,
|
|
41
|
+
patching_rect: Optional[Rect] = None,
|
|
42
|
+
**kwds,
|
|
43
|
+
):
|
|
44
|
+
self.id = id
|
|
45
|
+
self.maxclass = maxclass or "newobj"
|
|
46
|
+
self.numinlets = numinlets or 0
|
|
47
|
+
self.numoutlets = numoutlets or 1
|
|
48
|
+
# self.outlettype = outlettype
|
|
49
|
+
self.patching_rect = patching_rect or Rect(0, 0, 62, 22)
|
|
50
|
+
|
|
51
|
+
self._kwds = self._remove_none_entries(kwds)
|
|
52
|
+
self._patcher: Optional["Patcher"] = self._kwds.pop("patcher", None)
|
|
53
|
+
|
|
54
|
+
def _remove_none_entries(self, kwds):
|
|
55
|
+
"""removes items in the dict which have None values.
|
|
56
|
+
|
|
57
|
+
TODO: make recursive in case of nested dicts.
|
|
58
|
+
"""
|
|
59
|
+
return {k: v for k, v in kwds.items() if v is not None}
|
|
60
|
+
|
|
61
|
+
def __iter__(self):
|
|
62
|
+
yield self
|
|
63
|
+
if self._patcher:
|
|
64
|
+
yield from iter(self._patcher)
|
|
65
|
+
|
|
66
|
+
def __repr__(self):
|
|
67
|
+
return f"{self.__class__.__name__}(id='{self.id}', maxclass='{self.maxclass}')"
|
|
68
|
+
|
|
69
|
+
def __pt_repr__(self):
|
|
70
|
+
"""Custom representation for ptpython REPL.
|
|
71
|
+
|
|
72
|
+
Provides rich colored output when displaying objects in the ptpython REPL.
|
|
73
|
+
Shows object type, ID, position, and text content in a readable format.
|
|
74
|
+
"""
|
|
75
|
+
from prompt_toolkit.formatted_text import HTML
|
|
76
|
+
|
|
77
|
+
# Get object details
|
|
78
|
+
obj_type = self.maxclass or "newobj"
|
|
79
|
+
obj_id = self.id or "unknown"
|
|
80
|
+
text = getattr(self, "text", None) or ""
|
|
81
|
+
|
|
82
|
+
# Get position
|
|
83
|
+
rect = self.patching_rect
|
|
84
|
+
if rect:
|
|
85
|
+
pos = f"[{rect.x:.0f}, {rect.y:.0f}]"
|
|
86
|
+
else:
|
|
87
|
+
pos = "[?, ?]"
|
|
88
|
+
|
|
89
|
+
# Build colored representation
|
|
90
|
+
if text:
|
|
91
|
+
return HTML(
|
|
92
|
+
f"<ansigreen>{obj_type}</ansigreen> "
|
|
93
|
+
f"<ansicyan>{obj_id}</ansicyan> "
|
|
94
|
+
f"at {pos}: <ansiyellow>'{text}'</ansiyellow>"
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
return HTML(
|
|
98
|
+
f"<ansigreen>{obj_type}</ansigreen> "
|
|
99
|
+
f"<ansicyan>{obj_id}</ansicyan> "
|
|
100
|
+
f"at {pos}"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
def render(self):
|
|
104
|
+
"""convert self and children to dictionary."""
|
|
105
|
+
if self._patcher:
|
|
106
|
+
self._patcher.render()
|
|
107
|
+
self.patcher = self._patcher.to_dict()
|
|
108
|
+
|
|
109
|
+
def to_dict(self):
|
|
110
|
+
"""create dict from object with extra kwds included"""
|
|
111
|
+
d = vars(self).copy()
|
|
112
|
+
to_del = [k for k in d if k.startswith("_")]
|
|
113
|
+
for k in to_del:
|
|
114
|
+
del d[k]
|
|
115
|
+
d.update(self._kwds)
|
|
116
|
+
return dict(box=d)
|
|
117
|
+
|
|
118
|
+
@classmethod
|
|
119
|
+
def from_dict(cls, obj_dict):
|
|
120
|
+
"""create instance from dict"""
|
|
121
|
+
box = cls()
|
|
122
|
+
box.__dict__.update(obj_dict)
|
|
123
|
+
if hasattr(box, "patcher"):
|
|
124
|
+
# Lazy import to avoid circular dependency
|
|
125
|
+
from .patcher import Patcher
|
|
126
|
+
|
|
127
|
+
box._patcher = Patcher.from_dict(getattr(box, "patcher"))
|
|
128
|
+
return box
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def oid(self) -> Optional[int]:
|
|
132
|
+
"""numerical part of object id as int"""
|
|
133
|
+
if self.id:
|
|
134
|
+
return int(self.id[4:])
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def subpatcher(self):
|
|
139
|
+
"""synonym for parent patcher object"""
|
|
140
|
+
return self._patcher
|
|
141
|
+
|
|
142
|
+
@property
|
|
143
|
+
def text(self):
|
|
144
|
+
"""Get the text content of the box."""
|
|
145
|
+
# Check if text is stored as a direct attribute (from file loading)
|
|
146
|
+
if "text" in self.__dict__:
|
|
147
|
+
return self.__dict__["text"]
|
|
148
|
+
# Otherwise get from _kwds (from programmatic creation)
|
|
149
|
+
return self._kwds.get("text", "")
|
|
150
|
+
|
|
151
|
+
def help_text(self) -> str:
|
|
152
|
+
"""Get formatted help documentation for this Max object.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Formatted help string with object documentation from .maxref.xml files.
|
|
156
|
+
"""
|
|
157
|
+
from py2max import maxref
|
|
158
|
+
|
|
159
|
+
return maxref.get_object_help(self.maxclass)
|
|
160
|
+
|
|
161
|
+
def help(self) -> None:
|
|
162
|
+
"""Print formatted help documentation for this Max object."""
|
|
163
|
+
print(self.help_text())
|
|
164
|
+
|
|
165
|
+
def get_info(self) -> Optional[dict]:
|
|
166
|
+
"""Get complete object information from .maxref.xml files.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Dictionary with complete object information or None if not found.
|
|
170
|
+
"""
|
|
171
|
+
from py2max import maxref
|
|
172
|
+
|
|
173
|
+
return maxref.get_object_info(self.maxclass)
|
|
174
|
+
|
|
175
|
+
def get_inlet_count(self) -> Optional[int]:
|
|
176
|
+
"""Get the number of inlets for this object from maxref data.
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Number of inlets or None if unknown.
|
|
180
|
+
"""
|
|
181
|
+
from py2max.maxref import get_inlet_count
|
|
182
|
+
|
|
183
|
+
object_name = self._get_object_name()
|
|
184
|
+
return get_inlet_count(object_name)
|
|
185
|
+
|
|
186
|
+
def get_outlet_count(self) -> Optional[int]:
|
|
187
|
+
"""Get the number of outlets for this object from maxref data.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Number of outlets or None if unknown.
|
|
191
|
+
"""
|
|
192
|
+
from py2max.maxref import get_outlet_count
|
|
193
|
+
|
|
194
|
+
object_name = self._get_object_name()
|
|
195
|
+
return get_outlet_count(object_name)
|
|
196
|
+
|
|
197
|
+
def get_inlet_types(self) -> List[str]:
|
|
198
|
+
"""Get the inlet types for this object from maxref data
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
List of inlet type strings
|
|
202
|
+
"""
|
|
203
|
+
from py2max.maxref import get_inlet_types
|
|
204
|
+
|
|
205
|
+
object_name = self._get_object_name()
|
|
206
|
+
return get_inlet_types(object_name)
|
|
207
|
+
|
|
208
|
+
def get_outlet_types(self) -> List[str]:
|
|
209
|
+
"""Get the outlet types for this object from maxref data
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
List of outlet type strings
|
|
213
|
+
"""
|
|
214
|
+
from py2max.maxref import get_outlet_types
|
|
215
|
+
|
|
216
|
+
object_name = self._get_object_name()
|
|
217
|
+
return get_outlet_types(object_name)
|
|
218
|
+
|
|
219
|
+
def _get_object_name(self) -> str:
|
|
220
|
+
"""Get the actual object name for this Box.
|
|
221
|
+
|
|
222
|
+
For 'newobj' maxclass objects, extract the first word from the text field.
|
|
223
|
+
For other objects, use the maxclass directly.
|
|
224
|
+
"""
|
|
225
|
+
if self.maxclass == "newobj":
|
|
226
|
+
# Text is stored in _kwds for Box objects
|
|
227
|
+
text = self._kwds.get("text", "")
|
|
228
|
+
if text:
|
|
229
|
+
# Extract the first word from text (the object name)
|
|
230
|
+
return text.split()[0] if text.split() else self.maxclass
|
|
231
|
+
return self.maxclass
|
py2max/core/common.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Common data structures and utilities for py2max.
|
|
2
|
+
|
|
3
|
+
This module contains shared data structures used throughout the py2max library.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import NamedTuple
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Rect(NamedTuple):
|
|
10
|
+
"""Rectangle data structure for object positioning.
|
|
11
|
+
|
|
12
|
+
Represents a rectangular area in Max patch coordinates using four coordinates:
|
|
13
|
+
x (horizontal position), y (vertical position), w (width), and h (height).
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
x: float
|
|
17
|
+
y: float
|
|
18
|
+
w: float
|
|
19
|
+
h: float
|