Skip to content

parity: Markup's % escapes each conversion; %-format keyed-argument rules (0182, 0106) - #549

Merged
rmorozov merged 2 commits into
masterfrom
claude/project-thread-cjbc6q
Oct 17, 2026
Merged

rmorozov merged 2 commits into
masterfrom
claude/project-thread-cjbc6q

Conversation

@rmorozov

Copy link
Copy Markdown
Member

Requested by Ruslan · project thread

Before: {{ ('%r'|safe) % 'x' }} printed Markup('x'), and {{ ('%s'|safe) % {'a': '<'} }} printed {'a': Markup('&lt;')}, because Markup's % escaped every argument before formatting. '%(a)s %s' % d and '%(a)*d' % d rendered, and 'x' % [1, 2] raised.

After: these match Python Jinja2. They render &#39;x&#39; and {&#39;a&#39;: &#39;&lt;&#39;}. The two keyed formats raise "not enough arguments for format string", and 'x' % [1, 2] renders x.

The change copies markupsafe's _MarkupEscapeHelper. It escapes the str()/repr()/ascii() of the original argument, and does so before precision and width apply. The str() of a Markup argument is not escaped.

How.

  • PythonPercentFormat takes a markup flag. With it set, %s, %r and %a escape their text. %c, %x, %X, %o and * raise, as they do on the helper.
  • Markup %, the format filter on Markup and gettext under autoescape all pass their arguments through unchanged. EscapeFormatArgs is removed from markup.h.
  • In python_format.cpp, a (key) leaves its value as the only argument, and that value is used once, as in CPython. A non-tuple list counts as a mapping (PyMapping_Check), so it is never reported as "not all arguments converted".

Corpus.

  • autoescape.markup_percent_repr now matches. Its line is deleted.
  • New cases: autoescape.markup_* (9), filters.format_* (8, the 0106 block) and i18n.gettext_autoescape_repr.
  • From the 0106 list, %d of 1e30, %r/%a of non-ASCII and dict order already matched; they now have corpus cases.

Divergences listed.

  • autoescape.markup_percent_d_of_string (rejects): filed as 0210. The helper's __int__/__float__ lets %d/%f take a numeric string, which needs Python int()/float() string parsing.
  • wide.filters.format_c_surrogate (output, deliberate): % formats in UTF-8. The lone surrogate from %c matches in narrow templates but becomes U+FFFD on the way back to a wide string.

Task files: 0182 and 0106 are marked done with outcomes, and 0210 is new.

Roles and pushes.

  • Main session (Opus) implemented the change, and the verifier made 1 round. It found no blockers. Its two low findings (* on Markup, list-as-mapping) are fixed in the second commit, and its third (the format form of 0210) was added to the task file.
  • scripts/preflight.sh --perf is green, including the instruction gate.
  • Red CI pushes: 0 so far.

Generated by Claude Code

…lue is the only argument left (0182, 0106)

Markup % and format() on Markup pass the arguments through and let
PythonPercentFormat escape the text of %s/%r/%a, as markupsafe's
_MarkupEscapeHelper does, so %r prints the escaped repr of the original
and containers print their own repr escaped as a whole. %c, %x, %X and %o
raise on a Markup format, as on the helper. EscapeFormatArgs is removed.

After "(key)" the mapping value is the only argument, used once, so
"%(a)*d" and "%(a)s %s" raise "not enough arguments" as in Python.

Files 0210 for %d/%f of a string argument in Markup's %.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsAUaeKoPqQBF7sQ1ikEMC
…unconverted (0182, 0106)

Verifier findings: markupsafe's helper is not an int, so '*' raises on a
Markup format; CPython counts a list as a mapping, so 'x' % [1, 2] is 'x'.
CompiledPercentFormat drops the unused markup flag: a Markup format is
never parsed at Load.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AsAUaeKoPqQBF7sQ1ikEMC
@rmorozov rmorozov self-assigned this Oct 17, 2026
rmorozov added a commit that referenced this pull request Oct 17, 2026
@rmorozov
rmorozov merged commit 9755100 into master Oct 17, 2026
41 checks passed
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.

2 participants