From 6a8c0b51025314cdb1a8b4be24d45e690f1217dd Mon Sep 17 00:00:00 2001 From: Kevin Wolf Date: Thu, 6 Jun 2019 17:37:57 +0200 Subject: qapi: Add feature flags to struct types Sometimes, the behaviour of QEMU changes without a change in the QMP syntax (usually by allowing values or operations that previously resulted in an error). QMP clients may still need to know whether they can rely on the changed behavior. Let's add feature flags to the QAPI schema language, so that we can make such changes visible with schema introspection. An example for a schema definition using feature flags looks like this: { 'struct': 'TestType', 'data': { 'number': 'int' }, 'features': [ 'allow-negative-numbers' ] } Introspection information then looks like this: { "name": "TestType", "meta-type": "object", "members": [ { "name": "number", "type": "int" } ], "features": [ "allow-negative-numbers" ] } This patch implements feature flags only for struct types. We'll implement them more widely as needed. Signed-off-by: Kevin Wolf Message-Id: <20190606153803.5278-2-armbru@redhat.com> Reviewed-by: Markus Armbruster Signed-off-by: Markus Armbruster --- scripts/qapi/common.py | 66 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 10 deletions(-) (limited to 'scripts/qapi/common.py') diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py index f07869ec73..9e4b6c00b5 100644 --- a/scripts/qapi/common.py +++ b/scripts/qapi/common.py @@ -886,12 +886,26 @@ def check_enum(expr, info): def check_struct(expr, info): name = expr['struct'] members = expr['data'] + features = expr.get('features') check_type(info, "'data' for struct '%s'" % name, members, allow_dict=True, allow_optional=True) check_type(info, "'base' for struct '%s'" % name, expr.get('base'), allow_metas=['struct']) + if features: + if not isinstance(features, list): + raise QAPISemError(info, + "Struct '%s' requires an array for 'features'" % + name) + for f in features: + assert isinstance(f, dict) + check_known_keys(info, "feature of struct %s" % name, f, + ['name'], ['if']) + + check_if(f, info) + check_name(info, "Feature of struct %s" % name, f['name']) + def check_known_keys(info, source, keys, required, optional): @@ -948,6 +962,12 @@ def normalize_members(members): members[key] = {'type': arg} +def normalize_features(features): + if isinstance(features, list): + features[:] = [f if isinstance(f, dict) else {'name': f} + for f in features] + + def check_exprs(exprs): global all_names @@ -986,8 +1006,10 @@ def check_exprs(exprs): normalize_members(expr['data']) elif 'struct' in expr: meta = 'struct' - check_keys(expr_elem, 'struct', ['data'], ['base', 'if']) + check_keys(expr_elem, 'struct', ['data'], + ['base', 'if', 'features']) normalize_members(expr['data']) + normalize_features(expr.get('features')) struct_types[expr[meta]] = expr elif 'command' in expr: meta = 'command' @@ -1126,10 +1148,12 @@ class QAPISchemaVisitor(object): def visit_array_type(self, name, info, ifcond, element_type): pass - def visit_object_type(self, name, info, ifcond, base, members, variants): + def visit_object_type(self, name, info, ifcond, base, members, variants, + features): pass - def visit_object_type_flat(self, name, info, ifcond, members, variants): + def visit_object_type_flat(self, name, info, ifcond, members, variants, + features): pass def visit_alternate_type(self, name, info, ifcond, variants): @@ -1290,7 +1314,7 @@ class QAPISchemaArrayType(QAPISchemaType): class QAPISchemaObjectType(QAPISchemaType): def __init__(self, name, info, doc, ifcond, - base, local_members, variants): + base, local_members, variants, features): # struct has local_members, optional base, and no variants # flat union has base, variants, and no local_members # simple union has local_members, variants, and no base @@ -1302,11 +1326,15 @@ class QAPISchemaObjectType(QAPISchemaType): if variants is not None: assert isinstance(variants, QAPISchemaObjectTypeVariants) variants.set_owner(name) + for f in features: + assert isinstance(f, QAPISchemaFeature) + f.set_owner(name) self._base_name = base self.base = None self.local_members = local_members self.variants = variants self.members = None + self.features = features def check(self, schema): QAPISchemaType.check(self, schema) @@ -1332,6 +1360,12 @@ class QAPISchemaObjectType(QAPISchemaType): self.variants.check(schema, seen) assert self.variants.tag_member in self.members self.variants.check_clash(self.info, seen) + + # Features are in a name space separate from members + seen = {} + for f in self.features: + f.check_clash(self.info, seen) + if self.doc: self.doc.check() @@ -1368,12 +1402,15 @@ class QAPISchemaObjectType(QAPISchemaType): def visit(self, visitor): visitor.visit_object_type(self.name, self.info, self.ifcond, - self.base, self.local_members, self.variants) + self.base, self.local_members, self.variants, + self.features) visitor.visit_object_type_flat(self.name, self.info, self.ifcond, - self.members, self.variants) + self.members, self.variants, + self.features) class QAPISchemaMember(object): + """ Represents object members, enum members and features """ role = 'member' def __init__(self, name, ifcond=None): @@ -1419,6 +1456,10 @@ class QAPISchemaMember(object): return "'%s' %s" % (self.name, self._pretty_owner()) +class QAPISchemaFeature(QAPISchemaMember): + role = 'feature' + + class QAPISchemaObjectTypeMember(QAPISchemaMember): def __init__(self, name, typ, optional, ifcond=None): QAPISchemaMember.__init__(self, name, ifcond) @@ -1675,7 +1716,7 @@ class QAPISchema(object): ('null', 'null', 'QNull' + pointer_suffix)]: self._def_builtin_type(*t) self.the_empty_object_type = QAPISchemaObjectType( - 'q_empty', None, None, None, None, [], None) + 'q_empty', None, None, None, None, [], None, []) self._def_entity(self.the_empty_object_type) qtypes = ['none', 'qnull', 'qnum', 'qstring', 'qdict', 'qlist', @@ -1685,6 +1726,9 @@ class QAPISchema(object): self._def_entity(QAPISchemaEnumType('QType', None, None, None, qtype_values, 'QTYPE')) + def _make_features(self, features): + return [QAPISchemaFeature(f['name'], f.get('if')) for f in features] + def _make_enum_members(self, values): return [QAPISchemaMember(v['name'], v.get('if')) for v in values] @@ -1721,7 +1765,7 @@ class QAPISchema(object): assert ifcond == typ._ifcond # pylint: disable=protected-access else: self._def_entity(QAPISchemaObjectType(name, info, doc, ifcond, - None, members, None)) + None, members, None, [])) return name def _def_enum_type(self, expr, info, doc): @@ -1752,9 +1796,11 @@ class QAPISchema(object): base = expr.get('base') data = expr['data'] ifcond = expr.get('if') + features = expr.get('features', []) self._def_entity(QAPISchemaObjectType(name, info, doc, ifcond, base, self._make_members(data, info), - None)) + None, + self._make_features(features))) def _make_variant(self, case, typ, ifcond): return QAPISchemaObjectTypeVariant(case, typ, ifcond) @@ -1795,7 +1841,7 @@ class QAPISchema(object): QAPISchemaObjectType(name, info, doc, ifcond, base, members, QAPISchemaObjectTypeVariants(tag_name, tag_member, - variants))) + variants), [])) def _def_alternate_type(self, expr, info, doc): name = expr['alternate'] -- cgit v1.2.3 From 03bf06bdc13c066a4703ab16e767dd91214969e3 Mon Sep 17 00:00:00 2001 From: Kevin Wolf Date: Thu, 6 Jun 2019 17:38:00 +0200 Subject: qapi: Disentangle QAPIDoc code Documentation comments follow a certain structure: First, we have a text with a general description (called QAPIDoc.body). After this, descriptions of the arguments follow. Finally, we have a part that contains various named sections. The code doesn't show this structure, but just checks various attributes that indicate indirectly which part is being processed, so it happens to do the right set of things in the right phase. This is hard to follow, and adding support for documentation of features would be even harder. This patch restructures the code so that the three parts are clearly separated. The code becomes a bit longer, but easier to follow. The resulting output remains unchanged. Signed-off-by: Kevin Wolf Message-Id: <20190606153803.5278-5-armbru@redhat.com> Reviewed-by: Markus Armbruster Signed-off-by: Markus Armbruster --- scripts/qapi/common.py | 127 +++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 102 insertions(+), 25 deletions(-) (limited to 'scripts/qapi/common.py') diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py index 9e4b6c00b5..f40a2cd4c5 100644 --- a/scripts/qapi/common.py +++ b/scripts/qapi/common.py @@ -120,6 +120,24 @@ class QAPIDoc(object): def connect(self, member): self.member = member + class DocPart: + """ + Describes which part of the documentation we're parsing right now. + + Expression documentation blocks consist of + * a BODY part: first line naming the expression, plus an + optional overview + * an ARGS part: description of each argument (for commands and + events) or member (for structs, unions and alternates), + * a VARIOUS part: optional tagged sections. + + Free-form documentation blocks consist only of a BODY part. + """ + # TODO Make it a subclass of Enum when Python 2 support is removed + BODY = 1 + ARGS = 2 + VARIOUS = 3 + def __init__(self, parser, info): # self._parser is used to report errors with QAPIParseError. The # resulting error position depends on the state of the parser. @@ -135,6 +153,7 @@ class QAPIDoc(object): self.sections = [] # the current section self._section = self.body + self._part = QAPIDoc.DocPart.BODY def has_section(self, name): """Return True if we have a section with this name.""" @@ -144,7 +163,27 @@ class QAPIDoc(object): return False def append(self, line): - """Parse a comment line and add it to the documentation.""" + """ + Parse a comment line and add it to the documentation. + + The way that the line is dealt with depends on which part of + the documentation we're parsing right now: + + BODY means that we're ready to process free-form text into + self.body. A symbol name is only allowed if no other text was + parsed yet. It is interpreted as the symbol name that + describes the currently documented object. On getting the + second symbol name, we proceed to ARGS. + + ARGS means that we're parsing the arguments section. Any + symbol name is interpreted as an argument and an ArgSection is + created for it. + + VARIOUS is the final part where free-form sections may appear. + This includes named sections such as "Return:" as well as + unnamed paragraphs. Symbols are not allowed any more in this + part. + """ line = line[1:] if not line: self._append_freeform(line) @@ -154,37 +193,85 @@ class QAPIDoc(object): raise QAPIParseError(self._parser, "Missing space after #") line = line[1:] + if self._part == QAPIDoc.DocPart.BODY: + self._append_body_line(line) + elif self._part == QAPIDoc.DocPart.ARGS: + self._append_args_line(line) + elif self._part == QAPIDoc.DocPart.VARIOUS: + self._append_various_line(line) + else: + assert False + + def end_comment(self): + self._end_section() + + @staticmethod + def _is_section_tag(name): + return name in ('Returns:', 'Since:', + # those are often singular or plural + 'Note:', 'Notes:', + 'Example:', 'Examples:', + 'TODO:') + + def _append_body_line(self, line): + name = line.split(' ', 1)[0] # FIXME not nice: things like '# @foo:' and '# @foo: ' aren't # recognized, and get silently treated as ordinary text - if self.symbol: - self._append_symbol_line(line) - elif not self.body.text and line.startswith('@'): + if not self.symbol and not self.body.text and line.startswith('@'): if not line.endswith(':'): raise QAPIParseError(self._parser, "Line should end with :") self.symbol = line[1:-1] # FIXME invalid names other than the empty string aren't flagged if not self.symbol: raise QAPIParseError(self._parser, "Invalid name") + elif self.symbol: + # We already know that we document some symbol + if name.startswith('@') and name.endswith(':'): + self._part = QAPIDoc.DocPart.ARGS + self._append_args_line(line) + elif self._is_section_tag(name): + self._part = QAPIDoc.DocPart.VARIOUS + self._append_various_line(line) + else: + self._append_freeform(line.strip()) else: - self._append_freeform(line) + # This is free-form documentation without a symbol + self._append_freeform(line.strip()) - def end_comment(self): - self._end_section() - - def _append_symbol_line(self, line): + def _append_args_line(self, line): name = line.split(' ', 1)[0] if name.startswith('@') and name.endswith(':'): line = line[len(name)+1:] self._start_args_section(name[1:-1]) - elif name in ('Returns:', 'Since:', - # those are often singular or plural - 'Note:', 'Notes:', - 'Example:', 'Examples:', - 'TODO:'): + elif self._is_section_tag(name): + self._part = QAPIDoc.DocPart.VARIOUS + self._append_various_line(line) + return + elif (self._section.text.endswith('\n\n') + and line and not line[0].isspace()): + self._start_section() + self._part = QAPIDoc.DocPart.VARIOUS + self._append_various_line(line) + return + + self._append_freeform(line.strip()) + + def _append_various_line(self, line): + name = line.split(' ', 1)[0] + + if name.startswith('@') and name.endswith(':'): + raise QAPIParseError(self._parser, + "'%s' can't follow '%s' section" + % (name, self.sections[0].name)) + elif self._is_section_tag(name): line = line[len(name)+1:] self._start_section(name[:-1]) + if (not self._section.name or + not self._section.name.startswith('Example')): + line = line.strip() + self._append_freeform(line) def _start_args_section(self, name): @@ -194,10 +281,7 @@ class QAPIDoc(object): if name in self.args: raise QAPIParseError(self._parser, "'%s' parameter name duplicated" % name) - if self.sections: - raise QAPIParseError(self._parser, - "'@%s:' can't follow '%s' section" - % (name, self.sections[0].name)) + assert not self.sections self._end_section() self._section = QAPIDoc.ArgSection(name) self.args[name] = self._section @@ -219,13 +303,6 @@ class QAPIDoc(object): self._section = None def _append_freeform(self, line): - in_arg = isinstance(self._section, QAPIDoc.ArgSection) - if (in_arg and self._section.text.endswith('\n\n') - and line and not line[0].isspace()): - self._start_section() - if (in_arg or not self._section.name - or not self._section.name.startswith('Example')): - line = line.strip() match = re.match(r'(@\S+:)', line) if match: raise QAPIParseError(self._parser, -- cgit v1.2.3 From f3ed93d545297afbf8c67092b84a14037ec380bd Mon Sep 17 00:00:00 2001 From: Kevin Wolf Date: Thu, 6 Jun 2019 17:38:01 +0200 Subject: qapi: Allow documentation for features Features will be documented in a new part introduced by a "Features:" line, after arguments and before named sections. Signed-off-by: Kevin Wolf Message-Id: <20190606153803.5278-6-armbru@redhat.com> Reviewed-by: Markus Armbruster Signed-off-by: Markus Armbruster --- scripts/qapi/common.py | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) (limited to 'scripts/qapi/common.py') diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py index f40a2cd4c5..1164301edf 100644 --- a/scripts/qapi/common.py +++ b/scripts/qapi/common.py @@ -129,6 +129,7 @@ class QAPIDoc(object): optional overview * an ARGS part: description of each argument (for commands and events) or member (for structs, unions and alternates), + * a FEATURES part: description of each feature, * a VARIOUS part: optional tagged sections. Free-form documentation blocks consist only of a BODY part. @@ -136,7 +137,8 @@ class QAPIDoc(object): # TODO Make it a subclass of Enum when Python 2 support is removed BODY = 1 ARGS = 2 - VARIOUS = 3 + FEATURES = 3 + VARIOUS = 4 def __init__(self, parser, info): # self._parser is used to report errors with QAPIParseError. The @@ -149,6 +151,7 @@ class QAPIDoc(object): self.body = QAPIDoc.Section() # dict mapping parameter name to ArgSection self.args = OrderedDict() + self.features = OrderedDict() # a list of Section self.sections = [] # the current section @@ -197,6 +200,8 @@ class QAPIDoc(object): self._append_body_line(line) elif self._part == QAPIDoc.DocPart.ARGS: self._append_args_line(line) + elif self._part == QAPIDoc.DocPart.FEATURES: + self._append_features_line(line) elif self._part == QAPIDoc.DocPart.VARIOUS: self._append_various_line(line) else: @@ -229,6 +234,8 @@ class QAPIDoc(object): if name.startswith('@') and name.endswith(':'): self._part = QAPIDoc.DocPart.ARGS self._append_args_line(line) + elif line == 'Features:': + self._part = QAPIDoc.DocPart.FEATURES elif self._is_section_tag(name): self._part = QAPIDoc.DocPart.VARIOUS self._append_various_line(line) @@ -248,6 +255,28 @@ class QAPIDoc(object): self._part = QAPIDoc.DocPart.VARIOUS self._append_various_line(line) return + elif (self._section.text.endswith('\n\n') + and line and not line[0].isspace()): + if line == 'Features:': + self._part = QAPIDoc.DocPart.FEATURES + else: + self._start_section() + self._part = QAPIDoc.DocPart.VARIOUS + self._append_various_line(line) + return + + self._append_freeform(line.strip()) + + def _append_features_line(self, line): + name = line.split(' ', 1)[0] + + if name.startswith('@') and name.endswith(':'): + line = line[len(name)+1:] + self._start_features_section(name[1:-1]) + elif self._is_section_tag(name): + self._part = QAPIDoc.DocPart.VARIOUS + self._append_various_line(line) + return elif (self._section.text.endswith('\n\n') and line and not line[0].isspace()): self._start_section() @@ -274,17 +303,23 @@ class QAPIDoc(object): self._append_freeform(line) - def _start_args_section(self, name): + def _start_symbol_section(self, symbols_dict, name): # FIXME invalid names other than the empty string aren't flagged if not name: raise QAPIParseError(self._parser, "Invalid parameter name") - if name in self.args: + if name in symbols_dict: raise QAPIParseError(self._parser, "'%s' parameter name duplicated" % name) assert not self.sections self._end_section() self._section = QAPIDoc.ArgSection(name) - self.args[name] = self._section + symbols_dict[name] = self._section + + def _start_args_section(self, name): + self._start_symbol_section(self.args, name) + + def _start_features_section(self, name): + self._start_symbol_section(self.features, name) def _start_section(self, name=None): if name in ('Returns', 'Since') and self.has_section(name): -- cgit v1.2.3 From 157dd363955b961ef378eb1f7817c31a7fa94d10 Mon Sep 17 00:00:00 2001 From: Markus Armbruster Date: Thu, 6 Jun 2019 17:38:03 +0200 Subject: qapi: Simplify how QAPIDoc implements its state machine QAPIDoc uses a state machine to for processing of documentation lines. Its state is encoded as an enum QAPIDoc._state (well, as enum-like class actually, thanks to our infatuation with Python 2). All we ever do with the state is calling the state's function to process a line of documentation. The enum values effectively serve as handles for the functions. Eliminate the rather wordy indirection: store the function to call in QAPIDoc._append_line. Update and improve comments. Signed-off-by: Markus Armbruster Message-Id: <20190606153803.5278-8-armbru@redhat.com> Reviewed-by: Kevin Wolf [Commit message typo fixed] --- scripts/qapi/common.py | 125 +++++++++++++++++++++++++++---------------------- 1 file changed, 68 insertions(+), 57 deletions(-) (limited to 'scripts/qapi/common.py') diff --git a/scripts/qapi/common.py b/scripts/qapi/common.py index 1164301edf..d61bfdc526 100644 --- a/scripts/qapi/common.py +++ b/scripts/qapi/common.py @@ -102,6 +102,24 @@ class QAPISemError(QAPIError): class QAPIDoc(object): + """ + A documentation comment block, either expression or free-form + + Expression documentation blocks consist of + + * a body section: one line naming the expression, 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. + """ + class Section(object): def __init__(self, name=None): # optional section name (argument/member or section name) @@ -120,26 +138,6 @@ class QAPIDoc(object): def connect(self, member): self.member = member - class DocPart: - """ - Describes which part of the documentation we're parsing right now. - - Expression documentation blocks consist of - * a BODY part: first line naming the expression, plus an - optional overview - * an ARGS part: description of each argument (for commands and - events) or member (for structs, unions and alternates), - * a FEATURES part: description of each feature, - * a VARIOUS part: optional tagged sections. - - Free-form documentation blocks consist only of a BODY part. - """ - # TODO Make it a subclass of Enum when Python 2 support is removed - BODY = 1 - ARGS = 2 - FEATURES = 3 - VARIOUS = 4 - def __init__(self, parser, info): # self._parser is used to report errors with QAPIParseError. The # resulting error position depends on the state of the parser. @@ -156,7 +154,7 @@ class QAPIDoc(object): self.sections = [] # the current section self._section = self.body - self._part = QAPIDoc.DocPart.BODY + self._append_line = self._append_body_line def has_section(self, name): """Return True if we have a section with this name.""" @@ -171,21 +169,10 @@ class QAPIDoc(object): The way that the line is dealt with depends on which part of the documentation we're parsing right now: - - BODY means that we're ready to process free-form text into - self.body. A symbol name is only allowed if no other text was - parsed yet. It is interpreted as the symbol name that - describes the currently documented object. On getting the - second symbol name, we proceed to ARGS. - - ARGS means that we're parsing the arguments section. Any - symbol name is interpreted as an argument and an ArgSection is - created for it. - - VARIOUS is the final part where free-form sections may appear. - This includes named sections such as "Return:" as well as - unnamed paragraphs. Symbols are not allowed any more in this - part. + * The body section: ._append_line is ._append_body_line + * An argument section: ._append_line is ._append_args_line + * A features section: ._append_line is ._append_features_line + * An additional section: ._append_line is ._append_various_line """ line = line[1:] if not line: @@ -195,17 +182,7 @@ class QAPIDoc(object): if line[0] != ' ': raise QAPIParseError(self._parser, "Missing space after #") line = line[1:] - - if self._part == QAPIDoc.DocPart.BODY: - self._append_body_line(line) - elif self._part == QAPIDoc.DocPart.ARGS: - self._append_args_line(line) - elif self._part == QAPIDoc.DocPart.FEATURES: - self._append_features_line(line) - elif self._part == QAPIDoc.DocPart.VARIOUS: - self._append_various_line(line) - else: - assert False + self._append_line(line) def end_comment(self): self._end_section() @@ -219,6 +196,19 @@ class QAPIDoc(object): 'TODO:') def _append_body_line(self, line): + """ + Process a line of documentation text in the body section. + + If this a symbol line and it is the section's first line, this + is an expression documentation block for that symbol. + + If it's an expression documentation block, another symbol line + begins the argument section for the argument named by it, and + a section tag begins an additional section. Start that + section and append the line to it. + + Else, append the line to the current section. + """ name = line.split(' ', 1)[0] # FIXME not nice: things like '# @foo:' and '# @foo: ' aren't # recognized, and get silently treated as ordinary text @@ -230,38 +220,49 @@ class QAPIDoc(object): if not self.symbol: raise QAPIParseError(self._parser, "Invalid name") elif self.symbol: - # We already know that we document some symbol + # This is an expression documentation block if name.startswith('@') and name.endswith(':'): - self._part = QAPIDoc.DocPart.ARGS + self._append_line = self._append_args_line self._append_args_line(line) elif line == 'Features:': - self._part = QAPIDoc.DocPart.FEATURES + self._append_line = self._append_features_line elif self._is_section_tag(name): - self._part = QAPIDoc.DocPart.VARIOUS + self._append_line = self._append_various_line self._append_various_line(line) else: self._append_freeform(line.strip()) else: - # This is free-form documentation without a symbol + # This is a free-form documentation block self._append_freeform(line.strip()) def _append_args_line(self, line): + """ + Process a line of documentation text in an argument section. + + A symbol line begins the next argument section, a section tag + section or a non-indented line after a blank line begins an + additional section. Start that section and append the line to + it. + + Else, append the line to the current section. + + """ name = line.split(' ', 1)[0] if name.startswith('@') and name.endswith(':'): line = line[len(name)+1:] self._start_args_section(name[1:-1]) elif self._is_section_tag(name): - self._part = QAPIDoc.DocPart.VARIOUS + self._append_line = self._append_various_line self._append_various_line(line) return elif (self._section.text.endswith('\n\n') and line and not line[0].isspace()): if line == 'Features:': - self._part = QAPIDoc.DocPart.FEATURES + self._append_line = self._append_features_line else: self._start_section() - self._part = QAPIDoc.DocPart.VARIOUS + self._append_line = self._append_various_line self._append_various_line(line) return @@ -274,19 +275,29 @@ class QAPIDoc(object): line = line[len(name)+1:] self._start_features_section(name[1:-1]) elif self._is_section_tag(name): - self._part = QAPIDoc.DocPart.VARIOUS + self._append_line = self._append_various_line self._append_various_line(line) return elif (self._section.text.endswith('\n\n') and line and not line[0].isspace()): self._start_section() - self._part = QAPIDoc.DocPart.VARIOUS + self._append_line = self._append_various_line self._append_various_line(line) return self._append_freeform(line.strip()) def _append_various_line(self, line): + """ + Process a line of documentation text in an additional section. + + A symbol line is an error. + + A section tag begins an additional section. Start that + section and append the line to it. + + Else, append the line to the current section. + """ name = line.split(' ', 1)[0] if name.startswith('@') and name.endswith(':'): -- cgit v1.2.3