Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Intersphinx delegation to domains #8929

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cf6561c
intersphinx, refactoring preparing for delegation
jakobandersen Apr 14, 2024
4c5ab4d
fixup, rename process_disabled_reftypes
jakobandersen Apr 13, 2024
494ef25
intersphinx, initial delegation to Domain
jakobandersen Jun 10, 2022
bcc6a4e
fixup, use ClassVar for the intersphinx data
jakobandersen Apr 14, 2024
474b256
fixup, clarify the structure of data for adding
jakobandersen Apr 13, 2024
435fa82
fixup, typing fix
jakobandersen Apr 14, 2024
955655a
fixup, clarify the adjust objtypes method
jakobandersen Apr 14, 2024
e8d060b
fixup, clarify inner lookup method
jakobandersen Apr 14, 2024
c257a98
intersphinx, complete delegation for std
jakobandersen Jun 14, 2022
d32cb42
fixup, change adjust objtypes method type
jakobandersen Apr 14, 2024
6d6f077
fixup, clarify inner lookup method override
jakobandersen Apr 14, 2024
4a2a853
fixup, just return directly after the first match
jakobandersen Apr 14, 2024
cc13794
intersphinx, complete delegation for py
jakobandersen Jun 14, 2022
ed452c2
fixup, change adjust objtypes method type
jakobandersen Apr 14, 2024
5666f07
fixup, re-fix old bug in Python-related objtypes
jakobandersen Apr 13, 2024
74e08b6
intersphinx, bump env version
jakobandersen Jun 14, 2022
a635156
C, update intersphinx test with anon
jakobandersen Feb 23, 2021
bcf9c6b
C, intersphinx delegation
jakobandersen Feb 21, 2021
bbc2e18
fixup, various, e.g., typing and naming
jakobandersen Apr 13, 2024
99cbdcd
fixup, use ClassVar for intersphinx data
jakobandersen Apr 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions sphinx/domains/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

if TYPE_CHECKING:
from collections.abc import Iterable, Sequence
from typing import ClassVar

from docutils import nodes
from docutils.parsers.rst import Directive
Expand Down Expand Up @@ -201,6 +202,13 @@ class Domain:
data: dict
#: data version, bump this when the format of `self.data` changes
data_version = 0
#: Value for an empty inventory of objects for this domain.
#: It must be copy.deepcopy-able.
#: If this value is overridden, then the various intersphinx methods in the domain should
#: probably also be overridden.
#: Intersphinx is not inspecting this dictionary, so the domain has complete freedom in
#: the key and value type.
initial_intersphinx_inventory: ClassVar[dict] = {}

def __init__(self, env: BuildEnvironment) -> None:
self.env: BuildEnvironment = env
Expand Down Expand Up @@ -404,3 +412,128 @@ def get_enumerable_node_type(self, node: Node) -> str | None:
def get_full_qualified_name(self, node: Element) -> str | None:
"""Return full qualified name for given node."""
pass

def intersphinx_add_entries_v2(self, store: Any,
data: dict[str, dict[str, Any]]) -> None:
"""Store the given *data* for later intersphinx reference resolution.

This method is called at most once with all data loaded from inventories in
v1 and v2 format.

The *data* is a dictionary indexed by **object type**, i.e, a key from
:attr:`object_types`. The value is a dictionary indexed by **object name**,
i.e., the **name** part of each tuple returned by :meth:`get_objects`.
jakobandersen marked this conversation as resolved.
Show resolved Hide resolved
The value of those inner dictionaries are objects with intersphinx data,
that should not be inspected by the domain, but merely returned as a result
of reference resolution.

For example, for Python the data could look like the following.

.. code-block:: python

data = {
'class': {'pkg.mod.Foo': SomeIntersphinxData(...)},
'method': {'pkg.mod.Foo.bar': SomeIntersphinxData(...)},
}

The domain must store the given inner intersphinx data in the given *store*,
in whichever way makes sense for later reference resolution.
This *store* was initially a copy of :attr:`initial_intersphinx_inventory`.

Later in :meth:`intersphinx_resolve_xref` during intersphinx reference resolution,
the domain is again given the *store* object to perform the resolution.

.. versionadded:: 8.0
"""
store = cast(dict[str, dict[str, Any]], store)
assert len(store) == 0 # the method is called at most once
store.update(data) # update so the object is changed in-place

def _intersphinx_adjust_object_types(self, env: BuildEnvironment,
store: Any,
typ: str, target: str,
disabled_object_types: list[str],
node: pending_xref, contnode: Element,
objtypes: list[str]) -> None:
"""For implementing backwards compatibility.

This method is an internal implementation detail used in the std and python domains,
for implementing backwards compatibility.

The given *objtypes* is the list of object types computed based on the *typ*,
which will be used for lookup.
By overriding this method this list can be manipulated, e.g., adding types
that were removed in earlier Sphinx versions.
After this method returns, the types in *disabled_object_types* are removed
from *objtypes*. This final list is given to the lookup method.
"""
pass

def _intersphinx_resolve_xref_lookup(self, store: dict[str, dict[str, Any]],
target: str, objtypes: list[str]
) -> Any | None:
"""Default implementation for the actual lookup.

This method is an internal implementation detail, and may be overridden
by the bundled domains, e.g., std, for customizing the lookup, while
reusing the rest of the default implementation.
"""
for objtype in objtypes:
if objtype not in store:
continue
if target in store[objtype]:
return store[objtype][target]
return None

def intersphinx_resolve_xref(self, env: BuildEnvironment,
store: Any,
typ: str, target: str,
disabled_object_types: list[str],
node: pending_xref, contnode: Element
) -> Any | None:
"""Resolve the pending_xref *node* with the given *target* via intersphinx.

This method should perform lookup of the pending cross-reference
in the given *store*, but otherwise behave very similarly to :meth:`resolve_xref`.

The *typ* may be ``any`` if the cross-references comes from an any-role.

The *store* was created through a previous call to :meth:`intersphinx_add_entries_v2`.

The *disabled_object_types* is a list of object types that the reference may not
resolve to, per user request through :confval:`intersphinx_disabled_reftypes`.
These disabled object types are just the names of the types, without a domain prefix.

If a candidate is found in the store, the associated object must be returned.

If no candidates can be found, *None* can be returned; and subsequent event
handlers will be given a chance to resolve the reference.
The method can also raise :exc:`sphinx.environment.NoUri` to suppress
any subsequent resolution of this reference.

.. versionadded:: 8.0
"""
if typ == 'any':
objtypes = list(self.object_types)
else:
for_role = self.objtypes_for_role(typ)
if not for_role:
return None
objtypes = for_role

self._intersphinx_adjust_object_types(
env, store, typ, target, disabled_object_types, node, contnode, objtypes)
objtypes = [o for o in objtypes if o not in disabled_object_types]

typed_store = cast(dict[str, dict[str, Any]], store)
# we try the target either as is, or with full qualification based on the scope of node
res = self._intersphinx_resolve_xref_lookup(typed_store, target, objtypes)
if res is not None:
return res
# try with qualification of the current scope instead
full_qualified_name = self.get_full_qualified_name(node)
if full_qualified_name:
return self._intersphinx_resolve_xref_lookup(
typed_store, full_qualified_name, objtypes)
else:
return None
87 changes: 76 additions & 11 deletions sphinx/domains/c/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Any, ClassVar
from typing import TYPE_CHECKING, Any, ClassVar, cast

from docutils import nodes
from docutils.parsers.rst import directives
Expand All @@ -13,6 +13,7 @@
from sphinx.domains.c._ast import (
ASTDeclaration,
ASTIdentifier,
ASTIntersphinx_v2,
ASTNestedName,
)
from sphinx.domains.c._ids import _macroKeywords, _max_id
Expand All @@ -34,6 +35,7 @@

if TYPE_CHECKING:
from collections.abc import Iterator
from typing import ClassVar

from docutils.nodes import Element, Node, TextElement, system_message

Expand Down Expand Up @@ -666,6 +668,10 @@ class CDomain(Domain):
'objects': {}, # fullname -> docname, node_id, objtype
}

initial_intersphinx_inventory: ClassVar[dict[str, Symbol]] = {
'root_symbol': Symbol(None, None, None, None, None),
}

def clear_doc(self, docname: str) -> None:
if Symbol.debug_show_tree:
logger.debug("clear_doc: %s", docname)
Expand Down Expand Up @@ -712,9 +718,10 @@ def merge_domaindata(self, docnames: list[str], otherdata: dict[str, Any]) -> No
ourObjects[fullname] = (fn, id_, objtype)
# no need to warn on duplicates, the symbol merge already does that

def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
typ: str, target: str, node: pending_xref,
contnode: Element) -> tuple[Element | None, str | None]:
def _resolve_xref_in_tree(self, env: BuildEnvironment, root: Symbol,
softParent: bool,
typ: str, target: str,
node: pending_xref) -> tuple[Symbol, ASTNestedName]:
parser = DefinitionParser(target, location=node, config=env.config)
try:
name = parser.parse_xref_object()
Expand All @@ -723,20 +730,32 @@ def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder:
location=node)
return None, None
parentKey: LookupKey = node.get("c:parent_key", None)
rootSymbol = self.data['root_symbol']
if parentKey:
parentSymbol: Symbol = rootSymbol.direct_lookup(parentKey)
parentSymbol: Symbol = root.direct_lookup(parentKey)
if not parentSymbol:
logger.debug("Target: %s", target)
logger.debug("ParentKey: %s", parentKey)
logger.debug(rootSymbol.dump(1))
assert parentSymbol # should be there
if softParent:
parentSymbol = root
else:
msg = f"Target: {target}\nParentKey: {parentKey}\n{root.dump(1)}\n"
raise AssertionError(msg)
else:
parentSymbol = rootSymbol
parentSymbol = root
s = parentSymbol.find_declaration(name, typ,
matchSelf=True, recurseInAnon=True)
if s is None or s.declaration is None:
return None, None
# TODO: conditionally warn about xrefs with incorrect tagging?
jakobandersen marked this conversation as resolved.
Show resolved Hide resolved
return s, name

def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: Builder,
typ: str, target: str, node: pending_xref,
contnode: Element) -> tuple[Element, str]:
if Symbol.debug_lookup:
Symbol.debug_print("C._resolve_xref_inner(type={}, target={})".format(typ, target))
s, name = self._resolve_xref_in_tree(env, self.data['root_symbol'],
False, typ, target, node)
if s is None:
return None, None

# TODO: check role type vs. object type

Expand Down Expand Up @@ -779,6 +798,52 @@ def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]:
newestId = symbol.declaration.get_newest_id()
yield (name, dispname, objectType, docname, newestId, 1)

def intersphinx_add_entries_v2(self, store: dict[str, Symbol],
data: dict[str, dict[str, Any]]) -> None:
root = store['root_symbol']
for object_type, per_type_data in data.items():
for object_name, item_set in per_type_data.items():
parser = DefinitionParser(
object_name, location=('intersphinx', 0), config=self.env.config)
try:
ast = parser._parse_nested_name()
except DefinitionError as e:
logger.warning("Error in C entry in intersphinx inventory:\n%s", e)
continue
decl = ASTDeclaration(object_type, 'intersphinx',
ASTIntersphinx_v2(ast, item_set))
root.add_declaration(decl, docname="_$FakeIntersphinxDoc", line=0)

def _intersphinx_resolve_xref_inner(self, env: BuildEnvironment,
store: dict[str, Symbol],
target: str,
node: pending_xref,
typ: str) -> Any | None:
if Symbol.debug_lookup:
Symbol.debug_print(
f"C._intersphinx_resolve_xref_inner(type={typ}, target={target})")
s, name = self._resolve_xref_in_tree(env, store['root_symbol'],
True, typ, target, node)
if s is None:
return None
assert s.declaration is not None
decl = cast(ASTIntersphinx_v2, s.declaration.declaration)
return decl.data

def intersphinx_resolve_xref(self, env: BuildEnvironment,
store: dict[str, Symbol],
typ: str, target: str,
disabled_object_types: list[str],
node: pending_xref, contnode: Element
) -> Any | None:
if typ == 'any':
with logging.suppress_logging():
return self._intersphinx_resolve_xref_inner(
env, store, target, node, typ)
else:
return self._intersphinx_resolve_xref_inner(
env, store, target, node, typ)


def setup(app: Sphinx) -> ExtensionMetadata:
app.add_domain(CDomain)
Expand Down
23 changes: 23 additions & 0 deletions sphinx/domains/c/_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
DeclarationType = Union[
"ASTStruct", "ASTUnion", "ASTEnum", "ASTEnumerator",
"ASTType", "ASTTypeWithInit", "ASTMacro",
"ASTIntersphinx_v2",
]


Expand Down Expand Up @@ -1329,6 +1330,28 @@ def describe_signature(self, signode: TextElement, mode: str,
self.init.describe_signature(signode, 'markType', env, symbol)


class ASTIntersphinx_v2(ASTBaseBase):
def __init__(self, name: ASTNestedName, data: Any) -> None:
self.name = name
self.data = data

def _stringify(self, transform: StringifyTransform) -> str:
return transform(self.name) + " (has data)"

def get_id(self, version: int, objectType: str, symbol: "Symbol") -> str:
return symbol.get_full_nested_name().get_id(version)

def describe_signature(self, signode: TextElement, mode: str,
env: "BuildEnvironment", symbol: "Symbol") -> None:
raise AssertionError # should not happen

@property
def function_params(self) -> list[ASTFunctionParameter] | None:
# the v2 data does not contain actual declarations, but just names
# so return nothing here
return None


class ASTDeclaration(ASTBaseBase):
def __init__(self, objectType: str, directiveType: str | None,
declaration: DeclarationType | ASTFunctionParameter,
Expand Down
11 changes: 11 additions & 0 deletions sphinx/domains/python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,17 @@ def _make_module_refnode(self, builder: Builder, fromdocname: str, name: str,
return make_refnode(builder, fromdocname, module.docname, module.node_id,
contnode, title)

def _intersphinx_adjust_object_types(self, env: BuildEnvironment,
store: Any,
typ: str, target: str,
disabled_object_types: list[str],
node: pending_xref, contnode: Element,
objtypes: list[str]) -> None:
# we adjust the object types for backwards compatibility
if 'attribute' in objtypes and 'method' not in objtypes:
# Since Sphinx-2.1, properties are stored as py:method
objtypes.append('method')

def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]:
for modname, mod in self.modules.items():
yield (modname, modname, 'module', mod.docname, mod.node_id, 0)
Expand Down
32 changes: 32 additions & 0 deletions sphinx/domains/std/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,38 @@ def resolve_any_xref(self, env: BuildEnvironment, fromdocname: str,
labelid, contnode)))
return results

def _intersphinx_adjust_object_types(self, env: BuildEnvironment,
store: Any,
typ: str, target: str,
disabled_object_types: list[str],
node: pending_xref, contnode: Element,
objtypes: list[str]) -> None:
# we adjust the object types for backwards compatibility
if 'cmdoption' in objtypes:
# until Sphinx-1.6, cmdoptions are stored as std:option
objtypes.append('option')

def _intersphinx_resolve_xref_lookup(self, store: dict[str, dict[str, Any]],
target: str, objtypes: list[str]
) -> Any | None:
# Overridden as we also need to do case-insensitive lookup for label and term.
for objtype in objtypes:
if objtype not in store:
continue

if target in store[objtype]:
# Case-sensitive match, use it
return store[objtype][target]
elif objtype in ('label', 'term'):
# Some types require case-insensitive matches:
# * 'term': https://github.com/sphinx-doc/sphinx/issues/9291
# * 'label': https://github.com/sphinx-doc/sphinx/issues/12008
target_lower = target.lower()
jakobandersen marked this conversation as resolved.
Show resolved Hide resolved
for object_name, object_data in store[objtype].items():
if object_name.lower() == target_lower:
return object_data
return None

def get_objects(self) -> Iterator[tuple[str, str, str, str, str, int]]:
# handle the special 'doc' reference here
for doc in self.env.all_docs:
Expand Down
Loading
Loading