Skip to content

Commit

Permalink
edtlib: preserve the origin of child-bindings definitions
Browse files Browse the repository at this point in the history
All child bindings are initialized at once after included
binding files have been merged, with the top-level binding
file set as their origin.

As a consequence, properties of child bindings appear
to all come from ("last modified" semantic) the top-level file.

For example, properties of a channel child of an ADC controller
with compatible "nordic,nrf-saadc" will all appear last modified
by nordic,nrf-saadc.yaml instead of adc-controller.yaml.

We should use the same approach we've already come to
for property specifications:
- #5
- zephyrproject-rtos/zephyr#65221
  • Loading branch information
dottspina committed Sep 6, 2024
1 parent 5cdb1bf commit 6970a15
Show file tree
Hide file tree
Showing 9 changed files with 541 additions and 16 deletions.
139 changes: 123 additions & 16 deletions src/devicetree/edtlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,25 +223,31 @@ def __init__(self, path: Optional[str], fname2path: Dict[str, str],
# this binding itself defines or modifies
self.prop2specs: Dict[str, 'PropertySpec'] = {}

# This binding's child-binding,
# using the same approach as above for property specifications:
# - we initially have no child-binding
# - we may inherit child-binding definitions from other bindings
# when merging included files
# - eventually, we may update inherited child-binding definitions
# with the content of the "child-binding" element of this binding's file
self.child_binding: Optional[Binding] = None
# Save child-binding element before merging.
if "child-binding" in raw:
raw_child_binding = deepcopy(raw["child-binding"])
else:
raw_child_binding = None

# Merge any included files into self.raw. This also pulls in
# inherited child binding definitions, so it has to be done
# before initializing those.
# inherited child binding definitions.
self.raw: dict = self._merge_includes(raw, self.path)

# Recursively initialize any child bindings. These don't
# require a 'compatible' or 'description' to be well defined,
# but they must be dicts.
if "child-binding" in raw:
if not isinstance(raw["child-binding"], dict):
_err(f"malformed 'child-binding:' in {self.path}, "
"expected a binding (dictionary with keys/values)")
self.child_binding: Optional['Binding'] = Binding(
path, fname2path,
raw=raw["child-binding"],
require_compatible=False,
require_description=False)
else:
self.child_binding = None
# When this binding's file contains a child-binding element,
# its content will:
# - update the child-binding definitions inherited from included files,
# if any
# - or initialize this binding's child-binding
if raw_child_binding:
self._update_child_binding(raw_child_binding)

# Make sure this is a well defined object.
self._check(require_compatible, require_description)
Expand Down Expand Up @@ -414,6 +420,17 @@ def _load_raw(self, fname: str,
# Register included property specs.
self._add_included_prop2specs(fname, contents, allowlist, blocklist)

# Pull in child-binding definitions we may inherit
# from the included file.
if "child-binding" in contents:
self._add_included_child_binding(
path,
contents["child-binding"],
# Properties in child-binding have already been filtered,
# child_filter is only used to determine their origin.
child_filter,
)

return self._merge_includes(contents, path)

def _add_included_prop2specs(self, fname: str, contents: dict,
Expand Down Expand Up @@ -455,6 +472,96 @@ def _add_included_prop2specs(self, fname: str, contents: dict,
if prop not in self.prop2specs:
self.prop2specs[prop] = spec

# Add child-binding definitions inherited from an included binding.
# - path: YAML file containing the "child-binding" element
# - raw: The content of the "child-binding" element
# - child_filter: filters applied by the including binding
def _add_included_child_binding(
self,
path: Optional[str],
raw: Dict[str, Any],
child_filter: Optional[dict],
) -> None:
inc_child_binding = self._mk_child_binding(path, raw)

if self.child_binding or child_filter:
# Take "last modified" semantic when:
# - we've already included a child-binding
# - the including binding applies some filter
# to the included child-binding's properties
child_binding = self._mk_child_binding(self.path, {})
if self.child_binding:
self._merge_child_binding(child_binding, self.child_binding)
self.child_binding = child_binding

if self.child_binding:
self._merge_child_binding(self.child_binding, inc_child_binding)
else:
self.child_binding = inc_child_binding

# Merge child bindings:
# - update child_binding's properties with properties
# from other_child_binding
# - update child_binding's cell specifiers with specifiers
# from other_child_binding
# - recursively merge child bindings of child_binding
# and other_child_binding
def _merge_child_binding(
self,
child_binding: "Binding",
other_child_binding: "Binding",
) -> None:
child_binding.prop2specs.update(other_child_binding.prop2specs)
child_binding.specifier2cells.update(
other_child_binding.specifier2cells
)

other_grandchild_binding = other_child_binding.child_binding
if other_grandchild_binding:
if child_binding.child_binding:
self._merge_child_binding(
child_binding.child_binding, other_grandchild_binding
)
else:
child_binding.child_binding = other_grandchild_binding

def _update_child_binding(self, raw: Dict[str, Any]) -> None:
# Child-binding specifications from this binding's file.
last_child_binding = self._mk_child_binding(self.path, raw)

if self.child_binding:
# This binding's child-binding is "last modified" here,
# create empty new one with "self" origin (path).
child_binding = self._mk_child_binding(self.path, {})
# First, add child-binding specifications inherited
# from included files.
self._merge_child_binding(child_binding, self.child_binding)
# Then, add/modify child-binding specifications
# from this binding's file.
self._merge_child_binding(child_binding, last_child_binding)
self.child_binding = child_binding
else:
self.child_binding = last_child_binding

def _mk_child_binding(
self, path: Optional[str], raw: Dict[str, Any]
) -> "Binding":
if not isinstance(raw, dict):
_err(
f"malformed 'child-binding:' in {path}, "
"expected a binding (dictionary with keys/values)"
)
return Binding(
path,
self._fname2path,
raw,
require_compatible=False,
require_description=False,
# Filters already applied.
inc_allowlist=None,
inc_blocklist=None,
)

def _check(self, require_compatible: bool, require_description: bool):
# Does sanity checking on the binding.

Expand Down
58 changes: 58 additions & 0 deletions tests/res/edtlib/allow-and-blocklist-anylevel.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# SPDX-License-Identifier: BSD-3-Clause

description: |
Extends edtlib's upstream allow-and-blocklist-multilevel.yaml test case.
compatible: vnd,allow-and-blocklist-anylevel

# child-binding:
# + child-prop-1
#
# grandchild-binding:
# + gandchild-prop-1
# + gandchild-prop-2
include:
- name: base.yaml
property-allowlist: [x]

child-binding:
property-allowlist: [child-prop-1]

child-binding:
property-blocklist: [grandchild-prop-blocked]

properties:
ceil-prop:
type: int

child-binding:
include:
# child-binding:
# ~ child-prop-1
# + x
# + z
- name: base.yaml
property-blocklist: [y]

# grandchild-binding:
# ~ gandchild-prop-1
# ~ gandchild-prop-2
# + child-prop-1
# + child-prop-1
child-binding:
property-blocklist: [child-prop-blocked]

# grandgrandchild-binding:
# + gandchild-prop-1
# + gandchild-prop-2
child-binding:
property-blocklist: [grandchild-prop-blocked]

properties:
cb-prop:
type: int

child-binding:
properties:
gcbb-prop:
type: int
16 changes: 16 additions & 0 deletions tests/res/edtlib/allow-and-blocklist-multilevel.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# SPDX-License-Identifier: BSD-3-Clause

description: |
Includes can be added at any level, so can property-allowlist and
property-blocklist.
compatible: allow-and-blocklist-multilevel

include:
- name: include.yaml
property-allowlist: [x]

child-binding:
include:
- name: include.yaml
property-blocklist: [y]
33 changes: 33 additions & 0 deletions tests/res/edtlib/base.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# SPDX-License-Identifier: BSD-3-Clause

description: |
Extends edtlib's upstream include.yaml with an additional property
for testing nested "child-binding:" filters.
See allow-and-blocklist-anylevel.yaml.
properties:
x:
type: int
y:
type: int
z:
type: int

child-binding:
properties:
child-prop-1:
type: int
child-prop-2:
type: int
child-prop-blocked:
type: int

child-binding:
properties:
grandchild-prop-1:
type: int
grandchild-prop-2:
type: int
grandchild-prop-blocked:
type: int
24 changes: 24 additions & 0 deletions tests/res/edtlib/include.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# SPDX-License-Identifier: BSD-3-Clause

description: Test file for including other bindings
compatible: include
properties:
x:
type: int
y:
type: int
z:
type: int
child-binding:
properties:
child-prop-1:
type: int
child-prop-2:
type: int

child-binding:
properties:
grandchild-prop-1:
type: int
grandchild-prop-2:
type: int
17 changes: 17 additions & 0 deletions tests/res/edtlib/left.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# SPDX-License-Identifier: BSD-3-Clause

description: |
Left include file for the multi-includes test case.
properties:
left-prop-1:
type: int
left-prop-2:
type: int

child-binding:
properties:
left-child-prop-1:
type: int
left-child-prop-2:
type: int
30 changes: 30 additions & 0 deletions tests/res/edtlib/left_right.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SPDX-License-Identifier: BSD-3-Clause

description: |
Test multi-includes from top-level binding.
compatible: vnd,left_right

include:
- name: left.yaml
property-allowlist: [left-prop-1]
child-binding:
property-allowlist: [left-child-prop-1]

- name: right.yaml
property-allowlist: [right-prop-2]
child-binding:
property-allowlist: [right-child-prop-2]

properties:
left-right-prop:
type: int

child-binding:
properties:
child-prop:
type: int
child-binding:
properties:
grandchild-prop:
type: int
17 changes: 17 additions & 0 deletions tests/res/edtlib/right.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# SPDX-License-Identifier: BSD-3-Clause

description: |
Right include file for the multi-includes test case.
properties:
right-prop-1:
type: int
right-prop-2:
type: int

child-binding:
properties:
right-child-prop-1:
type: int
right-child-prop-2:
type: int
Loading

0 comments on commit 6970a15

Please sign in to comment.