Skip to content

Python domain considers all function/method/class argument annotations to be "class", when some are "data" #13308

Open
@coretl

Description

@coretl

Describe the bug

This is somewhat related to a number of issues in #11991, but is wider than just autodoc.

It also affects sphinx-autodoc2 users as TypeVars are created as py:data rather than py:class.

When creating an xref from an argument annotation, this is called:

if reftarget == 'None' or reftarget.startswith('typing.'):
# typing module provides non-class types. Obj reference is good to refer them.
reftype = 'obj'
else:
reftype = 'class'
return reftype, reftarget, title, refspecific

This means that if you write code like:

T = int | str

class Foo:
    def __init__(t: T): ...

then the python domain expects T to be a class, but it is categorized as data:

WARNING: py:class reference target not found: foo.T [ref.class]

One possible solution is to widen the reftype in parse_reftarget to "obj" in every case, not just for the typing module:

def parse_reftarget(
    reftarget: str, suppress_prefix: bool = False
) -> tuple[str, str, str, bool]:
    """Parse a type string and return (reftype, reftarget, title, refspecific flag)"""
    refspecific = False
    if reftarget.startswith('.'):
        reftarget = reftarget[1:]
        title = reftarget
        refspecific = True
    elif reftarget.startswith('~'):
        reftarget = reftarget[1:]
        title = reftarget.split('.')[-1]
    elif suppress_prefix:
        title = reftarget.split('.')[-1]
    elif reftarget.startswith('typing.'):
        title = reftarget[7:]
    else:
        title = reftarget

    return 'obj', reftarget, title, refspecific

This would fix it for my application, but would break applications that create an attribute with the same name as a builtin, so would probably need a new configuration variable.

Another possible solution would be to get sphinx-autodoc2 to make all TypeVars or unions reftype "class" rather than "data", although that may also be wrong.

I hope you don't mind me tagging you @chrisjsewell but maybe as author of sphinx-autodoc2 you may have an opinion?

How to Reproduce

index
=====

.. py:module:: foo

.. py:data:: T
   :value: TypeVar(...)

.. py:class:: Foo(t: T)
# conf.py
nitpicky = True

Environment Information

Platform:              linux; (Linux-4.18.0-513.24.1.el8_9.x86_64-x86_64-with-glibc2.28)
Python version:        3.11.5 (main, Sep 22 2023, 15:34:29) [GCC 8.5.0 20210514 (Red Hat 8.5.0-20)])
Python implementation: CPython
Sphinx version:        8.2.0+/1ab62c9b0
Docutils version:      0.21.2
Jinja2 version:        3.1.5
Pygments version:      2.19.1

Sphinx extensions

[]

Additional context

I am working around this problem with the following in conf.py:

from sphinx import addnodes, application, environment
from sphinx.ext import intersphinx

# A custom handler for TypeVars and Unions
def missing_reference_handler(
    app: application.Sphinx,
    env: environment.BuildEnvironment,
    node: addnodes.pending_xref,
    contnode,
):
    target = node["reftarget"]
    if "." in target and node["reftype"] == "class":
        # Try again as `obj` so we pick up Unions, TypeVars and other things
        if target.startswith("ophyd_async"):
            # Pick it up from our domain
            domain = env.domains[node["refdomain"]]
            refdoc = node.get("refdoc")
            return domain.resolve_xref(
                env, refdoc, app.builder, "obj", target, node, contnode
            )
        else:
            # pass it to intersphinx with the right type
            node["reftype"] = "obj"
            return intersphinx.missing_reference(app, env, node, contnode)


def setup(app: application.Sphinx):
    app.connect("missing-reference", missing_reference_handler)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions