diff options
author | John Snow <jsnow@redhat.com> | 2021-09-30 16:57:11 -0400 |
---|---|---|
committer | Markus Armbruster <armbru@redhat.com> | 2021-10-02 07:33:42 +0200 |
commit | e7ac60fcd0623fe255f54c33f2bed2e7b2f780f5 (patch) | |
tree | b0cf6f1b453a43b42ac4305a4f553a99466eef9b /scripts/qapi | |
parent | f4c05aaf148a44d80855eb45b9342feaeeb4764a (diff) |
qapi/parser: add import cycle workaround
Adding static types causes a cycle in the QAPI generator:
[schema -> expr -> parser -> schema]. It exists because the QAPIDoc
class needs the names of types defined by the schema module, but the
schema module needs to import both expr.py/parser.py to do its actual
parsing.
Ultimately, the layering violation is that parser.py should not have any
knowledge of specifics of the Schema. QAPIDoc performs double-duty here
both as a parser *and* as a finalized object that is part of the schema.
In this patch, add the offending type hints alongside the workaround to
avoid the cycle becoming a problem at runtime. See
https://mypy.readthedocs.io/en/latest/runtime_troubles.html#import-cycles
for more information on this workaround technique.
I see three ultimate resolutions here:
(1) Just keep this patch and use the TYPE_CHECKING trick to eliminate
the cycle which is only present during static analysis.
(2) Don't bother to annotate connect_member() et al, give them 'object'
or 'Any'. I don't particularly like this, because it diminishes the
usefulness of type hints for documentation purposes. Still, it's an
extremely quick fix.
(3) Reimplement doc <--> definition correlation directly in schema.py,
integrating doc fields directly into QAPISchemaMember and relieving
the QAPIDoc class of the responsibility. Users of the information
would instead visit the members first and retrieve their
documentation instead of the inverse operation -- visiting the
documentation and retrieving their members.
My preference is (3), but in the short-term (1) is the easiest way to
have my cake (strong type hints) and eat it too (Not have import
cycles). Do (1) for now, but plan for (3).
Signed-off-by: John Snow <jsnow@redhat.com>
Message-Id: <20210930205716.1148693-9-jsnow@redhat.com>
Reviewed-by: Markus Armbruster <armbru@redhat.com>
Signed-off-by: Markus Armbruster <armbru@redhat.com>
Diffstat (limited to 'scripts/qapi')
-rw-r--r-- | scripts/qapi/parser.py | 15 |
1 files changed, 11 insertions, 4 deletions
diff --git a/scripts/qapi/parser.py b/scripts/qapi/parser.py index 40c5da4b17..75582ddb00 100644 --- a/scripts/qapi/parser.py +++ b/scripts/qapi/parser.py @@ -18,6 +18,7 @@ from collections import OrderedDict import os import re from typing import ( + TYPE_CHECKING, Dict, List, Optional, @@ -30,6 +31,12 @@ from .error import QAPISemError, QAPISourceError from .source import QAPISourceInfo +if TYPE_CHECKING: + # pylint: disable=cyclic-import + # TODO: Remove cycle. [schema -> expr -> parser -> schema] + from .schema import QAPISchemaFeature, QAPISchemaMember + + # Return value alias for get_expr(). _ExprValue = Union[List[object], Dict[str, object], str, bool] @@ -473,9 +480,9 @@ class QAPIDoc: class ArgSection(Section): def __init__(self, parser, name, indent=0): super().__init__(parser, name, indent) - self.member = None + self.member: Optional['QAPISchemaMember'] = None - def connect(self, member): + def connect(self, member: 'QAPISchemaMember') -> None: self.member = member class NullSection(Section): @@ -747,14 +754,14 @@ class QAPIDoc: % match.group(1)) self._section.append(line) - def connect_member(self, member): + def connect_member(self, member: 'QAPISchemaMember') -> None: if member.name not in self.args: # Undocumented TODO outlaw self.args[member.name] = QAPIDoc.ArgSection(self._parser, member.name) self.args[member.name].connect(member) - def connect_feature(self, feature): + def connect_feature(self, feature: 'QAPISchemaFeature') -> None: if feature.name not in self.features: raise QAPISemError(feature.info, "feature '%s' lacks documentation" |