2019-10-18 09:43:44 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
#
|
|
|
|
# QAPI schema parser
|
|
|
|
#
|
|
|
|
# Copyright IBM, Corp. 2011
|
|
|
|
# Copyright (c) 2013-2019 Red Hat Inc.
|
|
|
|
#
|
|
|
|
# Authors:
|
|
|
|
# Anthony Liguori <aliguori@us.ibm.com>
|
|
|
|
# Markus Armbruster <armbru@redhat.com>
|
|
|
|
# Marc-André Lureau <marcandre.lureau@redhat.com>
|
|
|
|
# Kevin Wolf <kwolf@redhat.com>
|
|
|
|
#
|
|
|
|
# This work is licensed under the terms of the GNU GPL, version 2.
|
|
|
|
# See the COPYING file in the top-level directory.
|
|
|
|
|
2020-10-09 12:15:29 -04:00
|
|
|
from collections import OrderedDict
|
2019-10-18 09:43:44 +02:00
|
|
|
import os
|
|
|
|
import re
|
2021-05-19 14:39:48 -04:00
|
|
|
from typing import (
|
2021-09-30 16:57:11 -04:00
|
|
|
TYPE_CHECKING,
|
qapi/parser: demote QAPIExpression to Dict[str, Any]
Dict[str, object] is a stricter type, but with the way that code is
currently arranged, it is infeasible to enforce this strictness.
In particular, although expr.py's entire raison d'être is normalization
and type-checking of QAPI Expressions, that type information is not
"remembered" in any meaningful way by mypy because each individual
expression is not downcast to a specific expression type that holds all
the details of each expression's unique form.
As a result, all of the code in schema.py that deals with actually
creating type-safe specialized structures has no guarantee (myopically)
that the data it is being passed is correct.
There are two ways to solve this:
(1) Re-assert that the incoming data is in the shape we expect it to be, or
(2) Disable type checking for this data.
(1) is appealing to my sense of strictness, but I gotta concede that it
is asinine to re-check the shape of a QAPIExpression in schema.py when
expr.py has just completed that work at length. The duplication of code
and the nightmare thought of needing to update both locations if and
when we change the shape of these structures makes me extremely
reluctant to go down this route.
(2) allows us the chance to miss updating types in the case that types
are updated in expr.py, but it *is* an awful lot simpler and,
importantly, gets us closer to type checking schema.py *at
all*. Something is better than nothing, I'd argue.
So, do the simpler dumber thing and worry about future strictness
improvements later.
Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
Message-ID: <20240315152301.3621858-20-armbru@redhat.com>
2024-03-15 16:22:55 +01:00
|
|
|
Any,
|
2021-05-19 14:39:48 -04:00
|
|
|
Dict,
|
|
|
|
List,
|
qapi/parser: add QAPIExpression type
This patch creates a new type, QAPIExpression, which represents a parsed
expression complete with QAPIDoc and QAPISourceInfo.
This patch turns parser.exprs into a list of QAPIExpression instead,
and adjusts expr.py to match.
This allows the types we specify in parser.py to be "remembered" all the
way through expr.py and into schema.py. Several assertions around
packing and unpacking this data can be removed as a result.
It also corrects a harmless typing error. Before the patch,
check_exprs() allegedly takes a List[_JSONObject]. It actually takes
a list of dicts of the form
{'expr': E, 'info': I, 'doc': D}
where E is of type _ExprValue, I is of type QAPISourceInfo, and D is
of type QAPIDoc. Key 'doc' is optional. This is not a _JSONObject!
Passes type checking anyway, because _JSONObject is Dict[str, object].
Signed-off-by: John Snow <jsnow@redhat.com>
Message-Id: <20230215000011.1725012-5-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
[Commit message amended to point out the typing fix]
2023-02-14 19:00:09 -05:00
|
|
|
Mapping,
|
2023-10-04 19:05:32 -04:00
|
|
|
Match,
|
2021-05-19 14:39:48 -04:00
|
|
|
Optional,
|
|
|
|
Set,
|
|
|
|
Union,
|
|
|
|
)
|
2019-10-18 09:43:44 +02:00
|
|
|
|
2021-05-19 14:39:45 -04:00
|
|
|
from .common import must_match
|
2021-04-21 15:22:30 -04:00
|
|
|
from .error import QAPISemError, QAPISourceError
|
qapi: Prefer explicit relative imports
All of the QAPI include statements are changed to be package-aware, as
explicit relative imports.
A quirk of Python packages is that the name of the package exists only
*outside* of the package. This means that to a module inside of the qapi
folder, there is inherently no such thing as the "qapi" package. The
reason these imports work is because the "qapi" package exists in the
context of the caller -- the execution shim, where sys.path includes a
directory that has a 'qapi' folder in it.
When we write "from qapi import sibling", we are NOT referencing the folder
'qapi', but rather "any package named qapi in sys.path". If you should
so happen to have a 'qapi' package in your path, it will use *that*
package.
When we write "from .sibling import foo", we always reference explicitly
our sibling module; guaranteeing consistency in *where* we are importing
these modules from.
This can be useful when working with virtual environments and packages
in development mode. In development mode, a package is installed as a
series of symlinks that forwards to your same source files. The problem
arises because code quality checkers will follow "import qapi.x" to the
"installed" version instead of the sibling file and -- even though they
are the same file -- they have different module paths, and this causes
cyclic import problems, false positive type mismatch errors, and more.
It can also be useful when dealing with hierarchical packages, e.g. if
we allow qemu.core.qmp, qemu.qapi.parser, etc.
Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Eduardo Habkost <ehabkost@redhat.com>
Reviewed-by: Cleber Rosa <crosa@redhat.com>
Reviewed-by: Philippe Mathieu-Daudé <philmd@redhat.com>
Message-Id: <20201009161558.107041-6-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
2020-10-09 12:15:27 -04:00
|
|
|
from .source import QAPISourceInfo
|
2019-10-18 09:43:44 +02:00
|
|
|
|
|
|
|
|
2021-09-30 16:57:11 -04:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
# pylint: disable=cyclic-import
|
|
|
|
# TODO: Remove cycle. [schema -> expr -> parser -> schema]
|
|
|
|
from .schema import QAPISchemaFeature, QAPISchemaMember
|
|
|
|
|
|
|
|
|
2021-05-19 14:39:48 -04:00
|
|
|
# Return value alias for get_expr().
|
|
|
|
_ExprValue = Union[List[object], Dict[str, object], str, bool]
|
|
|
|
|
qapi/parser: add QAPIExpression type
This patch creates a new type, QAPIExpression, which represents a parsed
expression complete with QAPIDoc and QAPISourceInfo.
This patch turns parser.exprs into a list of QAPIExpression instead,
and adjusts expr.py to match.
This allows the types we specify in parser.py to be "remembered" all the
way through expr.py and into schema.py. Several assertions around
packing and unpacking this data can be removed as a result.
It also corrects a harmless typing error. Before the patch,
check_exprs() allegedly takes a List[_JSONObject]. It actually takes
a list of dicts of the form
{'expr': E, 'info': I, 'doc': D}
where E is of type _ExprValue, I is of type QAPISourceInfo, and D is
of type QAPIDoc. Key 'doc' is optional. This is not a _JSONObject!
Passes type checking anyway, because _JSONObject is Dict[str, object].
Signed-off-by: John Snow <jsnow@redhat.com>
Message-Id: <20230215000011.1725012-5-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
[Commit message amended to point out the typing fix]
2023-02-14 19:00:09 -05:00
|
|
|
|
qapi/parser: demote QAPIExpression to Dict[str, Any]
Dict[str, object] is a stricter type, but with the way that code is
currently arranged, it is infeasible to enforce this strictness.
In particular, although expr.py's entire raison d'être is normalization
and type-checking of QAPI Expressions, that type information is not
"remembered" in any meaningful way by mypy because each individual
expression is not downcast to a specific expression type that holds all
the details of each expression's unique form.
As a result, all of the code in schema.py that deals with actually
creating type-safe specialized structures has no guarantee (myopically)
that the data it is being passed is correct.
There are two ways to solve this:
(1) Re-assert that the incoming data is in the shape we expect it to be, or
(2) Disable type checking for this data.
(1) is appealing to my sense of strictness, but I gotta concede that it
is asinine to re-check the shape of a QAPIExpression in schema.py when
expr.py has just completed that work at length. The duplication of code
and the nightmare thought of needing to update both locations if and
when we change the shape of these structures makes me extremely
reluctant to go down this route.
(2) allows us the chance to miss updating types in the case that types
are updated in expr.py, but it *is* an awful lot simpler and,
importantly, gets us closer to type checking schema.py *at
all*. Something is better than nothing, I'd argue.
So, do the simpler dumber thing and worry about future strictness
improvements later.
Signed-off-by: John Snow <jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
Message-ID: <20240315152301.3621858-20-armbru@redhat.com>
2024-03-15 16:22:55 +01:00
|
|
|
class QAPIExpression(Dict[str, Any]):
|
qapi/parser: add QAPIExpression type
This patch creates a new type, QAPIExpression, which represents a parsed
expression complete with QAPIDoc and QAPISourceInfo.
This patch turns parser.exprs into a list of QAPIExpression instead,
and adjusts expr.py to match.
This allows the types we specify in parser.py to be "remembered" all the
way through expr.py and into schema.py. Several assertions around
packing and unpacking this data can be removed as a result.
It also corrects a harmless typing error. Before the patch,
check_exprs() allegedly takes a List[_JSONObject]. It actually takes
a list of dicts of the form
{'expr': E, 'info': I, 'doc': D}
where E is of type _ExprValue, I is of type QAPISourceInfo, and D is
of type QAPIDoc. Key 'doc' is optional. This is not a _JSONObject!
Passes type checking anyway, because _JSONObject is Dict[str, object].
Signed-off-by: John Snow <jsnow@redhat.com>
Message-Id: <20230215000011.1725012-5-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
[Commit message amended to point out the typing fix]
2023-02-14 19:00:09 -05:00
|
|
|
# pylint: disable=too-few-public-methods
|
|
|
|
def __init__(self,
|
|
|
|
data: Mapping[str, object],
|
|
|
|
info: QAPISourceInfo,
|
|
|
|
doc: Optional['QAPIDoc'] = None):
|
|
|
|
super().__init__(data)
|
|
|
|
self.info = info
|
|
|
|
self.doc: Optional['QAPIDoc'] = doc
|
2021-09-30 16:57:13 -04:00
|
|
|
|
2021-05-19 14:39:48 -04:00
|
|
|
|
2021-04-21 15:22:30 -04:00
|
|
|
class QAPIParseError(QAPISourceError):
|
|
|
|
"""Error class for all QAPI schema parsing errors."""
|
2021-05-19 14:39:48 -04:00
|
|
|
def __init__(self, parser: 'QAPISchemaParser', msg: str):
|
2021-04-21 15:22:30 -04:00
|
|
|
col = 1
|
|
|
|
for ch in parser.src[parser.line_pos:parser.pos]:
|
|
|
|
if ch == '\t':
|
|
|
|
col = (col + 7) % 8 + 1
|
|
|
|
else:
|
|
|
|
col += 1
|
|
|
|
super().__init__(parser.info, msg, col)
|
|
|
|
|
|
|
|
|
2020-03-04 16:59:29 +01:00
|
|
|
class QAPISchemaParser:
|
2021-05-19 14:39:51 -04:00
|
|
|
"""
|
|
|
|
Parse QAPI schema source.
|
2019-10-18 09:43:44 +02:00
|
|
|
|
2021-05-19 14:39:51 -04:00
|
|
|
Parse a JSON-esque schema file and process directives. See
|
2024-01-20 10:53:23 +01:00
|
|
|
qapi-code-gen.rst section "Schema Syntax" for the exact syntax.
|
2021-05-19 14:39:51 -04:00
|
|
|
Grammatical validation is handled later by `expr.check_exprs()`.
|
|
|
|
|
|
|
|
:param fname: Source file name.
|
|
|
|
:param previously_included:
|
|
|
|
The absolute names of previously included source files,
|
|
|
|
if being invoked from another parser.
|
|
|
|
:param incl_info:
|
|
|
|
`QAPISourceInfo` belonging to the parent module.
|
|
|
|
``None`` implies this is the root module.
|
|
|
|
|
|
|
|
:ivar exprs: Resulting parsed expressions.
|
|
|
|
:ivar docs: Resulting parsed documentation blocks.
|
|
|
|
|
|
|
|
:raise OSError: For problems reading the root schema document.
|
|
|
|
:raise QAPIError: For errors in the schema source.
|
|
|
|
"""
|
2021-05-19 14:39:48 -04:00
|
|
|
def __init__(self,
|
|
|
|
fname: str,
|
|
|
|
previously_included: Optional[Set[str]] = None,
|
|
|
|
incl_info: Optional[QAPISourceInfo] = None):
|
2021-05-19 14:39:40 -04:00
|
|
|
self._fname = fname
|
|
|
|
self._included = previously_included or set()
|
|
|
|
self._included.add(os.path.abspath(self._fname))
|
|
|
|
self.src = ''
|
2019-10-18 09:43:44 +02:00
|
|
|
|
2021-05-19 14:39:40 -04:00
|
|
|
# Lexer state (see `accept` for details):
|
|
|
|
self.info = QAPISourceInfo(self._fname, incl_info)
|
2021-05-19 14:39:48 -04:00
|
|
|
self.tok: Union[None, str] = None
|
2021-05-19 14:39:40 -04:00
|
|
|
self.pos = 0
|
2019-10-18 09:43:44 +02:00
|
|
|
self.cursor = 0
|
2021-05-19 14:39:48 -04:00
|
|
|
self.val: Optional[Union[bool, str]] = None
|
2019-10-18 09:43:44 +02:00
|
|
|
self.line_pos = 0
|
2021-05-19 14:39:40 -04:00
|
|
|
|
|
|
|
# Parser output:
|
qapi/parser: add QAPIExpression type
This patch creates a new type, QAPIExpression, which represents a parsed
expression complete with QAPIDoc and QAPISourceInfo.
This patch turns parser.exprs into a list of QAPIExpression instead,
and adjusts expr.py to match.
This allows the types we specify in parser.py to be "remembered" all the
way through expr.py and into schema.py. Several assertions around
packing and unpacking this data can be removed as a result.
It also corrects a harmless typing error. Before the patch,
check_exprs() allegedly takes a List[_JSONObject]. It actually takes
a list of dicts of the form
{'expr': E, 'info': I, 'doc': D}
where E is of type _ExprValue, I is of type QAPISourceInfo, and D is
of type QAPIDoc. Key 'doc' is optional. This is not a _JSONObject!
Passes type checking anyway, because _JSONObject is Dict[str, object].
Signed-off-by: John Snow <jsnow@redhat.com>
Message-Id: <20230215000011.1725012-5-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
[Commit message amended to point out the typing fix]
2023-02-14 19:00:09 -05:00
|
|
|
self.exprs: List[QAPIExpression] = []
|
2021-05-19 14:39:48 -04:00
|
|
|
self.docs: List[QAPIDoc] = []
|
2021-05-19 14:39:40 -04:00
|
|
|
|
|
|
|
# Showtime!
|
|
|
|
self._parse()
|
|
|
|
|
2021-05-19 14:39:48 -04:00
|
|
|
def _parse(self) -> None:
|
2021-05-19 14:39:51 -04:00
|
|
|
"""
|
|
|
|
Parse the QAPI schema document.
|
|
|
|
|
|
|
|
:return: None. Results are stored in ``.exprs`` and ``.docs``.
|
|
|
|
"""
|
2019-10-18 09:43:44 +02:00
|
|
|
cur_doc = None
|
|
|
|
|
2021-05-19 14:39:40 -04:00
|
|
|
# May raise OSError; allow the caller to handle it.
|
|
|
|
with open(self._fname, 'r', encoding='utf-8') as fp:
|
|
|
|
self.src = fp.read()
|
|
|
|
if self.src == '' or self.src[-1] != '\n':
|
|
|
|
self.src += '\n'
|
|
|
|
|
|
|
|
# Prime the lexer:
|
|
|
|
self.accept()
|
|
|
|
|
|
|
|
# Parse until done:
|
2019-10-18 09:43:44 +02:00
|
|
|
while self.tok is not None:
|
|
|
|
info = self.info
|
|
|
|
if self.tok == '#':
|
|
|
|
self.reject_expr_doc(cur_doc)
|
2024-02-16 15:58:38 +01:00
|
|
|
cur_doc = self.get_doc()
|
|
|
|
self.docs.append(cur_doc)
|
2019-10-18 09:43:44 +02:00
|
|
|
continue
|
|
|
|
|
2021-05-19 14:39:42 -04:00
|
|
|
expr = self.get_expr()
|
|
|
|
if not isinstance(expr, dict):
|
|
|
|
raise QAPISemError(
|
|
|
|
info, "top-level expression must be an object")
|
|
|
|
|
2019-10-18 09:43:44 +02:00
|
|
|
if 'include' in expr:
|
|
|
|
self.reject_expr_doc(cur_doc)
|
|
|
|
if len(expr) != 1:
|
|
|
|
raise QAPISemError(info, "invalid 'include' directive")
|
|
|
|
include = expr['include']
|
|
|
|
if not isinstance(include, str):
|
|
|
|
raise QAPISemError(info,
|
|
|
|
"value of 'include' must be a string")
|
2021-05-19 14:39:40 -04:00
|
|
|
incl_fname = os.path.join(os.path.dirname(self._fname),
|
2019-10-18 09:43:44 +02:00
|
|
|
include)
|
qapi/parser: add QAPIExpression type
This patch creates a new type, QAPIExpression, which represents a parsed
expression complete with QAPIDoc and QAPISourceInfo.
This patch turns parser.exprs into a list of QAPIExpression instead,
and adjusts expr.py to match.
This allows the types we specify in parser.py to be "remembered" all the
way through expr.py and into schema.py. Several assertions around
packing and unpacking this data can be removed as a result.
It also corrects a harmless typing error. Before the patch,
check_exprs() allegedly takes a List[_JSONObject]. It actually takes
a list of dicts of the form
{'expr': E, 'info': I, 'doc': D}
where E is of type _ExprValue, I is of type QAPISourceInfo, and D is
of type QAPIDoc. Key 'doc' is optional. This is not a _JSONObject!
Passes type checking anyway, because _JSONObject is Dict[str, object].
Signed-off-by: John Snow <jsnow@redhat.com>
Message-Id: <20230215000011.1725012-5-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
[Commit message amended to point out the typing fix]
2023-02-14 19:00:09 -05:00
|
|
|
self._add_expr(OrderedDict({'include': incl_fname}), info)
|
2019-10-18 09:43:44 +02:00
|
|
|
exprs_include = self._include(include, info, incl_fname,
|
2021-05-19 14:39:40 -04:00
|
|
|
self._included)
|
2019-10-18 09:43:44 +02:00
|
|
|
if exprs_include:
|
|
|
|
self.exprs.extend(exprs_include.exprs)
|
|
|
|
self.docs.extend(exprs_include.docs)
|
|
|
|
elif "pragma" in expr:
|
|
|
|
self.reject_expr_doc(cur_doc)
|
|
|
|
if len(expr) != 1:
|
|
|
|
raise QAPISemError(info, "invalid 'pragma' directive")
|
|
|
|
pragma = expr['pragma']
|
|
|
|
if not isinstance(pragma, dict):
|
|
|
|
raise QAPISemError(
|
|
|
|
info, "value of 'pragma' must be an object")
|
|
|
|
for name, value in pragma.items():
|
|
|
|
self._pragma(name, value, info)
|
|
|
|
else:
|
qapi/parser: add QAPIExpression type
This patch creates a new type, QAPIExpression, which represents a parsed
expression complete with QAPIDoc and QAPISourceInfo.
This patch turns parser.exprs into a list of QAPIExpression instead,
and adjusts expr.py to match.
This allows the types we specify in parser.py to be "remembered" all the
way through expr.py and into schema.py. Several assertions around
packing and unpacking this data can be removed as a result.
It also corrects a harmless typing error. Before the patch,
check_exprs() allegedly takes a List[_JSONObject]. It actually takes
a list of dicts of the form
{'expr': E, 'info': I, 'doc': D}
where E is of type _ExprValue, I is of type QAPISourceInfo, and D is
of type QAPIDoc. Key 'doc' is optional. This is not a _JSONObject!
Passes type checking anyway, because _JSONObject is Dict[str, object].
Signed-off-by: John Snow <jsnow@redhat.com>
Message-Id: <20230215000011.1725012-5-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
[Commit message amended to point out the typing fix]
2023-02-14 19:00:09 -05:00
|
|
|
if cur_doc and not cur_doc.symbol:
|
|
|
|
raise QAPISemError(
|
|
|
|
cur_doc.info, "definition documentation required")
|
|
|
|
self._add_expr(expr, info, cur_doc)
|
2019-10-18 09:43:44 +02:00
|
|
|
cur_doc = None
|
|
|
|
self.reject_expr_doc(cur_doc)
|
|
|
|
|
qapi/parser: add QAPIExpression type
This patch creates a new type, QAPIExpression, which represents a parsed
expression complete with QAPIDoc and QAPISourceInfo.
This patch turns parser.exprs into a list of QAPIExpression instead,
and adjusts expr.py to match.
This allows the types we specify in parser.py to be "remembered" all the
way through expr.py and into schema.py. Several assertions around
packing and unpacking this data can be removed as a result.
It also corrects a harmless typing error. Before the patch,
check_exprs() allegedly takes a List[_JSONObject]. It actually takes
a list of dicts of the form
{'expr': E, 'info': I, 'doc': D}
where E is of type _ExprValue, I is of type QAPISourceInfo, and D is
of type QAPIDoc. Key 'doc' is optional. This is not a _JSONObject!
Passes type checking anyway, because _JSONObject is Dict[str, object].
Signed-off-by: John Snow <jsnow@redhat.com>
Message-Id: <20230215000011.1725012-5-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
[Commit message amended to point out the typing fix]
2023-02-14 19:00:09 -05:00
|
|
|
def _add_expr(self, expr: Mapping[str, object],
|
|
|
|
info: QAPISourceInfo,
|
|
|
|
doc: Optional['QAPIDoc'] = None) -> None:
|
|
|
|
self.exprs.append(QAPIExpression(expr, info, doc))
|
|
|
|
|
2019-10-18 09:43:44 +02:00
|
|
|
@staticmethod
|
2021-05-19 14:39:48 -04:00
|
|
|
def reject_expr_doc(doc: Optional['QAPIDoc']) -> None:
|
2019-10-18 09:43:44 +02:00
|
|
|
if doc and doc.symbol:
|
|
|
|
raise QAPISemError(
|
|
|
|
doc.info,
|
|
|
|
"documentation for '%s' is not followed by the definition"
|
|
|
|
% doc.symbol)
|
|
|
|
|
2021-05-19 14:39:44 -04:00
|
|
|
@staticmethod
|
2021-05-19 14:39:48 -04:00
|
|
|
def _include(include: str,
|
|
|
|
info: QAPISourceInfo,
|
|
|
|
incl_fname: str,
|
|
|
|
previously_included: Set[str]
|
|
|
|
) -> Optional['QAPISchemaParser']:
|
2019-10-18 09:43:44 +02:00
|
|
|
incl_abs_fname = os.path.abspath(incl_fname)
|
|
|
|
# catch inclusion cycle
|
2021-05-19 14:39:48 -04:00
|
|
|
inf: Optional[QAPISourceInfo] = info
|
2019-10-18 09:43:44 +02:00
|
|
|
while inf:
|
|
|
|
if incl_abs_fname == os.path.abspath(inf.fname):
|
|
|
|
raise QAPISemError(info, "inclusion loop for %s" % include)
|
|
|
|
inf = inf.parent
|
|
|
|
|
|
|
|
# skip multiple include of the same file
|
|
|
|
if incl_abs_fname in previously_included:
|
|
|
|
return None
|
|
|
|
|
qapi/parser: Don't try to handle file errors
Fixes: f5d4361cda
Fixes: 52a474180a
Fixes: 46f49468c6
Remove the try/except block that handles file-opening errors in
QAPISchemaParser.__init__() and add one each to
QAPISchemaParser._include() and QAPISchema.__init__() respectively.
This simultaneously fixes the typing of info.fname (f5d4361cda), A
static typing violation in test-qapi (46f49468c6), and a regression of
an error message (52a474180a).
The short-ish version of what motivates this patch is:
- It's hard to write a good error message in the init method,
because we need to determine the context of our caller to do so.
It's easier to just let the caller write the message.
- We don't want to allow QAPISourceInfo(None, None, None) to exist. The
typing introduced by commit f5d4361cda types the 'fname' field as
(non-optional) str, which was premature until the removal of this
construct.
- Errors made using such an object are currently incorrect (since
52a474180a)
- It's not technically a semantic error if we cannot open the schema.
- There are various typing constraints that make mixing these two cases
undesirable for a single special case.
- test-qapi's code handling an fname of 'None' is now dead, drop it.
Additionally, Not all QAPIError objects have an 'info' field (since
46f49468), so deleting this stanza corrects a typing oversight in
test-qapi introduced by that commit.
Other considerations:
- open() is moved to a 'with' block to ensure file pointers are
cleaned up deterministically.
- Python 3.3 deprecated IOError and made it a synonym for OSError.
Avoid the misleading perception these exception handlers are
narrower than they really are.
The long version:
The error message here is incorrect (since commit 52a474180a):
> python3 qapi-gen.py 'fake.json'
qapi-gen.py: qapi-gen.py: can't read schema file 'fake.json': No such file or directory
In pursuing it, we find that QAPISourceInfo has a special accommodation
for when there's no filename. Meanwhile, the intent when QAPISourceInfo
was typed (f5d4361cda) was non-optional 'str'. This usage was
overlooked.
To remove this, I'd want to avoid having a "fake" QAPISourceInfo
object. I also don't want to explicitly begin accommodating
QAPISourceInfo itself being None, because we actually want to eventually
prove that this can never happen -- We don't want to confuse "The file
isn't open yet" with "This error stems from a definition that wasn't
defined in any file".
(An earlier series tried to create a dummy info object, but it was tough
to prove in review that it worked correctly without creating new
regressions. This patch avoids that distraction. We would like to first
prove that we never raise QAPISemError for any built-in object before we
add "special" info objects. We aren't ready to do that yet.)
So, which way out of the labyrinth?
Here's one way: Don't try to handle errors at a level with "mixed"
semantic contexts; i.e. don't mix inclusion errors (should report a
source line where the include was triggered) and command line errors
(where we specified a file we couldn't read).
Remove the error handling from the initializer of the parser. Pythonic!
Now it's the caller's job to figure out what to do about it. Handle the
error in QAPISchemaParser._include() instead, where we can write a
targeted error message where we are guaranteed to have an 'info' context
to report with.
The root level error can similarly move to QAPISchema.__init__(), where
we know we'll never have an info context to report with, so we use a
more abstract error type.
Now the error looks sensible again:
> python3 qapi-gen.py 'fake.json'
qapi-gen.py: can't read schema file 'fake.json': No such file or directory
With these error cases separated, QAPISourceInfo can be solidified as
never having placeholder arguments that violate our desired types. Clean
up test-qapi along similar lines.
Signed-off-by: John Snow <jsnow@redhat.com>
Message-Id: <20210519183951.3946870-2-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
2021-05-19 14:39:37 -04:00
|
|
|
try:
|
|
|
|
return QAPISchemaParser(incl_fname, previously_included, info)
|
|
|
|
except OSError as err:
|
|
|
|
raise QAPISemError(
|
|
|
|
info,
|
|
|
|
f"can't read include file '{incl_fname}': {err.strerror}"
|
|
|
|
) from err
|
2019-10-18 09:43:44 +02:00
|
|
|
|
2021-05-19 14:39:44 -04:00
|
|
|
@staticmethod
|
2021-05-19 14:39:48 -04:00
|
|
|
def _pragma(name: str, value: object, info: QAPISourceInfo) -> None:
|
2021-05-19 14:39:47 -04:00
|
|
|
|
2021-05-19 14:39:48 -04:00
|
|
|
def check_list_str(name: str, value: object) -> List[str]:
|
2021-05-19 14:39:47 -04:00
|
|
|
if (not isinstance(value, list) or
|
2021-05-19 14:39:49 -04:00
|
|
|
any(not isinstance(elt, str) for elt in value)):
|
2021-05-19 14:39:47 -04:00
|
|
|
raise QAPISemError(
|
|
|
|
info,
|
|
|
|
"pragma %s must be a list of strings" % name)
|
|
|
|
return value
|
|
|
|
|
|
|
|
pragma = info.pragma
|
2021-03-23 10:40:13 +01:00
|
|
|
|
2019-10-18 09:43:44 +02:00
|
|
|
if name == 'doc-required':
|
|
|
|
if not isinstance(value, bool):
|
|
|
|
raise QAPISemError(info,
|
|
|
|
"pragma 'doc-required' must be boolean")
|
2021-05-19 14:39:47 -04:00
|
|
|
pragma.doc_required = value
|
2021-03-23 10:40:21 +01:00
|
|
|
elif name == 'command-name-exceptions':
|
2021-05-19 14:39:47 -04:00
|
|
|
pragma.command_name_exceptions = check_list_str(name, value)
|
2021-03-23 10:40:16 +01:00
|
|
|
elif name == 'command-returns-exceptions':
|
2021-05-19 14:39:47 -04:00
|
|
|
pragma.command_returns_exceptions = check_list_str(name, value)
|
qapi: Require member documentation (with loophole)
The QAPI generator forces you to document your stuff. Except for
command arguments, event data, and members of enum and object types:
these the generator silently "documents" as "Not documented".
We can't require proper documentation there without first fixing all
the offenders. We've always had too many offenders to pull that off.
Right now, we have more than 500. Worse, we seem to fix old ones no
faster than we add new ones: in the past year, we fixed 22 ones, but
added 26 new ones.
To help arrest the backsliding, make missing documentation an error
unless the command, type, or event is in listed in new pragma
documentation-exceptions.
List all the current offenders: 117 commands and types in qapi/, and 9
in qga/.
Signed-off-by: Markus Armbruster <armbru@redhat.com>
Message-ID: <20240205074709.3613229-7-armbru@redhat.com>
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
2024-02-05 08:47:00 +01:00
|
|
|
elif name == 'documentation-exceptions':
|
|
|
|
pragma.documentation_exceptions = check_list_str(name, value)
|
2021-03-23 10:40:16 +01:00
|
|
|
elif name == 'member-name-exceptions':
|
2021-05-19 14:39:47 -04:00
|
|
|
pragma.member_name_exceptions = check_list_str(name, value)
|
2019-10-18 09:43:44 +02:00
|
|
|
else:
|
|
|
|
raise QAPISemError(info, "unknown pragma '%s'" % name)
|
|
|
|
|
2021-05-19 14:39:48 -04:00
|
|
|
def accept(self, skip_comment: bool = True) -> None:
|
2021-05-19 14:39:51 -04:00
|
|
|
"""
|
|
|
|
Read and store the next token.
|
|
|
|
|
|
|
|
:param skip_comment:
|
|
|
|
When false, return COMMENT tokens ("#").
|
|
|
|
This is used when reading documentation blocks.
|
|
|
|
|
|
|
|
:return:
|
|
|
|
None. Several instance attributes are updated instead:
|
|
|
|
|
|
|
|
- ``.tok`` represents the token type. See below for values.
|
|
|
|
- ``.info`` describes the token's source location.
|
|
|
|
- ``.val`` is the token's value, if any. See below.
|
|
|
|
- ``.pos`` is the buffer index of the first character of
|
|
|
|
the token.
|
|
|
|
|
|
|
|
* Single-character tokens:
|
|
|
|
|
|
|
|
These are "{", "}", ":", ",", "[", and "]".
|
|
|
|
``.tok`` holds the single character and ``.val`` is None.
|
|
|
|
|
|
|
|
* Multi-character tokens:
|
|
|
|
|
|
|
|
* COMMENT:
|
|
|
|
|
|
|
|
This token is not normally returned by the lexer, but it can
|
|
|
|
be when ``skip_comment`` is False. ``.tok`` is "#", and
|
|
|
|
``.val`` is a string including all chars until end-of-line,
|
|
|
|
including the "#" itself.
|
|
|
|
|
|
|
|
* STRING:
|
|
|
|
|
|
|
|
``.tok`` is "'", the single quote. ``.val`` contains the
|
|
|
|
string, excluding the surrounding quotes.
|
|
|
|
|
|
|
|
* TRUE and FALSE:
|
|
|
|
|
|
|
|
``.tok`` is either "t" or "f", ``.val`` will be the
|
|
|
|
corresponding bool value.
|
|
|
|
|
|
|
|
* EOF:
|
|
|
|
|
|
|
|
``.tok`` and ``.val`` will both be None at EOF.
|
|
|
|
"""
|
2019-10-18 09:43:44 +02:00
|
|
|
while True:
|
|
|
|
self.tok = self.src[self.cursor]
|
|
|
|
self.pos = self.cursor
|
|
|
|
self.cursor += 1
|
|
|
|
self.val = None
|
|
|
|
|
|
|
|
if self.tok == '#':
|
|
|
|
if self.src[self.cursor] == '#':
|
|
|
|
# Start of doc comment
|
|
|
|
skip_comment = False
|
|
|
|
self.cursor = self.src.find('\n', self.cursor)
|
|
|
|
if not skip_comment:
|
|
|
|
self.val = self.src[self.pos:self.cursor]
|
|
|
|
return
|
|
|
|
elif self.tok in '{}:,[]':
|
|
|
|
return
|
|
|
|
elif self.tok == "'":
|
|
|
|
# Note: we accept only printable ASCII
|
|
|
|
string = ''
|
|
|
|
esc = False
|
|
|
|
while True:
|
|
|
|
ch = self.src[self.cursor]
|
|
|
|
self.cursor += 1
|
|
|
|
if ch == '\n':
|
|
|
|
raise QAPIParseError(self, "missing terminating \"'\"")
|
|
|
|
if esc:
|
|
|
|
# Note: we recognize only \\ because we have
|
|
|
|
# no use for funny characters in strings
|
|
|
|
if ch != '\\':
|
|
|
|
raise QAPIParseError(self,
|
|
|
|
"unknown escape \\%s" % ch)
|
|
|
|
esc = False
|
|
|
|
elif ch == '\\':
|
|
|
|
esc = True
|
|
|
|
continue
|
|
|
|
elif ch == "'":
|
|
|
|
self.val = string
|
|
|
|
return
|
|
|
|
if ord(ch) < 32 or ord(ch) >= 127:
|
|
|
|
raise QAPIParseError(
|
|
|
|
self, "funny character in string")
|
|
|
|
string += ch
|
|
|
|
elif self.src.startswith('true', self.pos):
|
|
|
|
self.val = True
|
|
|
|
self.cursor += 3
|
|
|
|
return
|
|
|
|
elif self.src.startswith('false', self.pos):
|
|
|
|
self.val = False
|
|
|
|
self.cursor += 4
|
|
|
|
return
|
|
|
|
elif self.tok == '\n':
|
|
|
|
if self.cursor == len(self.src):
|
|
|
|
self.tok = None
|
|
|
|
return
|
|
|
|
self.info = self.info.next_line()
|
|
|
|
self.line_pos = self.cursor
|
|
|
|
elif not self.tok.isspace():
|
|
|
|
# Show up to next structural, whitespace or quote
|
|
|
|
# character
|
2023-04-28 12:54:15 +02:00
|
|
|
match = must_match('[^[\\]{}:,\\s\']+',
|
2021-05-19 14:39:45 -04:00
|
|
|
self.src[self.cursor-1:])
|
2019-10-18 09:43:44 +02:00
|
|
|
raise QAPIParseError(self, "stray '%s'" % match.group(0))
|
|
|
|
|
2021-05-19 14:39:48 -04:00
|
|
|
def get_members(self) -> Dict[str, object]:
|
|
|
|
expr: Dict[str, object] = OrderedDict()
|
2019-10-18 09:43:44 +02:00
|
|
|
if self.tok == '}':
|
|
|
|
self.accept()
|
|
|
|
return expr
|
|
|
|
if self.tok != "'":
|
|
|
|
raise QAPIParseError(self, "expected string or '}'")
|
|
|
|
while True:
|
|
|
|
key = self.val
|
2021-05-19 14:39:43 -04:00
|
|
|
assert isinstance(key, str) # Guaranteed by tok == "'"
|
|
|
|
|
2019-10-18 09:43:44 +02:00
|
|
|
self.accept()
|
|
|
|
if self.tok != ':':
|
|
|
|
raise QAPIParseError(self, "expected ':'")
|
|
|
|
self.accept()
|
|
|
|
if key in expr:
|
|
|
|
raise QAPIParseError(self, "duplicate key '%s'" % key)
|
2021-05-19 14:39:42 -04:00
|
|
|
expr[key] = self.get_expr()
|
2019-10-18 09:43:44 +02:00
|
|
|
if self.tok == '}':
|
|
|
|
self.accept()
|
|
|
|
return expr
|
|
|
|
if self.tok != ',':
|
|
|
|
raise QAPIParseError(self, "expected ',' or '}'")
|
|
|
|
self.accept()
|
|
|
|
if self.tok != "'":
|
|
|
|
raise QAPIParseError(self, "expected string")
|
|
|
|
|
2021-05-19 14:39:48 -04:00
|
|
|
def get_values(self) -> List[object]:
|
|
|
|
expr: List[object] = []
|
2019-10-18 09:43:44 +02:00
|
|
|
if self.tok == ']':
|
|
|
|
self.accept()
|
|
|
|
return expr
|
2021-05-19 14:39:46 -04:00
|
|
|
if self.tok not in tuple("{['tf"):
|
2019-10-18 09:43:44 +02:00
|
|
|
raise QAPIParseError(
|
2021-02-24 11:14:42 +01:00
|
|
|
self, "expected '{', '[', ']', string, or boolean")
|
2019-10-18 09:43:44 +02:00
|
|
|
while True:
|
2021-05-19 14:39:42 -04:00
|
|
|
expr.append(self.get_expr())
|
2019-10-18 09:43:44 +02:00
|
|
|
if self.tok == ']':
|
|
|
|
self.accept()
|
|
|
|
return expr
|
|
|
|
if self.tok != ',':
|
|
|
|
raise QAPIParseError(self, "expected ',' or ']'")
|
|
|
|
self.accept()
|
|
|
|
|
2021-05-19 14:39:48 -04:00
|
|
|
def get_expr(self) -> _ExprValue:
|
|
|
|
expr: _ExprValue
|
2019-10-18 09:43:44 +02:00
|
|
|
if self.tok == '{':
|
|
|
|
self.accept()
|
|
|
|
expr = self.get_members()
|
|
|
|
elif self.tok == '[':
|
|
|
|
self.accept()
|
|
|
|
expr = self.get_values()
|
2021-05-19 14:39:46 -04:00
|
|
|
elif self.tok in tuple("'tf"):
|
|
|
|
assert isinstance(self.val, (str, bool))
|
2019-10-18 09:43:44 +02:00
|
|
|
expr = self.val
|
|
|
|
self.accept()
|
|
|
|
else:
|
|
|
|
raise QAPIParseError(
|
2021-02-24 11:14:42 +01:00
|
|
|
self, "expected '{', '[', string, or boolean")
|
2019-10-18 09:43:44 +02:00
|
|
|
return expr
|
|
|
|
|
2024-02-16 15:58:38 +01:00
|
|
|
def get_doc_line(self) -> Optional[str]:
|
|
|
|
if self.tok != '#':
|
|
|
|
raise QAPIParseError(
|
|
|
|
self, "documentation comment must end with '##'")
|
|
|
|
assert isinstance(self.val, str)
|
|
|
|
if self.val.startswith('##'):
|
|
|
|
# End of doc comment
|
|
|
|
if self.val != '##':
|
|
|
|
raise QAPIParseError(
|
|
|
|
self, "junk after '##' at end of documentation comment")
|
|
|
|
return None
|
|
|
|
if self.val == '#':
|
|
|
|
return ''
|
|
|
|
if self.val[1] != ' ':
|
|
|
|
raise QAPIParseError(self, "missing space after #")
|
|
|
|
return self.val[2:].rstrip()
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _match_at_name_colon(string: str) -> Optional[Match[str]]:
|
|
|
|
return re.match(r'@([^:]*): *', string)
|
|
|
|
|
|
|
|
def get_doc_indented(self, doc: 'QAPIDoc') -> Optional[str]:
|
|
|
|
self.accept(False)
|
|
|
|
line = self.get_doc_line()
|
|
|
|
while line == '':
|
|
|
|
doc.append_line(line)
|
|
|
|
self.accept(False)
|
|
|
|
line = self.get_doc_line()
|
|
|
|
if line is None:
|
|
|
|
return line
|
|
|
|
indent = must_match(r'\s*', line).end()
|
|
|
|
if not indent:
|
|
|
|
return line
|
2024-06-26 18:21:11 -04:00
|
|
|
doc.append_line(line)
|
2024-02-16 15:58:38 +01:00
|
|
|
prev_line_blank = False
|
|
|
|
while True:
|
|
|
|
self.accept(False)
|
|
|
|
line = self.get_doc_line()
|
|
|
|
if line is None:
|
|
|
|
return line
|
|
|
|
if self._match_at_name_colon(line):
|
|
|
|
return line
|
|
|
|
cur_indent = must_match(r'\s*', line).end()
|
|
|
|
if line != '' and cur_indent < indent:
|
|
|
|
if prev_line_blank:
|
|
|
|
return line
|
|
|
|
raise QAPIParseError(
|
|
|
|
self,
|
|
|
|
"unexpected de-indent (expected at least %d spaces)" %
|
|
|
|
indent)
|
2024-06-26 18:21:11 -04:00
|
|
|
doc.append_line(line)
|
2024-02-16 15:58:38 +01:00
|
|
|
prev_line_blank = True
|
|
|
|
|
|
|
|
def get_doc_paragraph(self, doc: 'QAPIDoc') -> Optional[str]:
|
|
|
|
while True:
|
|
|
|
self.accept(False)
|
|
|
|
line = self.get_doc_line()
|
|
|
|
if line is None:
|
|
|
|
return line
|
|
|
|
if line == '':
|
|
|
|
return line
|
|
|
|
doc.append_line(line)
|
|
|
|
|
|
|
|
def get_doc(self) -> 'QAPIDoc':
|
2019-10-18 09:43:44 +02:00
|
|
|
if self.val != '##':
|
|
|
|
raise QAPIParseError(
|
|
|
|
self, "junk after '##' at start of documentation comment")
|
2024-02-16 15:58:38 +01:00
|
|
|
info = self.info
|
2019-10-18 09:43:44 +02:00
|
|
|
self.accept(False)
|
2024-02-16 15:58:38 +01:00
|
|
|
line = self.get_doc_line()
|
|
|
|
if line is not None and line.startswith('@'):
|
|
|
|
# Definition documentation
|
|
|
|
if not line.endswith(':'):
|
|
|
|
raise QAPIParseError(self, "line should end with ':'")
|
|
|
|
# Invalid names are not checked here, but the name
|
|
|
|
# provided *must* match the following definition,
|
|
|
|
# which *is* validated in expr.py.
|
|
|
|
symbol = line[1:-1]
|
|
|
|
if not symbol:
|
|
|
|
raise QAPIParseError(self, "name required after '@'")
|
2024-02-16 15:58:40 +01:00
|
|
|
doc = QAPIDoc(info, symbol)
|
2024-02-16 15:58:38 +01:00
|
|
|
self.accept(False)
|
|
|
|
line = self.get_doc_line()
|
|
|
|
no_more_args = False
|
|
|
|
|
|
|
|
while line is not None:
|
|
|
|
# Blank lines
|
|
|
|
while line == '':
|
|
|
|
self.accept(False)
|
|
|
|
line = self.get_doc_line()
|
|
|
|
if line is None:
|
|
|
|
break
|
|
|
|
# Non-blank line, first of a section
|
2024-02-16 15:58:39 +01:00
|
|
|
if line == 'Features:':
|
|
|
|
if doc.features:
|
|
|
|
raise QAPIParseError(
|
|
|
|
self, "duplicated 'Features:' line")
|
2024-02-16 15:58:38 +01:00
|
|
|
self.accept(False)
|
|
|
|
line = self.get_doc_line()
|
|
|
|
while line == '':
|
|
|
|
self.accept(False)
|
|
|
|
line = self.get_doc_line()
|
|
|
|
while (line is not None
|
|
|
|
and (match := self._match_at_name_colon(line))):
|
2024-02-16 15:58:40 +01:00
|
|
|
doc.new_feature(self.info, match.group(1))
|
2024-02-16 15:58:38 +01:00
|
|
|
text = line[match.end():]
|
|
|
|
if text:
|
|
|
|
doc.append_line(text)
|
|
|
|
line = self.get_doc_indented(doc)
|
2024-02-16 15:58:39 +01:00
|
|
|
if not doc.features:
|
|
|
|
raise QAPIParseError(
|
|
|
|
self, 'feature descriptions expected')
|
2024-02-16 15:58:38 +01:00
|
|
|
no_more_args = True
|
|
|
|
elif match := self._match_at_name_colon(line):
|
|
|
|
# description
|
|
|
|
if no_more_args:
|
|
|
|
raise QAPIParseError(
|
|
|
|
self,
|
|
|
|
"description of '@%s:' follows a section"
|
|
|
|
% match.group(1))
|
|
|
|
while (line is not None
|
|
|
|
and (match := self._match_at_name_colon(line))):
|
2024-02-16 15:58:40 +01:00
|
|
|
doc.new_argument(self.info, match.group(1))
|
2024-02-16 15:58:38 +01:00
|
|
|
text = line[match.end():]
|
|
|
|
if text:
|
|
|
|
doc.append_line(text)
|
|
|
|
line = self.get_doc_indented(doc)
|
|
|
|
no_more_args = True
|
|
|
|
elif match := re.match(
|
2024-06-26 18:21:19 -04:00
|
|
|
r'(Returns|Errors|Since|Notes?|Examples?|TODO)'
|
|
|
|
r'(?!::): *',
|
|
|
|
line,
|
|
|
|
):
|
2024-02-16 15:58:38 +01:00
|
|
|
# tagged section
|
qapi: convert "Note" sections to plain rST
We do not need a dedicated section for notes. By eliminating a specially
parsed section, these notes can be treated as normal rST paragraphs in
the new QMP reference manual, and can be placed and styled much more
flexibly.
Convert all existing "Note" and "Notes" sections to pure rST. As part of
the conversion, capitalize the first letter of each sentence and add
trailing punctuation where appropriate to ensure notes look sensible and
consistent in rendered HTML documentation. Markup is also re-aligned to
the de-facto standard of 3 spaces for directives.
Update docs/devel/qapi-code-gen.rst to reflect the new paradigm, and
update the QAPI parser to prohibit "Note" sections while suggesting a
new syntax. The exact formatting to use is a matter of taste, but a good
candidate is simply:
.. note:: lorem ipsum ...
... dolor sit amet ...
... consectetur adipiscing elit ...
... but there are other choices, too. The Sphinx readthedocs theme
offers theming for the following forms (capitalization unimportant); all
are adorned with a (!) symbol () in the title bar for rendered HTML
docs.
See
https://sphinx-rtd-theme.readthedocs.io/en/stable/demo/demo.html#admonitions
for examples of each directive/admonition in use.
These are rendered in orange:
.. Attention:: ...
.. Caution:: ...
.. WARNING:: ...
These are rendered in red:
.. DANGER:: ...
.. Error:: ...
These are rendered in green:
.. Hint:: ...
.. Important:: ...
.. Tip:: ...
These are rendered in blue:
.. Note:: ...
.. admonition:: custom title
admonition body text
This patch uses ".. note::" almost everywhere, with just two "caution"
directives. Several instances of "Notes:" have been converted to
merely ".. note::", or multiple ".. note::" where appropriate.
".. admonition:: notes" is used in a few places where we had an
ordered list of multiple notes that would not make sense as
standalone/separate admonitions. Two "Note:" following "Example:"
have been turned into ordinary paragraphs within the example.
NOTE: Because qapidoc.py does not attempt to preserve source ordering of
sections, the conversion of Notes from a "tagged section" to an
"untagged section" means that rendering order for some notes *may
change* as a result of this patch. The forthcoming qapidoc.py rewrite
strictly preserves source ordering in the rendered documentation, so
this issue will be rectified in the new generator.
Signed-off-by: John Snow <jsnow@redhat.com>
Acked-by: Stefan Hajnoczi <stefanha@redhat.com> [for block*.json]
Message-ID: <20240626222128.406106-11-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
[Commit message clarified slightly, period added to one more note]
Signed-off-by: Markus Armbruster <armbru@redhat.com>
2024-06-26 18:21:16 -04:00
|
|
|
|
2024-06-26 18:21:19 -04:00
|
|
|
# Note: "sections" with two colons are left alone as
|
|
|
|
# rST markup and not interpreted as a section heading.
|
|
|
|
|
qapi: remove "Example" doc section
Fully eliminate the "Example" sections in QAPI doc blocks now that they
have all been converted to arbitrary rST syntax using the
".. qmp-example::" directive. Update tests to match.
Migrating to the new syntax
---------------------------
The old "Example:" or "Examples:" section syntax is now caught as an
error, but "Example::" is stil permitted as explicit rST syntax for an
un-lexed, generic preformatted text block.
('Example' is not special in this case, any sentence that ends with "::"
will start an indented code block in rST.)
Arbitrary rST for Examples is now possible, but it's strongly
recommended that documentation authors use the ".. qmp-example::"
directive for consistent visual formatting in rendered HTML docs. The
":title:" directive option may be used to add extra information into the
title bar for the example. The ":annotated:" option can be used to write
arbitrary rST instead, with nested "::" blocks applying QMP formatting
where desired.
Other choices available are ".. code-block:: QMP" which will not create
an "Example:" box, or the short-form "::" code-block syntax which will
not apply QMP highlighting when used outside of the qmp-example
directive.
Why?
----
This patch has several benefits:
1. Example sections can now be written more arbitrarily, mixing
explanatory paragraphs and code blocks however desired.
2. Example sections can now use fully arbitrary rST.
3. All code blocks are now lexed and validated as QMP; increasing
usability of the docs and ensuring validity of example snippets.
(To some extent - This patch only gaurantees it lexes correctly, not
that it's valid under the JSON or QMP grammars. It will catch most
small mistakes, however.)
4. Each qmp-example can be titled or annotated independently without
bypassing the QMP lexer/validator.
(i.e. code blocks are now for *code* only, so we don't have to
sacrifice exposition for having lexically valid examples.)
NOTE: As with the "Notes" conversion (d461c279737), this patch (and the
three preceding) may change the rendering order for Examples in
the current generator. The forthcoming qapidoc rewrite will fix
this by always generating documentation in source order.
Signed-off-by: John Snow <jsnow@redhat.com>
Message-ID: <20240717021312.606116-10-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
2024-07-16 22:13:11 -04:00
|
|
|
# TODO: Remove these errors sometime in 2025 or so
|
qapi: convert "Note" sections to plain rST
We do not need a dedicated section for notes. By eliminating a specially
parsed section, these notes can be treated as normal rST paragraphs in
the new QMP reference manual, and can be placed and styled much more
flexibly.
Convert all existing "Note" and "Notes" sections to pure rST. As part of
the conversion, capitalize the first letter of each sentence and add
trailing punctuation where appropriate to ensure notes look sensible and
consistent in rendered HTML documentation. Markup is also re-aligned to
the de-facto standard of 3 spaces for directives.
Update docs/devel/qapi-code-gen.rst to reflect the new paradigm, and
update the QAPI parser to prohibit "Note" sections while suggesting a
new syntax. The exact formatting to use is a matter of taste, but a good
candidate is simply:
.. note:: lorem ipsum ...
... dolor sit amet ...
... consectetur adipiscing elit ...
... but there are other choices, too. The Sphinx readthedocs theme
offers theming for the following forms (capitalization unimportant); all
are adorned with a (!) symbol () in the title bar for rendered HTML
docs.
See
https://sphinx-rtd-theme.readthedocs.io/en/stable/demo/demo.html#admonitions
for examples of each directive/admonition in use.
These are rendered in orange:
.. Attention:: ...
.. Caution:: ...
.. WARNING:: ...
These are rendered in red:
.. DANGER:: ...
.. Error:: ...
These are rendered in green:
.. Hint:: ...
.. Important:: ...
.. Tip:: ...
These are rendered in blue:
.. Note:: ...
.. admonition:: custom title
admonition body text
This patch uses ".. note::" almost everywhere, with just two "caution"
directives. Several instances of "Notes:" have been converted to
merely ".. note::", or multiple ".. note::" where appropriate.
".. admonition:: notes" is used in a few places where we had an
ordered list of multiple notes that would not make sense as
standalone/separate admonitions. Two "Note:" following "Example:"
have been turned into ordinary paragraphs within the example.
NOTE: Because qapidoc.py does not attempt to preserve source ordering of
sections, the conversion of Notes from a "tagged section" to an
"untagged section" means that rendering order for some notes *may
change* as a result of this patch. The forthcoming qapidoc.py rewrite
strictly preserves source ordering in the rendered documentation, so
this issue will be rectified in the new generator.
Signed-off-by: John Snow <jsnow@redhat.com>
Acked-by: Stefan Hajnoczi <stefanha@redhat.com> [for block*.json]
Message-ID: <20240626222128.406106-11-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
[Commit message clarified slightly, period added to one more note]
Signed-off-by: Markus Armbruster <armbru@redhat.com>
2024-06-26 18:21:16 -04:00
|
|
|
# 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)
|
|
|
|
|
qapi: remove "Example" doc section
Fully eliminate the "Example" sections in QAPI doc blocks now that they
have all been converted to arbitrary rST syntax using the
".. qmp-example::" directive. Update tests to match.
Migrating to the new syntax
---------------------------
The old "Example:" or "Examples:" section syntax is now caught as an
error, but "Example::" is stil permitted as explicit rST syntax for an
un-lexed, generic preformatted text block.
('Example' is not special in this case, any sentence that ends with "::"
will start an indented code block in rST.)
Arbitrary rST for Examples is now possible, but it's strongly
recommended that documentation authors use the ".. qmp-example::"
directive for consistent visual formatting in rendered HTML docs. The
":title:" directive option may be used to add extra information into the
title bar for the example. The ":annotated:" option can be used to write
arbitrary rST instead, with nested "::" blocks applying QMP formatting
where desired.
Other choices available are ".. code-block:: QMP" which will not create
an "Example:" box, or the short-form "::" code-block syntax which will
not apply QMP highlighting when used outside of the qmp-example
directive.
Why?
----
This patch has several benefits:
1. Example sections can now be written more arbitrarily, mixing
explanatory paragraphs and code blocks however desired.
2. Example sections can now use fully arbitrary rST.
3. All code blocks are now lexed and validated as QMP; increasing
usability of the docs and ensuring validity of example snippets.
(To some extent - This patch only gaurantees it lexes correctly, not
that it's valid under the JSON or QMP grammars. It will catch most
small mistakes, however.)
4. Each qmp-example can be titled or annotated independently without
bypassing the QMP lexer/validator.
(i.e. code blocks are now for *code* only, so we don't have to
sacrifice exposition for having lexically valid examples.)
NOTE: As with the "Notes" conversion (d461c279737), this patch (and the
three preceding) may change the rendering order for Examples in
the current generator. The forthcoming qapidoc rewrite will fix
this by always generating documentation in source order.
Signed-off-by: John Snow <jsnow@redhat.com>
Message-ID: <20240717021312.606116-10-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
2024-07-16 22:13:11 -04:00
|
|
|
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)
|
|
|
|
|
2024-02-16 15:58:40 +01:00
|
|
|
doc.new_tagged_section(self.info, match.group(1))
|
2024-02-16 15:58:38 +01:00
|
|
|
text = line[match.end():]
|
|
|
|
if text:
|
|
|
|
doc.append_line(text)
|
|
|
|
line = self.get_doc_indented(doc)
|
|
|
|
no_more_args = True
|
|
|
|
elif line.startswith('='):
|
2020-03-20 10:18:04 +01:00
|
|
|
raise QAPIParseError(
|
|
|
|
self,
|
|
|
|
"unexpected '=' markup in definition documentation")
|
2024-02-16 15:58:38 +01:00
|
|
|
else:
|
|
|
|
# tag-less paragraph
|
2024-02-16 15:58:40 +01:00
|
|
|
doc.ensure_untagged_section(self.info)
|
2024-02-16 15:58:38 +01:00
|
|
|
doc.append_line(line)
|
|
|
|
line = self.get_doc_paragraph(doc)
|
|
|
|
else:
|
|
|
|
# Free-form documentation
|
2024-02-16 15:58:40 +01:00
|
|
|
doc = QAPIDoc(info)
|
|
|
|
doc.ensure_untagged_section(self.info)
|
2024-02-16 15:58:38 +01:00
|
|
|
first = True
|
|
|
|
while line is not None:
|
|
|
|
if match := self._match_at_name_colon(line):
|
2024-02-16 15:58:33 +01:00
|
|
|
raise QAPIParseError(
|
|
|
|
self,
|
2024-02-16 15:58:38 +01:00
|
|
|
"'@%s:' not allowed in free-form documentation"
|
|
|
|
% match.group(1))
|
|
|
|
if line.startswith('='):
|
|
|
|
if not first:
|
|
|
|
raise QAPIParseError(
|
|
|
|
self,
|
|
|
|
"'=' heading must come first in a comment block")
|
|
|
|
doc.append_line(line)
|
|
|
|
self.accept(False)
|
|
|
|
line = self.get_doc_line()
|
|
|
|
first = False
|
2019-10-18 09:43:44 +02:00
|
|
|
|
2024-06-26 18:21:12 -04:00
|
|
|
self.accept()
|
2024-02-16 15:58:38 +01:00
|
|
|
doc.end()
|
|
|
|
return doc
|
2019-10-18 09:43:44 +02:00
|
|
|
|
|
|
|
|
2020-03-04 16:59:29 +01:00
|
|
|
class QAPIDoc:
|
2019-10-18 09:43:44 +02:00
|
|
|
"""
|
|
|
|
A documentation comment block, either definition or free-form
|
|
|
|
|
|
|
|
Definition documentation blocks consist of
|
|
|
|
|
|
|
|
* a body section: one line naming the definition, followed by an
|
|
|
|
overview (any number of lines)
|
|
|
|
|
|
|
|
* argument sections: a description of each argument (for commands
|
|
|
|
and events) or member (for structs, unions and alternates)
|
|
|
|
|
|
|
|
* features sections: a description of each feature flag
|
|
|
|
|
|
|
|
* additional (non-argument) sections, possibly tagged
|
|
|
|
|
|
|
|
Free-form documentation blocks consist only of a body section.
|
|
|
|
"""
|
|
|
|
|
2020-03-04 16:59:29 +01:00
|
|
|
class Section:
|
2024-03-15 16:22:38 +01:00
|
|
|
# pylint: disable=too-few-public-methods
|
2024-02-16 15:58:40 +01:00
|
|
|
def __init__(self, info: QAPISourceInfo,
|
2024-02-16 15:58:32 +01:00
|
|
|
tag: Optional[str] = None):
|
2024-02-16 15:58:29 +01:00
|
|
|
# section source info, i.e. where it begins
|
2024-02-16 15:58:40 +01:00
|
|
|
self.info = info
|
2024-02-16 15:58:31 +01:00
|
|
|
# section tag, if any ('Returns', '@name', ...)
|
2024-02-16 15:58:32 +01:00
|
|
|
self.tag = tag
|
2024-02-16 15:58:31 +01:00
|
|
|
# section text without tag
|
2019-10-18 09:43:44 +02:00
|
|
|
self.text = ''
|
2020-09-25 17:23:00 +01:00
|
|
|
|
2024-02-16 15:58:38 +01:00
|
|
|
def append_line(self, line: str) -> None:
|
qapi: Relax doc string @name: description indentation rules
The QAPI schema doc comment language provides special syntax for
command and event arguments, struct and union members, alternate
branches, enumeration values, and features: descriptions starting with
"@name:".
By convention, we format them like this:
# @name: Lorem ipsum dolor sit amet, consectetur adipiscing elit,
# sed do eiusmod tempor incididunt ut labore et dolore
# magna aliqua.
Okay for names as short as "name", but we have much longer ones. Their
description gets squeezed against the right margin, like this:
# @dirty-sync-missed-zero-copy: Number of times dirty RAM synchronization could
# not avoid copying dirty pages. This is between
# 0 and @dirty-sync-count * @multifd-channels.
# (since 7.1)
The description text is effectively just 50 characters wide. Easy
enough to read, but can be cumbersome to write.
The awkward squeeze against the right margin makes people go beyond it,
which produces two undesirables: arguments about style, and descriptions
that are unnecessarily hard to read, like this one:
# @postcopy-vcpu-blocktime: list of the postcopy blocktime per vCPU. This is
# only present when the postcopy-blocktime migration capability
# is enabled. (Since 3.0)
We could instead format it like
# @postcopy-vcpu-blocktime:
# list of the postcopy blocktime per vCPU. This is only present
# when the postcopy-blocktime migration capability is
# enabled. (Since 3.0)
or, since the commit before previous, like
# @postcopy-vcpu-blocktime:
# list of the postcopy blocktime per vCPU. This is only present
# when the postcopy-blocktime migration capability is
# enabled. (Since 3.0)
However, I'd rather have
# @postcopy-vcpu-blocktime: list of the postcopy blocktime per vCPU.
# This is only present when the postcopy-blocktime migration
# capability is enabled. (Since 3.0)
because this is how rST field and option lists work.
To get this, we need to let the first non-blank line after the
"@name:" line determine expected indentation.
This fills up the indentation pitfall mentioned in
docs/devel/qapi-code-gen.rst. A related pitfall still exists. Update
the text to show it.
Signed-off-by: Markus Armbruster <armbru@redhat.com>
Message-Id: <20230428105429.1687850-14-armbru@redhat.com>
Reviewed-by: Juan Quintela <quintela@redhat.com>
[Work around lack of walrus operator in Python 3.7 and older]
2023-04-28 12:54:25 +02:00
|
|
|
self.text += line + '\n'
|
2019-10-18 09:43:44 +02:00
|
|
|
|
|
|
|
class ArgSection(Section):
|
2024-02-16 15:58:40 +01:00
|
|
|
def __init__(self, info: QAPISourceInfo, tag: str):
|
|
|
|
super().__init__(info, tag)
|
2021-09-30 16:57:11 -04:00
|
|
|
self.member: Optional['QAPISchemaMember'] = None
|
2019-10-18 09:43:44 +02:00
|
|
|
|
2021-09-30 16:57:11 -04:00
|
|
|
def connect(self, member: 'QAPISchemaMember') -> None:
|
2019-10-18 09:43:44 +02:00
|
|
|
self.member = member
|
|
|
|
|
2024-02-16 15:58:40 +01:00
|
|
|
def __init__(self, info: QAPISourceInfo, symbol: Optional[str] = None):
|
2024-02-16 15:58:38 +01:00
|
|
|
# info points to the doc comment block's first line
|
2019-10-18 09:43:44 +02:00
|
|
|
self.info = info
|
2024-02-16 15:58:38 +01:00
|
|
|
# definition doc's symbol, None for free-form doc
|
|
|
|
self.symbol: Optional[str] = symbol
|
|
|
|
# the sections in textual order
|
2024-02-16 15:58:40 +01:00
|
|
|
self.all_sections: List[QAPIDoc.Section] = [QAPIDoc.Section(info)]
|
2024-02-16 15:58:38 +01:00
|
|
|
# the body section
|
|
|
|
self.body: Optional[QAPIDoc.Section] = self.all_sections[0]
|
|
|
|
# dicts mapping parameter/feature names to their description
|
|
|
|
self.args: Dict[str, QAPIDoc.ArgSection] = {}
|
|
|
|
self.features: Dict[str, QAPIDoc.ArgSection] = {}
|
2024-02-27 12:39:11 +01:00
|
|
|
# a command's "Returns" and "Errors" section
|
2024-02-27 12:39:09 +01:00
|
|
|
self.returns: Optional[QAPIDoc.Section] = None
|
2024-02-27 12:39:11 +01:00
|
|
|
self.errors: Optional[QAPIDoc.Section] = None
|
2024-02-27 12:39:09 +01:00
|
|
|
# "Since" section
|
|
|
|
self.since: Optional[QAPIDoc.Section] = None
|
2024-02-16 15:58:38 +01:00
|
|
|
# sections other than .body, .args, .features
|
2021-09-30 16:57:12 -04:00
|
|
|
self.sections: List[QAPIDoc.Section] = []
|
2023-04-28 12:54:24 +02:00
|
|
|
|
2024-02-16 15:58:38 +01:00
|
|
|
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 == '':
|
|
|
|
raise QAPISemError(
|
|
|
|
section.info, "text required after '%s:'" % section.tag)
|
2019-10-18 09:43:44 +02:00
|
|
|
|
2024-02-16 15:58:40 +01:00
|
|
|
def ensure_untagged_section(self, info: QAPISourceInfo) -> None:
|
2024-02-16 15:58:38 +01:00
|
|
|
if self.all_sections and not self.all_sections[-1].tag:
|
|
|
|
# extend current section
|
|
|
|
self.all_sections[-1].text += '\n'
|
2019-10-18 09:43:44 +02:00
|
|
|
return
|
2024-02-16 15:58:38 +01:00
|
|
|
# start new section
|
2024-02-16 15:58:40 +01:00
|
|
|
section = self.Section(info)
|
2024-02-16 15:58:38 +01:00
|
|
|
self.sections.append(section)
|
|
|
|
self.all_sections.append(section)
|
|
|
|
|
2024-02-16 15:58:40 +01:00
|
|
|
def new_tagged_section(self, info: QAPISourceInfo, tag: str) -> None:
|
|
|
|
section = self.Section(info, tag)
|
2024-02-27 12:39:09 +01:00
|
|
|
if tag == 'Returns':
|
|
|
|
if self.returns:
|
|
|
|
raise QAPISemError(
|
|
|
|
info, "duplicated '%s' section" % tag)
|
|
|
|
self.returns = section
|
2024-02-27 12:39:11 +01:00
|
|
|
elif tag == 'Errors':
|
|
|
|
if self.errors:
|
|
|
|
raise QAPISemError(
|
|
|
|
info, "duplicated '%s' section" % tag)
|
|
|
|
self.errors = section
|
2024-02-27 12:39:09 +01:00
|
|
|
elif tag == 'Since':
|
|
|
|
if self.since:
|
|
|
|
raise QAPISemError(
|
|
|
|
info, "duplicated '%s' section" % tag)
|
|
|
|
self.since = section
|
2024-02-16 15:58:38 +01:00
|
|
|
self.sections.append(section)
|
|
|
|
self.all_sections.append(section)
|
2019-10-18 09:43:44 +02:00
|
|
|
|
2024-02-16 15:58:40 +01:00
|
|
|
def _new_description(self, info: QAPISourceInfo, name: str,
|
2024-02-16 15:58:38 +01:00
|
|
|
desc: Dict[str, ArgSection]) -> None:
|
2019-10-18 09:43:44 +02:00
|
|
|
if not name:
|
2024-02-16 15:58:40 +01:00
|
|
|
raise QAPISemError(info, "invalid parameter name")
|
2024-02-16 15:58:38 +01:00
|
|
|
if name in desc:
|
2024-02-16 15:58:40 +01:00
|
|
|
raise QAPISemError(info, "'%s' parameter name duplicated" % name)
|
|
|
|
section = self.ArgSection(info, '@' + name)
|
2024-02-16 15:58:38 +01:00
|
|
|
self.all_sections.append(section)
|
|
|
|
desc[name] = section
|
2019-10-18 09:43:44 +02:00
|
|
|
|
2024-02-16 15:58:40 +01:00
|
|
|
def new_argument(self, info: QAPISourceInfo, name: str) -> None:
|
|
|
|
self._new_description(info, name, self.args)
|
2021-09-30 16:57:09 -04:00
|
|
|
|
2024-02-16 15:58:40 +01:00
|
|
|
def new_feature(self, info: QAPISourceInfo, name: str) -> None:
|
|
|
|
self._new_description(info, name, self.features)
|
2019-10-18 09:43:44 +02:00
|
|
|
|
2024-02-16 15:58:38 +01:00
|
|
|
def append_line(self, line: str) -> None:
|
|
|
|
self.all_sections[-1].append_line(line)
|
2019-10-18 09:43:44 +02:00
|
|
|
|
2021-09-30 16:57:11 -04:00
|
|
|
def connect_member(self, member: 'QAPISchemaMember') -> None:
|
2019-10-18 09:43:44 +02:00
|
|
|
if member.name not in self.args:
|
2024-03-15 16:22:56 +01:00
|
|
|
assert member.info
|
qapi: Require member documentation (with loophole)
The QAPI generator forces you to document your stuff. Except for
command arguments, event data, and members of enum and object types:
these the generator silently "documents" as "Not documented".
We can't require proper documentation there without first fixing all
the offenders. We've always had too many offenders to pull that off.
Right now, we have more than 500. Worse, we seem to fix old ones no
faster than we add new ones: in the past year, we fixed 22 ones, but
added 26 new ones.
To help arrest the backsliding, make missing documentation an error
unless the command, type, or event is in listed in new pragma
documentation-exceptions.
List all the current offenders: 117 commands and types in qapi/, and 9
in qga/.
Signed-off-by: Markus Armbruster <armbru@redhat.com>
Message-ID: <20240205074709.3613229-7-armbru@redhat.com>
Reviewed-by: Daniel P. Berrangé <berrange@redhat.com>
2024-02-05 08:47:00 +01:00
|
|
|
if self.symbol not in member.info.pragma.documentation_exceptions:
|
|
|
|
raise QAPISemError(member.info,
|
|
|
|
"%s '%s' lacks documentation"
|
|
|
|
% (member.role, member.name))
|
2024-02-16 15:58:38 +01:00
|
|
|
self.args[member.name] = QAPIDoc.ArgSection(
|
2024-02-16 15:58:40 +01:00
|
|
|
self.info, '@' + member.name)
|
2019-10-18 09:43:44 +02:00
|
|
|
self.args[member.name].connect(member)
|
|
|
|
|
2021-09-30 16:57:11 -04:00
|
|
|
def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
|
2019-10-24 13:02:37 +02:00
|
|
|
if feature.name not in self.features:
|
|
|
|
raise QAPISemError(feature.info,
|
|
|
|
"feature '%s' lacks documentation"
|
|
|
|
% feature.name)
|
|
|
|
self.features[feature.name].connect(feature)
|
|
|
|
|
qapi/parser: add QAPIExpression type
This patch creates a new type, QAPIExpression, which represents a parsed
expression complete with QAPIDoc and QAPISourceInfo.
This patch turns parser.exprs into a list of QAPIExpression instead,
and adjusts expr.py to match.
This allows the types we specify in parser.py to be "remembered" all the
way through expr.py and into schema.py. Several assertions around
packing and unpacking this data can be removed as a result.
It also corrects a harmless typing error. Before the patch,
check_exprs() allegedly takes a List[_JSONObject]. It actually takes
a list of dicts of the form
{'expr': E, 'info': I, 'doc': D}
where E is of type _ExprValue, I is of type QAPISourceInfo, and D is
of type QAPIDoc. Key 'doc' is optional. This is not a _JSONObject!
Passes type checking anyway, because _JSONObject is Dict[str, object].
Signed-off-by: John Snow <jsnow@redhat.com>
Message-Id: <20230215000011.1725012-5-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
[Commit message amended to point out the typing fix]
2023-02-14 19:00:09 -05:00
|
|
|
def check_expr(self, expr: QAPIExpression) -> None:
|
2024-02-27 12:39:21 +01:00
|
|
|
if 'command' in expr:
|
|
|
|
if self.returns and 'returns' not in expr:
|
|
|
|
raise QAPISemError(
|
|
|
|
self.returns.info,
|
|
|
|
"'Returns' section, but command doesn't return anything")
|
|
|
|
else:
|
2024-02-27 12:39:11 +01:00
|
|
|
if self.returns:
|
|
|
|
raise QAPISemError(
|
|
|
|
self.returns.info,
|
|
|
|
"'Returns' section is only valid for commands")
|
|
|
|
if self.errors:
|
|
|
|
raise QAPISemError(
|
2024-03-15 16:22:37 +01:00
|
|
|
self.errors.info,
|
2024-02-27 12:39:11 +01:00
|
|
|
"'Errors' section is only valid for commands")
|
2019-10-18 09:43:44 +02:00
|
|
|
|
2021-09-30 16:57:12 -04:00
|
|
|
def check(self) -> None:
|
2019-10-24 13:02:37 +02:00
|
|
|
|
2021-09-30 16:57:12 -04:00
|
|
|
def check_args_section(
|
|
|
|
args: Dict[str, QAPIDoc.ArgSection], what: str
|
|
|
|
) -> None:
|
2019-10-24 13:02:37 +02:00
|
|
|
bogus = [name for name, section in args.items()
|
|
|
|
if not section.member]
|
|
|
|
if bogus:
|
|
|
|
raise QAPISemError(
|
2024-02-16 15:58:29 +01:00
|
|
|
args[bogus[0]].info,
|
2021-09-30 16:57:06 -04:00
|
|
|
"documented %s%s '%s' %s not exist" % (
|
|
|
|
what,
|
|
|
|
"s" if len(bogus) > 1 else "",
|
|
|
|
"', '".join(bogus),
|
|
|
|
"do" if len(bogus) > 1 else "does"
|
|
|
|
))
|
|
|
|
|
|
|
|
check_args_section(self.args, 'member')
|
|
|
|
check_args_section(self.features, 'feature')
|