File: typevars.py

package info (click to toggle)
python-trio 0.29.0-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 2,920 kB
  • sloc: python: 28,766; sh: 144; makefile: 25
file content (106 lines) | stat: -rw-r--r-- 3,821 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
"""Transform references to typevars to avoid missing reference errors.

See https://github.com/sphinx-doc/sphinx/issues/7722 also.
"""

from __future__ import annotations

import re
from pathlib import Path
from typing import TYPE_CHECKING

import trio
from sphinx.errors import NoUri

if TYPE_CHECKING:
    from sphinx.addnodes import Element, pending_xref
    from sphinx.application import Sphinx
    from sphinx.environment import BuildEnvironment


def identify_typevars(trio_folder: Path) -> None:
    """Record all typevars in trio."""
    for filename in trio_folder.rglob("*.py"):
        with open(filename, encoding="utf8") as f:
            for line in f:
                # A simple regex should be sufficient to find them all, no need to actually parse.
                match = re.search(
                    r"\b(TypeVar|TypeVarTuple|ParamSpec)\(['\"]([^'\"]+)['\"]",
                    line,
                )
                if match is not None:
                    relative = "trio" / filename.relative_to(trio_folder)
                    relative = relative.with_suffix("")
                    if relative.name == "__init__":  # Package, remove.
                        relative = relative.parent
                    kind = match.group(1)
                    name = match.group(2)
                    typevars_qualified[f'{".".join(relative.parts)}.{name}'] = kind
                    existing = typevars_named.setdefault(name, kind)
                    if existing != kind:
                        print("Mismatch: {} = {}, {}", name, existing, kind)


# All our typevars, so we can suppress reference errors for them.
typevars_qualified: dict[str, str] = {}
typevars_named: dict[str, str] = {}


def lookup_reference(
    app: Sphinx,
    env: BuildEnvironment,
    node: pending_xref,
    contnode: Element,
) -> Element | None:
    """Handle missing references."""
    # If this is a typing_extensions object, redirect to typing.
    # Most things there are backports, so the stdlib docs should have an entry.
    target: str = node["reftarget"]
    if target.startswith("typing_extensions."):
        new_node = node.copy()
        new_node["reftarget"] = f"typing.{target[18:]}"
        # This fires off this same event, with our new modified node in order to fetch the right
        # URL to use.
        return app.emit_firstresult(  # type: ignore[no-any-return]
            "missing-reference",
            env,
            new_node,
            contnode,
            allowed_exceptions=(NoUri,),
        )

    try:
        typevar_type = typevars_qualified[target]
    except KeyError:
        # Imports might mean the typevar was defined in a different module or something.
        # Fall back to checking just by name.
        dot = target.rfind(".")
        stem = target[dot + 1 :] if dot >= 0 else target
        try:
            typevar_type = typevars_named[stem]
        except KeyError:
            # Let other handlers deal with this name, it's not a typevar.
            return None

    # Found a typevar. Redirect to the stdlib docs for that kind of var.
    new_node = node.copy()
    new_node["reftarget"] = f"typing.{typevar_type}"
    new_node = app.emit_firstresult(
        "missing-reference",
        env,
        new_node,
        contnode,
        allowed_exceptions=(NoUri,),
    )
    reftitle = new_node["reftitle"]
    # Is normally "(in Python 3.XX)", make it say typevar/paramspec/etc
    paren = "(" if reftitle.startswith("(") else ""
    new_node["reftitle"] = f"{paren}{typevar_type}, {reftitle.lstrip('(')}"
    # Add a CSS class, for restyling.
    new_node["classes"].append("typevarref")
    return new_node


def setup(app: Sphinx) -> None:
    identify_typevars(Path(trio.__file__).parent)
    app.connect("missing-reference", lookup_reference, -10)