Skip to content

Commit aa663ea

Browse files
committed
Merge branch 'investigate_post_submit_checks_gcp-sphinx-docfx-yaml_01_speed-optimizations' into investigate_post_submit_checks_ci_02_docfx-presubmit-sharding
2 parents 61851bf + 80ed8b3 commit aa663ea

2 files changed

Lines changed: 182 additions & 2 deletions

File tree

‎packages/gcp-sphinx-docfx-yaml/docfx_yaml/extension.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2069,7 +2069,12 @@ def convert_cross_references(
20692069
# match. However, a few packages in the repo don't use the `google.*`
20702070
# namespace (e.g. `pandas_gbq.Context`), so we only take this shortcut when
20712071
# the package's `known_uids` actually start with "google.".
2072-
if known_uids and known_uids[0].startswith("google.") and "google." not in content:
2072+
if (
2073+
known_uids
2074+
and known_uids[0].startswith("google.")
2075+
and known_uids[-1].startswith("google.")
2076+
and "google." not in content
2077+
):
20732078
return content
20742079

20752080
example_text = "Examples:"

‎packages/gcp-sphinx-docfx-yaml/tests/test_unit.py‎

Lines changed: 176 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1+
import tempfile
12
import unittest
3+
from pathlib import Path
4+
from unittest import mock
25

6+
from docutils import nodes
37
from parameterized import parameterized
8+
from sphinx import addnodes
49
from yaml import Loader, load
510

6-
from docfx_yaml import extension
11+
from docfx_yaml import extension, markdown_utils, utils
712

813

914
class TestGenerate(unittest.TestCase):
@@ -1289,6 +1294,176 @@ def test_merges_markdown_and_package_toc(self):
12891294
),
12901295
)
12911296

1297+
def test_get_class_lines(self):
1298+
source = (
1299+
"class TopLevel:\n" # line 1
1300+
" pass\n"
1301+
"\n"
1302+
"@dec1\n" # line 4
1303+
"@dec2\n" # line 5
1304+
"class Decorated:\n" # line 6
1305+
" class Nested:\n" # line 7
1306+
" pass\n"
1307+
"\n"
1308+
" @nested_dec\n" # line 10
1309+
" class DecoratedNested:\n" # line 11
1310+
" pass\n"
1311+
"\n"
1312+
"def factory():\n" # line 14
1313+
" class LocalClass:\n" # line 15
1314+
" pass\n"
1315+
"\n"
1316+
"async def async_factory():\n" # line 18
1317+
" @local_dec\n" # line 19
1318+
" class AsyncLocalClass:\n" # line 20
1319+
" pass\n"
1320+
)
1321+
with tempfile.NamedTemporaryFile(
1322+
mode="w", suffix=".py", delete=False
1323+
) as tmp_file:
1324+
tmp_file.write(source)
1325+
tmp_path = tmp_file.name
1326+
1327+
try:
1328+
extension._get_class_lines.cache_clear()
1329+
class_lines = extension._get_class_lines(tmp_path)
1330+
self.assertEqual(
1331+
class_lines,
1332+
{
1333+
"TopLevel": 1,
1334+
"Decorated": 4,
1335+
"Decorated.Nested": 7,
1336+
"Decorated.DecoratedNested": 10,
1337+
"factory.<locals>.LocalClass": 15,
1338+
"async_factory.<locals>.AsyncLocalClass": 19,
1339+
},
1340+
)
1341+
# Non-existent file returns empty dict without raising.
1342+
self.assertEqual(
1343+
extension._get_class_lines(tmp_path + ".missing"),
1344+
{},
1345+
)
1346+
finally:
1347+
Path(tmp_path).unlink(missing_ok=True)
1348+
1349+
def test_transform_node_does_not_mutate_live_doctree_or_pending_xref(self):
1350+
app = mock.MagicMock()
1351+
1352+
def fake_resolve_references(doctree, fromdocname, builder):
1353+
for xref in list(doctree.traverse(addnodes.pending_xref)):
1354+
ref = nodes.reference("", "", refuri=xref["reftarget"])
1355+
ref.append(nodes.Text(xref.astext()))
1356+
xref.replace_self(ref)
1357+
1358+
app.env.resolve_references.side_effect = fake_resolve_references
1359+
1360+
# 1. Detached node without pending_xref skips resolve_references.
1361+
plain_node = nodes.paragraph("", "Plain text")
1362+
self.assertEqual(utils.transform_node(app, plain_node).strip(), "Plain text")
1363+
app.env.resolve_references.assert_not_called()
1364+
1365+
# 2. Live doctree node with nested pending_xref is deep-copied so
1366+
# neither its parent pointer nor its nested pending_xref child is mutated.
1367+
live_parent = nodes.section()
1368+
live_para = nodes.paragraph("", "See ")
1369+
live_xref = addnodes.pending_xref(
1370+
"",
1371+
nodes.literal("", "Client"),
1372+
refdomain="py",
1373+
reftype="class",
1374+
reftarget="google.cloud.Client",
1375+
)
1376+
live_para.append(live_xref)
1377+
live_parent.append(live_para)
1378+
1379+
rendered = utils.transform_node(app, live_para)
1380+
1381+
app.env.resolve_references.assert_called_once()
1382+
self.assertIs(live_para.parent, live_parent)
1383+
self.assertIn(live_xref, live_para.children)
1384+
self.assertIs(live_xref.parent, live_para)
1385+
self.assertEqual(rendered.strip(), "See <xref:google.cloud.Client>")
1386+
1387+
def test_run_sphinx_markdown(self):
1388+
with tempfile.TemporaryDirectory() as tmp_dir:
1389+
html_outdir = Path(tmp_dir) / "html"
1390+
html_outdir.mkdir()
1391+
markdown_outdir = Path(tmp_dir) / "markdown"
1392+
1393+
orig_builder = mock.MagicMock()
1394+
orig_builder.outdir = str(html_outdir)
1395+
1396+
app = mock.MagicMock()
1397+
app.builder = orig_builder
1398+
app.env.found_docs = {"b_doc", "a_doc"}
1399+
1400+
doctree_a = mock.sentinel.doctree_a
1401+
doctree_b = mock.sentinel.doctree_b
1402+
app.env.get_and_resolve_doctree.side_effect = [doctree_a, doctree_b]
1403+
1404+
observed_builders = []
1405+
1406+
def record_builder(docname, doctree):
1407+
observed_builders.append((docname, doctree, app.builder))
1408+
1409+
with (
1410+
mock.patch("os.getcwd", return_value="/tmp/work"),
1411+
mock.patch(
1412+
"sphinx_markdown_builder.markdown_builder.MarkdownBuilder"
1413+
) as mock_md_builder_cls,
1414+
):
1415+
mock_md_builder = mock_md_builder_cls.return_value
1416+
mock_md_builder.write_doc.side_effect = record_builder
1417+
1418+
markdown_utils.run_sphinx_markdown(app)
1419+
1420+
mock_md_builder_cls.assert_called_once_with(app)
1421+
self.assertEqual(mock_md_builder.outdir, str(markdown_outdir))
1422+
mock_md_builder.set_environment.assert_called_once_with(app.env)
1423+
mock_md_builder.init.assert_called_once_with()
1424+
mock_md_builder.prepare_writing.assert_called_once_with(
1425+
["a_doc", "b_doc"]
1426+
)
1427+
self.assertEqual(
1428+
observed_builders,
1429+
[
1430+
("a_doc", doctree_a, mock_md_builder),
1431+
("b_doc", doctree_b, mock_md_builder),
1432+
],
1433+
)
1434+
# Original builder is restored after run_sphinx_markdown completes.
1435+
self.assertIs(app.builder, orig_builder)
1436+
1437+
# Subsequent call when markdown_outdir already exists is a no-op.
1438+
mock_md_builder_cls.reset_mock()
1439+
markdown_utils.run_sphinx_markdown(app)
1440+
mock_md_builder_cls.assert_not_called()
1441+
1442+
def test_convert_cross_references_fast_path_checks_both_ends(self):
1443+
# All UIDs start with "google." and content has no "google." -> fast-path.
1444+
all_google_uids = ["google.cloud.storage.Client", "google.api_core.Retry"]
1445+
content_no_google = "Plain description with custom_pkg.Client reference."
1446+
self.assertEqual(
1447+
extension.convert_cross_references(
1448+
content_no_google,
1449+
"google.cloud.storage.Blob",
1450+
all_google_uids,
1451+
),
1452+
content_no_google,
1453+
)
1454+
1455+
# Mixed sorted list where known_uids[0] starts with "google." but
1456+
# known_uids[-1] does not -> must NOT skip non-google references.
1457+
mixed_uids = ["google.cloud.storage.Client", "custom_pkg.Client"]
1458+
self.assertEqual(
1459+
extension.convert_cross_references(
1460+
content_no_google,
1461+
"google.cloud.storage.Blob",
1462+
mixed_uids,
1463+
),
1464+
'Plain description with <xref uid="custom_pkg.Client">custom_pkg.Client</xref> reference.',
1465+
)
1466+
12921467

12931468
if __name__ == "__main__":
12941469
unittest.main()

0 commit comments

Comments
 (0)