rune-runtime 1.0.20__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rune/runtime/__init__.py +5 -0
- rune/runtime/base_data_class.py +407 -0
- rune/runtime/conditions.py +57 -0
- rune/runtime/func_proxy.py +72 -0
- rune/runtime/metadata.py +656 -0
- rune/runtime/object_registry.py +17 -0
- rune/runtime/py.typed +0 -0
- rune/runtime/utils.py +387 -0
- rune/runtime/version.py +24 -0
- rune_runtime-1.0.20.dist-info/METADATA +135 -0
- rune_runtime-1.0.20.dist-info/RECORD +16 -0
- rune_runtime-1.0.20.dist-info/WHEEL +5 -0
- rune_runtime-1.0.20.dist-info/licenses/LICENSE +201 -0
- rune_runtime-1.0.20.dist-info/licenses/LICENSE.spdx +7 -0
- rune_runtime-1.0.20.dist-info/licenses/NOTICE +5 -0
- rune_runtime-1.0.20.dist-info/top_level.txt +1 -0
rune/runtime/__init__.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
'''Base class for all Rune type classes'''
|
|
2
|
+
import logging
|
|
3
|
+
import importlib
|
|
4
|
+
import copy
|
|
5
|
+
import json
|
|
6
|
+
from typing import get_args, get_origin, Any, Literal
|
|
7
|
+
from typing_extensions import Self
|
|
8
|
+
from pydantic import (BaseModel, ValidationError, ConfigDict, model_serializer,
|
|
9
|
+
model_validator, ModelWrapValidatorHandler)
|
|
10
|
+
from pydantic.main import IncEx
|
|
11
|
+
from rune.runtime.conditions import ConditionViolationError
|
|
12
|
+
from rune.runtime.conditions import get_conditions
|
|
13
|
+
from rune.runtime.metadata import (ComplexTypeMetaDataMixin, Reference,
|
|
14
|
+
UnresolvedReference, BaseMetaDataMixin,
|
|
15
|
+
_EnumWrapper, RUNE_OBJ_MAPS)
|
|
16
|
+
|
|
17
|
+
ROOT_CONTAINER = '__rune_root_metadata'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class BaseDataClass(BaseModel, ComplexTypeMetaDataMixin):
|
|
21
|
+
''' A base class for all cdm generated classes. It is derived from
|
|
22
|
+
`pydantic.BaseModel` which provides type checking at object creation
|
|
23
|
+
for all cdm classes. It provides as well the `validate_model`,
|
|
24
|
+
`validate_conditions` and `validate_attribs` methods which perform the
|
|
25
|
+
conditions, cardinality and type checks as specified in the rune
|
|
26
|
+
type model. The method `validate_model` is not invoked automatically,
|
|
27
|
+
but is left to the user to determine when to check the validity of the
|
|
28
|
+
cdm model.
|
|
29
|
+
'''
|
|
30
|
+
model_config = ConfigDict(extra='ignore',
|
|
31
|
+
revalidate_instances='always',
|
|
32
|
+
arbitrary_types_allowed=True)
|
|
33
|
+
|
|
34
|
+
def __setattr__(self, name: str, value: Any) -> None:
|
|
35
|
+
if isinstance(value, Reference):
|
|
36
|
+
self._bind_property_to(name, value)
|
|
37
|
+
else:
|
|
38
|
+
# replace reference with an object
|
|
39
|
+
if name in self._get_rune_refs_container():
|
|
40
|
+
self._remove_rune_ref(name)
|
|
41
|
+
if isinstance(self.__dict__[name], _EnumWrapper):
|
|
42
|
+
self.__dict__[name] = _EnumWrapper()
|
|
43
|
+
# if the value is an enum, pass it to the EnumWrapper
|
|
44
|
+
if (isinstance(self.__dict__[name], _EnumWrapper)
|
|
45
|
+
and not isinstance(value, _EnumWrapper)):
|
|
46
|
+
value = _EnumWrapper(value)
|
|
47
|
+
# if the value is a "model", register as rune_parent
|
|
48
|
+
if isinstance(value, BaseMetaDataMixin):
|
|
49
|
+
value._set_rune_parent(self)
|
|
50
|
+
super().__setattr__(name, value)
|
|
51
|
+
|
|
52
|
+
@model_serializer(mode='wrap')
|
|
53
|
+
def _serialize_refs(self, serializer, info):
|
|
54
|
+
'''should replace objects with refs while serializing'''
|
|
55
|
+
res = serializer(self, info)
|
|
56
|
+
refs = self._get_rune_refs_container()
|
|
57
|
+
for property_nm, (key, ref_type) in refs.items():
|
|
58
|
+
res[property_nm] = {ref_type.rune_ref_tag: key}
|
|
59
|
+
res = self.__dict__.get(ROOT_CONTAINER, {}) | res
|
|
60
|
+
return res
|
|
61
|
+
|
|
62
|
+
@model_validator(mode='wrap')
|
|
63
|
+
@classmethod
|
|
64
|
+
def _deserialize_refs(cls, data: Any,
|
|
65
|
+
handler: ModelWrapValidatorHandler[Self]) -> Self:
|
|
66
|
+
'''should resolve refs after creation'''
|
|
67
|
+
obj = handler(data)
|
|
68
|
+
obj._init_rune_parent() # pylint: disable=protected-access
|
|
69
|
+
obj.resolve_references(ignore_dangling=True, recurse=False)
|
|
70
|
+
return obj
|
|
71
|
+
|
|
72
|
+
def _init_rune_parent(self):
|
|
73
|
+
'''sets the rune parent in all properties'''
|
|
74
|
+
refs = self._get_rune_refs_container()
|
|
75
|
+
if not self.get_rune_parent() and RUNE_OBJ_MAPS not in self.__dict__:
|
|
76
|
+
self.__dict__[RUNE_OBJ_MAPS] = {}
|
|
77
|
+
|
|
78
|
+
for prop_nm, obj in self.__dict__.items():
|
|
79
|
+
if (isinstance(obj, BaseMetaDataMixin)
|
|
80
|
+
and not prop_nm.startswith('__') and prop_nm not in refs):
|
|
81
|
+
obj._set_rune_parent(self) # pylint: disable=protected-access
|
|
82
|
+
|
|
83
|
+
def rune_serialize(
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
validate_model: bool = True,
|
|
87
|
+
check_rune_constraints: bool = True,
|
|
88
|
+
strict: bool = True,
|
|
89
|
+
raise_validation_errors: bool = True,
|
|
90
|
+
indent: int | None = None,
|
|
91
|
+
include: IncEx | None = None,
|
|
92
|
+
exclude: IncEx | None = None,
|
|
93
|
+
exclude_unset: bool = True,
|
|
94
|
+
exclude_defaults: bool = True,
|
|
95
|
+
exclude_none: bool = False,
|
|
96
|
+
round_trip: bool = False,
|
|
97
|
+
warnings: bool | Literal['none', 'warn', 'error'] = True,
|
|
98
|
+
serialize_as_any: bool = False,
|
|
99
|
+
) -> str:
|
|
100
|
+
'''Rune conform serialization to json string. To be invoked on the model
|
|
101
|
+
root.
|
|
102
|
+
|
|
103
|
+
#### Args:
|
|
104
|
+
`validate_model (bool, optional):` Validate the model prior
|
|
105
|
+
serialization. It checks also all Rune type constraints.
|
|
106
|
+
Defaults to True.
|
|
107
|
+
|
|
108
|
+
`check_rune_constraints (bool, optional):` If `validate_model` is
|
|
109
|
+
set to `True`, executes all model defined Rune constraints after
|
|
110
|
+
deserialization. Defaults to True.
|
|
111
|
+
|
|
112
|
+
`strict (bool, optional):` Perform strict attribute validation.
|
|
113
|
+
Defaults to True.
|
|
114
|
+
|
|
115
|
+
`raise_validation_errors (bool, optional):` Raise an exception in
|
|
116
|
+
case a validation error has occurred. Defaults to True.
|
|
117
|
+
|
|
118
|
+
`indent (int | None, optional):` Indentation to use in the JSON
|
|
119
|
+
output. If None is passed, the output will be compact. Defaults to
|
|
120
|
+
None.
|
|
121
|
+
|
|
122
|
+
`include (IncEx | None, optional):` Field(s) to include in the JSON
|
|
123
|
+
output. Defaults to None.
|
|
124
|
+
|
|
125
|
+
`exclude (IncEx | None, optional):` Field(s) to exclude from the
|
|
126
|
+
JSON output. Defaults to None.
|
|
127
|
+
|
|
128
|
+
`exclude_unset (bool, optional):` Whether to exclude fields that
|
|
129
|
+
have not been explicitly set. Defaults to True.
|
|
130
|
+
|
|
131
|
+
`exclude_defaults (bool, optional):` Whether to exclude fields that
|
|
132
|
+
are set to their default value. Defaults to True.
|
|
133
|
+
|
|
134
|
+
`exclude_none (bool, optional):` Whether to exclude fields that have
|
|
135
|
+
a value of `None`. Defaults to False.
|
|
136
|
+
|
|
137
|
+
`round_trip (bool, optional):` If True, dumped values should be
|
|
138
|
+
valid as input for non-idempotent types such as Json[T]. Defaults to
|
|
139
|
+
False.
|
|
140
|
+
|
|
141
|
+
`warnings (bool | Literal['none', 'warn', 'error'], optional):` How
|
|
142
|
+
to handle serialization errors. False/"none" ignores them,
|
|
143
|
+
True/"warn" logs errors, "error" raises a
|
|
144
|
+
`PydanticSerializationError`. Defaults to True.
|
|
145
|
+
|
|
146
|
+
`serialize_as_any (bool, optional):` Whether to serialize fields
|
|
147
|
+
with duck-typing serialization behavior. Defaults to False.
|
|
148
|
+
|
|
149
|
+
#### Returns:
|
|
150
|
+
`str:` A Rune conforming JSON string representation of the model.
|
|
151
|
+
'''
|
|
152
|
+
try:
|
|
153
|
+
if validate_model:
|
|
154
|
+
self.validate_model(
|
|
155
|
+
check_rune_constraints=check_rune_constraints,
|
|
156
|
+
strict=strict,
|
|
157
|
+
raise_exc=raise_validation_errors)
|
|
158
|
+
|
|
159
|
+
root_meta = self.__dict__.setdefault(ROOT_CONTAINER, {})
|
|
160
|
+
root_meta['@type'] = self._FQRTN
|
|
161
|
+
root_meta['@model'] = self._FQRTN.split('.', maxsplit=1)[0]
|
|
162
|
+
root_meta['@version'] = self.get_model_version()
|
|
163
|
+
|
|
164
|
+
return self.model_dump_json(indent=indent,
|
|
165
|
+
include=include,
|
|
166
|
+
exclude=exclude,
|
|
167
|
+
exclude_unset=exclude_unset,
|
|
168
|
+
exclude_defaults=exclude_defaults,
|
|
169
|
+
exclude_none=exclude_none,
|
|
170
|
+
round_trip=round_trip,
|
|
171
|
+
warnings=warnings,
|
|
172
|
+
serialize_as_any=serialize_as_any)
|
|
173
|
+
finally:
|
|
174
|
+
self.__dict__.pop(ROOT_CONTAINER)
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def rune_deserialize(cls,
|
|
178
|
+
rune_data: str | dict[str, Any],
|
|
179
|
+
validate_model: bool = True,
|
|
180
|
+
check_rune_constraints: bool = True,
|
|
181
|
+
strict: bool = False,
|
|
182
|
+
raise_validation_errors: bool = True) -> BaseModel:
|
|
183
|
+
# pylint: disable=line-too-long
|
|
184
|
+
'''Rune compliant deserialization
|
|
185
|
+
|
|
186
|
+
#### Args:
|
|
187
|
+
`rune_json (str):` A JSON string.
|
|
188
|
+
|
|
189
|
+
`validate_model (bool, optional):` Validate the model after
|
|
190
|
+
deserialization. It checks also all Rune type constraints. Defaults
|
|
191
|
+
to True.
|
|
192
|
+
|
|
193
|
+
`check_rune_constraints (bool, optional):` If `validate_model` is
|
|
194
|
+
set to `True`, executes all model defined Rune constraints after
|
|
195
|
+
deserialization. Defaults to True.
|
|
196
|
+
|
|
197
|
+
`strict (bool, optional):` Perform strict attribute validation.
|
|
198
|
+
Defaults to False.
|
|
199
|
+
|
|
200
|
+
`raise_validation_errors (bool, optional):` Raise an exception in
|
|
201
|
+
case a validation error has occurred. Defaults to True.
|
|
202
|
+
|
|
203
|
+
#### Returns:
|
|
204
|
+
`BaseModel:` The Rune model.
|
|
205
|
+
'''
|
|
206
|
+
if isinstance(rune_data, str):
|
|
207
|
+
rune_dict = json.loads(rune_data)
|
|
208
|
+
# NOTE: json.loads will not create the right atomic types
|
|
209
|
+
# (e.g. date, datetime, time etc) and the strict model validate
|
|
210
|
+
# will not attempt to convert them.
|
|
211
|
+
strict = False
|
|
212
|
+
elif not isinstance(rune_data, dict):
|
|
213
|
+
raise ValueError(f'rune_data is of type {type(rune_data)}, '
|
|
214
|
+
'alas it has to be either dict or str!')
|
|
215
|
+
else:
|
|
216
|
+
rune_dict = copy.deepcopy(rune_data)
|
|
217
|
+
rune_dict.pop('@version', None)
|
|
218
|
+
rune_dict.pop('@model', None)
|
|
219
|
+
rune_cls = cls._type_to_cls(rune_dict)
|
|
220
|
+
model = rune_cls.model_validate(rune_dict, strict=strict)
|
|
221
|
+
model.resolve_references(ignore_dangling=False, recurse=True)
|
|
222
|
+
if validate_model:
|
|
223
|
+
model.validate_model(check_rune_constraints=check_rune_constraints,
|
|
224
|
+
strict=strict,
|
|
225
|
+
raise_exc=raise_validation_errors)
|
|
226
|
+
return model
|
|
227
|
+
|
|
228
|
+
def resolve_references(self, ignore_dangling=False, recurse=True):
|
|
229
|
+
'''resolves all attributes which are references'''
|
|
230
|
+
if recurse:
|
|
231
|
+
for prop_nm, obj in self.__dict__.items():
|
|
232
|
+
if (isinstance(obj, BaseDataClass)
|
|
233
|
+
and not prop_nm.startswith('__')):
|
|
234
|
+
obj.resolve_references(ignore_dangling=ignore_dangling,
|
|
235
|
+
recurse=recurse)
|
|
236
|
+
|
|
237
|
+
refs = []
|
|
238
|
+
for prop_nm, obj in self.__dict__.items():
|
|
239
|
+
if isinstance(obj, (UnresolvedReference, Reference)):
|
|
240
|
+
try:
|
|
241
|
+
refs.append((prop_nm, obj.get_reference(self)))
|
|
242
|
+
except KeyError:
|
|
243
|
+
if not ignore_dangling:
|
|
244
|
+
raise
|
|
245
|
+
|
|
246
|
+
for prop_nm, ref in refs:
|
|
247
|
+
self._bind_property_to(prop_nm, ref)
|
|
248
|
+
|
|
249
|
+
def validate_model(self,
|
|
250
|
+
check_rune_constraints=True,
|
|
251
|
+
recursively: bool = True,
|
|
252
|
+
raise_exc: bool = True,
|
|
253
|
+
strict: bool = True) -> list:
|
|
254
|
+
''' This method performs full model validation. It will validate all
|
|
255
|
+
attributes and it will also invoke `validate_conditions` to check
|
|
256
|
+
all conditions and the cardinality of all attributes of this object.
|
|
257
|
+
The parameter `raise_exc` controls whether an exception should be
|
|
258
|
+
thrown if a validation or condition is violated or if a list with
|
|
259
|
+
all encountered violations should be returned instead.
|
|
260
|
+
'''
|
|
261
|
+
try:
|
|
262
|
+
self.disable_meta_checks()
|
|
263
|
+
att_errors = self.validate_attribs(raise_exc=raise_exc,
|
|
264
|
+
strict=strict)
|
|
265
|
+
if check_rune_constraints:
|
|
266
|
+
att_errors.extend(
|
|
267
|
+
self.validate_conditions(recursively=recursively,
|
|
268
|
+
raise_exc=raise_exc))
|
|
269
|
+
return att_errors
|
|
270
|
+
finally:
|
|
271
|
+
self.enable_meta_checks()
|
|
272
|
+
|
|
273
|
+
def validate_attribs(self,
|
|
274
|
+
raise_exc: bool = True,
|
|
275
|
+
strict: bool = True) -> list:
|
|
276
|
+
''' This method performs attribute type validation.
|
|
277
|
+
The parameter `raise_exc` controls whether an exception should be
|
|
278
|
+
thrown if a validation or condition is violated or if a list with
|
|
279
|
+
all encountered violations should be returned instead.
|
|
280
|
+
'''
|
|
281
|
+
try:
|
|
282
|
+
self.model_validate(self, strict=strict)
|
|
283
|
+
except ValidationError as validation_error:
|
|
284
|
+
if raise_exc:
|
|
285
|
+
raise validation_error
|
|
286
|
+
return [validation_error]
|
|
287
|
+
return []
|
|
288
|
+
|
|
289
|
+
def validate_conditions(self,
|
|
290
|
+
recursively: bool = True,
|
|
291
|
+
raise_exc: bool = True) -> list:
|
|
292
|
+
''' This method will check all conditions and the cardinality of all
|
|
293
|
+
attributes of this object. This includes conditions and cardinality
|
|
294
|
+
of properties specified in the base classes. If the parameter
|
|
295
|
+
`recursively` is set to `True`, it will invoke the validation on the
|
|
296
|
+
rune defined attributes of this object too.
|
|
297
|
+
The parameter `raise_exc` controls whether an exception should be
|
|
298
|
+
thrown if a condition is not met or if a list with all encountered
|
|
299
|
+
condition violations should be returned instead.
|
|
300
|
+
'''
|
|
301
|
+
self_rep = object.__repr__(self)
|
|
302
|
+
logging.info('Checking conditions for %s ...', self_rep)
|
|
303
|
+
exceptions = []
|
|
304
|
+
for name, condition in get_conditions(self.__class__, BaseDataClass):
|
|
305
|
+
logging.info('Checking condition %s for %s...', name, self_rep)
|
|
306
|
+
if not condition(self):
|
|
307
|
+
msg = f'Condition "{name}" for {repr(self)} failed!'
|
|
308
|
+
logging.error(msg)
|
|
309
|
+
exc = ConditionViolationError(msg)
|
|
310
|
+
if raise_exc:
|
|
311
|
+
raise exc
|
|
312
|
+
exceptions.append(exc)
|
|
313
|
+
else:
|
|
314
|
+
logging.info('Condition %s for %s satisfied.', name, self_rep)
|
|
315
|
+
if recursively:
|
|
316
|
+
for k, v in self.__dict__.items():
|
|
317
|
+
if k.startswith('__'): # ignore *all* private vars!
|
|
318
|
+
continue
|
|
319
|
+
logging.info('Validating conditions of property %s', k)
|
|
320
|
+
exceptions += _validate_conditions_recursively(
|
|
321
|
+
v, raise_exc=raise_exc)
|
|
322
|
+
err = f'with {len(exceptions)}' if exceptions else 'without'
|
|
323
|
+
logging.info('Done conditions checking for %s %s errors.', self_rep,
|
|
324
|
+
err)
|
|
325
|
+
return exceptions
|
|
326
|
+
|
|
327
|
+
def add_to_list_attribute(self, attr_name: str, value) -> None:
|
|
328
|
+
'''
|
|
329
|
+
Adds a value to a list attribute, ensuring the value is of an allowed
|
|
330
|
+
type.
|
|
331
|
+
|
|
332
|
+
Parameters:
|
|
333
|
+
attr_name (str): Name of the list attribute.
|
|
334
|
+
value: Value to add to the list.
|
|
335
|
+
|
|
336
|
+
Raises:
|
|
337
|
+
AttributeError: If the attribute name is not found or not a list.
|
|
338
|
+
TypeError: If the value type is not one of the allowed types.
|
|
339
|
+
'''
|
|
340
|
+
if not hasattr(self, attr_name):
|
|
341
|
+
raise AttributeError(f"Attribute {attr_name} not found.")
|
|
342
|
+
|
|
343
|
+
attr = getattr(self, attr_name)
|
|
344
|
+
if not isinstance(attr, list):
|
|
345
|
+
raise AttributeError(f"Attribute {attr_name} is not a list.")
|
|
346
|
+
|
|
347
|
+
# Get allowed types for the list elements
|
|
348
|
+
allowed_types = self.get_allowed_types_for_list_field(attr_name)
|
|
349
|
+
|
|
350
|
+
# Check if value is an instance of one of the allowed types
|
|
351
|
+
if not isinstance(value, allowed_types):
|
|
352
|
+
raise TypeError(f"Value must be an instance of {allowed_types}, "
|
|
353
|
+
f"not {type(value)}")
|
|
354
|
+
|
|
355
|
+
attr.append(value)
|
|
356
|
+
|
|
357
|
+
@classmethod
|
|
358
|
+
def get_allowed_types_for_list_field(cls, field_name: str):
|
|
359
|
+
'''
|
|
360
|
+
Gets the allowed types for a list field in a Pydantic model, supporting
|
|
361
|
+
both Union and | operator.
|
|
362
|
+
|
|
363
|
+
Parameters:
|
|
364
|
+
cls (type): The Pydantic model class.
|
|
365
|
+
field_name (str): The field name.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
tuple: A tuple of allowed types.
|
|
369
|
+
'''
|
|
370
|
+
field_type = cls.__annotations__.get(field_name)
|
|
371
|
+
if field_type and get_origin(field_type) is list:
|
|
372
|
+
list_elem_type = get_args(field_type)[0]
|
|
373
|
+
if get_origin(list_elem_type):
|
|
374
|
+
return get_args(list_elem_type)
|
|
375
|
+
return (list_elem_type, ) # Single type or | operator used
|
|
376
|
+
return ()
|
|
377
|
+
|
|
378
|
+
@classmethod
|
|
379
|
+
def get_model_version(cls):
|
|
380
|
+
''' Attempt to obtain the Rune model version, in case of a failure,
|
|
381
|
+
0.0.0 will be returned
|
|
382
|
+
'''
|
|
383
|
+
try:
|
|
384
|
+
module = importlib.import_module(
|
|
385
|
+
cls.__module__.split('.', maxsplit=1)[0])
|
|
386
|
+
return getattr(module, 'rune_model_version', default='0.0.0')
|
|
387
|
+
# pylint: disable=bare-except
|
|
388
|
+
except: # noqa
|
|
389
|
+
return '0.0.0'
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _validate_conditions_recursively(obj, raise_exc=True):
|
|
393
|
+
'''Helper to execute conditions recursively on a model.'''
|
|
394
|
+
if not obj:
|
|
395
|
+
return []
|
|
396
|
+
if isinstance(obj, BaseDataClass):
|
|
397
|
+
return obj.validate_conditions(
|
|
398
|
+
recursively=True, # type:ignore
|
|
399
|
+
raise_exc=raise_exc)
|
|
400
|
+
if isinstance(obj, (list, tuple)):
|
|
401
|
+
exc = []
|
|
402
|
+
for item in obj:
|
|
403
|
+
exc += _validate_conditions_recursively(item, raise_exc=raise_exc)
|
|
404
|
+
return exc
|
|
405
|
+
return []
|
|
406
|
+
|
|
407
|
+
# EOF
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'''facilities for rune conditions'''
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
_CONDITIONS_REGISTRY: defaultdict[str, dict[str, Any]] = defaultdict(dict)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ConditionViolationError(ValueError):
|
|
9
|
+
'''Exception thrown on violation of a constraint'''
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def rune_condition(condition):
|
|
13
|
+
'''Wrapper to register all constraint functions in the global registry'''
|
|
14
|
+
path_components = condition.__qualname__.split('.')
|
|
15
|
+
path = '.'.join([condition.__module__ or ''] + path_components[:-1])
|
|
16
|
+
name = path_components[-1]
|
|
17
|
+
_CONDITIONS_REGISTRY[path][name] = condition
|
|
18
|
+
|
|
19
|
+
return condition
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def rune_local_condition(registry: dict):
|
|
23
|
+
'''Registers a condition function in a local registry.'''
|
|
24
|
+
|
|
25
|
+
def decorator(condition):
|
|
26
|
+
path_components = condition.__qualname__.split('.')
|
|
27
|
+
path = '.'.join([condition.__module__ or ''] + path_components)
|
|
28
|
+
registry[path] = condition
|
|
29
|
+
|
|
30
|
+
return condition
|
|
31
|
+
|
|
32
|
+
return decorator
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def rune_execute_local_conditions(registry: dict, cond_type: str):
|
|
36
|
+
'''Executes all registered in a local registry.'''
|
|
37
|
+
for condition_path, condition_func in registry.items():
|
|
38
|
+
if not condition_func():
|
|
39
|
+
raise ConditionViolationError(
|
|
40
|
+
f"{cond_type} '{condition_path}' failed.")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def get_conditions(cls, base_class) -> list:
|
|
44
|
+
'''returns the conditions registered for the passed in class'''
|
|
45
|
+
res = []
|
|
46
|
+
index = cls.__mro__.index(base_class)
|
|
47
|
+
for c in reversed(cls.__mro__[:index]):
|
|
48
|
+
fqcn = _fqcn(c)
|
|
49
|
+
res += [('.'.join([fqcn, k]), v)
|
|
50
|
+
for k, v in _CONDITIONS_REGISTRY.get(fqcn, {}).items()]
|
|
51
|
+
return res
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _fqcn(cls) -> str:
|
|
55
|
+
return '.'.join([cls.__module__ or '', cls.__qualname__])
|
|
56
|
+
|
|
57
|
+
# EOF
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'''func proxy'''
|
|
2
|
+
import inspect
|
|
3
|
+
import functools
|
|
4
|
+
|
|
5
|
+
__all__ = ['FuncProxy', 'replaceable', 'create_module_attr_guardian']
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class FuncProxy:
|
|
9
|
+
'''A callable proxy allowing functions to be replaced at runtime'''
|
|
10
|
+
__slots__ = ('_func',)
|
|
11
|
+
|
|
12
|
+
def __init__(self, func):
|
|
13
|
+
self._func = func
|
|
14
|
+
|
|
15
|
+
def __call__(self, *args, **kwargs):
|
|
16
|
+
'''pass the call to the current function'''
|
|
17
|
+
return self._func(*args, **kwargs)
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def func(self):
|
|
21
|
+
'''current function'''
|
|
22
|
+
return self._func
|
|
23
|
+
|
|
24
|
+
@func.setter
|
|
25
|
+
def func(self, func):
|
|
26
|
+
'''replace the current function with a new one'''
|
|
27
|
+
self.__assign__(func)
|
|
28
|
+
|
|
29
|
+
def __assign__(self, func):
|
|
30
|
+
'''assigns the new function and checks parameter list compatibility'''
|
|
31
|
+
if not callable(func):
|
|
32
|
+
raise ValueError(f'Need a callable, but got {str(func)}')
|
|
33
|
+
|
|
34
|
+
curr_params = inspect.signature(self._func).parameters
|
|
35
|
+
new_params = inspect.signature(func).parameters
|
|
36
|
+
if curr_params.keys() != new_params.keys():
|
|
37
|
+
raise ValueError(
|
|
38
|
+
'Replacement function parameter list do not match the current '
|
|
39
|
+
f'parameter list of {str(self._func)}'
|
|
40
|
+
)
|
|
41
|
+
self._func = func
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def replaceable(func):
|
|
45
|
+
'''wrapper for a function which can be replaced at runtime'''
|
|
46
|
+
proxy = FuncProxy(func)
|
|
47
|
+
|
|
48
|
+
@functools.wraps(func)
|
|
49
|
+
def wrapper(*args, **kwargs):
|
|
50
|
+
return proxy(*args, **kwargs)
|
|
51
|
+
|
|
52
|
+
wrapper.__assign__ = proxy.__assign__ # type: ignore
|
|
53
|
+
return wrapper
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def create_module_attr_guardian(module):
|
|
57
|
+
'''Returns a module setter class derived from the invoking module'''
|
|
58
|
+
# pylint: disable=too-few-public-methods
|
|
59
|
+
class ModuleAttrSetter(module):
|
|
60
|
+
''' Redirects the assignment of an attribute to its __assign__ method
|
|
61
|
+
if defined, otherwise the default functionality is used and the
|
|
62
|
+
attribute is just replaced.
|
|
63
|
+
'''
|
|
64
|
+
def __setattr__(self, attr, val):
|
|
65
|
+
exists = getattr(self, attr, None)
|
|
66
|
+
if exists is not None and hasattr(exists, '__assign__'):
|
|
67
|
+
exists.__assign__(val)
|
|
68
|
+
else:
|
|
69
|
+
super().__setattr__(attr, val)
|
|
70
|
+
return ModuleAttrSetter
|
|
71
|
+
|
|
72
|
+
# EOF
|