Skip to content

Commit 7127e14

Browse files
jnsnowMarkus Armbruster
authored andcommitted
docs/qapi_domain: add namespace support to cross-references
This patch does three things: 1. Record the current namespace context in pending_xrefs so it can be used for link resolution later, 2. Pass that recorded namespace context to find_obj() when resolving a reference, and 3. Wildly and completely rewrite find_obj(). cross-reference support is expanded to tolerate the presence or absence of either namespace or module, and to cope with the presence or absence of contextual information for either. References now work like this: 1. If the explicit reference target is recorded in the domain's object registry, we link to that target and stop looking. We do this lookup regardless of how fully qualified the target is, which allows direct references to modules (which don't have a module component to their names) or direct references to definitions that may or may not belong to a namespace or module. 2. If contextual information is available from qapi:namespace or qapi:module directives, try using those components to find a direct match to the implied target name. 3. If both prior lookups fail, generate a series of regular expressions looking for wildcard matches in order from most to least specific. Any explicitly provided components (namespace, module) *must* match exactly, but both contextual and entirely omitted components are allowed to differ from the search result. Note that if more than one result is found, Sphinx will emit a warning (a build error for QEMU) and list all of the candidate references. The practical upshot is that in the large majority of cases, namespace and module information is not required when creating simple `references` to definitions from within the same context -- even when identical definitions exist in other contexts. Even when using simple `references` from elsewhere in the QEMU documentation manual, explicit namespace info is not required if there is only one definition by that name. Disambiguation *will* be required from outside of the QAPI documentation when referencing e.g. block-core definitions, which are shared between QEMU QMP and the QEMU Storage Daemon. In that case, there are two options: A: References can be made partially or fully explicit, e.g. `QMP:block-dirty-bitmap-add` will link to the QEMU version of the definition, while `QSD:block-dirty-bitmap-add` would link to the QSD version. B: If all of the references in a document are intended to go to the same place, you can insert a "qapi:namespace:: QMP" directive to influence the fuzzy-searching for later references. Signed-off-by: John Snow <[email protected]> Message-ID: <[email protected]> Acked-by: Markus Armbruster <[email protected]> [Commit message typo fixed] Signed-off-by: Markus Armbruster <[email protected]>
1 parent b1df602 commit 7127e14

File tree

2 files changed

+114
-47
lines changed

2 files changed

+114
-47
lines changed

docs/devel/qapi-domain.rst

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -400,11 +400,10 @@ Namespaces
400400
Mimicking the `Python domain target specification syntax
401401
<https://www.sphinx-doc.org/en/master/usage/domains/python.html#target-specification>`_,
402402
QAPI allows you to specify the fully qualified path for a data
403-
type. QAPI enforces globally unique names, so it's unlikely you'll need
404-
this specific feature, but it may be extended in the near future to
405-
allow referencing identically named commands and data types from
406-
different utilities; i.e. QEMU Storage Daemon vs QMP.
403+
type.
407404

405+
* A namespace can be explicitly provided;
406+
e.g. ``:qapi:type:`QMP:BitmapSyncMode``
408407
* A module can be explicitly provided;
409408
``:qapi:type:`block-core.BitmapSyncMode``` will render to:
410409
:qapi:type:`block-core.BitmapSyncMode`
@@ -413,6 +412,28 @@ different utilities; i.e. QEMU Storage Daemon vs QMP.
413412
will render to: :qapi:type:`~block-core.BitmapSyncMode`
414413

415414

415+
Target resolution
416+
-----------------
417+
418+
Any cross-reference to a QAPI type, whether using the ```any``` style of
419+
reference or the more explicit ```:qapi:any:`target``` syntax, allows
420+
for the presence or absence of either the namespace or module
421+
information.
422+
423+
When absent, their value will be inferred from context by the presence
424+
of any ``qapi:namespace`` or ``qapi:module`` directives preceding the
425+
cross-reference.
426+
427+
If no results are found when using the inferred values, other
428+
namespaces/modules will be searched as a last resort; but any explicitly
429+
provided values must always match in order to succeed.
430+
431+
This allows for efficient cross-referencing with a minimum of syntax in
432+
the large majority of cases, but additional context or namespace markup
433+
may be required outside of the QAPI reference documents when linking to
434+
items that share a name across multiple documented QAPI schema.
435+
436+
416437
Custom link text
417438
----------------
418439

@@ -492,6 +513,11 @@ directives are associated with the most recent namespace. This affects
492513
the definition's "fully qualified name", allowing two different
493514
namespaces to create an otherwise identically named definition.
494515

516+
This directive also influences how reference resolution works for any
517+
references that do not explicity specify a namespace, so this directive
518+
can be used to nudge references into preferring targets from within that
519+
namespace.
520+
495521
Example::
496522

497523
.. qapi:namespace:: QMP

docs/sphinx/qapi_domain.py

Lines changed: 84 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from __future__ import annotations
99

10+
import re
1011
from typing import (
1112
TYPE_CHECKING,
1213
List,
@@ -94,6 +95,7 @@ def process_link(
9495
title: str,
9596
target: str,
9697
) -> tuple[str, str]:
98+
refnode["qapi:namespace"] = env.ref_context.get("qapi:namespace")
9799
refnode["qapi:module"] = env.ref_context.get("qapi:module")
98100

99101
# Cross-references that begin with a tilde adjust the title to
@@ -830,66 +832,102 @@ def merge_domaindata(
830832
self.objects[fullname] = obj
831833

832834
def find_obj(
833-
self, modname: str, name: str, typ: Optional[str]
834-
) -> list[tuple[str, ObjectEntry]]:
835+
self, namespace: str, modname: str, name: str, typ: Optional[str]
836+
) -> List[Tuple[str, ObjectEntry]]:
835837
"""
836-
Find a QAPI object for "name", perhaps using the given module.
838+
Find a QAPI object for "name", maybe using contextual information.
837839
838840
Returns a list of (name, object entry) tuples.
839841
840-
:param modname: The current module context (if any!)
841-
under which we are searching.
842-
:param name: The name of the x-ref to resolve;
843-
may or may not include a leading module.
844-
:param type: The role name of the x-ref we're resolving, if provided.
845-
(This is absent for "any" lookups.)
842+
:param namespace: The current namespace context (if any!) under
843+
which we are searching.
844+
:param modname: The current module context (if any!) under
845+
which we are searching.
846+
:param name: The name of the x-ref to resolve; may or may not
847+
include leading context.
848+
:param type: The role name of the x-ref we're resolving, if
849+
provided. This is absent for "any" role lookups.
846850
"""
847851
if not name:
848852
return []
849853

850-
names: list[str] = []
851-
matches: list[tuple[str, ObjectEntry]] = []
854+
# ##
855+
# what to search for
856+
# ##
852857

853-
fullname = name
854-
if "." in fullname:
855-
# We're searching for a fully qualified reference;
856-
# ignore the contextual module.
857-
pass
858-
elif modname:
859-
# We're searching for something from somewhere;
860-
# try searching the current module first.
861-
# e.g. :qapi:cmd:`query-block` or `query-block` is being searched.
862-
fullname = f"{modname}.{name}"
858+
parts = list(QAPIDescription.split_fqn(name))
859+
explicit = tuple(bool(x) for x in parts)
860+
861+
# Fill in the blanks where possible:
862+
if namespace and not parts[0]:
863+
parts[0] = namespace
864+
if modname and not parts[1]:
865+
parts[1] = modname
866+
867+
implicit_fqn = ""
868+
if all(parts):
869+
implicit_fqn = f"{parts[0]}:{parts[1]}.{parts[2]}"
863870

864871
if typ is None:
865-
# type isn't specified, this is a generic xref.
866-
# search *all* qapi-specific object types.
872+
# :any: lookup, search everything:
867873
objtypes: List[str] = list(self.object_types)
868874
else:
869875
# type is specified and will be a role (e.g. obj, mod, cmd)
870876
# convert this to eligible object types (e.g. command, module)
871877
# using the QAPIDomain.object_types table.
872878
objtypes = self.objtypes_for_role(typ, [])
873879

874-
if name in self.objects and self.objects[name].objtype in objtypes:
875-
names = [name]
876-
elif (
877-
fullname in self.objects
878-
and self.objects[fullname].objtype in objtypes
879-
):
880-
names = [fullname]
881-
else:
882-
# exact match wasn't found; e.g. we are searching for
883-
# `query-block` from a different (or no) module.
884-
searchname = "." + name
885-
names = [
886-
oname
887-
for oname in self.objects
888-
if oname.endswith(searchname)
889-
and self.objects[oname].objtype in objtypes
890-
]
880+
# ##
881+
# search!
882+
# ##
883+
884+
def _search(needle: str) -> List[str]:
885+
if (
886+
needle
887+
and needle in self.objects
888+
and self.objects[needle].objtype in objtypes
889+
):
890+
return [needle]
891+
return []
891892

892-
matches = [(oname, self.objects[oname]) for oname in names]
893+
if found := _search(name):
894+
# Exact match!
895+
pass
896+
elif found := _search(implicit_fqn):
897+
# Exact match using contextual information to fill in the gaps.
898+
pass
899+
else:
900+
# No exact hits, perform applicable fuzzy searches.
901+
searches = []
902+
903+
esc = tuple(re.escape(s) for s in parts)
904+
905+
# Try searching for ns:*.name or ns:name
906+
if explicit[0] and not explicit[1]:
907+
searches.append(f"^{esc[0]}:([^\\.]+\\.)?{esc[2]}$")
908+
# Try searching for *:module.name or module.name
909+
if explicit[1] and not explicit[0]:
910+
searches.append(f"(^|:){esc[1]}\\.{esc[2]}$")
911+
# Try searching for context-ns:*.name or context-ns:name
912+
if parts[0] and not (explicit[0] or explicit[1]):
913+
searches.append(f"^{esc[0]}:([^\\.]+\\.)?{esc[2]}$")
914+
# Try searching for *:context-mod.name or context-mod.name
915+
if parts[1] and not (explicit[0] or explicit[1]):
916+
searches.append(f"(^|:){esc[1]}\\.{esc[2]}$")
917+
# Try searching for *:name, *.name, or name
918+
if not (explicit[0] or explicit[1]):
919+
searches.append(f"(^|:|\\.){esc[2]}$")
920+
921+
for search in searches:
922+
if found := [
923+
oname
924+
for oname in self.objects
925+
if re.search(search, oname)
926+
and self.objects[oname].objtype in objtypes
927+
]:
928+
break
929+
930+
matches = [(oname, self.objects[oname]) for oname in found]
893931
if len(matches) > 1:
894932
matches = [m for m in matches if not m[1].aliased]
895933
return matches
@@ -904,8 +942,9 @@ def resolve_xref(
904942
node: pending_xref,
905943
contnode: Element,
906944
) -> nodes.reference | None:
945+
namespace = node.get("qapi:namespace")
907946
modname = node.get("qapi:module")
908-
matches = self.find_obj(modname, target, typ)
947+
matches = self.find_obj(namespace, modname, target, typ)
909948

910949
if not matches:
911950
# Normally, we could pass warn_dangling=True to QAPIXRefRole(),
@@ -958,7 +997,9 @@ def resolve_any_xref(
958997
contnode: Element,
959998
) -> List[Tuple[str, nodes.reference]]:
960999
results: List[Tuple[str, nodes.reference]] = []
961-
matches = self.find_obj(node.get("qapi:module"), target, None)
1000+
matches = self.find_obj(
1001+
node.get("qapi:namespace"), node.get("qapi:module"), target, None
1002+
)
9621003
for name, obj in matches:
9631004
rolename = self.role_for_objtype(obj.objtype)
9641005
assert rolename is not None

0 commit comments

Comments
 (0)