Skip to content

Commit f000c53

Browse files
committed
Reuse parsed value enclosures by default
* Preserve quote, brace, and safe bare styles during ordinary writes. * Fall back to braces when edits make stale bare metadata unsafe. * Retain brace defaults for values created without parser metadata.
1 parent 08e370a commit f000c53

4 files changed

Lines changed: 65 additions & 5 deletions

File tree

bibtexparser/middlewares/enclosing.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,12 @@ def _enclose(
148148
if demanded_enclosing is not None:
149149
enclosing = demanded_enclosing
150150
elif self._reuse_previous_enclosing and metadata_enclosing is not None:
151-
enclosing = metadata_enclosing
151+
# Parser metadata describes the source value, not necessarily its
152+
# current edited value. Reusing a bare integer's metadata for new
153+
# ordinary text would emit invalid BibTeX, so fall back to the
154+
# configured default unless the bare representation remains safe.
155+
if metadata_enclosing != "no-enclosing" or str(value).isdigit():
156+
enclosing = metadata_enclosing
152157
elif apply_int_rule and not self._enclose_integers and str(value).isdigit():
153158
return str(value)
154159

bibtexparser/middlewares/parsestack.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def default_unparse_stack(allow_inplace_modification: bool = False) -> list[Midd
1919
AddEnclosingMiddleware(
2020
allow_inplace_modification=allow_inplace_modification,
2121
default_enclosing="{",
22-
reuse_previous_enclosing=False,
22+
reuse_previous_enclosing=True,
2323
enclose_integers=True,
2424
)
2525
]

docs/source/customize.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@ The default write stack consists of the following layers:
5555

5656
* :class:`bibtexparser.middlewares.AddEnclosingMiddleware`: Encloses values in curly braces where needed.
5757

58+
For parsed values, the default writer reuses the quote, brace, or safe
59+
unenclosed style recorded by
60+
:class:`bibtexparser.middlewares.RemoveEnclosingMiddleware`. New programmatic
61+
values without prior enclosure metadata use curly braces. If a formerly bare
62+
integer is edited to ordinary text, the writer also falls back to braces rather
63+
than emitting invalid bare text.
64+
5865
When specifying their own stack, user get to chose if they want to add to or overwrite the default stack
5966
by selecting the corresponding argument when calling :code:`bibtexparser.parse` or :code:`bibtexparser.write`:
6067

tests/middleware_tests/test_enclosing.py

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,10 @@ def test_addition_of_enclosing_on_entry(
166166
used_enclosing = _figure_out_added_enclosing(changed_value, value)
167167

168168
# Assert correct enclosing was added
169-
if reuse_previous_enclosing and metadata_enclosing is not None:
169+
can_safely_reuse_metadata = metadata_enclosing != "no-enclosing" or (
170+
isinstance(value, int) or value.isdigit()
171+
)
172+
if reuse_previous_enclosing and metadata_enclosing is not None and can_safely_reuse_metadata:
170173
expected_enclosing = metadata_enclosing
171174
elif (isinstance(value, int) or value.isdigit()) and not enclose_ints:
172175
expected_enclosing = "no-enclosing"
@@ -417,8 +420,53 @@ def test_string_reference_roundtrip():
417420

418421
assert "month = jan" in written
419422
assert 'pages = intro # "--" # outro' in written
420-
# Ints are still enclosed by the default unparse stack
421-
assert "year = {2019}" in written
423+
# The default unparse stack also retains the integer's bare source style.
424+
assert "year = 2019" in written
425+
426+
427+
def test_default_roundtrip_reuses_parsed_value_enclosures():
428+
"""Ordinary writing retains quote, brace, and safe bare source choices."""
429+
library = bibtexparser.parse_string(
430+
'@article{key, quoted="Quoted", braced={Braced}, year=2019}'
431+
)
432+
433+
written = bibtexparser.write_string(library)
434+
435+
assert 'quoted = "Quoted"' in written
436+
assert "braced = {Braced}" in written
437+
assert "year = 2019" in written
438+
439+
440+
def test_edit_reuses_quoted_and_braced_enclosures():
441+
"""Changing a value must not also replace its established safe enclosure style."""
442+
library = bibtexparser.parse_string('@article{key, quoted="Before", braced={Before}}')
443+
library.entries[0]["quoted"] = "After"
444+
library.entries[0]["braced"] = "After"
445+
446+
written = bibtexparser.write_string(library)
447+
448+
assert 'quoted = "After"' in written
449+
assert "braced = {After}" in written
450+
451+
452+
def test_editing_bare_integer_to_text_falls_back_to_braces():
453+
"""Stale bare metadata cannot make an edited ordinary string invalid BibTeX."""
454+
library = bibtexparser.parse_string("@article{key, year=2019}")
455+
library.entries[0]["year"] = "forthcoming soon"
456+
457+
written = bibtexparser.write_string(library)
458+
459+
assert "year = {forthcoming soon}" in written
460+
461+
462+
def test_new_field_without_enclosure_metadata_uses_braces():
463+
"""Source-style reuse does not change the safe default for programmatic values."""
464+
library = bibtexparser.parse_string('@article{key, title="Existing"}')
465+
library.entries[0]["note"] = "New value"
466+
467+
written = bibtexparser.write_string(library)
468+
469+
assert "note = {New value}" in written
422470

423471

424472
# TODO round-trip tests (removal -> addition -> removal)

0 commit comments

Comments
 (0)