The Gaudi Framework  master (bb4415cc)
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
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
566 """Sequence semantics whose elements are data handles."""
567
568 __handled_types__ = (
569 re.compile(r"Gaudi::DataHandleVector<DataObject(Read|Write)Handle,.*>$"),
570 )
571
572 def __init__(self, cpp_type):
573 if not self.__handled_types__[0].match(cpp_type):
574 raise TypeError(f"C++ type {cpp_type!r} not supported")
575 handle_type, value_type = extract_template_args(cpp_type)
576 super().__init__(
577 cpp_type, valueSem=DataHandleSemantics(f"{handle_type}<{value_type}>")
578 )
579
580 def store(self, value):
581 if isinstance(value, DataHandleVector):
582 value = value.paths()
583 handles = super().store(value)
584 return DataHandleVector(
585 [handle.Path for handle in handles],
586 self.value_semantics._mode,
587 self.value_semantics._type,
588 self.value_semantics._isCond,
589 )
590
591 def default(self, value):
592 return self.store(value)
593
594 def opt_value(self, value):
595 return value.paths()
596
597 def merge(self, b, a):
598 paths = a.paths()
599 for handle in b:
600 path = self.value_semantics.store(handle).Path
601 if path not in paths:
602 paths.append(path)
603 return a
604
605
606class _SetHelper(MutableSet):
607 def __init__(self, semantics):
608 self.value_semantics = semantics
609 self.default = set() # cannot use None due to the way __ior__ is implemented
610 self._data = set()
611 self.is_dirty = False
612
613 # Aliases to match builtin `set`
614 union = MutableSet.__ior__
615 update = MutableSet.__ior__
616 intersection = MutableSet.__iand__
617 difference = MutableSet.__isub__
618 symmetric_difference = MutableSet.__ixor__
619
620 @property
621 def data(self):
622 return self._data if self.is_dirty else self.default
623
624 def __len__(self):
625 return len(self.data)
626
627 def __contains__(self, value):
628 return self.value_semantics.store(value) in self.data
629
630 def __eq__(self, other):
631 return self.data == other
632
633 def __iter__(self):
634 for value in self.data:
635 yield self.value_semantics.load(value)
636
637 def add(self, value):
638 self.is_dirty = True
639 self.data.add(self.value_semantics.store(value))
640
641 def discard(self, value):
642 if not self.is_dirty:
643 raise RuntimeError("cannot remove elements from the default value")
644 self.data.discard(value)
645
646 def pop(self):
647 if not self.is_dirty:
648 raise RuntimeError("cannot remove elements from the default value")
649 return self.data.pop()
650
651 def opt_value(self):
652 return set(self.value_semantics.opt_value(item) for item in self.data)
653
654 def __repr__(self):
655 if self.data:
656 # sort into list but print as set to get reproducible repr
657 return "{" + repr(sorted(self.data))[1:-1] + "}"
658 else:
659 return "set()"
660
661
663 """Merge semantics for (unordered) sets."""
664
665 __handled_types__ = (re.compile(r"(std::)?unordered_set<.*>$"),)
666
667 def __init__(self, cpp_type, valueSem=None):
668 super(SetSemantics, self).__init__(cpp_type)
670 list(extract_template_args(cpp_type))[0]
671 )
672
673 @property
674 def name(self):
675 return self._name
676
677 @name.setter
678 def name(self, value):
679 self._name = value
680 self.value_semantics.name = "{} element".format(self._name)
681
682 def store(self, value):
683 # We support assignment from list for backwards compatibility
684 if not isinstance(value, (set, _SetHelper, list, _ListHelper)):
685 raise TypeError(
686 "set expected, got {!r} in assignment to {}".format(value, self.name)
687 )
688
689 new_value = _SetHelper(self.value_semantics)
690 new_value |= value
691 return new_value
692
693 def default(self, value):
694 new_value = _SetHelper(self.value_semantics)
695 new_value.default = value
696 return new_value
697
698 def opt_value(self, value):
699 """
700 Option string version of value.
701 """
702 if not isinstance(value, _SetHelper):
703 value = self.default(value)
704 return value.opt_value()
705
706 def merge(self, bb, aa):
707 aa |= bb
708 return aa
709
710
712 """
713 Extend the sequence-semantics with a merge-method to behave like a
714 OrderedSet: Values are unique but the order is maintained.
715 Use 'OrderedSet<T>' as fifth parameter of the Gaudi::Property<T> constructor
716 to invoke this merging method. Also applies to std::set.
717 """
718
719 __handled_types__ = (
720 re.compile(r"(std::)?set<.*>$"),
721 re.compile(r"^OrderedSet<.*>$"),
722 )
723
724 def __init__(self, cpp_type):
725 super(OrderedSetSemantics, self).__init__(cpp_type)
726
727 def merge(self, bb, aa):
728 for b in bb:
729 if b not in aa:
730 aa.append(b)
731 return aa
732
733
734class _DictHelper(MutableMapping):
735 def __init__(self, key_semantics, value_semantics):
736 self.key_semantics = key_semantics
737 self.value_semantics = value_semantics
738 self.default = None
739 self._data = {}
740 self.is_dirty = False
741
742 @property
743 def data(self):
744 return self._data if self.is_dirty else self.default
745
746 def __len__(self):
747 return len(self.data)
748
749 def __getitem__(self, key):
750 return self.value_semantics.load(
751 self.data.__getitem__(self.key_semantics.store(key))
752 )
753
754 def __setitem__(self, key, value):
755 self.is_dirty = True
756 self.data.__setitem__(
757 self.key_semantics.store(key), self.value_semantics.store(value)
758 )
759
760 def __delitem__(self, key):
761 if not self.is_dirty:
762 raise RuntimeError("cannot remove elements from the default value")
763 self.data.__delitem__(self.key_semantics.store(key))
764
765 def __iter__(self):
766 for key in self.data:
767 yield self.key_semantics.load(key)
768
769 def keys(self):
770 return list(self)
771
772 def items(self):
773 for key, value in self.data.items():
774 yield (self.key_semantics.load(key), self.value_semantics.load(value))
775
776 def values(self):
777 for value in self.data.values():
778 yield self.value_semantics.load(value)
779
780 def __contains__(self, key):
781 return self.key_semantics.store(key) in self.data
782
783 def get(self, key, default=None):
784 key = self.key_semantics.store(key)
785 if key in self.data:
786 return self.value_semantics.load(self.data[key])
787 return default
788
789 # __contains__, , get, __eq__, __ne__
790 # popitem, clear, setdefault
791
792 def update(self, otherMap):
793 self.is_dirty = True
794 for key, value in otherMap.items():
795 self.data[self.key_semantics.store(key)] = self.value_semantics.store(value)
796
797 def opt_value(self):
798 return {
799 self.key_semantics.opt_value(key): self.value_semantics.opt_value(value)
800 for key, value in self.data.items()
801 }
802
803 def __repr__(self):
804 return repr(self.data)
805
806
808 __handled_types__ = (re.compile(r"(std::)?(unordered_)?map<.*>$"),)
809
810 def __init__(self, cpp_type):
811 super(MappingSemantics, self).__init__(cpp_type)
812 template_args = list(extract_template_args(cpp_type))
813 self.key_semantics = getSemanticsFor(template_args[0])
814 self.value_semantics = getSemanticsFor(template_args[1])
815
816 @property
817 def name(self):
818 return self._name
819
820 @name.setter
821 def name(self, value):
822 self._name = value
823 self.key_semantics.name = "{} key".format(self._name)
824 self.value_semantics.name = "{} value".format(self._name)
825
826 def store(self, value):
827 # No explicit type checking as anything else than dict fails in update call
828 new_value = _DictHelper(self.key_semantics, self.value_semantics)
829 new_value.update(value)
830 return new_value
831
832 def default(self, value):
833 new_value = _DictHelper(self.key_semantics, self.value_semantics)
834 new_value.default = value
835 return new_value
836
837 def opt_value(self, value):
838 """
839 Option string version of value.
840 """
841 if not isinstance(value, _DictHelper):
842 value = self.default(value)
843 return value.opt_value()
844
845 def merge(self, a, b):
846 """Merge two maps. Throw ValueError if there are conflicting key/value pairs."""
847
848 # Optimization for most common case
849 if a == b:
850 return a
851
852 for k, v in b.items():
853 try:
854 va = a[k]
855 except KeyError:
856 a[k] = v
857 else:
858 if va != v:
859 raise ValueError(
860 f"conflicting values in map for key {k}: {v} and {va}"
861 )
862 return a
863
864
865SEMANTICS = [
866 c
867 for c in globals().values()
868 if isinstance(c, type)
869 and issubclass(c, PropertySemantics)
870 and c not in (PropertySemantics, DefaultSemantics)
871]
872
873
874def getSemanticsFor(cpp_type, strict=False):
875 """Return semantics for given type. If no type-specific semantics can be found
876 return DefaultSemantics. In strict mode, raise a TypeError instead.
877 """
878
879 for semantics in SEMANTICS:
880 try:
881 return semantics(cpp_type)
882 except TypeError:
883 pass
884
885 if strict:
886 raise TypeError(f"No semantics found for {cpp_type}")
887
888 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:735
get(self, key, default=None)
Definition semantics.py:783
__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:667
getSemanticsFor(cpp_type, strict=False)
Definition semantics.py:874
extract_template_args(cpp_type)
Definition semantics.py:446