Skip to content

Commit 51be66b

Browse files
committed
Further attempts at resolving types because in a Cython environment we still get ForwardRef instances we shouldn't be getting
1 parent 0aabf09 commit 51be66b

6 files changed

Lines changed: 124 additions & 43 deletions

File tree

pylasu/emf/metamodel_builder.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from pylasu.emf.model import find_eclassifier
1111
from pylasu.model import Node
1212
from pylasu.model.model import InternalField
13-
from pylasu.reflection import getannotations
13+
from pylasu.reflection import get_type_annotations
1414
from pylasu.reflection.reflection import get_type_origin, is_enum_type, is_sequence_type, get_type_arguments
1515

1616

@@ -91,7 +91,7 @@ def setup_base_classes(self, cls):
9191
return bases
9292

9393
def setup_attributes(self, cls):
94-
anns = getannotations(cls)
94+
anns = get_type_annotations(cls)
9595
nmspc = {
9696
"position": EReference("position", starlasu.Position, containment=True)
9797
}

pylasu/model/model.py

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
1+
import dataclasses
12
import inspect
3+
import sys
4+
import typing
25
from abc import ABC, abstractmethod, ABCMeta
36
from dataclasses import Field, MISSING, dataclass, field
47
from typing import Optional, Callable, List, Union
58

69
from .naming import ReferenceByName
710
from .position import Position, Source
811
from .reflection import Multiplicity, PropertyDescription
9-
from ..reflection import getannotations, get_type_arguments, is_sequence_type
12+
from ..reflection import get_type_annotations, get_type_arguments, is_sequence_type
1013
from ..reflection.reflection import get_type_origin
1114

15+
PYLASU_FEATURE = "pylasu_feature"
16+
1217

1318
class internal_property(property):
1419
pass
@@ -40,6 +45,24 @@ def internal_field(
4045
return InternalField(default, default_factory, init, repr, hash, compare, metadata)
4146

4247

48+
def node_property(default=MISSING):
49+
description = PropertyDescription(
50+
"", None,
51+
multiplicity=Multiplicity.OPTIONAL if default is None else Multiplicity.SINGULAR)
52+
return field(default=default, metadata={PYLASU_FEATURE: description})
53+
54+
55+
def node_containment(multiplicity: Multiplicity = Multiplicity.SINGULAR):
56+
description = PropertyDescription("", None, is_containment=True, multiplicity=multiplicity)
57+
58+
if multiplicity == Multiplicity.SINGULAR:
59+
return field(metadata={PYLASU_FEATURE: description})
60+
elif multiplicity == Multiplicity.OPTIONAL:
61+
return field(default=None, metadata={PYLASU_FEATURE: description})
62+
elif multiplicity == Multiplicity.MANY:
63+
return field(default_factory=list, metadata={PYLASU_FEATURE: description})
64+
65+
4366
class Origin(ABC):
4467
@internal_property
4568
@abstractmethod
@@ -105,15 +128,58 @@ def get_only_type_arg(decl_type):
105128
return None
106129

107130

108-
def process_annotated_property(name, decl_type, known_property_names):
109-
multiplicity = Multiplicity.SINGULAR
110-
is_reference = False
131+
def process_annotated_property(cl: type, name: str, decl_type):
132+
try:
133+
fields = dataclasses.fields(cl)
134+
except TypeError:
135+
fields = tuple()
136+
for field in fields:
137+
if field.name == name and PYLASU_FEATURE in field.metadata:
138+
feature = field.metadata[PYLASU_FEATURE]
139+
feature.name = name
140+
if isinstance(decl_type, type):
141+
feature.type = decl_type
142+
elif type(field.type) is str:
143+
feature.type = try_to_resolve_string_type(field.type, name, cl)
144+
return feature
145+
return compute_feature_from_annotation(cl, name, decl_type)
146+
147+
148+
def compute_feature_from_annotation(cl, name, decl_type):
149+
feature = PropertyDescription(name, None, False, False, Multiplicity.SINGULAR)
150+
decl_type = try_to_resolve_type(decl_type, feature)
151+
if not isinstance(decl_type, type):
152+
fwref = None
153+
if hasattr(typing, "ForwardRef"):
154+
fwref = typing.ForwardRef
155+
if fwref and isinstance(decl_type, fwref):
156+
raise Exception(f"Feature {name}'s type is unresolved forward reference {decl_type}, "
157+
f"please use node_containment or node_property")
158+
elif type(decl_type) is str:
159+
decl_type = try_to_resolve_string_type(decl_type, name, cl)
160+
if not isinstance(decl_type, type):
161+
raise Exception(f"Unsupported feature {name} of type {decl_type}")
162+
feature.type = decl_type
163+
feature.is_containment = provides_nodes(decl_type) and not feature.is_reference
164+
return feature
165+
166+
167+
def try_to_resolve_string_type(decl_type, name, cl):
168+
try:
169+
ns = getattr(sys.modules.get(cl.__module__, None), '__dict__', globals())
170+
decl_type = ns[decl_type]
171+
except KeyError:
172+
raise Exception(f"Unsupported feature {name} of unknown type {decl_type}")
173+
return decl_type
174+
175+
176+
def try_to_resolve_type(decl_type, feature):
111177
if get_type_origin(decl_type) is ReferenceByName:
112178
decl_type = get_only_type_arg(decl_type) or decl_type
113-
is_reference = True
179+
feature.is_reference = True
114180
if is_sequence_type(decl_type):
115181
decl_type = get_only_type_arg(decl_type) or decl_type
116-
multiplicity = Multiplicity.MANY
182+
feature.multiplicity = Multiplicity.MANY
117183
if get_type_origin(decl_type) is Union:
118184
type_args = get_type_arguments(decl_type)
119185
if len(type_args) == 1:
@@ -124,16 +190,12 @@ def process_annotated_property(name, decl_type, known_property_names):
124190
elif type_args[1] is type(None):
125191
decl_type = type_args[0]
126192
else:
127-
raise Exception(f"Unsupported feature {name} of type {decl_type}")
128-
if multiplicity == Multiplicity.SINGULAR:
129-
multiplicity = Multiplicity.OPTIONAL
193+
raise Exception(f"Unsupported feature {feature.name} of union type {decl_type}")
194+
if feature.multiplicity == Multiplicity.SINGULAR:
195+
feature.multiplicity = Multiplicity.OPTIONAL
130196
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)
197+
raise Exception(f"Unsupported feature {feature.name} of union type {decl_type}")
198+
return decl_type
137199

138200

139201
class Concept(ABCMeta):
@@ -155,16 +217,21 @@ def node_properties(cls):
155217
yield from cls._direct_node_properties(cl, names)
156218

157219
def _direct_node_properties(cls, cl, known_property_names):
158-
anns = getannotations(cl)
220+
if not isinstance(cls, Concept):
221+
return
222+
anns = get_type_annotations(cl)
159223
if not anns:
160224
return
161225
for name in anns:
162226
if name not in known_property_names and cls.is_node_property(name):
163-
yield process_annotated_property(name, anns[name], known_property_names)
227+
feature = process_annotated_property(cl, name, anns[name])
228+
known_property_names.add(name)
229+
yield feature
164230
for name in dir(cl):
165231
if name not in known_property_names and cls.is_node_property(name):
232+
feature = PropertyDescription(name, None, False, False)
166233
known_property_names.add(name)
167-
yield PropertyDescription(name, None, False, False)
234+
yield feature
168235

169236
def is_node_property(cls, name):
170237
return not name.startswith('_') and name not in cls.__internal_properties__

pylasu/model/reflection.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ class Multiplicity(enum.Enum):
1313
class PropertyDescription:
1414
name: str
1515
type: Optional[type]
16-
is_containment: bool
17-
is_reference: bool
16+
is_containment: bool = False
17+
is_reference: bool = False
1818
multiplicity: Multiplicity = Multiplicity.SINGULAR
1919
value: object = None
2020

pylasu/reflection/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from .reflection import getannotations, get_type_arguments, is_sequence_type
1+
from .reflection import get_type_annotations, get_type_arguments, is_sequence_type

pylasu/reflection/reflection.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,24 @@
33
from typing import Callable
44

55

6-
def getannotations(cls):
7-
try:
6+
def get_type_annotations(cls: type):
7+
if hasattr(typing, "get_type_hints"):
88
# https://peps.python.org/pep-0563/
9-
return typing.get_type_hints(cls, globalns=None, localns=None)
10-
except AttributeError:
9+
return typing.get_type_hints(cls)
10+
else:
1111
try:
1212
# On Python 3.10+
1313
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)
14+
if hasattr(inspect, "get_annotations"):
15+
return inspect.get_annotations(cls)
16+
elif hasattr(inspect, "getannotations"):
17+
return inspect.getannotations(cls)
18+
except ModuleNotFoundError:
19+
pass
20+
if isinstance(cls, type):
21+
return cls.__dict__.get('__annotations__', {})
22+
else:
23+
return getattr(cls, '__annotations__', {})
2024

2125

2226
def get_type_origin(tp):

tests/model/test_model.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import List, Optional, Union
44

55
from pylasu.model import Node, Position, Point, internal_field
6+
from pylasu.model.model import node_property, node_containment
67
from pylasu.model.reflection import Multiplicity, PropertyDescription
78
from pylasu.model.naming import ReferenceByName, Named, Scope, Symbol
89
from pylasu.support import extension_method
@@ -32,6 +33,8 @@ class ExtendedNode(SomeNode):
3233
multiple2: List[SomeNode] = dataclasses.field(default_factory=list)
3334
multiple_fwd: List["ForwardReferencedNode"] = dataclasses.field(default_factory=list)
3435
internal2: Node = internal_field(default=None)
36+
explicit_property: str = node_property("42")
37+
explicit_containment: "ExtendedNode" = node_containment(Multiplicity.MANY)
3538

3639

3740
@dataclasses.dataclass
@@ -234,7 +237,7 @@ def frob_node_2(_: Node):
234237
pass
235238

236239
pds = [pd for pd in sorted(ExtendedNode.node_properties, key=lambda x: x.name)]
237-
self.assertEqual(13, len(pds), f"{pds} should be 7")
240+
self.assertEqual(15, len(pds), f"{pds}")
238241
self.assertEqual("bar", pds[0].name)
239242
self.assertFalse(pds[0].is_containment)
240243
self.assertEqual("cont_fwd", pds[1].name)
@@ -245,16 +248,23 @@ def frob_node_2(_: Node):
245248
self.assertEqual(ForwardReferencedNode, pds[2].type)
246249
self.assertEqual("containment", pds[3].name)
247250
self.assertTrue(pds[3].is_containment)
248-
self.assertEqual("foo", pds[4].name)
249-
self.assertEqual("multiple", pds[5].name)
250-
self.assertTrue(pds[5].is_containment)
251-
self.assertEqual(Multiplicity.MANY, pds[5].multiplicity)
252-
self.assertEqual("multiple2", pds[6].name)
253-
self.assertTrue(pds[6].is_containment)
254-
self.assertEqual(Multiplicity.MANY, pds[6].multiplicity)
255-
self.assertEqual("multiple_fwd", pds[7].name)
251+
self.assertEqual("explicit_containment", pds[4].name)
252+
self.assertTrue(pds[4].is_containment)
253+
self.assertEqual(ExtendedNode, pds[4].type)
254+
self.assertEqual(Multiplicity.MANY, pds[4].multiplicity)
255+
self.assertEqual("explicit_property", pds[5].name)
256+
self.assertEqual(str, pds[5].type)
257+
self.assertEqual(Multiplicity.SINGULAR, pds[5].multiplicity)
258+
self.assertEqual("foo", pds[6].name)
259+
self.assertEqual("multiple", pds[7].name)
256260
self.assertTrue(pds[7].is_containment)
257-
self.assertEqual(ForwardReferencedNode, pds[7].type)
258261
self.assertEqual(Multiplicity.MANY, pds[7].multiplicity)
262+
self.assertEqual("multiple2", pds[8].name)
263+
self.assertTrue(pds[8].is_containment)
264+
self.assertEqual(Multiplicity.MANY, pds[8].multiplicity)
265+
self.assertEqual("multiple_fwd", pds[9].name)
266+
self.assertTrue(pds[9].is_containment)
267+
self.assertEqual(ForwardReferencedNode, pds[9].type)
268+
self.assertEqual(Multiplicity.MANY, pds[9].multiplicity)
259269

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

0 commit comments

Comments
 (0)