Skip to content

Commit 2ba4323

Browse files
author
Michael Fruth
authored
🐛 align_values allows int or bool, fixes previous min alignment (#316)
Fixes #315 The `align_values` of the BibTexWriter now accepts a bool or int value. If the bool value `true` is specified, the maximal number of characters used in any field name is used as length. If an integer value is specified, the greater of the specified integer value and the overall maximal number of characters used in any field name is used. This commit also fixes the previous behavior of align_values which results in a breaking change. The `ENTRYTYPE` entry was considered for calculating the maximal number of characters, which always leads to a minimum value of `9`. Now, keys that are not written into the BibTex output are ignored which leads to an exact computation of the field name lengths.
1 parent a9c47a6 commit 2ba4323

2 files changed

Lines changed: 123 additions & 38 deletions

File tree

bibtexparser/bwriter.py

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import logging
77
from enum import Enum, auto
8-
from typing import Dict, Callable, Iterable
8+
from typing import Dict, Callable, Iterable, Union
99
from bibtexparser.bibdatabase import (BibDatabase, COMMON_STRINGS,
1010
BibDataString,
1111
BibDataStringExpression)
@@ -15,6 +15,9 @@
1515

1616
__all__ = ['BibTexWriter']
1717

18+
# A list of entries that should not be included in the content (key = value) of a BibTex entry
19+
ENTRY_TO_BIBTEX_IGNORE_ENTRIES = ['ENTRYTYPE', 'ID']
20+
1821

1922
class SortingStrategy(Enum):
2023
"""
@@ -89,9 +92,12 @@ def __init__(self, write_common_strings=False):
8992
self.contents = ['comments', 'preambles', 'strings', 'entries']
9093
#: Character(s) for indenting BibTeX field-value pairs. Default: single space.
9194
self.indent = ' '
92-
#: Align values. Determines the maximal number of characters used in any fieldname and aligns all values
93-
# according to that by filling up with single spaces. Default: False
94-
self.align_values = False
95+
#: Align values. Aligns all values according to a given length by padding with single spaces.
96+
# If align_values is true, the maximum number of characters used in any field name is used as the length.
97+
# If align_values is a number, the greater of the specified value or the number of characters used in the
98+
# field name is used as the length.
99+
# Default: False
100+
self.align_values: Union[int, bool] = False
95101
#: Align multi-line values. Formats a multi-line value such that the text is aligned exactly
96102
# on top of each other. Default: False
97103
self.align_multiline_values = False
@@ -112,7 +118,7 @@ def __init__(self, write_common_strings=False):
112118
#: BibTeX syntax allows the comma to be optional at the end of the last field in an entry.
113119
#: Use this to enable writing this last comma in the bwriter output. Defaults: False.
114120
self.add_trailing_comma = False
115-
#: internal variable used if self.align_values = True
121+
#: internal variable used if self.align_values = True or self.align_values = <number>
116122
self._max_field_width = 0
117123
#: Whether common strings are written
118124
self.common_strings = write_common_strings
@@ -143,10 +149,13 @@ def _entries_to_bibtex(self, bib_database):
143149
else:
144150
entries = bib_database.entries
145151

146-
if self.align_values:
152+
if self.align_values is True:
147153
# determine maximum field width to be used
148-
widths = [max(map(len, entry.keys())) for entry in entries]
154+
widths = [len(ele) for entry in entries for ele in entry if ele not in ENTRY_TO_BIBTEX_IGNORE_ENTRIES]
149155
self._max_field_width = max(widths)
156+
elif type(self.align_values) == int:
157+
# Use specified value
158+
self._max_field_width = self.align_values
150159

151160
return self.entry_separator.join(self._entry_to_bibtex(entry) for entry in entries)
152161

@@ -165,7 +174,8 @@ def _entry_to_bibtex(self, entry):
165174
else:
166175
field_fmt = u",\n{indent}{field:<{field_max_w}} = {value}"
167176
# Write field = value lines
168-
for field in [i for i in display_order if i not in ['ENTRYTYPE', 'ID']]:
177+
for field in [i for i in display_order if i not in ENTRY_TO_BIBTEX_IGNORE_ENTRIES]:
178+
max_field_width = max(len(field), self._max_field_width)
169179
try:
170180
value = _str_or_expr_to_bibtex(entry[field])
171181

@@ -176,12 +186,7 @@ def _entry_to_bibtex(self, entry):
176186
# World}
177187
# Calculate the indent of "World":
178188
# Left of field (whitespaces before e.g. 'title')
179-
value_indent = len(self.indent)
180-
# Field itself (e.g. len('title'))
181-
if self._max_field_width > 0:
182-
value_indent += self._max_field_width
183-
else:
184-
value_indent += len(field)
189+
value_indent = len(self.indent) + max_field_width
185190
# Right of field ' = ' (<- 3 chars) + '{' (<- 1 char)
186191
value_indent += 3 + 1
187192

@@ -190,7 +195,7 @@ def _entry_to_bibtex(self, entry):
190195
bibtex += field_fmt.format(
191196
indent=self.indent,
192197
field=field,
193-
field_max_w=self._max_field_width,
198+
field_max_w=max_field_width,
194199
value=value)
195200
except TypeError:
196201
raise TypeError(u"The field %s in entry %s must be a string"

bibtexparser/tests/test_bibtexwriter.py

Lines changed: 103 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def test_indent(self):
7070
"""
7171
self.assertEqual(result, expected)
7272

73-
def test_align(self):
73+
def test_align_bool(self):
7474
bib_database = BibDatabase()
7575
bib_database.entries = [{'ID': 'abc123',
7676
'ENTRYTYPE': 'book',
@@ -87,6 +87,22 @@ def test_align(self):
8787
"""
8888
self.assertEqual(result, expected)
8989

90+
bib_database = BibDatabase()
91+
bib_database.entries = [{'ID': 'veryveryverylongID',
92+
'ENTRYTYPE': 'book',
93+
'a': 'test',
94+
'bb': 'longvalue'}]
95+
writer = BibTexWriter()
96+
writer.align_values = True
97+
result = bibtexparser.dumps(bib_database, writer)
98+
expected = \
99+
"""@book{veryveryverylongID,
100+
a = {test},
101+
bb = {longvalue}
102+
}
103+
"""
104+
self.assertEqual(result, expected)
105+
90106
with open('bibtexparser/tests/data/multiple_entries_and_comments.bib') as bibtex_file:
91107
bib_database = bibtexparser.load(bibtex_file)
92108
writer = BibTexWriter()
@@ -121,6 +137,70 @@ def test_align(self):
121137
"""
122138
self.assertEqual(result, expected)
123139

140+
def test_align_int(self):
141+
bib_database = BibDatabase()
142+
bib_database.entries = [{'ID': 'abc123',
143+
'ENTRYTYPE': 'book',
144+
'author': 'test',
145+
'thisisaverylongkey': 'longvalue'}]
146+
# Negative value should have no effect
147+
writer = BibTexWriter()
148+
writer.align_values = -20
149+
result = bibtexparser.dumps(bib_database, writer)
150+
expected = \
151+
"""@book{abc123,
152+
author = {test},
153+
thisisaverylongkey = {longvalue}
154+
}
155+
"""
156+
self.assertEqual(result, expected)
157+
158+
# Value smaller than longest field name should only impact the "short" field names
159+
writer = BibTexWriter()
160+
writer.align_values = 10
161+
result = bibtexparser.dumps(bib_database, writer)
162+
expected = \
163+
"""@book{abc123,
164+
author = {test},
165+
thisisaverylongkey = {longvalue}
166+
}
167+
"""
168+
self.assertEqual(result, expected)
169+
170+
171+
with open('bibtexparser/tests/data/multiple_entries_and_comments.bib') as bibtex_file:
172+
bib_database = bibtexparser.load(bibtex_file)
173+
writer = BibTexWriter()
174+
writer.contents = ['entries']
175+
writer.align_values = 15
176+
result = bibtexparser.dumps(bib_database, writer)
177+
expected = \
178+
"""@book{Toto3000,
179+
author = {Toto, A and Titi, B},
180+
title = {A title}
181+
}
182+
183+
@article{Wigner1938,
184+
author = {Wigner, E.},
185+
doi = {10.1039/TF9383400029},
186+
issn = {0014-7672},
187+
journal = {Trans. Faraday Soc.},
188+
owner = {fr},
189+
pages = {29--41},
190+
publisher = {The Royal Society of Chemistry},
191+
title = {The transition state method},
192+
volume = {34},
193+
year = {1938}
194+
}
195+
196+
@book{Yablon2005,
197+
author = {Yablon, A.D.},
198+
publisher = {Springer},
199+
title = {Optical fiber fusion slicing},
200+
year = {2005}
201+
}
202+
"""
203+
self.assertEqual(result, expected)
124204

125205
def test_entry_separator(self):
126206
bib_database = BibDatabase()
@@ -206,17 +286,17 @@ def test_align_multiline_values_with_align(self):
206286
result = bibtexparser.dumps(bib_database, writer)
207287
expected = \
208288
"""@article{Cesar2013,
209-
author = {Jean César},
210-
title = {A mutline line title is very amazing. It should be
211-
long enough to test multilines... with two lines or should we
212-
even test three lines... What an amazing title.},
213-
year = {2013},
214-
journal = {Nice Journal},
215-
abstract = {This is an abstract. This line should be long enough to test
216-
multilines... and with a french érudit word},
217-
comments = {A comment},
218-
keyword = {keyword1, keyword2,
219-
multiline-keyword1, multiline-keyword2}
289+
author = {Jean César},
290+
title = {A mutline line title is very amazing. It should be
291+
long enough to test multilines... with two lines or should we
292+
even test three lines... What an amazing title.},
293+
year = {2013},
294+
journal = {Nice Journal},
295+
abstract = {This is an abstract. This line should be long enough to test
296+
multilines... and with a french érudit word},
297+
comments = {A comment},
298+
keyword = {keyword1, keyword2,
299+
multiline-keyword1, multiline-keyword2}
220300
}
221301
"""
222302
self.assertEqual(result, expected)
@@ -331,17 +411,17 @@ def test_align_multiline_values_with_align_with_indent(self):
331411
result = bibtexparser.dumps(bib_database, writer)
332412
expected = \
333413
"""@article{Cesar2013,
334-
author = {Jean César},
335-
title = {A mutline line title is very amazing. It should be
336-
long enough to test multilines... with two lines or should we
337-
even test three lines... What an amazing title.},
338-
year = {2013},
339-
journal = {Nice Journal},
340-
abstract = {This is an abstract. This line should be long enough to test
341-
multilines... and with a french érudit word},
342-
comments = {A comment},
343-
keyword = {keyword1, keyword2,
344-
multiline-keyword1, multiline-keyword2}
414+
author = {Jean César},
415+
title = {A mutline line title is very amazing. It should be
416+
long enough to test multilines... with two lines or should we
417+
even test three lines... What an amazing title.},
418+
year = {2013},
419+
journal = {Nice Journal},
420+
abstract = {This is an abstract. This line should be long enough to test
421+
multilines... and with a french érudit word},
422+
comments = {A comment},
423+
keyword = {keyword1, keyword2,
424+
multiline-keyword1, multiline-keyword2}
345425
}
346426
"""
347427
self.assertEqual(result, expected)

0 commit comments

Comments
 (0)