All of lore.kernel.org
 help / color / mirror / Atom feed
* [PATCH v5 00/17] qapi: static typing conversion, pt3
@ 2021-04-21 18:20 John Snow
  2021-04-21 18:20 ` [PATCH v5 01/17] qapi/expr: Comment cleanup John Snow
                   ` (17 more replies)
  0 siblings, 18 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

Hi, this series adds static types to the QAPI module.
This is part three, and it focuses on expr.py.

Environment:
- Python >= 3.6, <= 3.8 *
- mypy >= 0.770
- pylint >= 2.6.0
- flake8
- isort

Every commit should pass with (from ./scripts/):
 - flake8 qapi/
 - pylint --rcfile=qapi/pylintrc qapi/
 - mypy --config-file=qapi/mypy.ini qapi/
 - pushd qapi && isort -c . && popd

V5:

001/17:[----] [--] 'qapi/expr: Comment cleanup'
002/19: -DROPPED-  'flake8: Enforce shorter line length for comments and docstrings'
002/17:[----] [--] 'qapi/expr.py: Remove 'info' argument from nested check_if_str'
003/17:[----] [--] 'qapi/expr.py: Check for dict instead of OrderedDict'
004/17:[0006] [FC] 'qapi/expr.py: constrain incoming expression types'
005/17:[----] [--] 'qapi/expr.py: Add assertion for union type 'check_dict''
006/17:[----] [--] 'qapi/expr.py: move string check upwards in check_type'
007/17:[down]      'qapi/expr.py: Check type of union and alternate 'data' member'
008/19: -MERGED- ^ 'qapi: add tests for invalid 'data' field type'
009/19: -MERGED- ^ 'qapi/expr.py: Check type of 'data' member'
008/17:[0004] [FC] 'qapi/expr.py: Add casts in a few select cases'
009/17:[----] [--] 'qapi/expr.py: Modify check_keys to accept any Collection'
010/17:[----] [-C] 'qapi/expr.py: add type hint annotations'
011/17:[0013] [FC] 'qapi/expr.py: Consolidate check_if_str calls in check_if'
012/17:[----] [--] 'qapi/expr.py: Remove single-letter variable'
013/17:[----] [--] 'qapi/expr.py: enable pylint checks'
014/17:[down]      'qapi/expr: Only explicitly prohibit 'Kind' nor 'List' for type names'
015/17:[0227] [FC] 'qapi/expr.py: Add docstrings'
016/17:[----] [-C] 'qapi/expr.py: Use tuples instead of lists for static data'
017/17:[down]      'qapi/expr: Update authorship and copyright information'
018/19: -DROPPED-  'qapi/expr.py: move related checks inside check_xxx functions'
019/19: -DROPPED-  'qapi/expr.py: Use an expression checker dispatch table'

- Dropped what was 02, 18, 19.
- Merged what was 08, 09 into what is now 07.
- 04: Expanded comment for _JSONObject type alias.
- 07: "New", combines the fix and the test patches from the previous series into
      one patch.
- 08: Fixed spelling of "Checked" from "Asserted"
- 11: Reverted back to Python2 style string interpolation
- 14: New, fix the code to reflect the docs for Kind/List reservations.
- 15: Many, many docstring changes. (Too many to list!)
- 17: New, update copyright/authorship. (Could be folded into 15 if desired,
      as that's what I'm claiming credit for.)

John Snow (17):
  qapi/expr: Comment cleanup
  qapi/expr.py: Remove 'info' argument from nested check_if_str
  qapi/expr.py: Check for dict instead of OrderedDict
  qapi/expr.py: constrain incoming expression types
  qapi/expr.py: Add assertion for union type 'check_dict'
  qapi/expr.py: move string check upwards in check_type
  qapi/expr.py: Check type of union and alternate 'data' member
  qapi/expr.py: Add casts in a few select cases
  qapi/expr.py: Modify check_keys to accept any Collection
  qapi/expr.py: add type hint annotations
  qapi/expr.py: Consolidate check_if_str calls in check_if
  qapi/expr.py: Remove single-letter variable
  qapi/expr.py: enable pylint checks
  qapi/expr: Only explicitly prohibit 'Kind' nor 'List' for type names
  qapi/expr.py: Add docstrings
  qapi/expr.py: Use tuples instead of lists for static data
  qapi/expr: Update authorship and copyright information

 scripts/qapi/expr.py                          | 444 +++++++++++++++---
 scripts/qapi/mypy.ini                         |   5 -
 scripts/qapi/pylintrc                         |   1 -
 tests/qapi-schema/alternate-data-invalid.err  |   2 +
 tests/qapi-schema/alternate-data-invalid.json |   4 +
 tests/qapi-schema/alternate-data-invalid.out  |   0
 tests/qapi-schema/meson.build                 |   2 +
 tests/qapi-schema/union-invalid-data.err      |   2 +
 tests/qapi-schema/union-invalid-data.json     |   6 +
 tests/qapi-schema/union-invalid-data.out      |   0
 10 files changed, 385 insertions(+), 81 deletions(-)
 create mode 100644 tests/qapi-schema/alternate-data-invalid.err
 create mode 100644 tests/qapi-schema/alternate-data-invalid.json
 create mode 100644 tests/qapi-schema/alternate-data-invalid.out
 create mode 100644 tests/qapi-schema/union-invalid-data.err
 create mode 100644 tests/qapi-schema/union-invalid-data.json
 create mode 100644 tests/qapi-schema/union-invalid-data.out

-- 
2.30.2




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

* [PATCH v5 01/17] qapi/expr: Comment cleanup
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 02/17] qapi/expr.py: Remove 'info' argument from nested check_if_str John Snow
                   ` (16 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

The linter yaps after 0825f62c842. Fix this trivial issue to restore the
linter baseline.

(Yes, ideally -- and soon -- the linter will be part of CI so we don't
clutter up the log with fixups. For now, though, the baseline is useful
for testing intermediate commits as types are added to the QAPI
library.)

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

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index 540b3982b10..c207481f7e9 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -241,7 +241,7 @@ def check_enum(expr, info):
         source = "%s '%s'" % (source, member_name)
         # Enum members may start with a digit
         if member_name[0].isdigit():
-            member_name = 'd' + member_name # Hack: hide the digit
+            member_name = 'd' + member_name  # Hack: hide the digit
         check_name_lower(member_name, info, source,
                          permit_upper=permissive,
                          permit_underscore=permissive)
-- 
2.30.2



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

* [PATCH v5 02/17] qapi/expr.py: Remove 'info' argument from nested check_if_str
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
  2021-04-21 18:20 ` [PATCH v5 01/17] qapi/expr: Comment cleanup John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 03/17] qapi/expr.py: Check for dict instead of OrderedDict John Snow
                   ` (15 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

The function can just use the argument from the scope above. Otherwise,
we get shadowed argument errors because the parameter name clashes with
the name of a variable already in-scope.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
Reviewed-by: Cleber Rosa <crosa@redhat.com>
---
 scripts/qapi/expr.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index c207481f7e9..3fda5d50827 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -122,7 +122,7 @@ def check_flags(expr, info):
 
 def check_if(expr, info, source):
 
-    def check_if_str(ifcond, info):
+    def check_if_str(ifcond):
         if not isinstance(ifcond, str):
             raise QAPISemError(
                 info,
@@ -142,9 +142,9 @@ def check_if_str(ifcond, info):
             raise QAPISemError(
                 info, "'if' condition [] of %s is useless" % source)
         for elt in ifcond:
-            check_if_str(elt, info)
+            check_if_str(elt)
     else:
-        check_if_str(ifcond, info)
+        check_if_str(ifcond)
         expr['if'] = [ifcond]
 
 
-- 
2.30.2



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

* [PATCH v5 03/17] qapi/expr.py: Check for dict instead of OrderedDict
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
  2021-04-21 18:20 ` [PATCH v5 01/17] qapi/expr: Comment cleanup John Snow
  2021-04-21 18:20 ` [PATCH v5 02/17] qapi/expr.py: Remove 'info' argument from nested check_if_str John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 04/17] qapi/expr.py: constrain incoming expression types John Snow
                   ` (14 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

OrderedDict is a subtype of dict, so we can check for a more general
form. These functions do not themselves depend on it being any
particular type.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
Reviewed-by: Cleber Rosa <crosa@redhat.com>
---
 scripts/qapi/expr.py | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index 3fda5d50827..b4bbcd54c08 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -14,7 +14,6 @@
 # This work is licensed under the terms of the GNU GPL, version 2.
 # See the COPYING file in the top-level directory.
 
-from collections import OrderedDict
 import re
 
 from .common import c_name
@@ -149,7 +148,7 @@ def check_if_str(ifcond):
 
 
 def normalize_members(members):
-    if isinstance(members, OrderedDict):
+    if isinstance(members, dict):
         for key, arg in members.items():
             if isinstance(arg, dict):
                 continue
@@ -180,7 +179,7 @@ def check_type(value, info, source,
     if not allow_dict:
         raise QAPISemError(info, "%s should be a type name" % source)
 
-    if not isinstance(value, OrderedDict):
+    if not isinstance(value, dict):
         raise QAPISemError(info,
                            "%s should be an object or type name" % source)
 
-- 
2.30.2



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

* [PATCH v5 04/17] qapi/expr.py: constrain incoming expression types
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
                   ` (2 preceding siblings ...)
  2021-04-21 18:20 ` [PATCH v5 03/17] qapi/expr.py: Check for dict instead of OrderedDict John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 05/17] qapi/expr.py: Add assertion for union type 'check_dict' John Snow
                   ` (13 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

mypy does not know the types of values stored in Dicts that masquerade
as objects. Help the type checker out by constraining the type.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/expr.py | 28 +++++++++++++++++++++++++---
 1 file changed, 25 insertions(+), 3 deletions(-)

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index b4bbcd54c08..06a00810015 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -15,9 +15,20 @@
 # See the COPYING file in the top-level directory.
 
 import re
+from typing import Dict, Optional
 
 from .common import c_name
 from .error import QAPISemError
+from .parser import QAPIDoc
+from .source import QAPISourceInfo
+
+
+# Deserialized JSON objects as returned by the parser.
+# The values of this mapping are not necessary to exhaustively type
+# here (and also not practical as long as mypy lacks recursive
+# types), because the purpose of this module is to interrogate that
+# type.
+_JSONObject = Dict[str, object]
 
 
 # Names consist of letters, digits, -, and _, starting with a letter.
@@ -315,9 +326,20 @@ def check_event(expr, info):
 
 def check_exprs(exprs):
     for expr_elem in exprs:
-        expr = expr_elem['expr']
-        info = expr_elem['info']
-        doc = expr_elem.get('doc')
+        # Expression
+        assert isinstance(expr_elem['expr'], dict)
+        for key in expr_elem['expr'].keys():
+            assert isinstance(key, str)
+        expr: _JSONObject = expr_elem['expr']
+
+        # QAPISourceInfo
+        assert isinstance(expr_elem['info'], QAPISourceInfo)
+        info: QAPISourceInfo = expr_elem['info']
+
+        # Optional[QAPIDoc]
+        tmp = expr_elem.get('doc')
+        assert tmp is None or isinstance(tmp, QAPIDoc)
+        doc: Optional[QAPIDoc] = tmp
 
         if 'include' in expr:
             continue
-- 
2.30.2



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

* [PATCH v5 05/17] qapi/expr.py: Add assertion for union type 'check_dict'
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
                   ` (3 preceding siblings ...)
  2021-04-21 18:20 ` [PATCH v5 04/17] qapi/expr.py: constrain incoming expression types John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 06/17] qapi/expr.py: move string check upwards in check_type John Snow
                   ` (12 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

mypy isn't fond of allowing you to check for bool membership in a
collection of str elements. Guard this lookup for precisely when we were
given a name.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
Reviewed-by: Cleber Rosa <crosa@redhat.com>
---
 scripts/qapi/expr.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index 06a00810015..3ab78a555dc 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -194,7 +194,9 @@ def check_type(value, info, source,
         raise QAPISemError(info,
                            "%s should be an object or type name" % source)
 
-    permissive = allow_dict in info.pragma.member_name_exceptions
+    permissive = False
+    if isinstance(allow_dict, str):
+        permissive = allow_dict in info.pragma.member_name_exceptions
 
     # value is a dictionary, check that each member is okay
     for (key, arg) in value.items():
-- 
2.30.2



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

* [PATCH v5 06/17] qapi/expr.py: move string check upwards in check_type
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
                   ` (4 preceding siblings ...)
  2021-04-21 18:20 ` [PATCH v5 05/17] qapi/expr.py: Add assertion for union type 'check_dict' John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 07/17] qapi/expr.py: Check type of union and alternate 'data' member John Snow
                   ` (11 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

For readability purposes only, shimmy the early return upwards to the
top of the function, so cases proceed in order from least to most
complex.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
Reviewed-by: Cleber Rosa <crosa@redhat.com>
---
 scripts/qapi/expr.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index 3ab78a555dc..c0d18dcc018 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -171,6 +171,10 @@ def check_type(value, info, source,
     if value is None:
         return
 
+    # Type name
+    if isinstance(value, str):
+        return
+
     # Array type
     if isinstance(value, list):
         if not allow_array:
@@ -181,10 +185,6 @@ def check_type(value, info, source,
                                source)
         return
 
-    # Type name
-    if isinstance(value, str):
-        return
-
     # Anonymous type
 
     if not allow_dict:
-- 
2.30.2



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

* [PATCH v5 07/17] qapi/expr.py: Check type of union and alternate 'data' member
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
                   ` (5 preceding siblings ...)
  2021-04-21 18:20 ` [PATCH v5 06/17] qapi/expr.py: move string check upwards in check_type John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 08/17] qapi/expr.py: Add casts in a few select cases John Snow
                   ` (10 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

Prior to this commit, specifying a non-object value here causes the QAPI
parser to crash in expr.py with a stack trace with (likely) an
AttributeError when we attempt to call that value's items() method.

This member needs to be an object (Dict), and not anything else. Add a
check for this with a nicer error message, and formalize that check with
new test cases that exercise that error.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/expr.py                          | 7 +++++++
 tests/qapi-schema/alternate-data-invalid.err  | 2 ++
 tests/qapi-schema/alternate-data-invalid.json | 4 ++++
 tests/qapi-schema/alternate-data-invalid.out  | 0
 tests/qapi-schema/meson.build                 | 2 ++
 tests/qapi-schema/union-invalid-data.err      | 2 ++
 tests/qapi-schema/union-invalid-data.json     | 6 ++++++
 tests/qapi-schema/union-invalid-data.out      | 0
 8 files changed, 23 insertions(+)
 create mode 100644 tests/qapi-schema/alternate-data-invalid.err
 create mode 100644 tests/qapi-schema/alternate-data-invalid.json
 create mode 100644 tests/qapi-schema/alternate-data-invalid.out
 create mode 100644 tests/qapi-schema/union-invalid-data.err
 create mode 100644 tests/qapi-schema/union-invalid-data.json
 create mode 100644 tests/qapi-schema/union-invalid-data.out

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index c0d18dcc018..03624bdf3f3 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -283,6 +283,9 @@ def check_union(expr, info):
             raise QAPISemError(info, "'discriminator' requires 'base'")
         check_name_is_str(discriminator, info, "'discriminator'")
 
+    if not isinstance(members, dict):
+        raise QAPISemError(info, "'data' must be an object")
+
     for (key, value) in members.items():
         source = "'data' member '%s'" % key
         if discriminator is None:
@@ -298,6 +301,10 @@ def check_alternate(expr, info):
 
     if not members:
         raise QAPISemError(info, "'data' must not be empty")
+
+    if not isinstance(members, dict):
+        raise QAPISemError(info, "'data' must be an object")
+
     for (key, value) in members.items():
         source = "'data' member '%s'" % key
         check_name_lower(key, info, source)
diff --git a/tests/qapi-schema/alternate-data-invalid.err b/tests/qapi-schema/alternate-data-invalid.err
new file mode 100644
index 00000000000..55f1033aef5
--- /dev/null
+++ b/tests/qapi-schema/alternate-data-invalid.err
@@ -0,0 +1,2 @@
+alternate-data-invalid.json: In alternate 'Alt':
+alternate-data-invalid.json:2: 'data' must be an object
diff --git a/tests/qapi-schema/alternate-data-invalid.json b/tests/qapi-schema/alternate-data-invalid.json
new file mode 100644
index 00000000000..7d5d9055811
--- /dev/null
+++ b/tests/qapi-schema/alternate-data-invalid.json
@@ -0,0 +1,4 @@
+# Alternate type requires an object for 'data'
+{ 'alternate': 'Alt',
+  'data': ['rubbish', 'nonsense']
+}
diff --git a/tests/qapi-schema/alternate-data-invalid.out b/tests/qapi-schema/alternate-data-invalid.out
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/tests/qapi-schema/meson.build b/tests/qapi-schema/meson.build
index 8ba69171329..d7163e6601c 100644
--- a/tests/qapi-schema/meson.build
+++ b/tests/qapi-schema/meson.build
@@ -14,6 +14,7 @@ schemas = [
   'alternate-conflict-string.json',
   'alternate-conflict-bool-string.json',
   'alternate-conflict-num-string.json',
+  'alternate-data-invalid.json',
   'alternate-empty.json',
   'alternate-invalid-dict.json',
   'alternate-nested.json',
@@ -192,6 +193,7 @@ schemas = [
   'union-clash-branches.json',
   'union-empty.json',
   'union-invalid-base.json',
+  'union-invalid-data.json',
   'union-optional-branch.json',
   'union-unknown.json',
   'unknown-escape.json',
diff --git a/tests/qapi-schema/union-invalid-data.err b/tests/qapi-schema/union-invalid-data.err
new file mode 100644
index 00000000000..e26cf769a3e
--- /dev/null
+++ b/tests/qapi-schema/union-invalid-data.err
@@ -0,0 +1,2 @@
+union-invalid-data.json: In union 'TestUnion':
+union-invalid-data.json:2: 'data' must be an object
diff --git a/tests/qapi-schema/union-invalid-data.json b/tests/qapi-schema/union-invalid-data.json
new file mode 100644
index 00000000000..395ba24d395
--- /dev/null
+++ b/tests/qapi-schema/union-invalid-data.json
@@ -0,0 +1,6 @@
+# the union data type must be an object.
+{ 'union': 'TestUnion',
+  'base': 'int',
+  'discriminator': 'int',
+  'data': ['rubbish', 'nonsense']
+}
diff --git a/tests/qapi-schema/union-invalid-data.out b/tests/qapi-schema/union-invalid-data.out
new file mode 100644
index 00000000000..e69de29bb2d
-- 
2.30.2



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

* [PATCH v5 08/17] qapi/expr.py: Add casts in a few select cases
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
                   ` (6 preceding siblings ...)
  2021-04-21 18:20 ` [PATCH v5 07/17] qapi/expr.py: Check type of union and alternate 'data' member John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 09/17] qapi/expr.py: Modify check_keys to accept any Collection John Snow
                   ` (9 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

Casts are instructions to the type checker only, they aren't "safe" and
should probably be avoided in general. In this case, when we perform
type checking on a nested structure, the type of each field does not
"stick".

(See PEP 647 for an example of "type narrowing" that does "stick".
 It is available in Python 3.10, so we can't use it yet.)

We don't need to assert that something is a str if we've already checked
or asserted that it is -- use a cast instead for these cases.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/expr.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index 03624bdf3f3..f3a4a8536e8 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -15,7 +15,7 @@
 # See the COPYING file in the top-level directory.
 
 import re
-from typing import Dict, Optional
+from typing import Dict, Optional, cast
 
 from .common import c_name
 from .error import QAPISemError
@@ -261,7 +261,7 @@ def check_enum(expr, info):
 
 
 def check_struct(expr, info):
-    name = expr['struct']
+    name = cast(str, expr['struct'])  # Checked in check_exprs
     members = expr['data']
 
     check_type(members, info, "'data'", allow_dict=name)
@@ -269,7 +269,7 @@ def check_struct(expr, info):
 
 
 def check_union(expr, info):
-    name = expr['union']
+    name = cast(str, expr['union'])  # Checked in check_exprs
     base = expr.get('base')
     discriminator = expr.get('discriminator')
     members = expr['data']
@@ -368,8 +368,8 @@ def check_exprs(exprs):
         else:
             raise QAPISemError(info, "expression is missing metatype")
 
-        name = expr[meta]
-        check_name_is_str(name, info, "'%s'" % meta)
+        check_name_is_str(expr[meta], info, "'%s'" % meta)
+        name = cast(str, expr[meta])
         info.set_defn(meta, name)
         check_defn_name_str(name, info, meta)
 
-- 
2.30.2



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

* [PATCH v5 09/17] qapi/expr.py: Modify check_keys to accept any Collection
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
                   ` (7 preceding siblings ...)
  2021-04-21 18:20 ` [PATCH v5 08/17] qapi/expr.py: Add casts in a few select cases John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 10/17] qapi/expr.py: add type hint annotations John Snow
                   ` (8 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

This is a minor adjustment that lets parameters @required and
@optional take tuple arguments, in particular ().  Later patches will
make use of that.

(Iterable would also have worked, but Iterable also includes things like
generator expressions which are consumed upon iteration, which would
require a rewrite to make sure that each input was only traversed
once. Collection implies the "can re-iterate" property.)

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

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index f3a4a8536e8..396c8126d6a 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -102,7 +102,7 @@ def pprint(elems):
             "%s misses key%s %s"
             % (source, 's' if len(missing) > 1 else '',
                pprint(missing)))
-    allowed = set(required + optional)
+    allowed = set(required) | set(optional)
     unknown = set(value) - allowed
     if unknown:
         raise QAPISemError(
-- 
2.30.2



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

* [PATCH v5 10/17] qapi/expr.py: add type hint annotations
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
                   ` (8 preceding siblings ...)
  2021-04-21 18:20 ` [PATCH v5 09/17] qapi/expr.py: Modify check_keys to accept any Collection John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 11/17] qapi/expr.py: Consolidate check_if_str calls in check_if John Snow
                   ` (7 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

Annotations do not change runtime behavior.
This commit *only* adds annotations.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/expr.py  | 68 +++++++++++++++++++++++++++----------------
 scripts/qapi/mypy.ini |  5 ----
 2 files changed, 43 insertions(+), 30 deletions(-)

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index 396c8126d6a..4ebed4c4884 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -15,7 +15,15 @@
 # See the COPYING file in the top-level directory.
 
 import re
-from typing import Dict, Optional, cast
+from typing import (
+    Collection,
+    Dict,
+    Iterable,
+    List,
+    Optional,
+    Union,
+    cast,
+)
 
 from .common import c_name
 from .error import QAPISemError
@@ -39,12 +47,14 @@
                         r'([a-z][a-z0-9_-]*)$', re.IGNORECASE)
 
 
-def check_name_is_str(name, info, source):
+def check_name_is_str(name: object,
+                      info: QAPISourceInfo,
+                      source: str) -> None:
     if not isinstance(name, str):
         raise QAPISemError(info, "%s requires a string name" % source)
 
 
-def check_name_str(name, info, source):
+def check_name_str(name: str, info: QAPISourceInfo, source: str) -> str:
     # Reserve the entire 'q_' namespace for c_name(), and for 'q_empty'
     # and 'q_obj_*' implicit type names.
     match = valid_name.match(name)
@@ -53,16 +63,16 @@ def check_name_str(name, info, source):
     return match.group(3)
 
 
-def check_name_upper(name, info, source):
+def check_name_upper(name: str, info: QAPISourceInfo, source: str) -> None:
     stem = check_name_str(name, info, source)
     if re.search(r'[a-z-]', stem):
         raise QAPISemError(
             info, "name of %s must not use lowercase or '-'" % source)
 
 
-def check_name_lower(name, info, source,
-                     permit_upper=False,
-                     permit_underscore=False):
+def check_name_lower(name: str, info: QAPISourceInfo, source: str,
+                     permit_upper: bool = False,
+                     permit_underscore: bool = False) -> None:
     stem = check_name_str(name, info, source)
     if ((not permit_upper and re.search(r'[A-Z]', stem))
             or (not permit_underscore and '_' in stem)):
@@ -70,13 +80,13 @@ def check_name_lower(name, info, source,
             info, "name of %s must not use uppercase or '_'" % source)
 
 
-def check_name_camel(name, info, source):
+def check_name_camel(name: str, info: QAPISourceInfo, source: str) -> None:
     stem = check_name_str(name, info, source)
     if not re.match(r'[A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*$', stem):
         raise QAPISemError(info, "name of %s must use CamelCase" % source)
 
 
-def check_defn_name_str(name, info, meta):
+def check_defn_name_str(name: str, info: QAPISourceInfo, meta: str) -> None:
     if meta == 'event':
         check_name_upper(name, info, meta)
     elif meta == 'command':
@@ -90,9 +100,13 @@ def check_defn_name_str(name, info, meta):
             info, "%s name should not end in '%s'" % (meta, name[-4:]))
 
 
-def check_keys(value, info, source, required, optional):
+def check_keys(value: _JSONObject,
+               info: QAPISourceInfo,
+               source: str,
+               required: Collection[str],
+               optional: Collection[str]) -> None:
 
-    def pprint(elems):
+    def pprint(elems: Iterable[str]) -> str:
         return ', '.join("'" + e + "'" for e in sorted(elems))
 
     missing = set(required) - set(value)
@@ -112,7 +126,7 @@ def pprint(elems):
                pprint(unknown), pprint(allowed)))
 
 
-def check_flags(expr, info):
+def check_flags(expr: _JSONObject, info: QAPISourceInfo) -> None:
     for key in ['gen', 'success-response']:
         if key in expr and expr[key] is not False:
             raise QAPISemError(
@@ -130,9 +144,9 @@ def check_flags(expr, info):
                                  "are incompatible")
 
 
-def check_if(expr, info, source):
+def check_if(expr: _JSONObject, info: QAPISourceInfo, source: str) -> None:
 
-    def check_if_str(ifcond):
+    def check_if_str(ifcond: object) -> None:
         if not isinstance(ifcond, str):
             raise QAPISemError(
                 info,
@@ -158,7 +172,7 @@ def check_if_str(ifcond):
         expr['if'] = [ifcond]
 
 
-def normalize_members(members):
+def normalize_members(members: object) -> None:
     if isinstance(members, dict):
         for key, arg in members.items():
             if isinstance(arg, dict):
@@ -166,8 +180,11 @@ def normalize_members(members):
             members[key] = {'type': arg}
 
 
-def check_type(value, info, source,
-               allow_array=False, allow_dict=False):
+def check_type(value: Optional[object],
+               info: QAPISourceInfo,
+               source: str,
+               allow_array: bool = False,
+               allow_dict: Union[bool, str] = False) -> None:
     if value is None:
         return
 
@@ -214,7 +231,8 @@ def check_type(value, info, source,
         check_type(arg['type'], info, key_source, allow_array=True)
 
 
-def check_features(features, info):
+def check_features(features: Optional[object],
+                   info: QAPISourceInfo) -> None:
     if features is None:
         return
     if not isinstance(features, list):
@@ -231,7 +249,7 @@ def check_features(features, info):
         check_if(f, info, source)
 
 
-def check_enum(expr, info):
+def check_enum(expr: _JSONObject, info: QAPISourceInfo) -> None:
     name = expr['enum']
     members = expr['data']
     prefix = expr.get('prefix')
@@ -260,7 +278,7 @@ def check_enum(expr, info):
         check_if(member, info, source)
 
 
-def check_struct(expr, info):
+def check_struct(expr: _JSONObject, info: QAPISourceInfo) -> None:
     name = cast(str, expr['struct'])  # Checked in check_exprs
     members = expr['data']
 
@@ -268,7 +286,7 @@ def check_struct(expr, info):
     check_type(expr.get('base'), info, "'base'")
 
 
-def check_union(expr, info):
+def check_union(expr: _JSONObject, info: QAPISourceInfo) -> None:
     name = cast(str, expr['union'])  # Checked in check_exprs
     base = expr.get('base')
     discriminator = expr.get('discriminator')
@@ -296,7 +314,7 @@ def check_union(expr, info):
         check_type(value['type'], info, source, allow_array=not base)
 
 
-def check_alternate(expr, info):
+def check_alternate(expr: _JSONObject, info: QAPISourceInfo) -> None:
     members = expr['data']
 
     if not members:
@@ -313,7 +331,7 @@ def check_alternate(expr, info):
         check_type(value['type'], info, source)
 
 
-def check_command(expr, info):
+def check_command(expr: _JSONObject, info: QAPISourceInfo) -> None:
     args = expr.get('data')
     rets = expr.get('returns')
     boxed = expr.get('boxed', False)
@@ -324,7 +342,7 @@ def check_command(expr, info):
     check_type(rets, info, "'returns'", allow_array=True)
 
 
-def check_event(expr, info):
+def check_event(expr: _JSONObject, info: QAPISourceInfo) -> None:
     args = expr.get('data')
     boxed = expr.get('boxed', False)
 
@@ -333,7 +351,7 @@ def check_event(expr, info):
     check_type(args, info, "'data'", allow_dict=not boxed)
 
 
-def check_exprs(exprs):
+def check_exprs(exprs: List[_JSONObject]) -> List[_JSONObject]:
     for expr_elem in exprs:
         # Expression
         assert isinstance(expr_elem['expr'], dict)
diff --git a/scripts/qapi/mypy.ini b/scripts/qapi/mypy.ini
index 0a000d58b37..7797c834328 100644
--- a/scripts/qapi/mypy.ini
+++ b/scripts/qapi/mypy.ini
@@ -8,11 +8,6 @@ disallow_untyped_defs = False
 disallow_incomplete_defs = False
 check_untyped_defs = False
 
-[mypy-qapi.expr]
-disallow_untyped_defs = False
-disallow_incomplete_defs = False
-check_untyped_defs = False
-
 [mypy-qapi.parser]
 disallow_untyped_defs = False
 disallow_incomplete_defs = False
-- 
2.30.2



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

* [PATCH v5 11/17] qapi/expr.py: Consolidate check_if_str calls in check_if
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
                   ` (9 preceding siblings ...)
  2021-04-21 18:20 ` [PATCH v5 10/17] qapi/expr.py: add type hint annotations John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 12/17] qapi/expr.py: Remove single-letter variable John Snow
                   ` (6 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

This is a small rewrite to address some minor style nits.

Don't compare against the empty list to check for the empty condition, and
move the normalization forward to unify the check on the now-normalized
structure.

With the check unified, the local nested function isn't needed anymore
and can be brought down into the normal flow of the function. With the
nesting level changed, shuffle the error strings around a bit to get
them to fit in 79 columns.

Note: although ifcond is typed as Sequence[str] elsewhere, we *know* that
the parser will produce real, bona-fide lists. It's okay to check
isinstance(ifcond, list) here.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/expr.py | 33 ++++++++++++++++-----------------
 1 file changed, 16 insertions(+), 17 deletions(-)

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index 4ebed4c4884..de7fc16fac2 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -146,30 +146,29 @@ def check_flags(expr: _JSONObject, info: QAPISourceInfo) -> None:
 
 def check_if(expr: _JSONObject, info: QAPISourceInfo, source: str) -> None:
 
-    def check_if_str(ifcond: object) -> None:
-        if not isinstance(ifcond, str):
-            raise QAPISemError(
-                info,
-                "'if' condition of %s must be a string or a list of strings"
-                % source)
-        if ifcond.strip() == '':
-            raise QAPISemError(
-                info,
-                "'if' condition '%s' of %s makes no sense"
-                % (ifcond, source))
-
     ifcond = expr.get('if')
     if ifcond is None:
         return
+
     if isinstance(ifcond, list):
-        if ifcond == []:
+        if not ifcond:
             raise QAPISemError(
                 info, "'if' condition [] of %s is useless" % source)
-        for elt in ifcond:
-            check_if_str(elt)
     else:
-        check_if_str(ifcond)
-        expr['if'] = [ifcond]
+        # Normalize to a list
+        ifcond = expr['if'] = [ifcond]
+
+    for elt in ifcond:
+        if not isinstance(elt, str):
+            raise QAPISemError(
+                info,
+                "'if' condition of %s must be a string or a list of strings"
+                % source)
+        if not elt.strip():
+            raise QAPISemError(
+                info,
+                "'if' condition '%s' of %s makes no sense"
+                % (elt, source))
 
 
 def normalize_members(members: object) -> None:
-- 
2.30.2



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

* [PATCH v5 12/17] qapi/expr.py: Remove single-letter variable
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
                   ` (10 preceding siblings ...)
  2021-04-21 18:20 ` [PATCH v5 11/17] qapi/expr.py: Consolidate check_if_str calls in check_if John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 13/17] qapi/expr.py: enable pylint checks John Snow
                   ` (5 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/expr.py | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index de7fc16fac2..5e4d5f80aa7 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -238,14 +238,14 @@ def check_features(features: Optional[object],
         raise QAPISemError(info, "'features' must be an array")
     features[:] = [f if isinstance(f, dict) else {'name': f}
                    for f in features]
-    for f in features:
+    for feat in features:
         source = "'features' member"
-        assert isinstance(f, dict)
-        check_keys(f, info, source, ['name'], ['if'])
-        check_name_is_str(f['name'], info, source)
-        source = "%s '%s'" % (source, f['name'])
-        check_name_lower(f['name'], info, source)
-        check_if(f, info, source)
+        assert isinstance(feat, dict)
+        check_keys(feat, info, source, ['name'], ['if'])
+        check_name_is_str(feat['name'], info, source)
+        source = "%s '%s'" % (source, feat['name'])
+        check_name_str(feat['name'], info, source)
+        check_if(feat, info, source)
 
 
 def check_enum(expr: _JSONObject, info: QAPISourceInfo) -> None:
-- 
2.30.2



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

* [PATCH v5 13/17] qapi/expr.py: enable pylint checks
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
                   ` (11 preceding siblings ...)
  2021-04-21 18:20 ` [PATCH v5 12/17] qapi/expr.py: Remove single-letter variable John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 14/17] qapi/expr: Only explicitly prohibit 'Kind' nor 'List' for type names John Snow
                   ` (4 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

Signed-off-by: John Snow <jsnow@redhat.com>
Tested-by: Eduardo Habkost <ehabkost@redhat.com>
Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
Reviewed-by: Cleber Rosa <crosa@redhat.com>
Tested-by: Cleber Rosa <crosa@redhat.com>
---
 scripts/qapi/pylintrc | 1 -
 1 file changed, 1 deletion(-)

diff --git a/scripts/qapi/pylintrc b/scripts/qapi/pylintrc
index b9e077a1642..fb0386d529a 100644
--- a/scripts/qapi/pylintrc
+++ b/scripts/qapi/pylintrc
@@ -3,7 +3,6 @@
 # Add files or directories matching the regex patterns to the ignore list.
 # The regex matches against base names, not paths.
 ignore-patterns=error.py,
-                expr.py,
                 parser.py,
                 schema.py,
 
-- 
2.30.2



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

* [PATCH v5 14/17] qapi/expr: Only explicitly prohibit 'Kind' nor 'List' for type names
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
                   ` (12 preceding siblings ...)
  2021-04-21 18:20 ` [PATCH v5 13/17] qapi/expr.py: enable pylint checks John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 15/17] qapi/expr.py: Add docstrings John Snow
                   ` (3 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

Per list review: qapi-code-gen.txt reserves suffixes Kind and
List only for type names, but the code rejects them for events and
commands, too.

It turns out we reject them earlier anyway: In check_name_upper() for
event names, and in check_name_lower() for command names.

Still, adjust the code for clarity over what precisely we are guarding
against.

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/expr.py | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index 5e4d5f80aa7..c33caf00d91 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -95,9 +95,9 @@ def check_defn_name_str(name: str, info: QAPISourceInfo, meta: str) -> None:
             permit_underscore=name in info.pragma.command_name_exceptions)
     else:
         check_name_camel(name, info, meta)
-    if name.endswith('Kind') or name.endswith('List'):
-        raise QAPISemError(
-            info, "%s name should not end in '%s'" % (meta, name[-4:]))
+        if name.endswith('Kind') or name.endswith('List'):
+            raise QAPISemError(
+                info, "%s name should not end in '%s'" % (meta, name[-4:]))
 
 
 def check_keys(value: _JSONObject,
-- 
2.30.2



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

* [PATCH v5 15/17] qapi/expr.py: Add docstrings
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
                   ` (13 preceding siblings ...)
  2021-04-21 18:20 ` [PATCH v5 14/17] qapi/expr: Only explicitly prohibit 'Kind' nor 'List' for type names John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 16/17] qapi/expr.py: Use tuples instead of lists for static data John Snow
                   ` (2 subsequent siblings)
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

Now with more :words:!

Signed-off-by: John Snow <jsnow@redhat.com>
---
 scripts/qapi/expr.py | 256 ++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 251 insertions(+), 5 deletions(-)

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index c33caf00d91..0b66d808421 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -1,7 +1,5 @@
 # -*- coding: utf-8 -*-
 #
-# Check (context-free) QAPI schema expression structure
-#
 # Copyright IBM, Corp. 2011
 # Copyright (c) 2013-2019 Red Hat Inc.
 #
@@ -14,6 +12,24 @@
 # This work is licensed under the terms of the GNU GPL, version 2.
 # See the COPYING file in the top-level directory.
 
+"""
+Normalize and validate (context-free) QAPI schema expression structures.
+
+`QAPISchemaParser` parses a QAPI schema into abstract syntax trees
+consisting of dict, list, str, bool, and int nodes.  This module ensures
+that these nested structures have the correct type(s) and key(s) where
+appropriate for the QAPI context-free grammar.
+
+The QAPI schema expression language allows for certain syntactic sugar;
+this module also handles the normalization process of these nested
+structures.
+
+See `check_exprs` for the main entry point.
+
+See `schema.QAPISchema` for processing into native Python data
+structures and contextual semantic validation.
+"""
+
 import re
 from typing import (
     Collection,
@@ -39,9 +55,7 @@
 _JSONObject = Dict[str, object]
 
 
-# Names consist of letters, digits, -, and _, starting with a letter.
-# An experimental name is prefixed with x-.  A name of a downstream
-# extension is prefixed with __RFQDN_.  The latter prefix goes first.
+# See check_name_str(), below.
 valid_name = re.compile(r'(__[a-z0-9.-]+_)?'
                         r'(x-)?'
                         r'([a-z][a-z0-9_-]*)$', re.IGNORECASE)
@@ -50,11 +64,33 @@
 def check_name_is_str(name: object,
                       info: QAPISourceInfo,
                       source: str) -> None:
+    """
+    Ensure that ``name`` is a ``str``.
+
+    :raise QAPISemError: When ``name`` fails validation.
+    """
     if not isinstance(name, str):
         raise QAPISemError(info, "%s requires a string name" % source)
 
 
 def check_name_str(name: str, info: QAPISourceInfo, source: str) -> str:
+    """
+    Ensure that ``name`` is a valid QAPI name.
+
+    A valid name consists of ASCII letters, digits, ``-``, and ``_``,
+    starting with a letter.  It may be prefixed by a downstream prefix
+    of the form __RFQDN_, or the experimental prefix ``x-``.  If both
+    prefixes are present, the __RFDQN_ prefix goes first.
+
+    A valid name cannot start with ``q_``, which is reserved.
+
+    :param name: Name to check.
+    :param info: QAPI schema source file information.
+    :param source: Error string describing what ``name`` belongs to.
+
+    :raise QAPISemError: When ``name`` fails validation.
+    :return: The stem of the valid name, with no prefixes.
+    """
     # Reserve the entire 'q_' namespace for c_name(), and for 'q_empty'
     # and 'q_obj_*' implicit type names.
     match = valid_name.match(name)
@@ -64,6 +100,19 @@ def check_name_str(name: str, info: QAPISourceInfo, source: str) -> str:
 
 
 def check_name_upper(name: str, info: QAPISourceInfo, source: str) -> None:
+    """
+    Ensure that ``name`` is a valid event name.
+
+    This means it must be a valid QAPI name as checked by
+    `check_name_str()`, but where the stem prohibits lowercase
+    characters and ``-``.
+
+    :param name: Name to check.
+    :param info: QAPI schema source file information.
+    :param source: Error string describing what ``name`` belongs to.
+
+    :raise QAPISemError: When ``name`` fails validation.
+    """
     stem = check_name_str(name, info, source)
     if re.search(r'[a-z-]', stem):
         raise QAPISemError(
@@ -73,6 +122,21 @@ def check_name_upper(name: str, info: QAPISourceInfo, source: str) -> None:
 def check_name_lower(name: str, info: QAPISourceInfo, source: str,
                      permit_upper: bool = False,
                      permit_underscore: bool = False) -> None:
+    """
+    Ensure that ``name`` is a valid command or member name.
+
+    This means it must be a valid QAPI name as checked by
+    `check_name_str()`, but where the stem prohibits uppercase
+    characters and ``_``.
+
+    :param name: Name to check.
+    :param info: QAPI schema source file information.
+    :param source: Error string describing what ``name`` belongs to.
+    :param permit_upper: Additionally permit uppercase.
+    :param permit_underscore: Additionally permit ``_``.
+
+    :raise QAPISemError: When ``name`` fails validation.
+    """
     stem = check_name_str(name, info, source)
     if ((not permit_upper and re.search(r'[A-Z]', stem))
             or (not permit_underscore and '_' in stem)):
@@ -81,12 +145,39 @@ def check_name_lower(name: str, info: QAPISourceInfo, source: str,
 
 
 def check_name_camel(name: str, info: QAPISourceInfo, source: str) -> None:
+    """
+    Ensure that ``name`` is a valid user-defined type name.
+
+    This means it must be a valid QAPI name as checked by
+    `check_name_str()`, but where the stem must be in CamelCase.
+
+    :param name: Name to check.
+    :param info: QAPI schema source file information.
+    :param source: Error string describing what ``name`` belongs to.
+
+    :raise QAPISemError: When ``name`` fails validation.
+    """
     stem = check_name_str(name, info, source)
     if not re.match(r'[A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*$', stem):
         raise QAPISemError(info, "name of %s must use CamelCase" % source)
 
 
 def check_defn_name_str(name: str, info: QAPISourceInfo, meta: str) -> None:
+    """
+    Ensure that ``name`` is a valid definition name.
+
+    Based on the value of ``meta``, this means that:
+      - 'event' names adhere to `check_name_upper()`.
+      - 'command' names adhere to `check_name_lower()`.
+      - Else, meta is a type, and must pass `check_name_camel()`.
+        These names must not end with ``Kind`` nor ``List``.
+
+    :param name: Name to check.
+    :param info: QAPI schema source file information.
+    :param meta: Meta-type name of the QAPI expression.
+
+    :raise QAPISemError: When ``name`` fails validation.
+    """
     if meta == 'event':
         check_name_upper(name, info, meta)
     elif meta == 'command':
@@ -105,6 +196,17 @@ def check_keys(value: _JSONObject,
                source: str,
                required: Collection[str],
                optional: Collection[str]) -> None:
+    """
+    Ensure that a dict has a specific set of keys.
+
+    :param value: The dict to check.
+    :param info: QAPI schema source file information.
+    :param source: Error string describing this ``value``.
+    :param required: Keys that *must* be present.
+    :param optional: Keys that *may* be present.
+
+    :raise QAPISemError: When unknown keys are present.
+    """
 
     def pprint(elems: Iterable[str]) -> str:
         return ', '.join("'" + e + "'" for e in sorted(elems))
@@ -127,6 +229,16 @@ def pprint(elems: Iterable[str]) -> str:
 
 
 def check_flags(expr: _JSONObject, info: QAPISourceInfo) -> None:
+    """
+    Ensure flag members (if present) have valid values.
+
+    :param expr: The expression to validate.
+    :param info: QAPI schema source file information.
+
+    :raise QAPISemError:
+        When certain flags have an invalid value, or when
+        incompatible flags are present.
+    """
     for key in ['gen', 'success-response']:
         if key in expr and expr[key] is not False:
             raise QAPISemError(
@@ -145,7 +257,25 @@ def check_flags(expr: _JSONObject, info: QAPISourceInfo) -> None:
 
 
 def check_if(expr: _JSONObject, info: QAPISourceInfo, source: str) -> None:
+    """
+    Normalize and validate the ``if`` member of an object.
 
+    The ``if`` member may be either a ``str`` or a ``List[str]``.
+    A ``str`` value will be normalized to ``List[str]``.
+
+    :forms:
+      :sugared: ``Union[str, List[str]]``
+      :canonical: ``List[str]``
+
+    :param expr: The expression containing the ``if`` member to validate.
+    :param info: QAPI schema source file information.
+    :param source: Error string describing ``expr``.
+
+    :raise QAPISemError:
+        When the "if" member fails validation, or when there are no
+        non-empty conditions.
+    :return: None, ``expr`` is normalized in-place as needed.
+    """
     ifcond = expr.get('if')
     if ifcond is None:
         return
@@ -172,6 +302,21 @@ def check_if(expr: _JSONObject, info: QAPISourceInfo, source: str) -> None:
 
 
 def normalize_members(members: object) -> None:
+    """
+    Normalize a "members" value.
+
+    If ``members`` is a dict, for every value in that dict, if that
+    value is not itself already a dict, normalize it to
+    ``{'type': value}``.
+
+    :forms:
+      :sugared: ``Dict[str, Union[str, TypeRef]]``
+      :canonical: ``Dict[str, TypeRef]``
+
+    :param members: The members value to normalize.
+
+    :return: None, ``members`` is normalized in-place as needed.
+    """
     if isinstance(members, dict):
         for key, arg in members.items():
             if isinstance(arg, dict):
@@ -184,6 +329,26 @@ def check_type(value: Optional[object],
                source: str,
                allow_array: bool = False,
                allow_dict: Union[bool, str] = False) -> None:
+    """
+    Normalize and validate the QAPI type of ``value``.
+
+    Python types of ``str`` or ``None`` are always allowed.
+
+    :param value: The value to check.
+    :param info: QAPI schema source file information.
+    :param source: Error string describing this ``value``.
+    :param allow_array:
+        Allow a ``List[str]`` of length 1, which indicates an array of
+        the type named by the list element.
+    :param allow_dict:
+        Allow a dict.  Its members can be struct type members or union
+        branches.  When the value of ``allow_dict`` is in pragma
+        ``member-name-exceptions``, the dict's keys may violate the
+        member naming rules.  The dict members are normalized in place.
+
+    :raise QAPISemError: When ``value`` fails validation.
+    :return: None, ``value`` is normalized in-place as needed.
+    """
     if value is None:
         return
 
@@ -232,6 +397,22 @@ def check_type(value: Optional[object],
 
 def check_features(features: Optional[object],
                    info: QAPISourceInfo) -> None:
+    """
+    Normalize and validate the ``features`` member.
+
+    ``features`` may be a ``list`` of either ``str`` or ``dict``.
+    Any ``str`` element will be normalized to ``{'name': element}``.
+
+    :forms:
+      :sugared: ``List[Union[str, Feature]]``
+      :canonical: ``List[Feature]``
+
+    :param features: The features member value to validate.
+    :param info: QAPI schema source file information.
+
+    :raise QAPISemError: When ``features`` fails validation.
+    :return: None, ``features`` is normalized in-place as needed.
+    """
     if features is None:
         return
     if not isinstance(features, list):
@@ -249,6 +430,15 @@ def check_features(features: Optional[object],
 
 
 def check_enum(expr: _JSONObject, info: QAPISourceInfo) -> None:
+    """
+    Normalize and validate this expression as an ``enum`` definition.
+
+    :param expr: The expression to validate.
+    :param info: QAPI schema source file information.
+
+    :raise QAPISemError: When ``expr`` is not a valid ``enum``.
+    :return: None, ``expr`` is normalized in-place as needed.
+    """
     name = expr['enum']
     members = expr['data']
     prefix = expr.get('prefix')
@@ -278,6 +468,15 @@ def check_enum(expr: _JSONObject, info: QAPISourceInfo) -> None:
 
 
 def check_struct(expr: _JSONObject, info: QAPISourceInfo) -> None:
+    """
+    Normalize and validate this expression as a ``struct`` definition.
+
+    :param expr: The expression to validate.
+    :param info: QAPI schema source file information.
+
+    :raise QAPISemError: When ``expr`` is not a valid ``struct``.
+    :return: None, ``expr`` is normalized in-place as needed.
+    """
     name = cast(str, expr['struct'])  # Checked in check_exprs
     members = expr['data']
 
@@ -286,6 +485,15 @@ def check_struct(expr: _JSONObject, info: QAPISourceInfo) -> None:
 
 
 def check_union(expr: _JSONObject, info: QAPISourceInfo) -> None:
+    """
+    Normalize and validate this expression as a ``union`` definition.
+
+    :param expr: The expression to validate.
+    :param info: QAPI schema source file information.
+
+    :raise QAPISemError: when ``expr`` is not a valid ``union``.
+    :return: None, ``expr`` is normalized in-place as needed.
+    """
     name = cast(str, expr['union'])  # Checked in check_exprs
     base = expr.get('base')
     discriminator = expr.get('discriminator')
@@ -314,6 +522,15 @@ def check_union(expr: _JSONObject, info: QAPISourceInfo) -> None:
 
 
 def check_alternate(expr: _JSONObject, info: QAPISourceInfo) -> None:
+    """
+    Normalize and validate this expression as an ``alternate`` definition.
+
+    :param expr: The expression to validate.
+    :param info: QAPI schema source file information.
+
+    :raise QAPISemError: When ``expr`` is not a valid ``alternate``.
+    :return: None, ``expr`` is normalized in-place as needed.
+    """
     members = expr['data']
 
     if not members:
@@ -331,6 +548,15 @@ def check_alternate(expr: _JSONObject, info: QAPISourceInfo) -> None:
 
 
 def check_command(expr: _JSONObject, info: QAPISourceInfo) -> None:
+    """
+    Normalize and validate this expression as a ``command`` definition.
+
+    :param expr: The expression to validate.
+    :param info: QAPI schema source file information.
+
+    :raise QAPISemError: When ``expr`` is not a valid ``command``.
+    :return: None, ``expr`` is normalized in-place as needed.
+    """
     args = expr.get('data')
     rets = expr.get('returns')
     boxed = expr.get('boxed', False)
@@ -342,6 +568,15 @@ def check_command(expr: _JSONObject, info: QAPISourceInfo) -> None:
 
 
 def check_event(expr: _JSONObject, info: QAPISourceInfo) -> None:
+    """
+    Normalize and validate this expression as an ``event`` definition.
+
+    :param expr: The expression to validate.
+    :param info: QAPI schema source file information.
+
+    :raise QAPISemError: When ``expr`` is not a valid ``event``.
+    :return: None, ``expr`` is normalized in-place as needed.
+    """
     args = expr.get('data')
     boxed = expr.get('boxed', False)
 
@@ -351,6 +586,17 @@ def check_event(expr: _JSONObject, info: QAPISourceInfo) -> None:
 
 
 def check_exprs(exprs: List[_JSONObject]) -> List[_JSONObject]:
+    """
+    Validate and normalize a list of parsed QAPI schema expressions.
+
+    This function accepts a list of expressions and metadata as returned
+    by the parser.  It destructively normalizes the expressions in-place.
+
+    :param exprs: The list of expressions to normalize and validate.
+
+    :raise QAPISemError: When any expression fails validation.
+    :return: The same list of expressions (now modified).
+    """
     for expr_elem in exprs:
         # Expression
         assert isinstance(expr_elem['expr'], dict)
-- 
2.30.2



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

* [PATCH v5 16/17] qapi/expr.py: Use tuples instead of lists for static data
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
                   ` (14 preceding siblings ...)
  2021-04-21 18:20 ` [PATCH v5 15/17] qapi/expr.py: Add docstrings John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-21 18:20 ` [PATCH v5 17/17] qapi/expr: Update authorship and copyright information John Snow
  2021-04-22  7:07 ` [PATCH v5 00/17] qapi: static typing conversion, pt3 Markus Armbruster
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

It is -- maybe -- possibly -- three nanoseconds faster.

Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
Reviewed-by: Cleber Rosa <crosa@redhat.com>
---
 scripts/qapi/expr.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index 0b66d808421..225a82e20d3 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -239,11 +239,11 @@ def check_flags(expr: _JSONObject, info: QAPISourceInfo) -> None:
         When certain flags have an invalid value, or when
         incompatible flags are present.
     """
-    for key in ['gen', 'success-response']:
+    for key in ('gen', 'success-response'):
         if key in expr and expr[key] is not False:
             raise QAPISemError(
                 info, "flag '%s' may only use false value" % key)
-    for key in ['boxed', 'allow-oob', 'allow-preconfig', 'coroutine']:
+    for key in ('boxed', 'allow-oob', 'allow-preconfig', 'coroutine'):
         if key in expr and expr[key] is not True:
             raise QAPISemError(
                 info, "flag '%s' may only use true value" % key)
-- 
2.30.2



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

* [PATCH v5 17/17] qapi/expr: Update authorship and copyright information
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
                   ` (15 preceding siblings ...)
  2021-04-21 18:20 ` [PATCH v5 16/17] qapi/expr.py: Use tuples instead of lists for static data John Snow
@ 2021-04-21 18:20 ` John Snow
  2021-04-22  7:07 ` [PATCH v5 00/17] qapi: static typing conversion, pt3 Markus Armbruster
  17 siblings, 0 replies; 19+ messages in thread
From: John Snow @ 2021-04-21 18:20 UTC (permalink / raw)
  To: Markus Armbruster, qemu-devel
  Cc: Michael Roth, John Snow, Eduardo Habkost, Cleber Rosa

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

diff --git a/scripts/qapi/expr.py b/scripts/qapi/expr.py
index 225a82e20d3..496f7e0333e 100644
--- a/scripts/qapi/expr.py
+++ b/scripts/qapi/expr.py
@@ -1,13 +1,14 @@
 # -*- coding: utf-8 -*-
 #
 # Copyright IBM, Corp. 2011
-# Copyright (c) 2013-2019 Red Hat Inc.
+# Copyright (c) 2013-2021 Red Hat Inc.
 #
 # Authors:
 #  Anthony Liguori <aliguori@us.ibm.com>
 #  Markus Armbruster <armbru@redhat.com>
 #  Eric Blake <eblake@redhat.com>
 #  Marc-André Lureau <marcandre.lureau@redhat.com>
+#  John Snow <jsnow@redhat.com>
 #
 # This work is licensed under the terms of the GNU GPL, version 2.
 # See the COPYING file in the top-level directory.
-- 
2.30.2



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

* Re: [PATCH v5 00/17] qapi: static typing conversion, pt3
  2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
                   ` (16 preceding siblings ...)
  2021-04-21 18:20 ` [PATCH v5 17/17] qapi/expr: Update authorship and copyright information John Snow
@ 2021-04-22  7:07 ` Markus Armbruster
  17 siblings, 0 replies; 19+ messages in thread
From: Markus Armbruster @ 2021-04-22  7:07 UTC (permalink / raw)
  To: John Snow; +Cc: Michael Roth, Cleber Rosa, qemu-devel, Eduardo Habkost

John Snow <jsnow@redhat.com> writes:

> Hi, this series adds static types to the QAPI module.
> This is part three, and it focuses on expr.py.
>
> Environment:
> - Python >= 3.6, <= 3.8 *
> - mypy >= 0.770
> - pylint >= 2.6.0
> - flake8
> - isort
>
> Every commit should pass with (from ./scripts/):
>  - flake8 qapi/
>  - pylint --rcfile=qapi/pylintrc qapi/
>  - mypy --config-file=qapi/mypy.ini qapi/
>  - pushd qapi && isort -c . && popd

Series
Reviewed-by: Markus Armbruster <armbru@redhat.com>
and queued.  Thanks!



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

end of thread, other threads:[~2021-04-22  7:08 UTC | newest]

Thread overview: 19+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2021-04-21 18:20 [PATCH v5 00/17] qapi: static typing conversion, pt3 John Snow
2021-04-21 18:20 ` [PATCH v5 01/17] qapi/expr: Comment cleanup John Snow
2021-04-21 18:20 ` [PATCH v5 02/17] qapi/expr.py: Remove 'info' argument from nested check_if_str John Snow
2021-04-21 18:20 ` [PATCH v5 03/17] qapi/expr.py: Check for dict instead of OrderedDict John Snow
2021-04-21 18:20 ` [PATCH v5 04/17] qapi/expr.py: constrain incoming expression types John Snow
2021-04-21 18:20 ` [PATCH v5 05/17] qapi/expr.py: Add assertion for union type 'check_dict' John Snow
2021-04-21 18:20 ` [PATCH v5 06/17] qapi/expr.py: move string check upwards in check_type John Snow
2021-04-21 18:20 ` [PATCH v5 07/17] qapi/expr.py: Check type of union and alternate 'data' member John Snow
2021-04-21 18:20 ` [PATCH v5 08/17] qapi/expr.py: Add casts in a few select cases John Snow
2021-04-21 18:20 ` [PATCH v5 09/17] qapi/expr.py: Modify check_keys to accept any Collection John Snow
2021-04-21 18:20 ` [PATCH v5 10/17] qapi/expr.py: add type hint annotations John Snow
2021-04-21 18:20 ` [PATCH v5 11/17] qapi/expr.py: Consolidate check_if_str calls in check_if John Snow
2021-04-21 18:20 ` [PATCH v5 12/17] qapi/expr.py: Remove single-letter variable John Snow
2021-04-21 18:20 ` [PATCH v5 13/17] qapi/expr.py: enable pylint checks John Snow
2021-04-21 18:20 ` [PATCH v5 14/17] qapi/expr: Only explicitly prohibit 'Kind' nor 'List' for type names John Snow
2021-04-21 18:20 ` [PATCH v5 15/17] qapi/expr.py: Add docstrings John Snow
2021-04-21 18:20 ` [PATCH v5 16/17] qapi/expr.py: Use tuples instead of lists for static data John Snow
2021-04-21 18:20 ` [PATCH v5 17/17] qapi/expr: Update authorship and copyright information John Snow
2021-04-22  7:07 ` [PATCH v5 00/17] qapi: static typing conversion, pt3 Markus Armbruster

This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.