From 781dc61870a802222051bcc8f6b2a592cd5eec7b Mon Sep 17 00:00:00 2001 From: Hood Chatham Date: Tue, 31 Dec 2024 12:54:15 +0100 Subject: [PATCH] Add @omitFromAutoModule and @summaryLink tags to control autodoc output (#178) --- sphinx_js/js/cli.ts | 4 +- sphinx_js/renderers.py | 42 +++--- tests/test_build_ts/source/module.ts | 8 +- tests/test_build_ts/test_build_ts.py | 18 ++- tests/test_renderers.py | 124 +++++++++++++++--- tests/test_typedoc_analysis/source/types.ts | 8 ++ .../test_typedoc_analysis.py | 10 ++ 7 files changed, 165 insertions(+), 49 deletions(-) diff --git a/sphinx_js/js/cli.ts b/sphinx_js/js/cli.ts index 88cc42dd..9e5d6549 100644 --- a/sphinx_js/js/cli.ts +++ b/sphinx_js/js/cli.ts @@ -53,8 +53,8 @@ async function makeApp(args: string[]): Promise { throw new ExitError(ExitCodes.Ok); } app.extraData = {}; - app.options.getValue("modifierTags").push("@hidetype"); - app.options.getValue("blockTags").push("@destructure"); + app.options.getValue("modifierTags").push("@hidetype", "@omitFromAutoModule"); + app.options.getValue("blockTags").push("@destructure", "@summaryLink"); return app; } diff --git a/sphinx_js/renderers.py b/sphinx_js/renderers.py index e8de493d..987733da 100644 --- a/sphinx_js/renderers.py +++ b/sphinx_js/renderers.py @@ -710,7 +710,11 @@ def get_object(self) -> Module: # type:ignore[override] return analyzer._modules_by_path.get(self._partial_path) def rst_for_group(self, objects: Iterable[TopLevel]) -> list[str]: - return [self.rst_for(obj) for obj in objects] + return [ + self.rst_for(obj) + for obj in objects + if "@omitFromAutoModule" not in obj.modifier_tags + ] def rst( # type:ignore[override] self, @@ -722,7 +726,7 @@ def rst( # type:ignore[override] rst.append([f".. js:module:: {''.join(partial_path)}"]) 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]) + return "\n\n".join(["\n\n".join(r) for r in rst if r]) class AutoSummaryRenderer(Renderer): @@ -787,26 +791,27 @@ def get_sig(self, obj: TopLevel) -> str: else: return "" - def get_summary_row( - self, pkgname: str, obj: TopLevel - ) -> tuple[str, str, str, str, str, str]: + def get_summary_row(self, pkgname: str, obj: TopLevel) -> tuple[str, str, str]: """Get the summary table row for obj. The output is designed to be input to format_table. The link name needs to be set up so that :any:`link_name` makes a link to the actual API docs for this object. """ - sig = self.get_sig(obj) display_name = obj.name prefix = "**async** " if getattr(obj, "is_async", False) else "" qualifier = "any" - summary = self.extract_summary(render_description(obj.description)) link_name = pkgname + "." + display_name - return (prefix, qualifier, display_name, sig, summary, link_name) + main = f"{prefix}:{qualifier}:`{display_name} <{link_name}>`" + if slink := obj.block_tags.get("summaryLink"): + main = render_description(slink[0]) + sig = self.get_sig(obj) + summary = self.extract_summary(render_description(obj.description)) + return (main, sig, summary) def get_summary_table( self, pkgname: str, group: Iterable[TopLevel] - ) -> list[tuple[str, str, str, str, str, str]]: + ) -> list[tuple[str, str, str]]: """Get the data for a summary tget_summary_tableable. Return value is set up to be an argument of format_table. """ @@ -818,9 +823,7 @@ def get_summary_table( # We have to change the value of one string: qualifier = 'obj ==> # qualifier = 'any' # https://github.com/sphinx-doc/sphinx/blob/6.0.x/sphinx/ext/autosummary/__init__.py#L375 - def format_table( - self, items: list[tuple[str, str, str, str, str, str]] - ) -> list[Node]: + def format_table(self, items: list[tuple[str, str, str]]) -> list[Node]: """Generate a proper list of table nodes for autosummary:: directive. *items* is a list produced by :meth:`get_items`. @@ -857,16 +860,13 @@ def append_row(column_texts: list[tuple[str, str]]) -> None: row.append(entry) body.append(row) - for prefix, qualifier, name, sig, summary, real_name in items: + for name, sig, summary in items: # The body of this loop is changed from copied code. - sig = rst.escape(sig) - if sig: - sig = f"**{sig}**" if "nosignatures" not in self._options: - col1 = rf"{prefix}:{qualifier}:`{name} <{real_name}>`\ {sig}" - else: - col1 = f"{prefix}:{qualifier}:`{name} <{real_name}>`" - col2 = summary - append_row([(col1, "name"), (col2, "summary")]) + sig = rst.escape(sig) + if sig: + sig = f"**{sig}**" + name = rf"{name}\ {sig}" + append_row([(name, "name"), (summary, "summary")]) return [table_spec, table] diff --git a/tests/test_build_ts/source/module.ts b/tests/test_build_ts/source/module.ts index 86bc22e4..58ffc0cf 100644 --- a/tests/test_build_ts/source/module.ts +++ b/tests/test_build_ts/source/module.ts @@ -63,6 +63,12 @@ export let interfaceInstance: I = {}; */ export type TestTypeAlias = { a: T }; export type TestTypeAlias2 = { a: number }; +/** + * Omit from automodule and send summary link somewhere else + * @omitFromAutoModule + * @summaryLink :js:typealias:`TestTypeAlias3 ` + */ +export type TestTypeAlias3 = { a: number }; export let t: TestTypeAlias; export let t2: TestTypeAlias2; @@ -70,7 +76,7 @@ export let t2: TestTypeAlias2; /** * A function with a type parameter! * - * We'll refer to ourselves: :js:func:`functionWithTypeParam` + * We'll refer to ourselves: :js:func:`~module.functionWithTypeParam` * * @typeParam T The type parameter * @typeParam S Another type param diff --git a/tests/test_build_ts/test_build_ts.py b/tests/test_build_ts/test_build_ts.py index c52db5c9..e30b6cc2 100644 --- a/tests/test_build_ts/test_build_ts.py +++ b/tests/test_build_ts/test_build_ts.py @@ -490,12 +490,20 @@ def test_autosummary(self): classes = soup.find(class_="classes") assert classes.find(class_="summary").get_text() == "This is a summary." - classes = soup.find(class_="interfaces") + interfaces = soup.find(class_="interfaces") assert ( - classes.find(class_="summary").get_text() + interfaces.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" + type_aliases = soup.find(class_="type_aliases") + assert type_aliases + assert ( + type_aliases.find(class_="summary").get_text() + == "A super special type alias" + ) + rows = list(type_aliases.find_all("tr")) + assert len(rows) == 3 + href = rows[2].find("a") + assert href.get_text() == "TestTypeAlias3" + assert href["href"] == "automodule.html#module.TestTypeAlias" diff --git a/tests/test_renderers.py b/tests/test_renderers.py index e9fe7830..ff60b07d 100644 --- a/tests/test_renderers.py +++ b/tests/test_renderers.py @@ -12,6 +12,7 @@ Exc, Function, Interface, + Module, Param, Return, TypeAlias, @@ -22,6 +23,7 @@ from sphinx_js.renderers import ( AutoAttributeRenderer, AutoFunctionRenderer, + AutoModuleRenderer, render_description, ) @@ -68,23 +70,26 @@ def ts_xref_formatter(config, xref): return xref.name -@pytest.fixture() -def function_renderer(): - class _config: - pass - +def make_renderer(cls): class _app: - config = _config - - def lookup_object(self, partial_path: list[str]): - return self.objects[partial_path[-1]] + class config: + ts_type_xref_formatter = ts_xref_formatter - renderer = AutoFunctionRenderer.__new__(AutoFunctionRenderer) + renderer = cls.__new__(cls) renderer._app = _app renderer._explicit_formal_params = None renderer._content = [] renderer._set_type_xref_formatter(ts_xref_formatter) renderer._add_span = False + return renderer + + +@pytest.fixture() +def function_renderer(): + def lookup_object(self, partial_path: list[str]): + return self.objects[partial_path[-1]] + + renderer = make_renderer(AutoFunctionRenderer) renderer.lookup_object = lookup_object.__get__(renderer) renderer.objects = {} return renderer @@ -92,18 +97,20 @@ def lookup_object(self, partial_path: list[str]): @pytest.fixture() def attribute_renderer(): - class _config: - pass + return make_renderer(AutoAttributeRenderer) - class _app: - config = _config - renderer = AutoAttributeRenderer.__new__(AutoAttributeRenderer) - renderer._app = _app - renderer._explicit_formal_params = None - renderer._content = [] - renderer._set_type_xref_formatter(ts_xref_formatter) - renderer._add_span = False +@pytest.fixture() +def auto_module_renderer(): + renderer = make_renderer(AutoModuleRenderer) + + class directive: + class state: + class document: + class settings: + pass + + renderer._directive = directive return renderer @@ -146,6 +153,18 @@ def type_alias_render(partial_path=None, use_short_name=False, **args): return type_alias_render +@pytest.fixture() +def auto_module_render(auto_module_renderer) -> Any: + def auto_module_render(partial_path=None, use_short_name=False, **args): + if not partial_path: + partial_path = ["blah"] + return auto_module_renderer.rst( + partial_path, make_module(**args), use_short_name + ) + + return auto_module_render + + top_level_dict = dict( name="", path=[], @@ -187,6 +206,17 @@ def type_alias_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=[]) +module_dict = dict( + filename="", + deppath=None, + path=[], + line=0, + attributes=[], + functions=[], + classes=[], + interfaces=[], + type_aliases=[], +) def make_class(**args): @@ -209,6 +239,10 @@ def make_type_alias(**args): return TypeAlias(**(type_alias_dict | args)) +def make_module(**args): + return Module(**(module_dict | args)) + + DEFAULT_RESULT = ".. js:function:: blah()\n" @@ -562,3 +596,53 @@ def test_type_alias(type_alias_render): :typeparam T: ABC (extends **number**) """ ) + + +def test_auto_module_render(auto_module_render): + assert auto_module_render() == ".. js:module:: blah" + assert auto_module_render( + functions=[ + make_function( + name="f", + description="this is a description", + params=[Param("a", description="a description")], + ), + make_function(name="g"), + ], + attributes=[make_attribute(name="x", type="any"), make_attribute(name="y")], + type_aliases=[ + make_type_alias(name="S"), + make_type_alias(name="T"), + # Check that we omit stuff marked with @omitFromAutoModule + make_type_alias(name="U", modifier_tags=["@omitFromAutoModule"]), + ], + ) == dedent( + """\ + .. js:module:: blah + + .. js:typealias:: S + + + .. js:typealias:: T + + + .. js:attribute:: x + + .. rst-class:: js attribute type + + type: **any** + + + .. js:attribute:: y + + + .. js:function:: f(a) + + this is a description + + :param a: a description + + + .. js:function:: g() + """ + ) diff --git a/tests/test_typedoc_analysis/source/types.ts b/tests/test_typedoc_analysis/source/types.ts index eabcad64..dc83b5a7 100644 --- a/tests/test_typedoc_analysis/source/types.ts +++ b/tests/test_typedoc_analysis/source/types.ts @@ -246,3 +246,11 @@ export type MappedType3 = { readonly [property in keys]-?: number }; export type TemplateLiteral = `${number}: ${string}`; export type OptionalType = [number?]; + +/** + * @hidetype + * @omitFromAutoModule + * @destructure a.b + * @summaryLink :role:`target` + */ +export type CustomTags = {}; diff --git a/tests/test_typedoc_analysis/test_typedoc_analysis.py b/tests/test_typedoc_analysis/test_typedoc_analysis.py index 12bc48f8..44696bec 100644 --- a/tests/test_typedoc_analysis/test_typedoc_analysis.py +++ b/tests/test_typedoc_analysis/test_typedoc_analysis.py @@ -742,3 +742,13 @@ def test_template_literal(self): obj = self.analyzer.get_object(["TemplateLiteral"]) assert isinstance(obj, TypeAlias) assert join_type(obj.type) == "`${number}: ${string}`" + + def test_custom_tags(self): + obj = self.analyzer.get_object(["CustomTags"]) + assert isinstance(obj, TypeAlias) + assert "@hidetype" in obj.modifier_tags + assert "@omitFromAutoModule" in obj.modifier_tags + assert [join_description(d) for d in obj.block_tags["summaryLink"]] == [ + ":role:`target`" + ] + assert [join_description(d) for d in obj.block_tags["destructure"]] == ["a.b"]