Files
stack/tests/bib/test_pincite.py
kert 828ea9611d
Some checks failed
CI / test (push) Has been cancelled
CI / notebooks-smoke (push) Has been cancelled
CI / lint (push) Has been cancelled
Deploy / notebooks (push) Has been cancelled
Deploy / zotero (push) Has been cancelled
Deploy / docs (push) Has been cancelled
Deploy / api (push) Has been cancelled
Deploy / llm (push) Has been cancelled
Deploy / mc (push) Has been cancelled
Deploy / report (push) Has been cancelled
Infra CI / mc (push) Has been cancelled
Infra CI / notebooks (push) Has been cancelled
Infra CI / zotero (push) Has been cancelled
Infra CI / docs (push) Has been cancelled
Infra CI / api (push) Has been cancelled
Infra CI / llm (push) Has been cancelled
feat(bib,notebooks): cfr locator jump URLs + live eCFR links in CY2027 notebook (closes #641)
2026-08-17 15:05:23 -04:00

611 lines
21 KiB
Python

"""Tests for bib.pincite — pinpoint citation system."""
from __future__ import annotations
import textwrap
import pytest
from bib.pincite import (
Pincite,
_classify_locator,
build_citation_graph,
check_pincite_keys,
format_pincite_block,
inject_pincites_into_source,
list_pincites,
parse_pincites,
upsert_pincites,
)
from bib.store import Store
# ── Fixtures ─────────────────────────────────────────────────────────
@pytest.fixture
def store(tmp_path):
"""In-memory-like store using a temp database."""
db = tmp_path / "test_bib.sqlite"
s = Store(database=str(db))
return s
@pytest.fixture
def store_with_items(store):
"""Store with two items for pincite testing."""
from bib.item import Source
store.create(Source(key="JX46GQ9K", title="Tag vocabulary item"))
store.create(Source(key="9ASETKJ4", title="Nature citation standards"))
return store
# ── Parse ────────────────────────────────────────────────────────────
class TestParsePincites:
def test_basic(self):
doc = ":pincite:`JX46GQ9K p.14` -- NDC validation rules"
result = parse_pincites(doc, "mod.fn")
assert len(result) == 1
assert result[0].item_key == "JX46GQ9K"
assert result[0].locator == "p.14"
assert result[0].note == "NDC validation rules"
assert result[0].fn_path == "mod.fn"
def test_no_locator(self):
doc = ":pincite:`JX46GQ9K`"
result = parse_pincites(doc, "mod.fn")
assert len(result) == 1
assert result[0].locator == ""
def test_no_note(self):
doc = ":pincite:`JX46GQ9K p.14`"
result = parse_pincites(doc, "mod.fn")
assert len(result) == 1
assert result[0].note == ""
def test_section_locator(self):
doc = ":pincite:`9ASETKJ4 §3.2` — exclusion criteria"
result = parse_pincites(doc, "mod.fn")
assert result[0].locator == "§3.2"
assert result[0].locator_type == "section"
assert result[0].note == "exclusion criteria"
def test_cfr_locator(self):
doc = ":pincite:`ABC23456 42 CFR §414.22` -- payment rates"
result = parse_pincites(doc, "mod.fn")
assert result[0].locator == "42 CFR §414.22"
assert result[0].locator_type == "cfr"
def test_fr_locator(self):
doc = ":pincite:`DEF78923 90 FR 86252` -- final rule"
result = parse_pincites(doc, "mod.fn")
assert result[0].locator == "90 FR 86252"
assert result[0].locator_type == "fr"
def test_multiple(self):
doc = textwrap.dedent("""
:pincite:`JX46GQ9K p.14` -- first
:pincite:`9ASETKJ4 §3.2` -- second
:pincite:`ABC23456 Ch. 12` -- third
""")
result = parse_pincites(doc, "mod.fn")
assert len(result) == 3
def test_deduplication(self):
doc = ":pincite:`JX46GQ9K p.14` -- first\n:pincite:`JX46GQ9K p.14` -- dupe"
result = parse_pincites(doc, "mod.fn")
assert len(result) == 1
def test_empty_docstring(self):
assert parse_pincites("", "mod.fn") == []
assert parse_pincites(None, "mod.fn") == []
def test_no_pincites(self):
doc = "Just a plain docstring with no citations."
assert parse_pincites(doc, "mod.fn") == []
def test_invalid_key_charset(self):
# 0, 1, O, L are not valid Zotero key chars
doc = ":pincite:`00000000 p.1` -- bad key"
assert parse_pincites(doc, "mod.fn") == []
def test_em_dash(self):
doc = ":pincite:`JX46GQ9K p.14` — em dash note"
result = parse_pincites(doc, "mod.fn")
assert result[0].note == "em dash note"
# ── Classify locator ─────────────────────────────────────────────────
class TestClassifyLocator:
def test_page(self):
assert _classify_locator("p.14") == "page"
assert _classify_locator("pp.8-12") == "page"
def test_section(self):
assert _classify_locator("§3.2") == "section"
def test_cfr(self):
assert _classify_locator("42 CFR §414.22") == "cfr"
def test_fr(self):
assert _classify_locator("90 FR 86252") == "fr"
def test_chapter(self):
assert _classify_locator("Ch. 12") == "chapter"
def test_paragraph(self):
assert _classify_locator("¶4") == "paragraph"
def test_empty(self):
assert _classify_locator("") == ""
# ── Store operations ─────────────────────────────────────────────────
class TestUpsertPincites:
def test_insert(self, store_with_items):
pincites = [
Pincite(fn_path="mod.fn", item_key="JX46GQ9K", locator="p.14", note="test"),
]
count = upsert_pincites(store_with_items, pincites)
assert count == 1
result = list_pincites(store_with_items, fn_path="mod.fn")
assert len(result) == 1
assert result[0].item_key == "JX46GQ9K"
def test_upsert_idempotent(self, store_with_items):
p = Pincite(fn_path="mod.fn", item_key="JX46GQ9K", locator="p.14")
upsert_pincites(store_with_items, [p])
upsert_pincites(store_with_items, [p])
result = list_pincites(store_with_items, fn_path="mod.fn")
assert len(result) == 1
def test_skips_missing_item(self, store_with_items):
p = Pincite(fn_path="mod.fn", item_key="ZZZZZZZZ", locator="p.1")
count = upsert_pincites(store_with_items, [p])
assert count == 0
def test_tags_added(self, store_with_items):
p = Pincite(fn_path="mod.fn", item_key="JX46GQ9K", locator="p.14")
upsert_pincites(store_with_items, [p])
item = store_with_items.get("JX46GQ9K")
assert any("pin:" in t for t in item.tags)
class TestListPincites:
def test_by_fn(self, store_with_items):
upsert_pincites(
store_with_items,
[
Pincite(fn_path="a.b", item_key="JX46GQ9K", locator="p.1"),
Pincite(fn_path="c.d", item_key="9ASETKJ4", locator="§2"),
],
)
result = list_pincites(store_with_items, fn_path="a.b")
assert len(result) == 1
assert result[0].item_key == "JX46GQ9K"
def test_by_item(self, store_with_items):
upsert_pincites(
store_with_items,
[
Pincite(fn_path="a.b", item_key="JX46GQ9K", locator="p.1"),
Pincite(fn_path="c.d", item_key="JX46GQ9K", locator="p.2"),
],
)
result = list_pincites(store_with_items, item_key="JX46GQ9K")
assert len(result) == 2
def test_empty_store(self, store):
assert list_pincites(store) == []
# ── Check keys ───────────────────────────────────────────────────────
class TestCheckKeys:
def test_valid(self, store_with_items):
p = Pincite(fn_path="mod.fn", item_key="JX46GQ9K")
errors = check_pincite_keys([p], store_with_items)
assert len(errors) == 0
def test_invalid(self, store_with_items):
p = Pincite(fn_path="mod.fn", item_key="NOTFOUND")
errors = check_pincite_keys([p], store_with_items)
assert len(errors) == 1
assert "not found" in errors[0][1]
# ── Knowledge graph ──────────────────────────────────────────────────
class TestCitationGraph:
def test_build(self, store_with_items):
upsert_pincites(
store_with_items,
[
Pincite(fn_path="mod.fn_a", item_key="JX46GQ9K", locator="p.1"),
Pincite(fn_path="mod.fn_b", item_key="JX46GQ9K", locator="p.2"),
Pincite(fn_path="mod.fn_a", item_key="9ASETKJ4", locator="§3"),
],
)
graph = build_citation_graph(store_with_items)
assert len(graph.nodes) == 4 # 2 functions + 2 items
assert len(graph.edges) == 3
def test_functions_citing(self, store_with_items):
upsert_pincites(
store_with_items,
[
Pincite(fn_path="a.b", item_key="JX46GQ9K"),
Pincite(fn_path="c.d", item_key="JX46GQ9K"),
],
)
graph = build_citation_graph(store_with_items)
fns = graph.functions_citing("JX46GQ9K")
assert set(fns) == {"a.b", "c.d"}
def test_items_cited_by(self, store_with_items):
upsert_pincites(
store_with_items,
[
Pincite(fn_path="a.b", item_key="JX46GQ9K"),
Pincite(fn_path="a.b", item_key="9ASETKJ4"),
],
)
graph = build_citation_graph(store_with_items)
items = graph.items_cited_by("a.b")
assert set(items) == {"JX46GQ9K", "9ASETKJ4"}
def test_to_mermaid(self, store_with_items):
upsert_pincites(
store_with_items,
[
Pincite(fn_path="mod.fn", item_key="JX46GQ9K", locator="p.1"),
],
)
graph = build_citation_graph(store_with_items)
mermaid = graph.to_mermaid()
assert "graph LR" in mermaid
assert "mod_fn" in mermaid
def test_to_networkx_dict(self, store_with_items):
upsert_pincites(
store_with_items,
[
Pincite(fn_path="mod.fn", item_key="JX46GQ9K"),
],
)
graph = build_citation_graph(store_with_items)
d = graph.to_networkx_dict()
assert d["directed"] is True
assert len(d["nodes"]) == 2
assert len(d["links"]) == 1
# ── Format + inject ──────────────────────────────────────────────────
class TestFormatPinciteBlock:
def test_single(self):
p = Pincite(fn_path="m.f", item_key="JX46GQ9K", locator="p.14", note="test")
block = format_pincite_block([p])
assert ":pincite:`JX46GQ9K p.14` -- test" in block
assert block.startswith("References")
def test_no_locator(self):
p = Pincite(fn_path="m.f", item_key="JX46GQ9K")
block = format_pincite_block([p])
assert ":pincite:`JX46GQ9K`" in block
def test_empty(self):
assert format_pincite_block([]) == ""
class TestInjectPincites:
def test_append_to_docstring(self, tmp_path):
source = textwrap.dedent('''
def foo():
"""Existing docstring."""
return 1
''').lstrip()
f = tmp_path / "test_mod.py"
f.write_text(source)
block = "References\n~~~~~~~~~~\n:pincite:`JX46GQ9K p.14` -- test"
result = inject_pincites_into_source(f, "foo", block, dry_run=True)
assert result is not None
assert ":pincite:`JX46GQ9K p.14`" in result
assert "Existing docstring." in result
def test_skip_no_docstring(self, tmp_path):
source = "def foo():\n return 1\n"
f = tmp_path / "test_mod.py"
f.write_text(source)
result = inject_pincites_into_source(f, "foo", "block")
assert result is None
def test_skip_missing_function(self, tmp_path):
source = "def bar():\n pass\n"
f = tmp_path / "test_mod.py"
f.write_text(source)
result = inject_pincites_into_source(f, "foo", "block")
assert result is None
def test_syntax_error_returns_none(self, tmp_path):
"""Lines 505, 506: inject returns None for syntax errors."""
f = tmp_path / "bad.py"
f.write_text("def incomplete(:")
result = inject_pincites_into_source(f, "incomplete", "block")
assert result is None
def test_replace_existing_references(self, tmp_path):
"""Lines 542, 543, 545, 553, 557-559: replace existing References block."""
source = textwrap.dedent('''
def foo():
"""Existing docstring.
References
~~~~~~~~~~
:pincite:`AAAAAAAA p.1` -- old ref
"""
return 1
''').lstrip()
f = tmp_path / "test_mod.py"
f.write_text(source)
block = "References\n~~~~~~~~~~\n:pincite:`JX46GQ9K p.14` -- new ref"
result = inject_pincites_into_source(f, "foo", block, dry_run=True)
assert result is not None
assert "JX46GQ9K" in result
assert "AAAAAAAA" not in result
assert "Existing docstring." in result
def test_write_when_not_dry_run(self, tmp_path):
"""Lines 569, 572: inject actually writes when dry_run=False."""
source = textwrap.dedent('''
def foo():
"""Some docstring."""
return 1
''').lstrip()
f = tmp_path / "test_mod.py"
f.write_text(source)
block = "References\n~~~~~~~~~~\n:pincite:`JX46GQ9K p.14` -- test"
result = inject_pincites_into_source(f, "foo", block, dry_run=False)
assert result is not None
# File should have been written
content = f.read_text()
assert "JX46GQ9K" in content
def test_no_change_returns_none(self, tmp_path):
"""Line 569: if source is unchanged, returns None."""
source = textwrap.dedent('''
def foo():
"""References
~~~~~~~~~~
:pincite:`JX46GQ9K p.14` -- test
"""
return 1
''').lstrip()
f = tmp_path / "test_mod.py"
f.write_text(source)
# The exact same block — may or may not change depending on indent
block = "References\n~~~~~~~~~~\n:pincite:`JX46GQ9K p.14` -- test"
result = inject_pincites_into_source(f, "foo", block, dry_run=True)
# Either None (unchanged) or a string (reformatted); both acceptable
assert result is None or isinstance(result, str)
# ── extract_all_pincites edge cases (lines 255, 260, 275) ───────────
class TestExtractAllPincitesEdge:
def test_default_src_root(self):
"""Line 255: extract_all_pincites(None) defaults to src/."""
from bib.pincite import extract_all_pincites
# Just verify it doesn't crash and returns a list
result = extract_all_pincites()
assert isinstance(result, list)
def test_skips_egg_info(self, tmp_path):
"""Line 260: skips .egg-info directories."""
from bib.pincite import extract_all_pincites
src = tmp_path / "src"
(src / "pkg.egg-info").mkdir(parents=True)
(src / "pkg.egg-info" / "mod.py").write_text(
textwrap.dedent('''\
@narwhalify
def fn():
""":pincite:`ABCD2345 p.1` -- ref"""
pass
''')
)
result = extract_all_pincites(src)
assert len(result) == 0
def test_skips_no_docstring(self, tmp_path):
"""Line 275: narwhalify function with no docstring is skipped."""
from bib.pincite import extract_all_pincites
src = tmp_path / "src"
src.mkdir()
(src / "mod.py").write_text(
textwrap.dedent("""\
@narwhalify
def fn():
pass
""")
)
result = extract_all_pincites(src)
assert len(result) == 0
# ── build_citation_graph item lookup failure (lines 427, 428) ───────
class TestBuildCitationGraphEdge:
def test_item_get_failure_uses_key(self, store_with_items):
"""Lines 427, 428: when store.get fails, label falls back to key."""
from unittest.mock import patch
from bib.pincite import build_citation_graph, upsert_pincites
upsert_pincites(
store_with_items,
[Pincite(fn_path="mod.fn", item_key="JX46GQ9K", locator="p.1")],
)
# Patch store.get to raise KeyError for the item
orig_get = store_with_items.get
def failing_get(key):
if key == "JX46GQ9K":
raise KeyError("not found")
return orig_get(key)
with patch.object(store_with_items, "get", side_effect=failing_get):
graph = build_citation_graph(store_with_items)
# The item node should use the key as fallback label
item_nodes = [n for n in graph.nodes if n.kind == "item"]
assert len(item_nodes) == 1
assert item_nodes[0].label == "JX46GQ9K"
# ── upsert_pincites tag add_tag failure (lines 338, 339) ────────────
class TestUpsertPincitesTagFailure:
def test_tag_error_suppressed(self, store_with_items):
"""Lines 338, 339: add_tag exceptions don't prevent upsert."""
from unittest.mock import patch
from bib.pincite import upsert_pincites
with patch.object(
store_with_items, "add_tag", side_effect=Exception("tag err")
):
count = upsert_pincites(
store_with_items,
[Pincite(fn_path="mod.fn", item_key="JX46GQ9K", locator="p.1")],
)
assert count == 1
# ── FR locators + jump_url (#636) ───────────────────────────────────
class TestFrLocators:
def test_plain_fr_cite_keeps_fr_type(self) -> None:
assert _classify_locator("91 FR 44218") == "fr"
def test_fr_para_locator(self) -> None:
assert _classify_locator("91 FR 44218 ¶3") == "fr_para"
assert _classify_locator("91 FR 44218, para 3") == "fr_para"
assert _classify_locator("91 FR 44218 p.3") == "fr_para"
def test_fr_anchor_locator(self) -> None:
assert _classify_locator("p-3600") == "fr_anchor"
def test_existing_types_unchanged(self) -> None:
assert _classify_locator("p.14") == "page"
assert _classify_locator("§3.2") == "section"
assert _classify_locator("42 CFR 425.400") == "cfr"
class TestJumpUrl:
def _frlink_store(self):
from bib import frlink
from tests.bib.test_frlink import DOC_META, FIXTURE_HTML, _store_with_rule
s, key = _store_with_rule()
frlink.grab(s, key, fetch=lambda _doc: (DOC_META, FIXTURE_HTML))
return s, key
def test_jump_url_resolves_fr_para(self) -> None:
s, key = self._frlink_store()
p = Pincite(
fn_path="mod.fn",
item_key=key,
locator="91 FR 100 ¶2",
locator_type="fr_para",
)
assert p.jump_url(s) == "https://example.test/doc#p-2"
s.close()
def test_jump_url_resolves_plain_fr(self) -> None:
s, key = self._frlink_store()
p = Pincite(
fn_path="mod.fn", item_key=key, locator="91 FR 100", locator_type="fr"
)
assert p.jump_url(s) == "https://example.test/doc#page-100"
s.close()
def test_jump_url_empty_when_unresolvable(self) -> None:
s, key = self._frlink_store()
# non-FR locator → no URL; missing anchor map → no URL, no raise
p = Pincite(
fn_path="mod.fn", item_key=key, locator="§3.2", locator_type="section"
)
assert p.jump_url(s) == ""
p2 = Pincite(
fn_path="mod.fn", item_key=key, locator="91 FR 999", locator_type="fr"
)
assert p2.jump_url(s) == ""
s.close()
class TestGraphJumpUrl:
def test_edges_carry_jump_url(self) -> None:
from bib import frlink
from tests.bib.test_frlink import DOC_META, FIXTURE_HTML, _store_with_rule
s, key = _store_with_rule()
frlink.grab(s, key, fetch=lambda _doc: (DOC_META, FIXTURE_HTML))
upsert_pincites(
s,
[
Pincite(
fn_path="mod.fn",
item_key=key,
locator="91 FR 100 ¶2",
locator_type="fr_para",
)
],
)
g = build_citation_graph(s)
edge = next(e for e in g.edges if e.source == "mod.fn")
assert edge.jump_url == "https://example.test/doc#p-2"
s.close()
class TestCfrJumpUrl:
def test_cfr_locator_resolves_store_free(self) -> None:
s = Store(":memory:")
p = Pincite(
fn_path="mod.fn",
item_key="AAAA2222",
locator="42 CFR 425.400(c)",
locator_type="cfr",
)
assert p.jump_url(s) == (
"https://www.ecfr.gov/current/title-42/section-425.400#p-425.400(c)"
)
s.close()
def test_titleless_section_locator_stays_empty(self) -> None:
s = Store(":memory:")
p = Pincite(
fn_path="mod.fn",
item_key="AAAA2222",
locator="§3.2",
locator_type="section",
)
assert p.jump_url(s) == ""
s.close()