Skip to content

Commit d206736

Browse files
Merge pull request #39 from Strumenta/feature/pep-0563
Support string-encoded types as per PEP-0563
2 parents 94c2777 + 03a6dd0 commit d206736

7 files changed

Lines changed: 168 additions & 68 deletions

File tree

pylasu/model/model.py

Lines changed: 53 additions & 18 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
@@ -95,13 +96,57 @@ def provides_nodes(decl_type):
9596
return isinstance(decl_type, type) and issubclass(decl_type, Node)
9697

9798

99+
def get_only_type_arg(decl_type):
100+
"""If decl_type has a single type argument, return it, otherwise return None"""
101+
type_args = get_type_arguments(decl_type)
102+
if len(type_args) == 1:
103+
return type_args[0]
104+
else:
105+
return None
106+
107+
108+
def process_annotated_property(name, decl_type, known_property_names):
109+
multiplicity = Multiplicity.SINGULAR
110+
is_reference = False
111+
if get_type_origin(decl_type) is ReferenceByName:
112+
decl_type = get_only_type_arg(decl_type) or decl_type
113+
is_reference = True
114+
if is_sequence_type(decl_type):
115+
decl_type = get_only_type_arg(decl_type) or decl_type
116+
multiplicity = Multiplicity.MANY
117+
if get_type_origin(decl_type) is Union:
118+
type_args = get_type_arguments(decl_type)
119+
if len(type_args) == 1:
120+
decl_type = type_args[0]
121+
elif len(type_args) == 2:
122+
if type_args[0] is type(None):
123+
decl_type = type_args[1]
124+
elif type_args[1] is type(None):
125+
decl_type = type_args[0]
126+
else:
127+
raise Exception(f"Unsupported feature {name} of type {decl_type}")
128+
if multiplicity == Multiplicity.SINGULAR:
129+
multiplicity = Multiplicity.OPTIONAL
130+
else:
131+
raise Exception(f"Unsupported feature {name} of type {decl_type}")
132+
if not isinstance(decl_type, type):
133+
raise Exception(f"Unsupported feature {name} of type {decl_type}")
134+
is_containment = provides_nodes(decl_type) and not is_reference
135+
known_property_names.add(name)
136+
return PropertyDescription(name, decl_type, is_containment, is_reference, multiplicity)
137+
138+
98139
class Concept(ABCMeta):
99140

100141
def __init__(cls, what, bases=None, dict=None):
101142
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)])
143+
cls.__internal_properties__ = []
144+
for base in bases:
145+
if hasattr(base, "__internal_properties__"):
146+
cls.__internal_properties__.extend(base.__internal_properties__)
147+
if not cls.__internal_properties__:
148+
cls.__internal_properties__ = ["origin", "destination", "parent", "position", "position_override"]
149+
cls.__internal_properties__.extend([n for n, v in inspect.getmembers(cls, is_internal_property_or_method)])
105150

106151
@property
107152
def node_properties(cls):
@@ -115,23 +160,11 @@ def _direct_node_properties(cls, cl, known_property_names):
115160
return
116161
for name in anns:
117162
if name not in known_property_names and cls.is_node_property(name):
118-
is_child_property = False
119-
multiplicity = Multiplicity.SINGULAR
120-
if name in anns:
121-
decl_type = anns[name]
122-
if is_sequence_type(decl_type):
123-
multiplicity = Multiplicity.MANY
124-
type_args = get_type_arguments(decl_type)
125-
if len(type_args) == 1:
126-
is_child_property = provides_nodes(type_args[0])
127-
else:
128-
is_child_property = provides_nodes(decl_type)
129-
known_property_names.add(name)
130-
yield PropertyDescription(name, is_child_property, multiplicity)
163+
yield process_annotated_property(name, anns[name], known_property_names)
131164
for name in dir(cl):
132165
if name not in known_property_names and cls.is_node_property(name):
133166
known_property_names.add(name)
134-
yield PropertyDescription(name, False)
167+
yield PropertyDescription(name, None, False, False)
135168

136169
def is_node_property(cls, name):
137170
return not name.startswith('_') and name not in cls.__internal_properties__
@@ -180,7 +213,9 @@ def source(self) -> Optional[Source]:
180213

181214
@internal_property
182215
def properties(self):
183-
return (PropertyDescription(p.name, p.provides_nodes, p.multiplicity, getattr(self, p.name))
216+
return (PropertyDescription(p.name, p.type,
217+
is_containment=p.is_containment, is_reference=p.is_reference,
218+
multiplicity=p.multiplicity, value=getattr(self, p.name))
184219
for p in self.__class__.node_properties)
185220

186221
@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: 91 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,14 +13,26 @@ 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

2326

27+
@dataclasses.dataclass
28+
class ExtendedNode(SomeNode):
29+
prop = 2
30+
cont_fwd: "ExtendedNode" = None
31+
cont_ref: ReferenceByName["ExtendedNode"] = None
32+
multiple2: List[SomeNode] = dataclasses.field(default_factory=list)
33+
internal2: Node = internal_field(default=None)
34+
35+
2436
@dataclasses.dataclass
2537
class SomeSymbol(Symbol):
2638
index: int = dataclasses.field(default=None)
@@ -39,6 +51,14 @@ class InvalidNode(Node):
3951
another_child: Node = None
4052

4153

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

4464
def test_reference_by_name_unsolved_str(self):
@@ -77,9 +97,29 @@ def test_node_with_position(self):
7797

7898
def test_node_properties(self):
7999
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"))
100+
self.assertIsNotNone(find_feature(node, 'foo'))
101+
self.assertFalse(find_feature(node, 'foo').is_containment)
102+
self.assertIsNotNone(find_feature(node, 'bar'))
103+
self.assertFalse(find_feature(node, 'bar').is_containment)
104+
self.assertIsNotNone(find_feature(node, 'name'))
105+
self.assertTrue(find_feature(node, 'containment').is_containment)
106+
self.assertFalse(find_feature(node, 'containment').is_reference)
107+
self.assertFalse(find_feature(node, 'reference').is_containment)
108+
self.assertTrue(find_feature(node, 'reference').is_reference)
109+
with self.assertRaises(StopIteration):
110+
next(n for n in node.properties if n.name == '__private__')
111+
with self.assertRaises(StopIteration):
112+
next(n for n in node.properties if n.name == 'non_existent')
113+
with self.assertRaises(StopIteration):
114+
next(n for n in node.properties if n.name == 'properties')
115+
with self.assertRaises(StopIteration):
116+
next(n for n in node.properties if n.name == "origin")
117+
118+
def test_node_properties_inheritance(self):
119+
node = ExtendedNode("n").with_position(Position(Point(1, 0), Point(2, 1)))
120+
self.assertIsNotNone(find_feature(node, 'foo'))
121+
self.assertIsNotNone(find_feature(node, 'bar'))
122+
self.assertIsNotNone(find_feature(node, 'name'))
83123
with self.assertRaises(StopIteration):
84124
next(n for n in node.properties if n.name == '__private__')
85125
with self.assertRaises(StopIteration):
@@ -159,20 +199,52 @@ def frob_node(_: Node):
159199
pass
160200

161201
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")
202+
self.assertEqual(8, len(pds), f"{pds} should be 7")
163203
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)
204+
self.assertFalse(pds[0].is_containment)
205+
self.assertEqual("containment", pds[1].name)
206+
self.assertTrue(pds[1].is_containment)
207+
self.assertEqual("foo", pds[2].name)
208+
self.assertFalse(pds[2].is_containment)
209+
self.assertEqual("multiple", pds[3].name)
210+
self.assertTrue(pds[3].is_containment)
172211
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)
212+
self.assertEqual("multiple_opt", pds[4].name)
213+
self.assertTrue(pds[4].is_containment)
214+
self.assertEqual(Multiplicity.MANY, pds[4].multiplicity)
215+
self.assertEqual("name", pds[5].name)
216+
self.assertFalse(pds[5].is_containment)
217+
self.assertEqual("optional", pds[6].name)
218+
self.assertTrue(pds[6].is_containment)
219+
self.assertEqual(Multiplicity.OPTIONAL, pds[6].multiplicity)
220+
self.assertEqual("reference", pds[7].name)
221+
self.assertTrue(pds[7].is_reference)
222+
223+
self.assertRaises(Exception, lambda: [x for x in InvalidNode.node_properties])
224+
225+
def test_node_properties_meta_inheritance(self):
226+
@extension_method(Node)
227+
def frob_node_2(_: Node):
228+
pass
229+
230+
pds = [pd for pd in sorted(ExtendedNode.node_properties, key=lambda x: x.name)]
231+
self.assertEqual(12, len(pds), f"{pds} should be 7")
232+
self.assertEqual("bar", pds[0].name)
233+
self.assertFalse(pds[0].is_containment)
234+
self.assertEqual("cont_fwd", pds[1].name)
235+
self.assertTrue(pds[1].is_containment)
236+
self.assertEqual(ExtendedNode, pds[1].type)
237+
self.assertEqual("cont_ref", pds[2].name)
238+
self.assertTrue(pds[2].is_reference)
239+
self.assertEqual(ExtendedNode, pds[2].type)
240+
self.assertEqual("containment", pds[3].name)
241+
self.assertTrue(pds[3].is_containment)
242+
self.assertEqual("foo", pds[4].name)
243+
self.assertEqual("multiple", pds[5].name)
244+
self.assertTrue(pds[5].is_containment)
245+
self.assertEqual(Multiplicity.MANY, pds[5].multiplicity)
246+
self.assertEqual("multiple2", pds[6].name)
247+
self.assertTrue(pds[6].is_containment)
248+
self.assertEqual(Multiplicity.MANY, pds[6].multiplicity)
177249

178250
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: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import unittest
22
from dataclasses import dataclass
3-
from typing import List, Set
3+
from typing import List
44

55
from pylasu.model import Node
66
from tests.fixtures import box, Item
@@ -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)