Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions great_docs/_apiref/_render/_label.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from typing import TYPE_CHECKING

from .._type_checks import is_typevar

if TYPE_CHECKING:
import griffe as gf

Expand Down Expand Up @@ -108,6 +110,7 @@ def _attribute_label(obj: gf.Attribute) -> str:
"TypeVar" in annotation_text
or "ParamSpec" in annotation_text
or "TypeVarTuple" in annotation_text
or is_typevar(obj)
):
return "typevar"
elif "property" in obj.labels:
Expand Down
17 changes: 12 additions & 5 deletions great_docs/_apiref/_type_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,16 +135,23 @@ def is_enum(obj: gf.Object | gf.Alias) -> bool:
)


_TYPE_VARIABLE_KINDS = frozenset({"TypeVar", "ParamSpec", "TypeVarTuple"})


def is_typevar(obj: gf.Object | gf.Alias) -> bool:
"""
Whether `obj` is a declaration of a `TypeVar`
Whether `obj` is a declaration of a type variable

A type variable is a `TypeVar`, `ParamSpec` or `TypeVarTuple`, called by
its bare name or through any module, such as `typing.TypeVar`.
"""
target = _resolved(obj)
if not (isinstance(target, gf.Attribute) and isinstance(target.value, gf.ExprCall)):
return False
function = target.value.function
return (
isinstance(target, gf.Attribute)
and isinstance(target.value, gf.ExprCall)
and isinstance(target.value.function, gf.ExprName)
and target.value.function.name == "TypeVar"
isinstance(function, (gf.ExprName, gf.ExprAttribute))
and function.canonical_path.rpartition(".")[2] in _TYPE_VARIABLE_KINDS
)


Expand Down
8 changes: 8 additions & 0 deletions great_docs/assets/great-docs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ $label-exception-color: mix($primary, $red-600, 20%);
$label-property-color: mix($primary, $teal-600, 20%);
$label-constant-color: mix($primary, $orange-500, 20%);
$label-typealias-color: mix($primary, $yellow-500, 20%);
$label-typevar-color: mix($primary, $purple-500, 20%);
$label-cli-color: mix($primary, $green-600, 20%);
$label-cli-group-color: mix($primary, $green-600, 20%);
$label-mcp-tool-color: #e6194B;
Expand Down Expand Up @@ -555,6 +556,7 @@ $all_labels: (
property,
constant,
typealias,
typevar,
cli,
cli-group,
mcp-tool,
Expand Down Expand Up @@ -632,6 +634,12 @@ $method-like-labels: (method, classmethod, staticmethod);
background-color: tint-color($label-typealias-color, 95%);
}

:is(.content, .quarto-secondary-nav-title) span.doc-label-typevar::after {
color: $label-typevar-color;
border-color: $label-typevar-color;
background-color: tint-color($label-typevar-color, 95%);
}

:is(.content, .quarto-secondary-nav-title) span.doc-label-cli::after {
color: $label-cli-color;
border-color: $label-cli-color;
Expand Down
15 changes: 15 additions & 0 deletions tests/renderer/test_apiref_internals.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,21 @@ def test_is_typevar_follows_a_reexported_typevar():
assert is_typevar(_reexported(source, "T")) is True


@pytest.mark.parametrize(
"source",
[
'import typing\nT = typing.TypeVar("T")\n',
'import typing as t\nT = t.TypeVar("T")\n',
'import typing_extensions\nT = typing_extensions.TypeVar("T")\n',
'from typing import ParamSpec\nT = ParamSpec("T")\n',
'from typing import TypeVarTuple\nT = TypeVarTuple("T")\n',
],
)
def test_is_typevar_recognises_qualified_and_variadic_spellings(source: str):
"""Recognise every kind of type variable under any import spelling"""
assert is_typevar(_reexported(source, "T")) is True


def test_classification_of_an_unloadable_target_is_false():
"""Return `False` when an alias target cannot be loaded"""
modules = {"__init__.py": "from never_imported_package import Contract\n"}
Expand Down
16 changes: 12 additions & 4 deletions tests/renderer/test_type_aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,20 @@ def test_label_legacy_spelling():
assert get_label(obj) == "typealias"


def test_label_bare_typevar_is_constant():
"""A bare `T = TypeVar("T")` has no annotation, so it labels as a constant"""
@pytest.mark.parametrize(
("code", "name"),
[
('from typing import TypeVar\nT = TypeVar("T")\n', "T"),
('import typing\nT = typing.TypeVar("T")\n', "T"),
('from typing import ParamSpec\nP = ParamSpec("P")\n', "P"),
('from typing import TypeVarTuple\nTs = TypeVarTuple("Ts")\n', "Ts"),
],
)
def test_label_type_variable_without_annotation(code: str, name: str):
"""Label a type variable by its declared value when it has no annotation"""
from great_docs._apiref._render._label import get_label

code = 'from typing import TypeVar\nT = TypeVar("T")\n'
assert get_label(_load_member(code, "T")) == "constant"
assert get_label(_load_member(code, name)) == "typevar"


def test_label_plain_constant_still_works():
Expand Down
11 changes: 10 additions & 1 deletion tests/test_gdg_rendered.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,7 @@ def test_reference_pages_have_type_badge(pkg_name: str):
"constant",
"type_alias",
"typealias",
"typevar",
"dataclass",
"exception",
"protocol",
Expand Down Expand Up @@ -551,7 +552,15 @@ def test_function_pages_have_signature(pkg_name: str):
if badge_text is None:
continue

if badge_text in ("constant", "type_alias", "typealias", "enum", "namedtuple", "typeddict"):
if badge_text in (
"constant",
"type_alias",
"typealias",
"typevar",
"enum",
"namedtuple",
"typeddict",
):
continue

# Callable names use `sig-name` in `plain` signatures and `fu` in highlighted ones.
Expand Down
Loading