The Gaudi Framework  master (3de0aae8)
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, DataHandleVector
20from GaudiKernel.GaudiHandles import GaudiHandle
21
22from . import Configurable, Configurables
23
24_log = logging.getLogger(__name__)
25is_64bits = sys.maxsize > 2**32
26
27
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(f"C++ type {value!r} not supported")
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(f"cannot merge values {a!r} and {b!r}")
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().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(f"cannot set property {self.name} to {value!r}")
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 f"number expected, got {value!r} in assignment to {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 f"number expected, got {value!r} in assignment to {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 f"value {v} outside limits for {self.cpp_type!r} {self.INT_RANGES[self.cpp_type]}"
246 )
247 return v
248
249
250_IDENTIFIER_RE = r"[a-zA-Z_][a-zA-Z0-9_]*"
251_NS_IDENT_RE = rf"{_IDENTIFIER_RE}(::{_IDENTIFIER_RE})*"
252_COMMA_SEPARATION_RE = r"{exp}(,{exp})*"
253
254
256 __handled_types__ = (
257 "Algorithm",
258 "Auditor",
259 re.compile(rf"AlgTool(:{_COMMA_SEPARATION_RE.format(exp=_NS_IDENT_RE)})?$"),
260 re.compile(rf"Service(:{_COMMA_SEPARATION_RE.format(exp=_NS_IDENT_RE)})?$"),
261 )
262
263 def __init__(self, cpp_type):
264 super().__init__(cpp_type)
265 if ":" in cpp_type:
266 self.cpp_type, self.interfaces = cpp_type.split(":", 1)
267 self.interfaces = set(self.interfaces.split(","))
268 else:
269 self.cpp_type = cpp_type
270 self.interfaces = set()
271
272 def store(self, value):
273 if isinstance(value, Configurable):
274 value.name # make sure the configurable has a name
275 elif isinstance(value, str):
276 # try to map the sring to an existing Configurable
277 if value in Configurable.instances:
278 value = Configurable.instances[value]
279 else:
280 # or create one from type and name
281 if "/" in value:
282 t, n = value.split("/")
283 else:
284 t = n = value
285 value = Configurables.getByType(t).getInstance(n)
286 else:
287 raise TypeError(
288 f"cannot assign {value!r} to {self.name!r}, requested string or {self.cpp_type!r}"
289 )
290 if value.__component_type__ != self.cpp_type:
291 raise TypeError(
292 f"wrong type for {self.name!r}: expected {self.cpp_type!r}, got {value.__component_type__!r}"
293 )
294 try:
295 # if no interface is declared we cannot check
296 if value.__interfaces__:
297 if not self.interfaces.issubset(value.__interfaces__):
298 raise TypeError(
299 f"wrong interfaces for {self.name!r}: required {list(self.interfaces)}"
300 )
301 except AttributeError:
302 pass # no interfaces declared by the configrable, cannot check
303 return value
304
305 def default(self, value):
306 return self.store(value)
307
308
310 """
311 Semantics for component (tool, service) handles. On access, it will create the
312 corresponding Configurable instance and store it in the property.
313 """
314
315 __handled_types__ = ("PrivateToolHandle", "PublicToolHandle", "ServiceHandle")
316
317 def __init__(self, cpp_type):
318 super().__init__(cpp_type)
320
321 def store(self, value):
322 # Configurable: store if correct type
323 if (
324 isinstance(value, Configurable)
325 and value.getGaudiType() == self.handle_type.componentType
326 ):
327 return value
328
329 # Handle: create Configurable
330 elif isinstance(value, GaudiHandle):
331 return (
332 Configurables.getByType(value.getType()).getInstance(value.getName())
333 if value.typeAndName
334 else self.handle_type() # empty handle
335 )
336
337 # Empty: empty Handle
338 elif value is None or value == "":
339 return self.handle_type()
340
341 # String: create Configurable
342 elif isinstance(value, str):
343 tn = value.split("/", maxsplit=1) # type[/name]
344 name = tn[1] if len(tn) == 2 else tn[0]
345 return Configurables.getByType(tn[0]).getInstance(name)
346
347 raise TypeError(f"cannot assign {value!r} ({type(value)}) to {self.name}")
348
349 def default(self, value):
350 return self.store(value)
351
352 def merge(self, b, a):
353 return a.merge(b)
354
355
357 def __opt_repr__(self):
358 return "[" + ",".join(map(repr, self)) + "]"
359
360
362 """Semantics for GaudiHandleArrays."""
363
364 __handled_types__ = (
365 "PrivateToolHandleArray",
366 "PublicToolHandleArray",
367 "ServiceHandleArray",
368 )
369
370 def __init__(self, cpp_type):
371 super().__init__(cpp_type)
373
374 def store(self, value):
375 # flag that the value was explicitly set (see DefaultSemantics)
376 self._is_set = True
377
378 # Create HandleArray from value if needed (it does all the type checking)
379 if not isinstance(value, self.handle_type):
380 value = self.handle_type(value)
381 return value
382
383 def opt_value(self, value):
385 handle.toStringProperty() for handle in value
386 )
387
388 def merge(self, b, a):
389 for comp in b:
390 try:
391 # If a component with that name exists in a, we merge it
392 a.__getitem__(comp.getName()).merge(comp)
393 except IndexError:
394 # Otherwise append it
395 a.append(comp)
396 return a
397
398
400 """
401 Semantics for data handles.
402 """
403
404 __handled_types__ = (re.compile(r"DataObject(Read|Write)Handle<.*>$"),)
405
406 def __init__(self, cpp_type):
407 super().__init__(cpp_type)
408 self._type = next(extract_template_args(cpp_type))
409 self._isCond = False # no specific conditions handle in Gaudi yet
410
411 if cpp_type.startswith("DataObjectReadHandle"):
412 self._mode = "R"
413 elif cpp_type.startswith("DataObjectWriteHandle"):
414 self._mode = "W"
415 else:
416 raise TypeError(f"C++ type {cpp_type} not supported")
417
418 def store(self, value):
419 if isinstance(value, DataHandle):
420 v = value.Path
421 elif isinstance(value, str):
422 v = value
423 else:
424 raise TypeError(
425 f"cannot assign {value!r} ({type(value)}) to {self.name}"
426 ", expected string or DataHandle"
427 )
428 return DataHandle(v, self._mode, self._type, self._isCond)
429
430 def opt_value(self, value):
431 return value.Path
432
433
435 """
436 Return an iterator over the list of template arguments in a C++ type
437 string.
438
439 >>> t = 'map<string, vector<int, allocator<int> >, allocator<v<i>, a<i>> >'
440 >>> list(extract_template_args(t))
441 ['string', 'vector<int, allocator<int> >', 'allocator<v<i>, a<i>>']
442 >>> list(extract_template_args('int'))
443 []
444 """
445 template_level = 0
446 arg_start = -1
447 for p, c in enumerate(cpp_type):
448 if c == ",":
449 if template_level == 1:
450 yield cpp_type[arg_start:p].strip()
451 arg_start = p + 1
452 elif c == "<":
453 template_level += 1
454 if template_level == 1:
455 arg_start = p + 1
456 elif c == ">":
457 template_level -= 1
458 if template_level == 0:
459 yield cpp_type[arg_start:p].strip()
460
461
462class _ListHelper(MutableSequence):
463 def __init__(self, semantics):
464 self.value_semantics = semantics
465 self.default = None
466 self._data = []
467 self.is_dirty = False
468
469 @property
470 def data(self):
471 return self._data if self.is_dirty else self.default
472
473 def __len__(self):
474 return len(self.data)
475
476 def __getitem__(self, key):
477 return self.value_semantics.load(self.data.__getitem__(key))
478
479 def __setitem__(self, key, value):
480 self.is_dirty = True
481 self.data.__setitem__(key, self.value_semantics.store(value))
482
483 def __delitem__(self, key):
484 if not self.is_dirty:
485 raise RuntimeError("cannot remove elements from the default value")
486 self.data.__delitem__(key)
487
488 def __eq__(self, other):
489 return self.data == other
490
491 def insert(self, key, value):
492 self.is_dirty = True
493 self.data.insert(key, self.value_semantics.store(value))
494
495 def append(self, value):
496 self.is_dirty = True
497 self.data.append(self.value_semantics.store(value))
498
499 def extend(self, iterable):
500 self.is_dirty = True
501 self.data.extend(self.value_semantics.store(value) for value in iterable)
502
503 def opt_value(self):
504 return [self.value_semantics.opt_value(item) for item in self.data]
505
506 def __repr__(self):
507 return repr(self.data)
508
509
511 __handled_types__ = (re.compile(r"(std::)?(vector|list)<.*>$"),)
512
513 def __init__(self, cpp_type, valueSem=None):
514 super().__init__(cpp_type)
516 list(extract_template_args(cpp_type))[0]
517 )
518
519 @property
520 def name(self):
521 return self._name
522
523 @name.setter
524 def name(self, value):
525 self._name = value
526 self.value_semantics.name = f"{self._name} element"
527
528 def store(self, value):
529 if not isinstance(value, (list, _ListHelper, tuple)):
530 raise TypeError(
531 f"list or tuple expected, got {value!r} in assignment to {self.name}"
532 )
533 new_value = _ListHelper(self.value_semantics)
534 new_value.extend(value)
535 return new_value
536
537 def default(self, value):
538 new_value = _ListHelper(self.value_semantics)
539 new_value.default = value
540 return new_value
541
542 def opt_value(self, value):
543 """
544 Option string version of value.
545 """
546 if not isinstance(value, _ListHelper):
547 value = self.default(value)
548 return value.opt_value()
549
550
552 """Sequence semantics whose elements are data handles."""
553
554 __handled_types__ = (
555 re.compile(r"Gaudi::DataHandleVector<DataObject(Read|Write)Handle,.*>$"),
556 )
557
558 def __init__(self, cpp_type):
559 if not self.__handled_types__[0].match(cpp_type):
560 raise TypeError(f"C++ type {cpp_type!r} not supported")
561 handle_type, value_type = extract_template_args(cpp_type)
562 super().__init__(
563 cpp_type, valueSem=DataHandleSemantics(f"{handle_type}<{value_type}>")
564 )
565
566 def store(self, value):
567 if isinstance(value, DataHandleVector):
568 value = value.paths()
569 handles = super().store(value)
570 return DataHandleVector(
571 [handle.Path for handle in handles],
572 self.value_semantics._mode,
573 self.value_semantics._type,
574 self.value_semantics._isCond,
575 )
576
577 def default(self, value):
578 return self.store(value)
579
580 def opt_value(self, value):
581 return value.paths()
582
583 def merge(self, b, a):
584 paths = a.paths()
585 for handle in b:
586 path = self.value_semantics.store(handle).Path
587 if path not in paths:
588 paths.append(path)
589 return a
590
591
592class _SetHelper(MutableSet):
593 def __init__(self, semantics):
594 self.value_semantics = semantics
595 self.default = set() # cannot use None due to the way __ior__ is implemented
596 self._data = set()
597 self.is_dirty = False
598
599 # Aliases to match builtin `set`
600 union = MutableSet.__ior__
601 update = MutableSet.__ior__
602 intersection = MutableSet.__iand__
603 difference = MutableSet.__isub__
604 symmetric_difference = MutableSet.__ixor__
605
606 @property
607 def data(self):
608 return self._data if self.is_dirty else self.default
609
610 def __len__(self):
611 return len(self.data)
612
613 def __contains__(self, value):
614 return self.value_semantics.store(value) in self.data
615
616 def __eq__(self, other):
617 return self.data == other
618
619 def __iter__(self):
620 for value in self.data:
621 yield self.value_semantics.load(value)
622
623 def add(self, value):
624 self.is_dirty = True
625 self.data.add(self.value_semantics.store(value))
626
627 def discard(self, value):
628 if not self.is_dirty:
629 raise RuntimeError("cannot remove elements from the default value")
630 self.data.discard(value)
631
632 def pop(self):
633 if not self.is_dirty:
634 raise RuntimeError("cannot remove elements from the default value")
635 return self.data.pop()
636
637 def opt_value(self):
638 return set(self.value_semantics.opt_value(item) for item in self.data)
639
640 def __repr__(self):
641 if self.data:
642 # sort into list but print as set to get reproducible repr
643 return "{" + repr(sorted(self.data))[1:-1] + "}"
644 else:
645 return "set()"
646
647
649 """Merge semantics for (unordered) sets."""
650
651 __handled_types__ = (re.compile(r"(std::)?unordered_set<.*>$"),)
652
653 def __init__(self, cpp_type, valueSem=None):
654 super().__init__(cpp_type)
656 list(extract_template_args(cpp_type))[0]
657 )
658
659 @property
660 def name(self):
661 return self._name
662
663 @name.setter
664 def name(self, value):
665 self._name = value
666 self.value_semantics.name = f"{self._name} element"
667
668 def store(self, value):
669 # We support assignment from list for backwards compatibility
670 if not isinstance(value, (set, _SetHelper, list, _ListHelper)):
671 raise TypeError(f"set expected, got {value!r} in assignment to {self.name}")
672
673 new_value = _SetHelper(self.value_semantics)
674 new_value |= value
675 return new_value
676
677 def default(self, value):
678 new_value = _SetHelper(self.value_semantics)
679 new_value.default = value
680 return new_value
681
682 def opt_value(self, value):
683 """
684 Option string version of value.
685 """
686 if not isinstance(value, _SetHelper):
687 value = self.default(value)
688 return value.opt_value()
689
690 def merge(self, bb, aa):
691 aa |= bb
692 return aa
693
694
696 """
697 Extend the sequence-semantics with a merge-method to behave like a
698 OrderedSet: Values are unique but the order is maintained.
699 Use 'OrderedSet<T>' as fifth parameter of the Gaudi::Property<T> constructor
700 to invoke this merging method. Also applies to std::set.
701 """
702
703 __handled_types__ = (
704 re.compile(r"(std::)?set<.*>$"),
705 re.compile(r"^OrderedSet<.*>$"),
706 )
707
708 def __init__(self, cpp_type):
709 super().__init__(cpp_type)
710
711 def merge(self, bb, aa):
712 for b in bb:
713 if b not in aa:
714 aa.append(b)
715 return aa
716
717
718class _DictHelper(MutableMapping):
719 def __init__(self, key_semantics, value_semantics):
720 self.key_semantics = key_semantics
721 self.value_semantics = value_semantics
722 self.default = None
723 self._data = {}
724 self.is_dirty = False
725
726 @property
727 def data(self):
728 return self._data if self.is_dirty else self.default
729
730 def __len__(self):
731 return len(self.data)
732
733 def __getitem__(self, key):
734 return self.value_semantics.load(
735 self.data.__getitem__(self.key_semantics.store(key))
736 )
737
738 def __setitem__(self, key, value):
739 self.is_dirty = True
740 self.data.__setitem__(
741 self.key_semantics.store(key), self.value_semantics.store(value)
742 )
743
744 def __delitem__(self, key):
745 if not self.is_dirty:
746 raise RuntimeError("cannot remove elements from the default value")
747 self.data.__delitem__(self.key_semantics.store(key))
748
749 def __iter__(self):
750 for key in self.data:
751 yield self.key_semantics.load(key)
752
753 def keys(self):
754 return list(self)
755
756 def items(self):
757 for key, value in self.data.items():
758 yield (self.key_semantics.load(key), self.value_semantics.load(value))
759
760 def values(self):
761 for value in self.data.values():
762 yield self.value_semantics.load(value)
763
764 def __contains__(self, key):
765 return self.key_semantics.store(key) in self.data
766
767 def get(self, key, default=None):
768 key = self.key_semantics.store(key)
769 if key in self.data:
770 return self.value_semantics.load(self.data[key])
771 return default
772
773 # __contains__, , get, __eq__, __ne__
774 # popitem, clear, setdefault
775
776 def update(self, otherMap):
777 self.is_dirty = True
778 for key, value in otherMap.items():
779 self.data[self.key_semantics.store(key)] = self.value_semantics.store(value)
780
781 def opt_value(self):
782 return {
783 self.key_semantics.opt_value(key): self.value_semantics.opt_value(value)
784 for key, value in self.data.items()
785 }
786
787 def __repr__(self):
788 return repr(self.data)
789
790
792 __handled_types__ = (re.compile(r"(std::)?(unordered_)?map<.*>$"),)
793
794 def __init__(self, cpp_type):
795 super().__init__(cpp_type)
796 template_args = list(extract_template_args(cpp_type))
797 self.key_semantics = getSemanticsFor(template_args[0])
798 self.value_semantics = getSemanticsFor(template_args[1])
799
800 @property
801 def name(self):
802 return self._name
803
804 @name.setter
805 def name(self, value):
806 self._name = value
807 self.key_semantics.name = f"{self._name} key"
808 self.value_semantics.name = f"{self._name} value"
809
810 def store(self, value):
811 # No explicit type checking as anything else than dict fails in update call
812 new_value = _DictHelper(self.key_semantics, self.value_semantics)
813 new_value.update(value)
814 return new_value
815
816 def default(self, value):
817 new_value = _DictHelper(self.key_semantics, self.value_semantics)
818 new_value.default = value
819 return new_value
820
821 def opt_value(self, value):
822 """
823 Option string version of value.
824 """
825 if not isinstance(value, _DictHelper):
826 value = self.default(value)
827 return value.opt_value()
828
829 def merge(self, a, b):
830 """Merge two maps. Throw ValueError if there are conflicting key/value pairs."""
831
832 # Optimization for most common case
833 if a == b:
834 return a
835
836 for k, v in b.items():
837 try:
838 va = a[k]
839 except KeyError:
840 a[k] = v
841 else:
842 if va != v:
843 raise ValueError(
844 f"conflicting values in map for key {k}: {v} and {va}"
845 )
846 return a
847
848
849SEMANTICS = [
850 c
851 for c in globals().values()
852 if isinstance(c, type)
853 and issubclass(c, PropertySemantics)
854 and c not in (PropertySemantics, DefaultSemantics)
855]
856
857
858def getSemanticsFor(cpp_type, strict=False):
859 """Return semantics for given type. If no type-specific semantics can be found
860 return DefaultSemantics. In strict mode, raise a TypeError instead.
861 """
862
863 for semantics in SEMANTICS:
864 try:
865 return semantics(cpp_type)
866 except TypeError:
867 pass
868
869 if strict:
870 raise TypeError(f"No semantics found for {cpp_type}")
871
872 return DefaultSemantics(cpp_type)
__init__(self, key_semantics, value_semantics)
Definition semantics.py:719
get(self, key, default=None)
Definition semantics.py:767
__init__(self, data, explicitly_set)
Definition semantics.py:135
__init__(self, cpp_type, valueSem=None)
Definition semantics.py:513
__init__(self, cpp_type, valueSem=None)
Definition semantics.py:653
getSemanticsFor(cpp_type, strict=False)
Definition semantics.py:858
extract_template_args(cpp_type)
Definition semantics.py:434