View arch normalization, inheritance, and resolution.
The arch stored in ir.ui.view.arch is JSON, but its authoring form is
deliberately terse ("fields": ["name", "age"]). On the way in, the
normalizer promotes those shortcuts to addressable shapes
({"name": "name"}); on the way out, the resolver walks the parent
chain and applies operations from every extension view in ascending
priority order.
Operation shape
{"op": "",
"target": [, ...],
"value": }
Target segments (Stage 7 Slice C — pyvelm/docs/web-layer.md):
- `str` + dict node → dict key lookup
- `str` + list of dicts → match the dict whose `name` equals it
(shorthand for `{"name": "<str>"}`)
- `int` + list → positional index
- `dict` + list of dicts → predicate; first list entry where every
key/value in the dict matches. Matches
Odoo's `xpath="//tag[@a='1'][@b='2']"`
attribute-filter idiom.
- `"**"` as the **first** segment → start the lookup anywhere in
the arch (depth-first). The next
segment selects the entry point;
remaining segments resolve normally
from there. Equivalent to Odoo's
`xpath="//.../foo"` prefix.
callable_ref
callable_ref(fn: Callable[..., Any]) -> str
Serialize a view-schema callable as module:qualname.
Source code in pyvelm/views.py
| def callable_ref(fn: Callable[..., Any]) -> str:
"""Serialize a view-schema callable as ``module:qualname``."""
mod = getattr(fn, "__module__", None)
qual = getattr(fn, "__qualname__", None)
if not mod or not qual or qual == "<lambda>":
raise TypeError(
"View schema callables must be module-level named functions; "
f"cannot serialize {fn!r}"
)
return f"{mod}:{qual}"
|
resolve_callable_ref
resolve_callable_ref(ref: str) -> Callable[..., Any]
Import a callable previously stored via :func:callable_ref.
Source code in pyvelm/views.py
| def resolve_callable_ref(ref: str) -> Callable[..., Any]:
"""Import a callable previously stored via :func:`callable_ref`."""
if ":" not in ref:
raise ValueError(f"Invalid callable ref {ref!r}")
mod_name, attr = ref.rsplit(":", 1)
mod = importlib.import_module(mod_name)
obj: Any = mod
for part in attr.split("."):
obj = getattr(obj, part)
if not callable(obj):
raise TypeError(f"Callable ref {ref!r} did not resolve to a function")
return obj
|
encode_arch_callables
encode_arch_callables(node: Any) -> Any
Replace callables with JSON-safe refs before persisting view arch.
Source code in pyvelm/views.py
| def encode_arch_callables(node: Any) -> Any:
"""Replace callables with JSON-safe refs before persisting view arch."""
if callable(node):
return {_CALLABLE_MARKER: callable_ref(node)}
if isinstance(node, dict):
return {k: encode_arch_callables(v) for k, v in node.items()}
if isinstance(node, list):
return [encode_arch_callables(item) for item in node]
return node
|
decode_arch_callables
decode_arch_callables(node: Any) -> Any
Restore callables from refs produced by :func:encode_arch_callables.
Source code in pyvelm/views.py
| def decode_arch_callables(node: Any) -> Any:
"""Restore callables from refs produced by :func:`encode_arch_callables`."""
if isinstance(node, dict):
if list(node.keys()) == [_CALLABLE_MARKER]:
return resolve_callable_ref(node[_CALLABLE_MARKER])
return {k: decode_arch_callables(v) for k, v in node.items()}
if isinstance(node, list):
return [decode_arch_callables(item) for item in node]
return node
|
normalize_arch
normalize_arch(arch: dict, view_type: str) -> dict
Promote authoring-sugar strings to dicts in known list positions.
Returns a new dict; never mutates the input. Idempotent — passing an
already-normalized arch is a no-op.
Source code in pyvelm/views.py
| def normalize_arch(arch: dict, view_type: str) -> dict:
"""Promote authoring-sugar strings to dicts in known list positions.
Returns a new dict; never mutates the input. Idempotent — passing an
already-normalized arch is a no-op.
"""
if arch is None:
return arch
result = copy.deepcopy(arch)
for path in _LIST_PROMOTION_PATHS.get(view_type, []):
_promote_list_at(result, list(path))
return result
|
apply_operations
apply_operations(arch: dict, operations: list[dict]) -> dict
Apply each op to arch in order, in place. Returns the same dict.
Caller should pass a deepcopy if they need the original preserved.
Source code in pyvelm/views.py
| def apply_operations(arch: dict, operations: list[dict]) -> dict:
"""Apply each op to `arch` in order, in place. Returns the same dict.
Caller should pass a deepcopy if they need the original preserved.
"""
for op in operations:
kind = op["op"]
target = list(op["target"])
# `**` as the first segment finds any descendant whose next
# step matches. Effectively: "ignore the path, just find this
# node anywhere." Resolves to a normal target rooted at the
# match's parent so the rest of the segment list keeps the
# existing semantics.
if target and target and target[0] == "**":
if len(target) < 2:
raise ValueError(
"'**' must be followed by at least one selector "
"(`name` string or predicate dict)"
)
entry_seg = target[1]
try:
root, _ = _find_descendant(arch, entry_seg)
except KeyError:
raise KeyError(
f"`**` lookup found no descendant matching {entry_seg!r}"
)
# Splice: drop `**`, anchor at the discovered subtree.
# The remaining target list (entry_seg + rest) resolves
# normally against `root`.
target = target[1:]
arch_root = root
else:
arch_root = arch
# `update` walks all the way INTO the target (must end on a dict)
# and merges in op["value"]. Equivalent to Odoo's
# `position="attributes"` with multiple <attribute> children.
if kind == "update":
node = arch_root
for seg in target:
node = _step_into(node, seg)
if not isinstance(node, dict):
raise ValueError(
f"'update' target must resolve to a dict, got "
f"{type(node).__name__} at {target!r}"
)
value = op["value"]
if not isinstance(value, dict):
raise ValueError(
f"'update' value must be a dict, got {type(value).__name__}"
)
node.update(value)
continue
if not target:
raise ValueError(f"Operation {op!r} has empty target")
# Walk into the parent container.
parent = arch_root
for seg in target[:-1]:
parent = _step_into(parent, seg)
last = target[-1]
if kind == "remove":
position = _resolve_position(parent, last)
del parent[position]
elif kind in ("set", "replace"):
# Granular attribute set: the final segment may be a new key
# on a dict parent (think: add `readonly` to a field). Existing
# keys are overwritten. Lists still require the entry to exist
# — use `before`/`after` to grow a list.
if isinstance(parent, dict) and isinstance(last, str):
parent[last] = op["value"]
else:
position = _resolve_position(parent, last)
parent[position] = op["value"]
elif kind == "before":
if not isinstance(parent, list):
raise ValueError(
f"'before' requires a list parent, got "
f"{type(parent).__name__} at target {target!r}"
)
position = _resolve_position(parent, last)
parent.insert(position, op["value"])
elif kind == "after":
if not isinstance(parent, list):
raise ValueError(
f"'after' requires a list parent, got "
f"{type(parent).__name__} at target {target!r}"
)
position = _resolve_position(parent, last)
parent.insert(position + 1, op["value"])
else:
raise ValueError(f"Unknown view-arch op: {kind!r}")
return arch
|
resolve_arch
resolve_arch(view) -> dict
Return the fully-resolved arch for view.
If view is an extension, walks up to the root base view first.
Then applies every extension in the family in (priority, id) order.
Depth-first: extensions-of-extensions are applied after their parent
extension's ops, in the same priority sweep.
ACL is bypassed for the whole resolution — view arch is system metadata
(same policy as render._search_ui_views / _menu).
Source code in pyvelm/views.py
| def resolve_arch(view) -> dict:
"""Return the fully-resolved arch for `view`.
If `view` is an extension, walks up to the root base view first.
Then applies every extension in the family in (priority, id) order.
Depth-first: extensions-of-extensions are applied after their parent
extension's ops, in the same priority sweep.
ACL is bypassed for the whole resolution — view arch is system metadata
(same policy as ``render._search_ui_views`` / ``_menu``).
"""
env = view.env
prev = env._acl_bypass
env._acl_bypass = True
try:
root = view
while root.inherit_id:
root = root.inherit_id
if not root.arch:
raise ValueError(
f"View {root.module}.{root.name} has no arch (extension views "
f"need an inherit_id to a base view)"
)
arch = decode_arch_callables(copy.deepcopy(json.loads(root.arch)))
_apply_chain(root, arch)
# Normalize the resolved arch so that any plain-string entries
# inserted by before/after/replace operations are promoted to dicts.
view_type = root.view_type
if view_type:
arch = normalize_arch(arch, view_type)
return arch
finally:
env._acl_bypass = prev
|