fix(zot): drop L from Zotero key charset
Zotero's allowedKeyChars excludes L (alongside 0, 1, O), but the local generators in zot.db and bib.store emitted L, producing keys the live Zotero UI flags as invalid. Align both generators and the pincite parser regex; pin the charset via assertion and add regression tests for L/O rejection. Also sweep stale fixture and docstring keys (JX46GQ9L, 9ASETLJ4, IJKL3456, JRNLEFGH, WEBIJKLM) for consistency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,8 +7,8 @@ to table to column.
|
||||
|
||||
Directive format::
|
||||
|
||||
:pincite:`JX46GQ9L p.14` — NDC validation rules
|
||||
:pincite:`9ASETLJ4 §3.2` — exclusion criteria
|
||||
:pincite:`JX46GQ9K p.14` — NDC validation rules
|
||||
:pincite:`9ASETKJ4 §3.2` — exclusion criteria
|
||||
|
||||
Components:
|
||||
|
||||
@@ -45,10 +45,10 @@ if TYPE_CHECKING:
|
||||
|
||||
# ── Regex ────────────────────────────────────────────────────────────
|
||||
|
||||
# Zotero key charset: 23456789ABCDEFGHIJKLMNPQRSTUVWXYZ (no 0, 1, O, L)
|
||||
# Zotero key charset: 23456789ABCDEFGHIJKMNPQRSTUVWXYZ (no 0, 1, L, O)
|
||||
_PINCITE_RE = re.compile(
|
||||
r":pincite:`"
|
||||
r"([23456789A-HJ-NP-Z]{8})" # group 1: item key (strict Zotero charset)
|
||||
r"([23456789A-KMNP-Z]{8})" # group 1: item key (strict Zotero charset)
|
||||
r"(?:\s+(.+?))?" # group 2: locator (optional)
|
||||
r"`"
|
||||
r"(?:\s*(?:--|—)\s*(.+))?" # group 3: note (optional)
|
||||
@@ -454,7 +454,7 @@ def format_pincite_block(pincites: list[Pincite]) -> str:
|
||||
|
||||
References
|
||||
~~~~~~~~~~
|
||||
:pincite:`JX46GQ9L p.14` -- NDC validation rules
|
||||
:pincite:`JX46GQ9K p.14` -- NDC validation rules
|
||||
"""
|
||||
if not pincites:
|
||||
return ""
|
||||
|
||||
@@ -29,8 +29,8 @@ from bib.item import Item
|
||||
|
||||
|
||||
def _generate_key() -> str:
|
||||
"""8-char key using Zotero-compatible charset (no 0, 1, O, L)."""
|
||||
chars = "23456789ABCDEFGHIJKLMNPQRSTUVWXYZ"
|
||||
"""8-char key using Zotero-compatible charset (no 0, 1, L, O)."""
|
||||
chars = "23456789ABCDEFGHIJKMNPQRSTUVWXYZ"
|
||||
return "".join(random.choices(chars, k=8)) # noqa: S311
|
||||
|
||||
|
||||
|
||||
@@ -232,7 +232,7 @@ class Tag(BaseModel):
|
||||
|
||||
Examples::
|
||||
|
||||
Tag.pin("JX46GQ9L", "p.14") # pin:JX46GQ9L/p.14
|
||||
Tag.pin("JX46GQ9K", "p.14") # pin:JX46GQ9K/p.14
|
||||
Tag.pin("ABC12345") # pin:ABC12345
|
||||
"""
|
||||
value = f"{item_key}/{locator}" if locator else item_key
|
||||
|
||||
@@ -22,13 +22,13 @@ import random
|
||||
import sqlite3
|
||||
from datetime import datetime, timezone
|
||||
|
||||
ALLOWED_KEY_CHARS = "23456789ABCDEFGHIJKLMNPQRSTUVWXYZ"
|
||||
ALLOWED_KEY_CHARS = "23456789ABCDEFGHIJKMNPQRSTUVWXYZ"
|
||||
|
||||
|
||||
def generate_key() -> str:
|
||||
"""Generate an 8-char Zotero key.
|
||||
|
||||
Character set: ``23456789ABCDEFGHIJKLMNPQRSTUVWXYZ`` (no 0, 1, O, lowercase).
|
||||
Character set: ``23456789ABCDEFGHIJKMNPQRSTUVWXYZ`` (no 0, 1, L, O, lowercase).
|
||||
Source: ``Zotero.Utilities.allowedKeyChars`` in zotero/utilities.js.
|
||||
"""
|
||||
return "".join(random.choices(ALLOWED_KEY_CHARS, k=8)) # noqa: S311
|
||||
|
||||
@@ -35,8 +35,8 @@ def store_with_items(store):
|
||||
"""Store with two items for pincite testing."""
|
||||
from bib.item import Source
|
||||
|
||||
store.create(Source(key="JX46GQ9L", title="Tag vocabulary item"))
|
||||
store.create(Source(key="9ASETLJ4", title="Nature citation standards"))
|
||||
store.create(Source(key="JX46GQ9K", title="Tag vocabulary item"))
|
||||
store.create(Source(key="9ASETKJ4", title="Nature citation standards"))
|
||||
return store
|
||||
|
||||
|
||||
@@ -45,28 +45,28 @@ def store_with_items(store):
|
||||
|
||||
class TestParsePincites:
|
||||
def test_basic(self):
|
||||
doc = ":pincite:`JX46GQ9L p.14` -- NDC validation rules"
|
||||
doc = ":pincite:`JX46GQ9K p.14` -- NDC validation rules"
|
||||
result = parse_pincites(doc, "mod.fn")
|
||||
assert len(result) == 1
|
||||
assert result[0].item_key == "JX46GQ9L"
|
||||
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:`JX46GQ9L`"
|
||||
doc = ":pincite:`JX46GQ9K`"
|
||||
result = parse_pincites(doc, "mod.fn")
|
||||
assert len(result) == 1
|
||||
assert result[0].locator == ""
|
||||
|
||||
def test_no_note(self):
|
||||
doc = ":pincite:`JX46GQ9L p.14`"
|
||||
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:`9ASETLJ4 §3.2` — exclusion criteria"
|
||||
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"
|
||||
@@ -86,15 +86,15 @@ class TestParsePincites:
|
||||
|
||||
def test_multiple(self):
|
||||
doc = textwrap.dedent("""
|
||||
:pincite:`JX46GQ9L p.14` -- first
|
||||
:pincite:`9ASETLJ4 §3.2` -- second
|
||||
: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:`JX46GQ9L p.14` -- first\n:pincite:`JX46GQ9L p.14` -- dupe"
|
||||
doc = ":pincite:`JX46GQ9K p.14` -- first\n:pincite:`JX46GQ9K p.14` -- dupe"
|
||||
result = parse_pincites(doc, "mod.fn")
|
||||
assert len(result) == 1
|
||||
|
||||
@@ -112,7 +112,7 @@ class TestParsePincites:
|
||||
assert parse_pincites(doc, "mod.fn") == []
|
||||
|
||||
def test_em_dash(self):
|
||||
doc = ":pincite:`JX46GQ9L p.14` — em dash note"
|
||||
doc = ":pincite:`JX46GQ9K p.14` — em dash note"
|
||||
result = parse_pincites(doc, "mod.fn")
|
||||
assert result[0].note == "em dash note"
|
||||
|
||||
@@ -150,17 +150,17 @@ class TestClassifyLocator:
|
||||
class TestUpsertPincites:
|
||||
def test_insert(self, store_with_items):
|
||||
pincites = [
|
||||
Pincite(fn_path="mod.fn", item_key="JX46GQ9L", locator="p.14", note="test"),
|
||||
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 == "JX46GQ9L"
|
||||
assert result[0].item_key == "JX46GQ9K"
|
||||
|
||||
def test_upsert_idempotent(self, store_with_items):
|
||||
p = Pincite(fn_path="mod.fn", item_key="JX46GQ9L", locator="p.14")
|
||||
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")
|
||||
@@ -172,9 +172,9 @@ class TestUpsertPincites:
|
||||
assert count == 0
|
||||
|
||||
def test_tags_added(self, store_with_items):
|
||||
p = Pincite(fn_path="mod.fn", item_key="JX46GQ9L", locator="p.14")
|
||||
p = Pincite(fn_path="mod.fn", item_key="JX46GQ9K", locator="p.14")
|
||||
upsert_pincites(store_with_items, [p])
|
||||
item = store_with_items.get("JX46GQ9L")
|
||||
item = store_with_items.get("JX46GQ9K")
|
||||
assert any("pin:" in t for t in item.tags)
|
||||
|
||||
|
||||
@@ -183,23 +183,23 @@ class TestListPincites:
|
||||
upsert_pincites(
|
||||
store_with_items,
|
||||
[
|
||||
Pincite(fn_path="a.b", item_key="JX46GQ9L", locator="p.1"),
|
||||
Pincite(fn_path="c.d", item_key="9ASETLJ4", locator="§2"),
|
||||
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 == "JX46GQ9L"
|
||||
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="JX46GQ9L", locator="p.1"),
|
||||
Pincite(fn_path="c.d", item_key="JX46GQ9L", locator="p.2"),
|
||||
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="JX46GQ9L")
|
||||
result = list_pincites(store_with_items, item_key="JX46GQ9K")
|
||||
assert len(result) == 2
|
||||
|
||||
def test_empty_store(self, store):
|
||||
@@ -211,7 +211,7 @@ class TestListPincites:
|
||||
|
||||
class TestCheckKeys:
|
||||
def test_valid(self, store_with_items):
|
||||
p = Pincite(fn_path="mod.fn", item_key="JX46GQ9L")
|
||||
p = Pincite(fn_path="mod.fn", item_key="JX46GQ9K")
|
||||
errors = check_pincite_keys([p], store_with_items)
|
||||
assert len(errors) == 0
|
||||
|
||||
@@ -230,9 +230,9 @@ class TestCitationGraph:
|
||||
upsert_pincites(
|
||||
store_with_items,
|
||||
[
|
||||
Pincite(fn_path="mod.fn_a", item_key="JX46GQ9L", locator="p.1"),
|
||||
Pincite(fn_path="mod.fn_b", item_key="JX46GQ9L", locator="p.2"),
|
||||
Pincite(fn_path="mod.fn_a", item_key="9ASETLJ4", locator="§3"),
|
||||
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)
|
||||
@@ -243,31 +243,31 @@ class TestCitationGraph:
|
||||
upsert_pincites(
|
||||
store_with_items,
|
||||
[
|
||||
Pincite(fn_path="a.b", item_key="JX46GQ9L"),
|
||||
Pincite(fn_path="c.d", item_key="JX46GQ9L"),
|
||||
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("JX46GQ9L")
|
||||
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="JX46GQ9L"),
|
||||
Pincite(fn_path="a.b", item_key="9ASETLJ4"),
|
||||
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) == {"JX46GQ9L", "9ASETLJ4"}
|
||||
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="JX46GQ9L", locator="p.1"),
|
||||
Pincite(fn_path="mod.fn", item_key="JX46GQ9K", locator="p.1"),
|
||||
],
|
||||
)
|
||||
graph = build_citation_graph(store_with_items)
|
||||
@@ -279,7 +279,7 @@ class TestCitationGraph:
|
||||
upsert_pincites(
|
||||
store_with_items,
|
||||
[
|
||||
Pincite(fn_path="mod.fn", item_key="JX46GQ9L"),
|
||||
Pincite(fn_path="mod.fn", item_key="JX46GQ9K"),
|
||||
],
|
||||
)
|
||||
graph = build_citation_graph(store_with_items)
|
||||
@@ -294,15 +294,15 @@ class TestCitationGraph:
|
||||
|
||||
class TestFormatPinciteBlock:
|
||||
def test_single(self):
|
||||
p = Pincite(fn_path="m.f", item_key="JX46GQ9L", locator="p.14", note="test")
|
||||
p = Pincite(fn_path="m.f", item_key="JX46GQ9K", locator="p.14", note="test")
|
||||
block = format_pincite_block([p])
|
||||
assert ":pincite:`JX46GQ9L p.14` -- test" in block
|
||||
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="JX46GQ9L")
|
||||
p = Pincite(fn_path="m.f", item_key="JX46GQ9K")
|
||||
block = format_pincite_block([p])
|
||||
assert ":pincite:`JX46GQ9L`" in block
|
||||
assert ":pincite:`JX46GQ9K`" in block
|
||||
|
||||
def test_empty(self):
|
||||
assert format_pincite_block([]) == ""
|
||||
@@ -318,10 +318,10 @@ class TestInjectPincites:
|
||||
f = tmp_path / "test_mod.py"
|
||||
f.write_text(source)
|
||||
|
||||
block = "References\n~~~~~~~~~~\n:pincite:`JX46GQ9L p.14` -- test"
|
||||
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:`JX46GQ9L p.14`" in result
|
||||
assert ":pincite:`JX46GQ9K p.14`" in result
|
||||
assert "Existing docstring." in result
|
||||
|
||||
def test_skip_no_docstring(self, tmp_path):
|
||||
@@ -359,10 +359,10 @@ class TestInjectPincites:
|
||||
''').lstrip()
|
||||
f = tmp_path / "test_mod.py"
|
||||
f.write_text(source)
|
||||
block = "References\n~~~~~~~~~~\n:pincite:`JX46GQ9L p.14` -- new ref"
|
||||
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 "JX46GQ9L" in result
|
||||
assert "JX46GQ9K" in result
|
||||
assert "AAAAAAAA" not in result
|
||||
assert "Existing docstring." in result
|
||||
|
||||
@@ -375,12 +375,12 @@ class TestInjectPincites:
|
||||
''').lstrip()
|
||||
f = tmp_path / "test_mod.py"
|
||||
f.write_text(source)
|
||||
block = "References\n~~~~~~~~~~\n:pincite:`JX46GQ9L p.14` -- test"
|
||||
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 "JX46GQ9L" in content
|
||||
assert "JX46GQ9K" in content
|
||||
|
||||
def test_no_change_returns_none(self, tmp_path):
|
||||
"""Line 569: if source is unchanged, returns None."""
|
||||
@@ -388,14 +388,14 @@ class TestInjectPincites:
|
||||
def foo():
|
||||
"""References
|
||||
~~~~~~~~~~
|
||||
:pincite:`JX46GQ9L p.14` -- test
|
||||
: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:`JX46GQ9L p.14` -- test"
|
||||
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)
|
||||
@@ -459,13 +459,13 @@ class TestBuildCitationGraphEdge:
|
||||
|
||||
upsert_pincites(
|
||||
store_with_items,
|
||||
[Pincite(fn_path="mod.fn", item_key="JX46GQ9L", locator="p.1")],
|
||||
[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 == "JX46GQ9L":
|
||||
if key == "JX46GQ9K":
|
||||
raise KeyError("not found")
|
||||
return orig_get(key)
|
||||
|
||||
@@ -474,7 +474,7 @@ class TestBuildCitationGraphEdge:
|
||||
# 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 == "JX46GQ9L"
|
||||
assert item_nodes[0].label == "JX46GQ9K"
|
||||
|
||||
|
||||
# ── upsert_pincites tag add_tag failure (lines 338, 339) ────────────
|
||||
@@ -492,6 +492,6 @@ class TestUpsertPincitesTagFailure:
|
||||
):
|
||||
count = upsert_pincites(
|
||||
store_with_items,
|
||||
[Pincite(fn_path="mod.fn", item_key="JX46GQ9L", locator="p.1")],
|
||||
[Pincite(fn_path="mod.fn", item_key="JX46GQ9K", locator="p.1")],
|
||||
)
|
||||
assert count == 1
|
||||
|
||||
@@ -47,6 +47,14 @@ class TestGenerateKey:
|
||||
keys = {_generate_key() for _ in range(100)}
|
||||
assert len(keys) > 90
|
||||
|
||||
def test_charset_excludes_zotero_forbidden(self) -> None:
|
||||
# Zotero rejects 0, 1, L, O — see Zotero.Utilities.allowedKeyChars.
|
||||
# The bib generator must stay in lockstep with zot.db.ALLOWED_KEY_CHARS.
|
||||
forbidden = set("01LO")
|
||||
for _ in range(500):
|
||||
key = _generate_key()
|
||||
assert not (set(key) & forbidden), f"forbidden char in {key}"
|
||||
|
||||
|
||||
# ── Store initialization ────────────────────────────────────────
|
||||
|
||||
@@ -778,8 +786,8 @@ class TestStorePinciteMethods:
|
||||
|
||||
db = tmp_path / "bib.sqlite"
|
||||
s = Store(db)
|
||||
s.create(Source(key="JX46GQ9L", title="Test item"))
|
||||
result = s.upsert_pincite("mod.func", "JX46GQ9L", locator="p.14")
|
||||
s.create(Source(key="JX46GQ9K", title="Test item"))
|
||||
result = s.upsert_pincite("mod.func", "JX46GQ9K", locator="p.14")
|
||||
assert isinstance(result, int)
|
||||
s.close()
|
||||
|
||||
@@ -789,8 +797,8 @@ class TestStorePinciteMethods:
|
||||
|
||||
db = tmp_path / "bib.sqlite"
|
||||
s = Store(db)
|
||||
s.create(Source(key="JX46GQ9L", title="Test item"))
|
||||
s.upsert_pincite("mod.func", "JX46GQ9L", locator="p.14")
|
||||
s.create(Source(key="JX46GQ9K", title="Test item"))
|
||||
s.upsert_pincite("mod.func", "JX46GQ9K", locator="p.14")
|
||||
pincites = s.list_pincites(fn_path="mod.func")
|
||||
assert isinstance(pincites, list)
|
||||
assert len(pincites) >= 1
|
||||
@@ -804,9 +812,9 @@ class TestStorePinciteMethods:
|
||||
|
||||
db = tmp_path / "bib.sqlite"
|
||||
s = Store(db)
|
||||
s.create(Source(key="JX46GQ9L", title="Test item"))
|
||||
s.upsert_pincite("mod.func", "JX46GQ9L", locator="p.14")
|
||||
pincites = s.list_pincites(item_key="JX46GQ9L")
|
||||
s.create(Source(key="JX46GQ9K", title="Test item"))
|
||||
s.upsert_pincite("mod.func", "JX46GQ9K", locator="p.14")
|
||||
pincites = s.list_pincites(item_key="JX46GQ9K")
|
||||
assert len(pincites) >= 1
|
||||
s.close()
|
||||
|
||||
@@ -816,8 +824,8 @@ class TestStorePinciteMethods:
|
||||
|
||||
db = tmp_path / "bib.sqlite"
|
||||
s = Store(db)
|
||||
s.create(Source(key="JX46GQ9L", title="Test"))
|
||||
s.upsert_pincite("mod.func", "JX46GQ9L", locator="p.14")
|
||||
s.create(Source(key="JX46GQ9K", title="Test"))
|
||||
s.upsert_pincite("mod.func", "JX46GQ9K", locator="p.14")
|
||||
deleted = s.delete_pincites(fn_path="mod.func")
|
||||
assert deleted >= 1
|
||||
# Verify empty
|
||||
@@ -831,9 +839,9 @@ class TestStorePinciteMethods:
|
||||
|
||||
db = tmp_path / "bib.sqlite"
|
||||
s = Store(db)
|
||||
s.create(Source(key="JX46GQ9L", title="Test"))
|
||||
s.upsert_pincite("mod.func", "JX46GQ9L", locator="p.14")
|
||||
deleted = s.delete_pincites(item_key="JX46GQ9L")
|
||||
s.create(Source(key="JX46GQ9K", title="Test"))
|
||||
s.upsert_pincite("mod.func", "JX46GQ9K", locator="p.14")
|
||||
deleted = s.delete_pincites(item_key="JX46GQ9K")
|
||||
assert deleted >= 1
|
||||
s.close()
|
||||
|
||||
@@ -851,9 +859,9 @@ class TestStorePinciteMethods:
|
||||
|
||||
db = tmp_path / "bib.sqlite"
|
||||
s = Store(db)
|
||||
s.create(Source(key="JX46GQ9L", title="Test"))
|
||||
s.upsert_pincite("mod.a", "JX46GQ9L", locator="p.1")
|
||||
s.upsert_pincite("mod.b", "JX46GQ9L", locator="p.2")
|
||||
s.create(Source(key="JX46GQ9K", title="Test"))
|
||||
s.upsert_pincite("mod.a", "JX46GQ9K", locator="p.1")
|
||||
s.upsert_pincite("mod.b", "JX46GQ9K", locator="p.2")
|
||||
deleted = s.delete_pincites()
|
||||
assert deleted >= 2
|
||||
s.close()
|
||||
|
||||
@@ -7,6 +7,7 @@ import sqlite3
|
||||
import pytest
|
||||
|
||||
from zot.db import (
|
||||
ALLOWED_KEY_CHARS,
|
||||
CREATOR_TYPES,
|
||||
FIELD_MAP,
|
||||
TYPE_MAP,
|
||||
@@ -202,8 +203,13 @@ class TestKeyUtilities:
|
||||
assert len(generate_key()) == 8
|
||||
|
||||
def test_generate_key_charset(self):
|
||||
# Pinned to Zotero.Utilities.allowedKeyChars in zotero/utilities.js.
|
||||
# If this drifts again, the live Zotero UI flags every offending key
|
||||
# as "not a valid key" — see prior incident around char L.
|
||||
assert ALLOWED_KEY_CHARS == "23456789ABCDEFGHIJKMNPQRSTUVWXYZ"
|
||||
for _ in range(200):
|
||||
key = generate_key()
|
||||
assert all(c in "23456789ABCDEFGHIJKLMNPQRSTUVWXYZ" for c in key)
|
||||
assert all(c in ALLOWED_KEY_CHARS for c in key), key
|
||||
|
||||
def test_is_valid_key_accepts_good(self):
|
||||
assert is_valid_key("ABCD2345")
|
||||
@@ -217,6 +223,12 @@ class TestKeyUtilities:
|
||||
def test_is_valid_key_rejects_zero(self):
|
||||
assert not is_valid_key("0BCD2345")
|
||||
|
||||
def test_is_valid_key_rejects_l(self):
|
||||
assert not is_valid_key("LBCD2345")
|
||||
|
||||
def test_is_valid_key_rejects_o(self):
|
||||
assert not is_valid_key("OBCD2345")
|
||||
|
||||
def test_now_iso_format(self):
|
||||
result = now_iso()
|
||||
assert " " in result
|
||||
@@ -493,16 +505,16 @@ class TestContextManager:
|
||||
|
||||
with Db(path) as db:
|
||||
_seed_fields(db.con)
|
||||
item_id = db.create_item(TYPE_MAP["statute"], key="IJKL3456")
|
||||
item_id = db.create_item(TYPE_MAP["statute"], key="IJKK3456")
|
||||
db.set_field(item_id, "nameOfAct", "Context Test")
|
||||
db.commit()
|
||||
|
||||
# Verify data persists after close
|
||||
with Db(path) as db:
|
||||
_seed_fields(db.con)
|
||||
assert db.find_item_by_key("IJKL3456") is not None
|
||||
assert db.find_item_by_key("IJKK3456") is not None
|
||||
assert (
|
||||
db.get_field(db.find_item_by_key("IJKL3456"), "nameOfAct")
|
||||
db.get_field(db.find_item_by_key("IJKK3456"), "nameOfAct")
|
||||
== "Context Test"
|
||||
)
|
||||
|
||||
|
||||
@@ -161,7 +161,7 @@ def extractor(zotero_db) -> Extractor:
|
||||
'<p>"Geographic adjustments apply to all services."</p>',
|
||||
)
|
||||
|
||||
id2 = db.create_item(TYPE_MAP["journalArticle"], key="JRNLEFGH")
|
||||
id2 = db.create_item(TYPE_MAP["journalArticle"], key="JRNKEFGH")
|
||||
db.set_fields(
|
||||
id2,
|
||||
{
|
||||
@@ -174,7 +174,7 @@ def extractor(zotero_db) -> Extractor:
|
||||
db.add_creators(id2, [("Jane", "Smith")])
|
||||
|
||||
# Item with no notes
|
||||
id3 = db.create_item(TYPE_MAP["webpage"], key="WEBIJKLM")
|
||||
id3 = db.create_item(TYPE_MAP["webpage"], key="WEBIJKKM")
|
||||
db.set_fields(id3, {"title": "CMS Data Portal", "url": "https://cms.gov"})
|
||||
db.sync_tags(id3, ["module:pfs"])
|
||||
|
||||
@@ -195,7 +195,7 @@ class TestExtractorQuotes:
|
||||
assert extractor.quotes_for_item("ZZZZZZZZ") == []
|
||||
|
||||
def test_quotes_for_item_no_notes(self, extractor: Extractor):
|
||||
assert extractor.quotes_for_item("WEBIJKLM") == []
|
||||
assert extractor.quotes_for_item("WEBIJKKM") == []
|
||||
|
||||
def test_quotes_for_tag(self, extractor: Extractor):
|
||||
result = extractor.quotes_for_tag("module:pfs")
|
||||
@@ -217,7 +217,7 @@ class TestExtractorTags:
|
||||
assert "pfs" in result
|
||||
assert "STATABCD" in result["pfs"]
|
||||
assert "skin-subs" in result
|
||||
assert "JRNLEFGH" in result["skin-subs"]
|
||||
assert "JRNKEFGH" in result["skin-subs"]
|
||||
|
||||
|
||||
class TestExtractorDocstring:
|
||||
@@ -228,7 +228,7 @@ class TestExtractorDocstring:
|
||||
assert "physician fee schedule" in block
|
||||
|
||||
def test_docstring_block_no_notes(self, extractor: Extractor):
|
||||
block = extractor.docstring_block("WEBIJKLM")
|
||||
block = extractor.docstring_block("WEBIJKKM")
|
||||
assert "References" in block
|
||||
assert "CMS Data Portal" in block
|
||||
|
||||
@@ -257,7 +257,7 @@ class TestExtractorExport:
|
||||
def test_export_provenance_filtered(self, extractor: Extractor):
|
||||
items = extractor.export_provenance(tag="module:skin-subs")
|
||||
assert len(items) == 1
|
||||
assert items[0]["key"] == "JRNLEFGH"
|
||||
assert items[0]["key"] == "JRNKEFGH"
|
||||
|
||||
|
||||
# ── Integration with real DB ─────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user