edtlib: move Binding

This is just moving the class definition higher in the file. I am
reordering the classes to make it possible to type annotate the module
in a more readable way.

Git might make the diff look bigger than it really is.
To verify this is just moving code, use 'git diff --minimal'.

Signed-off-by: Martí Bolívar <marti.bolivar@nordicsemi.no>
This commit is contained in:
Martí Bolívar 2023-04-14 01:15:23 -07:00 committed by Marti Bolivar
commit bef3970573

View file

@ -90,6 +90,375 @@ from devicetree._private import _slice_helper
#
class Binding:
"""
Represents a parsed binding.
These attributes are available on Binding objects:
path:
The absolute path to the file defining the binding.
description:
The free-form description of the binding, or None.
compatible:
The compatible string the binding matches.
This may be None. For example, it's None when the Binding is inferred
from node properties. It can also be None for Binding objects created
using 'child-binding:' with no compatible.
prop2specs:
A dict mapping property names to PropertySpec objects
describing those properties' values.
specifier2cells:
A dict that maps specifier space names (like "gpio",
"clock", "pwm", etc.) to lists of cell names.
For example, if the binding YAML contains 'pin' and 'flags' cell names
for the 'gpio' specifier space, like this:
gpio-cells:
- pin
- flags
Then the Binding object will have a 'specifier2cells' attribute mapping
"gpio" to ["pin", "flags"]. A missing key should be interpreted as zero
cells.
raw:
The binding as an object parsed from YAML.
bus:
If nodes with this binding's 'compatible' describe a bus, a string
describing the bus type (like "i2c") or a list describing supported
protocols (like ["i3c", "i2c"]). None otherwise.
Note that this is the raw value from the binding where it can be
a string or a list. Use "buses" instead unless you need the raw
value, where "buses" is always a list.
buses:
Deprived property from 'bus' where 'buses' is a list of bus(es),
for example, ["i2c"] or ["i3c", "i2c"]. Or an empty list if there is
no 'bus:' in this binding.
on_bus:
If nodes with this binding's 'compatible' appear on a bus, a string
describing the bus type (like "i2c"). None otherwise.
child_binding:
If this binding describes the properties of child nodes, then
this is a Binding object for those children; it is None otherwise.
A Binding object's 'child_binding.child_binding' is not None if there
are multiple levels of 'child-binding' descriptions in the binding.
"""
def __init__(self, path, fname2path, raw=None,
require_compatible=True, require_description=True):
"""
Binding constructor.
path:
Path to binding YAML file. May be None.
fname2path:
Map from include files to their absolute paths. Must
not be None, but may be empty.
raw:
Optional raw content in the binding.
This does not have to have any "include:" lines resolved.
May be left out, in which case 'path' is opened and read.
This can be used to resolve child bindings, for example.
require_compatible:
If True, it is an error if the binding does not contain a
"compatible:" line. If False, a missing "compatible:" is
not an error. Either way, "compatible:" must be a string
if it is present in the binding.
require_description:
If True, it is an error if the binding does not contain a
"description:" line. If False, a missing "description:" is
not an error. Either way, "description:" must be a string
if it is present in the binding.
"""
self.path = path
self._fname2path = fname2path
if raw is None:
with open(path, encoding="utf-8") as f:
raw = yaml.load(f, Loader=_BindingLoader)
# Merge any included files into self.raw. This also pulls in
# inherited child binding definitions, so it has to be done
# before initializing those.
self.raw = 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 = Binding(path, fname2path,
raw=raw["child-binding"],
require_compatible=False,
require_description=False)
else:
self.child_binding = None
# Make sure this is a well defined object.
self._check(require_compatible, require_description)
# Initialize look up tables.
self.prop2specs = {}
for prop_name in self.raw.get("properties", {}).keys():
self.prop2specs[prop_name] = PropertySpec(prop_name, self)
self.specifier2cells = {}
for key, val in self.raw.items():
if key.endswith("-cells"):
self.specifier2cells[key[:-len("-cells")]] = val
def __repr__(self):
if self.compatible:
compat = f" for compatible '{self.compatible}'"
else:
compat = ""
basename = os.path.basename(self.path or "")
return f"<Binding {basename}" + compat + ">"
@property
def description(self):
"See the class docstring"
return self.raw.get('description')
@property
def compatible(self):
"See the class docstring"
return self.raw.get('compatible')
@property
def bus(self):
"See the class docstring"
return self.raw.get('bus')
@property
def buses(self):
"See the class docstring"
if self.raw.get('bus') is not None:
return self._buses
else:
return []
@property
def on_bus(self):
"See the class docstring"
return self.raw.get('on-bus')
def _merge_includes(self, raw, binding_path):
# Constructor helper. Merges included files in
# 'raw["include"]' into 'raw' using 'self._include_paths' as a
# source of include files, removing the "include" key while
# doing so.
#
# This treats 'binding_path' as the binding file being built up
# and uses it for error messages.
if "include" not in raw:
return raw
include = raw.pop("include")
# First, merge the included files together. If more than one included
# file has a 'required:' for a particular property, OR the values
# together, so that 'required: true' wins.
merged = {}
if isinstance(include, str):
# Simple scalar string case
_merge_props(merged, self._load_raw(include), None, binding_path,
False)
elif isinstance(include, list):
# List of strings and maps. These types may be intermixed.
for elem in include:
if isinstance(elem, str):
_merge_props(merged, self._load_raw(elem), None,
binding_path, False)
elif isinstance(elem, dict):
name = elem.pop('name', None)
allowlist = elem.pop('property-allowlist', None)
blocklist = elem.pop('property-blocklist', None)
child_filter = elem.pop('child-binding', None)
if elem:
# We've popped out all the valid keys.
_err(f"'include:' in {binding_path} should not have "
f"these unexpected contents: {elem}")
_check_include_dict(name, allowlist, blocklist,
child_filter, binding_path)
contents = self._load_raw(name)
_filter_properties(contents, allowlist, blocklist,
child_filter, binding_path)
_merge_props(merged, contents, None, binding_path, False)
else:
_err(f"all elements in 'include:' in {binding_path} "
"should be either strings or maps with a 'name' key "
"and optional 'property-allowlist' or "
f"'property-blocklist' keys, but got: {elem}")
else:
# Invalid item.
_err(f"'include:' in {binding_path} "
f"should be a string or list, but has type {type(include)}")
# Next, merge the merged included files into 'raw'. Error out if
# 'raw' has 'required: false' while the merged included files have
# 'required: true'.
_merge_props(raw, merged, None, binding_path, check_required=True)
return raw
def _load_raw(self, fname):
# Returns the contents of the binding given by 'fname' after merging
# any bindings it lists in 'include:' into it. 'fname' is just the
# basename of the file, so we check that there aren't multiple
# candidates.
path = self._fname2path.get(fname)
if not path:
_err(f"'{fname}' not found")
with open(path, encoding="utf-8") as f:
contents = yaml.load(f, Loader=_BindingLoader)
return self._merge_includes(contents, path)
def _check(self, require_compatible, require_description):
# Does sanity checking on the binding.
raw = self.raw
if "compatible" in raw:
compatible = raw["compatible"]
if not isinstance(compatible, str):
_err(f"malformed 'compatible: {compatible}' "
f"field in {self.path} - "
f"should be a string, not {type(compatible).__name__}")
elif require_compatible:
_err(f"missing 'compatible' in {self.path}")
if "description" in raw:
description = raw["description"]
if not isinstance(description, str) or not description:
_err(f"malformed or empty 'description' in {self.path}")
elif require_description:
_err(f"missing 'description' in {self.path}")
# Allowed top-level keys. The 'include' key should have been
# removed by _load_raw() already.
ok_top = {"description", "compatible", "bus", "on-bus",
"properties", "child-binding"}
# Descriptive errors for legacy bindings.
legacy_errors = {
"#cells": "expected *-cells syntax",
"child": "use 'bus: <bus>' instead",
"child-bus": "use 'bus: <bus>' instead",
"parent": "use 'on-bus: <bus>' instead",
"parent-bus": "use 'on-bus: <bus>' instead",
"sub-node": "use 'child-binding' instead",
"title": "use 'description' instead",
}
for key in raw:
if key in legacy_errors:
_err(f"legacy '{key}:' in {self.path}, {legacy_errors[key]}")
if key not in ok_top and not key.endswith("-cells"):
_err(f"unknown key '{key}' in {self.path}, "
"expected one of {', '.join(ok_top)}, or *-cells")
if "bus" in raw:
bus = raw["bus"]
if not isinstance(bus, str) and \
(not isinstance(bus, list) and \
not all(isinstance(elem, str) for elem in bus)):
_err(f"malformed 'bus:' value in {self.path}, "
"expected string or list of strings")
if isinstance(bus, list):
self._buses = bus
else:
# Convert bus into a list
self._buses = [bus]
if "on-bus" in raw and \
not isinstance(raw["on-bus"], str):
_err(f"malformed 'on-bus:' value in {self.path}, "
"expected string")
self._check_properties()
for key, val in raw.items():
if key.endswith("-cells"):
if not isinstance(val, list) or \
not all(isinstance(elem, str) for elem in val):
_err(f"malformed '{key}:' in {self.path}, "
"expected a list of strings")
def _check_properties(self):
# _check() helper for checking the contents of 'properties:'.
raw = self.raw
if "properties" not in raw:
return
ok_prop_keys = {"description", "type", "required",
"enum", "const", "default", "deprecated",
"specifier-space"}
for prop_name, options in raw["properties"].items():
for key in options:
if key not in ok_prop_keys:
_err(f"unknown setting '{key}' in "
f"'properties: {prop_name}: ...' in {self.path}, "
f"expected one of {', '.join(ok_prop_keys)}")
_check_prop_by_type(prop_name, options, self.path)
for true_false_opt in ["required", "deprecated"]:
if true_false_opt in options:
option = options[true_false_opt]
if not isinstance(option, bool):
_err(f"malformed '{true_false_opt}:' setting '{option}' "
f"for '{prop_name}' in 'properties' in {self.path}, "
"expected true/false")
if options.get("deprecated") and options.get("required"):
_err(f"'{prop_name}' in 'properties' in {self.path} should not "
"have both 'deprecated' and 'required' set")
if "description" in options and \
not isinstance(options["description"], str):
_err("missing, malformed, or empty 'description' for "
f"'{prop_name}' in 'properties' in {self.path}")
if "enum" in options and not isinstance(options["enum"], list):
_err(f"enum in {self.path} for property '{prop_name}' "
"is not a list")
class EDT:
"""
Represents a devicetree augmented with information from bindings.
@ -1744,375 +2113,6 @@ class Property:
return "<Property, {}>".format(", ".join(fields))
class Binding:
"""
Represents a parsed binding.
These attributes are available on Binding objects:
path:
The absolute path to the file defining the binding.
description:
The free-form description of the binding, or None.
compatible:
The compatible string the binding matches.
This may be None. For example, it's None when the Binding is inferred
from node properties. It can also be None for Binding objects created
using 'child-binding:' with no compatible.
prop2specs:
A dict mapping property names to PropertySpec objects
describing those properties' values.
specifier2cells:
A dict that maps specifier space names (like "gpio",
"clock", "pwm", etc.) to lists of cell names.
For example, if the binding YAML contains 'pin' and 'flags' cell names
for the 'gpio' specifier space, like this:
gpio-cells:
- pin
- flags
Then the Binding object will have a 'specifier2cells' attribute mapping
"gpio" to ["pin", "flags"]. A missing key should be interpreted as zero
cells.
raw:
The binding as an object parsed from YAML.
bus:
If nodes with this binding's 'compatible' describe a bus, a string
describing the bus type (like "i2c") or a list describing supported
protocols (like ["i3c", "i2c"]). None otherwise.
Note that this is the raw value from the binding where it can be
a string or a list. Use "buses" instead unless you need the raw
value, where "buses" is always a list.
buses:
Deprived property from 'bus' where 'buses' is a list of bus(es),
for example, ["i2c"] or ["i3c", "i2c"]. Or an empty list if there is
no 'bus:' in this binding.
on_bus:
If nodes with this binding's 'compatible' appear on a bus, a string
describing the bus type (like "i2c"). None otherwise.
child_binding:
If this binding describes the properties of child nodes, then
this is a Binding object for those children; it is None otherwise.
A Binding object's 'child_binding.child_binding' is not None if there
are multiple levels of 'child-binding' descriptions in the binding.
"""
def __init__(self, path, fname2path, raw=None,
require_compatible=True, require_description=True):
"""
Binding constructor.
path:
Path to binding YAML file. May be None.
fname2path:
Map from include files to their absolute paths. Must
not be None, but may be empty.
raw:
Optional raw content in the binding.
This does not have to have any "include:" lines resolved.
May be left out, in which case 'path' is opened and read.
This can be used to resolve child bindings, for example.
require_compatible:
If True, it is an error if the binding does not contain a
"compatible:" line. If False, a missing "compatible:" is
not an error. Either way, "compatible:" must be a string
if it is present in the binding.
require_description:
If True, it is an error if the binding does not contain a
"description:" line. If False, a missing "description:" is
not an error. Either way, "description:" must be a string
if it is present in the binding.
"""
self.path = path
self._fname2path = fname2path
if raw is None:
with open(path, encoding="utf-8") as f:
raw = yaml.load(f, Loader=_BindingLoader)
# Merge any included files into self.raw. This also pulls in
# inherited child binding definitions, so it has to be done
# before initializing those.
self.raw = 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 = Binding(path, fname2path,
raw=raw["child-binding"],
require_compatible=False,
require_description=False)
else:
self.child_binding = None
# Make sure this is a well defined object.
self._check(require_compatible, require_description)
# Initialize look up tables.
self.prop2specs = {}
for prop_name in self.raw.get("properties", {}).keys():
self.prop2specs[prop_name] = PropertySpec(prop_name, self)
self.specifier2cells = {}
for key, val in self.raw.items():
if key.endswith("-cells"):
self.specifier2cells[key[:-len("-cells")]] = val
def __repr__(self):
if self.compatible:
compat = f" for compatible '{self.compatible}'"
else:
compat = ""
basename = os.path.basename(self.path or "")
return f"<Binding {basename}" + compat + ">"
@property
def description(self):
"See the class docstring"
return self.raw.get('description')
@property
def compatible(self):
"See the class docstring"
return self.raw.get('compatible')
@property
def bus(self):
"See the class docstring"
return self.raw.get('bus')
@property
def buses(self):
"See the class docstring"
if self.raw.get('bus') is not None:
return self._buses
else:
return []
@property
def on_bus(self):
"See the class docstring"
return self.raw.get('on-bus')
def _merge_includes(self, raw, binding_path):
# Constructor helper. Merges included files in
# 'raw["include"]' into 'raw' using 'self._include_paths' as a
# source of include files, removing the "include" key while
# doing so.
#
# This treats 'binding_path' as the binding file being built up
# and uses it for error messages.
if "include" not in raw:
return raw
include = raw.pop("include")
# First, merge the included files together. If more than one included
# file has a 'required:' for a particular property, OR the values
# together, so that 'required: true' wins.
merged = {}
if isinstance(include, str):
# Simple scalar string case
_merge_props(merged, self._load_raw(include), None, binding_path,
False)
elif isinstance(include, list):
# List of strings and maps. These types may be intermixed.
for elem in include:
if isinstance(elem, str):
_merge_props(merged, self._load_raw(elem), None,
binding_path, False)
elif isinstance(elem, dict):
name = elem.pop('name', None)
allowlist = elem.pop('property-allowlist', None)
blocklist = elem.pop('property-blocklist', None)
child_filter = elem.pop('child-binding', None)
if elem:
# We've popped out all the valid keys.
_err(f"'include:' in {binding_path} should not have "
f"these unexpected contents: {elem}")
_check_include_dict(name, allowlist, blocklist,
child_filter, binding_path)
contents = self._load_raw(name)
_filter_properties(contents, allowlist, blocklist,
child_filter, binding_path)
_merge_props(merged, contents, None, binding_path, False)
else:
_err(f"all elements in 'include:' in {binding_path} "
"should be either strings or maps with a 'name' key "
"and optional 'property-allowlist' or "
f"'property-blocklist' keys, but got: {elem}")
else:
# Invalid item.
_err(f"'include:' in {binding_path} "
f"should be a string or list, but has type {type(include)}")
# Next, merge the merged included files into 'raw'. Error out if
# 'raw' has 'required: false' while the merged included files have
# 'required: true'.
_merge_props(raw, merged, None, binding_path, check_required=True)
return raw
def _load_raw(self, fname):
# Returns the contents of the binding given by 'fname' after merging
# any bindings it lists in 'include:' into it. 'fname' is just the
# basename of the file, so we check that there aren't multiple
# candidates.
path = self._fname2path.get(fname)
if not path:
_err(f"'{fname}' not found")
with open(path, encoding="utf-8") as f:
contents = yaml.load(f, Loader=_BindingLoader)
return self._merge_includes(contents, path)
def _check(self, require_compatible, require_description):
# Does sanity checking on the binding.
raw = self.raw
if "compatible" in raw:
compatible = raw["compatible"]
if not isinstance(compatible, str):
_err(f"malformed 'compatible: {compatible}' "
f"field in {self.path} - "
f"should be a string, not {type(compatible).__name__}")
elif require_compatible:
_err(f"missing 'compatible' in {self.path}")
if "description" in raw:
description = raw["description"]
if not isinstance(description, str) or not description:
_err(f"malformed or empty 'description' in {self.path}")
elif require_description:
_err(f"missing 'description' in {self.path}")
# Allowed top-level keys. The 'include' key should have been
# removed by _load_raw() already.
ok_top = {"description", "compatible", "bus", "on-bus",
"properties", "child-binding"}
# Descriptive errors for legacy bindings.
legacy_errors = {
"#cells": "expected *-cells syntax",
"child": "use 'bus: <bus>' instead",
"child-bus": "use 'bus: <bus>' instead",
"parent": "use 'on-bus: <bus>' instead",
"parent-bus": "use 'on-bus: <bus>' instead",
"sub-node": "use 'child-binding' instead",
"title": "use 'description' instead",
}
for key in raw:
if key in legacy_errors:
_err(f"legacy '{key}:' in {self.path}, {legacy_errors[key]}")
if key not in ok_top and not key.endswith("-cells"):
_err(f"unknown key '{key}' in {self.path}, "
"expected one of {', '.join(ok_top)}, or *-cells")
if "bus" in raw:
bus = raw["bus"]
if not isinstance(bus, str) and \
(not isinstance(bus, list) and \
not all(isinstance(elem, str) for elem in bus)):
_err(f"malformed 'bus:' value in {self.path}, "
"expected string or list of strings")
if isinstance(bus, list):
self._buses = bus
else:
# Convert bus into a list
self._buses = [bus]
if "on-bus" in raw and \
not isinstance(raw["on-bus"], str):
_err(f"malformed 'on-bus:' value in {self.path}, "
"expected string")
self._check_properties()
for key, val in raw.items():
if key.endswith("-cells"):
if not isinstance(val, list) or \
not all(isinstance(elem, str) for elem in val):
_err(f"malformed '{key}:' in {self.path}, "
"expected a list of strings")
def _check_properties(self):
# _check() helper for checking the contents of 'properties:'.
raw = self.raw
if "properties" not in raw:
return
ok_prop_keys = {"description", "type", "required",
"enum", "const", "default", "deprecated",
"specifier-space"}
for prop_name, options in raw["properties"].items():
for key in options:
if key not in ok_prop_keys:
_err(f"unknown setting '{key}' in "
f"'properties: {prop_name}: ...' in {self.path}, "
f"expected one of {', '.join(ok_prop_keys)}")
_check_prop_by_type(prop_name, options, self.path)
for true_false_opt in ["required", "deprecated"]:
if true_false_opt in options:
option = options[true_false_opt]
if not isinstance(option, bool):
_err(f"malformed '{true_false_opt}:' setting '{option}' "
f"for '{prop_name}' in 'properties' in {self.path}, "
"expected true/false")
if options.get("deprecated") and options.get("required"):
_err(f"'{prop_name}' in 'properties' in {self.path} should not "
"have both 'deprecated' and 'required' set")
if "description" in options and \
not isinstance(options["description"], str):
_err("missing, malformed, or empty 'description' for "
f"'{prop_name}' in 'properties' in {self.path}")
if "enum" in options and not isinstance(options["enum"], list):
_err(f"enum in {self.path} for property '{prop_name}' "
"is not a list")
def bindings_from_paths(yaml_paths, ignore_errors=False):
"""
Get a list of Binding objects from the yaml files 'yaml_paths'.