Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion bibtexparser/middlewares/enclosing.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,12 @@ def _enclose(
if demanded_enclosing is not None:
enclosing = demanded_enclosing
elif self._reuse_previous_enclosing and metadata_enclosing is not None:
enclosing = metadata_enclosing
# Parser metadata describes the source value, not necessarily its
# current edited value. Reusing a bare integer's metadata for new
# ordinary text would emit invalid BibTeX, so fall back to the
# configured default unless the bare representation remains safe.
if metadata_enclosing != "no-enclosing" or str(value).isdigit():
enclosing = metadata_enclosing
elif apply_int_rule and not self._enclose_integers and str(value).isdigit():
return str(value)

Expand Down
2 changes: 1 addition & 1 deletion bibtexparser/middlewares/parsestack.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ def default_unparse_stack(allow_inplace_modification: bool = False) -> list[Midd
AddEnclosingMiddleware(
allow_inplace_modification=allow_inplace_modification,
default_enclosing="{",
reuse_previous_enclosing=False,
reuse_previous_enclosing=True,
enclose_integers=True,
)
]
7 changes: 7 additions & 0 deletions docs/source/customize.rst
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,13 @@ The default write stack consists of the following layers:

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

For parsed values, the default writer reuses the quote, brace, or safe
unenclosed style recorded by
:class:`bibtexparser.middlewares.RemoveEnclosingMiddleware`. New programmatic
values without prior enclosure metadata use curly braces. If a formerly bare
integer is edited to ordinary text, the writer also falls back to braces rather
than emitting invalid bare text.

When specifying their own stack, user get to chose if they want to add to or overwrite the default stack
by selecting the corresponding argument when calling :code:`bibtexparser.parse` or :code:`bibtexparser.write`:

Expand Down
54 changes: 51 additions & 3 deletions tests/middleware_tests/test_enclosing.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,10 @@ def test_addition_of_enclosing_on_entry(
used_enclosing = _figure_out_added_enclosing(changed_value, value)

# Assert correct enclosing was added
if reuse_previous_enclosing and metadata_enclosing is not None:
can_safely_reuse_metadata = metadata_enclosing != "no-enclosing" or (
isinstance(value, int) or value.isdigit()
)
if reuse_previous_enclosing and metadata_enclosing is not None and can_safely_reuse_metadata:
expected_enclosing = metadata_enclosing
elif (isinstance(value, int) or value.isdigit()) and not enclose_ints:
expected_enclosing = "no-enclosing"
Expand Down Expand Up @@ -417,8 +420,53 @@ def test_string_reference_roundtrip():

assert "month = jan" in written
assert 'pages = intro # "--" # outro' in written
# Ints are still enclosed by the default unparse stack
assert "year = {2019}" in written
# The default unparse stack also retains the integer's bare source style.
assert "year = 2019" in written


def test_default_roundtrip_reuses_parsed_value_enclosures():
"""Ordinary writing retains quote, brace, and safe bare source choices."""
library = bibtexparser.parse_string(
'@article{key, quoted="Quoted", braced={Braced}, year=2019}'
)

written = bibtexparser.write_string(library)

assert 'quoted = "Quoted"' in written
assert "braced = {Braced}" in written
assert "year = 2019" in written


def test_edit_reuses_quoted_and_braced_enclosures():
"""Changing a value must not also replace its established safe enclosure style."""
library = bibtexparser.parse_string('@article{key, quoted="Before", braced={Before}}')
library.entries[0]["quoted"] = "After"
library.entries[0]["braced"] = "After"

written = bibtexparser.write_string(library)

assert 'quoted = "After"' in written
assert "braced = {After}" in written


def test_editing_bare_integer_to_text_falls_back_to_braces():
"""Stale bare metadata cannot make an edited ordinary string invalid BibTeX."""
library = bibtexparser.parse_string("@article{key, year=2019}")
library.entries[0]["year"] = "forthcoming soon"

written = bibtexparser.write_string(library)

assert "year = {forthcoming soon}" in written


def test_new_field_without_enclosure_metadata_uses_braces():
"""Source-style reuse does not change the safe default for programmatic values."""
library = bibtexparser.parse_string('@article{key, title="Existing"}')
library.entries[0]["note"] = "New value"

written = bibtexparser.write_string(library)

assert "note = {New value}" in written


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