The Gaudi Framework  master (76fc3702)
Loading...
Searching...
No Matches
semantics.py
Go to the documentation of this file.
11import copy
12import json
13import logging
14import re
15import sys
16from collections.abc import MutableMapping, MutableSequence, MutableSet
17
19from GaudiKernel.DataHandle import DataHandle
20from GaudiKernel.GaudiHandles import GaudiHandle
21
22from . import Configurable, Configurables
23
24_log = logging.getLogger(__name__)
25is_64bits = sys.maxsize > 2**32
26
27
28class PropertySemantics(object):
29 """
30 Basic property semantics implementation, with no validation/transformation.
31
32 Not to be used directly for any actual property, use only specializations.
33 """
34
35 __handled_types__ = ()
36
37 def __init__(self, cpp_type):
38 self._name = None
39 self.cpp_type = cpp_type
40
41 @property
42 def name(self):
43 return self._name
44
45 @name.setter
46 def name(self, value):
47 self._name = value
48
49 @property
50 def cpp_type(self):
51 return self._cpp_type
52
53 @cpp_type.setter
54 def cpp_type(self, value):
55 if not any(
56 h.match(value) if hasattr(h, "match") else h == value
57 for h in self.__handled_types__
58 ):
59 raise TypeError("C++ type {!r} not supported".format(value))
60 self._cpp_type = value
61
62 def load(self, value):
63 """
64 Transformation for data when reading the property.
65 """
66 return value
67
68 def store(self, value):
69 """
70 Validation/transformation of the data to be stored.
71 """
72 return value
73
74 def is_set(self, value):
75 """
76 Allow overriding the definition of "is set" if we need helper types.
77 """
78 return True
79
80 def opt_value(self, value):
81 """
82 Option string version of value.
83 """
84 if hasattr(value, "__opt_value__"):
85 return value.__opt_value__()
86 return value
87
88 def merge(self, a, b):
89 """
90 "Merge" two values.
91
92 Used when merging two Configurable instances, by default just ensure
93 the two values do not conflict, but it can be overridden in
94 derived semantics to, for example, append to the two lists.
95 """
96 if self.store(a) != self.store(b):
97 raise ValueError("cannot merge values %r and %r" % (a, b))
98 return a
99
100
102 """
103 Special semantics that makes a deep copy of the default value on first access
104 and considers a property set if its value is different from the default.
105
106 This semantics is meant to be used whenever there is no specific semantic
107 (with proper change detection) implemented for a type.
108 """
109
110 __handled_types__ = (re.compile(r".*"),)
111
112 def default(self, value):
113 # remember the default value we got and return a copy
114 self._default = value
115 self._is_set = False
116 return copy.deepcopy(value)
117
118 def store(self, value):
119 # flag that the value was explicitly set
120 self._is_set = True
121 return super(DefaultSemantics, self).store(value)
122
123 def is_set(self, value):
124 try:
125 # we assume the property was set if it was changed
126 return self._is_set or self._default != value
127 except AttributeError:
128 # either self._is_set or self._default is not defined,
129 # so the value was not explicitly set nor modified
130 # from the default
131 return False
132
133
135 def __init__(self, data, explicitly_set):
136 self.data = data
137 self.default = copy.deepcopy(data)
138 self.explicitly_set = explicitly_set
139
140 def __eq__(self, other):
141 return isinstance(other, _JSONValue) and self.data == other.data
142
143
144class _JSONOption(str):
145 def __opt_repr__(self):
146 return str(self)
147
148
150 __handled_types__ = (
151 "nlohmann::json",
152 re.compile(r"nlohmann::(?:json_abi[^:]*::)?basic_json<.*>$"),
153 )
154
155 @staticmethod
156 def _normalize(value):
157 return json.loads(json.dumps(value, allow_nan=False))
158
159 def default(self, value):
160 data = json.loads(value) if isinstance(value, str) else self._normalize(value)
161 return _JSONValue(data, False)
162
163 def load(self, value):
164 return value.data
165
166 def store(self, value):
167 return _JSONValue(self._normalize(value), True)
168
169 def is_set(self, value):
170 return value.explicitly_set or value.data != value.default
171
172 def opt_value(self, value):
173 if not isinstance(value, _JSONValue):
174 value = self.default(value)
175 return _JSONOption(
176 json.dumps(
177 value.data, allow_nan=False, separators=(",", ":"), sort_keys=True
178 )
179 )
180
181
183 __handled_types__ = ("std::string",)
184
185 def store(self, value):
186 if not isinstance(value, str):
187 raise TypeError("cannot set property {} to {!r}".format(self.name, value))
188 return value
189
190
192 __handled_types__ = ("bool",)
193
194 def store(self, value):
195 return bool(value)
196
197
199 __handled_types__ = ("float", "double")
200
201 def store(self, value):
202 from numbers import Number
203
204 if not isinstance(value, Number):
205 raise TypeError(
206 "number expected, got {!r} in assignment to {}".format(value, self.name)
207 )
208 return float(value)
209
210
212 # dictionary generated with tools/print_limits.cpp
213 INT_RANGES = {
214 "signed char": (-128, 127),
215 "short": (-32768, 32767),
216 "int": (-2147483648, 2147483647),
217 "long": (
218 (-9223372036854775808, 9223372036854775807)
219 if is_64bits
220 else (-2147483648, 2147483647)
221 ),
222 "long long": (-9223372036854775808, 9223372036854775807),
223 "unsigned char": (0, 255),
224 "unsigned short": (0, 65535),
225 "unsigned int": (0, 4294967295),
226 "unsigned long": (0, 18446744073709551615 if is_64bits else 4294967295),
227 "unsigned long long": (0, 18446744073709551615),
228 }
229
230 __handled_types__ = tuple(INT_RANGES)
231
232 def store(self, value):
233 from numbers import Number
234
235 if not isinstance(value, Number):
236 raise TypeError(
237 "number expected, got {!r} in assignment to {}".format(value, self.name)
238 )
239 v = int(value)
240 if v != value:
241 _log.warning("converted %s to %d in assignment to %s", value, v, self.name)
242 min_value, max_value = self.INT_RANGES[self.cpp_type]
243 if v < min_value or v > max_value:
244 raise ValueError(
245 "value {} outside limits for {!r} {}".format(
246 v, self.cpp_type, self.INT_RANGES[self.cpp_type]
247 )
248 )
249 return v
250
251
252_IDENTIFIER_RE = r"[a-zA-Z_][a-zA-Z0-9_]*"
253_NS_IDENT_RE = r"{ident}(::{ident})*".format(ident=_IDENTIFIER_RE)
254_COMMA_SEPARATION_RE = r"{exp}(,{exp})*"
255
256
258 __handled_types__ = (
259 "Algorithm",
260 "Auditor",
261 re.compile(
262 r"AlgTool(:{})?$".format(_COMMA_SEPARATION_RE.format(exp=_NS_IDENT_RE))
263 ),
264 re.compile(
265 r"Service(:{})?$".format(_COMMA_SEPARATION_RE.format(exp=_NS_IDENT_RE))
266 ),
267 )
268
269 def __init__(self, cpp_type):
270 super(ComponentSemantics, self).__init__(cpp_type)
271 if ":" in cpp_type:
272 self.cpp_type, self.interfaces = cpp_type.split(":", 1)
273 self.interfaces = set(self.interfaces.split(","))
274 else:
275 self.cpp_type = cpp_type
276 self.interfaces = set()
277
278 def store(self, value):
279 if isinstance(value, Configurable):
280 value.name # make sure the configurable has a name
281 elif isinstance(value, str):
282 # try to map the sring to an existing Configurable
283 if value in Configurable.instances:
284 value = Configurable.instances[value]
285 else:
286 # or create one from type and name
287 if "/" in value:
288 t, n = value.split("/")
289 else:
290 t = n = value
291 value = Configurables.getByType(t).getInstance(n)
292 else:
293 raise TypeError(
294 "cannot assign {!r} to {!r}, requested string or {!r}".format(
295 value, self.name, self.cpp_type
296 )
297 )
298 if value.__component_type__ != self.cpp_type:
299 raise TypeError(
300 "wrong type for {!r}: expected {!r}, got {!r}".format(
301 self.name, self.cpp_type, value.__component_type__
302 )
303 )
304 try:
305 # if no interface is declared we cannot check
306 if value.__interfaces__:
307 if not self.interfaces.issubset(value.__interfaces__):
308 raise TypeError(
309 "wrong interfaces for {!r}: required {}".format(
310 self.name, list(self.interfaces)
311 )
312 )
313 except AttributeError:
314 pass # no interfaces declared by the configrable, cannot check
315 return value
316
317 def default(self, value):
318 return self.store(value)
319
320
322 """
323 Semantics for component (tool, service) handles. On access, it will create the
324 corresponding Configurable instance and store it in the property.
325 """
326
327 __handled_types__ = ("PrivateToolHandle", "PublicToolHandle", "ServiceHandle")
328
329 def __init__(self, cpp_type):
330 super().__init__(cpp_type)
332
333 def store(self, value):
334 # Configurable: store if correct type
335 if (
336 isinstance(value, Configurable)
337 and value.getGaudiType() == self.handle_type.componentType
338 ):
339 return value
340
341 # Handle: create Configurable
342 elif isinstance(value, GaudiHandle):
343 return (
344 Configurables.getByType(value.getType()).getInstance(value.getName())
345 if value.typeAndName
346 else self.handle_type() # empty handle
347 )
348
349 # Empty: empty Handle
350 elif value is None or value == "":
351 return self.handle_type()
352
353 # String: create Configurable
354 elif isinstance(value, str):
355 tn = value.split("/", maxsplit=1) # type[/name]
356 name = tn[1] if len(tn) == 2 else tn[0]
357 return Configurables.getByType(tn[0]).getInstance(name)
358
359 raise TypeError(f"cannot assign {value!r} ({type(value)}) to {self.name}")
360
361 def default(self, value):
362 return self.store(value)
363
364 def merge(self, b, a):
365 return a.merge(b)
366
367
369 def __opt_repr__(self):
370 return "[" + ",".join(map(repr, self)) + "]"
371
372
374 """Semantics for GaudiHandleArrays."""
375
376 __handled_types__ = (
377 "PrivateToolHandleArray",
378 "PublicToolHandleArray",
379 "ServiceHandleArray",
380 )
381
382 def __init__(self, cpp_type):
383 super().__init__(cpp_type)
385
386 def store(self, value):
387 # flag that the value was explicitly set (see DefaultSemantics)
388 self._is_set = True
389
390 # Create HandleArray from value if needed (it does all the type checking)
391 if not isinstance(value, self.handle_type):
392 value = self.handle_type(value)
393 return value
394
395 def opt_value(self, value):
397 handle.toStringProperty() for handle in value
398 )
399
400 def merge(self, b, a):
401 for comp in b:
402 try:
403 # If a component with that name exists in a, we merge it
404 a.__getitem__(comp.getName()).merge(comp)
405 except IndexError:
406 # Otherwise append it
407 a.append(comp)
408 return a
409
410
412 """
413 Semantics for data handles.
414 """
415
416 __handled_types__ = (re.compile(r"DataObject(Read|Write)Handle<.*>$"),)
417
418 def __init__(self, cpp_type):
419 super().__init__(cpp_type)
420 self._type = next(extract_template_args(cpp_type))
421 self._isCond = False # no specific conditions handle in Gaudi yet
422
423 if cpp_type.startswith("DataObjectReadHandle"):
424 self._mode = "R"
425 elif cpp_type.startswith("DataObjectWriteHandle"):
426 self._mode = "W"
427 else:
428 raise TypeError(f"C++ type {cpp_type} not supported")
429
430 def store(self, value):
431 if isinstance(value, DataHandle):
432 v = value.Path
433 elif isinstance(value, str):
434 v = value
435 else:
436 raise TypeError(
437 f"cannot assign {value!r} ({type(value)}) to {self.name}"
438 ", expected string or DataHandle"
439 )
440 return DataHandle(v, self._mode, self._type, self._isCond)
441
442 def opt_value(self, value):
443 return value.Path
444
445
447 """
448 Return an iterator over the list of template arguments in a C++ type
449 string.
450
451 >>> t = 'map<string, vector<int, allocator<int> >, allocator<v<i>, a<i>> >'
452 >>> list(extract_template_args(t))
453 ['string', 'vector<int, allocator<int> >', 'allocator<v<i>, a<i>>']
454 >>> list(extract_template_args('int'))
455 []
456 """
457 template_level = 0
458 arg_start = -1
459 for p, c in enumerate(cpp_type):
460 if c == ",":
461 if template_level == 1:
462 yield cpp_type[arg_start:p].strip()
463 arg_start = p + 1
464 elif c == "<":
465 template_level += 1
466 if template_level == 1:
467 arg_start = p + 1
468 elif c == ">":
469 template_level -= 1
470 if template_level == 0:
471 yield cpp_type[arg_start:p].strip()
472
473
474class _ListHelper(MutableSequence):
475 def __init__(self, semantics):
476 self.value_semantics = semantics
477 self.default = None
478 self._data = []
479 self.is_dirty = False
480
481 @property
482 def data(self):
483 return self._data if self.is_dirty else self.default
484
485 def __len__(self):
486 return len(self.data)
487
488 def __getitem__(self, key):
489 return self.value_semantics.load(self.data.__getitem__(key))
490
491 def __setitem__(self, key, value):
492 self.is_dirty = True
493 self.data.__setitem__(key, self.value_semantics.store(value))
494
495 def __delitem__(self, key):
496 if not self.is_dirty:
497 raise RuntimeError("cannot remove elements from the default value")
498 self.data.__delitem__(key)
499
500 def __eq__(self, other):
501 return self.data == other
502
503 def insert(self, key, value):
504 self.is_dirty = True
505 self.data.insert(key, self.value_semantics.store(value))
506
507 def append(self, value):
508 self.is_dirty = True
509 self.data.append(self.value_semantics.store(value))
510
511 def extend(self, iterable):
512 self.is_dirty = True
513 self.data.extend(self.value_semantics.store(value) for value in iterable)
514
515 def opt_value(self):
516 return [self.value_semantics.opt_value(item) for item in self.data]
517
518 def __repr__(self):
519 return repr(self.data)
520
521
523 __handled_types__ = (re.compile(r"(std::)?(vector|list)<.*>$"),)
524
525 def __init__(self, cpp_type, valueSem=None):
526 super(SequenceSemantics, self).__init__(cpp_type)
528 list(extract_template_args(cpp_type))[0]
529 )
530
531 @property
532 def name(self):
533 return self._name
534
535 @name.setter
536 def name(self, value):
537 self._name = value
538 self.value_semantics.name = "{} element".format(self._name)
539
540 def store(self, value):
541 if not isinstance(value, (list, _ListHelper, tuple)):
542 raise TypeError(
543 "list or tuple expected, got {!r} in assignment to {}".format(
544 value, self.name
545 )
546 )
547 new_value = _ListHelper(self.value_semantics)
548 new_value.extend(value)
549 return new_value
550
551 def default(self, value):
552 new_value = _ListHelper(self.value_semantics)
553 new_value.default = value
554 return new_value
555
556 def opt_value(self, value):
557 """
558 Option string version of value.
559 """
560 if not isinstance(value, _ListHelper):
561 value = self.default(value)
562 return value.opt_value()
563
564
565class _SetHelper(MutableSet):
566 def __init__(self, semantics):
567 self.value_semantics = semantics
568 self.default = set() # cannot use None due to the way __ior__ is implemented
569 self._data = set()
570 self.is_dirty = False
571
572 # Aliases to match builtin `set`
573 union = MutableSet.__ior__
574 update = MutableSet.__ior__
575 intersection = MutableSet.__iand__
576 difference = MutableSet.__isub__
577 symmetric_difference = MutableSet.__ixor__
578
579 @property
580 def data(self):
581 return self._data if self.is_dirty else self.default
582
583 def __len__(self):
584 return len(self.data)
585
586 def __contains__(self, value):
587 return self.value_semantics.store(value) in self.data
588
589 def __eq__(self, other):
590 return self.data == other
591
592 def __iter__(self):
593 for value in self.data:
594 yield self.value_semantics.load(value)
595
596 def add(self, value):
597 self.is_dirty = True
598 self.data.add(self.value_semantics.store(value))
599
600 def discard(self, value):
601 if not self.is_dirty:
602 raise RuntimeError("cannot remove elements from the default value")
603 self.data.discard(value)
604
605 def pop(self):
606 if not self.is_dirty:
607 raise RuntimeError("cannot remove elements from the default value")
608 return self.data.pop()
609
610 def opt_value(self):
611 return set(self.value_semantics.opt_value(item) for item in self.data)
612
613 def __repr__(self):
614 if self.data:
615 # sort into list but print as set to get reproducible repr
616 return "{" + repr(sorted(self.data))[1:-1] + "}"
617 else:
618 return "set()"
619
620
622 """Merge semantics for (unordered) sets."""
623
624 __handled_types__ = (re.compile(r"(std::)?unordered_set<.*>$"),)
625
626 def __init__(self, cpp_type, valueSem=None):
627 super(SetSemantics, self).__init__(cpp_type)
629 list(extract_template_args(cpp_type))[0]
630 )
631
632 @property
633 def name(self):
634 return self._name
635
636 @name.setter
637 def name(self, value):
638 self._name = value
639 self.value_semantics.name = "{} element".format(self._name)
640
641 def store(self, value):
642 # We support assignment from list for backwards compatibility
643 if not isinstance(value, (set, _SetHelper, list, _ListHelper)):
644 raise TypeError(
645 "set expected, got {!r} in assignment to {}".format(value, self.name)
646 )
647
648 new_value = _SetHelper(self.value_semantics)
649 new_value |= value
650 return new_value
651
652 def default(self, value):
653 new_value = _SetHelper(self.value_semantics)
654 new_value.default = value
655 return new_value
656
657 def opt_value(self, value):
658 """
659 Option string version of value.
660 """
661 if not isinstance(value, _SetHelper):
662 value = self.default(value)
663 return value.opt_value()
664
665 def merge(self, bb, aa):
666 aa |= bb
667 return aa
668
669
671 """
672 Extend the sequence-semantics with a merge-method to behave like a
673 OrderedSet: Values are unique but the order is maintained.
674 Use 'OrderedSet<T>' as fifth parameter of the Gaudi::Property<T> constructor
675 to invoke this merging method. Also applies to std::set.
676 """
677
678 __handled_types__ = (
679 re.compile(r"(std::)?set<.*>$"),
680 re.compile(r"^OrderedSet<.*>$"),
681 )
682
683 def __init__(self, cpp_type):
684 super(OrderedSetSemantics, self).__init__(cpp_type)
685
686 def merge(self, bb, aa):
687 for b in bb:
688 if b not in aa:
689 aa.append(b)
690 return aa
691
692
693class _DictHelper(MutableMapping):
694 def __init__(self, key_semantics, value_semantics):
695 self.key_semantics = key_semantics
696 self.value_semantics = value_semantics
697 self.default = None
698 self._data = {}
699 self.is_dirty = False
700
701 @property
702 def data(self):
703 return self._data if self.is_dirty else self.default
704
705 def __len__(self):
706 return len(self.data)
707
708 def __getitem__(self, key):
709 return self.value_semantics.load(
710 self.data.__getitem__(self.key_semantics.store(key))
711 )
712
713 def __setitem__(self, key, value):
714 self.is_dirty = True
715 self.data.__setitem__(
716 self.key_semantics.store(key), self.value_semantics.store(value)
717 )
718
719 def __delitem__(self, key):
720 if not self.is_dirty:
721 raise RuntimeError("cannot remove elements from the default value")
722 self.data.__delitem__(self.key_semantics.store(key))
723
724 def __iter__(self):
725 for key in self.data:
726 yield self.key_semantics.load(key)
727
728 def keys(self):
729 return list(self)
730
731 def items(self):
732 for key, value in self.data.items():
733 yield (self.key_semantics.load(key), self.value_semantics.load(value))
734
735 def values(self):
736 for value in self.data.values():
737 yield self.value_semantics.load(value)
738
739 def __contains__(self, key):
740 return self.key_semantics.store(key) in self.data
741
742 def get(self, key, default=None):
743 key = self.key_semantics.store(key)
744 if key in self.data:
745 return self.value_semantics.load(self.data[key])
746 return default
747
748 # __contains__, , get, __eq__, __ne__
749 # popitem, clear, setdefault
750
751 def update(self, otherMap):
752 self.is_dirty = True
753 for key, value in otherMap.items():
754 self.data[self.key_semantics.store(key)] = self.value_semantics.store(value)
755
756 def opt_value(self):
757 return {
758 self.key_semantics.opt_value(key): self.value_semantics.opt_value(value)
759 for key, value in self.data.items()
760 }
761
762 def __repr__(self):
763 return repr(self.data)
764
765
767 __handled_types__ = (re.compile(r"(std::)?(unordered_)?map<.*>$"),)
768
769 def __init__(self, cpp_type):
770 super(MappingSemantics, self).__init__(cpp_type)
771 template_args = list(extract_template_args(cpp_type))
772 self.key_semantics = getSemanticsFor(template_args[0])
773 self.value_semantics = getSemanticsFor(template_args[1])
774
775 @property
776 def name(self):
777 return self._name
778
779 @name.setter
780 def name(self, value):
781 self._name = value
782 self.key_semantics.name = "{} key".format(self._name)
783 self.value_semantics.name = "{} value".format(self._name)
784
785 def store(self, value):
786 # No explicit type checking as anything else than dict fails in update call
787 new_value = _DictHelper(self.key_semantics, self.value_semantics)
788 new_value.update(value)
789 return new_value
790
791 def default(self, value):
792 new_value = _DictHelper(self.key_semantics, self.value_semantics)
793 new_value.default = value
794 return new_value
795
796 def opt_value(self, value):
797 """
798 Option string version of value.
799 """
800 if not isinstance(value, _DictHelper):
801 value = self.default(value)
802 return value.opt_value()
803
804 def merge(self, a, b):
805 """Merge two maps. Throw ValueError if there are conflicting key/value pairs."""
806
807 # Optimization for most common case
808 if a == b:
809 return a
810
811 for k, v in b.items():
812 try:
813 va = a[k]
814 except KeyError:
815 a[k] = v
816 else:
817 if va != v:
818 raise ValueError(
819 f"conflicting values in map for key {k}: {v} and {va}"
820 )
821 return a
822
823
824SEMANTICS = [
825 c
826 for c in globals().values()
827 if isinstance(c, type)
828 and issubclass(c, PropertySemantics)
829 and c not in (PropertySemantics, DefaultSemantics)
830]
831
832
833def getSemanticsFor(cpp_type, strict=False):
834 """Return semantics for given type. If no type-specific semantics can be found
835 return DefaultSemantics. In strict mode, raise a TypeError instead.
836 """
837
838 for semantics in SEMANTICS:
839 try:
840 return semantics(cpp_type)
841 except TypeError:
842 pass
843
844 if strict:
845 raise TypeError(f"No semantics found for {cpp_type}")
846
847 return DefaultSemantics(cpp_type)
GAUDI_API std::string format(const char *,...)
MsgStream format utility "a la sprintf(...)".
Definition MsgStream.cpp:93
__init__(self, key_semantics, value_semantics)
Definition semantics.py:694
get(self, key, default=None)
Definition semantics.py:742
__init__(self, data, explicitly_set)
Definition semantics.py:135
__init__(self, cpp_type, valueSem=None)
Definition semantics.py:525
__init__(self, cpp_type, valueSem=None)
Definition semantics.py:626
getSemanticsFor(cpp_type, strict=False)
Definition semantics.py:833
extract_template_args(cpp_type)
Definition semantics.py:446