-
Notifications
You must be signed in to change notification settings - Fork 67
Expand file tree
/
Copy pathtest_python_ref_builder.py
More file actions
400 lines (325 loc) · 15 KB
/
test_python_ref_builder.py
File metadata and controls
400 lines (325 loc) · 15 KB
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
# (C) 2026 GoodData Corporation
from __future__ import annotations
import sys
from pathlib import Path
import pytest
# The scripts/docs directory is not a package — add it to sys.path so we can import.
# The module also reads *_template.md at import time from cwd, so we chdir into docs/.
_SCRIPTS_DOCS = Path(__file__).resolve().parent.parent
_DOCS_DIR = _SCRIPTS_DOCS.parent.parent / "docs"
@pytest.fixture(autouse=True)
def _chdir_to_docs(monkeypatch: pytest.MonkeyPatch) -> None:
"""python_ref_builder reads template files relative to cwd at import time."""
monkeypatch.chdir(_DOCS_DIR)
if str(_SCRIPTS_DOCS) not in sys.path:
monkeypatch.syspath_prepend(str(_SCRIPTS_DOCS))
@pytest.fixture()
def _mod():
"""Lazily import the module (after cwd/syspath are set)."""
import python_ref_builder as mod # noqa: PLC0415
return mod
# ---------------------------------------------------------------------------
# Sample data fixtures (mimic json_builder.py output structure)
# ---------------------------------------------------------------------------
SAMPLE_LINKS: dict[str, dict] = {
"CatalogWorkspace": {"path": "/latest/api-reference/catalogworkspace", "kind": "class"},
"CatalogDataSource": {"path": "/latest/api-reference/catalogdatasource", "kind": "class"},
"some_util": {"path": "/latest/api-reference/some_util", "kind": "function"},
"Insight": {"path": "/latest/api-reference/insight", "kind": "class"},
}
SAMPLE_FUNCTION_DATA: dict = {
"kind": "function",
"docstring": "List all workspaces.",
"signature": {
"params": [("workspace_id", "str"), ("name", "str")],
"return_annotation": "list[CatalogWorkspace]",
},
"docstring_parsed": {
"short_description": "Return a `CatalogWorkspace` list.",
"long_description": "Fetches all workspaces from the server.",
"params": [
{"arg_name": "workspace_id", "type_name": "str", "description": "The workspace ID."},
{"arg_name": "name", "type_name": "Optional[str]", "description": "Optional filter."},
],
"returns": {
"type_name": "list[CatalogWorkspace]",
"description": "All matching `CatalogWorkspace` objects.",
},
},
}
SAMPLE_PROPERTY_DATA: dict = {
"kind": "function",
"is_property": True,
"docstring": "The workspace name.",
"signature": {"params": [], "return_annotation": "str"},
"docstring_parsed": {
"short_description": "The workspace name.",
"long_description": "",
"params": [],
"returns": None,
},
}
SAMPLE_CLASS_DATA: dict = {
"kind": "class",
"docstring": "Represents a workspace.",
"docstring_parsed": {
"short_description": "A catalog workspace object.",
"long_description": "",
},
"functions": {
"list_workspaces": SAMPLE_FUNCTION_DATA,
"name": SAMPLE_PROPERTY_DATA,
"_private": {"kind": "function"},
},
}
SAMPLE_MODULE_DATA: dict = {
"kind": "module",
"CatalogWorkspace": {"kind": "class"},
"some_util": {"kind": "function"},
}
# ===================================================================
# LinkResolver
# ===================================================================
class TestLinkResolver:
def test_type_link_known_type(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
result = resolver.type_link("CatalogWorkspace")
assert result == '<a href="/latest/api-reference/catalogworkspace/">CatalogWorkspace</a>'
def test_type_link_unknown_type(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
assert resolver.type_link("UnknownType") == "UnknownType"
def test_type_link_empty(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
assert resolver.type_link("") == ""
def test_type_link_none(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
assert resolver.type_link(None) == ""
def test_type_link_optional_wrapper(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
result = resolver.type_link("Optional[CatalogWorkspace]")
assert "Optional[" in result
assert '<a href="/latest/api-reference/catalogworkspace/">CatalogWorkspace</a>' in result
def test_type_link_list_wrapper(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
result = resolver.type_link("list[CatalogWorkspace]")
assert "list[" in result
assert '<a href="/latest/api-reference/catalogworkspace/">CatalogWorkspace</a>' in result
def test_all_links_backtick_name_without_underscore(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
result = resolver.all_links("Returns a `CatalogWorkspace` object.")
assert '<a href="/latest/api-reference/catalogworkspace/">CatalogWorkspace</a>' in result
# Backticks around a resolved link should be stripped
assert "`CatalogWorkspace`" not in result
def test_all_links_name_with_underscore_in_backticks(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
result = resolver.all_links("Call `some_util` for help.")
assert '<a href="/latest/api-reference/some_util/">some_util</a>' in result
def test_all_links_name_with_underscore_after_space(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
result = resolver.all_links("Use some_util here.")
assert '<a href="/latest/api-reference/some_util/">some_util</a>' in result
def test_all_links_empty(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
assert resolver.all_links("") == ""
def test_all_links_none(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
assert resolver.all_links(None) == ""
def test_all_links_no_matches(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
text = "Plain text with no type names."
assert resolver.all_links(text) == text
def test_all_links_multiple_names(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
result = resolver.all_links("Returns `CatalogWorkspace` or `Insight`.")
assert "catalogworkspace" in result
assert "insight" in result
# ===================================================================
# TemplateReplacementSpec
# ===================================================================
class TestTemplateReplacementSpec:
def test_render_replaces_all_tokens(self, _mod):
spec = _mod.TemplateReplacementSpec(parent="sdk", name="MyClass", link="MyClass", content="<div>hello</div>")
template = "PARENT.NAME (LINK)\nCONTENT"
result = spec.render_template_to_str(template)
assert result == "sdk.MyClass (MyClass)\n<div>hello</div>"
def test_render_skips_none_tokens(self, _mod):
spec = _mod.TemplateReplacementSpec(name="Foo")
template = "PARENT.NAME"
result = spec.render_template_to_str(template)
# PARENT is None so left as-is
assert result == "PARENT.Foo"
# ===================================================================
# _function_signature
# ===================================================================
class TestFunctionSignature:
def test_with_docstring_params(self, _mod):
result = _mod._function_signature(SAMPLE_FUNCTION_DATA)
assert result == "workspace_id: str, name: Optional[str]"
def test_without_docstring(self, _mod):
data: dict = {"kind": "function", "signature": {"params": []}}
assert _mod._function_signature(data) == ""
def test_empty_params(self, _mod):
data: dict = {"kind": "function", "docstring_parsed": {"params": []}}
assert _mod._function_signature(data) == ""
# ===================================================================
# _object_partial_context
# ===================================================================
class TestObjectPartialContext:
def test_function_context(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
ctx = _mod._object_partial_context(SAMPLE_FUNCTION_DATA, ["sdk", "list_workspaces"], resolver)
assert ctx["kind"] == "function"
assert ctx["name"] == "list_workspaces"
assert ctx["is_property"] is False
assert "params" in ctx
assert len(ctx["params"]) == 2
assert isinstance(ctx["returns"], dict)
assert ctx["returns"]["type"] # should have a rendered link
def test_property_context(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
ctx = _mod._object_partial_context(SAMPLE_PROPERTY_DATA, ["CatalogWorkspace", "name"], resolver)
assert ctx["is_property"] is True
assert ctx["returns"] == "no_docs"
def test_class_context(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
ctx = _mod._object_partial_context(SAMPLE_CLASS_DATA, ["sdk", "CatalogWorkspace"], resolver)
assert ctx["kind"] == "class"
assert ctx["parent_name"] == "sdk"
assert ctx["class_name"] == "CatalogWorkspace"
assert ctx["docstring"] is True
def test_class_context_no_docstring(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
data: dict = {"kind": "class"}
ctx = _mod._object_partial_context(data, ["mod", "Empty"], resolver)
assert ctx["docstring"] is False
# ===================================================================
# HTML rendering functions
# ===================================================================
class TestRenderFunctionHtml:
def test_produces_html(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
html = _mod.render_function_html(SAMPLE_FUNCTION_DATA, "sdk.list_workspaces", resolver)
assert '<div class="python-ref">' in html
assert "list_workspaces" in html
assert "Parameters" in html
def test_property_no_parameters(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
html = _mod.render_function_html(SAMPLE_PROPERTY_DATA, "CatalogWorkspace.name", resolver)
assert "name" in html
# Properties should not show a Parameters section
assert "Parameters" not in html
class TestRenderClassHtml:
def test_produces_html_with_methods_and_properties(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
html = _mod.render_class_html(SAMPLE_CLASS_DATA, "sdk", "sdk.CatalogWorkspace", resolver)
assert '<div class="python-ref">' in html
assert "Properties" in html
assert "Methods" in html
# Private method should be excluded
assert "_private" not in html
def test_class_no_functions(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
data: dict = {"kind": "class", "functions": {}}
html = _mod.render_class_html(data, "sdk", "sdk.Empty", resolver)
assert "Properties" in html
assert "<i> None </i>" in html
class TestRenderModuleHtml:
def test_produces_table(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
html = _mod.render_module_html(SAMPLE_MODULE_DATA, resolver)
assert '<div class="python-api-ref">' in html
assert "class" in html
assert "function" in html
def test_skips_kind_key(self, _mod):
resolver = _mod.LinkResolver(SAMPLE_LINKS)
html = _mod.render_module_html(SAMPLE_MODULE_DATA, resolver)
# The "kind" key on the module dict itself should not appear as an entry
# 1 header row + 2 data rows = 3
assert html.count("<tr>") == 3
# ===================================================================
# change_json_root
# ===================================================================
class TestChangeJsonRoot:
def test_none_paths_returns_original(self, _mod):
data = {"a": 1}
assert _mod.change_json_root(data, None) is data
def test_single_path(self, _mod):
data = {"sdk": {"catalog": {"kind": "module"}}}
result = _mod.change_json_root(data, ["sdk.catalog"])
assert result == {"catalog": {"kind": "module"}}
def test_multiple_paths(self, _mod):
data = {
"sdk": {"kind": "module", "cls": {"kind": "class"}},
"pandas": {"kind": "module"},
}
result = _mod.change_json_root(data, ["sdk", "pandas"])
assert "sdk" in result
assert "pandas" in result
# ===================================================================
# create_file_structure (integration — uses tmp_path)
# ===================================================================
class TestCreateFileStructure:
def test_creates_module_class_and_function_files(self, _mod, tmp_path):
data = {
"mymodule": {
"kind": "module",
"MyClass": {
"kind": "class",
"docstring": "A class.",
"docstring_parsed": {
"short_description": "A class.",
"long_description": "",
},
"functions": {
"do_stuff": {
"kind": "function",
"docstring": "Do stuff.",
"signature": {
"params": [],
"return_annotation": "None",
},
"docstring_parsed": {
"short_description": "Do stuff.",
"long_description": "",
"params": [],
"returns": None,
},
},
"_hidden": {"kind": "function"},
},
},
},
}
_mod.create_file_structure(data, tmp_path, "/latest/api-reference")
# Module index
module_index = tmp_path / "mymodule" / "_index.md"
assert module_index.exists()
content = module_index.read_text()
assert "mymodule" in content
# Class index
class_index = tmp_path / "mymodule" / "MyClass" / "_index.md"
assert class_index.exists()
content = class_index.read_text()
assert "MyClass" in content
assert "python-ref" in content
# Function page
func_page = tmp_path / "mymodule" / "MyClass" / "do_stuff.md"
assert func_page.exists()
content = func_page.read_text()
assert "do_stuff" in content
# Private function should be skipped
assert not (tmp_path / "mymodule" / "MyClass" / "_hidden.md").exists()
def test_duplicate_names_skipped(self, _mod, tmp_path):
data = {
"mod1": {
"kind": "module",
"Shared": {"kind": "class", "functions": {}},
},
"mod2": {
"kind": "module",
"Shared": {"kind": "class", "functions": {}},
},
}
# Should not raise — second "Shared" is skipped
_mod.create_file_structure(data, tmp_path, "/latest/api-reference")
assert (tmp_path / "mod1" / "Shared" / "_index.md").exists()