Skip to content

Commit 067d045

Browse files
Merge pull request #6 from Cloudrisk/develop
fixed the reference resolution issue when deeper nesting was present
2 parents b0dacaa + 83c59ae commit 067d045

File tree

10 files changed

+810
-131
lines changed

10 files changed

+810
-131
lines changed

src/rune/runtime/base_data_class.py

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ class BaseDataClass(BaseModel, ComplexTypeMetaDataMixin):
3232

3333
def __setattr__(self, name: str, value: Any) -> None:
3434
if isinstance(value, Reference):
35-
self.bind_property_to(name, value)
35+
self._bind_property_to(name, value)
3636
else:
3737
# replace reference with an object
38-
if name in self.get_rune_refs_container():
39-
self.remove_rune_ref(name)
38+
if name in self._get_rune_refs_container():
39+
self._remove_rune_ref(name)
4040
if isinstance(self.__dict__[name], _EnumWrapper):
4141
self.__dict__[name] = _EnumWrapper()
4242
# if the value is an enum, pass it to the EnumWrapper
@@ -45,14 +45,14 @@ def __setattr__(self, name: str, value: Any) -> None:
4545
value = _EnumWrapper(value)
4646
# if the value is a "model", register as rune_parent
4747
if isinstance(value, BaseMetaDataMixin):
48-
value.set_rune_parent(self)
48+
value._set_rune_parent(self)
4949
super().__setattr__(name, value)
5050

5151
@model_serializer(mode='wrap')
5252
def _serialize_refs(self, serializer, info):
5353
'''should replace objects with refs while serializing'''
5454
res = serializer(self, info)
55-
refs = self.get_rune_refs_container()
55+
refs = self._get_rune_refs_container()
5656
for property_nm, (key, ref_type) in refs.items():
5757
res[property_nm] = {ref_type.rune_ref_tag: key}
5858
res = self.__dict__.get(ROOT_CONTAINER, {}) | res
@@ -64,10 +64,21 @@ def _deserialize_refs(cls, data: Any,
6464
handler: ModelWrapValidatorHandler[Self]) -> Self:
6565
'''should resolve refs after creation'''
6666
obj = handler(data)
67-
obj.init_rune_parent()
68-
obj.resolve_references()
67+
obj._init_rune_parent() # pylint: disable=protected-access
68+
obj.resolve_references(ignore_dangling=True, recurse=False)
6969
return obj
7070

71+
def _init_rune_parent(self):
72+
'''sets the rune parent in all properties'''
73+
refs = self._get_rune_refs_container()
74+
if not self.get_rune_parent() and RUNE_OBJ_MAPS not in self.__dict__:
75+
self.__dict__[RUNE_OBJ_MAPS] = {}
76+
77+
for prop_nm, obj in self.__dict__.items():
78+
if (isinstance(obj, BaseMetaDataMixin)
79+
and not prop_nm.startswith('__') and prop_nm not in refs):
80+
obj._set_rune_parent(self) # pylint: disable=protected-access
81+
7182
def rune_serialize(
7283
self,
7384
*,
@@ -146,8 +157,7 @@ def rune_serialize(
146157

147158
root_meta = self.__dict__.setdefault(ROOT_CONTAINER, {})
148159
root_meta['@type'] = self._FQRTN
149-
root_meta['@model'] = self._FQRTN.split(
150-
'.', maxsplit=1)[0]
160+
root_meta['@model'] = self._FQRTN.split('.', maxsplit=1)[0]
151161
root_meta['@version'] = self.get_model_version()
152162

153163
return self.model_dump_json(indent=indent,
@@ -164,7 +174,7 @@ def rune_serialize(
164174

165175
@classmethod
166176
def rune_deserialize(cls,
167-
rune_json: str,
177+
rune_data: str | dict[str, Any],
168178
validate_model: bool = True,
169179
check_rune_constraints: bool = True,
170180
strict: bool = True,
@@ -192,37 +202,42 @@ def rune_deserialize(cls,
192202
#### Returns:
193203
`BaseModel:` The Rune model.
194204
'''
195-
rune_dict = json.loads(rune_json)
196-
rune_dict.pop('@version', None)
197-
rune_dict.pop('@model', None)
198-
rune_cls = cls._type_to_cls(rune_dict)
199-
model = rune_cls.model_validate(rune_dict, strict=strict)
205+
if isinstance(rune_data, str):
206+
rune_data = json.loads(rune_data)
207+
elif not isinstance(rune_data, dict):
208+
raise ValueError(f'rune_data is of type {type(rune_data)}, '
209+
'alas it has to be either dict or str!')
210+
rune_data.pop('@version', None)
211+
rune_data.pop('@model', None)
212+
rune_cls = cls._type_to_cls(rune_data)
213+
model = rune_cls.model_validate(rune_data, strict=strict)
214+
model.resolve_references(ignore_dangling=False, recurse=True)
200215
if validate_model:
201216
model.validate_model(check_rune_constraints=check_rune_constraints,
202217
strict=strict,
203218
raise_exc=raise_validation_errors)
204219
return model
205220

206-
def init_rune_parent(self):
207-
'''sets the rune parent in all properties'''
208-
refs = self.get_rune_refs_container()
209-
if not self.get_rune_parent() and RUNE_OBJ_MAPS not in self.__dict__:
210-
self.__dict__[RUNE_OBJ_MAPS] = {}
211-
212-
for prop_nm, obj in self.__dict__.items():
213-
if (isinstance(obj, BaseMetaDataMixin)
214-
and not prop_nm.startswith('__') and prop_nm not in refs):
215-
obj.set_rune_parent(self)
216-
217-
def resolve_references(self):
221+
def resolve_references(self, ignore_dangling=False, recurse=True):
218222
'''resolves all attributes which are references'''
223+
if recurse:
224+
for prop_nm, obj in self.__dict__.items():
225+
if (isinstance(obj, BaseDataClass)
226+
and not prop_nm.startswith('__')):
227+
obj.resolve_references(ignore_dangling=ignore_dangling,
228+
recurse=recurse)
229+
219230
refs = []
220231
for prop_nm, obj in self.__dict__.items():
221232
if isinstance(obj, (UnresolvedReference, Reference)):
222-
refs.append((prop_nm, obj.get_reference(self)))
233+
try:
234+
refs.append((prop_nm, obj.get_reference(self)))
235+
except KeyError:
236+
if not ignore_dangling:
237+
raise
223238

224239
for prop_nm, ref in refs:
225-
self.bind_property_to(prop_nm, ref)
240+
self._bind_property_to(prop_nm, ref)
226241

227242
def validate_model(self,
228243
check_rune_constraints=True,
@@ -241,8 +256,9 @@ def validate_model(self,
241256
att_errors = self.validate_attribs(raise_exc=raise_exc,
242257
strict=strict)
243258
if check_rune_constraints:
244-
att_errors.extend(self.validate_conditions(
245-
recursively=recursively, raise_exc=raise_exc))
259+
att_errors.extend(
260+
self.validate_conditions(recursively=recursively,
261+
raise_exc=raise_exc))
246262
return att_errors
247263
finally:
248264
self.enable_meta_checks()

src/rune/runtime/metadata.py

Lines changed: 63 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pydantic_core import PydanticCustomError
1313
# from rune.runtime.object_registry import get_object
1414

15+
DEFAULT_META = '_ALLOWED_METADATA'
1516
META_CONTAINER = '__rune_metadata'
1617
REFS_CONTAINER = '__rune_references'
1718
PARENT_PROP = '__rune_parent'
@@ -120,7 +121,7 @@ def get_reference(self, parent):
120121

121122
class BaseMetaDataMixin:
122123
'''Base class for the meta data support of basic amd complex types'''
123-
_DEFAULT_SCOPE_TYPE = 'cdm.event.common.TradeState.TradeState'
124+
_DEFAULT_SCOPE_TYPE = 'cdm.event.common.TradeState'
124125
__meta_check_disabled = False
125126

126127
@classmethod
@@ -138,29 +139,13 @@ def meta_checks_enabled(cls):
138139
'''is metadata checked during deserialize'''
139140
return not BaseMetaDataMixin.__meta_check_disabled
140141

141-
def _get_meta_container(self) -> dict[str, Any]:
142-
return self.__dict__.get(META_CONTAINER, {})
143-
144-
def _check_props_allowed(self, props: dict[str, Any]):
145-
if not props:
146-
return
147-
allowed = set(self._get_meta_container().keys())
148-
prop_keys = set(props.keys())
149-
if not prop_keys.issubset(allowed):
150-
raise ValueError('Not allowed metadata provided: '
151-
f'{prop_keys - allowed}')
152-
153-
def init_meta(self, allowed_meta: set[str]):
154-
''' if not initialised, just creates empty meta slots. If the metadata
155-
container is not empty, it will check if the already present keys
156-
are conform to the allowed keys.
157-
'''
158-
meta = self.__dict__.setdefault(META_CONTAINER, {})
159-
current_meta = set(meta.keys())
160-
if not current_meta.issubset(allowed_meta):
161-
raise ValueError(f'Allowed meta {allowed_meta} differs from the '
162-
f'currently existing meta slots: {current_meta}')
163-
meta |= {k: None for k in allowed_meta - current_meta}
142+
def is_scope_instance(self):
143+
'''is this object a scope for `scoped` keys/references'''
144+
if not (scope := self._get_rune_scope_type()):
145+
scope = self._DEFAULT_SCOPE_TYPE
146+
if not (fqcn := getattr(self, '_FQRTN', None)):
147+
fqcn = f'{self.__class__.__module__}.{self.__class__.__qualname__}'
148+
return fqcn == scope
164149

165150
def set_meta(self, check_allowed=True, **kwds):
166151
'''set some/all metadata properties'''
@@ -211,7 +196,40 @@ def get_object_by_key(self, key: str, key_type: KeyType):
211196
'''retrieve an object with a key an key type'''
212197
return self._get_object_map(key_type)[key]
213198

214-
def bind_property_to(self, property_nm: str, ref: Reference):
199+
def get_rune_parent(self) -> Self | None:
200+
'''the parent object'''
201+
return self.__dict__.get(PARENT_PROP)
202+
203+
def _get_meta_container(self) -> dict[str, Any]:
204+
return self.__dict__.get(META_CONTAINER, {})
205+
206+
def _merged_allowed_meta(self, allowed_meta: set[str]) -> set[str]:
207+
default_meta = getattr(self, DEFAULT_META, set())
208+
return set(allowed_meta) | default_meta
209+
210+
def _check_props_allowed(self, props: dict[str, Any]):
211+
if not props:
212+
return
213+
allowed = self._merged_allowed_meta(self._get_meta_container().keys())
214+
prop_keys = set(props.keys())
215+
if not prop_keys.issubset(allowed):
216+
raise ValueError('Not allowed metadata provided: '
217+
f'{prop_keys - allowed}')
218+
219+
def _init_meta(self, allowed_meta: set[str]):
220+
''' if not initialised, just creates empty meta slots. If the metadata
221+
container is not empty, it will check if the already present keys
222+
are conform to the allowed keys.
223+
'''
224+
allowed_meta = self._merged_allowed_meta(allowed_meta)
225+
meta = self.__dict__.setdefault(META_CONTAINER, {})
226+
current_meta = set(meta.keys())
227+
if not current_meta.issubset(allowed_meta):
228+
raise ValueError(f'Allowed meta {allowed_meta} differs from the '
229+
f'currently existing meta slots: {current_meta}')
230+
meta |= {k: None for k in allowed_meta - current_meta}
231+
232+
def _bind_property_to(self, property_nm: str, ref: Reference):
215233
'''set the property to reference the object referenced by the key'''
216234
allowed_ref_types = getattr(self, '_KEY_REF_CONSTRAINTS', {})
217235
if ref.key_type.rune_ref_tag not in allowed_ref_types.get(
@@ -257,23 +275,7 @@ def _get_object_map(self, key_type: KeyType) -> dict[str, Any]:
257275
# pylint: disable=protected-access
258276
return self.get_rune_parent()._get_object_map(key_type) # type:ignore
259277

260-
@classmethod
261-
def _create_unresolved_ref(cls, metadata) -> UnresolvedReference | None:
262-
if ref := {k: v for k, v in metadata.items() if k.startswith('@ref')}:
263-
if len(ref) != 1:
264-
ref.pop(KeyType.INTERNAL.rune_ref_tag, None)
265-
if len(ref) != 1:
266-
ref.pop(KeyType.EXTERNAL.rune_ref_tag, None)
267-
if len(ref) != 1:
268-
raise ValueError(f'Multiple references found: {ref}!')
269-
return UnresolvedReference(ref)
270-
return None
271-
272-
def get_rune_parent(self) -> Self | None:
273-
'''the parent object'''
274-
return self.__dict__.get(PARENT_PROP)
275-
276-
def set_rune_parent(self, parent: Self):
278+
def _set_rune_parent(self, parent: Self):
277279
'''sets the parent object'''
278280
self.__dict__[PARENT_PROP] = parent
279281
if obj_maps := self.__dict__.pop(RUNE_OBJ_MAPS, None):
@@ -305,14 +307,26 @@ def _update_object_maps(self, new_maps):
305307
f'Duplicated keys {dup_keys}')
306308
local_map |= new_map
307309

308-
def get_rune_refs_container(self):
310+
def _get_rune_refs_container(self):
309311
'''return the dictionary of the refs held'''
310312
return self.__dict__.get(REFS_CONTAINER, {})
311313

312-
def remove_rune_ref(self, name):
314+
def _remove_rune_ref(self, name):
313315
'''remove a reference'''
314316
return self.__dict__[REFS_CONTAINER].pop(name)
315317

318+
@classmethod
319+
def _create_unresolved_ref(cls, metadata) -> UnresolvedReference | None:
320+
if ref := {k: v for k, v in metadata.items() if k.startswith('@ref')}:
321+
if len(ref) != 1:
322+
ref.pop(KeyType.INTERNAL.rune_ref_tag, None)
323+
if len(ref) != 1:
324+
ref.pop(KeyType.EXTERNAL.rune_ref_tag, None)
325+
if len(ref) != 1:
326+
raise ValueError(f'Multiple references found: {ref}!')
327+
return UnresolvedReference(ref)
328+
return None
329+
316330
@classmethod
317331
@lru_cache
318332
def _get_rune_scope_type(cls):
@@ -322,18 +336,11 @@ def _get_rune_scope_type(cls):
322336
try:
323337
module = importlib.import_module(
324338
cls.__module__.split('.', maxsplit=1)[0])
325-
return getattr(module, 'rune_scope_type', default=None)
339+
return getattr(module, 'rune_scope_type', None)
326340
# pylint: disable=bare-except
327341
except: # noqa
328342
return None
329343

330-
def is_scope_instance(self):
331-
'''is this object a scope for `scoped` keys/references'''
332-
if not (scope := self._get_rune_scope_type()):
333-
scope = self._DEFAULT_SCOPE_TYPE
334-
fqcn = f'{self.__class__.__module__}.{self.__class__.__qualname__}'
335-
return fqcn == scope
336-
337344

338345
class ComplexTypeMetaDataMixin(BaseMetaDataMixin):
339346
'''metadata support for complex types'''
@@ -360,7 +367,7 @@ def deserialize(cls, obj, allowed_meta: set[str]):
360367
'''method used as pydantic `validator`'''
361368
if isinstance(obj, cls):
362369
if cls.meta_checks_enabled():
363-
obj.init_meta(allowed_meta)
370+
obj._init_meta(allowed_meta) # pylint: disable=protected-access
364371
return obj
365372

366373
if isinstance(obj, Reference):
@@ -387,7 +394,7 @@ def deserialize(cls, obj, allowed_meta: set[str]):
387394
model = rune_cls.model_validate(obj) # type: ignore
388395
model.__dict__[META_CONTAINER] = metadata
389396
if cls.meta_checks_enabled():
390-
model.init_meta(allowed_meta)
397+
model._init_meta(allowed_meta) # pylint: disable=protected-access
391398

392399
# Keys deserialization treatment
393400
model._register_keys(metadata) # pylint: disable=protected-access
@@ -441,7 +448,7 @@ def deserialize(cls, obj, handler, base_types, allowed_meta: set[str]):
441448
model = cls(data, **obj) # type: ignore
442449
model._register_keys(obj)
443450
if cls.meta_checks_enabled():
444-
model.init_meta(allowed_meta)
451+
model._init_meta(allowed_meta) # pylint: disable=protected-access
445452
return handler(model)
446453

447454
@classmethod
@@ -553,23 +560,6 @@ def __new__(cls, value, **kwds):
553560
obj.set_meta(check_allowed=False, **kwds)
554561
return obj
555562

556-
# @classmethod
557-
# @lru_cache
558-
# def serializer(cls):
559-
# '''should return the validator for the specific class'''
560-
# ser_fn = partial(cls.serialise, base_type=Decimal)
561-
# return PlainSerializer(ser_fn, return_type=dict)
562-
563-
# @classmethod
564-
# @lru_cache
565-
# def validator(cls, allowed_meta: tuple[str]):
566-
# '''default validator for the specific class'''
567-
# allowed = set(allowed_meta)
568-
# return WrapValidator(partial(cls.deserialize,
569-
# base_types=(Decimal, float, int, str),
570-
# allowed_meta=allowed),
571-
# json_schema_input_type=float | int | str | dict)
572-
573563

574564
class _EnumWrapperDefaultVal(Enum):
575565
'''marker for not set value in enum wrapper'''
@@ -641,7 +631,7 @@ def deserialize(cls, obj, allowed_meta: set[str]):
641631
model.set_meta(check_allowed=False, **obj)
642632
model._register_keys(obj) # pylint: disable=protected-access
643633
if _EnumWrapper.meta_checks_enabled():
644-
model.init_meta(allowed_meta)
634+
model._init_meta(allowed_meta) # pylint: disable=protected-access
645635
return model
646636

647637
@classmethod

0 commit comments

Comments
 (0)