Skip to content

✨ Honor enclosing demands on fields and strings when writing (#447) - #528

Merged
MiWeiss merged 5 commits into
mainfrom
447-field-enclosing
Jun 12, 2026
Merged

MiWeiss merged 5 commits into
mainfrom
447-field-enclosing

Conversation

@MiWeiss

@MiWeiss MiWeiss commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator

Closes #447

What this PR does

Adds an explicit, always-honored enclosing demand to field and string values, and uses it to make month macros (and other bibtex string references) survive writing with the default stack.

1. Field.enclosing / String.enclosing (new model attribute)

  • Allowed values: '{', '"', 'no-enclosing', or None (default = writer middleware decides).
  • AddEnclosingMiddleware honors a non-None demand with precedence over reuse_previous_enclosing, the int-rule, and default_enclosing.
  • Assigning a new value resets the demand to None, so a stale demand can never be applied to a changed value (and the demand is naturally "consumed" once the enclosed value is written).

2. Month middlewares set the demand (the actual #447 use-case)

MonthAbbreviationMiddleware and MonthIntMiddleware now demand no-enclosing for the macros/ints they produce. With the default unparse stack:

library = bibtexparser.parse_string("@article{a, month = {1}}")
bibtexparser.write_string(library, prepend_middleware=[MonthAbbreviationMiddleware()])
# now contains `month = jan` (string reference), previously `month = {jan}` (literal)

MonthLongStringMiddleware deliberately sets no demand (full month names are literals).

3. Parse→write roundtrip no longer corrupts references (latent bug)

Previously, a plain parse_string → write_string roundtrip turned month = jan into month = {jan} and pages = intro # outro into {intro # outro} — silently changing semantics. RemoveEnclosingMiddleware now sets the no-enclosing demand on unenclosed, non-numeric values. Bare integers (year = 2019) remain governed by enclose_integers, as before.

Note: This changes default write output for unenclosed non-numeric inputs (verbatim instead of brace-normalized). I'd argue the old behavior was a bug since it changed semantics, but it's in a separate commit (7e37177) for easy discussion/removal.

Drive-by fixes

  • AddEnclosingMiddleware._enclose no longer crashes on int values when enclose_integers=False (value.isdigit() → AttributeError).
  • Month middlewares' unchanged-month paths returned the Field object instead of its value (assigning the field to itself as field.value).

Design discussion

Alternatives considered (marker str subclass, parser-metadata convention, month special-casing in the writer) — the explicit attribute was chosen because it is discoverable, expresses any enclosing preference (not just "none"), is robust against field-key renames/duplicates, and fails safe via the value-setter reset.

🤖 Generated with Claude Code

MiWeiss and others added 5 commits June 12, 2026 07:36
A non-None `enclosing` ('{', '"' or 'no-enclosing') on a Field or
String is now honored by the `AddEnclosingMiddleware` with precedence
over all other enclosing rules. Assigning a new value resets the
demand, such that stale demands cannot be applied to changed values.

Also makes `AddEnclosingMiddleware._enclose` safe for int values
(previously, `value.isdigit()` raised AttributeError for int values
when `enclose_integers=False`).

Towards #447

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`MonthAbbreviationMiddleware` and `MonthIntMiddleware` now set a
`no-enclosing` demand on the month field, such that the default
unparse stack writes `month = jan` instead of `month = {jan}`
(which would turn the string reference into a literal).

Also fixes a latent bug where the middlewares' unchanged-month paths
returned the `Field` object instead of its value, assigning the field
to itself as value.

Fixes #447

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Previously, a plain parse -> write roundtrip turned unresolved string
references and concatenations into literals (e.g. `month = jan`
became `month = {jan}`), silently changing their semantics.

The `RemoveEnclosingMiddleware` now sets a `no-enclosing` demand on
unenclosed, non-numeric values, which the `AddEnclosingMiddleware`
honors. Plain integer values (e.g. `year = 2019`) remain subject to
the writer's `enclose_integers` option, as before.

Related to #447

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@MiWeiss

MiWeiss commented Jun 12, 2026

Copy link
Copy Markdown
Collaborator Author

reviewed

@MiWeiss
MiWeiss merged commit 9fe14b0 into main Jun 12, 2026
19 checks passed
@MiWeiss
MiWeiss deleted the 447-field-enclosing branch June 12, 2026 10:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

✨ Nice way to handle enclosings for months while writing with the default stack

1 participant