From 7165b77a81f35bd76fbbdc391f8c76f3d64d58a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=AD=20Bol=C3=ADvar?= Date: Mon, 12 Oct 2020 12:32:25 -0700 Subject: [PATCH] scripts: edtlib: refactor for first class bindings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add two new types: Binding and PropertySpec. - Binding is a first-class abstraction for a devicetree binding file as defined by a YAML file in the Zephyr syntax. - PropertySpec is a helper type which represents a property definition within a Binding. Make the Binding constructor a new entry point to the library. This enables users to deal with bindings as standalone entities, apart from how they characterize a particular devicetree. Rework the EDT and Node internals that deal with bindings as dict values to use the Binding type instead. To make this less ambiguous, use the variable name 'raw' when we're dealing with a binding as it's parsed from YAML, and 'binding' when we're dealing with a Python Binding object. This commit drops support for the following legacy bindings keys - '#cells': use '*-cells' instead (e.g. 'gpio-cells', 'pwm-cells') - "child-bus: foo" and "child: bus: foo": use "bus:" instead - "parent-bus" and "parent: bus: ": use "on-bus:" instead Officially, legacy bindings have been gone since 6bf761fc0a2811b037 ("dts: Remove support for deprecated DTS binding syntax"), so this is vestigial code, and I couldn't find any in-tree users. It also drops the convention in this file that ""-strings are preferred. I honestly don't understand why this was ever enforced; the file itself admits single quotes are common in Python and we use them elsewhere in Zephyr's Python code. Signed-off-by: Martí Bolívar --- scripts/dts/edtlib.py | 845 ++++++++++++++++++++++++++---------------- 1 file changed, 522 insertions(+), 323 deletions(-) diff --git a/scripts/dts/edtlib.py b/scripts/dts/edtlib.py index 67a3009246f..6e0eb58568f 100644 --- a/scripts/dts/edtlib.py +++ b/scripts/dts/edtlib.py @@ -12,16 +12,16 @@ properties. Some of this interpretation is based on conventions established by the Linux kernel, so the Documentation/devicetree/bindings in the Linux source code is sometimes good reference material. -In Zephyr, bindings are YAML files that describe devicetree nodes. Devicetree +Bindings are YAML files that describe devicetree nodes. Devicetree nodes are usually mapped to bindings via their 'compatible = "..."' property, but a binding can also come from a 'child-binding:' key in the binding for the -parent devicetree node. See binding-template.yaml for details. +parent devicetree node. Each devicetree node (dtlib.Node) gets a corresponding edtlib.Node instance, which has all the information related to the node. -The top-level entry point of the library is the EDT class. EDT.__init__() takes -a .dts file to parse and a list of paths to directories containing bindings. +The top-level entry points for the library are the EDT and Binding classes. +See their constructor docstrings for details. """ # NOTE: testedtlib.py is the test suite for this library. @@ -65,9 +65,6 @@ a .dts file to parse and a list of paths to directories containing bindings. # # @properties are documented in the class docstring, as if they were # variables. See the existing @properties for a template. -# -# - Please use ""-quoted strings instead of ''-quoted strings, just to make -# things consistent (''-quoting is more common otherwise in Python) from collections import OrderedDict, defaultdict import os @@ -152,7 +149,7 @@ class EDT: default_prop_types=True, support_fixed_partitions_on_any_bus=True, infer_binding_for_paths=None): - """EDT constructor. This is the top-level entry point to the library. + """EDT constructor. dts: Path to devicetree .dts file @@ -200,7 +197,7 @@ class EDT: self._dt = DT(dts) _check_dt(self._dt) - self._init_compat2binding(bindings_dirs) + self._init_compat2binding() self._init_nodes() self._init_graph() self._init_luts() @@ -294,18 +291,19 @@ class EDT: for intr in node.interrupts: self._graph.add_edge(node, intr.controller) - def _init_compat2binding(self, bindings_dirs): - # Creates self._compat2binding. This is a dictionary that maps - # (, ) tuples (both strings) to (, ) - # tuples. is the binding in parsed PyYAML format, and - # the path to the binding (nice for binding-related error messages). + def _init_compat2binding(self): + # Creates self._compat2binding, a dictionary that maps + # (, ) tuples (both strings) to Binding objects. # - # For example, self._compat2binding["company,dev", "can"] contains the - # binding/path for the 'company,dev' device, when it appears on the CAN - # bus. + # The Binding objects are created from YAML files discovered + # in self.bindings_dirs as needed. + # + # For example, self._compat2binding["company,dev", "can"] + # contains the Binding for the 'company,dev' device, when it + # appears on the CAN bus. # # For bindings that don't specify a bus, is None, so that e.g. - # self._compat2binding["company,notonbus", None] contains the binding. + # self._compat2binding["company,notonbus", None] is the Binding. # # Only bindings for 'compatible' strings that appear in the devicetree # are loaded. @@ -317,7 +315,9 @@ class EDT: "|".join(re.escape(compat) for compat in dt_compats) ).search - self._binding_paths = _binding_paths(bindings_dirs) + self._binding_paths = _binding_paths(self.bindings_dirs) + self._binding_fname2path = {os.path.basename(path): path + for path in self._binding_paths} self._compat2binding = {} for binding_path in self._binding_paths: @@ -336,127 +336,59 @@ class EDT: try: # Parsed PyYAML output (Python lists/dictionaries/strings/etc., # representing the file) - binding = yaml.load(contents, Loader=_BindingLoader) + raw = yaml.load(contents, Loader=_BindingLoader) except yaml.YAMLError as e: self._warn("'{}' appears in binding directories but isn't " "valid YAML: {}".format(binding_path, e)) continue + # Convert the raw data to a Binding object, erroring out + # if necessary. + binding = self._binding(raw, binding_path, dt_compats) - # Returns the string listed in 'compatible:' in 'binding', or None if - # no compatible is found. - - if binding is None or "compatible" not in binding: - # Empty file, binding fragment, spurious file, or old-style - # compat - binding_compat = None - else: - binding_compat = binding["compatible"] - if not isinstance(binding_compat, str): - _err("malformed 'compatible: {}' field in {} - " - "should be a string, not {}" - .format(binding_compat, binding_path, - type(binding_compat).__name__)) - - - if binding_compat not in dt_compats: - # Either not a binding (binding_compat is None -- might be a - # binding fragment or a spurious file), or a binding whose - # compatible does not appear in the devicetree (picked up via - # some unrelated text in the binding file that happened to - # match a compatible) + if binding is None: + # Either the file is not a binding or it's a binding + # whose compatible does not appear in the devicetree + # (picked up via some unrelated text in the binding + # file that happened to match a compatible). continue - # It's a match. Merge in the included bindings, do sanity checks, - # and register the binding. - - binding = self._merge_included_bindings(binding, binding_path) - self._check_binding(binding, binding_path) - - on_bus = _on_bus_from_binding(binding) - # Do not allow two different bindings to have the same # 'compatible:'/'on-bus:' combo - old_binding = self._compat2binding.get((binding_compat, on_bus)) + old_binding = self._compat2binding.get((binding.compatible, + binding.on_bus)) if old_binding: - msg = "both {} and {} have 'compatible: {}'".format( - old_binding[1], binding_path, binding_compat) - if on_bus is not None: - msg += " and 'on-bus: {}'".format(on_bus) + msg = (f"both {old_binding.path} and {binding_path} have " + f"'compatible: {binding.compatible}'") + if binding.on_bus is not None: + msg += f" and 'on-bus: {binding.on_bus}'" _err(msg) - self._compat2binding[binding_compat, on_bus] = (binding, binding_path) + # Register the binding. + self._compat2binding[binding.compatible, binding.on_bus] = binding - def _merge_included_bindings(self, binding, binding_path): - # Merges any bindings listed in the 'include:' section of 'binding' - # into the top level of 'binding'. + def _binding(self, raw, binding_path, dt_compats): + # Convert a 'raw' binding from YAML to a Binding object and return it. # - # Properties in 'binding' take precedence over properties from included - # bindings. + # Error out if the raw data looks like an invalid binding. + # + # Return None if the file doesn't contain a binding or the + # binding's compatible isn't in dt_compats. - fnames = [] + # Get the 'compatible:' string. + if raw is None or "compatible" not in raw: + # Empty file, binding fragment, spurious file, etc. + return None - if "include" in binding: - include = binding.pop("include") - if isinstance(include, str): - fnames.append(include) - elif isinstance(include, list): - if not all(isinstance(elm, str) for elm in include): - _err("all elements in 'include:' in {} should be strings" - .format(binding_path)) - fnames += include - else: - _err("'include:' in {} should be a string or a list of strings" - .format(binding_path)) + compatible = raw["compatible"] - if "child-binding" in binding and "include" in binding["child-binding"]: - self._merge_included_bindings(binding["child-binding"], binding_path) + if compatible not in dt_compats: + # Not a compatible we care about. + return None - if not fnames: - return binding - - # Got a list of included files in 'fnames'. Now we need to merge them - # together and then merge them into 'binding'. - - # 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_included = self._load_binding(fnames[0]) - for fname in fnames[1:]: - included = self._load_binding(fname) - _merge_props(merged_included, included, None, binding_path, - check_required=False) - - # Next, merge the merged included files into 'binding'. Error out if - # 'binding' has 'required: false' while the merged included files have - # 'required: true'. - - _merge_props(binding, merged_included, None, binding_path, - check_required=True) - - return binding - - def _load_binding(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. - - paths = [path for path in self._binding_paths - if os.path.basename(path) == fname] - - if not paths: - _err("'{}' not found".format(fname)) - - if len(paths) > 1: - _err("multiple candidates for included file '{}': {}" - .format(fname, ", ".join(paths))) - - with open(paths[0], encoding="utf-8") as f: - return self._merge_included_bindings( - yaml.load(f, Loader=_BindingLoader), - paths[0]) + # Initialize and return the Binding object. + return Binding(binding_path, self._binding_fname2path, raw=raw, + warn_file=self._warn_file) def _init_nodes(self): # Creates a list of edtlib.Node objects from the dtlib.Node objects, in @@ -478,7 +410,7 @@ class EDT: else: node.compats = [] node.bus_node = node._bus_node(self._fixed_partitions_no_bus) - node._init_binding() + node._init_binding(warn_file=self._warn_file) node._init_regs() self.nodes.append(node) @@ -522,106 +454,6 @@ class EDT: node = nodeset[0] self.dep_ord2node[node.dep_ordinal] = node - def _check_binding(self, binding, binding_path): - # Does sanity checking on 'binding'. Only takes 'self' for the sake of - # self._warn(). - - if "description" not in binding: - _err("missing 'description' property in " + binding_path) - - for prop in "title", "description": - if prop in binding and (not isinstance(binding[prop], str) or - not binding[prop]): - _err("malformed or empty '{}' in {}" - .format(prop, binding_path)) - - ok_top = {"title", "description", "compatible", "properties", "#cells", - "bus", "on-bus", "parent-bus", "child-bus", "parent", "child", - "child-binding", "sub-node"} - - for prop in binding: - if prop not in ok_top and not prop.endswith("-cells"): - _err("unknown key '{}' in {}, expected one of {}, or *-cells" - .format(prop, binding_path, ", ".join(ok_top))) - - for bus_key in "bus", "on-bus": - if bus_key in binding and \ - not isinstance(binding[bus_key], str): - _err("malformed '{}:' value in {}, expected string" - .format(bus_key, binding_path)) - - self._check_binding_properties(binding, binding_path) - - if "child-binding" in binding: - if not isinstance(binding["child-binding"], dict): - _err("malformed 'child-binding:' in {}, expected a binding " - "(dictionary with keys/values)".format(binding_path)) - - self._check_binding(binding["child-binding"], binding_path) - - def ok_cells_val(val): - # Returns True if 'val' is an okay value for '*-cells:' (or the - # legacy '#cells:') - - return isinstance(val, list) and \ - all(isinstance(elm, str) for elm in val) - - for key, val in binding.items(): - if key.endswith("-cells") or key == "#cells": - if not ok_cells_val(val): - _err("malformed '{}:' in {}, expected a list of strings" - .format(key, binding_path)) - - def _check_binding_properties(self, binding, binding_path): - # _check_binding() helper for checking the contents of 'properties:'. - # Only takes 'self' for the sake of self._warn(). - - if "properties" not in binding: - return - - ok_prop_keys = {"description", "type", "required", "category", - "enum", "const", "default"} - - for prop_name, options in binding["properties"].items(): - for key in options: - if key == "category": - self._warn( - "please put 'required: {}' instead of 'category: {}' " - "in properties: {}: ...' in {} - 'category' will be " - "removed".format( - "true" if options["category"] == "required" - else "false", - options["category"], prop_name, binding_path)) - - if key not in ok_prop_keys: - _err("unknown setting '{}' in 'properties: {}: ...' in {}, " - "expected one of {}".format( - key, prop_name, binding_path, - ", ".join(ok_prop_keys))) - - _check_prop_type_and_default( - prop_name, options.get("type"), - options.get("required") or options.get("category") == "required", - options.get("default"), binding_path) - - if "required" in options and not isinstance(options["required"], bool): - _err("malformed 'required:' setting '{}' for '{}' in 'properties' " - "in {}, expected true/false" - .format(options["required"], prop_name, binding_path)) - - if "description" in options and \ - not isinstance(options["description"], str): - _err("missing, malformed, or empty 'description' for '{}' in " - "'properties' in {}".format(prop_name, binding_path)) - - if "enum" in options and not isinstance(options["enum"], list): - _err("enum in {} for property '{}' is not a list" - .format(binding_path, prop_name)) - - if "const" in options and not isinstance(options["const"], (int, str)): - _err("const in {} for property '{}' is not a scalar" - .format(binding_path, prop_name)) - def _warn(self, msg): if self._warn_file is not None: print("warning: " + msg, file=self._warn_file) @@ -777,8 +609,8 @@ class Node: @property def description(self): "See the class docstring." - if self._binding and "description" in self._binding: - return self._binding["description"].strip() + if self._binding: + return self._binding.description return None @property @@ -851,22 +683,8 @@ class Node: @property def bus(self): "See the class docstring" - binding = self._binding - if not binding: - return None - - if "bus" in binding: - return binding["bus"] - - # Legacy key - if "child-bus" in binding: - return binding["child-bus"] - - # Legacy key - if "child" in binding: - # _check_binding() has checked that the "bus" key exists - return binding["child"]["bus"] - + if self._binding: + return self._binding.bus return None @property @@ -923,7 +741,7 @@ class Node: "binding " + self.binding_path if self.binding_path else "no binding") - def _init_binding(self): + def _init_binding(self, warn_file=None): # Initializes Node.matching_compat, Node._binding, and # Node.binding_path. # @@ -936,7 +754,7 @@ class Node: # node_iter() order. if self.path in self.edt._infer_binding_for_paths: - self._binding_from_properties() + self._binding_from_properties(warn_file=warn_file) return if self.compats: @@ -945,43 +763,40 @@ class Node: for compat in self.compats: if (compat, on_bus) in self.edt._compat2binding: # Binding found + binding = self.edt._compat2binding[compat, on_bus] + self.binding_path = binding.path self.matching_compat = compat - self._binding, self.binding_path = \ - self.edt._compat2binding[compat, on_bus] - + self._binding = binding return else: - # No 'compatible' property. See if the parent binding has a - # 'child-binding:' key that gives the binding (or a legacy - # 'sub-node:' key). + # No 'compatible' property. See if the parent binding has + # a compatible. This can come from one or more levels of + # nesting with 'child-binding:'. binding_from_parent = self._binding_from_parent() if binding_from_parent: self._binding = binding_from_parent - self.binding_path = self.parent.binding_path - self.matching_compat = self.parent.matching_compat + self.binding_path = self._binding.path + self.matching_compat = self._binding.compatible return # No binding found self._binding = self.binding_path = self.matching_compat = None - def _binding_from_properties(self): - # Returns a binding synthesized from the properties in the node. + def _binding_from_properties(self, warn_file): + # Sets up a Binding object synthesized from the properties in the node. if self.compats: _err(f"compatible in node with inferred binding: {self.path}") - self._binding = OrderedDict() - self.matching_compat = self.path.split('/')[-1] - self.compats = [self.matching_compat] - self.binding_path = None - - properties = OrderedDict() - self._binding["properties"] = properties + # Synthesize a 'raw' binding as if it had been parsed from YAML. + raw = { + 'description': 'Inferred binding from properties, via edtlib.', + 'properties': {}, + } for name, prop in self._node.props.items(): pp = OrderedDict() - properties[name] = pp if prop.type == TYPE_EMPTY: pp["type"] = "boolean" elif prop.type == TYPE_BYTES: @@ -1002,10 +817,18 @@ class Node: pp["type"] = "phandle-array" else: _err(f"cannot infer binding from property: {prop}") + raw['properties'][name] = pp + + # Set up Node state. + self.binding_path = None + self.matching_compat = None + self.compats = [] + self._binding = Binding(None, {}, raw=raw, require_compatible=False, + warn_file=warn_file) def _binding_from_parent(self): # Returns the binding from 'child-binding:' in the parent node's - # binding (or from the legacy 'sub-node:' key), or None if missing + # binding. if not self.parent: return None @@ -1014,14 +837,8 @@ class Node: if not pbinding: return None - if "child-binding" in pbinding: - return pbinding["child-binding"] - - # Backwards compatibility - if "sub-node" in pbinding: - return {"title": pbinding["title"], - "description": pbinding["description"], - "properties": pbinding["sub-node"]["properties"]} + if pbinding.child_binding: + return pbinding.child_binding return None @@ -1056,14 +873,14 @@ class Node: node = self._node if self._binding: - binding_props = self._binding.get("properties") + prop2specs = self._binding.prop2specs else: - binding_props = None + prop2specs = None # Initialize self.props - if binding_props: - for name, options in binding_props.items(): - self._init_prop(name, options) + if prop2specs: + for prop_spec in prop2specs.values(): + self._init_prop(prop_spec) self._check_undeclared_props() elif default_prop_types: for name in node.props: @@ -1080,31 +897,31 @@ class Node: prop.enum_index = None self.props[name] = prop - def _init_prop(self, name, options): - # _init_props() helper for initializing a single property + def _init_prop(self, prop_spec): + # _init_props() helper for initializing a single property. + # 'prop_spec' is a PropertySpec object from the node's binding. - prop_type = options.get("type") + name = prop_spec.name + prop_type = prop_spec.type if not prop_type: _err("'{}' in {} lacks 'type'".format(name, self.binding_path)) - val = self._prop_val( - name, prop_type, - options.get("required") or options.get("category") == "required", - options.get("default")) + val = self._prop_val(name, prop_type, prop_spec.required, + prop_spec.default) if val is None: # 'required: false' property that wasn't there, or a property type # for which we store no data. return - enum = options.get("enum") + enum = prop_spec.enum if enum and val not in enum: _err("value of property '{}' on {} in {} ({!r}) is not in 'enum' " "list in {} ({!r})" .format(name, self.path, self.edt.dts_path, val, self.binding_path, enum)) - const = options.get("const") + const = prop_spec.const if const is not None and val != const: _err("value of property '{}' on {} in {} ({!r}) is different from " "the 'const' value specified in {} ({!r})" @@ -1119,7 +936,7 @@ class Node: prop = Property() prop.node = self prop.name = name - prop.description = options.get("description") + prop.description = prop_spec.description if prop.description: prop.description = prop.description.strip() prop.val = val @@ -1207,8 +1024,8 @@ class Node: if prop_type == "path": return self.edt._node2enode[prop.to_path()] - # prop_type == "compound". We have already checked that the 'type:' - # value is valid, in _check_binding(). + # prop_type == "compound". Checking that the 'type:' + # value is valid is done in _check_prop_type_and_default(). # # 'compound' is a dummy type for properties that don't fit any of the # patterns above, so that we can require all entries in 'properties:' @@ -1218,11 +1035,6 @@ class Node: def _check_undeclared_props(self): # Checks that all properties are declared in the binding - if "properties" in self._binding: - declared_props = self._binding["properties"].keys() - else: - declared_props = set() - for prop_name in self._node.props: # Allow a few special properties to not be declared in the binding if prop_name.endswith("-controller") or \ @@ -1233,7 +1045,7 @@ class Node: "interrupt-parent", "interrupts-extended", "device_type"}: continue - if prop_name not in declared_props: + if prop_name not in self._binding.prop2specs: _err("'{}' appears in {} in {}, but is not declared in " "'properties:' in {}" .format(prop_name, self._node.path, self.edt.dts_path, @@ -1347,7 +1159,7 @@ class Node: basename = "gpio" else: # Strip -s. We've already checked that the property names end in -s - # in _check_binding(). + # in _check_prop_type_and_default(). basename = prop.name[:-1] res = [] @@ -1378,11 +1190,8 @@ class Node: _err("{} controller {!r} for {!r} lacks binding" .format(basename, controller._node, self._node)) - if basename + "-cells" in controller._binding: - cell_names = controller._binding[basename + "-cells"] - elif "#cells" in controller._binding: - # Backwards compatibility - cell_names = controller._binding["#cells"] + if basename in controller._binding.specifier2cells: + cell_names = controller._binding.specifier2cells[basename] else: # Treat no *-cells in the binding the same as an empty *-cells, so # that bindings don't have to have e.g. an empty 'clock-cells:' for @@ -1560,6 +1369,418 @@ class Property: return "".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. + + compatible: + The compatible string the binding matches. This may be None if the + Binding object is a child binding or if it's inferred from node + properties. + + prop2specs: + A collections.OrderedDict mapping property names to PropertySpec objects + describing those properties' values. + + specifier2cells: + A collections.OrderedDict 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"). None otherwise. + + 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, + warn_file=None): + """ + 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. + + warn_file (default: None): + 'file' object to write warnings to. If None, sys.stderr is used. + """ + # Do this indirection with None in case sys.stderr is + # deliberately overridden. We'll only hold on to this file + # while we're initializing. + self._warn_file = sys.stderr if warn_file is None else warn_file + + 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 = OrderedDict() + for prop_name in self.raw.get("properties", {}).keys(): + self.prop2specs[prop_name] = PropertySpec(prop_name, self) + self.specifier2cells = OrderedDict() + for key, val in self.raw.items(): + if key.endswith("-cells"): + self.specifier2cells[key[:-len("-cells")]] = val + + # Drop the reference to the open warn file. This is necessary + # to make this object pickleable, but also allows it to get + # garbage collected and closed if nobody else is using it. + self._warn_file = None + + def __repr__(self): + if self.compatible: + compat = f" for compatible '{self.compatible}'" + else: + compat = "" + return f"" + + @property + def description(self): + "See the class docstring" + return self.raw['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 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") + fnames = [] + if isinstance(include, str): + fnames.append(include) + elif isinstance(include, list): + if not all(isinstance(elm, str) for elm in include): + _err(f"all elements in 'include:' in {binding_path} " + "should be strings") + fnames += include + else: + _err(f"'include:' in {binding_path} " + "should be a string or a list of strings") + + # 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 = {} + for fname in fnames: + _merge_props(merged, self._load_raw(fname), None, binding_path, + check_required=False) + + # 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' property in {self.path}") + + if "description" not in raw and require_description: + _err(f"missing 'description' property in {self.path}") + + for prop in "title", "description": + if prop in raw and (not isinstance(raw[prop], str) or + not raw[prop]): + _err(f"malformed or empty '{prop}' in {self.path}") + + ok_top = {"title", "description", "compatible", "properties", + "bus", "on-bus", "parent-bus", "child-bus", "parent", "child", + "child-binding", "sub-node"} + + for prop in raw: + if prop == "#cells": # clean error for users of legacy syntax + _err(f"malformed '{prop}:' in {self.path}, " + "expected *-cells syntax") + if prop not in ok_top and not prop.endswith("-cells"): + _err(f"unknown key '{prop}' in {self.path}, " + "expected one of {', '.join(ok_top)}, or *-cells") + + for bus_key in "bus", "on-bus": + if bus_key in raw and \ + not isinstance(raw[bus_key], str): + _err(f"malformed '{bus_key}:' 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(elm, str) for elm 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", "category", + "enum", "const", "default"} + + for prop_name, options in raw["properties"].items(): + for key in options: + if key == "category": + self._warn( + "please put 'required: {}' instead of 'category: {}' " + "in properties: {}: ...' in {} - 'category' will be " + "removed".format( + "true" if options["category"] == "required" + else "false", + options["category"], prop_name, self.path)) + + 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_type_and_default( + prop_name, options.get("type"), + options.get("required") or + options.get("category") == "required", + options.get("default"), + self.path) + + if "required" in options: + required = options["required"] + if not isinstance(required, bool): + _err(f"malformed 'required:' setting '{required}' " + f"for '{prop_name}' in 'properties' in {self.path}, " + "expected true/false") + + 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") + + if "const" in options and not isinstance(options["const"], + (int, str)): + _err(f"const in {self.path} for property '{prop_name}' " + "is not a scalar") + + def _warn(self, msg): + if self._warn_file is not None: + print("warning: " + msg, file=self._warn_file) + else: + raise _err("can't _warn() outside of Binding.__init__") + + +class PropertySpec: + """ + Represents a "property specification", i.e. the description of a + property provided by a binding file, like its type and description. + + These attributes are available on PropertySpec objects: + + binding: + The Binding object which defined this property. + + name: + The property's name. + + path: + The file where this property was defined. In case a binding includes + other bindings, this is the file where the property was last modified. + + type: + The type of the property as a string, as given in the binding. + + description: + The free-form description of the property as a string, or None. + + enum: + A list of values the property may take as given in the binding, or None. + + const: + The property's constant value as given in the binding, or None. + + default: + The property's default value as given in the binding, or None. + + required: + True if the property is marked required; False otherwise. + """ + + def __init__(self, name, binding): + self.binding = binding + self.name = name + self._raw = self.binding.raw["properties"][name] + + def __repr__(self): + return f"" + + @property + def path(self): + "See the class docstring" + return self.binding.path + + @property + def type(self): + "See the class docstring" + return self._raw["type"] + + @property + def description(self): + "See the class docstring" + return self._raw.get("description") + + @property + def enum(self): + "See the class docstring" + return self._raw.get("enum") + + @property + def const(self): + "See the class docstring" + return self._raw.get("const") + + @property + def default(self): + "See the class docstring" + return self._raw.get("default") + + @property + def required(self): + "See the class docstring" + return self._raw.get("required", False) + + class EDTError(Exception): "Exception raised for devicetree- and binding-related errors" @@ -1594,28 +1815,6 @@ def _binding_paths(bindings_dirs): return binding_paths -def _on_bus_from_binding(binding): - # Returns the bus specified by 'on-bus:' in the binding (or the - # legacy 'parent-bus:' and 'parent: bus:'), or None if missing - - if not binding: - return None - - if "on-bus" in binding: - return binding["on-bus"] - - # Legacy key - if "parent-bus" in binding: - return binding["parent-bus"] - - # Legacy key - if "parent" in binding: - # _check_binding() has checked that the "bus" key exists - return binding["parent"]["bus"] - - return None - - def _binding_inc_error(msg): # Helper for reporting errors in the !include implementation @@ -1655,7 +1854,7 @@ def _merge_props(to_dict, from_dict, parent, binding_path, check_required): to_dict[prop])) elif prop == "required": # Need a separate check here, because this code runs before - # _check_binding() + # Binding._check() if not (isinstance(from_dict["required"], bool) and isinstance(to_dict["required"], bool)): _err("malformed 'required:' setting for '{}' in 'properties' " @@ -1711,8 +1910,8 @@ def _binding_include(loader, node): def _check_prop_type_and_default(prop_name, prop_type, required, default, binding_path): - # _check_binding() helper. Checks 'type:' and 'default:' for the property - # named 'prop_name' + # Binding._check_properties() helper. Checks 'type:' and 'default:' for the + # property named 'prop_name' if prop_type is None: _err("missing 'type:' for '{}' in 'properties' in {}"