The Gaudi Framework  master (adcf1ca6)
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
385 """
386 Return an iterator over the list of template arguments in a C++ type
387 string.
388
389 >>> t = 'map<string, vector<int, allocator<int> >, allocator<v<i>, a<i>> >'
390 >>> list(extract_template_args(t))
391 ['string', 'vector<int, allocator<int> >', 'allocator<v<i>, a<i>>']
392 >>> list(extract_template_args('int'))
393 []
394 """
395 template_level = 0
396 arg_start = -1
397 for p, c in enumerate(cpp_type):
398 if c == ",":
399 if template_level == 1:
400 yield cpp_type[arg_start:p].strip()
401 arg_start = p + 1
402 elif c == "<":
403 template_level += 1
404 if template_level == 1:
405 arg_start = p + 1
406 elif c == ">":
407 template_level -= 1
408 if template_level == 0:
409 yield cpp_type[arg_start:p].strip()
410
411
412class _ListHelper(MutableSequence):
413 def __init__(self, semantics):
414 self.value_semantics = semantics
415 self.default = None
416 self._data = []
417 self.is_dirty = False
418
419 @property
420 def data(self):
421 return self._data if self.is_dirty else self.default
422
423 def __len__(self):
424 return len(self.data)
425
426 def __getitem__(self, key):
427 return self.value_semantics.load(self.data.__getitem__(key))
428
429 def __setitem__(self, key, value):
430 self.is_dirty = True
431 self.data.__setitem__(key, self.value_semantics.store(value))
432
433 def __delitem__(self, key):
434 if not self.is_dirty:
435 raise RuntimeError("cannot remove elements from the default value")
436 self.data.__delitem__(key)
437
438 def __eq__(self, other):
439 return self.data == other
440
441 def insert(self, key, value):
442 self.is_dirty = True
443 self.data.insert(key, self.value_semantics.store(value))
444
445 def append(self, value):
446 self.is_dirty = True
447 self.data.append(self.value_semantics.store(value))
448
449 def extend(self, iterable):
450 self.is_dirty = True
451 self.data.extend(self.value_semantics.store(value) for value in iterable)
452
453 def opt_value(self):
454 return [self.value_semantics.opt_value(item) for item in self.data]
455
456 def __repr__(self):
457 return repr(self.data)
458
459
461 __handled_types__ = (re.compile(r"(std::)?(vector|list)<.*>$"),)
462
463 def __init__(self, cpp_type, valueSem=None):
464 super(SequenceSemantics, self).__init__(cpp_type)
466 list(extract_template_args(cpp_type))[0]
467 )
468
469 @property
470 def name(self):
471 return self._name
472
473 @name.setter
474 def name(self, value):
475 self._name = value
476 self.value_semantics.name = "{} element".format(self._name)
477
478 def store(self, value):
479 if not isinstance(value, (list, _ListHelper, tuple)):
480 raise TypeError(
481 "list or tuple expected, got {!r} in assignment to {}".format(
482 value, self.name
483 )
484 )
485 new_value = _ListHelper(self.value_semantics)
486 new_value.extend(value)
487 return new_value
488
489 def default(self, value):
490 new_value = _ListHelper(self.value_semantics)
491 new_value.default = value
492 return new_value
493
494 def opt_value(self, value):
495 """
496 Option string version of value.
497 """
498 if not isinstance(value, _ListHelper):
499 value = self.default(value)
500 return value.opt_value()
501
502
503class _SetHelper(MutableSet):
504 def __init__(self, semantics):
505 self.value_semantics = semantics
506 self.default = set() # cannot use None due to the way __ior__ is implemented
507 self._data = set()
508 self.is_dirty = False
509
510 # Aliases to match builtin `set`
511 union = MutableSet.__ior__
512 update = MutableSet.__ior__
513 intersection = MutableSet.__iand__
514 difference = MutableSet.__isub__
515 symmetric_difference = MutableSet.__ixor__
516
517 @property
518 def data(self):
519 return self._data if self.is_dirty else self.default
520
521 def __len__(self):
522 return len(self.data)
523
524 def __contains__(self, value):
525 return self.value_semantics.store(value) in self.data
526
527 def __eq__(self, other):
528 return self.data == other
529
530 def __iter__(self):
531 for value in self.data:
532 yield self.value_semantics.load(value)
533
534 def add(self, value):
535 self.is_dirty = True
536 self.data.add(self.value_semantics.store(value))
537
538 def discard(self, value):
539 if not self.is_dirty:
540 raise RuntimeError("cannot remove elements from the default value")
541 self.data.discard(value)
542
543 def pop(self):
544 if not self.is_dirty:
545 raise RuntimeError("cannot remove elements from the default value")
546 return self.data.pop()
547
548 def opt_value(self):
549 return set(self.value_semantics.opt_value(item) for item in self.data)
550
551 def __repr__(self):
552 if self.data:
553 # sort into list but print as set to get reproducible repr
554 return "{" + repr(sorted(self.data))[1:-1] + "}"
555 else:
556 return "set()"
557
558
560 """Merge semantics for (unordered) sets."""
561
562 __handled_types__ = (re.compile(r"(std::)?unordered_set<.*>$"),)
563
564 def __init__(self, cpp_type, valueSem=None):
565 super(SetSemantics, self).__init__(cpp_type)
567 list(extract_template_args(cpp_type))[0]
568 )
569
570 @property
571 def name(self):
572 return self._name
573
574 @name.setter
575 def name(self, value):
576 self._name = value
577 self.value_semantics.name = "{} element".format(self._name)
578
579 def store(self, value):
580 # We support assignment from list for backwards compatibility
581 if not isinstance(value, (set, _SetHelper, list, _ListHelper)):
582 raise TypeError(
583 "set expected, got {!r} in assignment to {}".format(value, self.name)
584 )
585
586 new_value = _SetHelper(self.value_semantics)
587 new_value |= value
588 return new_value
589
590 def default(self, value):
591 new_value = _SetHelper(self.value_semantics)
592 new_value.default = value
593 return new_value
594
595 def opt_value(self, value):
596 """
597 Option string version of value.
598 """
599 if not isinstance(value, _SetHelper):
600 value = self.default(value)
601 return value.opt_value()
602
603 def merge(self, bb, aa):
604 aa |= bb
605 return aa
606
607
609 """
610 Extend the sequence-semantics with a merge-method to behave like a
611 OrderedSet: Values are unique but the order is maintained.
612 Use 'OrderedSet<T>' as fifth parameter of the Gaudi::Property<T> constructor
613 to invoke this merging method. Also applies to std::set.
614 """
615
616 __handled_types__ = (
617 re.compile(r"(std::)?set<.*>$"),
618 re.compile(r"^OrderedSet<.*>$"),
619 )
620
621 def __init__(self, cpp_type):
622 super(OrderedSetSemantics, self).__init__(cpp_type)
623
624 def merge(self, bb, aa):
625 for b in bb:
626 if b not in aa:
627 aa.append(b)
628 return aa
629
630
631class _DictHelper(MutableMapping):
632 def __init__(self, key_semantics, value_semantics):
633 self.key_semantics = key_semantics
634 self.value_semantics = value_semantics
635 self.default = None
636 self._data = {}
637 self.is_dirty = False
638
639 @property
640 def data(self):
641 return self._data if self.is_dirty else self.default
642
643 def __len__(self):
644 return len(self.data)
645
646 def __getitem__(self, key):
647 return self.value_semantics.load(
648 self.data.__getitem__(self.key_semantics.store(key))
649 )
650
651 def __setitem__(self, key, value):
652 self.is_dirty = True
653 self.data.__setitem__(
654 self.key_semantics.store(key), self.value_semantics.store(value)
655 )
656
657 def __delitem__(self, key):
658 if not self.is_dirty:
659 raise RuntimeError("cannot remove elements from the default value")
660 self.data.__delitem__(self.key_semantics.store(key))
661
662 def __iter__(self):
663 for key in self.data:
664 yield self.key_semantics.load(key)
665
666 def keys(self):
667 return list(self)
668
669 def items(self):
670 for key, value in self.data.items():
671 yield (self.key_semantics.load(key), self.value_semantics.load(value))
672
673 def values(self):
674 for value in self.data.values():
675 yield self.value_semantics.load(value)
676
677 def __contains__(self, key):
678 return self.key_semantics.store(key) in self.data
679
680 def get(self, key, default=None):
681 key = self.key_semantics.store(key)
682 if key in self.data:
683 return self.value_semantics.load(self.data[key])
684 return default
685
686 # __contains__, , get, __eq__, __ne__
687 # popitem, clear, setdefault
688
689 def update(self, otherMap):
690 self.is_dirty = True
691 for key, value in otherMap.items():
692 self.data[self.key_semantics.store(key)] = self.value_semantics.store(value)
693
694 def opt_value(self):
695 return {
696 self.key_semantics.opt_value(key): self.value_semantics.opt_value(value)
697 for key, value in self.data.items()
698 }
699
700 def __repr__(self):
701 return repr(self.data)
702
703
705 __handled_types__ = (re.compile(r"(std::)?(unordered_)?map<.*>$"),)
706
707 def __init__(self, cpp_type):
708 super(MappingSemantics, self).__init__(cpp_type)
709 template_args = list(extract_template_args(cpp_type))
710 self.key_semantics = getSemanticsFor(template_args[0])
711 self.value_semantics = getSemanticsFor(template_args[1])
712
713 @property
714 def name(self):
715 return self._name
716
717 @name.setter
718 def name(self, value):
719 self._name = value
720 self.key_semantics.name = "{} key".format(self._name)
721 self.value_semantics.name = "{} value".format(self._name)
722
723 def store(self, value):
724 # No explicit type checking as anything else than dict fails in update call
725 new_value = _DictHelper(self.key_semantics, self.value_semantics)
726 new_value.update(value)
727 return new_value
728
729 def default(self, value):
730 new_value = _DictHelper(self.key_semantics, self.value_semantics)
731 new_value.default = value
732 return new_value
733
734 def opt_value(self, value):
735 """
736 Option string version of value.
737 """
738 if not isinstance(value, _DictHelper):
739 value = self.default(value)
740 return value.opt_value()
741
742 def merge(self, a, b):
743 """Merge two maps. Throw ValueError if there are conflicting key/value pairs."""
744
745 # Optimization for most common case
746 if a == b:
747 return a
748
749 for k, v in b.items():
750 try:
751 va = a[k]
752 except KeyError:
753 a[k] = v
754 else:
755 if va != v:
756 raise ValueError(
757 f"conflicting values in map for key {k}: {v} and {va}"
758 )
759 return a
760
761
762SEMANTICS = [
763 c
764 for c in globals().values()
765 if isinstance(c, type)
766 and issubclass(c, PropertySemantics)
767 and c not in (PropertySemantics, DefaultSemantics)
768]
769
770
771def getSemanticsFor(cpp_type, strict=False):
772 """Return semantics for given type. If no type-specific semantics can be found
773 return DefaultSemantics. In strict mode, raise a TypeError instead.
774 """
775
776 for semantics in SEMANTICS:
777 try:
778 return semantics(cpp_type)
779 except TypeError:
780 pass
781
782 if strict:
783 raise TypeError(f"No semantics found for {cpp_type}")
784
785 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:632
get(self, key, default=None)
Definition semantics.py:680
__init__(self, cpp_type, valueSem=None)
Definition semantics.py:463
__init__(self, cpp_type, valueSem=None)
Definition semantics.py:564
getSemanticsFor(cpp_type, strict=False)
Definition semantics.py:771
extract_template_args(cpp_type)
Definition semantics.py:384