Skip to content

Commit 18fc36e

Browse files
cpmpercussionclaude
andcommitted
Make collate output deterministic and convert LaTeX in every published field
Ordering (#99, #103): - glob_for_proc sorts its results, so files are processed year by year on every platform instead of in filesystem order (macOS and the Linux CI runner differed, so the published files were not reproducible). - collate keeps entries in source-file order (page/article order) for all four output formats. The --id_order flag was a no-op: ("ID") is a string, which bibtexparser read as fields "I" and "D" and never sorted. The flag is removed from collate and the Makefile; harmonise --id_order now sorts by key for real; add-dois explicitly preserves file order. - collate builds its own BibDatabase instead of reusing the last parsed one, which raised NameError when a proceedings directory was empty. Conversion (#98): - The abstract block ended in `except Exception: continue`, so any entry without an abstract skipped title/author conversion and was published with raw LaTeX (28 fields across 27 entries, e.g. G\''{u}nter). - The three copied blocks are replaced by _convert_entry_field, which reuses the _convert_latex pipeline validate already had, so the two commands cannot drift. Verified by building before and after: the .bib outputs are byte-identical and entry order is unchanged; exactly 37 fields changed, all LaTeX or brace cleanups. Two consecutive clean builds are byte-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 4c9b4e3 commit 18fc36e

3 files changed

Lines changed: 81 additions & 110 deletions

File tree

‎Makefile‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,52 +7,52 @@ JSONFILES = release/nime_alt.json release/nime_installations.json release/nime_m
77
all: $(BIBFILES) $(CSVFILES) $(YAMLFILES) $(JSONFILES) release/index.html
88

99
release/nime_alt.bib:
10-
poetry run python nime_bib collate --type alt --format bib --id_order
10+
poetry run python nime_bib collate --type alt --format bib
1111

1212
release/nime_alt.csv:
13-
poetry run python nime_bib collate --type alt --format csv --id_order
13+
poetry run python nime_bib collate --type alt --format csv
1414

1515
release/nime_alt.yaml:
16-
poetry run python nime_bib collate --type alt --format yaml --id_order
16+
poetry run python nime_bib collate --type alt --format yaml
1717

1818
release/nime_alt.json:
19-
poetry run python nime_bib collate --type alt --format json --id_order
19+
poetry run python nime_bib collate --type alt --format json
2020

2121
release/nime_installations.bib:
22-
poetry run python nime_bib collate --type installation --format bib --id_order
22+
poetry run python nime_bib collate --type installation --format bib
2323

2424
release/nime_installations.csv:
25-
poetry run python nime_bib collate --type installation --format csv --id_order
25+
poetry run python nime_bib collate --type installation --format csv
2626

2727
release/nime_installations.yaml:
28-
poetry run python nime_bib collate --type installation --format yaml --id_order
28+
poetry run python nime_bib collate --type installation --format yaml
2929

3030
release/nime_installations.json:
31-
poetry run python nime_bib collate --type installation --format json --id_order
31+
poetry run python nime_bib collate --type installation --format json
3232

3333
release/nime_music.bib:
34-
poetry run python nime_bib collate --type music --format bib --id_order
34+
poetry run python nime_bib collate --type music --format bib
3535

3636
release/nime_music.csv:
37-
poetry run python nime_bib collate --type music --format csv --id_order
37+
poetry run python nime_bib collate --type music --format csv
3838

3939
release/nime_music.yaml:
40-
poetry run python nime_bib collate --type music --format yaml --id_order
40+
poetry run python nime_bib collate --type music --format yaml
4141

4242
release/nime_music.json:
43-
poetry run python nime_bib collate --type music --format json --id_order
43+
poetry run python nime_bib collate --type music --format json
4444

4545
release/nime_papers.bib:
46-
poetry run python nime_bib collate --type paper --format bib --id_order
46+
poetry run python nime_bib collate --type paper --format bib
4747

4848
release/nime_papers.csv:
49-
poetry run python nime_bib collate --type paper --format csv --id_order
49+
poetry run python nime_bib collate --type paper --format csv
5050

5151
release/nime_papers.yaml:
52-
poetry run python nime_bib collate --type paper --format yaml --id_order
52+
poetry run python nime_bib collate --type paper --format yaml
5353

5454
release/nime_papers.json:
55-
poetry run python nime_bib collate --type paper --format json --id_order
55+
poetry run python nime_bib collate --type paper --format json
5656

5757
release/index.html:
5858
sh scripts/create_release_index.sh

‎nime_bib/__main__.py‎

Lines changed: 54 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,27 @@
1313

1414

1515
def set_id_order(id_order):
16-
"""Sets the order for the bibtex writer
16+
"""Sets the entry order for the bibtex writer.
17+
18+
id_order=True sorts entries by their BibTeX key. Otherwise the writer's
19+
default (article number, url, key) is used. Note the trailing comma:
20+
("ID") is just the string "ID", which bibtexparser would read as the two
21+
field names "I" and "D" and silently not sort at all.
1722
"""
18-
# set ordering property:
19-
if (id_order):
20-
utils.writer.order_entries_by = ("ID")
23+
if id_order:
24+
utils.writer.order_entries_by = ("ID",)
2125
click.secho("ordering by ID")
2226
else:
2327
click.secho(f"Using default order: {utils.writer.order_entries_by}")
24-
# else, canonical order: #utils.writer.order_entries_by = ("articleno", "url", "ID")
28+
29+
30+
def preserve_file_order():
31+
"""Makes the bibtex writer keep entries in the order they were parsed.
32+
33+
The source files are maintained in page/article order, and the collated
34+
output concatenates them year by year, so this is the canonical order.
35+
"""
36+
utils.writer.order_entries_by = None
2537

2638

2739
@click.command()
@@ -73,49 +85,44 @@ def harmonise(year, type, id_order):
7385
default="paper",
7486
help="type of proceedings"
7587
)
76-
@click.option(
77-
"--id_order",
78-
"-I",
79-
is_flag=True,
80-
default=False,
81-
help="sorts the output by entry ID/key (default: sort by article/page number)"
82-
)
8388
@click.option(
8489
"--format",
8590
"-F",
8691
type=click.Choice(["bib", "csv", "yaml", "json"]),
8792
default="bib",
8893
help="format of output"
8994
)
90-
def collate(type, id_order, format):
95+
def collate(type, format):
9196
"""Collates all NIME proceedings of a certain type and saves to an output
9297
file.
98+
99+
Entries are output year by year (files sorted by name) and, within a
100+
year, in the order they appear in the source file, which is page/article
101+
order. The order is the same for every output format and every platform,
102+
so two builds of the same sources can be compared byte for byte.
93103
"""
94104
output_file = utils.collated_path(type, format)
95-
bibfiles = []
105+
bibfiles = utils.glob_for_proc(type)
96106
bib_entries = []
97-
bib_databases = []
98-
for file in utils.glob_for_proc(type):
99-
bibfiles.append(file)
100107

101108
with click.progressbar(bibfiles) as bar:
102109
for bf in bar:
103-
with open(bf) as bibtex_file:
110+
with open(bf, encoding="utf-8") as bibtex_file:
104111
bd = bibtexparser.bparser.BibTexParser(
105112
common_strings=True
106113
).parse_file(bibtex_file)
107-
bib_databases.append(bd)
108114
bib_entries.extend(bd.entries)
109-
115+
110116
# set up collated database
117+
bd = bibtexparser.bibdatabase.BibDatabase()
111118
bd.entries = bib_entries
112119
accent_converter = latex_accents.AccentConverter()
113120

114121
# BibTex Output (preserve latex accents)
115122
if format == "bib":
116-
set_id_order(id_order)
117-
with open(output_file, 'w') as bibtex_file:
118-
bibtex_file.write(utils.writer.write(bd))
123+
preserve_file_order()
124+
with open(output_file, 'w', encoding="utf-8") as bibtex_file:
125+
bibtex_file.write(utils.writer.write(bd))
119126

120127
# Other formats (convert accents to UTF8)
121128
for e in bib_entries:
@@ -125,70 +132,12 @@ def collate(type, id_order, format):
125132
bibtex_str = bibtexparser.dumps(small_bd)
126133
e['bibtex'] = bibtex_str
127134

128-
# look at entries with \
129-
# look at abstract, title, author
130-
try:
131-
if '\\' in e['abstract']:
132-
e['abstract'] = accent_converter.decode_Tex_Accents(
133-
e['abstract'],
134-
utf8_or_ascii=1
135-
)
136-
e['abstract'] = latex_symbols.replace_symbols(e['abstract'])
137-
e['abstract'] = accent_converter.decode_Tex_Accents(
138-
e['abstract'],
139-
utf8_or_ascii=1
140-
) # do this twice to catch them all.
141-
if '\\' in e['abstract']:
142-
click.secho(
143-
f"{e['ID']} abstract still contains backslashes!",
144-
fg="red"
145-
)
146-
click.secho(f"{e['ID']} Abstract: {e['abstract']}", fg="yellow")
147-
e['abstract'] = latex_symbols.clean_braces(e['abstract'])
148-
except Exception:
149-
continue
150-
151-
try:
152-
if '\\' in e['title']:
153-
e['title'] = accent_converter.decode_Tex_Accents(
154-
e['title'],
155-
utf8_or_ascii=1
156-
)
157-
e['title'] = latex_symbols.replace_symbols(e['title'])
158-
e['title'] = accent_converter.decode_Tex_Accents(
159-
e['title'],
160-
utf8_or_ascii=1
161-
) # do this twice to catch them all.
162-
if '\\' in e['title']:
163-
click.secho(
164-
f"{e['ID']} title still contains backslashes!",
165-
fg="red"
166-
)
167-
click.secho(f"{e['ID']} Title: {e['title']}", fg="yellow")
168-
e['title'] = latex_symbols.clean_braces(e['title'])
169-
except Exception as exc:
170-
click.secho(f"Exception: {exc}", fg="red")
171-
172-
try:
173-
if '\\' in e['author']:
174-
e['author'] = accent_converter.decode_Tex_Accents(
175-
e['author'],
176-
utf8_or_ascii=1
177-
)
178-
e['author'] = latex_symbols.replace_symbols(e['author'])
179-
e['author'] = accent_converter.decode_Tex_Accents(
180-
e['author'],
181-
utf8_or_ascii=1
182-
) # do this twice to catch them all.
183-
if '\\' in e['author']:
184-
click.secho(
185-
f"{e['ID']} author still contains backslashes!",
186-
fg="red"
187-
)
188-
click.secho(f"{e['ID']} Author: {e['author']}", fg="yellow")
189-
e['author'] = latex_symbols.clean_braces(e['author'])
190-
except Exception as exc:
191-
click.secho(f"Exception: {exc}", fg="red")
135+
# Convert LaTeX in the published text fields to UTF-8. Each field is
136+
# handled independently: an entry with no abstract must still have its
137+
# title and author converted (this used to `continue` on the missing
138+
# abstract and skip them, see issue #98).
139+
for field in ("abstract", "title", "author"):
140+
_convert_entry_field(e, field, accent_converter)
192141

193142
try:
194143
if '\\' in e['ID']:
@@ -417,7 +366,7 @@ def add_dois(year, csvfile, type, translated):
417366
# IMPORTANT: In translation mode we do NOT add or modify e['doi'].
418367

419368
# Write back to the bibtex file
420-
set_id_order(True)
369+
preserve_file_order()
421370
with open(nime_file, 'w') as bibtex_file:
422371
bibtex_file.write(utils.writer.write(bib_database))
423372

@@ -436,6 +385,23 @@ def _convert_latex(text, accent_converter):
436385
return text
437386

438387

388+
def _convert_entry_field(entry, field, accent_converter):
389+
"""Converts LaTeX in one text field of an entry to UTF-8, in place.
390+
391+
Does nothing if the entry lacks the field. Warns in red about backslashes
392+
that survive conversion, since those reach the published files as-is.
393+
"""
394+
value = entry.get(field)
395+
if value is None:
396+
return
397+
if '\\' in value:
398+
value = _convert_latex(value, accent_converter)
399+
if '\\' in value:
400+
click.secho(f"{entry['ID']} {field} still contains backslashes!", fg="red")
401+
click.secho(f"{entry['ID']} {field}: {value}", fg="yellow")
402+
entry[field] = latex_symbols.clean_braces(value)
403+
404+
439405
def _raw_entry_keys(text):
440406
"""Returns the BibTeX keys of every entry header found in raw file text.
441407

‎nime_bib/utils.py‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,18 +41,23 @@ def path_for_proc(year, proc_type):
4141
return None
4242

4343
def glob_for_proc(proc_type):
44-
"""Returns the glob of available procs for a given type.
44+
"""Returns the available proceedings files for a given type, sorted by
45+
filename (i.e. by year).
46+
47+
Sorted deliberately: Path.glob returns filesystem order, which differs
48+
between macOS and the Linux CI runner and would make the collated output
49+
non-reproducible.
4550
"""
4651
if proc_type == "paper":
47-
return PAPER_PROC.glob(f"nime*.bib")
52+
return sorted(PAPER_PROC.glob("nime*.bib"))
4853
elif proc_type == "installation":
49-
return INSTALL_PROC.glob(f"nime*_installations.bib")
54+
return sorted(INSTALL_PROC.glob("nime*_installations.bib"))
5055
elif proc_type == "music":
51-
return MUSIC_PROC.glob(f"nime*_music.bib")
56+
return sorted(MUSIC_PROC.glob("nime*_music.bib"))
5257
elif proc_type == "alt":
53-
return ALT_PROC.glob(f"nime*_alt.bib")
58+
return sorted(ALT_PROC.glob("nime*_alt.bib"))
5459
else:
55-
return None
60+
return []
5661

5762

5863
# field order for nime proc entries.

0 commit comments

Comments
 (0)