Skip to content

Commit cc434af

Browse files
author
Michael Fruth
authored
✨Allow preservation of existing order of entry fields in writer (#317)
This significantly extends the possibility to change field order when writing entries. Most crucially, it allows to preserve the order in the database (which typically is the order found on parsing).
1 parent b9c82a3 commit cc434af

2 files changed

Lines changed: 115 additions & 4 deletions

File tree

bibtexparser/bwriter.py

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55

66
import logging
7+
from enum import Enum, auto
8+
from typing import Dict, Callable, Iterable
79
from bibtexparser.bibdatabase import (BibDatabase, COMMON_STRINGS,
810
BibDataString,
911
BibDataStringExpression)
@@ -14,6 +16,38 @@
1416
__all__ = ['BibTexWriter']
1517

1618

19+
class SortingStrategy(Enum):
20+
"""
21+
Defines different strategies for sorting the entries not defined in :py:attr:`~.BibTexWriter.display_order` and that are added at the end.
22+
"""
23+
ALPHABETICAL_ASC = auto()
24+
"""
25+
Alphabetical sorting in ascending order.
26+
"""
27+
ALPHABETICAL_DESC = auto()
28+
"""
29+
Alphabetical sorting in descending order.
30+
"""
31+
PRESERVE = auto()
32+
"""
33+
Preserves the order of the entries. Entries are not sorted.
34+
"""
35+
36+
37+
def _apply_sorting_strategy(strategy: SortingStrategy, items: Iterable[str]) -> Iterable[str]:
38+
"""
39+
Sorts the items based on the given sorting strategy.
40+
"""
41+
if strategy == SortingStrategy.ALPHABETICAL_ASC:
42+
return sorted(items)
43+
elif strategy == SortingStrategy.ALPHABETICAL_DESC:
44+
return reversed(sorted(items))
45+
elif strategy == SortingStrategy.PRESERVE:
46+
return items
47+
else:
48+
raise NotImplementedError(f"The strategy {strategy.name} is not implemented.")
49+
50+
1751
def to_bibtex(parsed):
1852
"""
1953
Convenience function for backwards compatibility.
@@ -65,9 +99,12 @@ def __init__(self, write_common_strings=False):
6599
self.entry_separator = '\n'
66100
#: Tuple of fields for ordering BibTeX entries. Set to `None` to disable sorting. Default: BibTeX key `('ID', )`.
67101
self.order_entries_by = ('ID', )
68-
#: Tuple of fields for display order in a single BibTeX entry. Fields not listed here will be displayed
69-
#: alphabetically at the end. Set to '[]' for alphabetical order. Default: '[]'
102+
#: Tuple of fields for display order in a single BibTeX entry. Fields not listed here will be displayed at the
103+
# end in the order defined by display_order_sorting. Default: '[]'
70104
self.display_order = []
105+
# Sorting strategy for entries not contained in display_order. Entries not defined in display_order are added
106+
# at the end in the order defined by this strategy. Default: SortingStrategy.ALPHABETICAL_ASC
107+
self.display_order_sorting: SortingStrategy = SortingStrategy.ALPHABETICAL_ASC
71108
#: BibTeX syntax allows comma first syntax
72109
#: (common in functional languages), use this to enable
73110
#: comma first syntax as the bwriter output
@@ -122,7 +159,7 @@ def _entry_to_bibtex(self, entry):
122159
# first those keys which are both in self.display_order and in entry.keys
123160
display_order = [i for i in self.display_order if i in entry]
124161
# then all the other fields sorted alphabetically
125-
display_order += [i for i in sorted(entry) if i not in self.display_order]
162+
display_order += [i for i in _apply_sorting_strategy(self.display_order_sorting, entry) if i not in self.display_order]
126163
if self.comma_first:
127164
field_fmt = u"\n{indent}, {field:<{field_max_w}} = {value}"
128165
else:

bibtexparser/tests/test_bibtexwriter.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import tempfile
33
import unittest
44
import bibtexparser
5-
from bibtexparser.bwriter import BibTexWriter
5+
from bibtexparser.bwriter import BibTexWriter, SortingStrategy
66
from bibtexparser.bibdatabase import BibDatabase
77

88

@@ -218,6 +218,80 @@ def test_align_multiline_values_with_align(self):
218218
keyword = {keyword1, keyword2,
219219
multiline-keyword1, multiline-keyword2}
220220
}
221+
"""
222+
self.assertEqual(result, expected)
223+
224+
def test_display_order_sorting(self):
225+
bib_database = BibDatabase()
226+
bib_database.entries = [{'ID': 'abc123',
227+
'ENTRYTYPE': 'book',
228+
'b': 'test2',
229+
'a': 'test1',
230+
'd': 'test4',
231+
'c': 'test3',
232+
'e': 'test5'}]
233+
# Only 'a' is not ordered. As it's only one element, strategy should not matter.
234+
for strategy in SortingStrategy:
235+
writer = BibTexWriter()
236+
writer.display_order = ['b', 'c', 'd', 'e', 'a']
237+
writer.display_order_sorting = strategy
238+
result = bibtexparser.dumps(bib_database, writer)
239+
expected = \
240+
"""@book{abc123,
241+
b = {test2},
242+
c = {test3},
243+
d = {test4},
244+
e = {test5},
245+
a = {test1}
246+
}
247+
"""
248+
self.assertEqual(result, expected)
249+
250+
# Test ALPHABETICAL_ASC strategy
251+
writer = BibTexWriter()
252+
writer.display_order = ['c']
253+
writer.display_order_sorting = SortingStrategy.ALPHABETICAL_ASC
254+
result = bibtexparser.dumps(bib_database, writer)
255+
expected = \
256+
"""@book{abc123,
257+
c = {test3},
258+
a = {test1},
259+
b = {test2},
260+
d = {test4},
261+
e = {test5}
262+
}
263+
"""
264+
self.assertEqual(result, expected)
265+
266+
# Test ALPHABETICAL_DESC strategy
267+
writer = BibTexWriter()
268+
writer.display_order = ['c']
269+
writer.display_order_sorting = SortingStrategy.ALPHABETICAL_DESC
270+
result = bibtexparser.dumps(bib_database, writer)
271+
expected = \
272+
"""@book{abc123,
273+
c = {test3},
274+
e = {test5},
275+
d = {test4},
276+
b = {test2},
277+
a = {test1}
278+
}
279+
"""
280+
self.assertEqual(result, expected)
281+
282+
# Test PRESERVE strategy
283+
writer = BibTexWriter()
284+
writer.display_order = ['c']
285+
writer.display_order_sorting = SortingStrategy.PRESERVE
286+
result = bibtexparser.dumps(bib_database, writer)
287+
expected = \
288+
"""@book{abc123,
289+
c = {test3},
290+
b = {test2},
291+
a = {test1},
292+
d = {test4},
293+
e = {test5}
294+
}
221295
"""
222296
self.assertEqual(result, expected)
223297

0 commit comments

Comments
 (0)