From 5fb83b50d1f24bb6acff41140863ac867667c04b Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Sat, 4 May 2024 15:18:37 +0200 Subject: [PATCH 1/4] Add documentation entries for type aliases Add a TypeAlias ir entry, repurpose Attribute renderer to also render type aliases, add a type alias directive and xref role. --- sphinx_js/directives.py | 17 +++++++++++++ sphinx_js/ir.py | 10 +++++++- sphinx_js/js/convertTopLevel.ts | 14 ++++++++--- sphinx_js/js/ir.ts | 8 +++++- sphinx_js/renderers.py | 42 +++++++++++++++++++------------ sphinx_js/templates/attribute.rst | 5 ++++ sphinx_js/typedoc.py | 2 ++ 7 files changed, 77 insertions(+), 21 deletions(-) diff --git a/sphinx_js/directives.py b/sphinx_js/directives.py index b93fcd4e..d5d161ca 100644 --- a/sphinx_js/directives.py +++ b/sphinx_js/directives.py @@ -371,6 +371,20 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str] return handle_typeparams_for_signature(self, sig, signode, keep_callsig=False) +class JSTypeAlias(JSObject): + doc_field_types = [ + GroupedField( + "typeparam", + label="Type parameters", + names=("typeparam",), + can_collapse=True, + ) + ] + + def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: + return handle_typeparams_for_signature(self, sig, signode, keep_callsig=False) + + class JSClass(JSConstructor): def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str]: return handle_typeparams_for_signature(self, sig, signode, keep_callsig=True) @@ -438,6 +452,9 @@ def add_directives(app: Sphinx) -> None: JavaScriptDomain.object_types["interface"] = ObjType(_("interface"), "interface") app.add_directive_to_domain("js", "interface", JSInterface) app.add_role_to_domain("js", "interface", JSXRefRole()) + JavaScriptDomain.object_types["typealias"] = ObjType(_("type alias"), "typealias") + app.add_directive_to_domain("js", "typealias", JSTypeAlias) + app.add_role_to_domain("js", "typealias", JSXRefRole()) app.add_node( desc_js_type_parameter_list, html=(visit_desc_js_type_parameter_list, depart_desc_js_type_parameter_list), diff --git a/sphinx_js/ir.py b/sphinx_js/ir.py index d12d1993..aae25ee4 100644 --- a/sphinx_js/ir.py +++ b/sphinx_js/ir.py @@ -203,6 +203,7 @@ class Module: functions: list["Function"] = Factory(list) classes: list["Class"] = Factory(list) interfaces: list["Interface"] = Factory(list) + type_aliases: list["TypeAlias"] = Factory(list) @define(slots=False) @@ -327,7 +328,14 @@ class Class(TopLevel, _MembersAndSupers): kind: Literal["class"] = "class" -TopLevelUnion = Class | Interface | Function | Attribute +@define +class TypeAlias(TopLevel): + type: Type + type_params: list[TypeParam] = Factory(list) + kind: Literal["typeAlias"] = "typeAlias" + + +TopLevelUnion = Class | Interface | Function | Attribute | TypeAlias # Now make a serializer/deserializer. # TODO: Add tests to make sure that serialization and deserialization are a diff --git a/sphinx_js/js/convertTopLevel.ts b/sphinx_js/js/convertTopLevel.ts index 4d30f207..c1b57f6f 100644 --- a/sphinx_js/js/convertTopLevel.ts +++ b/sphinx_js/js/convertTopLevel.ts @@ -421,8 +421,6 @@ export class Converter { object.kindOf( ReflectionKind.Module | ReflectionKind.Namespace | - // TODO: document TypeAliases - ReflectionKind.TypeAlias | // TODO: document enums ReflectionKind.Enum | ReflectionKind.EnumMember | @@ -436,7 +434,7 @@ export class Converter { // be too? return [undefined, (object as DeclarationReflection).children]; } - const kind = ReflectionKind.singularString(object.kind); + const kind = ReflectionKind[object.kind]; const convertFunc = `convert${kind}` as keyof this; if (!this[convertFunc]) { throw new Error(`No known converter for kind ${kind}`); @@ -875,4 +873,14 @@ export class Converter { description: renderCommentSummary(typeParam.comment), }; } + + convertTypeAlias(ty: DeclarationReflection): ConvertResult { + const ir: TopLevelIR = { + ...this.topLevelProperties(ty), + kind: "typeAlias", + type: this.convertType(ty.type!), + type_params: this.typeParamsToIR(ty.typeParameters), + }; + return [ir, ty.children]; + } } diff --git a/sphinx_js/js/ir.ts b/sphinx_js/js/ir.ts index d38813de..5ad0c9be 100644 --- a/sphinx_js/js/ir.ts +++ b/sphinx_js/js/ir.ts @@ -146,4 +146,10 @@ export type Class = TopLevel & kind: "class"; }; -export type TopLevelIR = Attribute | IRFunction | Class | Interface; +export type TypeAlias = TopLevel & { + kind: "typeAlias"; + type: Type; + type_params: TypeParam[]; +}; + +export type TopLevelIR = Attribute | IRFunction | Class | Interface | TypeAlias; diff --git a/sphinx_js/renderers.py b/sphinx_js/renderers.py index 03e36428..733c8c84 100644 --- a/sphinx_js/renderers.py +++ b/sphinx_js/renderers.py @@ -36,6 +36,7 @@ Return, TopLevel, Type, + TypeAlias, TypeParam, TypeXRef, TypeXRefInternal, @@ -337,7 +338,7 @@ def rst_nodes(self) -> list[Node]: def rst_for(self, obj: TopLevel) -> str: renderer_class: type match obj: - case Attribute(_): + case Attribute(_) | TypeAlias(_): renderer_class = AutoAttributeRenderer case Function(_): renderer_class = AutoFunctionRenderer @@ -374,7 +375,7 @@ def rst( result = "\n".join(lines) + "\n" return result - def _type_params(self, obj: Function | Class | Interface) -> str: + def _type_params(self, obj: Function | Class | TypeAlias | Interface) -> str: if not obj.type_params: return "" return "<{}>".format(", ".join(tp.name for tp in obj.type_params)) @@ -654,12 +655,24 @@ class AutoAttributeRenderer(JsRenderer): _template = "attribute.rst" _renderer_type = "attribute" - def _template_vars(self, name: str, obj: Attribute) -> dict[str, Any]: # type: ignore[override] + def _template_vars(self, name: str, obj: Attribute | TypeAlias) -> dict[str, Any]: # type: ignore[override] + is_optional = False + if isinstance(obj, Attribute): + is_optional = obj.is_optional + type_params = "" + is_type_alias = isinstance(obj, TypeAlias) + fields: Iterator[tuple[list[str], str]] = iter([]) + if isinstance(obj, TypeAlias): + type_params = self._type_params(obj) + fields = self._fields(obj) return dict( name=name, + is_type_alias=is_type_alias, + type_params=type_params, + fields=fields, description=render_description(obj.description), deprecated=obj.deprecated, - is_optional=obj.is_optional, + is_optional=is_optional, see_also=obj.see_alsos, examples=[render_description(ex) for ex in obj.examples], type=self.render_type(obj.type), @@ -667,6 +680,9 @@ def _template_vars(self, name: str, obj: Attribute) -> dict[str, Any]: # type: ) +_SECTION_ORDER = ["type_aliases", "attributes", "functions", "interfaces", "classes"] + + class AutoModuleRenderer(JsRenderer): def _parse_path(self, arg: str) -> None: # content, arguments, options, app: all need to be accessible to @@ -692,10 +708,8 @@ def rst( # type:ignore[override] ) -> str: rst: list[Sequence[str]] = [] rst.append([f".. js:module:: {''.join(partial_path)}"]) - rst.append(self.rst_for_group(obj.attributes)) - rst.append(self.rst_for_group(obj.functions)) - rst.append(self.rst_for_group(obj.classes)) - rst.append(self.rst_for_group(obj.interfaces)) + for group_name in _SECTION_ORDER: + rst.append(self.rst_for_group(getattr(obj, group_name))) return "\n\n".join(["\n\n".join(r) for r in rst]) @@ -715,19 +729,15 @@ def get_object(self) -> Module: def rst_nodes(self) -> list[Node]: module = self.get_object() - pairs: list[tuple[str, Iterable[TopLevel]]] = [ - ("attributes", module.attributes), - ("functions", module.functions), - ("classes", module.classes), - ("interfaces", module.interfaces), - ] pkgname = "".join(self._partial_path) result: list[Node] = [] - for group_name, group_objects in pairs: - n = nodes.container() + for group_name in _SECTION_ORDER: + group_objects = getattr(module, group_name) if not group_objects: continue + group_name = group_name.replace("_", " ") + n = nodes.container() n += self.format_heading(group_name.title() + ":") table_items = self.get_summary_table(pkgname, group_objects) n += self.format_table(table_items) diff --git a/sphinx_js/templates/attribute.rst b/sphinx_js/templates/attribute.rst index e9763e98..6408fa85 100644 --- a/sphinx_js/templates/attribute.rst +++ b/sphinx_js/templates/attribute.rst @@ -1,6 +1,11 @@ {% import 'common.rst' as common %} +{% if is_type_alias -%} +.. js:typealias:: {{ name }}{{ type_params }} +{%- else -%} .. js:attribute:: {{ name }}{{ '?' if is_optional else '' }} +{%- endif %} + {{ common.deprecated(deprecated)|indent(3) }} diff --git a/sphinx_js/typedoc.py b/sphinx_js/typedoc.py index 57720e77..ed173c7c 100644 --- a/sphinx_js/typedoc.py +++ b/sphinx_js/typedoc.py @@ -165,6 +165,7 @@ def _create_modules(self, ir_objects: Sequence[ir.TopLevel]) -> Iterable[ir.Modu "interface": "interfaces", "function": "functions", "attribute": "attributes", + "typeAlias": "type_aliases", } for obj, path, kind in self._get_toplevel_objects(ir_objects): pathparts = path.split("/") @@ -182,4 +183,5 @@ def _create_modules(self, ir_objects: Sequence[ir.TopLevel]) -> Iterable[ir.Modu mod.functions = sorted(mod.functions, key=attrgetter("name")) mod.classes = sorted(mod.classes, key=attrgetter("name")) mod.interfaces = sorted(mod.interfaces, key=attrgetter("name")) + mod.type_aliases = sorted(mod.type_aliases, key=attrgetter("name")) return modules.values() From c3984cbd6f80f579ec98af5d2cb0e102aca2fc26 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Sat, 4 May 2024 15:50:12 +0200 Subject: [PATCH 2/4] Update tests --- sphinx_js/directives.py | 2 +- tests/test_build_ts/source/module.ts | 10 +++ tests/test_build_ts/test_build_ts.py | 23 ++++++- tests/test_renderers.py | 61 ++++++++++++++++++- tests/test_typedoc_analysis/source/nodes.ts | 6 ++ .../test_typedoc_analysis.py | 13 +++- tests/test_typedoc_analysis/typedoc.json | 0 tests/testing.py | 1 + 8 files changed, 110 insertions(+), 6 deletions(-) create mode 100644 tests/test_typedoc_analysis/typedoc.json diff --git a/sphinx_js/directives.py b/sphinx_js/directives.py index d5d161ca..6915687f 100644 --- a/sphinx_js/directives.py +++ b/sphinx_js/directives.py @@ -373,7 +373,7 @@ def handle_signature(self, sig: str, signode: desc_signature) -> tuple[str, str] class JSTypeAlias(JSObject): doc_field_types = [ - GroupedField( + JSGroupedField( "typeparam", label="Type parameters", names=("typeparam",), diff --git a/tests/test_build_ts/source/module.ts b/tests/test_build_ts/source/module.ts index b8344df8..86bc22e4 100644 --- a/tests/test_build_ts/source/module.ts +++ b/tests/test_build_ts/source/module.ts @@ -57,6 +57,16 @@ export interface I {} */ export let interfaceInstance: I = {}; +/** + * A super special type alias + * @typeParam T The whatsit + */ +export type TestTypeAlias = { a: T }; +export type TestTypeAlias2 = { a: number }; + +export let t: TestTypeAlias; +export let t2: TestTypeAlias2; + /** * A function with a type parameter! * diff --git a/tests/test_build_ts/test_build_ts.py b/tests/test_build_ts/test_build_ts.py index 264c227e..8abd391b 100644 --- a/tests/test_build_ts/test_build_ts.py +++ b/tests/test_build_ts/test_build_ts.py @@ -273,6 +273,14 @@ def test_automodule(self): Another thing. +module.t + + type: "TestTypeAlias"<"A"> + +module.t2 + + type: "TestTypeAlias2" + module.zInstance type: "Z"<"A"> @@ -353,6 +361,19 @@ class module.Z(a, b) Documentation for the interface I *exported from* "module" + +module.TestTypeAlias + + type: { a: T; } + + A super special type alias + + Type parameters: + **T** -- The whatsit (extends "A") + +module.TestTypeAlias2 + + type: { a: number; } """ ), ) @@ -434,7 +455,7 @@ def test_autosummary(self): soup = BeautifulSoup(self._file_contents("autosummary"), "html.parser") attrs = soup.find(class_="attributes") rows = list(attrs.find_all("tr")) - assert len(rows) == 5 + assert len(rows) == 7 href = rows[0].find("a") assert href.get_text() == "a" diff --git a/tests/test_renderers.py b/tests/test_renderers.py index 6c16fb69..b234ca6a 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -14,6 +14,7 @@ Interface, Param, Return, + TypeAlias, TypeParam, TypeXRefExternal, TypeXRefInternal, @@ -133,6 +134,18 @@ def attribute_render(partial_path=None, use_short_name=False, **args): return attribute_render +@pytest.fixture() +def type_alias_render(attribute_renderer) -> Any: + def type_alias_render(partial_path=None, use_short_name=False, **args): + if not partial_path: + partial_path = ["blah"] + return attribute_renderer.rst( + partial_path, make_type_alias(**args), use_short_name + ) + + return type_alias_render + + top_level_dict = dict( name="", path=[], @@ -173,6 +186,7 @@ def attribute_render(partial_path=None, use_short_name=False, **args): ) ) attribute_dict = top_level_dict | member_dict | dict(type="") +type_alias_dict = top_level_dict | dict(type="", type_params=[]) def make_class(**args): @@ -191,6 +205,10 @@ def make_attribute(**args): return Attribute(**(attribute_dict | args)) +def make_type_alias(**args): + return TypeAlias(**(type_alias_dict | args)) + + DEFAULT_RESULT = ".. js:function:: blah()\n" @@ -301,6 +319,11 @@ def test_render_xref(function_renderer: AutoFunctionRenderer): function_renderer.render_type([TypeXRefInternal(name="A", path=["a.", "A"])]) == ":js:class:`A`" ) + function_renderer.objects["A"] = make_type_alias() + assert ( + function_renderer.render_type([TypeXRefInternal(name="A", path=["a.", "A"])]) + == ":js:typealias:`A`" + ) function_renderer.objects["A"] = make_interface() assert ( function_renderer.render_type([TypeXRefInternal(name="A", path=["a.", "A"])]) @@ -341,7 +364,7 @@ def test_func_render_param_type(function_render): """ ) assert function_render( - objects={"A": make_interface()}, + objects={"A": make_type_alias()}, params=[ Param( "a", @@ -354,7 +377,7 @@ def test_func_render_param_type(function_render): .. js:function:: blah(a) :param a: a description - :type a: :js:interface:`A` + :type a: :js:typealias:`A` """ ) @@ -501,3 +524,37 @@ def test_examples(function_render): Something python """ ) + + +def test_type_alias(type_alias_render): + assert type_alias_render() == ".. js:typealias:: blah\n" + assert type_alias_render( + type="number", description="my great type alias!" + ) == dedent( + """\ + .. js:typealias:: blah + + .. rst-class:: js attribute type + + type: **number** + + my great type alias! + """ + ) + assert type_alias_render( + type="string | T", + type_params=[TypeParam("T", extends="number", description="ABC")], + description="With a type parameter", + ) == dedent( + """\ + .. js:typealias:: blah + + .. rst-class:: js attribute type + + type: **string | T** + + With a type parameter + + :typeparam T: ABC (extends **number**) + """ + ) diff --git a/tests/test_typedoc_analysis/source/nodes.ts b/tests/test_typedoc_analysis/source/nodes.ts index a040a252..946fe332 100644 --- a/tests/test_typedoc_analysis/source/nodes.ts +++ b/tests/test_typedoc_analysis/source/nodes.ts @@ -54,3 +54,9 @@ export class Indexable { // Test that we don't fail on a reexport export { Blah } from "./exports"; + +/** + * A super special type alias + * @typeparam T The whatsit + */ +export type TestTypeAlias = 1 | 2 | T; diff --git a/tests/test_typedoc_analysis/test_typedoc_analysis.py b/tests/test_typedoc_analysis/test_typedoc_analysis.py index acc7a367..a15e7a1a 100644 --- a/tests/test_typedoc_analysis/test_typedoc_analysis.py +++ b/tests/test_typedoc_analysis/test_typedoc_analysis.py @@ -5,6 +5,7 @@ from sphinx_js.ir import ( Attribute, Class, + Description, DescriptionCode, DescriptionText, Function, @@ -12,6 +13,7 @@ Pathname, Return, Type, + TypeAlias, TypeParam, TypeXRef, TypeXRefExternal, @@ -30,12 +32,12 @@ def join_type(t: Type) -> str: return "".join(e.name if isinstance(e, TypeXRef) else e for e in t) -def join_descri(t: Type) -> str: +def join_description(t: Description) -> str: if not t: return "" if isinstance(t, str): return t - return "".join(e.name if isinstance(e, TypeXRef) else e for e in t) + return "".join(e.code if isinstance(e, DescriptionCode) else e.text for e in t) class TestPathSegments(TypeDocTestCase): @@ -356,6 +358,13 @@ def test_setter(self): assert isinstance(setter, Attribute) assert setter.type == [TypeXRefIntrinsic("string")] + def test_type_alias(self): + alias = self.analyzer.get_object(["TestTypeAlias"]) + assert isinstance(alias, TypeAlias) + assert join_description(alias.description) == "A super special type alias" + assert join_type(alias.type) == "1 | 2 | T" + assert alias.type_params == [TypeParam(name="T", extends=None, description=[])] + class TestTypeName(TypeDocAnalyzerTestCase): """Make sure our rendering of TypeScript types into text works.""" diff --git a/tests/test_typedoc_analysis/typedoc.json b/tests/test_typedoc_analysis/typedoc.json new file mode 100644 index 00000000..e69de29b diff --git a/tests/testing.py b/tests/testing.py index 2bc6dd16..52c3b437 100644 --- a/tests/testing.py +++ b/tests/testing.py @@ -55,6 +55,7 @@ def _file_contents(self, filename): def _file_contents_eq(self, filename, contents): __tracebackhide__ = True + print(self._file_contents(filename)) assert self._file_contents(filename) == contents From 806201d5f901ccd8f1204646e33daa899d3a119d Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Mon, 6 May 2024 16:18:46 +0200 Subject: [PATCH 3/4] Remove useless file --- tests/test_typedoc_analysis/typedoc.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/test_typedoc_analysis/typedoc.json diff --git a/tests/test_typedoc_analysis/typedoc.json b/tests/test_typedoc_analysis/typedoc.json deleted file mode 100644 index e69de29b..00000000 From f19a2a23950ba5086cf3e06943a71fc1421d2d46 Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Mon, 6 May 2024 16:26:18 +0200 Subject: [PATCH 4/4] Add tests and fixes --- sphinx_js/renderers.py | 3 +- tests/test_build_ts/test_build_ts.py | 42 +++++++++++++++------------- 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/sphinx_js/renderers.py b/sphinx_js/renderers.py index 733c8c84..cbc52604 100644 --- a/sphinx_js/renderers.py +++ b/sphinx_js/renderers.py @@ -736,9 +736,8 @@ def rst_nodes(self) -> list[Node]: group_objects = getattr(module, group_name) if not group_objects: continue - group_name = group_name.replace("_", " ") n = nodes.container() - n += self.format_heading(group_name.title() + ":") + n += self.format_heading(group_name.replace("_", " ").title() + ":") table_items = self.get_summary_table(pkgname, group_objects) n += self.format_table(table_items) n["classes"] += ["jssummarytable", group_name] diff --git a/tests/test_build_ts/test_build_ts.py b/tests/test_build_ts/test_build_ts.py index 8abd391b..b6b8c961 100644 --- a/tests/test_build_ts/test_build_ts.py +++ b/tests/test_build_ts/test_build_ts.py @@ -249,6 +249,19 @@ def test_automodule(self): "automodule", dedent( """\ +module.TestTypeAlias + + type: { a: T; } + + A super special type alias + + Type parameters: + **T** -- The whatsit (extends "A") + +module.TestTypeAlias2 + + type: { a: number; } + module.a type: 7 @@ -317,6 +330,12 @@ def test_automodule(self): Returns: number +interface module.I + + Documentation for the interface I + + *exported from* "module" + class module.A() This is a summary. This is more info. @@ -355,25 +374,6 @@ class module.Z(a, b) type: T Z.z() - -interface module.I - - Documentation for the interface I - - *exported from* "module" - -module.TestTypeAlias - - type: { a: T; } - - A super special type alias - - Type parameters: - **T** -- The whatsit (extends "A") - -module.TestTypeAlias2 - - type: { a: number; } """ ), ) @@ -492,3 +492,7 @@ def test_autosummary(self): classes.find(class_="summary").get_text() == "Documentation for the interface I" ) + + classes = soup.find(class_="type_aliases") + assert classes + assert classes.find(class_="summary").get_text() == "A super special type alias"