align-equals
AlignmentPads the space before = so consecutive assignments, annotated parameter defaults, and an exploded call's keyword arguments share one column.
stack-method-chains breaks a dotted method chain inside a parenthesis pair and hangs every link beneath the head's own dot, so the chain reads down one aligned column of dots.
Two triggers open the break, one reading the link count and one the width. The count trigger fires on a chain carrying more links than max-links, so a chain that fits the width still breaks once it carries enough stages to read as a pipeline. The width trigger fires where the joined single-line form crosses code-line-length from the column it sits at, which reaches a two-link chain the count cap leaves alone. A link is a .name(...) call, so a long dotted prefix ahead of a single call carries one link and stays where it sits, and a .name access that is not itself called shares the row of the link below it.
The head keeps the receiver together with its first call, because a bare receiver alone on a line carries no information, and each link below hangs at the receiver's own width past the head's indent. Where that width would put the dot column further out than max-shift allows, the chain takes the full split instead, standing the receiver alone and running every link flush beneath it at one indent. The cap is the same max-shift the alignment rules read.
The break only ever opens a chain and never rejoins one, so a chain already hung at its dots keeps that layout even where its joined form would fit, because a count trigger paired with a fit test would alternate forever, the count breaking the chain and the fit test rejoining it on the next pass.
The chain reuses a parenthesis pair the source already carries and settles in the same run that first opens it, leaving each link's argument list to reflow-calls and the collection inside it to reflow-collections. Both the count and the width are read against that settled form, so a hand-wrapped link is measured at the width reflow-calls closes it to. A chain inside a link's argument or inside the receiver is measured from the column the break puts it at and, where it trips a trigger there, breaks in the same text, placed at the indent of the row it ends up on.
A chain spanning a comment keeps its source layout, because breaking it would move the links away from the row the comment describes. A link whose break never closes keeps the chain as written, which covers an argument list already written one argument per line, one past max-args, and a multi-line string.
A chain inside an f-string or t-string replacement field is left as written whatever its width, because a line break inside one is PEP 701 syntax that fails to parse before Python 3.12, so an over-wide interpolation is left for line-overflow to report.
| Key | Type | Default | Meaning |
|---|---|---|---|
enabled | bool | true | Turns the rule on or off. |
max-links | positive int | false | 2 | Breaks a method chain to one link per line once its link count exceeds the cap. false turns the count trigger off and leaves only the code-line-length budget. |
max-shift | positive int | 0 | false | 16 | How far past the broken chain's indent a hanging link's dot column may sit. A receiver wider than that takes the full split instead, standing alone with every link flush beneath it. 0 always takes the full split, and false lifts the cap so every chain hangs. |
A three-link chain breaks at the default cap of 2 even where it fits the line. Setting max-links = false leaves code-line-length as the only trigger, and setting max-shift = 0 takes the full split for every chain.
query.filter(a).order(b).limit(c) fits on one line, yet its three calls exceed max-links. The chain breaks anyway, with query.filter(a) on the head row and .order(b) and .limit(c) hanging beneath it, each . under the head's own, so a pipeline of that length reads down one column of dots.
result = (
query.filter(a)
.order(b)
.limit(c)
)
The assignment target base.one().two().three().attr and the del target other.first().second().third().slot are both chains. Each breaks like any other chain, wrapped in parentheses with the calls stacked and the trailing .attr or .slot kept on its last call's row.
Python accepts a parenthesized assignment target and the output compiles, so this layout is pinned as deliberate. Leaving a store target alone would take a target flag threaded through every nested expression the rule visits.
query.filter(alpha).order(beta).limit(gamma) is passed as the value= keyword of emit. The chain breaks inside a new parenthesis pair, with query.filter(alpha) one indent step past the nested = row and .order and .limit stacked beneath its dot, and the closing )) returns to that row's own indent rather than to the column the chain opened at, so the block hangs from the statement rather than from emit's argument list.
The subscript [index] applies to the result of the whole frame.select(cols).where(cond).rows() chain rather than to any one link. The added parentheses wrap only the chain, and [index] sits past the closing ), so the stacked calls read first and the subscript reads as the one operation applied after them.
In frame.loc[key].mean().round(digits).item(), the subscript [key] comes before the first call, so it belongs to the receiver rather than counting as a link. The head row is frame.loc[key].mean(), and .round(digits) and .item() hang beneath the . that follows the subscript.
Hanging each . beneath the dot after an_extremely_long_receiver_name would put the dot column further out than max-shift allows. The chain takes the full split instead, so the receiver stands alone on its row and .filter(alpha), .order(beta), and .limit(gamma) are written flush beneath it at one indent.
obj.first(alpha_value_here, beta_value_here).second(gamma_value_here, delta_value_here) carries only two links, within max-links, yet its single-line form crosses code-line-length from the column it starts at. Width alone breaks it, so obj.first(alpha_value_here, beta_value_here) keeps the head row and .second(gamma_value_here, delta_value_here) hangs beneath the dot after obj.
obj.first(a).second(b) is written across two rows and would fit joined on one line. The rule leaves it broken, because it only ever breaks a chain. Rejoining on a fit test while breaking on a link count would flip the same chain back and forth on every run, so a chain already broken across lines keeps that layout.
The % expression is already written with its operands on two rows, and os.urandom(8).hex() sits inside the parenthesized right operand. Nothing changes, because the two-link chain sits at max-links and fits its row, measured against the settled layout of the pair around it.
The stack-method-chains dependency on reflow-parentheses declares that order, so a pair that does split at a narrower width settles before the chain inside it is measured.
query.filter(a).order( # keep this layout carries a comment mid-chain on the .order( row. The chain keeps the layout the source gave it, because breaking it would move every link and carry the comment away from the row it describes.
The .lookup link carries the hand-wrapped compute(alpha, argument list, and reflow-calls = false turns off the rule that would close it. The chain stays as written, because a chain is measured with each link at the width its argument lists settle to, and no argument list closes while reflow-calls is off, so the .lookup link still spans lines when the chain is read.
obj.first(alpha).second(beta) carries two links, at max-links, and its single-line form fits the width budget. The line stays as written, because the rule breaks chains long enough to read as pipelines rather than every attribute call.
base.first(alpha, is hand-wrapped mid-call, with beta).second(gamma).third(delta) on the next line. reflow-calls joins first(alpha, beta) onto one row, which settles the chain's width before stack-method-chains counts its links, and the link count breaks the chain, wrapping the expression in parentheses and stacking .second(gamma) and .third(delta) under .first(alpha, beta).
process_env_var(tokenizer.read().text.replace(".", "_")) passes a method chain as its one positional. stack-method-chains wraps the chain in parentheses and stacks .text.replace(...) under tokenizer.read(), and reflow-calls then writes the argument in keyword form as env_var = .... The parentheses stay around the value, because the value spans rows, and the stacked chain keeps its layout through the = alignment instead of being re-wrapped.
f returns an outer chain whose first link takes an inner chain as its argument, on a row past the 60-column budget. stack-method-chains wraps the outer chain in parentheses, and the row holding o.first_link_that_is_quite_long( still crosses the budget, so reflow-calls explodes that argument list. The inner chain stays joined on one line, because it is measured from the column it ends up at, one indent step past that row, rather than from the wider column the joined row would have given it, and it fits there.
out.write(...) takes a chain of two .replace calls on repr(value), already stacked one link per line. stack-method-chains wraps the chain in parentheses and puts the receiver and its first link together on the row below the opener, and that row, holding repr(value).replace(, crosses the 88-column budget. reflow-calls explodes that link against the row it sits on rather than against the pair's opener, its two string arguments one indent step in from that row and its ) back at that row's indent, while the second .replace keeps its place in the . column below.
Pads the space before = so consecutive assignments, annotated parameter defaults, and an exploded call's keyword arguments share one column.
Explodes a call to one keyword argument per line once its argument count passes max-args, its width passes code-line-length, or an argument spans rows, and rejoins a list broken anywhere else onto one row.
Expands a list, tuple, dict, or set literal across lines once it overflows code-line-length or a dict passes max-dict-entries, and rejoins a construct broken anywhere but an entry boundary.
Reports a line still over its line budget once no layout rule can shorten it.