Coverage for gws-app/gws/core/tree_impl.py: 89%
299 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Internal implementations of ``gws.Node`` and ``gws.Root`` methods."""
3import sys
5from . import (
6 const as c,
7 util as u,
8 log,
9)
11Access = None
12Error = None
13Data = None
14Props = None
15Object = None
18def object_repr(self):
19 """Return a short representation of an object.
21 Args:
22 self: The object.
24 Returns:
25 A string with the class or extension name, title, uid and the object id.
26 """
27 r = getattr(self, 'extName', None) or class_name(self)
28 s = getattr(self, 'title', None)
29 if s:
30 r += f' title={s!r}'
31 s = getattr(self, 'uid', None)
32 if s:
33 r += f' uid={s}'
34 return '<' + r + ' ' + hex(id(self)) + '>'
37def node_initialize(self, config):
38 """Implement ``gws.Node.initialize``.
40 Args:
41 self: The node.
42 config: Configuration.
43 """
44 self.config = config
45 self.permissions = configure_permissions(self)
46 super_invoke(self, 'pre_configure')
47 super_invoke(self, 'configure')
50def node_create_child(self, classref, config, **kwargs):
51 """Implement ``gws.Node.create_child``.
53 Args:
54 self: The parent node.
55 classref: Class reference.
56 config: Configuration.
57 **kwargs: Additional configuration properties.
59 Returns:
60 A newly created node or ``None``.
61 """
62 return self.root.create(classref, parent=self, config=config, **kwargs)
65def node_create_child_if_configured(self, classref, config=None, **kwargs):
66 """Implement ``gws.Node.create_child_if_configured``.
68 Args:
69 self: The parent node.
70 classref: Class reference.
71 config: Configuration.
72 **kwargs: Additional configuration properties.
74 Returns:
75 A newly created node or ``None``.
76 """
77 if not config:
78 return None
79 return self.root.create(classref, parent=self, config=config, **kwargs)
82def node_create_children(self, classref, configs, **kwargs):
83 """Implement ``gws.Node.create_children``.
85 Args:
86 self: The parent node.
87 classref: Class reference.
88 configs: List of configurations.
89 **kwargs: Additional configuration properties.
91 Returns:
92 A list of newly created nodes, without the ones that failed.
93 """
94 if not configs:
95 return []
96 return u.compact(self.create_child(classref, cfg, **kwargs) for cfg in configs)
99def node_cfg(self, key: str, default=None):
100 """Implement ``gws.Node.cfg``.
102 Args:
103 self: The node.
104 key: Property key, nested keys are separated by dots.
105 default: Value to return if the property is ``None`` or missing.
107 Returns:
108 The property value or the default.
109 """
110 val = u.get(self.config, key)
111 return val if val is not None else default
114def node_find_all(self, classref):
115 """Implement ``gws.Node.find_all``.
117 Args:
118 self: The node.
119 classref: Class reference. If ``None``, all nodes match.
121 Returns:
122 A list of matching children.
123 """
124 return find_all_in(self.root, self.children, classref)
127def node_find_first(self, classref):
128 """Implement ``gws.Node.find_first``.
130 Args:
131 self: The node.
132 classref: Class reference. If ``None``, all nodes match.
134 Returns:
135 The first matching child or ``None``.
136 """
137 return find_first_in(self.root, self.children, classref)
140def node_find_closest(self, classref):
141 """Implement ``gws.Node.find_closest``.
143 Args:
144 self: The node.
145 classref: Class reference. If ``None``, all nodes match.
147 Returns:
148 The closest matching ancestor below the root, or ``None``.
149 """
150 node = self.parent
151 while True:
152 if not node or node is self.root:
153 return
154 if not classref or is_a(self.root, node, classref):
155 return node
156 node = node.parent
159def node_find_ancestors(self, classref):
160 """Implement ``gws.Node.find_ancestors``.
162 Args:
163 self: The node.
164 classref: Class reference. If ``None``, all nodes match.
166 Returns:
167 A list of matching ancestors below the root, from the parent upwards.
168 """
169 ls = []
170 node = self.parent
172 while True:
173 if not node or node is self.root:
174 break
175 if not classref or is_a(self.root, node, classref):
176 ls.append(node)
177 node = node.parent
179 return ls
182def node_find_descendants(self, classref):
183 """Implement ``gws.Node.find_descendants``.
185 Args:
186 self: The node.
187 classref: Class reference. If ``None``, all nodes match.
189 Returns:
190 A list of matching descendants in the depth-first order.
191 """
192 ls = []
194 def _walk(node):
195 for child_node in node.children:
196 if not classref or is_a(self.root, child_node, classref):
197 ls.append(child_node)
198 _walk(child_node)
200 _walk(self)
201 return ls
204##
207def root_init(self, specs):
208 """Implement ``gws.Root.__init__``.
210 Args:
211 self: The root.
212 specs: Specs runtime.
213 """
214 self.specs = specs
215 self.app = None
216 self.permissions = {}
217 self.configErrors = []
218 self.configWarnings = []
219 self.configStack = []
220 self.configPaths = []
221 self.nodes = []
222 self.uidMap = {}
223 self.uidCount = 1
226def root_initialize(self, node, config):
227 """Implement ``gws.Root.initialize``.
229 The node is pushed onto ``configStack`` while it is initialized.
230 Exceptions are logged and recorded in ``configErrors``.
232 Args:
233 self: The root.
234 node: The node.
235 config: Configuration.
237 Returns:
238 ``True`` if the node was initialized, ``False`` if it failed.
239 """
240 self.configStack.append(node)
242 try:
243 node.initialize(config)
244 ok = True
245 except Exception as exc:
246 log.exception()
247 register_config_error(self, exc)
248 ok = False
250 self.configStack.pop()
251 return ok
254def root_post_initialize(self):
255 """Implement ``gws.Root.post_initialize``.
257 Args:
258 self: The root.
259 """
260 for node in reversed(self.nodes):
261 self.configStack = []
262 p = node
263 while p:
264 self.configStack.insert(0, p)
265 p = getattr(p, 'parent', None)
266 try:
267 super_invoke(node, 'post_configure')
268 except Exception as exc:
269 log.exception()
270 register_config_error(self, exc)
271 self.configStack = []
274def root_activate(self):
275 """Implement ``gws.Root.activate``.
277 Args:
278 self: The root.
279 """
280 for node in self.nodes:
281 # if type(node).activate != Node.activate:
282 # log.debug(f'activate: {node!r}')
283 node.activate()
286def root_find_all(self, classref):
287 """Implement ``gws.Root.find_all``.
289 Args:
290 self: The root.
291 classref: Class reference. If ``None``, all nodes match.
293 Returns:
294 A list of matching nodes.
295 """
296 return find_all_in(self, self.nodes, classref)
299def root_find_first(self, classref):
300 """Implement ``gws.Root.find_first``.
302 Args:
303 self: The root.
304 classref: Class reference. If ``None``, all nodes match.
306 Returns:
307 The first matching node or ``None``.
308 """
309 return find_first_in(self, self.nodes, classref)
312def root_get(self, uid, classref):
313 """Implement ``gws.Root.get``.
315 Args:
316 self: The root.
317 uid: Node uid.
318 classref: Class reference. If given, the node must match it.
320 Returns:
321 The node or ``None``.
322 """
323 if not uid:
324 return
325 node = self.uidMap.get(uid)
326 if node and (not classref or is_a(self, node, classref)):
327 return node
330def root_object_count(self) -> int:
331 """Implement ``gws.Root.object_count``.
333 Args:
334 self: The root.
336 Returns:
337 The number of nodes.
338 """
339 return len(self.nodes)
342def root_create(self, classref, parent, config, **kwargs):
343 """Implement ``gws.Root.create``.
345 Args:
346 self: The root.
347 classref: Class reference.
348 parent: Parent node.
349 config: Configuration.
350 **kwargs: Additional configuration properties.
352 Returns:
353 A newly created node or ``None``.
354 """
355 config = to_config(config, kwargs)
356 return create_node(self, classref, parent, config)
359def root_create_shared(self, classref, config, **kwargs):
360 """Implement ``gws.Root.create_shared``.
362 Args:
363 self: The root.
364 classref: Class reference.
365 config: Configuration.
366 **kwargs: Additional configuration properties.
368 Returns:
369 An existing node with the same uid, a newly created node, or ``None``.
370 """
371 config = to_config(config, kwargs)
373 uid = config.uid
374 if not uid:
375 config.uid = '_s_' + u.sha256([repr(classref), config])
377 if config.uid in self.uidMap:
378 return self.uidMap[config.uid]
380 return create_node(self, classref, None, config)
383def root_create_temporary(self, classref, config, **kwargs):
384 """Implement ``gws.Root.create_temporary``.
386 The node is not registered in the root and ``post_configure`` is run immediately.
388 Args:
389 self: The root.
390 classref: Class reference.
391 config: Configuration.
392 **kwargs: Additional configuration properties.
394 Returns:
395 A newly created node or ``None``.
396 """
397 config = to_config(config, kwargs)
398 node = create_node(self, classref, None, config, temp=True)
399 if node:
400 super_invoke(node, 'post_configure')
401 return node
404def root_create_application(self, config, **kwargs):
405 """Implement ``gws.Root.create_application``.
407 The application gets the fixed uid ``const.APPLICATION_UID``.
409 Args:
410 self: The root.
411 config: Configuration.
412 **kwargs: Additional configuration properties.
414 Returns:
415 The Application object.
416 """
417 config = to_config(config, kwargs)
419 node = alloc_node(self, 'gws.base.application.core.Object')
420 node.uid = c.APPLICATION_UID
421 node.parent = self
422 node.children = []
424 self.nodes.append(node)
425 self.uidMap[node.uid] = node
426 self.app = node
428 self.initialize(node, config)
430 return node
433##
435_ROOT_NAME = 'gws_root_object'
438def save_root(root, path=None):
439 """Implement ``gws.save_root``.
441 Args:
442 root: The root.
443 path: File path.
445 Returns:
446 The file path.
447 """
448 path = path or c.ROOT_PICKLE_PATH
449 u.serialize_to_path(sys.path, f'{path}.syspath')
450 u.serialize_to_path(root, path)
451 return path
454def load_root(path=None):
455 """Implement ``gws.load_root``.
457 Args:
458 path: File path.
460 Returns:
461 The root.
462 """
463 path = path or c.ROOT_PICKLE_PATH
464 for p in u.unserialize_from_path(f'{path}.syspath'):
465 if p not in sys.path:
466 sys.path.insert(0, p)
467 log.debug(f'path {p!r} added to sys.path')
468 root = u.unserialize_from_path(path)
469 return activate_root(root)
472def activate_root(root):
473 """Implement ``gws.activate_root``.
475 Args:
476 root: The root.
478 Returns:
479 The root.
480 """
481 root.activate()
482 return u.set_app_global(_ROOT_NAME, root)
485def deactivate_root():
486 """Implement ``gws.deactivate_root``."""
487 u.delete_app_global(_ROOT_NAME)
490def get_root():
491 """Implement ``gws.get_root``.
493 Returns:
494 The root.
495 """
497 def _err():
498 raise Error('no configuration root found')
500 return u.get_app_global(_ROOT_NAME, _err)
503##
506def class_name(node):
507 """Return the full class name of an object.
509 Args:
510 node: An object.
512 Returns:
513 The module and class name, like ``gws.base.layer.core.Object``.
514 """
515 return node.__class__.__module__ + '.' + node.__class__.__name__
518def alloc_node(self, classref, typ=None):
519 """Create an uninitialized node of the class found in the specs.
521 Args:
522 self: The root.
523 classref: Class reference.
524 typ: Extension type, for ``ext`` class references.
526 Returns:
527 The new node, with ``root``, ``extName`` and ``extType`` set.
529 Raises:
530 Error: If the class is not found.
531 """
532 cls = self.specs.get_class(classref, typ)
533 if not cls:
534 raise Error(f'class {classref}:{typ} not found')
536 node = cls()
537 node.root = self
538 node.extName = getattr(cls, 'extName', '')
539 node.extType = getattr(cls, 'extType', '')
541 return node
544def configure_permissions(self):
545 """Compute the permissions of a node from its ``access`` and ``permissions`` config.
547 ``access`` sets the read permission. In ``permissions``, ``all`` sets all modes, ``edit`` sets write,
548 create and delete, and the specific modes override both.
550 Args:
551 self: The node.
553 Returns:
554 A mapping from an access mode to an ACL.
555 """
556 perms = {
557 Access.read: [],
558 Access.write: [],
559 Access.create: [],
560 Access.delete: [],
561 }
563 p = self.cfg('access')
564 if p:
565 perms[Access.read] = u.parse_acl(p)
567 p = self.cfg('permissions')
568 if p:
569 if isinstance(p, Data):
570 p = vars(p)
571 v = p.get('all')
572 if v:
573 perms[Access.read] = perms[Access.write] = perms[Access.create] = perms[Access.delete] = u.parse_acl(v)
575 v = p.get('edit')
576 if v:
577 perms[Access.write] = perms[Access.create] = perms[Access.delete] = u.parse_acl(v)
579 for k in {Access.read, Access.write, Access.create, Access.delete}:
580 v = p.get(k)
581 if v:
582 perms[k] = u.parse_acl(v)
584 return perms
587def create_node(self, classref, parent, config, temp=False):
588 """Create and initialize a node.
590 Args:
591 self: The root.
592 classref: Class reference.
593 parent: Parent node, the new node is appended to its children.
594 config: Configuration. Its ``type`` selects the extension type.
595 temp: If ``True``, the node is not registered in the root.
597 Returns:
598 The new node, or ``None`` if the initialization failed.
599 """
600 node = alloc_node(self, classref, config.get('type'))
601 node.uid = get_or_generate_uid(self, config)
602 node.parent = parent
603 node.children = []
605 log.debug('configure: ' + ('.' * 4 * len(self.configStack)) + f'{node!r} IN {parent or self!r}')
606 ok = self.initialize(node, config)
607 if not ok:
608 log.debug(f'FAILED {node!r}')
609 return
611 if not temp:
612 self.nodes.append(node)
613 self.uidMap[node.uid] = node
615 if parent:
616 parent.children.append(node)
618 return node
621def find_all_in(root, nodes, classref):
622 """Filter nodes by a class reference.
624 Args:
625 root: The root.
626 nodes: A list of nodes.
627 classref: Class reference. If ``None``, all nodes match.
629 Returns:
630 A list of matching nodes.
631 """
632 if not classref:
633 return nodes
634 cls, name, ext_name = root.specs.parse_classref(classref)
635 if cls:
636 return [node for node in nodes if isinstance(node, cls)]
637 if name:
638 return [node for node in nodes if class_name(node) == name]
639 if ext_name:
640 return [node for node in nodes if node.extName.startswith(ext_name)]
643def find_first_in(root, nodes, classref):
644 """Find the first node that matches a class reference.
646 Args:
647 root: The root.
648 nodes: A list of nodes.
649 classref: Class reference. If ``None``, all nodes match.
651 Returns:
652 The first matching node or ``None``.
653 """
654 found = find_all_in(root, nodes, classref)
655 return found[0] if found else None
658def get_or_generate_uid(self, config):
659 """Return the uid from the config, or generate a new numeric one.
661 Args:
662 self: The root.
663 config: Configuration.
665 Returns:
666 A uid.
667 """
668 if config.get('uid'):
669 return config.get('uid')
670 self.uidCount += 1
671 return str(self.uidCount)
674def is_a(root, node, classref):
675 """Check if a node matches a class reference.
677 A class matches by ``isinstance``, a class name matches exactly,
678 an ``ext`` name matches the node's extension name or its prefix.
680 Args:
681 root: The root.
682 node: The node.
683 classref: Class reference.
685 Returns:
686 ``True`` if the node matches.
687 """
688 cls, name, ext_name = root.specs.parse_classref(classref)
689 if cls:
690 return isinstance(node, cls)
691 if name:
692 return class_name(node) == name
693 if ext_name:
694 return node.extName == ext_name or node.extName.startswith(ext_name + '.')
695 return False
698def props_of(node, user, *context):
699 """Implement ``gws.props_of``.
701 Args:
702 node: The object.
703 user: The user.
704 *context: Context objects for the permission check.
706 Returns:
707 A ``Props`` object or ``None``.
709 Raises:
710 Error: If the object's ``props`` returns an invalid type.
711 """
712 if not user.can_use(node, *context):
713 return None
714 p = make_props2(node, user)
715 if p is None or isinstance(p, Data):
716 return p
717 if isinstance(p, dict):
718 return Props(p)
719 raise Error('invalid props type')
722def make_props2(obj, user):
723 """Recursively convert a value to props for a user.
725 Objects are converted with their ``props`` method, or dropped if the user has no read permission.
726 ``None`` values are removed from dicts and lists.
728 Args:
729 obj: An object, ``Data``, dict, list or a scalar value.
730 user: The user.
732 Returns:
733 A scalar, dict or list, or ``None``.
734 """
735 if u.is_atom(obj):
736 return obj
738 if isinstance(obj, Object):
739 if user.acl_bit(Access.read, obj) == c.DENY:
740 return None
741 obj = obj.props(user)
743 if isinstance(obj, Data):
744 obj = vars(obj)
746 if u.is_dict(obj):
747 return u.compact({k: make_props2(v, user) for k, v in obj.items()})
749 if u.is_list(obj):
750 return u.compact([make_props2(v, user) for v in obj])
752 return None
755def register_config_error(self, exc):
756 """Record an exception in ``configErrors``, with the current config stack.
758 Args:
759 self: The root.
760 exc: The exception.
761 """
762 try:
763 msg = getattr(exc, 'message', None) or str(exc.args[0])
764 except:
765 msg = repr(exc)
766 self.configErrors.append(config_info(self, msg))
769def root_config_warning(self, message):
770 """Implement ``gws.Root.config_warning``.
772 Args:
773 self: The root.
774 message: Warning message.
775 """
776 cei = config_info(self, message)
777 loc = ''
778 if cei.stack:
779 loc = ' in ' + config_location_repr(cei.stack[0])
780 log.warning(f'CONFIGURATION WARNING: {message}{loc}')
781 self.configWarnings.append(cei)
784def config_info(self, message):
785 """Create a configuration error or warning record.
787 Args:
788 self: The root.
789 message: The message.
791 Returns:
792 A ``Data`` object with ``message`` and ``stack``, a list of locations from the innermost node outwards.
793 """
794 cei = Data(message=message, stack=[])
795 for node in reversed(self.configStack):
796 cei.stack.append(
797 # @TODO actually this is a ConfigLocation object
798 Data(
799 objectUid=getattr(node, 'uid', ''),
800 objectType=getattr(node, 'extName', None) or class_name(node),
801 objectName=getattr(node, 'name', '') or getattr(node, 'title', ''),
802 propName='',
803 )
804 )
805 return cei
808def config_location_repr(loc):
809 """Return a short representation of a configuration location.
811 Args:
812 loc: A location from ``config_info``.
814 Returns:
815 A string with the object type, name, uid and the property name.
816 """
817 p = [
818 loc.objectType,
819 repr(loc.objectName) if loc.objectName else None,
820 f'uid={loc.objectUid}' if loc.objectUid else None,
821 ]
822 p = '<' + ' '.join(u.compact(p)) + '>'
823 if loc.propName:
824 p = f'{loc.propName!r} {p}'
825 return p
828def super_invoke(node, method):
829 """Invoke a method of every class in the node's MRO that defines it, base classes first.
831 Args:
832 node: The node.
833 method: Method name.
834 """
835 # since `super().configure` is mandatory in `configure` methods,
836 # let's automate this by collecting all super 'configure' methods
838 mro = []
840 for cls in type(node).mro():
841 try:
842 if method in vars(cls):
843 mro.append(cls)
844 except TypeError:
845 pass
847 for cls in reversed(mro):
848 getattr(cls, method)(node)
851def to_config(config, defaults):
852 """Merge keyword defaults and a configuration into a new ``Data`` object.
854 Values in ``config`` override the defaults, ``None`` values are skipped.
856 Args:
857 config: Configuration.
858 defaults: Default values.
860 Returns:
861 A new ``Data`` object.
862 """
863 return u.merge(Data(), defaults, config)