Skip to content

Commit 6c615e2

Browse files
committed
Support string-encoded types as per PEP-0563 #38
1 parent 94c2777 commit 6c615e2

7 files changed

Lines changed: 154 additions & 59 deletions

File tree

pylasu/model/model.py

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from dataclasses import Field, MISSING, dataclass, field
44
from typing import Optional, Callable, List, Union
55

6+
from .naming import ReferenceByName
67
from .position import Position, Source
78
from .reflection import Multiplicity, PropertyDescription
89
from ..reflection import getannotations, get_type_arguments, is_sequence_type
@@ -99,9 +100,13 @@ class Concept(ABCMeta):
99100

100101
def __init__(cls, what, bases=None, dict=None):
101102
super().__init__(what, bases, dict)
102-
cls.__internal_properties__ = \
103-
(["origin", "destination", "parent", "position", "position_override"]
104-
+ [n for n, v in inspect.getmembers(cls, is_internal_property_or_method)])
103+
cls.__internal_properties__ = []
104+
for base in bases:
105+
if hasattr(base, "__internal_properties__"):
106+
cls.__internal_properties__.extend(base.__internal_properties__)
107+
if not cls.__internal_properties__:
108+
cls.__internal_properties__ = ["origin", "destination", "parent", "position", "position_override"]
109+
cls.__internal_properties__.extend([n for n, v in inspect.getmembers(cls, is_internal_property_or_method)])
105110

106111
@property
107112
def node_properties(cls):
@@ -110,28 +115,54 @@ def node_properties(cls):
110115
yield from cls._direct_node_properties(cl, names)
111116

112117
def _direct_node_properties(cls, cl, known_property_names):
118+
def get_type_arg(decl_type):
119+
type_args = get_type_arguments(decl_type)
120+
if len(type_args) == 1:
121+
return type_args[0]
122+
else:
123+
return decl_type
124+
113125
anns = getannotations(cl)
114126
if not anns:
115127
return
116128
for name in anns:
117129
if name not in known_property_names and cls.is_node_property(name):
118-
is_child_property = False
130+
is_containment = False
119131
multiplicity = Multiplicity.SINGULAR
132+
decl_type = None
133+
is_reference = False
120134
if name in anns:
121135
decl_type = anns[name]
136+
if get_type_origin(decl_type) is ReferenceByName:
137+
decl_type = get_type_arg(decl_type)
138+
is_reference = True
122139
if is_sequence_type(decl_type):
140+
decl_type = get_type_arg(decl_type)
123141
multiplicity = Multiplicity.MANY
142+
if get_type_origin(decl_type) is Union:
124143
type_args = get_type_arguments(decl_type)
125144
if len(type_args) == 1:
126-
is_child_property = provides_nodes(type_args[0])
127-
else:
128-
is_child_property = provides_nodes(decl_type)
145+
decl_type = type_args[0]
146+
elif len(type_args) == 2:
147+
if type_args[0] is type(None):
148+
decl_type = type_args[1]
149+
elif type_args[1] is type(None):
150+
decl_type = type_args[0]
151+
else:
152+
raise Exception(f"Unsupported feature {name} of type {decl_type}")
153+
if multiplicity == Multiplicity.SINGULAR:
154+
multiplicity = Multiplicity.OPTIONAL
155+
else:
156+
raise Exception(f"Unsupported feature {name} of type {decl_type}")
157+
if not isinstance(decl_type, type):
158+
raise Exception(f"Unsupported feature {name} of type {decl_type}")
159+
is_containment = provides_nodes(decl_type) and not is_reference
129160
known_property_names.add(name)
130-
yield PropertyDescription(name, is_child_property, multiplicity)
161+
yield PropertyDescription(name, decl_type, is_containment, is_reference, multiplicity)
131162
for name in dir(cl):
132163
if name not in known_property_names and cls.is_node_property(name):
133164
known_property_names.add(name)
134-
yield PropertyDescription(name, False)
165+
yield PropertyDescription(name, None, False, False)
135166

136167
def is_node_property(cls, name):
137168
return not name.startswith('_') and name not in cls.__internal_properties__
@@ -180,7 +211,7 @@ def source(self) -> Optional[Source]:
180211

181212
@internal_property
182213
def properties(self):
183-
return (PropertyDescription(p.name, p.provides_nodes, p.multiplicity, getattr(self, p.name))
214+
return (PropertyDescription(p.name, p.type, p.is_containment, p.is_reference, p.multiplicity, getattr(self, p.name))
184215
for p in self.__class__.node_properties)
185216

186217
@internal_property

pylasu/model/reflection.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import enum
22
from dataclasses import dataclass
3+
from typing import Optional
34

45

56
class Multiplicity(enum.Enum):
@@ -11,7 +12,9 @@ class Multiplicity(enum.Enum):
1112
@dataclass
1213
class PropertyDescription:
1314
name: str
14-
provides_nodes: bool
15+
type: Optional[type]
16+
is_containment: bool
17+
is_reference: bool
1518
multiplicity: Multiplicity = Multiplicity.SINGULAR
1619
value: object = None
1720

pylasu/reflection/reflection.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,30 @@
44

55

66
def getannotations(cls):
7-
import inspect
8-
try: # On Python 3.10+
9-
return inspect.getannotations(cls)
7+
try:
8+
# https://peps.python.org/pep-0563/
9+
return typing.get_type_hints(cls, globalns=None, localns=None)
1010
except AttributeError:
11-
if isinstance(cls, type):
12-
return cls.__dict__.get('__annotations__', None)
13-
else:
14-
return getattr(cls, '__annotations__', None)
11+
try:
12+
# On Python 3.10+
13+
import inspect
14+
return inspect.getannotations(cls)
15+
except AttributeError:
16+
if isinstance(cls, type):
17+
return cls.__dict__.get('__annotations__', None)
18+
else:
19+
return getattr(cls, '__annotations__', None)
1520

1621

1722
def get_type_origin(tp):
23+
origin = None
1824
if hasattr(typing, "get_origin"):
19-
return typing.get_origin(tp)
25+
origin = typing.get_origin(tp)
2026
elif hasattr(tp, "__origin__"):
21-
return tp.__origin__
27+
origin = tp.__origin__
2228
elif tp is typing.Generic:
23-
return typing.Generic
24-
else:
25-
return None
29+
origin = typing.Generic
30+
return origin or (tp if isinstance(tp, type) else None)
2631

2732

2833
def is_enum_type(attr_type):

pylasu/testing/testing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def assert_asts_are_equal(
2020
case.fail(f"No property {expected_property.name} found at {context}")
2121
actual_prop_value = actual_property.value
2222
expected_prop_value = expected_property.value
23-
if expected_property.provides_nodes:
23+
if expected_property.is_containment:
2424
if expected_property.multiple:
2525
assert_multi_properties_are_equal(
2626
case, expected_property, expected_prop_value, actual_prop_value, context, consider_position)

tests/model/test_model.py

Lines changed: 90 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import unittest
33
from typing import List, Optional, Union
44

5-
from pylasu.model import Node, Position, Point
6-
from pylasu.model.reflection import Multiplicity
5+
from pylasu.model import Node, Position, Point, internal_field
6+
from pylasu.model.reflection import Multiplicity, PropertyDescription
77
from pylasu.model.naming import ReferenceByName, Named, Scope, Symbol
88
from pylasu.support import extension_method
99

@@ -13,13 +13,24 @@ class SomeNode(Node, Named):
1313
foo = 3
1414
bar: int = dataclasses.field(init=False)
1515
__private__ = 4
16-
ref: Node = None
16+
containment: Node = None
17+
reference: ReferenceByName[Node] = None
1718
multiple: List[Node] = dataclasses.field(default_factory=list)
19+
optional: Optional[Node] = None
1820
multiple_opt: List[Optional[Node]] = dataclasses.field(default_factory=list)
21+
internal: Node = internal_field(default=None)
1922

2023
def __post_init__(self):
2124
self.bar = 5
2225

26+
@dataclasses.dataclass
27+
class ExtendedNode(SomeNode):
28+
prop = 2
29+
cont_fwd: "ExtendedNode" = None
30+
cont_ref: ReferenceByName["ExtendedNode"] = None
31+
multiple2: List[SomeNode] = dataclasses.field(default_factory=list)
32+
internal2: Node = internal_field(default=None)
33+
2334

2435
@dataclasses.dataclass
2536
class SomeSymbol(Symbol):
@@ -39,6 +50,14 @@ class InvalidNode(Node):
3950
another_child: Node = None
4051

4152

53+
def require_feature(node, name) -> PropertyDescription:
54+
return next(n for n in node.properties if n.name == name)
55+
56+
57+
def find_feature(node, name) -> Optional[PropertyDescription]:
58+
return next((n for n in node.properties if n.name == name), None)
59+
60+
4261
class ModelTest(unittest.TestCase):
4362

4463
def test_reference_by_name_unsolved_str(self):
@@ -77,9 +96,29 @@ def test_node_with_position(self):
7796

7897
def test_node_properties(self):
7998
node = SomeNode("n").with_position(Position(Point(1, 0), Point(2, 1)))
80-
self.assertIsNotNone(next(n for n in node.properties if n.name == 'foo'))
81-
self.assertIsNotNone(next(n for n in node.properties if n.name == 'bar'))
82-
self.assertIsNotNone(next(n for n in node.properties if n.name == "name"))
99+
self.assertIsNotNone(find_feature(node, 'foo'))
100+
self.assertFalse(find_feature(node, 'foo').is_containment)
101+
self.assertIsNotNone(find_feature(node, 'bar'))
102+
self.assertFalse(find_feature(node, 'bar').is_containment)
103+
self.assertIsNotNone(find_feature(node, 'name'))
104+
self.assertTrue(find_feature(node, 'containment').is_containment)
105+
self.assertFalse(find_feature(node, 'containment').is_reference)
106+
self.assertFalse(find_feature(node, 'reference').is_containment)
107+
self.assertTrue(find_feature(node, 'reference').is_reference)
108+
with self.assertRaises(StopIteration):
109+
next(n for n in node.properties if n.name == '__private__')
110+
with self.assertRaises(StopIteration):
111+
next(n for n in node.properties if n.name == 'non_existent')
112+
with self.assertRaises(StopIteration):
113+
next(n for n in node.properties if n.name == 'properties')
114+
with self.assertRaises(StopIteration):
115+
next(n for n in node.properties if n.name == "origin")
116+
117+
def test_node_properties_inheritance(self):
118+
node = ExtendedNode("n").with_position(Position(Point(1, 0), Point(2, 1)))
119+
self.assertIsNotNone(find_feature(node, 'foo'))
120+
self.assertIsNotNone(find_feature(node, 'bar'))
121+
self.assertIsNotNone(find_feature(node, 'name'))
83122
with self.assertRaises(StopIteration):
84123
next(n for n in node.properties if n.name == '__private__')
85124
with self.assertRaises(StopIteration):
@@ -159,20 +198,52 @@ def frob_node(_: Node):
159198
pass
160199

161200
pds = [pd for pd in sorted(SomeNode.node_properties, key=lambda x: x.name)]
162-
self.assertEqual(6, len(pds), f"{pds} should be 6")
201+
self.assertEqual(8, len(pds), f"{pds} should be 7")
163202
self.assertEqual("bar", pds[0].name)
164-
self.assertFalse(pds[0].provides_nodes)
165-
self.assertEqual("foo", pds[1].name)
166-
self.assertFalse(pds[1].provides_nodes)
167-
self.assertEqual("multiple", pds[2].name)
168-
self.assertTrue(pds[2].provides_nodes)
169-
self.assertEqual(Multiplicity.MANY, pds[2].multiplicity)
170-
self.assertEqual("multiple_opt", pds[3].name)
171-
self.assertTrue(pds[3].provides_nodes)
203+
self.assertFalse(pds[0].is_containment)
204+
self.assertEqual("containment", pds[1].name)
205+
self.assertTrue(pds[1].is_containment)
206+
self.assertEqual("foo", pds[2].name)
207+
self.assertFalse(pds[2].is_containment)
208+
self.assertEqual("multiple", pds[3].name)
209+
self.assertTrue(pds[3].is_containment)
172210
self.assertEqual(Multiplicity.MANY, pds[3].multiplicity)
173-
self.assertEqual("name", pds[4].name)
174-
self.assertFalse(pds[4].provides_nodes)
175-
self.assertEqual("ref", pds[5].name)
176-
self.assertTrue(pds[5].provides_nodes)
211+
self.assertEqual("multiple_opt", pds[4].name)
212+
self.assertTrue(pds[4].is_containment)
213+
self.assertEqual(Multiplicity.MANY, pds[4].multiplicity)
214+
self.assertEqual("name", pds[5].name)
215+
self.assertFalse(pds[5].is_containment)
216+
self.assertEqual("optional", pds[6].name)
217+
self.assertTrue(pds[6].is_containment)
218+
self.assertEqual(Multiplicity.OPTIONAL, pds[6].multiplicity)
219+
self.assertEqual("reference", pds[7].name)
220+
self.assertTrue(pds[7].is_reference)
221+
222+
self.assertRaises(Exception, lambda: [x for x in InvalidNode.node_properties])
223+
224+
def test_node_properties_meta_inheritance(self):
225+
@extension_method(Node)
226+
def frob_node_2(_: Node):
227+
pass
228+
229+
pds = [pd for pd in sorted(ExtendedNode.node_properties, key=lambda x: x.name)]
230+
self.assertEqual(12, len(pds), f"{pds} should be 7")
231+
self.assertEqual("bar", pds[0].name)
232+
self.assertFalse(pds[0].is_containment)
233+
self.assertEqual("cont_fwd", pds[1].name)
234+
self.assertTrue(pds[1].is_containment)
235+
self.assertEqual(ExtendedNode, pds[1].type)
236+
self.assertEqual("cont_ref", pds[2].name)
237+
self.assertTrue(pds[2].is_reference)
238+
self.assertEqual(ExtendedNode, pds[2].type)
239+
self.assertEqual("containment", pds[3].name)
240+
self.assertTrue(pds[3].is_containment)
241+
self.assertEqual("foo", pds[4].name)
242+
self.assertEqual("multiple", pds[5].name)
243+
self.assertTrue(pds[5].is_containment)
244+
self.assertEqual(Multiplicity.MANY, pds[5].multiplicity)
245+
self.assertEqual("multiple2", pds[6].name)
246+
self.assertTrue(pds[6].is_containment)
247+
self.assertEqual(Multiplicity.MANY, pds[6].multiplicity)
177248

178249
self.assertRaises(Exception, lambda: [x for x in InvalidNode.node_properties])

tests/test_metamodel_builder.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def test_build_metamodel_single_package_inheritance(self):
118118
next((a for a in box.eClass.eAllAttributes() if a.name == "name"), None))
119119
self.assertIsNotNone(
120120
next((a for a in box.eClass.eAllAttributes() if a.name == "strength"), None))
121-
self.assertEqual(2, len(box.eClass.eAllAttributes()))
121+
self.assertEqual(3, len(box.eClass.eAllAttributes()))
122122

123123

124124
STARLASU_MODEL_JSON = '''{

tests/test_processing.py

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@ class BW(Node):
1717
many_as: List[AW]
1818

1919

20-
@dataclass
21-
class CW(Node):
22-
a: AW
23-
many_as: Set[AW]
24-
25-
2620
class ProcessingTest(unittest.TestCase):
2721
def test_search_by_type(self):
2822
self.assertEqual(["1", "2", "3", "4", "5", "6"], [i.name for i in box.search_by_type(Item)])
@@ -42,15 +36,6 @@ def test_replace_in_list(self):
4236
self.assertEqual("4", b.many_as[0].s)
4337
self.assertEqual(BW(a1, [a4, a3]), b)
4438

45-
def test_replace_in_set(self):
46-
a1 = AW("1")
47-
a2 = AW("2")
48-
a3 = AW("3")
49-
a4 = AW("4")
50-
c = CW(a1, {a2, a3})
51-
c.assign_parents()
52-
self.assertRaises(Exception, lambda: a2.replace_with(a4))
53-
5439
def test_replace_single(self):
5540
a1 = AW("1")
5641
a2 = AW("2")

0 commit comments

Comments
 (0)