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