1+ import dataclasses
12import inspect
3+ import sys
4+ import typing
25from abc import ABC , abstractmethod , ABCMeta
36from dataclasses import Field , MISSING , dataclass , field
47from typing import Optional , Callable , List , Union
58
69from .naming import ReferenceByName
710from .position import Position , Source
811from .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
1013from ..reflection .reflection import get_type_origin
1114
15+ PYLASU_FEATURE = "pylasu_feature"
16+
1217
1318class 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+
4366class 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
139201class 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__
0 commit comments