docstring-frame
DocsCanonicalizes a docstring's quotes and frames the opener and closer.
A value written three ways reads as three values. 0XABC beside 0xabc, U"y" beside 'y', and 1E5 beside 1e5 each name one thing while asking the reader to reconcile how it was typed before comparing it to anything.
The unify-quotes facet settles a string on ", falling back to ' only where that drops an escape, so 'plain' becomes "plain" while 'say "hi"' keeps the quotes that spare it two backslashes. A quote counts once wherever it appears, escaped or not, so a body spelling \" inside single quotes sheds a backslash it never needed and the delimiter stays put. A raw string never gains or loses a backslash, so it swaps only when every " it holds already carries one, and a triple-quoted string swaps only when no """ run and no trailing " would abut the closer. The facet passes over the docstring slot, whose quotes
""" frame, and over any literal inside a replacement field, whose quotes the enclosing f-string constrains before Python 3.12. The slot is read by position rather than by part count, so an implicitly concatenated leading expression holds its quotes too.Under unify-prefixes every prefix letter goes lowercase and the no-op u goes entirely, 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-numerics then reaches the numeric spelling, uppercasing hex digits while lowercasing the 0x, 0o, and 0b radix markers, the e exponent, and the j suffix, which leaves 0XdeadBEEF as 0xDEADBEEF and 10E+3J as 10e+3j. The digits, the _ separators, and every escape sequence that is not a quote pass through exactly as written.
The rule opens the pipeline, so every length-aware rule downstream measures a literal at the width it will ship at rather than the width it was typed at.
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled | bool | true | Toggles 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 | Settles a non-docstring string on ", falling to ' only where that drops an escape, and sheds a backslash the surviving quote does not need. false keeps the literal spelled as written. |
Each facet gates one spelling axis independently, so a project that has settled its quotes by hand can run unify-quotes = false while the prefix and numeric spellings still normalize. Setting enabled = false turns all three off together.
A single-quoted string whose body holds neither quote character swaps to ", the empty string included. 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 = ""
An implicitly concatenated leading expression fills the docstring slot without being a docstring
can reframe, so both parts keep the quotes they were written with while the return value beside them swaps to".Both docstrings lose the no-op u while neither one's delimiters move, the second staying single-quoted precisely because it sits in docstring position. The prefix facet reaches a docstring and the quote facet does not, leaving that frame to
The docstring keeps the quotes it was written with while the return value beside it swaps to ". The quote facet passes over docstring position entirely, leaving that frame to the rule that owns it.
already \" escaped loses its backslash by moving to ', so the swap runs even though it moves away from the preferred quote. The neighbouring it's fine holds its double quotes, where the same move would trade one escape for another.
The nested u'inner' drops its no-op prefix while keeping the single quotes the enclosing f-string constrains. The prefix and numeric facets reach inside a replacement field and the quote facet does not, since only the quote is version-sensitive.
Both numerics respell inside the field that holds them, the second nested one level deeper inside a format spec. The numeric and prefix facets reach into a replacement field where the quote facet stops, since only the quote is constrained by the string enclosing it.
RF collapses to rf with the letters raw-first, and the delimiters swap because no bare " sits in a literal run. The second line holds, since a raw string can neither gain nor lose a backslash and the " it carries would need one.
A raw string carries every backslash into its value, so the rule rewrites the delimiters and leaves the body byte-for-byte. r'\"exact\"' still swaps, since each " it holds already sits behind a backslash and needs none added.
A body spelling \" inside single quotes needs no backslash there, so the backslash goes while the delimiter stays put. A quote character counts once wherever it appears, escaped or not, which is what keeps the choice stable rather than flipping the delimiter on every run.
The first t-string swaps to " and the second holds against the two escapes it would gain, matching the f-string pair entry for entry. The template prefix rides its own token trio through the walk, so the shape is pinned against the tokens that actually differ rather than by resemblance.
A triple-apostrophe literal outside docstring position moves to the """ frame with its body untouched across every line. The delimiter follows the same preference a one-line string does, so a multi-line block reads the same frame as everything around it.
Both f-strings move to ", the first carrying a format spec through the swap and the second holding no replacement field at all. The opener, the closer, and every literal run between them move as one edit.
The 0x, 0o, and 0b radix markers, the e exponent, and the j suffix all read lowercase while hex digits read uppercase. The marker and the payload take opposite cases, so the digits stand clear of the notation that introduces them.
U disappears because every Python 3 string is already unicode, while R, B, and the combined BR each fall to lowercase and settle raw-first as rb. The delimiters move alongside them in the same edit.
With unify-numerics = false the hex marker stands as written, while the no-op u still drops and the quote still settles on ". Each facet gates one spelling axis on its own.
With unify-prefixes = false both the U and the R keep the case they were typed in, while the hex marker still lowercases. The delimiters still settle, since the quote facet reads the prefix without rewriting it.
With unify-quotes = false the single-quoted name stands as written, while the hex marker still lowercases and the no-op u still drops. Each facet gates one spelling axis on its own.
Swapping either body would add more backslashes than it drops, so both keep the quotes they were written with. The preference for " yields wherever honoring it would cost the reader an escape, which is what keeps say "hi" readable.
Canonicalizes a docstring's quotes and frames the opener and closer.
Surfaces a module-level constant whose name is not SCREAMING_CASE.
Names any line still over its governing cap once no reshape can shorten it.
For a single literal that must keep the spelling it was written with, Suppression covers the # prose: ignore[normalize-literals] line directive and the # fmt: off / # fmt: on block markers.