frame-docstrings
DocsRewrites a docstring's quotes to the triple-double-quote form and puts a multi-line docstring's opener and closer on their own lines.
normalize-literals rewrites every literal to one spelling, so 'y' reads "y", U"y" reads "y", and 0XABC reads 0xABC. A value written three ways reads as three values, and the reader has to work out how each was typed before comparing it to anything, so the rule settles quotes, prefixes, and numeric case in one pass over the token stream, each behind a facet of one rule rather than three rules reading the same tokens.
The rule runs second in the pipeline, behind only shed-backslash-continuations, so every length-aware rule downstream measures a literal at the width it ships at rather than the width it was typed at.
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled | bool | true | Turns the rule on or off. |
unify-numerics | bool | true | Uppercases hex digits and lowercases the 0x, 0o, and 0b radix markers, the e exponent, and the j suffix. false keeps every numeric literal spelled as written. |
unify-prefixes | bool | true | Lowercases a string prefix and drops the no-op u. false keeps the prefix cased and ordered as written. |
unify-quotes | bool | true | Rewrites a non-docstring string to " quotes, using ' only where that avoids an escape, and removes a backslash the chosen quote makes unnecessary. false keeps the literal as written. |
Each facet gates one spelling axis on its own, so a project that has settled its quotes by hand can set unify-quotes = false while the prefix and numeric spellings still normalize. Setting enabled = false turns all three off together.
unify-numerics unify-numerics rewrites the numeric spelling, uppercasing hex digits while lowercasing the 0x, 0o, and 0b radix markers, the e exponent, and the j suffix, so 0XdeadBEEF reads 0xDEADBEEF and 10E+3J reads 10e+3j. The digits and the _ separators pass through exactly as written.
unify-prefixes unify-prefixes lowercases every prefix letter and removes the no-op u, so U"y" reads "y", F"{x}" reads f"{x}", and BR"z\d" reads rb"z\d" with the letters ordered raw-first.
unify-quotes unify-quotes rewrites a string to ", keeping ' only where the swap would add an escape, so 'plain' becomes "plain" whereas 'say "hi"' keeps the quotes that spare it two backslashes. A quote character counts once wherever it appears, escaped or not, so a body spelling \" inside single quotes drops the backslash it never needed while the delimiter stays, and every escape sequence that is not a quote passes through as written. A raw string never gains or loses a backslash, which limits its swap to a body where every " already carries one, and a triple-quoted string swaps only when no """ run and no trailing " would sit against the closer.
The facet skips the docstring slot, whose quotes frame-docstrings rewrites to the """ frame, and skips any literal inside a replacement field, whose quotes the enclosing f-string constrains before Python 3.12. The docstring slot is read by position rather than by part count, so an implicitly concatenated leading expression keeps its quotes too.
'ada', 'hello, world', and the empty '' carry neither quote character in their bodies. All three swap to ", because the escape count ties at zero and the tie goes to the double quote, so the module reads one delimiter throughout.
name = "ada"
greeting = "hello, world"
empty = ""
The first statement in render is 'Summary ' 'continued.', an implicit concatenation sitting where a docstring would, with return 'body' below it. Both parts keep their single quotes, whereas 'body' swaps to "body", because the quotes facet skips whatever sits in the docstring slot, and a concatenation there is not a docstring frame-docstrings can reframe either.
render's docstring is U"""Doc.""" and tally's is u'inline.'. Each drops its no-op prefix and neither one's delimiters move, 'inline.' staying single-quoted because it sits in docstring position, since the prefixes facet reaches a docstring whereas the quotes facet leaves that frame to frame-docstrings.
render's docstring 'Summary.' sits above return 'body'. The docstring keeps its single quotes, whereas 'body' swaps to "body", because the quotes facet skips the docstring position entirely and leaves reframing a docstring to frame-docstrings.
quoted = "already \" escaped" carries an escaped ", and apostrophe = "it's fine" carries a bare '. quoted swaps to 'already " escaped' and loses the backslash, even though the swap moves away from the preferred double quote, whereas apostrophe keeps its double quotes because single-quoting it's fine would only trade one escape for another.
picked = f"{u'inner'}" nests u'inner' inside the replacement field of a double-quoted f-string. u'inner' drops its no-op u prefix and keeps its single quotes, because a " nested inside a double-quoted f-string parses only from Python 3.12, so the prefixes and numerics facets reach inside a replacement field whereas the quotes facet does not, only the quote choice being version-sensitive.
mask = f"{0XABC}" carries a hex literal inside its replacement field, and scaled = f"{1E5:>{0O7}}" carries 1E5 in its field and 0O7 a level deeper inside its format spec. All three respell, to 0xABC, 1e5, and 0o7, because the numerics and prefixes facets reach into a replacement field where the quotes facet stops, only the quote choice being constrained by the enclosing string.
pattern = RF'{mask}\d' carries an uppercase RF prefix, and guarded = rf'path "q" {x}' carries a bare "q" in its literal text. pattern becomes rf"{mask}\d", its prefix lowercased raw-first and its delimiters swapped, whereas guarded stays as written, because a raw string can neither gain nor lose a backslash and the "q" would need one after the swap.
digits = r'\d+' and guarded = r'\"exact\"' are raw strings, the second carrying two " characters that each already sit behind a backslash. Both swap to double quotes with their bodies left byte-for-byte, because a raw string carries every backslash into its value, so only the delimiters are the rule's to rewrite and guarded needs no escape added.
traced carries two \" escapes and spare one, both inside single quotes, and mirror carries a \' inside double quotes. Each redundant backslash is removed while the delimiters stay put, because neither escape is needed inside its own quotes, and a quote character counts once wherever it appears, escaped or not, which keeps the delimiter choice stable rather than flipping it on every run.
label = t'{mask:>8}' carries no ", and shout = t'say "hi" to {name}' carries two in its literal text. label swaps to t"{mask:>8}" and shout keeps its single quotes, because double-quoting say "hi" would add two escapes, the same outcome the f-string pair pins entry for entry.
The quotes facet reads a t-string through its own token kinds rather than treating it as an f-string, so this case pins the behavior against the tokens that differ.
banner is a two-line literal in triple single quotes sitting outside docstring position. Its delimiters move to """ with both lines of its body untouched, because the delimiter follows the same preference a one-line string does, so a multi-line block reads with the same frame as everything around it.
label = f'{mask:>8}' and plain = f'no field at all' carry no " anywhere inside them. Both swap to double quotes, label bringing its :>8 format spec through the swap untouched and plain having no replacement field at all, and the opener, the closer, and the literal text between them swap in a single edit.
0XdeadBEEF, 0O755, 0B1011, 1.5E10, and 2.5J each carry an uppercase marker. 0XdeadBEEF becomes 0xDEADBEEF, with the radix marker lowercased and the hex digits uppercased, and the same lowering reaches the 0O, 0B, E, and J markers, so the marker and the digits take opposite cases and the digits stand clear of the notation that introduces them.
U"unicode", R"a\d", B"bytes", and BR"z\d" each carry an uppercase prefix. The U disappears outright, R and B lowercase to r and b, and BR comes out raw-first as rb, because every Python 3 string is already unicode and every other prefix takes one lowercase spelling.
With unify-numerics = false, the module binds 0XABC, U"y", R"a\d", and 'ada'. 0XABC stays exactly as written while the other facets still run, so U"y" drops its no-op prefix, R"a\d" lowercases to r"a\d", and 'ada' swaps to double quotes, because each facet gates one spelling axis on its own.
With unify-prefixes = false, the module binds 0XABC, U"y", R"a\d", and 'ada'. The U in U"y" and the R in R"a\d" keep the case they were typed in, whereas 0XABC still lowercases its marker to 0xABC and 'ada' still swaps to double quotes, because the quotes facet reads a prefix without rewriting it and turning one facet off leaves the other two running.
With unify-quotes = false, the module binds 0XABC, U"y", R"a\d", and 'ada'. The single-quoted 'ada' stays exactly as written while the other facets still run, so 0XABC lowercases its marker to 0xABC, U"y" drops its no-op prefix, and R"a\d" lowercases to r"a\d", because each facet gates one spelling axis on its own.
shout = 'say "hi"' carries two bare " characters, and mixed = 'it\'s "quoted"' carries one \' beside two bare ". Both keep the quotes they were written with, because a swap would have to escape each " in shout and would trade mixed's one \' for two new \" escapes, so the preference for " yields wherever following it would cost the reader an escape.
Rewrites a docstring's quotes to the triple-double-quote form and puts a multi-line docstring's opener and closer on their own lines.
Reports a module-level constant whose name is not SCREAMING_CASE and suggests the renamed form.
Reports a line still over its line budget once no layout rule can shorten it.
For a single literal that has to keep the spelling it was written with, Suppression covers the # prose: skip[normalize-literals] line directive and the # fmt: off / # fmt: on block markers.