1"""NotebookNode - adding attribute access to dicts"""
2
3from __future__ import annotations
4
5from collections.abc import Mapping
6from copy import deepcopy as _deepcopy
7
8from ._struct import Struct
9
10# Values `deepcopy` returns as-is, so they can be stored without recursing.
11_ATOMIC_TYPES = (str, int, float, bool, bytes, type(None), complex)
12
13
14class NotebookNode(Struct):
15 """A dict-like node with attribute-access"""
16
17 def __setitem__(self, key, value):
18 """Set an item on the notebook."""
19 if isinstance(value, Mapping) and not isinstance(value, NotebookNode):
20 value = from_dict(value)
21 super().__setitem__(key, value)
22
23 def __deepcopy__(self, memo):
24 """Deep-copy the node without going through `copy._reconstruct`.
25
26 `copy.deepcopy` only takes its fast path for exact `dict`s, so a `dict`
27 subclass falls back to the generic `__reduce_ex__` machinery once per
28 node. Notebooks get deep-copied on every `isvalid`, `normalize` and
29 `writes` call, which makes that fallback worth avoiding.
30 """
31 new = self.__class__()
32 memo[id(self)] = new
33 for key, value in self.items():
34 value_type = type(value)
35 if value_type in _ATOMIC_TYPES:
36 dict.__setitem__(new, key, value)
37 elif value_type is list:
38 # a plain dict inside a list stays a plain dict, as it would
39 # under the generic deepcopy
40 dict.__setitem__(new, key, [_deepcopy(item, memo) for item in value])
41 else:
42 # assign through __setitem__ so a Mapping value is coerced to a
43 # NotebookNode, which is what the generic deepcopy path did
44 new[key] = _deepcopy(value, memo)
45 # restore instance state (Struct's `_allownew`) last: with `_allownew`
46 # False, __setitem__ refuses new keys.
47 if self.__dict__:
48 new.__dict__.update(_deepcopy(self.__dict__, memo))
49 return new
50
51 def update(self, *args, **kwargs):
52 """
53 A dict-like update method based on CPython's MutableMapping `update`
54 method.
55 """
56 if len(args) > 1:
57 raise TypeError("update expected at most 1 arguments, got %d" % len(args))
58 if args:
59 other = args[0]
60 if isinstance(other, Mapping): # noqa: SIM114
61 for key in other:
62 self[key] = other[key]
63 elif hasattr(other, "keys"):
64 for key in other:
65 self[key] = other[key]
66 else:
67 for key, value in other:
68 self[key] = value
69 for key, value in kwargs.items():
70 self[key] = value
71
72
73def from_dict(d):
74 """Convert dict to dict-like NotebookNode
75
76 Recursively converts any dict in the container to a NotebookNode.
77 This does not check that the contents of the dictionary make a valid
78 notebook or part of a notebook.
79 """
80 if isinstance(d, dict):
81 return NotebookNode({k: from_dict(v) for k, v in d.items()})
82 if isinstance(d, (tuple, list)):
83 return [from_dict(i) for i in d]
84 return d