Skip to content

Commit

Permalink
✨Allow preservation of existing order of entry fields in writer (#317)
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
michaelfruth authored Sep 9, 2022
1 parent b9c82a3 commit cc434af
Show file tree
Hide file tree
Showing 2 changed files with 115 additions and 4 deletions.
43 changes: 40 additions & 3 deletions bibtexparser/bwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@


import logging
from enum import Enum, auto
from typing import Dict, Callable, Iterable
from bibtexparser.bibdatabase import (BibDatabase, COMMON_STRINGS,
BibDataString,
BibDataStringExpression)
Expand All @@ -14,6 +16,38 @@
__all__ = ['BibTexWriter']


class SortingStrategy(Enum):
"""
Defines different strategies for sorting the entries not defined in :py:attr:`~.BibTexWriter.display_order` and that are added at the end.
"""
ALPHABETICAL_ASC = auto()
"""
Alphabetical sorting in ascending order.
"""
ALPHABETICAL_DESC = auto()
"""
Alphabetical sorting in descending order.
"""
PRESERVE = auto()
"""
Preserves the order of the entries. Entries are not sorted.
"""


def _apply_sorting_strategy(strategy: SortingStrategy, items: Iterable[str]) -> Iterable[str]:
"""
Sorts the items based on the given sorting strategy.
"""
if strategy == SortingStrategy.ALPHABETICAL_ASC:
return sorted(items)
elif strategy == SortingStrategy.ALPHABETICAL_DESC:
return reversed(sorted(items))
elif strategy == SortingStrategy.PRESERVE:
return items
else:
raise NotImplementedError(f"The strategy {strategy.name} is not implemented.")


def to_bibtex(parsed):
"""
Convenience function for backwards compatibility.
Expand Down Expand Up @@ -65,9 +99,12 @@ def __init__(self, write_common_strings=False):
self.entry_separator = '\n'
#: Tuple of fields for ordering BibTeX entries. Set to `None` to disable sorting. Default: BibTeX key `('ID', )`.
self.order_entries_by = ('ID', )
#: Tuple of fields for display order in a single BibTeX entry. Fields not listed here will be displayed
#: alphabetically at the end. Set to '[]' for alphabetical order. Default: '[]'
#: Tuple of fields for display order in a single BibTeX entry. Fields not listed here will be displayed at the
# end in the order defined by display_order_sorting. Default: '[]'
self.display_order = []
# Sorting strategy for entries not contained in display_order. Entries not defined in display_order are added
# at the end in the order defined by this strategy. Default: SortingStrategy.ALPHABETICAL_ASC
self.display_order_sorting: SortingStrategy = SortingStrategy.ALPHABETICAL_ASC
#: BibTeX syntax allows comma first syntax
#: (common in functional languages), use this to enable
#: comma first syntax as the bwriter output
Expand Down Expand Up @@ -122,7 +159,7 @@ def _entry_to_bibtex(self, entry):
# first those keys which are both in self.display_order and in entry.keys
display_order = [i for i in self.display_order if i in entry]
# then all the other fields sorted alphabetically
display_order += [i for i in sorted(entry) if i not in self.display_order]
display_order += [i for i in _apply_sorting_strategy(self.display_order_sorting, entry) if i not in self.display_order]
if self.comma_first:
field_fmt = u"\n{indent}, {field:<{field_max_w}} = {value}"
else:
Expand Down
76 changes: 75 additions & 1 deletion bibtexparser/tests/test_bibtexwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import tempfile
import unittest
import bibtexparser
from bibtexparser.bwriter import BibTexWriter
from bibtexparser.bwriter import BibTexWriter, SortingStrategy
from bibtexparser.bibdatabase import BibDatabase


Expand Down Expand Up @@ -218,6 +218,80 @@ def test_align_multiline_values_with_align(self):
keyword = {keyword1, keyword2,
multiline-keyword1, multiline-keyword2}
}
"""
self.assertEqual(result, expected)

def test_display_order_sorting(self):
bib_database = BibDatabase()
bib_database.entries = [{'ID': 'abc123',
'ENTRYTYPE': 'book',
'b': 'test2',
'a': 'test1',
'd': 'test4',
'c': 'test3',
'e': 'test5'}]
# Only 'a' is not ordered. As it's only one element, strategy should not matter.
for strategy in SortingStrategy:
writer = BibTexWriter()
writer.display_order = ['b', 'c', 'd', 'e', 'a']
writer.display_order_sorting = strategy
result = bibtexparser.dumps(bib_database, writer)
expected = \
"""@book{abc123,
b = {test2},
c = {test3},
d = {test4},
e = {test5},
a = {test1}
}
"""
self.assertEqual(result, expected)

# Test ALPHABETICAL_ASC strategy
writer = BibTexWriter()
writer.display_order = ['c']
writer.display_order_sorting = SortingStrategy.ALPHABETICAL_ASC
result = bibtexparser.dumps(bib_database, writer)
expected = \
"""@book{abc123,
c = {test3},
a = {test1},
b = {test2},
d = {test4},
e = {test5}
}
"""
self.assertEqual(result, expected)

# Test ALPHABETICAL_DESC strategy
writer = BibTexWriter()
writer.display_order = ['c']
writer.display_order_sorting = SortingStrategy.ALPHABETICAL_DESC
result = bibtexparser.dumps(bib_database, writer)
expected = \
"""@book{abc123,
c = {test3},
e = {test5},
d = {test4},
b = {test2},
a = {test1}
}
"""
self.assertEqual(result, expected)

# Test PRESERVE strategy
writer = BibTexWriter()
writer.display_order = ['c']
writer.display_order_sorting = SortingStrategy.PRESERVE
result = bibtexparser.dumps(bib_database, writer)
expected = \
"""@book{abc123,
c = {test3},
b = {test2},
a = {test1},
d = {test4},
e = {test5}
}
"""
self.assertEqual(result, expected)

Expand Down

0 comments on commit cc434af

Please sign in to comment.