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

1"""Internal implementations of ``gws.Node`` and ``gws.Root`` methods.""" 

2 

3import sys 

4 

5from . import ( 

6 const as c, 

7 util as u, 

8 log, 

9) 

10 

11Access = None 

12Error = None 

13Data = None 

14Props = None 

15Object = None 

16 

17 

18def object_repr(self): 

19 """Return a short representation of an object. 

20 

21 Args: 

22 self: The object. 

23 

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)) + '>' 

35 

36 

37def node_initialize(self, config): 

38 """Implement ``gws.Node.initialize``. 

39 

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') 

48 

49 

50def node_create_child(self, classref, config, **kwargs): 

51 """Implement ``gws.Node.create_child``. 

52 

53 Args: 

54 self: The parent node. 

55 classref: Class reference. 

56 config: Configuration. 

57 **kwargs: Additional configuration properties. 

58 

59 Returns: 

60 A newly created node or ``None``. 

61 """ 

62 return self.root.create(classref, parent=self, config=config, **kwargs) 

63 

64 

65def node_create_child_if_configured(self, classref, config=None, **kwargs): 

66 """Implement ``gws.Node.create_child_if_configured``. 

67 

68 Args: 

69 self: The parent node. 

70 classref: Class reference. 

71 config: Configuration. 

72 **kwargs: Additional configuration properties. 

73 

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) 

80 

81 

82def node_create_children(self, classref, configs, **kwargs): 

83 """Implement ``gws.Node.create_children``. 

84 

85 Args: 

86 self: The parent node. 

87 classref: Class reference. 

88 configs: List of configurations. 

89 **kwargs: Additional configuration properties. 

90 

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) 

97 

98 

99def node_cfg(self, key: str, default=None): 

100 """Implement ``gws.Node.cfg``. 

101 

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. 

106 

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 

112 

113 

114def node_find_all(self, classref): 

115 """Implement ``gws.Node.find_all``. 

116 

117 Args: 

118 self: The node. 

119 classref: Class reference. If ``None``, all nodes match. 

120 

121 Returns: 

122 A list of matching children. 

123 """ 

124 return find_all_in(self.root, self.children, classref) 

125 

126 

127def node_find_first(self, classref): 

128 """Implement ``gws.Node.find_first``. 

129 

130 Args: 

131 self: The node. 

132 classref: Class reference. If ``None``, all nodes match. 

133 

134 Returns: 

135 The first matching child or ``None``. 

136 """ 

137 return find_first_in(self.root, self.children, classref) 

138 

139 

140def node_find_closest(self, classref): 

141 """Implement ``gws.Node.find_closest``. 

142 

143 Args: 

144 self: The node. 

145 classref: Class reference. If ``None``, all nodes match. 

146 

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 

157 

158 

159def node_find_ancestors(self, classref): 

160 """Implement ``gws.Node.find_ancestors``. 

161 

162 Args: 

163 self: The node. 

164 classref: Class reference. If ``None``, all nodes match. 

165 

166 Returns: 

167 A list of matching ancestors below the root, from the parent upwards. 

168 """ 

169 ls = [] 

170 node = self.parent 

171 

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 

178 

179 return ls 

180 

181 

182def node_find_descendants(self, classref): 

183 """Implement ``gws.Node.find_descendants``. 

184 

185 Args: 

186 self: The node. 

187 classref: Class reference. If ``None``, all nodes match. 

188 

189 Returns: 

190 A list of matching descendants in the depth-first order. 

191 """ 

192 ls = [] 

193 

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) 

199 

200 _walk(self) 

201 return ls 

202 

203 

204## 

205 

206 

207def root_init(self, specs): 

208 """Implement ``gws.Root.__init__``. 

209 

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 

224 

225 

226def root_initialize(self, node, config): 

227 """Implement ``gws.Root.initialize``. 

228 

229 The node is pushed onto ``configStack`` while it is initialized. 

230 Exceptions are logged and recorded in ``configErrors``. 

231 

232 Args: 

233 self: The root. 

234 node: The node. 

235 config: Configuration. 

236 

237 Returns: 

238 ``True`` if the node was initialized, ``False`` if it failed. 

239 """ 

240 self.configStack.append(node) 

241 

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 

249 

250 self.configStack.pop() 

251 return ok 

252 

253 

254def root_post_initialize(self): 

255 """Implement ``gws.Root.post_initialize``. 

256 

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 = [] 

272 

273 

274def root_activate(self): 

275 """Implement ``gws.Root.activate``. 

276 

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() 

284 

285 

286def root_find_all(self, classref): 

287 """Implement ``gws.Root.find_all``. 

288 

289 Args: 

290 self: The root. 

291 classref: Class reference. If ``None``, all nodes match. 

292 

293 Returns: 

294 A list of matching nodes. 

295 """ 

296 return find_all_in(self, self.nodes, classref) 

297 

298 

299def root_find_first(self, classref): 

300 """Implement ``gws.Root.find_first``. 

301 

302 Args: 

303 self: The root. 

304 classref: Class reference. If ``None``, all nodes match. 

305 

306 Returns: 

307 The first matching node or ``None``. 

308 """ 

309 return find_first_in(self, self.nodes, classref) 

310 

311 

312def root_get(self, uid, classref): 

313 """Implement ``gws.Root.get``. 

314 

315 Args: 

316 self: The root. 

317 uid: Node uid. 

318 classref: Class reference. If given, the node must match it. 

319 

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 

328 

329 

330def root_object_count(self) -> int: 

331 """Implement ``gws.Root.object_count``. 

332 

333 Args: 

334 self: The root. 

335 

336 Returns: 

337 The number of nodes. 

338 """ 

339 return len(self.nodes) 

340 

341 

342def root_create(self, classref, parent, config, **kwargs): 

343 """Implement ``gws.Root.create``. 

344 

345 Args: 

346 self: The root. 

347 classref: Class reference. 

348 parent: Parent node. 

349 config: Configuration. 

350 **kwargs: Additional configuration properties. 

351 

352 Returns: 

353 A newly created node or ``None``. 

354 """ 

355 config = to_config(config, kwargs) 

356 return create_node(self, classref, parent, config) 

357 

358 

359def root_create_shared(self, classref, config, **kwargs): 

360 """Implement ``gws.Root.create_shared``. 

361 

362 Args: 

363 self: The root. 

364 classref: Class reference. 

365 config: Configuration. 

366 **kwargs: Additional configuration properties. 

367 

368 Returns: 

369 An existing node with the same uid, a newly created node, or ``None``. 

370 """ 

371 config = to_config(config, kwargs) 

372 

373 uid = config.uid 

374 if not uid: 

375 config.uid = '_s_' + u.sha256([repr(classref), config]) 

376 

377 if config.uid in self.uidMap: 

378 return self.uidMap[config.uid] 

379 

380 return create_node(self, classref, None, config) 

381 

382 

383def root_create_temporary(self, classref, config, **kwargs): 

384 """Implement ``gws.Root.create_temporary``. 

385 

386 The node is not registered in the root and ``post_configure`` is run immediately. 

387 

388 Args: 

389 self: The root. 

390 classref: Class reference. 

391 config: Configuration. 

392 **kwargs: Additional configuration properties. 

393 

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 

402 

403 

404def root_create_application(self, config, **kwargs): 

405 """Implement ``gws.Root.create_application``. 

406 

407 The application gets the fixed uid ``const.APPLICATION_UID``. 

408 

409 Args: 

410 self: The root. 

411 config: Configuration. 

412 **kwargs: Additional configuration properties. 

413 

414 Returns: 

415 The Application object. 

416 """ 

417 config = to_config(config, kwargs) 

418 

419 node = alloc_node(self, 'gws.base.application.core.Object') 

420 node.uid = c.APPLICATION_UID 

421 node.parent = self 

422 node.children = [] 

423 

424 self.nodes.append(node) 

425 self.uidMap[node.uid] = node 

426 self.app = node 

427 

428 self.initialize(node, config) 

429 

430 return node 

431 

432 

433## 

434 

435_ROOT_NAME = 'gws_root_object' 

436 

437 

438def save_root(root, path=None): 

439 """Implement ``gws.save_root``. 

440 

441 Args: 

442 root: The root. 

443 path: File path. 

444 

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 

452 

453 

454def load_root(path=None): 

455 """Implement ``gws.load_root``. 

456 

457 Args: 

458 path: File path. 

459 

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) 

470 

471 

472def activate_root(root): 

473 """Implement ``gws.activate_root``. 

474 

475 Args: 

476 root: The root. 

477 

478 Returns: 

479 The root. 

480 """ 

481 root.activate() 

482 return u.set_app_global(_ROOT_NAME, root) 

483 

484 

485def deactivate_root(): 

486 """Implement ``gws.deactivate_root``.""" 

487 u.delete_app_global(_ROOT_NAME) 

488 

489 

490def get_root(): 

491 """Implement ``gws.get_root``. 

492 

493 Returns: 

494 The root. 

495 """ 

496 

497 def _err(): 

498 raise Error('no configuration root found') 

499 

500 return u.get_app_global(_ROOT_NAME, _err) 

501 

502 

503## 

504 

505 

506def class_name(node): 

507 """Return the full class name of an object. 

508 

509 Args: 

510 node: An object. 

511 

512 Returns: 

513 The module and class name, like ``gws.base.layer.core.Object``. 

514 """ 

515 return node.__class__.__module__ + '.' + node.__class__.__name__ 

516 

517 

518def alloc_node(self, classref, typ=None): 

519 """Create an uninitialized node of the class found in the specs. 

520 

521 Args: 

522 self: The root. 

523 classref: Class reference. 

524 typ: Extension type, for ``ext`` class references. 

525 

526 Returns: 

527 The new node, with ``root``, ``extName`` and ``extType`` set. 

528 

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') 

535 

536 node = cls() 

537 node.root = self 

538 node.extName = getattr(cls, 'extName', '') 

539 node.extType = getattr(cls, 'extType', '') 

540 

541 return node 

542 

543 

544def configure_permissions(self): 

545 """Compute the permissions of a node from its ``access`` and ``permissions`` config. 

546 

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. 

549 

550 Args: 

551 self: The node. 

552 

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 } 

562 

563 p = self.cfg('access') 

564 if p: 

565 perms[Access.read] = u.parse_acl(p) 

566 

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) 

574 

575 v = p.get('edit') 

576 if v: 

577 perms[Access.write] = perms[Access.create] = perms[Access.delete] = u.parse_acl(v) 

578 

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) 

583 

584 return perms 

585 

586 

587def create_node(self, classref, parent, config, temp=False): 

588 """Create and initialize a node. 

589 

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. 

596 

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 = [] 

604 

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 

610 

611 if not temp: 

612 self.nodes.append(node) 

613 self.uidMap[node.uid] = node 

614 

615 if parent: 

616 parent.children.append(node) 

617 

618 return node 

619 

620 

621def find_all_in(root, nodes, classref): 

622 """Filter nodes by a class reference. 

623 

624 Args: 

625 root: The root. 

626 nodes: A list of nodes. 

627 classref: Class reference. If ``None``, all nodes match. 

628 

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)] 

641 

642 

643def find_first_in(root, nodes, classref): 

644 """Find the first node that matches a class reference. 

645 

646 Args: 

647 root: The root. 

648 nodes: A list of nodes. 

649 classref: Class reference. If ``None``, all nodes match. 

650 

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 

656 

657 

658def get_or_generate_uid(self, config): 

659 """Return the uid from the config, or generate a new numeric one. 

660 

661 Args: 

662 self: The root. 

663 config: Configuration. 

664 

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) 

672 

673 

674def is_a(root, node, classref): 

675 """Check if a node matches a class reference. 

676 

677 A class matches by ``isinstance``, a class name matches exactly, 

678 an ``ext`` name matches the node's extension name or its prefix. 

679 

680 Args: 

681 root: The root. 

682 node: The node. 

683 classref: Class reference. 

684 

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 

696 

697 

698def props_of(node, user, *context): 

699 """Implement ``gws.props_of``. 

700 

701 Args: 

702 node: The object. 

703 user: The user. 

704 *context: Context objects for the permission check. 

705 

706 Returns: 

707 A ``Props`` object or ``None``. 

708 

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') 

720 

721 

722def make_props2(obj, user): 

723 """Recursively convert a value to props for a user. 

724 

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. 

727 

728 Args: 

729 obj: An object, ``Data``, dict, list or a scalar value. 

730 user: The user. 

731 

732 Returns: 

733 A scalar, dict or list, or ``None``. 

734 """ 

735 if u.is_atom(obj): 

736 return obj 

737 

738 if isinstance(obj, Object): 

739 if user.acl_bit(Access.read, obj) == c.DENY: 

740 return None 

741 obj = obj.props(user) 

742 

743 if isinstance(obj, Data): 

744 obj = vars(obj) 

745 

746 if u.is_dict(obj): 

747 return u.compact({k: make_props2(v, user) for k, v in obj.items()}) 

748 

749 if u.is_list(obj): 

750 return u.compact([make_props2(v, user) for v in obj]) 

751 

752 return None 

753 

754 

755def register_config_error(self, exc): 

756 """Record an exception in ``configErrors``, with the current config stack. 

757 

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)) 

767 

768 

769def root_config_warning(self, message): 

770 """Implement ``gws.Root.config_warning``. 

771 

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) 

782 

783 

784def config_info(self, message): 

785 """Create a configuration error or warning record. 

786 

787 Args: 

788 self: The root. 

789 message: The message. 

790 

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 

806 

807 

808def config_location_repr(loc): 

809 """Return a short representation of a configuration location. 

810 

811 Args: 

812 loc: A location from ``config_info``. 

813 

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 

826 

827 

828def super_invoke(node, method): 

829 """Invoke a method of every class in the node's MRO that defines it, base classes first. 

830 

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 

837 

838 mro = [] 

839 

840 for cls in type(node).mro(): 

841 try: 

842 if method in vars(cls): 

843 mro.append(cls) 

844 except TypeError: 

845 pass 

846 

847 for cls in reversed(mro): 

848 getattr(cls, method)(node) 

849 

850 

851def to_config(config, defaults): 

852 """Merge keyword defaults and a configuration into a new ``Data`` object. 

853 

854 Values in ``config`` override the defaults, ``None`` values are skipped. 

855 

856 Args: 

857 config: Configuration. 

858 defaults: Default values. 

859 

860 Returns: 

861 A new ``Data`` object. 

862 """ 

863 return u.merge(Data(), defaults, config)