DUNE-DAQ
DUNE Trigger and Data Acquisition software
Loading...
Searching...
No Matches
dal.py
Go to the documentation of this file.
1# DUNE DAQ modification notice:
2# This file has been modified from the original ATLAS config source for the DUNE DAQ project.
3# Fork baseline commit: 67a24e731 (2022-10-27).
4# Renamed since fork: yes (from python/config/dal.py to python/conffwk/dal.py).
5
6"""Contains the base class for DAL types and auxiliary methods.
7
8This module defines the PyDALBase class that is used as the base type for all
9DAL classes. A few utilities are also available.
10"""
11# This variable holds the global list of DAL classes ever generated for the
12# current python session. This speeds up generation and makes python resolve
13# types in the correct, expected way.
14__dal__ = {}
15
16# Homogeneous comparison between strings and regular expressions
17
18
19def __strcmp__(v1, v2):
20 return v1 == v2
21
22
23def __recmp__(pat, v):
24 return pat.match(v) is not None
25
26
27def prettyprint_cardinality(not_null, multivalue):
28 """Returns a nice string representation for an object cardinality"""
29 if not_null:
30 if multivalue:
31 return '1..*'
32 else:
33 return '1..1'
34
35 else:
36 if multivalue:
37 return '0..*'
38 else:
39 return '0..1'
40
41
43 """Prints the range of an attribute in a nice way"""
44 to_print = list(attr['range']) # copy
45 for k in range(len(to_print)):
46 if isinstance(to_print[k], tuple):
47 to_print[k] = '..'.join([str(i) for i in to_print[k]])
48 return ', '.join([str(i) for i in to_print])
49
50
51def prettyprint_doc(entry):
52 """Pretty prints a schema Cache entry, to be used by __doc__ strings"""
53 from .schema import oks_types
54
55 akeys = list(entry['attribute'].keys())
56 akeys.sort()
57 retval = ' Attributes:'
58 if len(akeys) == 0:
59 retval += ' None'
60 retval += '\n'
61 for k in akeys:
62 retval += ' - "' + k + '": ' + \
63 entry['attribute'][k]['description'].strip() + '\n'
64 retval += ' oks-type: ' + entry['attribute'][k]['type'] + '\n'
65 retval += ' cardinality: ' + \
66 prettyprint_cardinality(entry['attribute'][k]['not-null'],
67 entry['attribute'][k]['multivalue']) + '\n'
68 if entry['attribute'][k]['range']:
69 retval += ' range: ' + \
70 prettyprint_range(entry['attribute'][k]) + '\n'
71 if entry['attribute'][k]['init-value']:
72 retval += ' initial value: ' + \
73 str(entry['attribute'][k]['init-value']) + '\n'
74
75 rkeys = list(entry['relation'].keys())
76 rkeys.sort()
77 retval += '\n Relationships:'
78 if len(rkeys) == 0:
79 retval += ' None'
80 retval += '\n'
81 for k in rkeys:
82 retval += ' - "' + k + '": ' + \
83 entry['relation'][k]['description'].strip() + '\n'
84 retval += ' oks-class: ' + entry['relation'][k]['type'] + '\n'
85 retval += ' cardinality: ' + \
86 prettyprint_cardinality(entry['relation'][k]['not-null'],
87 entry['relation'][k]['multivalue']) + '\n'
88 retval += ' aggregated: ' + \
89 str(entry['relation'][k]['aggregation']) + '\n'
90 return retval[:-1]
91
92
93class DalBase(object):
94 """This class is used to represent any DAL object in the system. """
95
96 # This will keep track of the dal objects that gets updated
97 _updated = set()
98
99 @staticmethod
100 def updated():
101 """Returns a set of DAL objects that were modified in this DB session
102 """
103 return set(DalBase._updated)
104
105 @staticmethod
107 """Reset the set keeping track of modified DAL objects
108 """
109 DalBase._updated.clear()
110
111 def __init__(self, id, **kwargs):
112 """Constructs an object by setting its id (UID in OKS jargon) at least.
113
114 This method will initialize an object of the DalBase type, by setting
115 its internal properties (with schema cross-checking where it is
116 possible). The user should at least set the object's id, which at this
117 moment is not checked for uniqueness.
118
119 Keyword arguments:
120
121 id -- This is the unique identifier (per database) that the user wants
122 to assign to this object. This identifier will be used as the OKS
123 identifier when and if this object is ever serialized in an OKS
124 database.
125
126 **kwargs -- This is a set of attributes and relationships that must
127 exist in the associated DAL class that inherits from this base.
128 """
129 from . import dalproperty
130 prop = property(dalproperty._return_attribute('id', self, id),
131 dalproperty._assign_attribute('id'))
132 setattr(self.__class__, 'id', prop)
133 self.__reset_identity__()
134
135 self.__touched__ = [] # optimization
136 for k, v in kwargs.items():
137 setattr(self, k, v)
138
140 self.__fullname__ = '%s@%s' % (self.id, self.className())
141 self.__hashvalue__ = hash(self.__fullname__)
142
143 def className(self):
144 return self.__class__.pyclassName()
145
146 def isDalType(self, val):
147 cmp = __strcmp__
148 if hasattr(val, 'match'):
149 cmp = __recmp__
150 return True in [cmp(val, k) for k in self.__class__.__okstypes__]
151
152 def oksTypes(self):
153 return self.__class__.pyoksTypes()
154
155 def fullName(self):
156 return self.__fullname__
157
158 def copy(self, other):
159 """Copies attributes and relationships from the other component.
160
161 This will copy whatever relevant attributes and relationships from
162 another component into myself. The implemented algorithm starts by
163 iterating on my own schema and looking for the counter part on the
164 other class's schema, only matching values are copied. This is useful
165 to copy values from base class objects or templated class objects.
166
167 Arguments:
168
169 other -- This is the other dal object you are trying to copy.
170 """
171 for k, v in self.__schema__['attribute'].items():
172 if k not in other.__schema__['attribute']:
173 continue
174 setattr(self, k, getattr(other, k))
175 for k, v in self.__schema__['relation'].items():
176 if k not in other.__schema__['relation']:
177 continue
178 obj = getattr(other, k)
179 try:
180 setattr(self, k, list(obj))
181 except TypeError:
182 setattr(self, k, obj)
183
184 def rename(self, new_name):
185 """
186 Rename the DAL object to a new name.
187
188 This will store the old name in a hidden attribute and when
189 Configuration.update_dal() is called we use it to check
190 if there is an existing object with that name in the database.
191
192 If yes, we call the underlying ConfigObject.rename() method
193 transparently. If the old name does not exist in the database,
194 nothing special is done.
195
196 This is the only 'official' way to rename an object on the
197 DAL level. Just changing the 'id' attribute will not have
198 the same effect.
199 """
200 if self.id == new_name:
201 return
202
203 if not hasattr(self, '__old_id'):
204 setattr(self, '__old_id', getattr(self, 'id'))
205
206 self.id = new_name
207
208 def __repr__(self):
209 """Returns a nice representation of this object."""
210 return "<%s>" % (self.__fullname__)
211
212 def __str__(self):
213 """Returns human readable information about the object."""
214
215 retval = "%s(id='%s'" % (self.className(), self.id)
216
217 for a, v in self.__schema__['attribute'].items():
218 retval += ',\n %s = ' % a
219 if hasattr(self, a):
220 retval += str(getattr(self, a))
221 else:
222 retval += 'None'
223 if v['init-value']:
224 retval += ", # defaults to '%s'" % v['init-value']
225 else:
226 if v['not-null']:
227 retval += ', # MUST be set, there is not default!'
228
229 for r, v in self.__schema__['relation'].items():
230 retval += ',\n %s = ' % r
231 rel = None
232 if hasattr(self, r):
233 rel = getattr(self, r)
234
235 if rel is None:
236 retval += "<unset>"
237 elif isinstance(rel, list):
238 retval += str([repr(k) for k in getattr(self, r)])
239 retval += ']'
240 else:
241 retval += repr(getattr(self, r))
242
243 if retval[-2:] == ',\n':
244 retval = retval[:-2]
245 retval += ')'
246
247 return retval
248
249 def __eq__(self, other):
250 """True is the 2 objects have the same class and ID."""
251 return self.__hashvalue__ == hash(other)
252
253 def __ne__(self, other):
254 """True if the 2 objects *not* have the same class and ID."""
255 return self.__hashvalue__ != hash(other)
256
257 def __gt__(self, other):
258 """True if the object is greater than the other alphabetically."""
259 if self.className() == other.className():
260 return self.id > other.id
261 return self.className() > other.className()
262
263 def __lt__(self, other):
264 """True if the class is smaller than the other. """
265 if self.className() == other.className():
266 return self.id < other.id
267 return (self.className() < other.className())
268
269 def __ge__(self, other):
270 """True if the object is greater or equal than the other
271 alphabetically.
272 """
273 return (self > other) or (self == other)
274
275 def __le__(self, other):
276 """Returns True if the class is smaller or equal than the other. """
277 return (self < other) or (self == other)
278
279 def __hash__(self):
280 """This method is meant to be used to allow DAL objects as map keys."""
281 return self.__hashvalue__
282
283 def __getall__(self, comp=None):
284 """Get all relations, includding a link to myself"""
285 top = False
286 if not comp:
287 top = True
288 comp = {}
289
290 if self.__fullname__ in comp:
291 return
292
293 comp[self.__fullname__] = self
294 for r in list(self.__schema__['relation'].keys()):
295
296 if not getattr(self, r):
297 continue
298
299 if isinstance(getattr(self, r), list):
300 for k in getattr(self, r):
301 k.__getall__(comp)
302 else:
303 getattr(self, r).__getall__(comp)
304
305 if top:
306 return comp
307
308 def get(self, className, idVal=None, lookBaseClasses=False):
309 """Get components in the object based on class name and/or id.
310
311 This method runs trough the components of its relationships and
312 returns a sorted list (sorting based on class name and object ID)
313 containing references to all components that match the search criteria.
314
315 Keyword Parameters (may be named):
316
317 className -- The name of the class to look for. Should be a string
318
319 idVal -- The id of the object to look for. If not set (or set to None),
320 the search will be based only on the class name. If set, it must be
321 either a string or an object that defines a match() method (such as a
322 regular expression).
323
324 lookBaseClasses -- If True and parameter to be search is a class, the
325 method will look also through the base classes names, so if value =
326 Application, for instance the method will return all objects of class
327 Application or that inherit from the Application class.
328
329 Returns a list with all the components that matched the search
330 criteria, if idVal is not set or is a type that defines a match()
331 method such as a regular expression. Otherwise (if it is a string)
332 returns a single object, if any is found following the criterias for
333 className and a exact idVal match.
334 """
335 retval = []
336
337 cmp_class = __strcmp__
338 if hasattr(className, 'match'):
339 cmp_class = __recmp__
340
341 for v in self.__getall__().values():
342 if cmp_class(className, v.__class__.__name__) or \
343 (lookBaseClasses and v.isDalType(className)):
344 if idVal:
345 if type(idVal) == str:
346 if idVal == v.id:
347 return v
348 else:
349 continue
350
351 # if idVal is set and is not a string,
352 # we just go brute force...
353 if idVal.match(v.id):
354 retval.append(v)
355 else:
356 # if idVal is not set and we are sure the class matched,
357 # just append
358 retval.append(v)
359
360 if isinstance(idVal, str):
361 raise KeyError('Did not find %s@%s under %s' %
362 (idVal, className, self.fullName()))
363 return retval
364
365 def __getattr__(self, par):
366 """Returns a given attribute or relationship.
367
368 This method returns an attribute or relationship from the current
369 object, or throws an AttributeError if no such thing exists. It sets
370 the field touched, so it does not get called twice.
371 """
372
373 if par in self.__schema__['attribute']:
374 if self.__schema__['attribute'][par]['init-value']:
375 setattr(self, par, self.__schema__[
376 'attribute'][par]['init-value'])
377 else:
378 if self.__schema__['attribute'][par]['multivalue']:
379 setattr(self, par, [])
380 else:
381 return None # in this case, does not set anything
382 return getattr(self, par)
383
384 elif par in self.__schema__['relation']:
385 if self.__schema__['relation'][par]['multivalue']:
386 setattr(self, par, [])
387 else:
388 return None
389 return getattr(self, par)
390
391 raise AttributeError("'%s' object has no attribute/relation '%s'" %
392 (self.className(), par))
393
394 def setattr_nocheck(self, par, val):
395 """Sets an attribute by-passing the built-in type check."""
396
397 self.__dict__[par] = val
398 if par in list(self.__schema__['relation'].keys()):
399 self.__touched__.append(par)
400 return val
401
402 def __setattr__(self, par, val):
403 """Sets an object attribute or relationship.
404
405 This method overrides the default setattr method, so it can apply
406 existence and type verification on class attributes. If the attribute
407 to be set starts with '__', or the passed value is None,
408 no verification is performed. If the value to set an attribute is a
409 list, the type verification is performed in every component of that
410 list.
411
412 N.B.: This method takes a reference to the object being passed. It does
413 not copy the value, so, if you do a.b = c, and then you apply changes
414 to 'c', these changes will be also applied to 'a.b'.
415
416 Parameters:
417
418 par -- The name of the parameter (attribute or relationship)
419
420 val -- The value that will be attributed to 'par'.
421
422 Raises AttributeError if the parameter does not exist.
423
424 Raises ValueError if the value you passed cannot be coerced to a
425 compatible OKS python type for the attribute or relationship you are
426 trying to set.
427 """
428 from .schema import coerce, check_relation, check_cardinality
429 from . import dalproperty
430
431 # no checks for control parameters
432 if par[0:2] == '__':
433 self.__dict__[par] = val
434 return
435
436 # and for the id it is special
437 if par == 'id':
438 if isinstance(val, str):
439 prop = getattr(self.__class__, par)
440 prop.__set__(self, val)
441 self.__reset_identity__()
442 return
443 else:
444 raise ValueError(
445 'The "id" attribute of a DAL object must be a string')
446
447 if par in list(self.__schema__['attribute'].keys()):
448
449 # If val is None, skip checks
450 if val is None:
451 self.__dict__[par] = val
452
453 try:
454 if val is not None:
455 check_cardinality(val, self.__schema__['attribute'][par])
456 if self.__schema__['attribute'][par]['multivalue']:
457 result = \
458 [coerce(v, self.__schema__['attribute'][par])
459 for v in val]
460 else:
461 result = coerce(val, self.__schema__['attribute'][par])
462 else:
463 result = val
464
465 try:
466 prop = getattr(self.__class__, par)
467
468 except AttributeError:
469
470 prop = property(dalproperty._return_attribute(par),
471 dalproperty._assign_attribute(par))
472 setattr(self.__class__, par, prop)
473
474 prop.__set__(self, result)
475
476 except ValueError as e:
477 raise ValueError('Problems setting attribute "%s" '
478 'at object %s: %s' %
479 (par, self.fullName(), str(e)))
480
481 elif par in list(self.__schema__['relation'].keys()):
482
483 try:
484 # If val is None, skip checks
485 if val is not None:
486 check_cardinality(val, self.__schema__['relation'][par])
487
488 tmpval = \
489 val if self.__schema__[
490 'relation'][par]['multivalue'] else [val]
491 for v in tmpval:
492 check_relation(v, self.__schema__['relation'][par])
493
494 try:
495 prop = getattr(self.__class__, par)
496
497 except AttributeError:
498
499 multi = self.__schema__['relation'][par]['multivalue']
500 prop = property(
501 dalproperty._return_relation(par, multi=multi),
502 dalproperty._assign_relation(par))
503 setattr(self.__class__, par, prop)
504
505 prop.__set__(self, val)
506 self.__touched__.append(par)
507
508 except ValueError as e:
509 raise ValueError('Problems setting relation "%s" at '
510 'object %s: %s' %
511 (par, self.fullName(), str(e)))
512
513 else:
514 raise AttributeError('Parameter "%s" is not ' % par +
515 'part of class "%s" or any of its '
516 'parent classes' %
517 (self.className()))
518
519
520class DalType(type):
521 """
522 This class is the metaclass that every DAL class will be created with.
523
524 DalType is a metaclass (something like a C++ template) that allows us to
525 create DAL classes without having to go through the 'exec' burden all the
526 time and being, therefore, much faster than that mechanism. The idea is
527 that we create DAL types everytime we see a new class and archive this in a
528 cache, together with the configuration object. Everytime an object of a
529 certain OKS type is needed by the user, we make use of the generated class
530 living in that cache to make it a new DAL object.
531
532 The DAL type consistency checks are limited by the amount of generic
533 functionality one can extract by looking at the C++ Configuration class.
534
535 The work here is modelled after the old PyDALBase implementation that used
536 to live in the "genconffwk" package.
537 """
538
539 def __init__(cls, name, bases, dct):
540 """Class constructor.
541
542 Keyword Parameters:
543
544 cls -- This is a pointer to the class being constructed
545
546 name -- The name that the class will have
547
548 bases -- These are the classes, objects of the new generated type will
549 inherit from. It is useful in our context, to express the OKS
550 inheritance relations between the classes.
551
552 dct -- This is a dictionary that will contain mappings between
553 methods/attributes of the newly generated class and values or methods
554 that will be bound to it. The dictionary should contain a pointer to
555 the class schema, and that should be called '__schema__'.
556 """
557 # set all types as expected by the DAL
558 alltypes = [name]
559 for b in bases:
560 if hasattr(b, 'pyoksTypes'):
561 for t in b.pyoksTypes():
562 if t not in alltypes:
563 alltypes.append(t)
564
565 super(DalType, cls).__init__(name, bases, dct)
566 cls.__okstypes__ = alltypes
567
568 def pyclassName(cls):
569 """Returns this class name"""
570 return cls.__name__
571
572 def pyoksTypes(cls):
573 """Returns a join of this class and base class names"""
574 return cls.__okstypes__
575
576
578 """Returns a map with classes in a module, the key is the class name."""
579
580 # assesses all classes from other modules, correlate with names
581 map = {}
582 for k in dir(m):
583 if k.find('__') == 0:
584 continue
585 map[k] = getattr(m, k)
586 return map
587
588
589def generate(configuration, other_dals=[]):
590 """Generates the DAL python access layer for the configuration passed.
591
592 This method will generate the python DAL access layer for all classes
593 declared through the conffwk.Configuration object passed. If this file
594 includes other schemas, the classes for those schemas will also be
595 generated, unless, classes with matching names are passed through the
596 "other_dals" parameters.
597
598 This method will re-use classes generated in other calls to this method,
599 either directly (in DAL binding to a python module) or while you created
600 Configuration type objects. So, you can call this as many times as you want
601 without incurring in much overhead.
602
603 Keyword parameters:
604
605 configuration -- The conffwk.Configuration object that you want the prepare
606 the DAL for.
607
608 other_dals -- This is a list of classes that contain other DALs that should
609 be considered for the inheritance structure of the classes that are going
610 to be generated here. These classes will not be regenerated. This parameter
611 can be either a list of modules or classes that won't be regenerated, but
612 re-used by this generation method.
613
614 Returns the DAL classes you asked for.
615 """
616 from types import ModuleType as module
617
618 klasses = []
619
620 other_classes = {}
621 for k in other_dals:
622 if isinstance(k, module):
623 other_classes.update(get_classes(k))
624 else:
625 other_classes[k.pyclassName()] = k
626
627 # we can save a few loops here, we order by number of bases
628 to_generate = {}
629 for k in configuration.classes():
630 if k in other_classes:
631 continue
632
633 N = len(configuration.superclasses(k))
634 if N in to_generate:
635 to_generate[N].append(k)
636 else:
637 to_generate[N] = [k]
638
639 ordered = []
640 run_order = list(to_generate.keys())
641 run_order.sort()
642 for k in run_order:
643 ordered += to_generate[k]
644
645 # generate what we need to
646 while ordered:
647 next = ordered[0] # gets the first one, no matter what it is
648
649 # if I generated this before, just re-use the class,
650 # so python can check the types in the way the user expects
651 if next in __dal__:
652 klasses.append(__dal__[next])
653 other_classes[next] = __dal__[next]
654
655 # else, I need to generate a brand new class here and
656 # add it to my __dal__
657 else:
658 bases = configuration.superclasses(next)
659 bases = [other_classes.get(k, None) for k in bases]
660 bases.append(DalBase)
661 if None in bases: # cannot yet generate for this one, rotate
662 ordered.append(next)
663 else: # we can generate this one now
664 klasses.append(DalType(next, tuple(bases),
665 {'__schema__':
666 configuration.__schema__[next]}))
667 # so we can use this next time
668 other_classes[next] = klasses[-1]
669 klasses[-1].__doc__ = prettyprint_doc(
670 configuration.__schema__[next])
671 __dal__[next] = klasses[-1]
672
673 del ordered[0]
674
675 return klasses
676
677
678def module(name, schema, other_dals=[], backend='oksconflibs', db=None):
679 """Creates a new python module with the OKS schema files passed as
680 parameter.
681
682 This method creates a new module for the user, using the schema files
683 passed as parameter. Classes from other DALs are not re-created, but just
684 re-used. This is an example usage:
685
686 import conffwk.dal
687 dal = conffwk.dal.module('dal', 'dal/schema/core.schema.xml')
688 DFdal = conffwk.dal.module('DFdal', 'DFConfiguration/schema/df.schema.xml',
689 [dal])
690
691 This will generate two python dals in the current context. One that binds
692 everything available in the first schema file and a second one that binds
693 everything else defined in the DF OKS schema file.
694
695 Keyword parameters:
696
697 name -- The name of the python module to create. It should match the name
698 of the variable you are attributing to, but it is not strictly required by
699 the python interpreter, just a good practice.
700
701 schema -- This is a list of OKS schema files that should be considered. You
702 can also pass OKS datafiles to this one, which actually includes the schema
703 files you want to have a DAL for. It will just work.
704
705 other_dals -- This is a list of other DAL modules that I'll not regenerate,
706 and which classes will *not* make part of the returned module. In fact,
707 this parameter is only used to restrict the amount of output classes since
708 once class is generated internally, it is not regenerated a second time.
709 In other words creating twice the same DAL implies in almost no overhead.
710
711 backend -- This is the OKS backend to use when retrieving the schemas. By
712 default it is set to 'oksconflibs', which is what we
713 """
714 import types
715 from .Configuration import Configuration
716
717 retval = types.ModuleType(name)
718 if isinstance(schema, str):
719 schema = [schema]
720 for s in schema:
721 if db is None:
722 db = Configuration(backend + ':' + s)
723 else:
724 db.load(s)
725 db.__core_init__()
726
727 for k in generate(db, other_dals):
728 retval.__dict__[k.pyclassName()] = k
729 return retval
setattr_nocheck(self, par, val)
Definition dal.py:394
copy(self, other)
Definition dal.py:158
__getattr__(self, par)
Definition dal.py:365
__setattr__(self, par, val)
Definition dal.py:402
__init__(self, id, **kwargs)
Definition dal.py:111
__reset_identity__(self)
Definition dal.py:139
__ne__(self, other)
Definition dal.py:253
__ge__(self, other)
Definition dal.py:269
__eq__(self, other)
Definition dal.py:249
isDalType(self, val)
Definition dal.py:146
__getall__(self, comp=None)
Definition dal.py:283
className(self)
Definition dal.py:143
__le__(self, other)
Definition dal.py:275
get(self, className, idVal=None, lookBaseClasses=False)
Definition dal.py:308
__lt__(self, other)
Definition dal.py:263
rename(self, new_name)
Definition dal.py:184
__gt__(self, other)
Definition dal.py:257
__init__(cls, name, bases, dct)
Definition dal.py:539
prettyprint_range(attr)
Definition dal.py:42
module(name, schema, other_dals=[], backend='oksconflibs', db=None)
Definition dal.py:678
__recmp__(pat, v)
Definition dal.py:23
prettyprint_cardinality(not_null, multivalue)
Definition dal.py:27
__strcmp__(v1, v2)
Definition dal.py:19
get_classes(m)
Definition dal.py:577
prettyprint_doc(entry)
Definition dal.py:51