Spaces:
Running
Running
Claude
Fix synopsis extraction: read the Sphinx definition line, not the signature head
f1d3985 unverified | from ingest.chunk_docs import chunk_page, page_kind, split_by_heading, write_units | |
| PAGE_MD = """Intro paragraph before any heading. | |
| # torch.optim.SGD | |
| Implements stochastic gradient descent. | |
| [source](https://github.com/pytorch/pytorch/blob/main/torch/optim/sgd.py#L26) | |
| ## Parameters | |
| * lr (float) — learning rate | |
| ```python | |
| optimizer = torch.optim.SGD(params, lr=0.01) | |
| ``` | |
| ## Notes | |
| Momentum is optional. | |
| # See also | |
| Other optimizers. | |
| """ | |
| META = { | |
| "url": "https://docs.pytorch.org/docs/stable/generated/torch.optim.SGD.html", | |
| "title": "torch.optim.SGD — PyTorch docs", | |
| "library": "core", | |
| "content_hash": "abc123", | |
| } | |
| def test_clean_heading_extracts_sphinx_anchor(): | |
| from ingest.chunk_docs import clean_heading | |
| title, anchor = clean_heading('SGD[\u00b6](#torch-optim-sgd "Permalink to this heading")') | |
| assert title == "SGD" | |
| assert anchor == "torch-optim-sgd" | |
| title, anchor = clean_heading("Plain Heading") | |
| assert (title, anchor) == ("Plain Heading", "plain-heading") | |
| title, _ = clean_heading('get\\_tokenizer[\u00b6](#get-tokenizer "p")') | |
| assert title == "get_tokenizer" | |
| def test_split_by_heading_paths_and_anchors(): | |
| sections = split_by_heading(PAGE_MD) | |
| assert sections[0].heading_path == [] # preamble | |
| by_title = {s.title: s for s in sections if s.title} | |
| assert by_title["Parameters"].heading_path == ["torch.optim.SGD", "Parameters"] | |
| assert by_title["Notes"].heading_path == ["torch.optim.SGD", "Notes"] | |
| assert by_title["See also"].heading_path == ["See also"] # sibling h1 resets stack | |
| assert by_title["Parameters"].anchor == "parameters" | |
| assert "optimizer = torch.optim.SGD" in by_title["Parameters"].text | |
| def test_source_link_captured(): | |
| sections = split_by_heading(PAGE_MD) | |
| sgd = next(s for s in sections if s.title == "torch.optim.SGD") | |
| assert sgd.source_link.startswith("https://github.com/pytorch/pytorch/blob/") | |
| def test_chunk_page_units_and_kind(): | |
| units = chunk_page(META, PAGE_MD) | |
| assert all(u["url"] == META["url"] for u in units) | |
| assert all(u["kind"] == "api" for u in units) | |
| assert page_kind("https://docs.pytorch.org/tutorials/beginner/intro.html") == "tutorial" | |
| def test_page_synopsis_reads_the_sphinx_definition_line(): | |
| # the REAL shape markdownify produces for a generated API page: the | |
| # signature and the docstring description are ONE paragraph, the | |
| # description on a `: ` definition line. v5 took the paragraph head — | |
| # signature + github url — which merely duplicated the chunk's opening. | |
| from ingest.chunk_docs import page_synopsis | |
| body = ( | |
| "# CrossEntropyLoss[¶](#crossentropyloss \"Permalink\")\n\n" | |
| "*class* torch.nn.CrossEntropyLoss(*weight=None*, *size\\_average=None*)" | |
| "[[source]](https://github.com/pytorch/pytorch/blob/main/torch/nn/modules/loss.py#L1000)\n" | |
| ": This criterion computes the cross entropy loss between input logits " | |
| "and target. It is useful for classification.\n\n" | |
| "## Parameters\n\n* weight (Tensor) — ..." | |
| ) | |
| assert page_synopsis(body) == ( | |
| "This criterion computes the cross entropy loss between input logits and target." | |
| ) | |
| def test_page_synopsis_plain_prose_page_still_works(): | |
| from ingest.chunk_docs import page_synopsis | |
| body = ( | |
| "# amp\n\nAutomatic mixed precision lets you run ops in float16 where " | |
| "safe. Some ops stay in float32.\n" | |
| ) | |
| assert page_synopsis(body) == ( | |
| "Automatic mixed precision lets you run ops in float16 where safe." | |
| ) | |
| def test_page_synopsis_empty_when_page_has_no_prose(): | |
| from ingest.chunk_docs import page_synopsis | |
| assert page_synopsis("# title\n\ntorch.add(input, other)\n\n* alpha — scale") == "" | |
| def test_chunk_page_attaches_synopsis_to_api_units_only(): | |
| units = chunk_page(META, PAGE_MD) | |
| # every unit of the api page carries the page's summary sentence | |
| assert all(u["synopsis"] == "Intro paragraph before any heading." for u in units) | |
| tut_meta = {**META, "url": "https://docs.pytorch.org/tutorials/beginner/intro.html"} | |
| assert all(u["synopsis"] == "" for u in chunk_page(tut_meta, PAGE_MD)) | |
| def test_write_units_valid_okf(tmp_path): | |
| import yaml | |
| paths = write_units(chunk_page(META, PAGE_MD), tmp_path) | |
| assert paths | |
| _, frontmatter, body = paths[0].read_text(encoding="utf-8").split("---\n", 2) | |
| meta = yaml.safe_load(frontmatter) | |
| assert {"url", "anchor", "heading_path", "library", "kind"} <= set(meta) | |
| assert body.strip() | |
| def test_write_units_same_anchor_sections_dont_collide(tmp_path): | |
| # regression: two sections on one page can slugify to the same anchor | |
| # (e.g. two "Parameters" headings); each must get its own file, not clobber | |
| units = [ | |
| {"url": META["url"], "anchor": "parameters", "heading_path": ["A", "Parameters"], | |
| "library": "core", "kind": "api", "content_hash": "h", "content": "first section"}, | |
| {"url": META["url"], "anchor": "parameters", "heading_path": ["B", "Parameters"], | |
| "library": "core", "kind": "api", "content_hash": "h", "content": "second section"}, | |
| ] | |
| paths = write_units(units, tmp_path) | |
| assert len(paths) == 2 | |
| assert len(set(paths)) == 2 # distinct files, nothing overwritten | |
| bodies = {p.read_text(encoding="utf-8").split("---\n", 2)[2].strip() for p in paths} | |
| assert bodies == {"first section", "second section"} | |
| # --- size-capped splitting ------------------------------------------------ | |
| def test_small_section_stays_whole(): | |
| from ingest.chunk_docs import split_oversized | |
| assert split_oversized("short text", limit=100) == ["short text"] | |
| def test_oversized_section_splits_at_paragraph_seams(): | |
| from ingest.chunk_docs import split_oversized | |
| paragraphs = [f"paragraph {i} " + "x" * 80 for i in range(10)] | |
| text = "\n\n".join(paragraphs) | |
| parts = split_oversized(text, limit=250) | |
| assert len(parts) > 1 | |
| assert all(len(p) <= 250 for p in parts) | |
| # nothing lost: every paragraph survives, whole, in exactly one part | |
| for paragraph in paragraphs: | |
| assert sum(p.count(paragraph) for p in parts) == 1 | |
| def test_code_fence_is_never_cut(): | |
| from ingest.chunk_docs import split_oversized | |
| fence = "```python\nx = 1\n\n\ny = 2\n```" # blank lines INSIDE the fence | |
| text = ("word " * 40 + "\n\n") + fence + ("\n\n" + "word " * 40) | |
| parts = split_oversized(text, limit=230) | |
| assert len(parts) > 1 | |
| joined = [p for p in parts if fence in p] | |
| assert len(joined) == 1 # the fence rode whole inside one part | |
| def test_single_giant_atom_hard_splits_on_lines(): | |
| from ingest.chunk_docs import split_oversized | |
| atom = "\n".join(f"line {i}" for i in range(100)) # one paragraph, no \n\n | |
| parts = split_oversized(atom, limit=120) | |
| assert all(len(p) <= 120 for p in parts) | |
| assert "".join(p.replace("\n", "") for p in parts).count("line 99") == 1 | |
| def test_chunk_page_emits_sequential_parts_sharing_identity(monkeypatch): | |
| import ingest.chunk_docs as cd | |
| monkeypatch.setattr(cd, "CHUNK_TARGET_CHARS", 120) | |
| body = "## Distributed Training\n\n" + "\n\n".join("prose " * 15 for _ in range(4)) | |
| units = cd.chunk_page({"url": "https://x/page.html", "title": "T"}, body) | |
| assert len(units) > 1 | |
| assert [u["part"] for u in units] == list(range(len(units))) | |
| # every part keeps the same citation identity (url/anchor/heading path) | |
| assert len({(u["url"], u["anchor"], tuple(u["heading_path"])) for u in units}) == 1 | |
| def test_unsplit_sections_have_part_zero(): | |
| from ingest.chunk_docs import chunk_page | |
| units = chunk_page({"url": "https://x/p.html", "title": "T"}, "## A\n\nshort") | |
| assert [u["part"] for u in units] == [0] | |