diff --git a/bibtexparser/middlewares/latex_encoding.py b/bibtexparser/middlewares/latex_encoding.py index d95d5ff..af24842 100644 --- a/bibtexparser/middlewares/latex_encoding.py +++ b/bibtexparser/middlewares/latex_encoding.py @@ -50,7 +50,10 @@ def transform_entry(self, entry: Entry, library: Library) -> Block: errors = [] for field in entry.fields: if isinstance(field.value, str): + # The value setter resets `enclosing`; only the representation changes here. + enclosing = field.enclosing field.value, e = self._transform_python_value_string(field.value) + field.enclosing = enclosing errors.append(e) elif isinstance(field.value, NameParts): field.value.first = self._transform_all_strings(field.value.first, errors) @@ -73,7 +76,10 @@ def transform_entry(self, entry: Entry, library: Library) -> Block: # docstr-coverage: inherited def transform_string(self, string: String, library: "Library") -> Block: if isinstance(string.value, str): + # See `transform_entry`. + enclosing = string.enclosing string.value, error = self._transform_python_value_string(string.value) + string.enclosing = enclosing if error != "": return MiddlewareErrorBlock(block=string, error=PartialMiddlewareException([error])) else: diff --git a/bibtexparser/middlewares/names.py b/bibtexparser/middlewares/names.py index bfb15c0..7169bd4 100644 --- a/bibtexparser/middlewares/names.py +++ b/bibtexparser/middlewares/names.py @@ -62,7 +62,10 @@ def transform_entry(self, entry: Entry, library: Library) -> Block: try: for field in entry.fields: if field.key in self.name_fields: + # The value setter resets `enclosing`; only the representation changes. + enclosing = field.enclosing field.value = self._transform_field_value(field.value) + field.enclosing = enclosing return entry except InvalidNameError as e: return MiddlewareErrorBlock(entry, e) diff --git a/tests/middleware_tests/test_latex_encoding.py b/tests/middleware_tests/test_latex_encoding.py index 64c3d88..7c11933 100644 --- a/tests/middleware_tests/test_latex_encoding.py +++ b/tests/middleware_tests/test_latex_encoding.py @@ -7,6 +7,7 @@ import pytest +import bibtexparser from bibtexparser import Library from bibtexparser.exceptions import PartialMiddlewareException from bibtexparser.middlewares.latex_encoding import LatexDecodingMiddleware @@ -175,7 +176,69 @@ def test_inplace(inplace: bool, middleware_class): assert_inplace_is_respected(inplace, input_entry, transformed_library.entries[0]) -def _entry_with_latex_string(latex_string): +@pytest.mark.parametrize("middleware_class", [LatexEncodingMiddleware, LatexDecodingMiddleware]) +@pytest.mark.parametrize("enclosing", ["no-enclosing", "{", '"']) +def test_enclosing_demand_survives_field_transformation(middleware_class, enclosing): + """Latex de-/encoding changes the representation of a value, not its kind. + + Thus, the enclosing demand (which the `Field.value` setter resets) must be restored.""" + input_entry = _entry_with_latex_string("jan", enclosing=enclosing) + + transformed_library = middleware_class(allow_inplace_modification=True).transform( + Library([input_entry]) + ) + + transformed_field = transformed_library.entries[0].fields_dict["tested_field"] + assert transformed_field.value == "jan" + assert transformed_field.enclosing == enclosing + + +@pytest.mark.parametrize("middleware_class", [LatexEncodingMiddleware, LatexDecodingMiddleware]) +@pytest.mark.parametrize("enclosing", ["no-enclosing", "{", '"']) +def test_enclosing_demand_survives_string_transformation(middleware_class, enclosing): + """Same as `test_enclosing_demand_survives_field_transformation`, for String blocks.""" + input_string = String( + key="me", value="jan", start_line=1, raw="irrelevant", enclosing=enclosing + ) + + transformed_library = middleware_class(allow_inplace_modification=True).transform( + Library([input_string]) + ) + + transformed_string = transformed_library.strings[0] + assert transformed_string.value == "jan" + assert transformed_string.enclosing == enclosing + + +@pytest.mark.parametrize("middleware_class", [LatexEncodingMiddleware, LatexDecodingMiddleware]) +@pytest.mark.parametrize( + "bibtex", + [ + pytest.param("@article{someEntry,\n\tmonth = jan\n}", id="entry_string_reference"), + pytest.param("@string{intro = tro}", id="string_block_reference"), + ], +) +def test_unenclosed_values_are_written_verbatim(middleware_class, bibtex): + """Unenclosed values must not be enclosed on write, even with a de-/encoder in the stack.""" + library = bibtexparser.parse_string( + bibtex, append_middleware=[middleware_class(allow_inplace_modification=True)] + ) + assert bibtexparser.write_string(library).strip() == bibtex.strip() + + +def test_concatenation_roundtrips_with_latex_decoding(): + """Concatenation expressions must survive a parse-transform-write roundtrip. + + Note: Only decoding is tested here, as the encoder (correctly, for regular values) + escapes the `#` of the concatenation itself.""" + bibtex = "@article{someEntry,\n\tpages = intro # outro\n}" + library = bibtexparser.parse_string( + bibtex, append_middleware=[LatexDecodingMiddleware(allow_inplace_modification=True)] + ) + assert bibtexparser.write_string(library).strip() == bibtex.strip() + + +def _entry_with_latex_string(latex_string, enclosing=None): return Entry( start_line=1, raw="Not relevant for this test", @@ -186,6 +249,7 @@ def _entry_with_latex_string(latex_string): start_line=1, key="tested_field", value=latex_string, + enclosing=enclosing, ) ], ) diff --git a/tests/middleware_tests/test_names.py b/tests/middleware_tests/test_names.py index ed28ec2..7d809f9 100644 --- a/tests/middleware_tests/test_names.py +++ b/tests/middleware_tests/test_names.py @@ -2,6 +2,7 @@ import pytest as pytest +import bibtexparser from bibtexparser.library import Library from bibtexparser.middlewares.names import InvalidNameError from bibtexparser.middlewares.names import MergeCoAuthors @@ -1125,3 +1126,54 @@ def test_split_name_parts_exception(name: str, reason: str): # Using same test as in test_name_splitting_strict_mode with pytest.raises(InvalidNameError, match=f".*{name}.*{reason}.*"): raise transformed_library.failed_blocks[0].error + + +@pytest.mark.parametrize( + "middleware, field_value", + [ + pytest.param(SeparateCoAuthors(), "someone", id="separate_coauthors"), + pytest.param(MergeCoAuthors(), ["someone"], id="merge_coauthors"), + pytest.param(SplitNameParts(), ["someone"], id="split_name_parts"), + pytest.param(MergeNameParts(), [NameParts(last=["someone"])], id="merge_name_parts"), + ], +) +@pytest.mark.parametrize("enclosing", ["no-enclosing", "{", '"']) +def test_enclosing_demand_survives_name_transformation(middleware, field_value, enclosing): + """Name middlewares change the representation of a value, not its kind. + + Thus, the enclosing demand (which the `Field.value` setter resets) must be restored.""" + input_entry = Entry( + start_line=0, + raw="irrelevant-for-this-test", + entry_type="article", + key="articleKey", + fields=[ + Field(start_line=1, key="author", value=deepcopy(field_value), enclosing=enclosing) + ], + ) + + transformed_library = middleware.transform(Library([input_entry])) + + transformed_field = transformed_library.entries[0].fields_dict["author"] + assert transformed_field.enclosing == enclosing + + +@pytest.mark.parametrize( + "bibtex", + [ + pytest.param("@article{articleKey,\n\tauthor = someone\n}", id="string_reference"), + pytest.param("@article{articleKey,\n\tauthor = first # last\n}", id="concatenation"), + ], +) +def test_unenclosed_name_fields_roundtrip(bibtex): + """Unenclosed name values must not be enclosed on write, despite the name middlewares.""" + library = bibtexparser.parse_string( + bibtex, + append_middleware=[ + SeparateCoAuthors(), + SplitNameParts(), + MergeNameParts(), + MergeCoAuthors(), + ], + ) + assert bibtexparser.write_string(library).strip() == bibtex.strip()