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