Skip to content

Commit f50ecf5

Browse files
committed
Merge remote-tracking branch 'remotes/armbru/tags/pull-qapi-2021-10-02' into staging
QAPI patches patches for 2021-10-02 # gpg: Signature made Sat 02 Oct 2021 01:37:11 AM EDT # gpg: using RSA key 354BC8B3D7EB2A6B68674E5F3870B400EB918653 # gpg: issuer "[email protected]" # gpg: Good signature from "Markus Armbruster <[email protected]>" [full] # gpg: aka "Markus Armbruster <[email protected]>" [full] * remotes/armbru/tags/pull-qapi-2021-10-02: qapi/parser: enable pylint checks qapi/parser: Silence too-few-public-methods warning qapi/parser: enable mypy checks qapi/parser: Add FIXME for consolidating JSON-related types qapi/parser: add type hint annotations (QAPIDoc) qapi/parser: add import cycle workaround qapi/parser: Introduce NullSection qapi/parser: clarify _end_section() logic qapi/parser: remove FIXME comment from _append_body_line qapi: Add spaces after symbol declaration for consistency qapi/parser: fix unused check_args_section arguments qapi/gen: use dict.items() to iterate over _modules qapi/pylintrc: ignore 'consider-using-f-string' warning Signed-off-by: Richard Henderson <[email protected]>
2 parents 5f99210 + d183e04 commit f50ecf5

File tree

9 files changed

+114
-66
lines changed

9 files changed

+114
-66
lines changed

qapi/block-core.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3132,6 +3132,7 @@
31323132

31333133
##
31343134
# @BlockdevQcow2EncryptionFormat:
3135+
#
31353136
# @aes: AES-CBC with plain64 initialization vectors
31363137
#
31373138
# Since: 2.10

qga/qapi-schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,7 @@
11401140

11411141
##
11421142
# @GuestExec:
1143+
#
11431144
# @pid: pid of child process in guest OS
11441145
#
11451146
# Since: 2.5
@@ -1171,6 +1172,7 @@
11711172

11721173
##
11731174
# @GuestHostName:
1175+
#
11741176
# @host-name: Fully qualified domain name of the guest OS
11751177
#
11761178
# Since: 2.10
@@ -1197,6 +1199,7 @@
11971199

11981200
##
11991201
# @GuestUser:
1202+
#
12001203
# @user: Username
12011204
# @domain: Logon domain (windows only)
12021205
# @login-time: Time of login of this user on the computer. If multiple

scripts/qapi/gen.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -296,10 +296,9 @@ def _temp_module(self, name: str) -> Iterator[None]:
296296
self._current_module = old_module
297297

298298
def write(self, output_dir: str, opt_builtins: bool = False) -> None:
299-
for name in self._module:
299+
for name, (genc, genh) in self._module.items():
300300
if QAPISchemaModule.is_builtin_module(name) and not opt_builtins:
301301
continue
302-
(genc, genh) = self._module[name]
303302
genc.write(output_dir)
304303
genh.write(output_dir)
305304

scripts/qapi/mypy.ini

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@ strict = True
33
disallow_untyped_calls = False
44
python_version = 3.6
55

6-
[mypy-qapi.parser]
7-
disallow_untyped_defs = False
8-
disallow_incomplete_defs = False
9-
check_untyped_defs = False
10-
116
[mypy-qapi.schema]
127
disallow_untyped_defs = False
138
disallow_incomplete_defs = False

scripts/qapi/parser.py

Lines changed: 97 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import os
1919
import re
2020
from typing import (
21+
TYPE_CHECKING,
2122
Dict,
2223
List,
2324
Optional,
@@ -30,9 +31,22 @@
3031
from .source import QAPISourceInfo
3132

3233

34+
if TYPE_CHECKING:
35+
# pylint: disable=cyclic-import
36+
# TODO: Remove cycle. [schema -> expr -> parser -> schema]
37+
from .schema import QAPISchemaFeature, QAPISchemaMember
38+
39+
40+
#: Represents a single Top Level QAPI schema expression.
41+
TopLevelExpr = Dict[str, object]
42+
3343
# Return value alias for get_expr().
3444
_ExprValue = Union[List[object], Dict[str, object], str, bool]
3545

46+
# FIXME: Consolidate and centralize definitions for TopLevelExpr,
47+
# _ExprValue, _JSONValue, and _JSONObject; currently scattered across
48+
# several modules.
49+
3650

3751
class QAPIParseError(QAPISourceError):
3852
"""Error class for all QAPI schema parsing errors."""
@@ -447,7 +461,10 @@ class QAPIDoc:
447461
"""
448462

449463
class Section:
450-
def __init__(self, parser, name=None, indent=0):
464+
# pylint: disable=too-few-public-methods
465+
def __init__(self, parser: QAPISchemaParser,
466+
name: Optional[str] = None, indent: int = 0):
467+
451468
# parser, for error messages about indentation
452469
self._parser = parser
453470
# optional section name (argument/member or section name)
@@ -456,7 +473,7 @@ def __init__(self, parser, name=None, indent=0):
456473
# the expected indent level of the text of this section
457474
self._indent = indent
458475

459-
def append(self, line):
476+
def append(self, line: str) -> None:
460477
# Strip leading spaces corresponding to the expected indent level
461478
# Blank lines are always OK.
462479
if line:
@@ -471,39 +488,47 @@ def append(self, line):
471488
self.text += line.rstrip() + '\n'
472489

473490
class ArgSection(Section):
474-
def __init__(self, parser, name, indent=0):
491+
def __init__(self, parser: QAPISchemaParser,
492+
name: str, indent: int = 0):
475493
super().__init__(parser, name, indent)
476-
self.member = None
494+
self.member: Optional['QAPISchemaMember'] = None
477495

478-
def connect(self, member):
496+
def connect(self, member: 'QAPISchemaMember') -> None:
479497
self.member = member
480498

481-
def __init__(self, parser, info):
499+
class NullSection(Section):
500+
"""
501+
Immutable dummy section for use at the end of a doc block.
502+
"""
503+
# pylint: disable=too-few-public-methods
504+
def append(self, line: str) -> None:
505+
assert False, "Text appended after end_comment() called."
506+
507+
def __init__(self, parser: QAPISchemaParser, info: QAPISourceInfo):
482508
# self._parser is used to report errors with QAPIParseError. The
483509
# resulting error position depends on the state of the parser.
484510
# It happens to be the beginning of the comment. More or less
485511
# servicable, but action at a distance.
486512
self._parser = parser
487513
self.info = info
488-
self.symbol = None
514+
self.symbol: Optional[str] = None
489515
self.body = QAPIDoc.Section(parser)
490-
# dict mapping parameter name to ArgSection
491-
self.args = OrderedDict()
492-
self.features = OrderedDict()
493-
# a list of Section
494-
self.sections = []
516+
# dicts mapping parameter/feature names to their ArgSection
517+
self.args: Dict[str, QAPIDoc.ArgSection] = OrderedDict()
518+
self.features: Dict[str, QAPIDoc.ArgSection] = OrderedDict()
519+
self.sections: List[QAPIDoc.Section] = []
495520
# the current section
496521
self._section = self.body
497522
self._append_line = self._append_body_line
498523

499-
def has_section(self, name):
524+
def has_section(self, name: str) -> bool:
500525
"""Return True if we have a section with this name."""
501526
for i in self.sections:
502527
if i.name == name:
503528
return True
504529
return False
505530

506-
def append(self, line):
531+
def append(self, line: str) -> None:
507532
"""
508533
Parse a comment line and add it to the documentation.
509534
@@ -524,18 +549,18 @@ def append(self, line):
524549
line = line[1:]
525550
self._append_line(line)
526551

527-
def end_comment(self):
528-
self._end_section()
552+
def end_comment(self) -> None:
553+
self._switch_section(QAPIDoc.NullSection(self._parser))
529554

530555
@staticmethod
531-
def _is_section_tag(name):
556+
def _is_section_tag(name: str) -> bool:
532557
return name in ('Returns:', 'Since:',
533558
# those are often singular or plural
534559
'Note:', 'Notes:',
535560
'Example:', 'Examples:',
536561
'TODO:')
537562

538-
def _append_body_line(self, line):
563+
def _append_body_line(self, line: str) -> None:
539564
"""
540565
Process a line of documentation text in the body section.
541566
@@ -556,9 +581,11 @@ def _append_body_line(self, line):
556581
if not line.endswith(':'):
557582
raise QAPIParseError(self._parser, "line should end with ':'")
558583
self.symbol = line[1:-1]
559-
# FIXME invalid names other than the empty string aren't flagged
584+
# Invalid names are not checked here, but the name provided MUST
585+
# match the following definition, which *is* validated in expr.py.
560586
if not self.symbol:
561-
raise QAPIParseError(self._parser, "invalid name")
587+
raise QAPIParseError(
588+
self._parser, "name required after '@'")
562589
elif self.symbol:
563590
# This is a definition documentation block
564591
if name.startswith('@') and name.endswith(':'):
@@ -575,7 +602,7 @@ def _append_body_line(self, line):
575602
# This is a free-form documentation block
576603
self._append_freeform(line)
577604

578-
def _append_args_line(self, line):
605+
def _append_args_line(self, line: str) -> None:
579606
"""
580607
Process a line of documentation text in an argument section.
581608
@@ -621,7 +648,7 @@ def _append_args_line(self, line):
621648

622649
self._append_freeform(line)
623650

624-
def _append_features_line(self, line):
651+
def _append_features_line(self, line: str) -> None:
625652
name = line.split(' ', 1)[0]
626653

627654
if name.startswith('@') and name.endswith(':'):
@@ -653,7 +680,7 @@ def _append_features_line(self, line):
653680

654681
self._append_freeform(line)
655682

656-
def _append_various_line(self, line):
683+
def _append_various_line(self, line: str) -> None:
657684
"""
658685
Process a line of documentation text in an additional section.
659686
@@ -689,80 +716,95 @@ def _append_various_line(self, line):
689716

690717
self._append_freeform(line)
691718

692-
def _start_symbol_section(self, symbols_dict, name, indent):
719+
def _start_symbol_section(
720+
self,
721+
symbols_dict: Dict[str, 'QAPIDoc.ArgSection'],
722+
name: str,
723+
indent: int) -> None:
693724
# FIXME invalid names other than the empty string aren't flagged
694725
if not name:
695726
raise QAPIParseError(self._parser, "invalid parameter name")
696727
if name in symbols_dict:
697728
raise QAPIParseError(self._parser,
698729
"'%s' parameter name duplicated" % name)
699730
assert not self.sections
700-
self._end_section()
701-
self._section = QAPIDoc.ArgSection(self._parser, name, indent)
702-
symbols_dict[name] = self._section
731+
new_section = QAPIDoc.ArgSection(self._parser, name, indent)
732+
self._switch_section(new_section)
733+
symbols_dict[name] = new_section
703734

704-
def _start_args_section(self, name, indent):
735+
def _start_args_section(self, name: str, indent: int) -> None:
705736
self._start_symbol_section(self.args, name, indent)
706737

707-
def _start_features_section(self, name, indent):
738+
def _start_features_section(self, name: str, indent: int) -> None:
708739
self._start_symbol_section(self.features, name, indent)
709740

710-
def _start_section(self, name=None, indent=0):
741+
def _start_section(self, name: Optional[str] = None,
742+
indent: int = 0) -> None:
711743
if name in ('Returns', 'Since') and self.has_section(name):
712744
raise QAPIParseError(self._parser,
713745
"duplicated '%s' section" % name)
714-
self._end_section()
715-
self._section = QAPIDoc.Section(self._parser, name, indent)
716-
self.sections.append(self._section)
717-
718-
def _end_section(self):
719-
if self._section:
720-
text = self._section.text = self._section.text.strip()
721-
if self._section.name and (not text or text.isspace()):
722-
raise QAPIParseError(
723-
self._parser,
724-
"empty doc section '%s'" % self._section.name)
725-
self._section = None
746+
new_section = QAPIDoc.Section(self._parser, name, indent)
747+
self._switch_section(new_section)
748+
self.sections.append(new_section)
749+
750+
def _switch_section(self, new_section: 'QAPIDoc.Section') -> None:
751+
text = self._section.text = self._section.text.strip()
752+
753+
# Only the 'body' section is allowed to have an empty body.
754+
# All other sections, including anonymous ones, must have text.
755+
if self._section != self.body and not text:
756+
# We do not create anonymous sections unless there is
757+
# something to put in them; this is a parser bug.
758+
assert self._section.name
759+
raise QAPIParseError(
760+
self._parser,
761+
"empty doc section '%s'" % self._section.name)
762+
763+
self._section = new_section
726764

727-
def _append_freeform(self, line):
765+
def _append_freeform(self, line: str) -> None:
728766
match = re.match(r'(@\S+:)', line)
729767
if match:
730768
raise QAPIParseError(self._parser,
731769
"'%s' not allowed in free-form documentation"
732770
% match.group(1))
733771
self._section.append(line)
734772

735-
def connect_member(self, member):
773+
def connect_member(self, member: 'QAPISchemaMember') -> None:
736774
if member.name not in self.args:
737775
# Undocumented TODO outlaw
738776
self.args[member.name] = QAPIDoc.ArgSection(self._parser,
739777
member.name)
740778
self.args[member.name].connect(member)
741779

742-
def connect_feature(self, feature):
780+
def connect_feature(self, feature: 'QAPISchemaFeature') -> None:
743781
if feature.name not in self.features:
744782
raise QAPISemError(feature.info,
745783
"feature '%s' lacks documentation"
746784
% feature.name)
747785
self.features[feature.name].connect(feature)
748786

749-
def check_expr(self, expr):
787+
def check_expr(self, expr: TopLevelExpr) -> None:
750788
if self.has_section('Returns') and 'command' not in expr:
751789
raise QAPISemError(self.info,
752790
"'Returns:' is only valid for commands")
753791

754-
def check(self):
792+
def check(self) -> None:
755793

756-
def check_args_section(args, info, what):
794+
def check_args_section(
795+
args: Dict[str, QAPIDoc.ArgSection], what: str
796+
) -> None:
757797
bogus = [name for name, section in args.items()
758798
if not section.member]
759799
if bogus:
760800
raise QAPISemError(
761801
self.info,
762-
"documented member%s '%s' %s not exist"
763-
% ("s" if len(bogus) > 1 else "",
764-
"', '".join(bogus),
765-
"do" if len(bogus) > 1 else "does"))
766-
767-
check_args_section(self.args, self.info, 'members')
768-
check_args_section(self.features, self.info, 'features')
802+
"documented %s%s '%s' %s not exist" % (
803+
what,
804+
"s" if len(bogus) > 1 else "",
805+
"', '".join(bogus),
806+
"do" if len(bogus) > 1 else "does"
807+
))
808+
809+
check_args_section(self.args, 'member')
810+
check_args_section(self.features, 'feature')

scripts/qapi/pylintrc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
# Add files or directories matching the regex patterns to the ignore list.
44
# The regex matches against base names, not paths.
5-
ignore-patterns=parser.py,
6-
schema.py,
5+
ignore-patterns=schema.py,
76

87

98
[MESSAGES CONTROL]
@@ -23,6 +22,7 @@ disable=fixme,
2322
too-many-branches,
2423
too-many-statements,
2524
too-many-instance-attributes,
25+
consider-using-f-string,
2626

2727
[REPORTS]
2828

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
doc-bad-feature.json:3: documented member 'a' does not exist
1+
doc-bad-feature.json:3: documented feature 'a' does not exist
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
doc-empty-symbol.json:4:1: invalid name
1+
doc-empty-symbol.json:4:1: name required after '@'

0 commit comments

Comments
 (0)