kausalflow/lightkone / components/bibtex-export/versions/1/properties.py
· raw
"""Behavioral properties for lightkone/bibtex-export.
Each property checks one rule of the description on generated papers whose
values have no newlines, so an entry can be read back line by line.
"""
import re
from hypothesis import given, strategies as st
from schema import Input, Paper
SAFE = st.text(st.characters(codec="utf-8", exclude_characters="\n\r{}\\", exclude_categories=["Cs", "Cc"]), max_size=20)
SPECIAL = st.lists(st.sampled_from(["a", "b c", "&", "%", "#", "\\&", "\\%", "\\#", "50", " "]), max_size=6).map("".join)
WORDS = st.text("abcdefghijklmnopqrstuvwxyz ", min_size=1, max_size=15)
NAMES = st.sampled_from(["pages", "volume", "publisher", "month", "journal", "booktitle", "lightkone-id", "lightkone-x", "note", "isbn"])
LINE = re.compile(r"^ (\S+)( *) = \{(.*)\}(,?)$")
@st.composite
def papers(draw, text=SAFE):
return Paper(
key=draw(st.text("abcdefghijklmnopqrstuvwxyzABC0123456789:-_", max_size=12)),
type=draw(st.sampled_from(["article", "inproceedings", "incollection", "book", "misc", ""])),
title=draw(text),
authors=draw(st.lists(st.sampled_from(["Hinton, Geoffrey", "others", "LIGO Collaboration", "Plato", "Others", ""]), max_size=4)),
year=draw(st.none() | st.integers(0, 3000)),
venue=draw(st.none() | text),
doi=draw(st.none() | SAFE),
arxiv=draw(st.none() | st.sampled_from(["", "2101.00001", "hep-th/9711200"])),
url=draw(st.none() | SAFE),
abstract=draw(st.none() | text),
tags=draw(st.lists(st.sampled_from(["ml", "to read", "", "a;b"]), max_size=3)),
file=draw(st.none() | SAFE),
fields=draw(st.dictionaries(NAMES, SAFE, max_size=4)),
)
def write(impl, p: Paper) -> str:
return impl(Input(paper=p)).text
def lines(text: str) -> list[tuple[str, str]]:
"""The (name, value) pairs of an entry written as the description says."""
body = text.split("\n")[1:-2]
out = []
for i, line in enumerate(body):
m = LINE.match(line)
assert m, f"not a field line: {line!r}"
assert (m.group(4) == ",") == (i < len(body) - 1), "lines are joined by ',\\n'"
out.append((m.group(1), m.group(3)))
return out
def escaped(s: str) -> str:
return re.sub(r"\\?([&%#])", r"\\\1", s)
def present(v) -> bool:
return v is not None and v != ""
@given(p=papers())
def test_entry_frame(impl, p):
text = write(impl, p)
assert text.startswith(f"@{p.type or 'misc'}{{{p.key},\n")
assert text.endswith("\n}\n")
@given(p=papers())
def test_names_are_aligned(impl, p):
text = write(impl, p)
body = text.split("\n")[1:-2]
width = max(len(n) for n, _ in lines(text))
assert all(line.index(" = {") == 2 + width for line in body)
@given(p=papers())
def test_fields_appear_in_order_when_present(impl, p):
venue_field = "booktitle" if p.type in ("inproceedings", "incollection") else "journal"
expected = ["title"]
expected += ["author"] if p.authors else []
expected += ["year"] if p.year is not None else []
expected += [venue_field] if present(p.venue) and not p.fields.get(venue_field) else []
expected += ["doi"] if present(p.doi) else []
expected += ["eprint", "archiveprefix"] if present(p.arxiv) else []
expected += ["url"] if present(p.url) else []
expected += ["abstract"] if present(p.abstract) else []
expected += ["keywords"] if ";".join(p.tags) else []
expected += ["file"] if present(p.file) else []
expected += [k for k, _ in kept_fields(p)]
assert [n for n, _ in lines(write(impl, p))] == expected
def kept_fields(p: Paper) -> list[tuple[str, str]]:
return [(k, v) for k, v in p.fields.items() if v and not k.startswith("lightkone-")]
@given(p=papers())
def test_values_are_written_as_given(impl, p):
got = lines(write(impl, p))
named = dict(got)
if p.year is not None:
assert named["year"] == str(p.year)
for name in ("doi", "url", "file"):
if present(getattr(p, name)):
assert named[name] == getattr(p, name)
if present(p.arxiv):
assert named["eprint"] == p.arxiv and named["archiveprefix"] == "arXiv"
kept = kept_fields(p)
assert got[len(got) - len(kept):] == kept
@given(p=papers(text=SPECIAL))
def test_title_venue_and_abstract_are_escaped(impl, p):
got = dict(lines(write(impl, p)))
assert got["title"] == "{" + escaped(p.title) + "}"
venue_field = "booktitle" if p.type in ("inproceedings", "incollection") else "journal"
if present(p.venue) and not p.fields.get(venue_field):
assert got[venue_field] == escaped(p.venue)
if present(p.abstract):
assert got["abstract"] == escaped(p.abstract)
@given(p=papers())
def test_authors_and_keywords(impl, p):
got = dict(lines(write(impl, p)))
joined = " and ".join(a if "," in a or a == "others" else "{" + a + "}" for a in p.authors)
assert got.get("author", "") == joined
assert got.get("keywords", "") == ";".join(p.tags)