qemu-devel.nongnu.org archive mirror
 help / color / mirror / Atom feed
* [PATCH v5 00/25] qapi: statically type schema.py
@ 2024-03-15 15:22 Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 01/25] qapi/parser: fix typo - self.returns.info => self.errors.info Markus Armbruster
                   ` (25 more replies)
  0 siblings, 26 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

v5:
* PATCH 05: Move QAPISchemaDefinition.check()'s
  super().check() back to where it was in v3
* PATCH 12: Replaced, necessitating minor adjustments in PATCH 17+22
* PATCH 16: Tweak comment
* PATCH 22: Tighten QAPISchema.lookup_entity()'s type hint
* PATCH 24+25: New

John Snow (22):
  qapi/parser: fix typo - self.returns.info => self.errors.info
  qapi/parser: shush up pylint
  qapi: sort pylint suppressions
  qapi/schema: add pylint suppressions
  qapi: create QAPISchemaDefinition
  qapi/schema: declare type for QAPISchemaObjectTypeMember.type
  qapi/schema: declare type for QAPISchemaArrayType.element_type
  qapi/schema: make c_type() and json_type() abstract methods
  qapi/schema: adjust type narrowing for mypy's benefit
  qapi/schema: add type narrowing to lookup_type()
  qapi/schema: assert resolve_type has 'info' and 'what' args on error
  qapi/schema: fix QAPISchemaArrayType.check's call to resolve_type
  qapi/schema: assert info is present when necessary
  qapi/schema: add _check_complete flag
  qapi/schema: Don't initialize "members" with `None`
  qapi/schema: fix typing for QAPISchemaVariants.tag_member
  qapi/schema: assert inner type of QAPISchemaVariants in check_clash()
  qapi/parser: demote QAPIExpression to Dict[str, Any]
  qapi/parser.py: assert member.info is present in connect_member
  qapi/schema: add type hints
  qapi/schema: turn on mypy strictness
  qapi/schema: remove unnecessary asserts

Markus Armbruster (3):
  qapi: Assert built-in types exist
  qapi: Tighten check whether implicit object type already exists
  qapi: Dumb down QAPISchema.lookup_entity()

 scripts/qapi/introspect.py |   8 +-
 scripts/qapi/mypy.ini      |   5 -
 scripts/qapi/parser.py     |   7 +-
 scripts/qapi/pylintrc      |  11 +-
 scripts/qapi/schema.py     | 794 ++++++++++++++++++++++++-------------
 5 files changed, 537 insertions(+), 288 deletions(-)

-- 
2.44.0



^ permalink raw reply	[flat|nested] 34+ messages in thread

* [PATCH v5 01/25] qapi/parser: fix typo - self.returns.info => self.errors.info
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 02/25] qapi/parser: shush up pylint Markus Armbruster
                   ` (24 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

Small copy-pasto. The correct info field to use in this conditional
block is self.errors.info.

Fixes: 3a025d3d1ffa
Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/parser.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index d8f76060b8..fed88e9074 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -733,7 +733,7 @@ def check_expr(self, expr: QAPIExpression) -> None:
                     "'Returns' section is only valid for commands")
             if self.errors:
                 raise QAPISemError(
-                    self.returns.info,
+                    self.errors.info,
                     "'Errors' section is only valid for commands")
 
     def check(self) -> None:
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 02/25] qapi/parser: shush up pylint
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 01/25] qapi/parser: fix typo - self.returns.info => self.errors.info Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 03/25] qapi: sort pylint suppressions Markus Armbruster
                   ` (23 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

Shhh!

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/parser.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index fed88e9074..ec4ebef4e3 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -607,6 +607,7 @@ class QAPIDoc:
     """
 
     class Section:
+        # pylint: disable=too-few-public-methods
         def __init__(self, info: QAPISourceInfo,
                      tag: Optional[str] = None):
             # section source info, i.e. where it begins
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 03/25] qapi: sort pylint suppressions
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 01/25] qapi/parser: fix typo - self.returns.info => self.errors.info Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 02/25] qapi/parser: shush up pylint Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 04/25] qapi/schema: add " Markus Armbruster
                   ` (22 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

Suggested-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/pylintrc | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/scripts/qapi/pylintrc b/scripts/qapi/pylintrc
index 90546df534..1342412c3c 100644
--- a/scripts/qapi/pylintrc
+++ b/scripts/qapi/pylintrc
@@ -16,13 +16,13 @@ ignore-patterns=schema.py,
 # --enable=similarities". If you want to run only the classes checker, but have
 # no Warning level messages displayed, use "--disable=all --enable=classes
 # --disable=W".
-disable=fixme,
+disable=consider-using-f-string,
+        fixme,
         missing-docstring,
         too-many-arguments,
         too-many-branches,
-        too-many-statements,
         too-many-instance-attributes,
-        consider-using-f-string,
+        too-many-statements,
         useless-option-value,
 
 [REPORTS]
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 04/25] qapi/schema: add pylint suppressions
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (2 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 03/25] qapi: sort pylint suppressions Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 05/25] qapi: create QAPISchemaDefinition Markus Armbruster
                   ` (21 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

With this patch, pylint is happy with the file, so enable it in the
configuration.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/pylintrc  | 5 -----
 scripts/qapi/schema.py | 5 +++++
 2 files changed, 5 insertions(+), 5 deletions(-)

diff --git a/scripts/qapi/pylintrc b/scripts/qapi/pylintrc
index 1342412c3c..c028a1f9f5 100644
--- a/scripts/qapi/pylintrc
+++ b/scripts/qapi/pylintrc
@@ -1,10 +1,5 @@
 [MASTER]
 
-# Add files or directories matching the regex patterns to the ignore list.
-# The regex matches against base names, not paths.
-ignore-patterns=schema.py,
-
-
 [MESSAGES CONTROL]
 
 # Disable the message, report, category or checker with the given id(s). You
diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 8ba5665bc6..117f0f78f0 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -12,6 +12,8 @@
 # This work is licensed under the terms of the GNU GPL, version 2.
 # See the COPYING file in the top-level directory.
 
+# pylint: disable=too-many-lines
+
 # TODO catching name collisions in generated code would be nice
 
 from collections import OrderedDict
@@ -83,6 +85,7 @@ def c_name(self):
         return c_name(self.name)
 
     def check(self, schema):
+        # pylint: disable=unused-argument
         assert not self._checked
         seen = {}
         for f in self.features:
@@ -113,6 +116,7 @@ def is_implicit(self):
         return not self.info
 
     def visit(self, visitor):
+        # pylint: disable=unused-argument
         assert self._checked
 
     def describe(self):
@@ -131,6 +135,7 @@ def visit_module(self, name):
         pass
 
     def visit_needed(self, entity):
+        # pylint: disable=unused-argument
         # Default to visiting everything
         return True
 
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 05/25] qapi: create QAPISchemaDefinition
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (3 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 04/25] qapi/schema: add " Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 06/25] qapi/schema: declare type for QAPISchemaObjectTypeMember.type Markus Armbruster
                   ` (20 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

Include entities don't have names, but we generally expect "entities" to
have names. Reclassify all entities with names as *definitions*, leaving
the nameless include entities as QAPISchemaEntity instances.

This is primarily to help simplify typing around expectations of what
callers expect for properties of an "entity".

Suggested-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 144 +++++++++++++++++++++++------------------
 1 file changed, 82 insertions(+), 62 deletions(-)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 117f0f78f0..b298c8b4f9 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -55,14 +55,13 @@ def is_present(self):
 
 
 class QAPISchemaEntity:
-    meta: Optional[str] = None
+    """
+    A schema entity.
 
-    def __init__(self, name: str, info, doc, ifcond=None, features=None):
-        assert name is None or isinstance(name, str)
-        for f in features or []:
-            assert isinstance(f, QAPISchemaFeature)
-            f.set_defined_in(name)
-        self.name = name
+    This is either a directive, such as include, or a definition.
+    The latter uses sub-class `QAPISchemaDefinition`.
+    """
+    def __init__(self, info):
         self._module = None
         # For explicitly defined entities, info points to the (explicit)
         # definition.  For builtins (and their arrays), info is None.
@@ -70,33 +69,17 @@ def __init__(self, name: str, info, doc, ifcond=None, features=None):
         # triggered the implicit definition (there may be more than one
         # such place).
         self.info = info
-        self.doc = doc
-        self._ifcond = ifcond or QAPISchemaIfCond()
-        self.features = features or []
         self._checked = False
 
     def __repr__(self):
-        if self.name is None:
-            return "<%s at 0x%x>" % (type(self).__name__, id(self))
-        return "<%s:%s at 0x%x>" % (type(self).__name__, self.name,
-                                    id(self))
-
-    def c_name(self):
-        return c_name(self.name)
+        return "<%s at 0x%x>" % (type(self).__name__, id(self))
 
     def check(self, schema):
         # pylint: disable=unused-argument
-        assert not self._checked
-        seen = {}
-        for f in self.features:
-            f.check_clash(self.info, seen)
         self._checked = True
 
     def connect_doc(self, doc=None):
-        doc = doc or self.doc
-        if doc:
-            for f in self.features:
-                doc.connect_feature(f)
+        pass
 
     def _set_module(self, schema, info):
         assert self._checked
@@ -107,6 +90,46 @@ def _set_module(self, schema, info):
     def set_module(self, schema):
         self._set_module(schema, self.info)
 
+    def visit(self, visitor):
+        # pylint: disable=unused-argument
+        assert self._checked
+
+
+class QAPISchemaDefinition(QAPISchemaEntity):
+    meta: Optional[str] = None
+
+    def __init__(self, name: str, info, doc, ifcond=None, features=None):
+        assert isinstance(name, str)
+        super().__init__(info)
+        for f in features or []:
+            assert isinstance(f, QAPISchemaFeature)
+            f.set_defined_in(name)
+        self.name = name
+        self.doc = doc
+        self._ifcond = ifcond or QAPISchemaIfCond()
+        self.features = features or []
+
+    def __repr__(self):
+        return "<%s:%s at 0x%x>" % (type(self).__name__, self.name,
+                                    id(self))
+
+    def c_name(self):
+        return c_name(self.name)
+
+    def check(self, schema):
+        assert not self._checked
+        super().check(schema)
+        seen = {}
+        for f in self.features:
+            f.check_clash(self.info, seen)
+
+    def connect_doc(self, doc=None):
+        super().connect_doc(doc)
+        doc = doc or self.doc
+        if doc:
+            for f in self.features:
+                doc.connect_feature(f)
+
     @property
     def ifcond(self):
         assert self._checked
@@ -115,10 +138,6 @@ def ifcond(self):
     def is_implicit(self):
         return not self.info
 
-    def visit(self, visitor):
-        # pylint: disable=unused-argument
-        assert self._checked
-
     def describe(self):
         assert self.meta
         return "%s '%s'" % (self.meta, self.name)
@@ -218,7 +237,7 @@ def visit(self, visitor):
 
 class QAPISchemaInclude(QAPISchemaEntity):
     def __init__(self, sub_module, info):
-        super().__init__(None, info, None)
+        super().__init__(info)
         self._sub_module = sub_module
 
     def visit(self, visitor):
@@ -226,7 +245,7 @@ def visit(self, visitor):
         visitor.visit_include(self._sub_module.name, self.info)
 
 
-class QAPISchemaType(QAPISchemaEntity):
+class QAPISchemaType(QAPISchemaDefinition):
     # Return the C type for common use.
     # For the types we commonly box, this is a pointer type.
     def c_type(self):
@@ -797,7 +816,7 @@ def __init__(self, name, info, typ, ifcond=None):
         super().__init__(name, info, typ, False, ifcond)
 
 
-class QAPISchemaCommand(QAPISchemaEntity):
+class QAPISchemaCommand(QAPISchemaDefinition):
     meta = 'command'
 
     def __init__(self, name, info, doc, ifcond, features,
@@ -868,7 +887,7 @@ def visit(self, visitor):
             self.coroutine)
 
 
-class QAPISchemaEvent(QAPISchemaEntity):
+class QAPISchemaEvent(QAPISchemaDefinition):
     meta = 'event'
 
     def __init__(self, name, info, doc, ifcond, features, arg_type, boxed):
@@ -939,23 +958,24 @@ def __init__(self, fname):
         self.check()
 
     def _def_entity(self, ent):
-        # Only the predefined types are allowed to not have info
-        assert ent.info or self._predefining
         self._entity_list.append(ent)
-        if ent.name is None:
-            return
+
+    def _def_definition(self, defn):
+        # Only the predefined types are allowed to not have info
+        assert defn.info or self._predefining
+        self._def_entity(defn)
         # TODO reject names that differ only in '_' vs. '.'  vs. '-',
         # because they're liable to clash in generated C.
-        other_ent = self._entity_dict.get(ent.name)
-        if other_ent:
-            if other_ent.info:
-                where = QAPISourceError(other_ent.info, "previous definition")
+        other_defn = self._entity_dict.get(defn.name)
+        if other_defn:
+            if other_defn.info:
+                where = QAPISourceError(other_defn.info, "previous definition")
                 raise QAPISemError(
-                    ent.info,
-                    "'%s' is already defined\n%s" % (ent.name, where))
+                    defn.info,
+                    "'%s' is already defined\n%s" % (defn.name, where))
             raise QAPISemError(
-                ent.info, "%s is already defined" % other_ent.describe())
-        self._entity_dict[ent.name] = ent
+                defn.info, "%s is already defined" % other_defn.describe())
+        self._entity_dict[defn.name] = defn
 
     def lookup_entity(self, name, typ=None):
         ent = self._entity_dict.get(name)
@@ -997,7 +1017,7 @@ def _def_include(self, expr: QAPIExpression):
             QAPISchemaInclude(self._make_module(include), expr.info))
 
     def _def_builtin_type(self, name, json_type, c_type):
-        self._def_entity(QAPISchemaBuiltinType(name, json_type, c_type))
+        self._def_definition(QAPISchemaBuiltinType(name, json_type, c_type))
         # Instantiating only the arrays that are actually used would
         # be nice, but we can't as long as their generated code
         # (qapi-builtin-types.[ch]) may be shared by some other
@@ -1023,15 +1043,15 @@ def _def_predefineds(self):
             self._def_builtin_type(*t)
         self.the_empty_object_type = QAPISchemaObjectType(
             'q_empty', None, None, None, None, None, [], None)
-        self._def_entity(self.the_empty_object_type)
+        self._def_definition(self.the_empty_object_type)
 
         qtypes = ['none', 'qnull', 'qnum', 'qstring', 'qdict', 'qlist',
                   'qbool']
         qtype_values = self._make_enum_members(
             [{'name': n} for n in qtypes], None)
 
-        self._def_entity(QAPISchemaEnumType('QType', None, None, None, None,
-                                            qtype_values, 'QTYPE'))
+        self._def_definition(QAPISchemaEnumType(
+            'QType', None, None, None, None, qtype_values, 'QTYPE'))
 
     def _make_features(self, features, info):
         if features is None:
@@ -1053,7 +1073,8 @@ def _make_enum_members(self, values, info):
     def _make_array_type(self, element_type, info):
         name = element_type + 'List'    # reserved by check_defn_name_str()
         if not self.lookup_type(name):
-            self._def_entity(QAPISchemaArrayType(name, info, element_type))
+            self._def_definition(QAPISchemaArrayType(
+                name, info, element_type))
         return name
 
     def _make_implicit_object_type(self, name, info, ifcond, role, members):
@@ -1068,7 +1089,7 @@ def _make_implicit_object_type(self, name, info, ifcond, role, members):
             # later.
             pass
         else:
-            self._def_entity(QAPISchemaObjectType(
+            self._def_definition(QAPISchemaObjectType(
                 name, info, None, ifcond, None, None, members, None))
         return name
 
@@ -1079,7 +1100,7 @@ def _def_enum_type(self, expr: QAPIExpression):
         ifcond = QAPISchemaIfCond(expr.get('if'))
         info = expr.info
         features = self._make_features(expr.get('features'), info)
-        self._def_entity(QAPISchemaEnumType(
+        self._def_definition(QAPISchemaEnumType(
             name, info, expr.doc, ifcond, features,
             self._make_enum_members(data, info), prefix))
 
@@ -1107,7 +1128,7 @@ def _def_struct_type(self, expr: QAPIExpression):
         info = expr.info
         ifcond = QAPISchemaIfCond(expr.get('if'))
         features = self._make_features(expr.get('features'), info)
-        self._def_entity(QAPISchemaObjectType(
+        self._def_definition(QAPISchemaObjectType(
             name, info, expr.doc, ifcond, features, base,
             self._make_members(data, info),
             None))
@@ -1137,7 +1158,7 @@ def _def_union_type(self, expr: QAPIExpression):
                                info)
             for (key, value) in data.items()]
         members: List[QAPISchemaObjectTypeMember] = []
-        self._def_entity(
+        self._def_definition(
             QAPISchemaObjectType(name, info, expr.doc, ifcond, features,
                                  base, members,
                                  QAPISchemaVariants(
@@ -1156,7 +1177,7 @@ def _def_alternate_type(self, expr: QAPIExpression):
                                info)
             for (key, value) in data.items()]
         tag_member = QAPISchemaObjectTypeMember('type', info, 'QType', False)
-        self._def_entity(
+        self._def_definition(
             QAPISchemaAlternateType(
                 name, info, expr.doc, ifcond, features,
                 QAPISchemaVariants(None, info, tag_member, variants)))
@@ -1181,11 +1202,10 @@ def _def_command(self, expr: QAPIExpression):
         if isinstance(rets, list):
             assert len(rets) == 1
             rets = self._make_array_type(rets[0], info)
-        self._def_entity(QAPISchemaCommand(name, info, expr.doc, ifcond,
-                                           features, data, rets,
-                                           gen, success_response,
-                                           boxed, allow_oob, allow_preconfig,
-                                           coroutine))
+        self._def_definition(
+            QAPISchemaCommand(name, info, expr.doc, ifcond, features, data,
+                              rets, gen, success_response, boxed, allow_oob,
+                              allow_preconfig, coroutine))
 
     def _def_event(self, expr: QAPIExpression):
         name = expr['event']
@@ -1198,8 +1218,8 @@ def _def_event(self, expr: QAPIExpression):
             data = self._make_implicit_object_type(
                 name, info, ifcond,
                 'arg', self._make_members(data, info))
-        self._def_entity(QAPISchemaEvent(name, info, expr.doc, ifcond,
-                                         features, data, boxed))
+        self._def_definition(QAPISchemaEvent(name, info, expr.doc, ifcond,
+                                             features, data, boxed))
 
     def _def_exprs(self, exprs):
         for expr in exprs:
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 06/25] qapi/schema: declare type for QAPISchemaObjectTypeMember.type
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (4 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 05/25] qapi: create QAPISchemaDefinition Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 07/25] qapi/schema: declare type for QAPISchemaArrayType.element_type Markus Armbruster
                   ` (19 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

A QAPISchemaObjectTypeMember's type gets resolved only during .check().
We have QAPISchemaObjectTypeMember.__init__() initialize self.type =
None, and .check() assign the actual type.  Using .type before .check()
is wrong, and hopefully crashes due to the value being None.  Works.

However, it makes for awkward typing.  With .type:
Optional[QAPISchemaType], mypy is of course unable to see that it's None
before .check(), and a QAPISchemaType after.  To help it over the hump,
we'd have to assert self.type is not None before all the (valid) uses.
The assertion catches invalid uses, but only at run time; mypy can't
flag them.

Instead, declare .type in .__init__() as QAPISchemaType *without*
initializing it.  Using .type before .check() now certainly crashes,
which is an improvement.  Mypy still can't flag invalid uses, but that's
okay.

Addresses typing errors such as these:

qapi/schema.py:657: error: "None" has no attribute "alternate_qtype"  [attr-defined]
qapi/schema.py:662: error: "None" has no attribute "describe"  [attr-defined]

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index b298c8b4f9..307f8af01a 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -786,7 +786,7 @@ def __init__(self, name, info, typ, optional, ifcond=None, features=None):
             assert isinstance(f, QAPISchemaFeature)
             f.set_defined_in(name)
         self._type_name = typ
-        self.type = None
+        self.type: QAPISchemaType  # set during check()
         self.optional = optional
         self.features = features or []
 
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 07/25] qapi/schema: declare type for QAPISchemaArrayType.element_type
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (5 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 06/25] qapi/schema: declare type for QAPISchemaObjectTypeMember.type Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 08/25] qapi/schema: make c_type() and json_type() abstract methods Markus Armbruster
                   ` (18 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

A QAPISchemaArrayType's element type gets resolved only during .check().
We have QAPISchemaArrayType.__init__() initialize self.element_type =
None, and .check() assign the actual type.  Using .element_type before
.check() is wrong, and hopefully crashes due to the value being None.
Works.

However, it makes for awkward typing.  With .element_type:
Optional[QAPISchemaType], mypy is of course unable to see that it's None
before .check(), and a QAPISchemaType after.  To help it over the hump,
we'd have to assert self.element_type is not None before all the (valid)
uses.  The assertion catches invalid uses, but only at run time; mypy
can't flag them.

Instead, declare .element_type in .__init__() as QAPISchemaType
*without* initializing it.  Using .element_type before .check() now
certainly crashes, which is an improvement.  Mypy still can't flag
invalid uses, but that's okay.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 307f8af01a..48f157fb91 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -381,7 +381,7 @@ def __init__(self, name, info, element_type):
         super().__init__(name, info, None)
         assert isinstance(element_type, str)
         self._element_type_name = element_type
-        self.element_type = None
+        self.element_type: QAPISchemaType
 
     def need_has_if_optional(self):
         # When FOO is an array, we still need has_FOO to distinguish
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 08/25] qapi/schema: make c_type() and json_type() abstract methods
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (6 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 07/25] qapi/schema: declare type for QAPISchemaArrayType.element_type Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 09/25] qapi/schema: adjust type narrowing for mypy's benefit Markus Armbruster
                   ` (17 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

These methods should always return a str, it's only the default abstract
implementation that doesn't. They can be marked "abstract", which
requires subclasses to override the method with the proper return type.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 48f157fb91..0b01c841ff 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -16,6 +16,7 @@
 
 # TODO catching name collisions in generated code would be nice
 
+from abc import ABC, abstractmethod
 from collections import OrderedDict
 import os
 import re
@@ -245,9 +246,10 @@ def visit(self, visitor):
         visitor.visit_include(self._sub_module.name, self.info)
 
 
-class QAPISchemaType(QAPISchemaDefinition):
+class QAPISchemaType(QAPISchemaDefinition, ABC):
     # Return the C type for common use.
     # For the types we commonly box, this is a pointer type.
+    @abstractmethod
     def c_type(self):
         pass
 
@@ -259,6 +261,7 @@ def c_param_type(self):
     def c_unboxed_type(self):
         return self.c_type()
 
+    @abstractmethod
     def json_type(self):
         pass
 
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 09/25] qapi/schema: adjust type narrowing for mypy's benefit
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (7 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 08/25] qapi/schema: make c_type() and json_type() abstract methods Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 10/25] qapi/schema: add type narrowing to lookup_type() Markus Armbruster
                   ` (16 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

We already take care to perform some type narrowing for arg_type and
ret_type, but not in a way where mypy can utilize the result once we add
type hints, e.g.:

qapi/schema.py:833: error: Incompatible types in assignment (expression
has type "QAPISchemaType", variable has type
"Optional[QAPISchemaObjectType]") [assignment]

qapi/schema.py:893: error: Incompatible types in assignment (expression
has type "QAPISchemaType", variable has type
"Optional[QAPISchemaObjectType]") [assignment]

A simple change to use a temporary variable helps the medicine go down.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 16 +++++++++-------
 1 file changed, 9 insertions(+), 7 deletions(-)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 0b01c841ff..e44802369d 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -843,13 +843,14 @@ def __init__(self, name, info, doc, ifcond, features,
     def check(self, schema):
         super().check(schema)
         if self._arg_type_name:
-            self.arg_type = schema.resolve_type(
+            arg_type = schema.resolve_type(
                 self._arg_type_name, self.info, "command's 'data'")
-            if not isinstance(self.arg_type, QAPISchemaObjectType):
+            if not isinstance(arg_type, QAPISchemaObjectType):
                 raise QAPISemError(
                     self.info,
                     "command's 'data' cannot take %s"
-                    % self.arg_type.describe())
+                    % arg_type.describe())
+            self.arg_type = arg_type
             if self.arg_type.variants and not self.boxed:
                 raise QAPISemError(
                     self.info,
@@ -866,8 +867,8 @@ def check(self, schema):
             if self.name not in self.info.pragma.command_returns_exceptions:
                 typ = self.ret_type
                 if isinstance(typ, QAPISchemaArrayType):
-                    typ = self.ret_type.element_type
                     assert typ
+                    typ = typ.element_type
                 if not isinstance(typ, QAPISchemaObjectType):
                     raise QAPISemError(
                         self.info,
@@ -903,13 +904,14 @@ def __init__(self, name, info, doc, ifcond, features, arg_type, boxed):
     def check(self, schema):
         super().check(schema)
         if self._arg_type_name:
-            self.arg_type = schema.resolve_type(
+            typ = schema.resolve_type(
                 self._arg_type_name, self.info, "event's 'data'")
-            if not isinstance(self.arg_type, QAPISchemaObjectType):
+            if not isinstance(typ, QAPISchemaObjectType):
                 raise QAPISemError(
                     self.info,
                     "event's 'data' cannot take %s"
-                    % self.arg_type.describe())
+                    % typ.describe())
+            self.arg_type = typ
             if self.arg_type.variants and not self.boxed:
                 raise QAPISemError(
                     self.info,
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 10/25] qapi/schema: add type narrowing to lookup_type()
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (8 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 09/25] qapi/schema: adjust type narrowing for mypy's benefit Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 11/25] qapi/schema: assert resolve_type has 'info' and 'what' args on error Markus Armbruster
                   ` (15 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

This function is a bit hard to type as-is; mypy needs some assertions to
assist with the type narrowing.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index e44802369d..1034825415 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -989,7 +989,9 @@ def lookup_entity(self, name, typ=None):
         return ent
 
     def lookup_type(self, name):
-        return self.lookup_entity(name, QAPISchemaType)
+        typ = self.lookup_entity(name, QAPISchemaType)
+        assert typ is None or isinstance(typ, QAPISchemaType)
+        return typ
 
     def resolve_type(self, name, info, what):
         typ = self.lookup_type(name)
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 11/25] qapi/schema: assert resolve_type has 'info' and 'what' args on error
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (9 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 10/25] qapi/schema: add type narrowing to lookup_type() Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 12/25] qapi: Assert built-in types exist Markus Armbruster
                   ` (14 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

resolve_type() is generally used to resolve configuration-provided type
names into type objects, and generally requires valid 'info' and 'what'
parameters.

In some cases, such as with QAPISchemaArrayType.check(), resolve_type
may be used to resolve built-in types and as such will not have an
'info' argument, but also must not fail in this scenario.

Use an assertion to sate mypy that we will indeed have 'info' and 'what'
parameters for the error pathway in resolve_type.

Note: there are only three callsites to resolve_type at present where
"info" is perceived by mypy to be possibly None:

    1) QAPISchemaArrayType.check()
    2) QAPISchemaObjectTypeMember.check()
    3) QAPISchemaEvent.check()

    Of those three, only the first actually ever passes None; the other two
    are limited by their base class initializers which accept info=None, but
    neither subclass actually use a None value in practice, currently.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 1034825415..0ef9b3398a 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -996,6 +996,7 @@ def lookup_type(self, name):
     def resolve_type(self, name, info, what):
         typ = self.lookup_type(name)
         if not typ:
+            assert info and what  # built-in types must not fail lookup
             if callable(what):
                 what = what(info)
             raise QAPISemError(
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 12/25] qapi: Assert built-in types exist
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (10 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 11/25] qapi/schema: assert resolve_type has 'info' and 'what' args on error Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-19 15:24   ` John Snow
  2024-03-15 15:22 ` [PATCH v5 13/25] qapi/schema: fix QAPISchemaArrayType.check's call to resolve_type Markus Armbruster
                   ` (13 subsequent siblings)
  25 siblings, 1 reply; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

QAPISchema.lookup_type('FOO') returns a QAPISchemaType when type 'FOO'
exists, else None.  It won't return None for built-in types like
'int'.

Since mypy can't see that, it'll complain that we assign the
Optional[QAPISchemaType] returned by .lookup_type() to QAPISchemaType
variables.

Add assertions to help it over the hump.

Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/introspect.py | 8 ++++++--
 1 file changed, 6 insertions(+), 2 deletions(-)

diff --git a/scripts/qapi/introspect.py b/scripts/qapi/introspect.py
index 67c7d89aae..4679b1bc2c 100644
--- a/scripts/qapi/introspect.py
+++ b/scripts/qapi/introspect.py
@@ -227,10 +227,14 @@ def _use_type(self, typ: QAPISchemaType) -> str:
 
         # Map the various integer types to plain int
         if typ.json_type() == 'int':
-            typ = self._schema.lookup_type('int')
+            type_int = self._schema.lookup_type('int')
+            assert type_int
+            typ = type_int
         elif (isinstance(typ, QAPISchemaArrayType) and
               typ.element_type.json_type() == 'int'):
-            typ = self._schema.lookup_type('intList')
+            type_intList = self._schema.lookup_type('intList')
+            assert type_intList
+            typ = type_intList
         # Add type to work queue if new
         if typ not in self._used_types:
             self._used_types.append(typ)
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 13/25] qapi/schema: fix QAPISchemaArrayType.check's call to resolve_type
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (11 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 12/25] qapi: Assert built-in types exist Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 14/25] qapi/schema: assert info is present when necessary Markus Armbruster
                   ` (12 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

Adjust the expression at the callsite to work around mypy's weak type
introspection that believes this expression can resolve to
QAPISourceInfo; it cannot.

(Fundamentally: self.info only resolves to false in a boolean expression
when it is None; therefore this expression may only ever produce
Optional[str]. mypy does not know that 'info', when it is a
QAPISourceInfo object, cannot ever be false.)

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 0ef9b3398a..087c6e9366 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -395,7 +395,7 @@ def check(self, schema):
         super().check(schema)
         self.element_type = schema.resolve_type(
             self._element_type_name, self.info,
-            self.info and self.info.defn_meta)
+            self.info.defn_meta if self.info else None)
         assert not isinstance(self.element_type, QAPISchemaArrayType)
 
     def set_module(self, schema):
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 14/25] qapi/schema: assert info is present when necessary
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (12 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 13/25] qapi/schema: fix QAPISchemaArrayType.check's call to resolve_type Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 15/25] qapi/schema: add _check_complete flag Markus Armbruster
                   ` (11 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

QAPISchemaInfo arguments can often be None because built-in definitions
don't have such information.  The type hint can only be
Optional[QAPISchemaInfo] then.  But, mypy gets upset about all the
places where we exploit that it can't actually be None there.  Add
assertions that will help mypy over the hump, to enable adding type
hints in a forthcoming commit.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 087c6e9366..173e27d9e2 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -751,6 +751,7 @@ def describe(self, info):
             else:
                 assert False
 
+        assert info is not None
         if defined_in != info.defn_name:
             return "%s '%s' of %s '%s'" % (role, self.name, meta, defined_in)
         return "%s '%s'" % (role, self.name)
@@ -841,6 +842,7 @@ def __init__(self, name, info, doc, ifcond, features,
         self.coroutine = coroutine
 
     def check(self, schema):
+        assert self.info is not None
         super().check(schema)
         if self._arg_type_name:
             arg_type = schema.resolve_type(
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 15/25] qapi/schema: add _check_complete flag
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (13 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 14/25] qapi/schema: assert info is present when necessary Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 16/25] qapi/schema: Don't initialize "members" with `None` Markus Armbruster
                   ` (10 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

Instead of using the None value for the members field, use a dedicated
flag to detect recursive misconfigurations.

This is intended to assist with subsequent patches that seek to remove
the "None" value from the members field (which can never hold that value
after the final call to check()) in order to simplify the static typing
of that field; avoiding the need of assertions littered at many
callsites to eliminate the possibility of the None value.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 8 +++++---
 1 file changed, 5 insertions(+), 3 deletions(-)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 173e27d9e2..2c3de72ae6 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -450,12 +450,13 @@ def __init__(self, name, info, doc, ifcond, features,
         self.local_members = local_members
         self.variants = variants
         self.members = None
+        self._check_complete = False
 
     def check(self, schema):
         # This calls another type T's .check() exactly when the C
         # struct emitted by gen_object() contains that T's C struct
         # (pointers don't count).
-        if self.members is not None:
+        if self._check_complete:
             # A previous .check() completed: nothing to do
             return
         if self._checked:
@@ -464,7 +465,7 @@ def check(self, schema):
                                "object %s contains itself" % self.name)
 
         super().check(schema)
-        assert self._checked and self.members is None
+        assert self._checked and not self._check_complete
 
         seen = OrderedDict()
         if self._base_name:
@@ -487,7 +488,8 @@ def check(self, schema):
             self.variants.check(schema, seen)
             self.variants.check_clash(self.info, seen)
 
-        self.members = members  # mark completed
+        self.members = members
+        self._check_complete = True  # mark completed
 
     # Check that the members of this type do not cause duplicate JSON members,
     # and update seen to track the members seen so far. Report any errors
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 16/25] qapi/schema: Don't initialize "members" with `None`
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (14 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 15/25] qapi/schema: add _check_complete flag Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 17/25] qapi/schema: fix typing for QAPISchemaVariants.tag_member Markus Armbruster
                   ` (9 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

Declare, but don't initialize the "members" field with type
List[QAPISchemaObjectTypeMember].

This simplifies the typing from what would otherwise be
Optional[List[T]] to merely List[T]. This removes the need to add
assertions to several callsites that this value is not None - which it
never will be after the delayed initialization in check() anyway.

The type declaration without initialization trick will cause accidental
uses of this field prior to full initialization to raise an
AttributeError.

(Note that it is valid to have an empty members list, see the internal
q_empty object as an example. For this reason, we cannot use the empty
list as a replacement test for full initialization and instead rely on
the _checked/_check_complete fields.)

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 2c3de72ae6..74b0d7b007 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -20,7 +20,7 @@
 from collections import OrderedDict
 import os
 import re
-from typing import List, Optional
+from typing import List, Optional, cast
 
 from .common import (
     POINTER_SUFFIX,
@@ -449,7 +449,7 @@ def __init__(self, name, info, doc, ifcond, features,
         self.base = None
         self.local_members = local_members
         self.variants = variants
-        self.members = None
+        self.members: List[QAPISchemaObjectTypeMember]
         self._check_complete = False
 
     def check(self, schema):
@@ -482,7 +482,11 @@ def check(self, schema):
         for m in self.local_members:
             m.check(schema)
             m.check_clash(self.info, seen)
-        members = seen.values()
+
+        # self.check_clash() works in terms of the supertype, but
+        # self.members is declared List[QAPISchemaObjectTypeMember].
+        # Cast down to the subtype.
+        members = cast(List[QAPISchemaObjectTypeMember], list(seen.values()))
 
         if self.variants:
             self.variants.check(schema, seen)
@@ -515,11 +519,9 @@ def is_implicit(self):
         return self.name.startswith('q_')
 
     def is_empty(self):
-        assert self.members is not None
         return not self.members and not self.variants
 
     def has_conditional_members(self):
-        assert self.members is not None
         return any(m.ifcond.is_present() for m in self.members)
 
     def c_name(self):
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 17/25] qapi/schema: fix typing for QAPISchemaVariants.tag_member
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (15 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 16/25] qapi/schema: Don't initialize "members" with `None` Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 18/25] qapi/schema: assert inner type of QAPISchemaVariants in check_clash() Markus Armbruster
                   ` (8 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

There are two related changes here:

(1) We need to perform type narrowing for resolving the type of
    tag_member during check(), and

(2) tag_member is a delayed initialization field, but we can hide it
    behind a property that raises an Exception if it's called too
    early. This simplifies the typing in quite a few places and avoids
    needing to assert that the "tag_member is not None" at a dozen
    callsites, which can be confusing and suggest the wrong thing to a
    drive-by contributor.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 22 +++++++++++++++++++---
 1 file changed, 19 insertions(+), 3 deletions(-)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 74b0d7b007..9c138badb0 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -627,25 +627,39 @@ def __init__(self, tag_name, info, tag_member, variants):
             assert isinstance(v, QAPISchemaVariant)
         self._tag_name = tag_name
         self.info = info
-        self.tag_member = tag_member
+        self._tag_member: Optional[QAPISchemaObjectTypeMember] = tag_member
         self.variants = variants
 
+    @property
+    def tag_member(self) -> 'QAPISchemaObjectTypeMember':
+        if self._tag_member is None:
+            raise RuntimeError(
+                "QAPISchemaVariants has no tag_member property until "
+                "after check() has been run."
+            )
+        return self._tag_member
+
     def set_defined_in(self, name):
         for v in self.variants:
             v.set_defined_in(name)
 
     def check(self, schema, seen):
         if self._tag_name:      # union
-            self.tag_member = seen.get(c_name(self._tag_name))
+            # We need to narrow the member type:
+            tmp = seen.get(c_name(self._tag_name))
+            assert tmp is None or isinstance(tmp, QAPISchemaObjectTypeMember)
+            self._tag_member = tmp
+
             base = "'base'"
             # Pointing to the base type when not implicit would be
             # nice, but we don't know it here
-            if not self.tag_member or self._tag_name != self.tag_member.name:
+            if not self._tag_member or self._tag_name != self._tag_member.name:
                 raise QAPISemError(
                     self.info,
                     "discriminator '%s' is not a member of %s"
                     % (self._tag_name, base))
             # Here we do:
+            assert self.tag_member.defined_in
             base_type = schema.lookup_type(self.tag_member.defined_in)
             assert base_type
             if not base_type.is_implicit():
@@ -666,11 +680,13 @@ def check(self, schema, seen):
                     "discriminator member '%s' of %s must not be conditional"
                     % (self._tag_name, base))
         else:                   # alternate
+            assert self._tag_member
             assert isinstance(self.tag_member.type, QAPISchemaEnumType)
             assert not self.tag_member.optional
             assert not self.tag_member.ifcond.is_present()
         if self._tag_name:      # union
             # branches that are not explicitly covered get an empty type
+            assert self.tag_member.defined_in
             cases = {v.name for v in self.variants}
             for m in self.tag_member.type.members:
                 if m.name not in cases:
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 18/25] qapi/schema: assert inner type of QAPISchemaVariants in check_clash()
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (16 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 17/25] qapi/schema: fix typing for QAPISchemaVariants.tag_member Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 19/25] qapi/parser: demote QAPIExpression to Dict[str, Any] Markus Armbruster
                   ` (7 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

QAPISchemaVariant's "variants" field is typed as
List[QAPISchemaVariant], where the typing for QAPISchemaVariant allows
its type field to be any QAPISchemaType.

However, QAPISchemaVariant expects that all of its variants contain the
narrower QAPISchemaObjectType. This relationship is enforced at runtime
in QAPISchemaVariants.check(). This relationship is not embedded in the
type system though, so QAPISchemaVariants.check_clash() needs to
re-assert this property in order to call
QAPISchemaVariant.type.check_clash().

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 9c138badb0..177bfa0d11 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -716,7 +716,10 @@ def check(self, schema, seen):
     def check_clash(self, info, seen):
         for v in self.variants:
             # Reset seen map for each variant, since qapi names from one
-            # branch do not affect another branch
+            # branch do not affect another branch.
+            #
+            # v.type's typing is enforced in check() above.
+            assert isinstance(v.type, QAPISchemaObjectType)
             v.type.check_clash(info, dict(seen))
 
 
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 19/25] qapi/parser: demote QAPIExpression to Dict[str, Any]
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (17 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 18/25] qapi/schema: assert inner type of QAPISchemaVariants in check_clash() Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 20/25] qapi/parser.py: assert member.info is present in connect_member Markus Armbruster
                   ` (6 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

Dict[str, object] is a stricter type, but with the way that code is
currently arranged, it is infeasible to enforce this strictness.

In particular, although expr.py's entire raison d'être is normalization
and type-checking of QAPI Expressions, that type information is not
"remembered" in any meaningful way by mypy because each individual
expression is not downcast to a specific expression type that holds all
the details of each expression's unique form.

As a result, all of the code in schema.py that deals with actually
creating type-safe specialized structures has no guarantee (myopically)
that the data it is being passed is correct.

There are two ways to solve this:

(1) Re-assert that the incoming data is in the shape we expect it to be, or
(2) Disable type checking for this data.

(1) is appealing to my sense of strictness, but I gotta concede that it
is asinine to re-check the shape of a QAPIExpression in schema.py when
expr.py has just completed that work at length. The duplication of code
and the nightmare thought of needing to update both locations if and
when we change the shape of these structures makes me extremely
reluctant to go down this route.

(2) allows us the chance to miss updating types in the case that types
are updated in expr.py, but it *is* an awful lot simpler and,
importantly, gets us closer to type checking schema.py *at
all*. Something is better than nothing, I'd argue.

So, do the simpler dumber thing and worry about future strictness
improvements later.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/parser.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index ec4ebef4e3..2f3c704fa2 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -19,6 +19,7 @@
 import re
 from typing import (
     TYPE_CHECKING,
+    Any,
     Dict,
     List,
     Mapping,
@@ -43,7 +44,7 @@
 _ExprValue = Union[List[object], Dict[str, object], str, bool]
 
 
-class QAPIExpression(Dict[str, object]):
+class QAPIExpression(Dict[str, Any]):
     # pylint: disable=too-few-public-methods
     def __init__(self,
                  data: Mapping[str, object],
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 20/25] qapi/parser.py: assert member.info is present in connect_member
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (18 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 19/25] qapi/parser: demote QAPIExpression to Dict[str, Any] Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 21/25] qapi/schema: add type hints Markus Armbruster
                   ` (5 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/parser.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index 2f3c704fa2..7b13a583ac 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -707,6 +707,7 @@ def append_line(self, line: str) -> None:
 
     def connect_member(self, member: 'QAPISchemaMember') -> None:
         if member.name not in self.args:
+            assert member.info
             if self.symbol not in member.info.pragma.documentation_exceptions:
                 raise QAPISemError(member.info,
                                    "%s '%s' lacks documentation"
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 21/25] qapi/schema: add type hints
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (19 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 20/25] qapi/parser.py: assert member.info is present in connect_member Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 22/25] qapi/schema: turn on mypy strictness Markus Armbruster
                   ` (4 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

This patch only adds type hints, which aren't utilized at runtime and
don't change the behavior of this module in any way.

In a scant few locations, type hints are removed where no longer
necessary due to inference power from typing all of the rest of
creation; and any type hints that no longer need string quotes are
changed.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 568 ++++++++++++++++++++++++++++-------------
 1 file changed, 396 insertions(+), 172 deletions(-)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 177bfa0d11..ef4d6a7def 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -16,11 +16,21 @@
 
 # TODO catching name collisions in generated code would be nice
 
+from __future__ import annotations
+
 from abc import ABC, abstractmethod
 from collections import OrderedDict
 import os
 import re
-from typing import List, Optional, cast
+from typing import (
+    Any,
+    Callable,
+    Dict,
+    List,
+    Optional,
+    Union,
+    cast,
+)
 
 from .common import (
     POINTER_SUFFIX,
@@ -32,26 +42,30 @@
 )
 from .error import QAPIError, QAPISemError, QAPISourceError
 from .expr import check_exprs
-from .parser import QAPIExpression, QAPISchemaParser
+from .parser import QAPIDoc, QAPIExpression, QAPISchemaParser
+from .source import QAPISourceInfo
 
 
 class QAPISchemaIfCond:
-    def __init__(self, ifcond=None):
+    def __init__(
+        self,
+        ifcond: Optional[Union[str, Dict[str, object]]] = None,
+    ) -> None:
         self.ifcond = ifcond
 
-    def _cgen(self):
+    def _cgen(self) -> str:
         return cgen_ifcond(self.ifcond)
 
-    def gen_if(self):
+    def gen_if(self) -> str:
         return gen_if(self._cgen())
 
-    def gen_endif(self):
+    def gen_endif(self) -> str:
         return gen_endif(self._cgen())
 
-    def docgen(self):
+    def docgen(self) -> str:
         return docgen_ifcond(self.ifcond)
 
-    def is_present(self):
+    def is_present(self) -> bool:
         return bool(self.ifcond)
 
 
@@ -62,8 +76,8 @@ class QAPISchemaEntity:
     This is either a directive, such as include, or a definition.
     The latter uses sub-class `QAPISchemaDefinition`.
     """
-    def __init__(self, info):
-        self._module = None
+    def __init__(self, info: Optional[QAPISourceInfo]):
+        self._module: Optional[QAPISchemaModule] = None
         # For explicitly defined entities, info points to the (explicit)
         # definition.  For builtins (and their arrays), info is None.
         # For implicitly defined entities, info points to a place that
@@ -72,34 +86,43 @@ def __init__(self, info):
         self.info = info
         self._checked = False
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return "<%s at 0x%x>" % (type(self).__name__, id(self))
 
-    def check(self, schema):
+    def check(self, schema: QAPISchema) -> None:
         # pylint: disable=unused-argument
         self._checked = True
 
-    def connect_doc(self, doc=None):
+    def connect_doc(self, doc: Optional[QAPIDoc] = None) -> None:
         pass
 
-    def _set_module(self, schema, info):
+    def _set_module(
+        self, schema: QAPISchema, info: Optional[QAPISourceInfo]
+    ) -> None:
         assert self._checked
         fname = info.fname if info else QAPISchemaModule.BUILTIN_MODULE_NAME
         self._module = schema.module_by_fname(fname)
         self._module.add_entity(self)
 
-    def set_module(self, schema):
+    def set_module(self, schema: QAPISchema) -> None:
         self._set_module(schema, self.info)
 
-    def visit(self, visitor):
+    def visit(self, visitor: QAPISchemaVisitor) -> None:
         # pylint: disable=unused-argument
         assert self._checked
 
 
 class QAPISchemaDefinition(QAPISchemaEntity):
-    meta: Optional[str] = None
+    meta: str
 
-    def __init__(self, name: str, info, doc, ifcond=None, features=None):
+    def __init__(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        doc: Optional[QAPIDoc],
+        ifcond: Optional[QAPISchemaIfCond] = None,
+        features: Optional[List[QAPISchemaFeature]] = None,
+    ):
         assert isinstance(name, str)
         super().__init__(info)
         for f in features or []:
@@ -110,21 +133,21 @@ def __init__(self, name: str, info, doc, ifcond=None, features=None):
         self._ifcond = ifcond or QAPISchemaIfCond()
         self.features = features or []
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return "<%s:%s at 0x%x>" % (type(self).__name__, self.name,
                                     id(self))
 
-    def c_name(self):
+    def c_name(self) -> str:
         return c_name(self.name)
 
-    def check(self, schema):
+    def check(self, schema: QAPISchema) -> None:
         assert not self._checked
         super().check(schema)
-        seen = {}
+        seen: Dict[str, QAPISchemaMember] = {}
         for f in self.features:
             f.check_clash(self.info, seen)
 
-    def connect_doc(self, doc=None):
+    def connect_doc(self, doc: Optional[QAPIDoc] = None) -> None:
         super().connect_doc(doc)
         doc = doc or self.doc
         if doc:
@@ -132,62 +155,120 @@ def connect_doc(self, doc=None):
                 doc.connect_feature(f)
 
     @property
-    def ifcond(self):
+    def ifcond(self) -> QAPISchemaIfCond:
         assert self._checked
         return self._ifcond
 
-    def is_implicit(self):
+    def is_implicit(self) -> bool:
         return not self.info
 
-    def describe(self):
+    def describe(self) -> str:
         assert self.meta
         return "%s '%s'" % (self.meta, self.name)
 
 
 class QAPISchemaVisitor:
-    def visit_begin(self, schema):
+    def visit_begin(self, schema: QAPISchema) -> None:
         pass
 
-    def visit_end(self):
+    def visit_end(self) -> None:
         pass
 
-    def visit_module(self, name):
+    def visit_module(self, name: str) -> None:
         pass
 
-    def visit_needed(self, entity):
+    def visit_needed(self, entity: QAPISchemaEntity) -> bool:
         # pylint: disable=unused-argument
         # Default to visiting everything
         return True
 
-    def visit_include(self, name, info):
+    def visit_include(self, name: str, info: Optional[QAPISourceInfo]) -> None:
         pass
 
-    def visit_builtin_type(self, name, info, json_type):
+    def visit_builtin_type(
+        self, name: str, info: Optional[QAPISourceInfo], json_type: str
+    ) -> None:
         pass
 
-    def visit_enum_type(self, name, info, ifcond, features, members, prefix):
+    def visit_enum_type(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        ifcond: QAPISchemaIfCond,
+        features: List[QAPISchemaFeature],
+        members: List[QAPISchemaEnumMember],
+        prefix: Optional[str],
+    ) -> None:
         pass
 
-    def visit_array_type(self, name, info, ifcond, element_type):
+    def visit_array_type(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        ifcond: QAPISchemaIfCond,
+        element_type: QAPISchemaType,
+    ) -> None:
         pass
 
-    def visit_object_type(self, name, info, ifcond, features,
-                          base, members, variants):
+    def visit_object_type(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        ifcond: QAPISchemaIfCond,
+        features: List[QAPISchemaFeature],
+        base: Optional[QAPISchemaObjectType],
+        members: List[QAPISchemaObjectTypeMember],
+        variants: Optional[QAPISchemaVariants],
+    ) -> None:
         pass
 
-    def visit_object_type_flat(self, name, info, ifcond, features,
-                               members, variants):
+    def visit_object_type_flat(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        ifcond: QAPISchemaIfCond,
+        features: List[QAPISchemaFeature],
+        members: List[QAPISchemaObjectTypeMember],
+        variants: Optional[QAPISchemaVariants],
+    ) -> None:
         pass
 
-    def visit_alternate_type(self, name, info, ifcond, features, variants):
+    def visit_alternate_type(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        ifcond: QAPISchemaIfCond,
+        features: List[QAPISchemaFeature],
+        variants: QAPISchemaVariants,
+    ) -> None:
         pass
 
-    def visit_command(self, name, info, ifcond, features,
-                      arg_type, ret_type, gen, success_response, boxed,
-                      allow_oob, allow_preconfig, coroutine):
+    def visit_command(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        ifcond: QAPISchemaIfCond,
+        features: List[QAPISchemaFeature],
+        arg_type: Optional[QAPISchemaObjectType],
+        ret_type: Optional[QAPISchemaType],
+        gen: bool,
+        success_response: bool,
+        boxed: bool,
+        allow_oob: bool,
+        allow_preconfig: bool,
+        coroutine: bool,
+    ) -> None:
         pass
 
-    def visit_event(self, name, info, ifcond, features, arg_type, boxed):
+    def visit_event(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        ifcond: QAPISchemaIfCond,
+        features: List[QAPISchemaFeature],
+        arg_type: Optional[QAPISchemaObjectType],
+        boxed: bool,
+    ) -> None:
         pass
 
 
@@ -195,9 +276,9 @@ class QAPISchemaModule:
 
     BUILTIN_MODULE_NAME = './builtin'
 
-    def __init__(self, name):
+    def __init__(self, name: str):
         self.name = name
-        self._entity_list = []
+        self._entity_list: List[QAPISchemaEntity] = []
 
     @staticmethod
     def is_system_module(name: str) -> bool:
@@ -226,10 +307,10 @@ def is_builtin_module(cls, name: str) -> bool:
         """
         return name == cls.BUILTIN_MODULE_NAME
 
-    def add_entity(self, ent):
+    def add_entity(self, ent: QAPISchemaEntity) -> None:
         self._entity_list.append(ent)
 
-    def visit(self, visitor):
+    def visit(self, visitor: QAPISchemaVisitor) -> None:
         visitor.visit_module(self.name)
         for entity in self._entity_list:
             if visitor.visit_needed(entity):
@@ -237,11 +318,11 @@ def visit(self, visitor):
 
 
 class QAPISchemaInclude(QAPISchemaEntity):
-    def __init__(self, sub_module, info):
+    def __init__(self, sub_module: QAPISchemaModule, info: QAPISourceInfo):
         super().__init__(info)
         self._sub_module = sub_module
 
-    def visit(self, visitor):
+    def visit(self, visitor: QAPISchemaVisitor) -> None:
         super().visit(visitor)
         visitor.visit_include(self._sub_module.name, self.info)
 
@@ -250,22 +331,22 @@ class QAPISchemaType(QAPISchemaDefinition, ABC):
     # Return the C type for common use.
     # For the types we commonly box, this is a pointer type.
     @abstractmethod
-    def c_type(self):
+    def c_type(self) -> str:
         pass
 
     # Return the C type to be used in a parameter list.
-    def c_param_type(self):
+    def c_param_type(self) -> str:
         return self.c_type()
 
     # Return the C type to be used where we suppress boxing.
-    def c_unboxed_type(self):
+    def c_unboxed_type(self) -> str:
         return self.c_type()
 
     @abstractmethod
-    def json_type(self):
+    def json_type(self) -> str:
         pass
 
-    def alternate_qtype(self):
+    def alternate_qtype(self) -> Optional[str]:
         json2qtype = {
             'null':    'QTYPE_QNULL',
             'string':  'QTYPE_QSTRING',
@@ -277,17 +358,17 @@ def alternate_qtype(self):
         }
         return json2qtype.get(self.json_type())
 
-    def doc_type(self):
+    def doc_type(self) -> Optional[str]:
         if self.is_implicit():
             return None
         return self.name
 
-    def need_has_if_optional(self):
+    def need_has_if_optional(self) -> bool:
         # When FOO is a pointer, has_FOO == !!FOO, i.e. has_FOO is redundant.
         # Except for arrays; see QAPISchemaArrayType.need_has_if_optional().
         return not self.c_type().endswith(POINTER_SUFFIX)
 
-    def check(self, schema):
+    def check(self, schema: QAPISchema) -> None:
         super().check(schema)
         for feat in self.features:
             if feat.is_special():
@@ -295,7 +376,7 @@ def check(self, schema):
                     self.info,
                     f"feature '{feat.name}' is not supported for types")
 
-    def describe(self):
+    def describe(self) -> str:
         assert self.meta
         return "%s type '%s'" % (self.meta, self.name)
 
@@ -303,7 +384,7 @@ def describe(self):
 class QAPISchemaBuiltinType(QAPISchemaType):
     meta = 'built-in'
 
-    def __init__(self, name, json_type, c_type):
+    def __init__(self, name: str, json_type: str, c_type: str):
         super().__init__(name, None, None)
         assert not c_type or isinstance(c_type, str)
         assert json_type in ('string', 'number', 'int', 'boolean', 'null',
@@ -311,24 +392,24 @@ def __init__(self, name, json_type, c_type):
         self._json_type_name = json_type
         self._c_type_name = c_type
 
-    def c_name(self):
+    def c_name(self) -> str:
         return self.name
 
-    def c_type(self):
+    def c_type(self) -> str:
         return self._c_type_name
 
-    def c_param_type(self):
+    def c_param_type(self) -> str:
         if self.name == 'str':
             return 'const ' + self._c_type_name
         return self._c_type_name
 
-    def json_type(self):
+    def json_type(self) -> str:
         return self._json_type_name
 
-    def doc_type(self):
+    def doc_type(self) -> str:
         return self.json_type()
 
-    def visit(self, visitor):
+    def visit(self, visitor: QAPISchemaVisitor) -> None:
         super().visit(visitor)
         visitor.visit_builtin_type(self.name, self.info, self.json_type())
 
@@ -336,7 +417,16 @@ def visit(self, visitor):
 class QAPISchemaEnumType(QAPISchemaType):
     meta = 'enum'
 
-    def __init__(self, name, info, doc, ifcond, features, members, prefix):
+    def __init__(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        doc: Optional[QAPIDoc],
+        ifcond: Optional[QAPISchemaIfCond],
+        features: Optional[List[QAPISchemaFeature]],
+        members: List[QAPISchemaEnumMember],
+        prefix: Optional[str],
+    ):
         super().__init__(name, info, doc, ifcond, features)
         for m in members:
             assert isinstance(m, QAPISchemaEnumMember)
@@ -345,32 +435,32 @@ def __init__(self, name, info, doc, ifcond, features, members, prefix):
         self.members = members
         self.prefix = prefix
 
-    def check(self, schema):
+    def check(self, schema: QAPISchema) -> None:
         super().check(schema)
-        seen = {}
+        seen: Dict[str, QAPISchemaMember] = {}
         for m in self.members:
             m.check_clash(self.info, seen)
 
-    def connect_doc(self, doc=None):
+    def connect_doc(self, doc: Optional[QAPIDoc] = None) -> None:
         super().connect_doc(doc)
         doc = doc or self.doc
         for m in self.members:
             m.connect_doc(doc)
 
-    def is_implicit(self):
+    def is_implicit(self) -> bool:
         # See QAPISchema._def_predefineds()
         return self.name == 'QType'
 
-    def c_type(self):
+    def c_type(self) -> str:
         return c_name(self.name)
 
-    def member_names(self):
+    def member_names(self) -> List[str]:
         return [m.name for m in self.members]
 
-    def json_type(self):
+    def json_type(self) -> str:
         return 'string'
 
-    def visit(self, visitor):
+    def visit(self, visitor: QAPISchemaVisitor) -> None:
         super().visit(visitor)
         visitor.visit_enum_type(
             self.name, self.info, self.ifcond, self.features,
@@ -380,60 +470,71 @@ def visit(self, visitor):
 class QAPISchemaArrayType(QAPISchemaType):
     meta = 'array'
 
-    def __init__(self, name, info, element_type):
+    def __init__(
+        self, name: str, info: Optional[QAPISourceInfo], element_type: str
+    ):
         super().__init__(name, info, None)
         assert isinstance(element_type, str)
         self._element_type_name = element_type
         self.element_type: QAPISchemaType
 
-    def need_has_if_optional(self):
+    def need_has_if_optional(self) -> bool:
         # When FOO is an array, we still need has_FOO to distinguish
         # absent (!has_FOO) from present and empty (has_FOO && !FOO).
         return True
 
-    def check(self, schema):
+    def check(self, schema: QAPISchema) -> None:
         super().check(schema)
         self.element_type = schema.resolve_type(
             self._element_type_name, self.info,
             self.info.defn_meta if self.info else None)
         assert not isinstance(self.element_type, QAPISchemaArrayType)
 
-    def set_module(self, schema):
+    def set_module(self, schema: QAPISchema) -> None:
         self._set_module(schema, self.element_type.info)
 
     @property
-    def ifcond(self):
+    def ifcond(self) -> QAPISchemaIfCond:
         assert self._checked
         return self.element_type.ifcond
 
-    def is_implicit(self):
+    def is_implicit(self) -> bool:
         return True
 
-    def c_type(self):
+    def c_type(self) -> str:
         return c_name(self.name) + POINTER_SUFFIX
 
-    def json_type(self):
+    def json_type(self) -> str:
         return 'array'
 
-    def doc_type(self):
+    def doc_type(self) -> Optional[str]:
         elt_doc_type = self.element_type.doc_type()
         if not elt_doc_type:
             return None
         return 'array of ' + elt_doc_type
 
-    def visit(self, visitor):
+    def visit(self, visitor: QAPISchemaVisitor) -> None:
         super().visit(visitor)
         visitor.visit_array_type(self.name, self.info, self.ifcond,
                                  self.element_type)
 
-    def describe(self):
+    def describe(self) -> str:
         assert self.meta
         return "%s type ['%s']" % (self.meta, self._element_type_name)
 
 
 class QAPISchemaObjectType(QAPISchemaType):
-    def __init__(self, name, info, doc, ifcond, features,
-                 base, local_members, variants):
+    def __init__(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        doc: Optional[QAPIDoc],
+        ifcond: Optional[QAPISchemaIfCond],
+        features: Optional[List[QAPISchemaFeature]],
+        base: Optional[str],
+        local_members: List[QAPISchemaObjectTypeMember],
+        variants: Optional[QAPISchemaVariants],
+    ):
         # struct has local_members, optional base, and no variants
         # union has base, variants, and no local_members
         super().__init__(name, info, doc, ifcond, features)
@@ -452,7 +553,7 @@ def __init__(self, name, info, doc, ifcond, features,
         self.members: List[QAPISchemaObjectTypeMember]
         self._check_complete = False
 
-    def check(self, schema):
+    def check(self, schema: QAPISchema) -> None:
         # This calls another type T's .check() exactly when the C
         # struct emitted by gen_object() contains that T's C struct
         # (pointers don't count).
@@ -498,14 +599,18 @@ def check(self, schema):
     # Check that the members of this type do not cause duplicate JSON members,
     # and update seen to track the members seen so far. Report any errors
     # on behalf of info, which is not necessarily self.info
-    def check_clash(self, info, seen):
+    def check_clash(
+        self,
+        info: Optional[QAPISourceInfo],
+        seen: Dict[str, QAPISchemaMember],
+    ) -> None:
         assert self._checked
         for m in self.members:
             m.check_clash(info, seen)
         if self.variants:
             self.variants.check_clash(info, seen)
 
-    def connect_doc(self, doc=None):
+    def connect_doc(self, doc: Optional[QAPIDoc] = None) -> None:
         super().connect_doc(doc)
         doc = doc or self.doc
         if self.base and self.base.is_implicit():
@@ -513,32 +618,32 @@ def connect_doc(self, doc=None):
         for m in self.local_members:
             m.connect_doc(doc)
 
-    def is_implicit(self):
+    def is_implicit(self) -> bool:
         # See QAPISchema._make_implicit_object_type(), as well as
         # _def_predefineds()
         return self.name.startswith('q_')
 
-    def is_empty(self):
+    def is_empty(self) -> bool:
         return not self.members and not self.variants
 
-    def has_conditional_members(self):
+    def has_conditional_members(self) -> bool:
         return any(m.ifcond.is_present() for m in self.members)
 
-    def c_name(self):
+    def c_name(self) -> str:
         assert self.name != 'q_empty'
         return super().c_name()
 
-    def c_type(self):
+    def c_type(self) -> str:
         assert not self.is_implicit()
         return c_name(self.name) + POINTER_SUFFIX
 
-    def c_unboxed_type(self):
+    def c_unboxed_type(self) -> str:
         return c_name(self.name)
 
-    def json_type(self):
+    def json_type(self) -> str:
         return 'object'
 
-    def visit(self, visitor):
+    def visit(self, visitor: QAPISchemaVisitor) -> None:
         super().visit(visitor)
         visitor.visit_object_type(
             self.name, self.info, self.ifcond, self.features,
@@ -551,7 +656,15 @@ def visit(self, visitor):
 class QAPISchemaAlternateType(QAPISchemaType):
     meta = 'alternate'
 
-    def __init__(self, name, info, doc, ifcond, features, variants):
+    def __init__(
+        self,
+        name: str,
+        info: QAPISourceInfo,
+        doc: Optional[QAPIDoc],
+        ifcond: Optional[QAPISchemaIfCond],
+        features: List[QAPISchemaFeature],
+        variants: QAPISchemaVariants,
+    ):
         super().__init__(name, info, doc, ifcond, features)
         assert isinstance(variants, QAPISchemaVariants)
         assert variants.tag_member
@@ -559,7 +672,7 @@ def __init__(self, name, info, doc, ifcond, features, variants):
         variants.tag_member.set_defined_in(self.name)
         self.variants = variants
 
-    def check(self, schema):
+    def check(self, schema: QAPISchema) -> None:
         super().check(schema)
         self.variants.tag_member.check(schema)
         # Not calling self.variants.check_clash(), because there's nothing
@@ -567,8 +680,8 @@ def check(self, schema):
         self.variants.check(schema, {})
         # Alternate branch names have no relation to the tag enum values;
         # so we have to check for potential name collisions ourselves.
-        seen = {}
-        types_seen = {}
+        seen: Dict[str, QAPISchemaMember] = {}
+        types_seen: Dict[str, str] = {}
         for v in self.variants.variants:
             v.check_clash(self.info, seen)
             qtype = v.type.alternate_qtype()
@@ -597,26 +710,32 @@ def check(self, schema):
                         % (v.describe(self.info), types_seen[qt]))
                 types_seen[qt] = v.name
 
-    def connect_doc(self, doc=None):
+    def connect_doc(self, doc: Optional[QAPIDoc] = None) -> None:
         super().connect_doc(doc)
         doc = doc or self.doc
         for v in self.variants.variants:
             v.connect_doc(doc)
 
-    def c_type(self):
+    def c_type(self) -> str:
         return c_name(self.name) + POINTER_SUFFIX
 
-    def json_type(self):
+    def json_type(self) -> str:
         return 'value'
 
-    def visit(self, visitor):
+    def visit(self, visitor: QAPISchemaVisitor) -> None:
         super().visit(visitor)
         visitor.visit_alternate_type(
             self.name, self.info, self.ifcond, self.features, self.variants)
 
 
 class QAPISchemaVariants:
-    def __init__(self, tag_name, info, tag_member, variants):
+    def __init__(
+        self,
+        tag_name: Optional[str],
+        info: QAPISourceInfo,
+        tag_member: Optional[QAPISchemaObjectTypeMember],
+        variants: List[QAPISchemaVariant],
+    ):
         # Unions pass tag_name but not tag_member.
         # Alternates pass tag_member but not tag_name.
         # After check(), tag_member is always set.
@@ -627,11 +746,11 @@ def __init__(self, tag_name, info, tag_member, variants):
             assert isinstance(v, QAPISchemaVariant)
         self._tag_name = tag_name
         self.info = info
-        self._tag_member: Optional[QAPISchemaObjectTypeMember] = tag_member
+        self._tag_member = tag_member
         self.variants = variants
 
     @property
-    def tag_member(self) -> 'QAPISchemaObjectTypeMember':
+    def tag_member(self) -> QAPISchemaObjectTypeMember:
         if self._tag_member is None:
             raise RuntimeError(
                 "QAPISchemaVariants has no tag_member property until "
@@ -639,11 +758,13 @@ def tag_member(self) -> 'QAPISchemaObjectTypeMember':
             )
         return self._tag_member
 
-    def set_defined_in(self, name):
+    def set_defined_in(self, name: str) -> None:
         for v in self.variants:
             v.set_defined_in(name)
 
-    def check(self, schema, seen):
+    def check(
+        self, schema: QAPISchema, seen: Dict[str, QAPISchemaMember]
+    ) -> None:
         if self._tag_name:      # union
             # We need to narrow the member type:
             tmp = seen.get(c_name(self._tag_name))
@@ -713,7 +834,11 @@ def check(self, schema, seen):
                         % (v.describe(self.info), v.type.describe()))
                 v.type.check(schema)
 
-    def check_clash(self, info, seen):
+    def check_clash(
+        self,
+        info: Optional[QAPISourceInfo],
+        seen: Dict[str, QAPISchemaMember],
+    ) -> None:
         for v in self.variants:
             # Reset seen map for each variant, since qapi names from one
             # branch do not affect another branch.
@@ -727,18 +852,27 @@ class QAPISchemaMember:
     """ Represents object members, enum members and features """
     role = 'member'
 
-    def __init__(self, name, info, ifcond=None):
+    def __init__(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        ifcond: Optional[QAPISchemaIfCond] = None,
+    ):
         assert isinstance(name, str)
         self.name = name
         self.info = info
         self.ifcond = ifcond or QAPISchemaIfCond()
-        self.defined_in = None
+        self.defined_in: Optional[str] = None
 
-    def set_defined_in(self, name):
+    def set_defined_in(self, name: str) -> None:
         assert not self.defined_in
         self.defined_in = name
 
-    def check_clash(self, info, seen):
+    def check_clash(
+        self,
+        info: Optional[QAPISourceInfo],
+        seen: Dict[str, QAPISchemaMember],
+    ) -> None:
         cname = c_name(self.name)
         if cname in seen:
             raise QAPISemError(
@@ -747,11 +881,11 @@ def check_clash(self, info, seen):
                 % (self.describe(info), seen[cname].describe(info)))
         seen[cname] = self
 
-    def connect_doc(self, doc):
+    def connect_doc(self, doc: Optional[QAPIDoc]) -> None:
         if doc:
             doc.connect_member(self)
 
-    def describe(self, info):
+    def describe(self, info: Optional[QAPISourceInfo]) -> str:
         role = self.role
         meta = 'type'
         defined_in = self.defined_in
@@ -783,14 +917,20 @@ def describe(self, info):
 class QAPISchemaEnumMember(QAPISchemaMember):
     role = 'value'
 
-    def __init__(self, name, info, ifcond=None, features=None):
+    def __init__(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        ifcond: Optional[QAPISchemaIfCond] = None,
+        features: Optional[List[QAPISchemaFeature]] = None,
+    ):
         super().__init__(name, info, ifcond)
         for f in features or []:
             assert isinstance(f, QAPISchemaFeature)
             f.set_defined_in(name)
         self.features = features or []
 
-    def connect_doc(self, doc):
+    def connect_doc(self, doc: Optional[QAPIDoc]) -> None:
         super().connect_doc(doc)
         if doc:
             for f in self.features:
@@ -800,12 +940,20 @@ def connect_doc(self, doc):
 class QAPISchemaFeature(QAPISchemaMember):
     role = 'feature'
 
-    def is_special(self):
+    def is_special(self) -> bool:
         return self.name in ('deprecated', 'unstable')
 
 
 class QAPISchemaObjectTypeMember(QAPISchemaMember):
-    def __init__(self, name, info, typ, optional, ifcond=None, features=None):
+    def __init__(
+        self,
+        name: str,
+        info: QAPISourceInfo,
+        typ: str,
+        optional: bool,
+        ifcond: Optional[QAPISchemaIfCond] = None,
+        features: Optional[List[QAPISchemaFeature]] = None,
+    ):
         super().__init__(name, info, ifcond)
         assert isinstance(typ, str)
         assert isinstance(optional, bool)
@@ -817,19 +965,19 @@ def __init__(self, name, info, typ, optional, ifcond=None, features=None):
         self.optional = optional
         self.features = features or []
 
-    def need_has(self):
+    def need_has(self) -> bool:
         assert self.type
         return self.optional and self.type.need_has_if_optional()
 
-    def check(self, schema):
+    def check(self, schema: QAPISchema) -> None:
         assert self.defined_in
         self.type = schema.resolve_type(self._type_name, self.info,
                                         self.describe)
-        seen = {}
+        seen: Dict[str, QAPISchemaMember] = {}
         for f in self.features:
             f.check_clash(self.info, seen)
 
-    def connect_doc(self, doc):
+    def connect_doc(self, doc: Optional[QAPIDoc]) -> None:
         super().connect_doc(doc)
         if doc:
             for f in self.features:
@@ -839,24 +987,42 @@ def connect_doc(self, doc):
 class QAPISchemaVariant(QAPISchemaObjectTypeMember):
     role = 'branch'
 
-    def __init__(self, name, info, typ, ifcond=None):
+    def __init__(
+        self,
+        name: str,
+        info: QAPISourceInfo,
+        typ: str,
+        ifcond: QAPISchemaIfCond,
+    ):
         super().__init__(name, info, typ, False, ifcond)
 
 
 class QAPISchemaCommand(QAPISchemaDefinition):
     meta = 'command'
 
-    def __init__(self, name, info, doc, ifcond, features,
-                 arg_type, ret_type,
-                 gen, success_response, boxed, allow_oob, allow_preconfig,
-                 coroutine):
+    def __init__(
+        self,
+        name: str,
+        info: QAPISourceInfo,
+        doc: Optional[QAPIDoc],
+        ifcond: QAPISchemaIfCond,
+        features: List[QAPISchemaFeature],
+        arg_type: Optional[str],
+        ret_type: Optional[str],
+        gen: bool,
+        success_response: bool,
+        boxed: bool,
+        allow_oob: bool,
+        allow_preconfig: bool,
+        coroutine: bool,
+    ):
         super().__init__(name, info, doc, ifcond, features)
         assert not arg_type or isinstance(arg_type, str)
         assert not ret_type or isinstance(ret_type, str)
         self._arg_type_name = arg_type
-        self.arg_type = None
+        self.arg_type: Optional[QAPISchemaObjectType] = None
         self._ret_type_name = ret_type
-        self.ret_type = None
+        self.ret_type: Optional[QAPISchemaType] = None
         self.gen = gen
         self.success_response = success_response
         self.boxed = boxed
@@ -864,7 +1030,7 @@ def __init__(self, name, info, doc, ifcond, features,
         self.allow_preconfig = allow_preconfig
         self.coroutine = coroutine
 
-    def check(self, schema):
+    def check(self, schema: QAPISchema) -> None:
         assert self.info is not None
         super().check(schema)
         if self._arg_type_name:
@@ -900,14 +1066,14 @@ def check(self, schema):
                         "command's 'returns' cannot take %s"
                         % self.ret_type.describe())
 
-    def connect_doc(self, doc=None):
+    def connect_doc(self, doc: Optional[QAPIDoc] = None) -> None:
         super().connect_doc(doc)
         doc = doc or self.doc
         if doc:
             if self.arg_type and self.arg_type.is_implicit():
                 self.arg_type.connect_doc(doc)
 
-    def visit(self, visitor):
+    def visit(self, visitor: QAPISchemaVisitor) -> None:
         super().visit(visitor)
         visitor.visit_command(
             self.name, self.info, self.ifcond, self.features,
@@ -919,14 +1085,23 @@ def visit(self, visitor):
 class QAPISchemaEvent(QAPISchemaDefinition):
     meta = 'event'
 
-    def __init__(self, name, info, doc, ifcond, features, arg_type, boxed):
+    def __init__(
+        self,
+        name: str,
+        info: QAPISourceInfo,
+        doc: Optional[QAPIDoc],
+        ifcond: QAPISchemaIfCond,
+        features: List[QAPISchemaFeature],
+        arg_type: Optional[str],
+        boxed: bool,
+    ):
         super().__init__(name, info, doc, ifcond, features)
         assert not arg_type or isinstance(arg_type, str)
         self._arg_type_name = arg_type
-        self.arg_type = None
+        self.arg_type: Optional[QAPISchemaObjectType] = None
         self.boxed = boxed
 
-    def check(self, schema):
+    def check(self, schema: QAPISchema) -> None:
         super().check(schema)
         if self._arg_type_name:
             typ = schema.resolve_type(
@@ -948,14 +1123,14 @@ def check(self, schema):
                     self.info,
                     "conditional event arguments require 'boxed': true")
 
-    def connect_doc(self, doc=None):
+    def connect_doc(self, doc: Optional[QAPIDoc] = None) -> None:
         super().connect_doc(doc)
         doc = doc or self.doc
         if doc:
             if self.arg_type and self.arg_type.is_implicit():
                 self.arg_type.connect_doc(doc)
 
-    def visit(self, visitor):
+    def visit(self, visitor: QAPISchemaVisitor) -> None:
         super().visit(visitor)
         visitor.visit_event(
             self.name, self.info, self.ifcond, self.features,
@@ -963,7 +1138,7 @@ def visit(self, visitor):
 
 
 class QAPISchema:
-    def __init__(self, fname):
+    def __init__(self, fname: str):
         self.fname = fname
 
         try:
@@ -975,9 +1150,9 @@ def __init__(self, fname):
 
         exprs = check_exprs(parser.exprs)
         self.docs = parser.docs
-        self._entity_list = []
-        self._entity_dict = {}
-        self._module_dict = OrderedDict()
+        self._entity_list: List[QAPISchemaEntity] = []
+        self._entity_dict: Dict[str, QAPISchemaDefinition] = {}
+        self._module_dict: Dict[str, QAPISchemaModule] = OrderedDict()
         self._schema_dir = os.path.dirname(fname)
         self._make_module(QAPISchemaModule.BUILTIN_MODULE_NAME)
         self._make_module(fname)
@@ -987,10 +1162,10 @@ def __init__(self, fname):
         self._def_exprs(exprs)
         self.check()
 
-    def _def_entity(self, ent):
+    def _def_entity(self, ent: QAPISchemaEntity) -> None:
         self._entity_list.append(ent)
 
-    def _def_definition(self, defn):
+    def _def_definition(self, defn: QAPISchemaDefinition) -> None:
         # Only the predefined types are allowed to not have info
         assert defn.info or self._predefining
         self._def_entity(defn)
@@ -1007,18 +1182,27 @@ def _def_definition(self, defn):
                 defn.info, "%s is already defined" % other_defn.describe())
         self._entity_dict[defn.name] = defn
 
-    def lookup_entity(self, name, typ=None):
+    def lookup_entity(
+        self,
+        name: str,
+        typ: Optional[type] = None,
+    ) -> Optional[QAPISchemaDefinition]:
         ent = self._entity_dict.get(name)
         if typ and not isinstance(ent, typ):
             return None
         return ent
 
-    def lookup_type(self, name):
+    def lookup_type(self, name: str) -> Optional[QAPISchemaType]:
         typ = self.lookup_entity(name, QAPISchemaType)
         assert typ is None or isinstance(typ, QAPISchemaType)
         return typ
 
-    def resolve_type(self, name, info, what):
+    def resolve_type(
+        self,
+        name: str,
+        info: Optional[QAPISourceInfo],
+        what: Union[None, str, Callable[[QAPISourceInfo], str]],
+    ) -> QAPISchemaType:
         typ = self.lookup_type(name)
         if not typ:
             assert info and what  # built-in types must not fail lookup
@@ -1033,23 +1217,25 @@ def _module_name(self, fname: str) -> str:
             return fname
         return os.path.relpath(fname, self._schema_dir)
 
-    def _make_module(self, fname):
+    def _make_module(self, fname: str) -> QAPISchemaModule:
         name = self._module_name(fname)
         if name not in self._module_dict:
             self._module_dict[name] = QAPISchemaModule(name)
         return self._module_dict[name]
 
-    def module_by_fname(self, fname):
+    def module_by_fname(self, fname: str) -> QAPISchemaModule:
         name = self._module_name(fname)
         return self._module_dict[name]
 
-    def _def_include(self, expr: QAPIExpression):
+    def _def_include(self, expr: QAPIExpression) -> None:
         include = expr['include']
         assert expr.doc is None
         self._def_entity(
             QAPISchemaInclude(self._make_module(include), expr.info))
 
-    def _def_builtin_type(self, name, json_type, c_type):
+    def _def_builtin_type(
+        self, name: str, json_type: str, c_type: str
+    ) -> None:
         self._def_definition(QAPISchemaBuiltinType(name, json_type, c_type))
         # Instantiating only the arrays that are actually used would
         # be nice, but we can't as long as their generated code
@@ -1057,7 +1243,7 @@ def _def_builtin_type(self, name, json_type, c_type):
         # schema.
         self._make_array_type(name, None)
 
-    def _def_predefineds(self):
+    def _def_predefineds(self) -> None:
         for t in [('str',    'string',  'char' + POINTER_SUFFIX),
                   ('number', 'number',  'double'),
                   ('int',    'int',     'int64_t'),
@@ -1086,31 +1272,52 @@ def _def_predefineds(self):
         self._def_definition(QAPISchemaEnumType(
             'QType', None, None, None, None, qtype_values, 'QTYPE'))
 
-    def _make_features(self, features, info):
+    def _make_features(
+        self,
+        features: Optional[List[Dict[str, Any]]],
+        info: Optional[QAPISourceInfo],
+    ) -> List[QAPISchemaFeature]:
         if features is None:
             return []
         return [QAPISchemaFeature(f['name'], info,
                                   QAPISchemaIfCond(f.get('if')))
                 for f in features]
 
-    def _make_enum_member(self, name, ifcond, features, info):
+    def _make_enum_member(
+        self,
+        name: str,
+        ifcond: Optional[Union[str, Dict[str, Any]]],
+        features: Optional[List[Dict[str, Any]]],
+        info: Optional[QAPISourceInfo],
+    ) -> QAPISchemaEnumMember:
         return QAPISchemaEnumMember(name, info,
                                     QAPISchemaIfCond(ifcond),
                                     self._make_features(features, info))
 
-    def _make_enum_members(self, values, info):
+    def _make_enum_members(
+        self, values: List[Dict[str, Any]], info: Optional[QAPISourceInfo]
+    ) -> List[QAPISchemaEnumMember]:
         return [self._make_enum_member(v['name'], v.get('if'),
                                        v.get('features'), info)
                 for v in values]
 
-    def _make_array_type(self, element_type, info):
+    def _make_array_type(
+        self, element_type: str, info: Optional[QAPISourceInfo]
+    ) -> str:
         name = element_type + 'List'    # reserved by check_defn_name_str()
         if not self.lookup_type(name):
             self._def_definition(QAPISchemaArrayType(
                 name, info, element_type))
         return name
 
-    def _make_implicit_object_type(self, name, info, ifcond, role, members):
+    def _make_implicit_object_type(
+        self,
+        name: str,
+        info: QAPISourceInfo,
+        ifcond: QAPISchemaIfCond,
+        role: str,
+        members: List[QAPISchemaObjectTypeMember],
+    ) -> Optional[str]:
         if not members:
             return None
         # See also QAPISchemaObjectTypeMember.describe()
@@ -1126,7 +1333,7 @@ def _make_implicit_object_type(self, name, info, ifcond, role, members):
                 name, info, None, ifcond, None, None, members, None))
         return name
 
-    def _def_enum_type(self, expr: QAPIExpression):
+    def _def_enum_type(self, expr: QAPIExpression) -> None:
         name = expr['enum']
         data = expr['data']
         prefix = expr.get('prefix')
@@ -1137,7 +1344,14 @@ def _def_enum_type(self, expr: QAPIExpression):
             name, info, expr.doc, ifcond, features,
             self._make_enum_members(data, info), prefix))
 
-    def _make_member(self, name, typ, ifcond, features, info):
+    def _make_member(
+        self,
+        name: str,
+        typ: Union[List[str], str],
+        ifcond: QAPISchemaIfCond,
+        features: Optional[List[Dict[str, Any]]],
+        info: QAPISourceInfo,
+    ) -> QAPISchemaObjectTypeMember:
         optional = False
         if name.startswith('*'):
             name = name[1:]
@@ -1148,13 +1362,17 @@ def _make_member(self, name, typ, ifcond, features, info):
         return QAPISchemaObjectTypeMember(name, info, typ, optional, ifcond,
                                           self._make_features(features, info))
 
-    def _make_members(self, data, info):
+    def _make_members(
+        self,
+        data: Dict[str, Any],
+        info: QAPISourceInfo,
+    ) -> List[QAPISchemaObjectTypeMember]:
         return [self._make_member(key, value['type'],
                                   QAPISchemaIfCond(value.get('if')),
                                   value.get('features'), info)
                 for (key, value) in data.items()]
 
-    def _def_struct_type(self, expr: QAPIExpression):
+    def _def_struct_type(self, expr: QAPIExpression) -> None:
         name = expr['struct']
         base = expr.get('base')
         data = expr['data']
@@ -1166,13 +1384,19 @@ def _def_struct_type(self, expr: QAPIExpression):
             self._make_members(data, info),
             None))
 
-    def _make_variant(self, case, typ, ifcond, info):
+    def _make_variant(
+        self,
+        case: str,
+        typ: str,
+        ifcond: QAPISchemaIfCond,
+        info: QAPISourceInfo,
+    ) -> QAPISchemaVariant:
         if isinstance(typ, list):
             assert len(typ) == 1
             typ = self._make_array_type(typ[0], info)
         return QAPISchemaVariant(case, info, typ, ifcond)
 
-    def _def_union_type(self, expr: QAPIExpression):
+    def _def_union_type(self, expr: QAPIExpression) -> None:
         name = expr['union']
         base = expr['base']
         tag_name = expr['discriminator']
@@ -1197,7 +1421,7 @@ def _def_union_type(self, expr: QAPIExpression):
                                  QAPISchemaVariants(
                                      tag_name, info, None, variants)))
 
-    def _def_alternate_type(self, expr: QAPIExpression):
+    def _def_alternate_type(self, expr: QAPIExpression) -> None:
         name = expr['alternate']
         data = expr['data']
         assert isinstance(data, dict)
@@ -1215,7 +1439,7 @@ def _def_alternate_type(self, expr: QAPIExpression):
                 name, info, expr.doc, ifcond, features,
                 QAPISchemaVariants(None, info, tag_member, variants)))
 
-    def _def_command(self, expr: QAPIExpression):
+    def _def_command(self, expr: QAPIExpression) -> None:
         name = expr['command']
         data = expr.get('data')
         rets = expr.get('returns')
@@ -1240,7 +1464,7 @@ def _def_command(self, expr: QAPIExpression):
                               rets, gen, success_response, boxed, allow_oob,
                               allow_preconfig, coroutine))
 
-    def _def_event(self, expr: QAPIExpression):
+    def _def_event(self, expr: QAPIExpression) -> None:
         name = expr['event']
         data = expr.get('data')
         boxed = expr.get('boxed', False)
@@ -1254,7 +1478,7 @@ def _def_event(self, expr: QAPIExpression):
         self._def_definition(QAPISchemaEvent(name, info, expr.doc, ifcond,
                                              features, data, boxed))
 
-    def _def_exprs(self, exprs):
+    def _def_exprs(self, exprs: List[QAPIExpression]) -> None:
         for expr in exprs:
             if 'enum' in expr:
                 self._def_enum_type(expr)
@@ -1273,7 +1497,7 @@ def _def_exprs(self, exprs):
             else:
                 assert False
 
-    def check(self):
+    def check(self) -> None:
         for ent in self._entity_list:
             ent.check(self)
             ent.connect_doc()
@@ -1282,7 +1506,7 @@ def check(self):
         for doc in self.docs:
             doc.check()
 
-    def visit(self, visitor):
+    def visit(self, visitor: QAPISchemaVisitor) -> None:
         visitor.visit_begin(self)
         for mod in self._module_dict.values():
             mod.visit(visitor)
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 22/25] qapi/schema: turn on mypy strictness
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (20 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 21/25] qapi/schema: add type hints Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:22 ` [PATCH v5 23/25] qapi/schema: remove unnecessary asserts Markus Armbruster
                   ` (3 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

This patch can be rolled in with the previous one once the series is
ready for merge, but for work-in-progress' sake, it's separate here.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/mypy.ini | 5 -----
 1 file changed, 5 deletions(-)

diff --git a/scripts/qapi/mypy.ini b/scripts/qapi/mypy.ini
index 56e0dfb132..8109470a03 100644
--- a/scripts/qapi/mypy.ini
+++ b/scripts/qapi/mypy.ini
@@ -2,8 +2,3 @@
 strict = True
 disallow_untyped_calls = False
 python_version = 3.8
-
-[mypy-qapi.schema]
-disallow_untyped_defs = False
-disallow_incomplete_defs = False
-check_untyped_defs = False
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 23/25] qapi/schema: remove unnecessary asserts
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (21 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 22/25] qapi/schema: turn on mypy strictness Markus Armbruster
@ 2024-03-15 15:22 ` Markus Armbruster
  2024-03-15 15:23 ` [PATCH v5 24/25] qapi: Tighten check whether implicit object type already exists Markus Armbruster
                   ` (2 subsequent siblings)
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:22 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

From: John Snow <jsnow@redhat.com>

With strict typing enabled, these runtime statements aren't necessary
anymore; we can prove them statically.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 25 -------------------------
 1 file changed, 25 deletions(-)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index ef4d6a7def..e52930a48a 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -123,10 +123,8 @@ def __init__(
         ifcond: Optional[QAPISchemaIfCond] = None,
         features: Optional[List[QAPISchemaFeature]] = None,
     ):
-        assert isinstance(name, str)
         super().__init__(info)
         for f in features or []:
-            assert isinstance(f, QAPISchemaFeature)
             f.set_defined_in(name)
         self.name = name
         self.doc = doc
@@ -163,7 +161,6 @@ def is_implicit(self) -> bool:
         return not self.info
 
     def describe(self) -> str:
-        assert self.meta
         return "%s '%s'" % (self.meta, self.name)
 
 
@@ -377,7 +374,6 @@ def check(self, schema: QAPISchema) -> None:
                     f"feature '{feat.name}' is not supported for types")
 
     def describe(self) -> str:
-        assert self.meta
         return "%s type '%s'" % (self.meta, self.name)
 
 
@@ -386,7 +382,6 @@ class QAPISchemaBuiltinType(QAPISchemaType):
 
     def __init__(self, name: str, json_type: str, c_type: str):
         super().__init__(name, None, None)
-        assert not c_type or isinstance(c_type, str)
         assert json_type in ('string', 'number', 'int', 'boolean', 'null',
                              'value')
         self._json_type_name = json_type
@@ -429,9 +424,7 @@ def __init__(
     ):
         super().__init__(name, info, doc, ifcond, features)
         for m in members:
-            assert isinstance(m, QAPISchemaEnumMember)
             m.set_defined_in(name)
-        assert prefix is None or isinstance(prefix, str)
         self.members = members
         self.prefix = prefix
 
@@ -474,7 +467,6 @@ def __init__(
         self, name: str, info: Optional[QAPISourceInfo], element_type: str
     ):
         super().__init__(name, info, None)
-        assert isinstance(element_type, str)
         self._element_type_name = element_type
         self.element_type: QAPISchemaType
 
@@ -519,7 +511,6 @@ def visit(self, visitor: QAPISchemaVisitor) -> None:
                                  self.element_type)
 
     def describe(self) -> str:
-        assert self.meta
         return "%s type ['%s']" % (self.meta, self._element_type_name)
 
 
@@ -539,12 +530,9 @@ def __init__(
         # union has base, variants, and no local_members
         super().__init__(name, info, doc, ifcond, features)
         self.meta = 'union' if variants else 'struct'
-        assert base is None or isinstance(base, str)
         for m in local_members:
-            assert isinstance(m, QAPISchemaObjectTypeMember)
             m.set_defined_in(name)
         if variants is not None:
-            assert isinstance(variants, QAPISchemaVariants)
             variants.set_defined_in(name)
         self._base_name = base
         self.base = None
@@ -666,7 +654,6 @@ def __init__(
         variants: QAPISchemaVariants,
     ):
         super().__init__(name, info, doc, ifcond, features)
-        assert isinstance(variants, QAPISchemaVariants)
         assert variants.tag_member
         variants.set_defined_in(name)
         variants.tag_member.set_defined_in(self.name)
@@ -742,8 +729,6 @@ def __init__(
         assert bool(tag_member) != bool(tag_name)
         assert (isinstance(tag_name, str) or
                 isinstance(tag_member, QAPISchemaObjectTypeMember))
-        for v in variants:
-            assert isinstance(v, QAPISchemaVariant)
         self._tag_name = tag_name
         self.info = info
         self._tag_member = tag_member
@@ -858,7 +843,6 @@ def __init__(
         info: Optional[QAPISourceInfo],
         ifcond: Optional[QAPISchemaIfCond] = None,
     ):
-        assert isinstance(name, str)
         self.name = name
         self.info = info
         self.ifcond = ifcond or QAPISchemaIfCond()
@@ -926,7 +910,6 @@ def __init__(
     ):
         super().__init__(name, info, ifcond)
         for f in features or []:
-            assert isinstance(f, QAPISchemaFeature)
             f.set_defined_in(name)
         self.features = features or []
 
@@ -955,10 +938,7 @@ def __init__(
         features: Optional[List[QAPISchemaFeature]] = None,
     ):
         super().__init__(name, info, ifcond)
-        assert isinstance(typ, str)
-        assert isinstance(optional, bool)
         for f in features or []:
-            assert isinstance(f, QAPISchemaFeature)
             f.set_defined_in(name)
         self._type_name = typ
         self.type: QAPISchemaType  # set during check()
@@ -966,7 +946,6 @@ def __init__(
         self.features = features or []
 
     def need_has(self) -> bool:
-        assert self.type
         return self.optional and self.type.need_has_if_optional()
 
     def check(self, schema: QAPISchema) -> None:
@@ -1017,8 +996,6 @@ def __init__(
         coroutine: bool,
     ):
         super().__init__(name, info, doc, ifcond, features)
-        assert not arg_type or isinstance(arg_type, str)
-        assert not ret_type or isinstance(ret_type, str)
         self._arg_type_name = arg_type
         self.arg_type: Optional[QAPISchemaObjectType] = None
         self._ret_type_name = ret_type
@@ -1058,7 +1035,6 @@ def check(self, schema: QAPISchema) -> None:
             if self.name not in self.info.pragma.command_returns_exceptions:
                 typ = self.ret_type
                 if isinstance(typ, QAPISchemaArrayType):
-                    assert typ
                     typ = typ.element_type
                 if not isinstance(typ, QAPISchemaObjectType):
                     raise QAPISemError(
@@ -1096,7 +1072,6 @@ def __init__(
         boxed: bool,
     ):
         super().__init__(name, info, doc, ifcond, features)
-        assert not arg_type or isinstance(arg_type, str)
         self._arg_type_name = arg_type
         self.arg_type: Optional[QAPISchemaObjectType] = None
         self.boxed = boxed
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 24/25] qapi: Tighten check whether implicit object type already exists
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (22 preceding siblings ...)
  2024-03-15 15:22 ` [PATCH v5 23/25] qapi/schema: remove unnecessary asserts Markus Armbruster
@ 2024-03-15 15:23 ` Markus Armbruster
  2024-03-15 16:39   ` Philippe Mathieu-Daudé
  2024-03-19 15:30   ` John Snow
  2024-03-15 15:23 ` [PATCH v5 25/25] qapi: Dumb down QAPISchema.lookup_entity() Markus Armbruster
  2024-03-19 18:23 ` [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
  25 siblings, 2 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:23 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

Entities with names starting with q_obj_ are implicit object types.
Therefore, QAPISchema._make_implicit_object_type()'s .lookup_entity()
can only return a QAPISchemaObjectType.  Assert that.

Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index e52930a48a..a6180f93c6 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -1297,8 +1297,9 @@ def _make_implicit_object_type(
             return None
         # See also QAPISchemaObjectTypeMember.describe()
         name = 'q_obj_%s-%s' % (name, role)
-        typ = self.lookup_entity(name, QAPISchemaObjectType)
+        typ = self.lookup_entity(name)
         if typ:
+            assert(isinstance(typ, QAPISchemaObjectType))
             # The implicit object type has multiple users.  This can
             # only be a duplicate definition, which will be flagged
             # later.
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* [PATCH v5 25/25] qapi: Dumb down QAPISchema.lookup_entity()
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (23 preceding siblings ...)
  2024-03-15 15:23 ` [PATCH v5 24/25] qapi: Tighten check whether implicit object type already exists Markus Armbruster
@ 2024-03-15 15:23 ` Markus Armbruster
  2024-03-18  9:11   ` Markus Armbruster
  2024-03-19 15:32   ` John Snow
  2024-03-19 18:23 ` [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
  25 siblings, 2 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-15 15:23 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

QAPISchema.lookup_entity() takes an optional type argument, a subtype
of QAPISchemaDefinition, and returns that type or None.  Callers can
use this to save themselves an isinstance() test.

The only remaining user of this convenience feature is .lookup_type().
But we don't actually save anything anymore there: we still the
isinstance() to help mypy over the hump.

Drop the .lookup_entity() argument, and adjust .lookup_type().

Signed-off-by: Markus Armbruster <armbru@redhat.com>
---
 scripts/qapi/schema.py | 18 ++++++------------
 1 file changed, 6 insertions(+), 12 deletions(-)

diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index a6180f93c6..5924947fc3 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -1157,20 +1157,14 @@ def _def_definition(self, defn: QAPISchemaDefinition) -> None:
                 defn.info, "%s is already defined" % other_defn.describe())
         self._entity_dict[defn.name] = defn
 
-    def lookup_entity(
-        self,
-        name: str,
-        typ: Optional[type] = None,
-    ) -> Optional[QAPISchemaDefinition]:
-        ent = self._entity_dict.get(name)
-        if typ and not isinstance(ent, typ):
-            return None
-        return ent
+    def lookup_entity(self,name: str) -> Optional[QAPISchemaEntity]:
+        return self._entity_dict.get(name)
 
     def lookup_type(self, name: str) -> Optional[QAPISchemaType]:
-        typ = self.lookup_entity(name, QAPISchemaType)
-        assert typ is None or isinstance(typ, QAPISchemaType)
-        return typ
+        typ = self.lookup_entity(name)
+        if isinstance(typ, QAPISchemaType):
+            return typ
+        return None
 
     def resolve_type(
         self,
-- 
2.44.0



^ permalink raw reply related	[flat|nested] 34+ messages in thread

* Re: [PATCH v5 24/25] qapi: Tighten check whether implicit object type already exists
  2024-03-15 15:23 ` [PATCH v5 24/25] qapi: Tighten check whether implicit object type already exists Markus Armbruster
@ 2024-03-15 16:39   ` Philippe Mathieu-Daudé
  2024-03-19 15:30   ` John Snow
  1 sibling, 0 replies; 34+ messages in thread
From: Philippe Mathieu-Daudé @ 2024-03-15 16:39 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

On 15/3/24 16:23, Markus Armbruster wrote:
> Entities with names starting with q_obj_ are implicit object types.
> Therefore, QAPISchema._make_implicit_object_type()'s .lookup_entity()
> can only return a QAPISchemaObjectType.  Assert that.
> 
> Signed-off-by: Markus Armbruster <armbru@redhat.com>
> ---
>   scripts/qapi/schema.py | 3 ++-
>   1 file changed, 2 insertions(+), 1 deletion(-)

Reviewed-by: Philippe Mathieu-Daudé <philmd@linaro.org>




^ permalink raw reply	[flat|nested] 34+ messages in thread

* Re: [PATCH v5 25/25] qapi: Dumb down QAPISchema.lookup_entity()
  2024-03-15 15:23 ` [PATCH v5 25/25] qapi: Dumb down QAPISchema.lookup_entity() Markus Armbruster
@ 2024-03-18  9:11   ` Markus Armbruster
  2024-03-19 15:32   ` John Snow
  1 sibling, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-18  9:11 UTC (permalink / raw)
  To: qemu-devel; +Cc: jsnow, peter.maydell, michael.roth

Markus Armbruster <armbru@redhat.com> writes:

> QAPISchema.lookup_entity() takes an optional type argument, a subtype
> of QAPISchemaDefinition, and returns that type or None.  Callers can
> use this to save themselves an isinstance() test.
>
> The only remaining user of this convenience feature is .lookup_type().
> But we don't actually save anything anymore there: we still the

we still need the

> isinstance() to help mypy over the hump.
>
> Drop the .lookup_entity() argument, and adjust .lookup_type().
>
> Signed-off-by: Markus Armbruster <armbru@redhat.com>



^ permalink raw reply	[flat|nested] 34+ messages in thread

* Re: [PATCH v5 12/25] qapi: Assert built-in types exist
  2024-03-15 15:22 ` [PATCH v5 12/25] qapi: Assert built-in types exist Markus Armbruster
@ 2024-03-19 15:24   ` John Snow
  0 siblings, 0 replies; 34+ messages in thread
From: John Snow @ 2024-03-19 15:24 UTC (permalink / raw)
  To: Markus Armbruster; +Cc: qemu-devel, Peter Maydell, Michael Roth

[-- Attachment #1: Type: text/plain, Size: 1786 bytes --]

On Fri, Mar 15, 2024, 11:24 AM Markus Armbruster <armbru@redhat.com> wrote:

> QAPISchema.lookup_type('FOO') returns a QAPISchemaType when type 'FOO'
> exists, else None.  It won't return None for built-in types like
> 'int'.
>
> Since mypy can't see that, it'll complain that we assign the
> Optional[QAPISchemaType] returned by .lookup_type() to QAPISchemaType
> variables.
>
> Add assertions to help it over the hump.
>
> Signed-off-by: Markus Armbruster <armbru@redhat.com>
> ---
>  scripts/qapi/introspect.py | 8 ++++++--
>  1 file changed, 6 insertions(+), 2 deletions(-)
>
> diff --git a/scripts/qapi/introspect.py b/scripts/qapi/introspect.py
> index 67c7d89aae..4679b1bc2c 100644
> --- a/scripts/qapi/introspect.py
> +++ b/scripts/qapi/introspect.py
> @@ -227,10 +227,14 @@ def _use_type(self, typ: QAPISchemaType) -> str:
>
>          # Map the various integer types to plain int
>          if typ.json_type() == 'int':
> -            typ = self._schema.lookup_type('int')
> +            type_int = self._schema.lookup_type('int')
> +            assert type_int
> +            typ = type_int
>          elif (isinstance(typ, QAPISchemaArrayType) and
>                typ.element_type.json_type() == 'int'):
> -            typ = self._schema.lookup_type('intList')
> +            type_intList = self._schema.lookup_type('intList')
> +            assert type_intList
> +            typ = type_intList
>          # Add type to work queue if new
>          if typ not in self._used_types:
>              self._used_types.append(typ)
> --
> 2.44.0
>

Yeah, if you like this more, go ahead. I know it works because I did it
this way at one point!

Matter of taste and preference etc.

Reviewed-by: John Snow <jsnow@redhat.com>

>

[-- Attachment #2: Type: text/html, Size: 2733 bytes --]

^ permalink raw reply	[flat|nested] 34+ messages in thread

* Re: [PATCH v5 24/25] qapi: Tighten check whether implicit object type already exists
  2024-03-15 15:23 ` [PATCH v5 24/25] qapi: Tighten check whether implicit object type already exists Markus Armbruster
  2024-03-15 16:39   ` Philippe Mathieu-Daudé
@ 2024-03-19 15:30   ` John Snow
  2024-03-19 16:02     ` Markus Armbruster
  1 sibling, 1 reply; 34+ messages in thread
From: John Snow @ 2024-03-19 15:30 UTC (permalink / raw)
  To: Markus Armbruster; +Cc: qemu-devel, Peter Maydell, Michael Roth

[-- Attachment #1: Type: text/plain, Size: 1344 bytes --]

On Fri, Mar 15, 2024, 11:23 AM Markus Armbruster <armbru@redhat.com> wrote:

> Entities with names starting with q_obj_ are implicit object types.
> Therefore, QAPISchema._make_implicit_object_type()'s .lookup_entity()
> can only return a QAPISchemaObjectType.  Assert that.
>
> Signed-off-by: Markus Armbruster <armbru@redhat.com>
> ---
>  scripts/qapi/schema.py | 3 ++-
>  1 file changed, 2 insertions(+), 1 deletion(-)
>
> diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
> index e52930a48a..a6180f93c6 100644
> --- a/scripts/qapi/schema.py
> +++ b/scripts/qapi/schema.py
> @@ -1297,8 +1297,9 @@ def _make_implicit_object_type(
>              return None
>          # See also QAPISchemaObjectTypeMember.describe()
>          name = 'q_obj_%s-%s' % (name, role)
> -        typ = self.lookup_entity(name, QAPISchemaObjectType)
> +        typ = self.lookup_entity(name)
>          if typ:
> +            assert(isinstance(typ, QAPISchemaObjectType))
>              # The implicit object type has multiple users.  This can
>              # only be a duplicate definition, which will be flagged
>              # later.
> --
> 2.44.0
>

Seems obviously fine, though I don't suppose this narrowing will be
"remembered" by the type system. Do we care?

Reviewed-by: John Snow <jsnow@redhat.com>

>

[-- Attachment #2: Type: text/html, Size: 2127 bytes --]

^ permalink raw reply	[flat|nested] 34+ messages in thread

* Re: [PATCH v5 25/25] qapi: Dumb down QAPISchema.lookup_entity()
  2024-03-15 15:23 ` [PATCH v5 25/25] qapi: Dumb down QAPISchema.lookup_entity() Markus Armbruster
  2024-03-18  9:11   ` Markus Armbruster
@ 2024-03-19 15:32   ` John Snow
  1 sibling, 0 replies; 34+ messages in thread
From: John Snow @ 2024-03-19 15:32 UTC (permalink / raw)
  To: Markus Armbruster; +Cc: qemu-devel, Peter Maydell, Michael Roth

[-- Attachment #1: Type: text/plain, Size: 2062 bytes --]

On Fri, Mar 15, 2024, 11:23 AM Markus Armbruster <armbru@redhat.com> wrote:

> QAPISchema.lookup_entity() takes an optional type argument, a subtype
> of QAPISchemaDefinition, and returns that type or None.  Callers can
> use this to save themselves an isinstance() test.
>
> The only remaining user of this convenience feature is .lookup_type().
> But we don't actually save anything anymore there: we still the
> isinstance() to help mypy over the hump.
>
> Drop the .lookup_entity() argument, and adjust .lookup_type().
>
> Signed-off-by: Markus Armbruster <armbru@redhat.com>
> ---
>  scripts/qapi/schema.py | 18 ++++++------------
>  1 file changed, 6 insertions(+), 12 deletions(-)
>
> diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
> index a6180f93c6..5924947fc3 100644
> --- a/scripts/qapi/schema.py
> +++ b/scripts/qapi/schema.py
> @@ -1157,20 +1157,14 @@ def _def_definition(self, defn:
> QAPISchemaDefinition) -> None:
>                  defn.info, "%s is already defined" %
> other_defn.describe())
>          self._entity_dict[defn.name] = defn
>
> -    def lookup_entity(
> -        self,
> -        name: str,
> -        typ: Optional[type] = None,
> -    ) -> Optional[QAPISchemaDefinition]:
> -        ent = self._entity_dict.get(name)
> -        if typ and not isinstance(ent, typ):
> -            return None
> -        return ent
> +    def lookup_entity(self,name: str) -> Optional[QAPISchemaEntity]:
> +        return self._entity_dict.get(name)
>
>      def lookup_type(self, name: str) -> Optional[QAPISchemaType]:
> -        typ = self.lookup_entity(name, QAPISchemaType)
> -        assert typ is None or isinstance(typ, QAPISchemaType)
> -        return typ
> +        typ = self.lookup_entity(name)
> +        if isinstance(typ, QAPISchemaType):
> +            return typ
> +        return None
>
>      def resolve_type(
>          self,
> --
> 2.44.0
>

Sure, dealer's choice.

with your commit message fixup:

Reviewed-by: John Snow <jsnow@redhat.com>

>

[-- Attachment #2: Type: text/html, Size: 3126 bytes --]

^ permalink raw reply	[flat|nested] 34+ messages in thread

* Re: [PATCH v5 24/25] qapi: Tighten check whether implicit object type already exists
  2024-03-19 15:30   ` John Snow
@ 2024-03-19 16:02     ` Markus Armbruster
  2024-03-19 16:06       ` John Snow
  0 siblings, 1 reply; 34+ messages in thread
From: Markus Armbruster @ 2024-03-19 16:02 UTC (permalink / raw)
  To: John Snow; +Cc: qemu-devel, Peter Maydell, Michael Roth

John Snow <jsnow@redhat.com> writes:

> On Fri, Mar 15, 2024, 11:23 AM Markus Armbruster <armbru@redhat.com> wrote:
>
>> Entities with names starting with q_obj_ are implicit object types.
>> Therefore, QAPISchema._make_implicit_object_type()'s .lookup_entity()
>> can only return a QAPISchemaObjectType.  Assert that.
>>
>> Signed-off-by: Markus Armbruster <armbru@redhat.com>
>> ---
>>  scripts/qapi/schema.py | 3 ++-
>>  1 file changed, 2 insertions(+), 1 deletion(-)
>>
>> diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
>> index e52930a48a..a6180f93c6 100644
>> --- a/scripts/qapi/schema.py
>> +++ b/scripts/qapi/schema.py
>> @@ -1297,8 +1297,9 @@ def _make_implicit_object_type(
>>              return None
>>          # See also QAPISchemaObjectTypeMember.describe()
>>          name = 'q_obj_%s-%s' % (name, role)
>> -        typ = self.lookup_entity(name, QAPISchemaObjectType)
>> +        typ = self.lookup_entity(name)
>>          if typ:
>> +            assert(isinstance(typ, QAPISchemaObjectType))
>>              # The implicit object type has multiple users.  This can
>>              # only be a duplicate definition, which will be flagged
>>              # later.
>> --
>> 2.44.0
>>
>
> Seems obviously fine, though I don't suppose this narrowing will be
> "remembered" by the type system. Do we care?

mypy passes without it.  It's for catching programming errors and
helping the reader along.  The former are unlikely, and the latter is
debatable, but when in doubt, assert.

> Reviewed-by: John Snow <jsnow@redhat.com>

Thanks!



^ permalink raw reply	[flat|nested] 34+ messages in thread

* Re: [PATCH v5 24/25] qapi: Tighten check whether implicit object type already exists
  2024-03-19 16:02     ` Markus Armbruster
@ 2024-03-19 16:06       ` John Snow
  0 siblings, 0 replies; 34+ messages in thread
From: John Snow @ 2024-03-19 16:06 UTC (permalink / raw)
  To: Markus Armbruster; +Cc: qemu-devel, Peter Maydell, Michael Roth

[-- Attachment #1: Type: text/plain, Size: 2142 bytes --]

On Tue, Mar 19, 2024, 12:02 PM Markus Armbruster <armbru@redhat.com> wrote:

> John Snow <jsnow@redhat.com> writes:
>
> > On Fri, Mar 15, 2024, 11:23 AM Markus Armbruster <armbru@redhat.com>
> wrote:
> >
> >> Entities with names starting with q_obj_ are implicit object types.
> >> Therefore, QAPISchema._make_implicit_object_type()'s .lookup_entity()
> >> can only return a QAPISchemaObjectType.  Assert that.
> >>
> >> Signed-off-by: Markus Armbruster <armbru@redhat.com>
> >> ---
> >>  scripts/qapi/schema.py | 3 ++-
> >>  1 file changed, 2 insertions(+), 1 deletion(-)
> >>
> >> diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
> >> index e52930a48a..a6180f93c6 100644
> >> --- a/scripts/qapi/schema.py
> >> +++ b/scripts/qapi/schema.py
> >> @@ -1297,8 +1297,9 @@ def _make_implicit_object_type(
> >>              return None
> >>          # See also QAPISchemaObjectTypeMember.describe()
> >>          name = 'q_obj_%s-%s' % (name, role)
> >> -        typ = self.lookup_entity(name, QAPISchemaObjectType)
> >> +        typ = self.lookup_entity(name)
> >>          if typ:
> >> +            assert(isinstance(typ, QAPISchemaObjectType))
> >>              # The implicit object type has multiple users.  This can
> >>              # only be a duplicate definition, which will be flagged
> >>              # later.
> >> --
> >> 2.44.0
> >>
> >
> > Seems obviously fine, though I don't suppose this narrowing will be
> > "remembered" by the type system. Do we care?
>
> mypy passes without it.  It's for catching programming errors and
> helping the reader along.  The former are unlikely, and the latter is
> debatable, but when in doubt, assert.
>

mmhmm. I was just wondering if we could tighten the typing of typ itself,
or if it conflicted with legitimate broader uses. it happens a lot in qapi
that we're regulated by broader base types in parent classes etc.

If we CAN tighten the variable/field (without runtime code changes), we
should do so. If we can't, this patch is 100% totally fine as is.


> > Reviewed-by: John Snow <jsnow@redhat.com>
>
> Thanks!
>
>

[-- Attachment #2: Type: text/html, Size: 3444 bytes --]

^ permalink raw reply	[flat|nested] 34+ messages in thread

* Re: [PATCH v5 00/25] qapi: statically type schema.py
  2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
                   ` (24 preceding siblings ...)
  2024-03-15 15:23 ` [PATCH v5 25/25] qapi: Dumb down QAPISchema.lookup_entity() Markus Armbruster
@ 2024-03-19 18:23 ` Markus Armbruster
  25 siblings, 0 replies; 34+ messages in thread
From: Markus Armbruster @ 2024-03-19 18:23 UTC (permalink / raw)
  To: Markus Armbruster; +Cc: qemu-devel, jsnow, peter.maydell, michael.roth

Queued for 9.1 with PATCH 25's commit message typo fixed.  Thanks!



^ permalink raw reply	[flat|nested] 34+ messages in thread

end of thread, other threads:[~2024-03-19 18:24 UTC | newest]

Thread overview: 34+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2024-03-15 15:22 [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 01/25] qapi/parser: fix typo - self.returns.info => self.errors.info Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 02/25] qapi/parser: shush up pylint Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 03/25] qapi: sort pylint suppressions Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 04/25] qapi/schema: add " Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 05/25] qapi: create QAPISchemaDefinition Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 06/25] qapi/schema: declare type for QAPISchemaObjectTypeMember.type Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 07/25] qapi/schema: declare type for QAPISchemaArrayType.element_type Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 08/25] qapi/schema: make c_type() and json_type() abstract methods Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 09/25] qapi/schema: adjust type narrowing for mypy's benefit Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 10/25] qapi/schema: add type narrowing to lookup_type() Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 11/25] qapi/schema: assert resolve_type has 'info' and 'what' args on error Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 12/25] qapi: Assert built-in types exist Markus Armbruster
2024-03-19 15:24   ` John Snow
2024-03-15 15:22 ` [PATCH v5 13/25] qapi/schema: fix QAPISchemaArrayType.check's call to resolve_type Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 14/25] qapi/schema: assert info is present when necessary Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 15/25] qapi/schema: add _check_complete flag Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 16/25] qapi/schema: Don't initialize "members" with `None` Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 17/25] qapi/schema: fix typing for QAPISchemaVariants.tag_member Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 18/25] qapi/schema: assert inner type of QAPISchemaVariants in check_clash() Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 19/25] qapi/parser: demote QAPIExpression to Dict[str, Any] Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 20/25] qapi/parser.py: assert member.info is present in connect_member Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 21/25] qapi/schema: add type hints Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 22/25] qapi/schema: turn on mypy strictness Markus Armbruster
2024-03-15 15:22 ` [PATCH v5 23/25] qapi/schema: remove unnecessary asserts Markus Armbruster
2024-03-15 15:23 ` [PATCH v5 24/25] qapi: Tighten check whether implicit object type already exists Markus Armbruster
2024-03-15 16:39   ` Philippe Mathieu-Daudé
2024-03-19 15:30   ` John Snow
2024-03-19 16:02     ` Markus Armbruster
2024-03-19 16:06       ` John Snow
2024-03-15 15:23 ` [PATCH v5 25/25] qapi: Dumb down QAPISchema.lookup_entity() Markus Armbruster
2024-03-18  9:11   ` Markus Armbruster
2024-03-19 15:32   ` John Snow
2024-03-19 18:23 ` [PATCH v5 00/25] qapi: statically type schema.py Markus Armbruster

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).