aboutsummaryrefslogtreecommitdiff
path: root/scripts/qapi
diff options
context:
space:
mode:
Diffstat (limited to 'scripts/qapi')
-rw-r--r--scripts/qapi/.flake83
-rw-r--r--scripts/qapi/.isort.cfg7
-rw-r--r--scripts/qapi/backend.py65
-rw-r--r--scripts/qapi/commands.py11
-rw-r--r--scripts/qapi/common.py44
-rw-r--r--scripts/qapi/events.py2
-rw-r--r--scripts/qapi/features.py48
-rw-r--r--scripts/qapi/gen.py9
-rw-r--r--scripts/qapi/introspect.py22
-rw-r--r--scripts/qapi/main.py72
-rw-r--r--scripts/qapi/mypy.ini4
-rw-r--r--scripts/qapi/parser.py166
-rw-r--r--scripts/qapi/pylintrc2
-rw-r--r--scripts/qapi/schema.py48
-rw-r--r--scripts/qapi/source.py4
-rw-r--r--scripts/qapi/types.py23
-rw-r--r--scripts/qapi/visit.py26
17 files changed, 393 insertions, 163 deletions
diff --git a/scripts/qapi/.flake8 b/scripts/qapi/.flake8
deleted file mode 100644
index a873ff6..0000000
--- a/scripts/qapi/.flake8
+++ /dev/null
@@ -1,3 +0,0 @@
-[flake8]
-# Prefer pylint's bare-except checks to flake8's
-extend-ignore = E722
diff --git a/scripts/qapi/.isort.cfg b/scripts/qapi/.isort.cfg
deleted file mode 100644
index 643caa1..0000000
--- a/scripts/qapi/.isort.cfg
+++ /dev/null
@@ -1,7 +0,0 @@
-[settings]
-force_grid_wrap=4
-force_sort_within_sections=True
-include_trailing_comma=True
-line_length=72
-lines_after_imports=2
-multi_line_output=3
diff --git a/scripts/qapi/backend.py b/scripts/qapi/backend.py
new file mode 100644
index 0000000..49ae6ec
--- /dev/null
+++ b/scripts/qapi/backend.py
@@ -0,0 +1,65 @@
+# This work is licensed under the terms of the GNU GPL, version 2 or later.
+# See the COPYING file in the top-level directory.
+
+from abc import ABC, abstractmethod
+
+from .commands import gen_commands
+from .events import gen_events
+from .features import gen_features
+from .introspect import gen_introspect
+from .schema import QAPISchema
+from .types import gen_types
+from .visit import gen_visit
+
+
+class QAPIBackend(ABC):
+ # pylint: disable=too-few-public-methods
+
+ @abstractmethod
+ def generate(self,
+ schema: QAPISchema,
+ output_dir: str,
+ prefix: str,
+ unmask: bool,
+ builtins: bool,
+ gen_tracing: bool) -> None:
+ """
+ Generate code for the given schema into the target directory.
+
+ :param schema: The primary QAPI schema object.
+ :param output_dir: The output directory to store generated code.
+ :param prefix: Optional C-code prefix for symbol names.
+ :param unmask: Expose non-ABI names through introspection?
+ :param builtins: Generate code for built-in types?
+
+ :raise QAPIError: On failures.
+ """
+
+
+class QAPICBackend(QAPIBackend):
+ # pylint: disable=too-few-public-methods
+
+ def generate(self,
+ schema: QAPISchema,
+ output_dir: str,
+ prefix: str,
+ unmask: bool,
+ builtins: bool,
+ gen_tracing: bool) -> None:
+ """
+ Generate C code for the given schema into the target directory.
+
+ :param schema_file: The primary QAPI schema file.
+ :param output_dir: The output directory to store generated code.
+ :param prefix: Optional C-code prefix for symbol names.
+ :param unmask: Expose non-ABI names through introspection?
+ :param builtins: Generate code for built-in types?
+
+ :raise QAPIError: On failures.
+ """
+ gen_types(schema, output_dir, prefix, builtins)
+ gen_features(schema, output_dir, prefix)
+ gen_visit(schema, output_dir, prefix, builtins)
+ gen_commands(schema, output_dir, prefix, gen_tracing)
+ gen_events(schema, output_dir, prefix)
+ gen_introspect(schema, output_dir, prefix, unmask)
diff --git a/scripts/qapi/commands.py b/scripts/qapi/commands.py
index 79951a8..7914227 100644
--- a/scripts/qapi/commands.py
+++ b/scripts/qapi/commands.py
@@ -25,7 +25,7 @@ from .gen import (
QAPIGenC,
QAPISchemaModularCVisitor,
build_params,
- gen_special_features,
+ gen_features,
ifcontext,
)
from .schema import (
@@ -298,7 +298,7 @@ def gen_register_command(name: str,
''',
name=name, c_name=c_name(name),
opts=' | '.join(options) or 0,
- feats=gen_special_features(features))
+ feats=gen_features(features))
return ret
@@ -320,7 +320,7 @@ class QAPISchemaGenCommandVisitor(QAPISchemaModularCVisitor):
#include "qemu/osdep.h"
#include "qapi/compat-policy.h"
#include "qapi/visitor.h"
-#include "qapi/qmp/qdict.h"
+#include "qobject/qdict.h"
#include "qapi/dealloc-visitor.h"
#include "qapi/error.h"
#include "%(visit)s.h"
@@ -330,7 +330,7 @@ class QAPISchemaGenCommandVisitor(QAPISchemaModularCVisitor):
if self._gen_tracing and commands != 'qapi-commands':
self._genc.add(mcgen('''
-#include "qapi/qmp/qjson.h"
+#include "qobject/qjson.h"
#include "trace/trace-%(nm)s_trace_events.h"
''',
nm=c_name(commands, protect=False)))
@@ -346,7 +346,7 @@ class QAPISchemaGenCommandVisitor(QAPISchemaModularCVisitor):
def visit_begin(self, schema: QAPISchema) -> None:
self._add_module('./init', ' * QAPI Commands initialization')
self._genh.add(mcgen('''
-#include "qapi/qmp/dispatch.h"
+#include "qapi/qmp-registry.h"
void %(c_prefix)sqmp_init_marshal(QmpCommandList *cmds);
''',
@@ -355,6 +355,7 @@ void %(c_prefix)sqmp_init_marshal(QmpCommandList *cmds);
#include "qemu/osdep.h"
#include "%(prefix)sqapi-commands.h"
#include "%(prefix)sqapi-init-commands.h"
+#include "%(prefix)sqapi-features.h"
void %(c_prefix)sqmp_init_marshal(QmpCommandList *cmds)
{
diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py
index 737b059..d7c8aa3 100644
--- a/scripts/qapi/common.py
+++ b/scripts/qapi/common.py
@@ -40,22 +40,28 @@ def camel_to_upper(value: str) -> str:
ENUM_Name2 -> ENUM_NAME2
ENUM24_Name -> ENUM24_NAME
"""
- c_fun_str = c_name(value, False)
- if value.isupper():
- return c_fun_str
-
- new_name = ''
- length = len(c_fun_str)
- for i in range(length):
- char = c_fun_str[i]
- # When char is upper case and no '_' appears before, do more checks
- if char.isupper() and (i > 0) and c_fun_str[i - 1] != '_':
- if i < length - 1 and c_fun_str[i + 1].islower():
- new_name += '_'
- elif c_fun_str[i - 1].isdigit():
- new_name += '_'
- new_name += char
- return new_name.lstrip('_').upper()
+ ret = value[0]
+ upc = value[0].isupper()
+
+ # Copy remainder of ``value`` to ``ret`` with '_' inserted
+ for ch in value[1:]:
+ if ch.isupper() == upc:
+ pass
+ elif upc:
+ # ``ret`` ends in upper case, next char isn't: insert '_'
+ # before the last upper case char unless there is one
+ # already, or it's at the beginning
+ if len(ret) > 2 and ret[-2].isalnum():
+ ret = ret[:-1] + '_' + ret[-1]
+ else:
+ # ``ret`` doesn't end in upper case, next char is: insert
+ # '_' before it
+ if ret[-1].isalnum():
+ ret += '_'
+ ret += ch
+ upc = ch.isupper()
+
+ return c_name(ret.upper()).lstrip('_')
def c_enum_const(type_name: str,
@@ -68,9 +74,9 @@ def c_enum_const(type_name: str,
:param const_name: The name of this constant.
:param prefix: Optional, prefix that overrides the type_name.
"""
- if prefix is not None:
- type_name = prefix
- return camel_to_upper(type_name) + '_' + c_name(const_name, False).upper()
+ if prefix is None:
+ prefix = camel_to_upper(type_name)
+ return prefix + '_' + c_name(const_name, False).upper()
def c_name(name: str, protect: bool = True) -> str:
diff --git a/scripts/qapi/events.py b/scripts/qapi/events.py
index d1f6399..d179b0e 100644
--- a/scripts/qapi/events.py
+++ b/scripts/qapi/events.py
@@ -194,7 +194,7 @@ class QAPISchemaGenEventVisitor(QAPISchemaModularCVisitor):
#include "%(visit)s.h"
#include "qapi/compat-policy.h"
#include "qapi/error.h"
-#include "qapi/qmp/qdict.h"
+#include "qobject/qdict.h"
#include "qapi/qmp-event.h"
''',
events=events, visit=visit,
diff --git a/scripts/qapi/features.py b/scripts/qapi/features.py
new file mode 100644
index 0000000..5756320
--- /dev/null
+++ b/scripts/qapi/features.py
@@ -0,0 +1,48 @@
+"""
+QAPI features generator
+
+Copyright 2024 Red Hat
+
+This work is licensed under the terms of the GNU GPL, version 2.
+# See the COPYING file in the top-level directory.
+"""
+
+from typing import ValuesView
+
+from .common import c_enum_const, c_name
+from .gen import QAPISchemaMonolithicCVisitor
+from .schema import QAPISchema, QAPISchemaFeature
+
+
+class QAPISchemaGenFeatureVisitor(QAPISchemaMonolithicCVisitor):
+
+ def __init__(self, prefix: str):
+ super().__init__(
+ prefix, 'qapi-features',
+ ' * Schema-defined QAPI features',
+ __doc__)
+
+ self.features: ValuesView[QAPISchemaFeature]
+
+ def visit_begin(self, schema: QAPISchema) -> None:
+ self.features = schema.features()
+ self._genh.add("#include \"qapi/util.h\"\n\n")
+
+ def visit_end(self) -> None:
+ self._genh.add("typedef enum {\n")
+ for f in self.features:
+ self._genh.add(f" {c_enum_const('qapi_feature', f.name)}")
+ if f.name in QAPISchemaFeature.SPECIAL_NAMES:
+ self._genh.add(f" = {c_enum_const('qapi', f.name)},\n")
+ else:
+ self._genh.add(",\n")
+
+ self._genh.add("} " + c_name('QapiFeature') + ";\n")
+
+
+def gen_features(schema: QAPISchema,
+ output_dir: str,
+ prefix: str) -> None:
+ vis = QAPISchemaGenFeatureVisitor(prefix)
+ schema.visit(vis)
+ vis.write(output_dir)
diff --git a/scripts/qapi/gen.py b/scripts/qapi/gen.py
index 6a8abe0..d3c56d45 100644
--- a/scripts/qapi/gen.py
+++ b/scripts/qapi/gen.py
@@ -24,6 +24,7 @@ from typing import (
)
from .common import (
+ c_enum_const,
c_fname,
c_name,
guardend,
@@ -40,10 +41,10 @@ from .schema import (
from .source import QAPISourceInfo
-def gen_special_features(features: Sequence[QAPISchemaFeature]) -> str:
- special_features = [f"1u << QAPI_{feat.name.upper()}"
- for feat in features if feat.is_special()]
- return ' | '.join(special_features) or '0'
+def gen_features(features: Sequence[QAPISchemaFeature]) -> str:
+ feats = [f"1u << {c_enum_const('qapi_feature', feat.name)}"
+ for feat in features]
+ return ' | '.join(feats) or '0'
class QAPIGen:
diff --git a/scripts/qapi/introspect.py b/scripts/qapi/introspect.py
index 86c075a..89ee5d5 100644
--- a/scripts/qapi/introspect.py
+++ b/scripts/qapi/introspect.py
@@ -11,6 +11,7 @@ This work is licensed under the terms of the GNU GPL, version 2.
See the COPYING file in the top-level directory.
"""
+from dataclasses import dataclass
from typing import (
Any,
Dict,
@@ -27,8 +28,8 @@ from .gen import QAPISchemaMonolithicCVisitor
from .schema import (
QAPISchema,
QAPISchemaAlternatives,
- QAPISchemaBranches,
QAPISchemaArrayType,
+ QAPISchemaBranches,
QAPISchemaBuiltinType,
QAPISchemaEntity,
QAPISchemaEnumMember,
@@ -79,19 +80,16 @@ SchemaInfoCommand = Dict[str, object]
_ValueT = TypeVar('_ValueT', bound=_Value)
+@dataclass
class Annotated(Generic[_ValueT]):
"""
Annotated generally contains a SchemaInfo-like type (as a dict),
But it also used to wrap comments/ifconds around scalar leaf values,
for the benefit of features and enums.
"""
- # TODO: Remove after Python 3.7 adds @dataclass:
- # pylint: disable=too-few-public-methods
- def __init__(self, value: _ValueT, ifcond: QAPISchemaIfCond,
- comment: Optional[str] = None):
- self.value = value
- self.comment: Optional[str] = comment
- self.ifcond = ifcond
+ value: _ValueT
+ ifcond: QAPISchemaIfCond
+ comment: Optional[str] = None
def _tree_to_qlit(obj: JSONValue,
@@ -197,7 +195,7 @@ class QAPISchemaGenIntrospectVisitor(QAPISchemaMonolithicCVisitor):
# generate C
name = c_name(self._prefix, protect=False) + 'qmp_schema_qlit'
self._genh.add(mcgen('''
-#include "qapi/qmp/qlit.h"
+#include "qobject/qlit.h"
extern const QLitObject %(c_name)s;
''',
@@ -233,9 +231,9 @@ const QLitObject %(c_name)s = %(c_string)s;
typ = type_int
elif (isinstance(typ, QAPISchemaArrayType) and
typ.element_type.json_type() == 'int'):
- type_intList = self._schema.lookup_type('intList')
- assert type_intList
- typ = 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)
diff --git a/scripts/qapi/main.py b/scripts/qapi/main.py
index 316736b..0e2a6ae 100644
--- a/scripts/qapi/main.py
+++ b/scripts/qapi/main.py
@@ -8,17 +8,14 @@ This is the main entry point for generating C code from the QAPI schema.
"""
import argparse
+from importlib import import_module
import sys
from typing import Optional
-from .commands import gen_commands
+from .backend import QAPIBackend, QAPICBackend
from .common import must_match
from .error import QAPIError
-from .events import gen_events
-from .introspect import gen_introspect
from .schema import QAPISchema
-from .types import gen_types
-from .visit import gen_visit
def invalid_prefix_char(prefix: str) -> Optional[str]:
@@ -28,31 +25,36 @@ def invalid_prefix_char(prefix: str) -> Optional[str]:
return None
-def generate(schema_file: str,
- output_dir: str,
- prefix: str,
- unmask: bool = False,
- builtins: bool = False,
- gen_tracing: bool = False) -> None:
- """
- Generate C code for the given schema into the target directory.
+def create_backend(path: str) -> QAPIBackend:
+ if path is None:
+ return QAPICBackend()
- :param schema_file: The primary QAPI schema file.
- :param output_dir: The output directory to store generated code.
- :param prefix: Optional C-code prefix for symbol names.
- :param unmask: Expose non-ABI names through introspection?
- :param builtins: Generate code for built-in types?
+ module_path, dot, class_name = path.rpartition('.')
+ if not dot:
+ raise QAPIError("argument of -B must be of the form MODULE.CLASS")
- :raise QAPIError: On failures.
- """
- assert invalid_prefix_char(prefix) is None
+ try:
+ mod = import_module(module_path)
+ except Exception as ex:
+ raise QAPIError(f"unable to import '{module_path}': {ex}") from ex
+
+ try:
+ klass = getattr(mod, class_name)
+ except AttributeError as ex:
+ raise QAPIError(
+ f"module '{module_path}' has no class '{class_name}'") from ex
+
+ try:
+ backend = klass()
+ except Exception as ex:
+ raise QAPIError(
+ f"backend '{path}' cannot be instantiated: {ex}") from ex
+
+ if not isinstance(backend, QAPIBackend):
+ raise QAPIError(
+ f"backend '{path}' must be an instance of QAPIBackend")
- schema = QAPISchema(schema_file)
- gen_types(schema, output_dir, prefix, builtins)
- gen_visit(schema, output_dir, prefix, builtins)
- gen_commands(schema, output_dir, prefix, gen_tracing)
- gen_events(schema, output_dir, prefix)
- gen_introspect(schema, output_dir, prefix, unmask)
+ return backend
def main() -> int:
@@ -75,6 +77,8 @@ def main() -> int:
parser.add_argument('-u', '--unmask-non-abi-names', action='store_true',
dest='unmask',
help="expose non-ABI names in introspection")
+ parser.add_argument('-B', '--backend', default=None,
+ help="Python module name for code generator")
# Option --suppress-tracing exists so we can avoid solving build system
# problems. TODO Drop it when we no longer need it.
@@ -91,12 +95,14 @@ def main() -> int:
return 1
try:
- generate(args.schema,
- output_dir=args.output_dir,
- prefix=args.prefix,
- unmask=args.unmask,
- builtins=args.builtins,
- gen_tracing=not args.suppress_tracing)
+ schema = QAPISchema(args.schema)
+ backend = create_backend(args.backend)
+ backend.generate(schema,
+ output_dir=args.output_dir,
+ prefix=args.prefix,
+ unmask=args.unmask,
+ builtins=args.builtins,
+ gen_tracing=not args.suppress_tracing)
except QAPIError as err:
print(err, file=sys.stderr)
return 1
diff --git a/scripts/qapi/mypy.ini b/scripts/qapi/mypy.ini
deleted file mode 100644
index 8109470..0000000
--- a/scripts/qapi/mypy.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[mypy]
-strict = True
-disallow_untyped_calls = False
-python_version = 3.8
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py
index 7b13a58..949d9e8 100644
--- a/scripts/qapi/parser.py
+++ b/scripts/qapi/parser.py
@@ -14,7 +14,7 @@
# 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 enum
import os
import re
from typing import (
@@ -154,7 +154,7 @@ class QAPISchemaParser:
"value of 'include' must be a string")
incl_fname = os.path.join(os.path.dirname(self._fname),
include)
- self._add_expr(OrderedDict({'include': incl_fname}), info)
+ self._add_expr({'include': incl_fname}, info)
exprs_include = self._include(include, info, incl_fname,
self._included)
if exprs_include:
@@ -355,7 +355,7 @@ class QAPISchemaParser:
raise QAPIParseError(self, "stray '%s'" % match.group(0))
def get_members(self) -> Dict[str, object]:
- expr: Dict[str, object] = OrderedDict()
+ expr: Dict[str, object] = {}
if self.tok == '}':
self.accept()
return expr
@@ -448,7 +448,7 @@ class QAPISchemaParser:
indent = must_match(r'\s*', line).end()
if not indent:
return line
- doc.append_line(line[indent:])
+ doc.append_line(line)
prev_line_blank = False
while True:
self.accept(False)
@@ -465,7 +465,7 @@ class QAPISchemaParser:
self,
"unexpected de-indent (expected at least %d spaces)" %
indent)
- doc.append_line(line[indent:])
+ doc.append_line(line)
prev_line_blank = True
def get_doc_paragraph(self, doc: 'QAPIDoc') -> Optional[str]:
@@ -544,10 +544,41 @@ class QAPISchemaParser:
line = self.get_doc_indented(doc)
no_more_args = True
elif match := re.match(
- r'(Returns|Errors|Since|Notes?|Examples?|TODO): *',
- line):
+ r'(Returns|Errors|Since|Notes?|Examples?|TODO)'
+ r'(?!::): *',
+ line,
+ ):
# tagged section
- doc.new_tagged_section(self.info, match.group(1))
+
+ # Note: "sections" with two colons are left alone as
+ # rST markup and not interpreted as a section heading.
+
+ # TODO: Remove these errors sometime in 2025 or so
+ # after we've fully transitioned to the new qapidoc
+ # generator.
+
+ # See commit message for more markup suggestions O:-)
+ if 'Note' in match.group(1):
+ emsg = (
+ f"The '{match.group(1)}' section is no longer "
+ "supported. Please use rST's '.. note::' or "
+ "'.. admonition:: notes' directives, or another "
+ "suitable admonition instead."
+ )
+ raise QAPIParseError(self, emsg)
+
+ if 'Example' in match.group(1):
+ emsg = (
+ f"The '{match.group(1)}' section is no longer "
+ "supported. Please use the '.. qmp-example::' "
+ "directive, or other suitable markup instead."
+ )
+ raise QAPIParseError(self, emsg)
+
+ doc.new_tagged_section(
+ self.info,
+ QAPIDoc.Kind.from_string(match.group(1))
+ )
text = line[match.end():]
if text:
doc.append_line(text)
@@ -558,7 +589,7 @@ class QAPISchemaParser:
self,
"unexpected '=' markup in definition documentation")
else:
- # tag-less paragraph
+ # plain paragraph
doc.ensure_untagged_section(self.info)
doc.append_line(line)
line = self.get_doc_paragraph(doc)
@@ -583,7 +614,7 @@ class QAPISchemaParser:
line = self.get_doc_line()
first = False
- self.accept(False)
+ self.accept()
doc.end()
return doc
@@ -607,23 +638,51 @@ class QAPIDoc:
Free-form documentation blocks consist only of a body section.
"""
+ class Kind(enum.Enum):
+ PLAIN = 0
+ MEMBER = 1
+ FEATURE = 2
+ RETURNS = 3
+ ERRORS = 4
+ SINCE = 5
+ TODO = 6
+
+ @staticmethod
+ def from_string(kind: str) -> 'QAPIDoc.Kind':
+ return QAPIDoc.Kind[kind.upper()]
+
+ def __str__(self) -> str:
+ return self.name.title()
+
class Section:
# pylint: disable=too-few-public-methods
- def __init__(self, info: QAPISourceInfo,
- tag: Optional[str] = None):
+ def __init__(
+ self,
+ info: QAPISourceInfo,
+ kind: 'QAPIDoc.Kind',
+ ):
# section source info, i.e. where it begins
self.info = info
- # section tag, if any ('Returns', '@name', ...)
- self.tag = tag
+ # section kind
+ self.kind = kind
# section text without tag
self.text = ''
+ def __repr__(self) -> str:
+ return f"<QAPIDoc.Section kind={self.kind!r} text={self.text!r}>"
+
def append_line(self, line: str) -> None:
self.text += line + '\n'
class ArgSection(Section):
- def __init__(self, info: QAPISourceInfo, tag: str):
- super().__init__(info, tag)
+ def __init__(
+ self,
+ info: QAPISourceInfo,
+ kind: 'QAPIDoc.Kind',
+ name: str
+ ):
+ super().__init__(info, kind)
+ self.name = name
self.member: Optional['QAPISchemaMember'] = None
def connect(self, member: 'QAPISchemaMember') -> None:
@@ -635,7 +694,9 @@ class QAPIDoc:
# definition doc's symbol, None for free-form doc
self.symbol: Optional[str] = symbol
# the sections in textual order
- self.all_sections: List[QAPIDoc.Section] = [QAPIDoc.Section(info)]
+ self.all_sections: List[QAPIDoc.Section] = [
+ QAPIDoc.Section(info, QAPIDoc.Kind.PLAIN)
+ ]
# the body section
self.body: Optional[QAPIDoc.Section] = self.all_sections[0]
# dicts mapping parameter/feature names to their description
@@ -652,55 +713,71 @@ class QAPIDoc:
def end(self) -> None:
for section in self.all_sections:
section.text = section.text.strip('\n')
- if section.tag is not None and section.text == '':
+ if section.kind != QAPIDoc.Kind.PLAIN and section.text == '':
raise QAPISemError(
- section.info, "text required after '%s:'" % section.tag)
+ section.info, "text required after '%s:'" % section.kind)
def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
- if self.all_sections and not self.all_sections[-1].tag:
+ kind = QAPIDoc.Kind.PLAIN
+
+ if self.all_sections and self.all_sections[-1].kind == kind:
# extend current section
- self.all_sections[-1].text += '\n'
+ section = self.all_sections[-1]
+ if not section.text:
+ # Section is empty so far; update info to start *here*.
+ section.info = info
+ section.text += '\n'
return
+
# start new section
- section = self.Section(info)
+ section = self.Section(info, kind)
self.sections.append(section)
self.all_sections.append(section)
- def new_tagged_section(self, info: QAPISourceInfo, tag: str) -> None:
- section = self.Section(info, tag)
- if tag == 'Returns':
+ def new_tagged_section(
+ self,
+ info: QAPISourceInfo,
+ kind: 'QAPIDoc.Kind',
+ ) -> None:
+ section = self.Section(info, kind)
+ if kind == QAPIDoc.Kind.RETURNS:
if self.returns:
raise QAPISemError(
- info, "duplicated '%s' section" % tag)
+ info, "duplicated '%s' section" % kind)
self.returns = section
- elif tag == 'Errors':
+ elif kind == QAPIDoc.Kind.ERRORS:
if self.errors:
raise QAPISemError(
- info, "duplicated '%s' section" % tag)
+ info, "duplicated '%s' section" % kind)
self.errors = section
- elif tag == 'Since':
+ elif kind == QAPIDoc.Kind.SINCE:
if self.since:
raise QAPISemError(
- info, "duplicated '%s' section" % tag)
+ info, "duplicated '%s' section" % kind)
self.since = section
self.sections.append(section)
self.all_sections.append(section)
- def _new_description(self, info: QAPISourceInfo, name: str,
- desc: Dict[str, ArgSection]) -> None:
+ def _new_description(
+ self,
+ info: QAPISourceInfo,
+ name: str,
+ kind: 'QAPIDoc.Kind',
+ desc: Dict[str, ArgSection]
+ ) -> None:
if not name:
raise QAPISemError(info, "invalid parameter name")
if name in desc:
raise QAPISemError(info, "'%s' parameter name duplicated" % name)
- section = self.ArgSection(info, '@' + name)
+ section = self.ArgSection(info, kind, name)
self.all_sections.append(section)
desc[name] = section
def new_argument(self, info: QAPISourceInfo, name: str) -> None:
- self._new_description(info, name, self.args)
+ self._new_description(info, name, QAPIDoc.Kind.MEMBER, self.args)
def new_feature(self, info: QAPISourceInfo, name: str) -> None:
- self._new_description(info, name, self.features)
+ self._new_description(info, name, QAPIDoc.Kind.FEATURE, self.features)
def append_line(self, line: str) -> None:
self.all_sections[-1].append_line(line)
@@ -712,8 +789,23 @@ class QAPIDoc:
raise QAPISemError(member.info,
"%s '%s' lacks documentation"
% (member.role, member.name))
- self.args[member.name] = QAPIDoc.ArgSection(
- self.info, '@' + member.name)
+ # Insert stub documentation section for missing member docs.
+ # TODO: drop when undocumented members are outlawed
+
+ section = QAPIDoc.ArgSection(
+ self.info, QAPIDoc.Kind.MEMBER, member.name)
+ self.args[member.name] = section
+
+ # Determine where to insert stub doc - it should go at the
+ # end of the members section(s), if any. Note that index 0
+ # is assumed to be an untagged intro section, even if it is
+ # empty.
+ index = 1
+ if len(self.all_sections) > 1:
+ while self.all_sections[index].kind == QAPIDoc.Kind.MEMBER:
+ index += 1
+ self.all_sections.insert(index, section)
+
self.args[member.name].connect(member)
def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
diff --git a/scripts/qapi/pylintrc b/scripts/qapi/pylintrc
index c028a1f..e16283a 100644
--- a/scripts/qapi/pylintrc
+++ b/scripts/qapi/pylintrc
@@ -17,7 +17,9 @@ disable=consider-using-f-string,
too-many-arguments,
too-many-branches,
too-many-instance-attributes,
+ too-many-positional-arguments,
too-many-statements,
+ unknown-option-value,
useless-option-value,
[REPORTS]
diff --git a/scripts/qapi/schema.py b/scripts/qapi/schema.py
index 721c470..cbe3b5a 100644
--- a/scripts/qapi/schema.py
+++ b/scripts/qapi/schema.py
@@ -19,7 +19,6 @@
from __future__ import annotations
from abc import ABC, abstractmethod
-from collections import OrderedDict
import os
import re
from typing import (
@@ -29,6 +28,7 @@ from typing import (
List,
Optional,
Union,
+ ValuesView,
cast,
)
@@ -556,7 +556,7 @@ class QAPISchemaObjectType(QAPISchemaType):
super().check(schema)
assert self._checked and not self._check_complete
- seen = OrderedDict()
+ seen = {}
if self._base_name:
self.base = schema.resolve_type(self._base_name, self.info,
"'base'")
@@ -730,6 +730,7 @@ class QAPISchemaVariants:
for v in self.variants:
v.set_defined_in(name)
+ # pylint: disable=unused-argument
def check(
self, schema: QAPISchema, seen: Dict[str, QAPISchemaMember]
) -> None:
@@ -932,8 +933,11 @@ class QAPISchemaEnumMember(QAPISchemaMember):
class QAPISchemaFeature(QAPISchemaMember):
role = 'feature'
+ # Features which are standardized across all schemas
+ SPECIAL_NAMES = ['deprecated', 'unstable']
+
def is_special(self) -> bool:
- return self.name in ('deprecated', 'unstable')
+ return self.name in QAPISchemaFeature.SPECIAL_NAMES
class QAPISchemaObjectTypeMember(QAPISchemaMember):
@@ -1136,7 +1140,17 @@ class QAPISchema:
self.docs = parser.docs
self._entity_list: List[QAPISchemaEntity] = []
self._entity_dict: Dict[str, QAPISchemaDefinition] = {}
- self._module_dict: Dict[str, QAPISchemaModule] = OrderedDict()
+ self._module_dict: Dict[str, QAPISchemaModule] = {}
+ # NB, values in the dict will identify the first encountered
+ # usage of a named feature only
+ self._feature_dict: Dict[str, QAPISchemaFeature] = {}
+
+ # All schemas get the names defined in the QapiSpecialFeature enum.
+ # Rely on dict iteration order matching insertion order so that
+ # the special names are emitted first when generating code.
+ for f in QAPISchemaFeature.SPECIAL_NAMES:
+ self._feature_dict[f] = QAPISchemaFeature(f, None)
+
self._schema_dir = os.path.dirname(fname)
self._make_module(QAPISchemaModule.BUILTIN_MODULE_NAME)
self._make_module(fname)
@@ -1146,6 +1160,9 @@ class QAPISchema:
self._def_exprs(exprs)
self.check()
+ def features(self) -> ValuesView[QAPISchemaFeature]:
+ return self._feature_dict.values()
+
def _def_entity(self, ent: QAPISchemaEntity) -> None:
self._entity_list.append(ent)
@@ -1166,7 +1183,7 @@ class QAPISchema:
defn.info, "%s is already defined" % other_defn.describe())
self._entity_dict[defn.name] = defn
- def lookup_entity(self,name: str) -> Optional[QAPISchemaEntity]:
+ def lookup_entity(self, name: str) -> Optional[QAPISchemaEntity]:
return self._entity_dict.get(name)
def lookup_type(self, name: str) -> Optional[QAPISchemaType]:
@@ -1248,7 +1265,7 @@ class QAPISchema:
[{'name': n} for n in qtypes], None)
self._def_definition(QAPISchemaEnumType(
- 'QType', None, None, None, None, qtype_values, 'QTYPE'))
+ 'QType', None, None, None, None, qtype_values, None))
def _make_features(
self,
@@ -1257,6 +1274,12 @@ class QAPISchema:
) -> List[QAPISchemaFeature]:
if features is None:
return []
+
+ for f in features:
+ feat = QAPISchemaFeature(f['name'], info)
+ if feat.name not in self._feature_dict:
+ self._feature_dict[feat.name] = feat
+
return [QAPISchemaFeature(f['name'], info,
QAPISchemaIfCond(f.get('if')))
for f in features]
@@ -1302,11 +1325,10 @@ class QAPISchema:
name = 'q_obj_%s-%s' % (name, role)
typ = self.lookup_entity(name)
if typ:
- assert(isinstance(typ, QAPISchemaObjectType))
+ assert isinstance(typ, QAPISchemaObjectType)
# The implicit object type has multiple users. This can
# only be a duplicate definition, which will be flagged
# later.
- pass
else:
self._def_definition(QAPISchemaObjectType(
name, info, None, ifcond, None, None, members, None))
@@ -1431,7 +1453,7 @@ class QAPISchema:
ifcond = QAPISchemaIfCond(expr.get('if'))
info = expr.info
features = self._make_features(expr.get('features'), info)
- if isinstance(data, OrderedDict):
+ if isinstance(data, dict):
data = self._make_implicit_object_type(
name, info, ifcond,
'arg', self._make_members(data, info))
@@ -1450,7 +1472,7 @@ class QAPISchema:
ifcond = QAPISchemaIfCond(expr.get('if'))
info = expr.info
features = self._make_features(expr.get('features'), info)
- if isinstance(data, OrderedDict):
+ if isinstance(data, dict):
data = self._make_implicit_object_type(
name, info, ifcond,
'arg', self._make_members(data, info))
@@ -1485,6 +1507,12 @@ class QAPISchema:
for doc in self.docs:
doc.check()
+ features = list(self._feature_dict.values())
+ if len(features) > 64:
+ raise QAPISemError(
+ features[64].info,
+ "Maximum of 64 schema features is permitted")
+
def visit(self, visitor: QAPISchemaVisitor) -> None:
visitor.visit_begin(self)
for mod in self._module_dict.values():
diff --git a/scripts/qapi/source.py b/scripts/qapi/source.py
index 7b379fd..ffdc3f4 100644
--- a/scripts/qapi/source.py
+++ b/scripts/qapi/source.py
@@ -47,9 +47,9 @@ class QAPISourceInfo:
self.defn_meta = meta
self.defn_name = name
- def next_line(self: T) -> T:
+ def next_line(self: T, n: int = 1) -> T:
info = copy.copy(self)
- info.line += 1
+ info.line += n
return info
def loc(self) -> str:
diff --git a/scripts/qapi/types.py b/scripts/qapi/types.py
index 0dd0b00..2bf7533 100644
--- a/scripts/qapi/types.py
+++ b/scripts/qapi/types.py
@@ -16,11 +16,7 @@ This work is licensed under the terms of the GNU GPL, version 2.
from typing import List, Optional
from .common import c_enum_const, c_name, mcgen
-from .gen import (
- QAPISchemaModularCVisitor,
- gen_special_features,
- ifcontext,
-)
+from .gen import QAPISchemaModularCVisitor, gen_features, ifcontext
from .schema import (
QAPISchema,
QAPISchemaAlternatives,
@@ -61,17 +57,17 @@ const QEnumLookup %(c_name)s_lookup = {
index=index, name=memb.name)
ret += memb.ifcond.gen_endif()
- special_features = gen_special_features(memb.features)
- if special_features != '0':
+ features = gen_features(memb.features)
+ if features != '0':
feats += mcgen('''
- [%(index)s] = %(special_features)s,
+ [%(index)s] = %(features)s,
''',
- index=index, special_features=special_features)
+ index=index, features=features)
if feats:
ret += mcgen('''
},
- .special_features = (const unsigned char[%(max_index)s]) {
+ .features = (const uint64_t[%(max_index)s]) {
''',
max_index=max_index)
ret += feats
@@ -308,11 +304,14 @@ class QAPISchemaGenTypeVisitor(QAPISchemaModularCVisitor):
#include "qapi/dealloc-visitor.h"
#include "%(types)s.h"
#include "%(visit)s.h"
+#include "%(prefix)sqapi-features.h"
''',
- types=types, visit=visit))
+ types=types, visit=visit,
+ prefix=self._prefix))
self._genh.preamble_add(mcgen('''
#include "qapi/qapi-builtin-types.h"
-'''))
+''',
+ prefix=self._prefix))
def visit_begin(self, schema: QAPISchema) -> None:
# gen_object() is recursive, ensure it doesn't visit the empty type
diff --git a/scripts/qapi/visit.py b/scripts/qapi/visit.py
index e766aca..36e2409 100644
--- a/scripts/qapi/visit.py
+++ b/scripts/qapi/visit.py
@@ -21,11 +21,7 @@ from .common import (
indent,
mcgen,
)
-from .gen import (
- QAPISchemaModularCVisitor,
- gen_special_features,
- ifcontext,
-)
+from .gen import QAPISchemaModularCVisitor, gen_features, ifcontext
from .schema import (
QAPISchema,
QAPISchemaAlternatives,
@@ -103,15 +99,15 @@ bool visit_type_%(c_name)s_members(Visitor *v, %(c_name)s *obj, Error **errp)
''',
name=memb.name, has=has)
indent.increase()
- special_features = gen_special_features(memb.features)
- if special_features != '0':
+ features = gen_features(memb.features)
+ if features != '0':
ret += mcgen('''
- if (visit_policy_reject(v, "%(name)s", %(special_features)s, errp)) {
+ if (visit_policy_reject(v, "%(name)s", %(features)s, errp)) {
return false;
}
- if (!visit_policy_skip(v, "%(name)s", %(special_features)s)) {
+ if (!visit_policy_skip(v, "%(name)s", %(features)s)) {
''',
- name=memb.name, special_features=special_features)
+ name=memb.name, features=features)
indent.increase()
ret += mcgen('''
if (!visit_type_%(c_type)s(v, "%(name)s", &obj->%(c_name)s, errp)) {
@@ -120,7 +116,7 @@ bool visit_type_%(c_name)s_members(Visitor *v, %(c_name)s *obj, Error **errp)
''',
c_type=memb.type.c_name(), name=memb.name,
c_name=c_name(memb.name))
- if special_features != '0':
+ if features != '0':
indent.decrease()
ret += mcgen('''
}
@@ -280,8 +276,9 @@ bool visit_type_%(c_name)s(Visitor *v, const char *name,
abort();
default:
assert(visit_is_input(v));
- error_setg(errp, "Invalid parameter type for '%%s', expected: %(name)s",
- name ? name : "null");
+ error_setg(errp,
+ "Invalid parameter type for '%%s', expected: %(name)s",
+ name ? name : "null");
/* Avoid passing invalid *obj to qapi_free_%(c_name)s() */
g_free(*obj);
*obj = NULL;
@@ -359,8 +356,9 @@ class QAPISchemaGenVisitVisitor(QAPISchemaModularCVisitor):
#include "qemu/osdep.h"
#include "qapi/error.h"
#include "%(visit)s.h"
+#include "%(prefix)sqapi-features.h"
''',
- visit=visit))
+ visit=visit, prefix=self._prefix))
self._genh.preamble_add(mcgen('''
#include "qapi/qapi-builtin-visit.h"
#include "%(types)s.h"