edtlib: type annotate Binding

Incremental progress towards type annotating the whole module.
Annotate helper procedures used by the class as well.

Signed-off-by: Martí Bolívar <marti.bolivar@nordicsemi.no>
diff --git a/scripts/dts/python-devicetree/src/devicetree/edtlib.py b/scripts/dts/python-devicetree/src/devicetree/edtlib.py
index 749cbff..f07fc45 100644
--- a/scripts/dts/python-devicetree/src/devicetree/edtlib.py
+++ b/scripts/dts/python-devicetree/src/devicetree/edtlib.py
@@ -69,6 +69,7 @@
 
 from collections import defaultdict
 from copy import deepcopy
+from typing import Any, Dict, List, NoReturn, Optional, TYPE_CHECKING, Union
 import logging
 import os
 import re
@@ -156,8 +157,9 @@
       are multiple levels of 'child-binding' descriptions in the binding.
     """
 
-    def __init__(self, path, fname2path, raw=None,
-                 require_compatible=True, require_description=True):
+    def __init__(self, path: Optional[str], fname2path: Dict[str, str],
+                 raw: Any = None, require_compatible: bool = True,
+                 require_description: bool = True):
         """
         Binding constructor.
 
@@ -186,8 +188,8 @@
           not an error. Either way, "description:" must be a string
           if it is present in the binding.
         """
-        self.path = path
-        self._fname2path = fname2path
+        self.path: Optional[str] = path
+        self._fname2path: Dict[str, str] = fname2path
 
         if raw is None:
             if path is None:
@@ -198,7 +200,7 @@
         # 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)
+        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,
@@ -207,10 +209,11 @@
             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)
+            self.child_binding: Optional['Binding'] = Binding(
+                path, fname2path,
+                raw=raw["child-binding"],
+                require_compatible=False,
+                require_description=False)
         else:
             self.child_binding = None
 
@@ -218,15 +221,15 @@
         self._check(require_compatible, require_description)
 
         # Initialize look up tables.
-        self.prop2specs = {}
+        self.prop2specs: Dict[str, 'PropertySpec'] = {}
         for prop_name in self.raw.get("properties", {}).keys():
             self.prop2specs[prop_name] = PropertySpec(prop_name, self)
-        self.specifier2cells = {}
+        self.specifier2cells: Dict[str, List[str]] = {}
         for key, val in self.raw.items():
             if key.endswith("-cells"):
                 self.specifier2cells[key[:-len("-cells")]] = val
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         if self.compatible:
             compat = f" for compatible '{self.compatible}'"
         else:
@@ -235,22 +238,22 @@
         return f"<Binding {basename}" + compat + ">"
 
     @property
-    def description(self):
+    def description(self) -> Optional[str]:
         "See the class docstring"
         return self.raw.get('description')
 
     @property
-    def compatible(self):
+    def compatible(self) -> Optional[str]:
         "See the class docstring"
         return self.raw.get('compatible')
 
     @property
-    def bus(self):
+    def bus(self) -> Union[None, str, List[str]]:
         "See the class docstring"
         return self.raw.get('bus')
 
     @property
-    def buses(self):
+    def buses(self) -> List[str]:
         "See the class docstring"
         if self.raw.get('bus') is not None:
             return self._buses
@@ -258,11 +261,11 @@
             return []
 
     @property
-    def on_bus(self):
+    def on_bus(self) -> Optional[str]:
         "See the class docstring"
         return self.raw.get('on-bus')
 
-    def _merge_includes(self, raw, binding_path):
+    def _merge_includes(self, raw: dict, binding_path: Optional[str]) -> dict:
         # 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
@@ -280,7 +283,7 @@
         # file has a 'required:' for a particular property, OR the values
         # together, so that 'required: true' wins.
 
-        merged = {}
+        merged: Dict[str, Any] = {}
 
         if isinstance(include, str):
             # Simple scalar string case
@@ -329,7 +332,7 @@
 
         return raw
 
-    def _load_raw(self, fname):
+    def _load_raw(self, fname: str) -> dict:
         # 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
@@ -347,7 +350,7 @@
 
         return self._merge_includes(contents, path)
 
-    def _check(self, require_compatible, require_description):
+    def _check(self, require_compatible: bool, require_description: bool):
         # Does sanity checking on the binding.
 
         raw = self.raw
@@ -420,7 +423,7 @@
                     _err(f"malformed '{key}:' in {self.path}, "
                          "expected a list of strings")
 
-    def _check_properties(self):
+    def _check_properties(self) -> None:
         # _check() helper for checking the contents of 'properties:'.
 
         raw = self.raw
@@ -2343,8 +2346,11 @@
     raise yaml.constructor.ConstructorError(None, None, "error: " + msg)
 
 
-def _check_include_dict(name, allowlist, blocklist, child_filter,
-                        binding_path):
+def _check_include_dict(name: Optional[str],
+                        allowlist: Optional[List[str]],
+                        blocklist: Optional[List[str]],
+                        child_filter: Optional[dict],
+                        binding_path: Optional[str]) -> None:
     # Check that an 'include:' named 'name' with property-allowlist
     # 'allowlist', property-blocklist 'blocklist', and
     # child-binding filter 'child_filter' has valid structure.
@@ -2360,9 +2366,12 @@
 
     while child_filter is not None:
         child_copy = deepcopy(child_filter)
-        child_allowlist = child_copy.pop('property-allowlist', None)
-        child_blocklist = child_copy.pop('property-blocklist', None)
-        next_child_filter = child_copy.pop('child-binding', None)
+        child_allowlist: Optional[List[str]] = \
+            child_copy.pop('property-allowlist', None)
+        child_blocklist: Optional[List[str]] = \
+            child_copy.pop('property-blocklist', None)
+        next_child_filter: Optional[dict] = \
+            child_copy.pop('child-binding', None)
 
         if child_copy:
             # We've popped out all the valid keys.
@@ -2378,8 +2387,11 @@
         child_filter = next_child_filter
 
 
-def _filter_properties(raw, allowlist, blocklist, child_filter,
-                       binding_path):
+def _filter_properties(raw: dict,
+                       allowlist: Optional[List[str]],
+                       blocklist: Optional[List[str]],
+                       child_filter: Optional[dict],
+                       binding_path: Optional[str]) -> None:
     # Destructively modifies 'raw["properties"]' and
     # 'raw["child-binding"]', if they exist, according to
     # 'allowlist', 'blocklist', and 'child_filter'.
@@ -2397,7 +2409,10 @@
         child_binding = child_binding.get('child-binding')
 
 
-def _filter_properties_helper(props, allowlist, blocklist, binding_path):
+def _filter_properties_helper(props: Optional[dict],
+                              allowlist: Optional[List[str]],
+                              blocklist: Optional[List[str]],
+                              binding_path: Optional[str]) -> None:
     if props is None or (allowlist is None and blocklist is None):
         return
 
@@ -2408,6 +2423,8 @@
         allowset = set(allowlist)
         to_del = [prop for prop in props if prop not in allowset]
     else:
+        if TYPE_CHECKING:
+            assert blocklist
         blockset = set(blocklist)
         to_del = [prop for prop in props if prop in blockset]
 
@@ -2415,7 +2432,8 @@
         del props[prop]
 
 
-def _check_prop_filter(name, value, binding_path):
+def _check_prop_filter(name: str, value: Optional[List[str]],
+                       binding_path: Optional[str]) -> None:
     # Ensure an include: ... property-allowlist or property-blocklist
     # is a list.
 
@@ -2426,7 +2444,11 @@
         _err(f"'{name}' value {value} in {binding_path} should be a list")
 
 
-def _merge_props(to_dict, from_dict, parent, binding_path, check_required):
+def _merge_props(to_dict: dict,
+                 from_dict: dict,
+                 parent: Optional[str],
+                 binding_path: Optional[str],
+                 check_required: bool = False):
     # Recursively merges 'from_dict' into 'to_dict', to implement 'include:'.
     #
     # If 'from_dict' and 'to_dict' contain a 'required:' key for the same
@@ -2468,7 +2490,8 @@
             to_dict["required"] = to_dict["required"] or from_dict["required"]
 
 
-def _bad_overwrite(to_dict, from_dict, prop, check_required):
+def _bad_overwrite(to_dict: dict, from_dict: dict, prop: str,
+                   check_required: bool) -> bool:
     # _merge_props() helper. Returns True in cases where it's bad that
     # to_dict[prop] takes precedence over from_dict[prop].
 
@@ -2502,7 +2525,9 @@
     _binding_inc_error("unrecognised node type in !include statement")
 
 
-def _check_prop_by_type(prop_name, options, binding_path):
+def _check_prop_by_type(prop_name: str,
+                        options: dict,
+                        binding_path: Optional[str]) -> None:
     # Binding._check_properties() helper. Checks 'type:', 'default:',
     # 'const:' and # 'specifier-space:' for the property named 'prop_name'
 
@@ -2550,7 +2575,7 @@
              f"'type: {prop_type}' for '{prop_name}' in "
              f"'properties:' in {binding_path}")
 
-    def ok_default():
+    def ok_default() -> bool:
         # Returns True if 'default' is an okay default for the property's type
 
         if prop_type == "int" and isinstance(default, int) or \
@@ -3026,7 +3051,7 @@
                      "(see the devicetree specification)")
 
 
-def _err(msg):
+def _err(msg) -> NoReturn:
     raise EDTError(msg)
 
 # Logging object