Metadata-Version: 2.4
Name: kaapstorm-parsimony
Version: 0.1.4
Summary: A minimalist Python formatting tool
Project-URL: Homepage, https://github.com/kaapstorm/parsimony
Project-URL: Source, https://github.com/kaapstorm/parsimony
Project-URL: Documentation, https://github.com/kaapstorm/parsimony/blob/main/README.md
Project-URL: Changelog, https://github.com/kaapstorm/parsimony/blob/main/CHANGELOG.md
Author-email: Norman Hooper <kaapstorm@gmail.com>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: autoformatter,code-formatter,code-style,formatter,libcst,line-length,linter
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: <3.15,>=3.11
Requires-Dist: libcst
Description-Content-Type: text/markdown

Parsimony
=========

A minimalist line-breaker that adds the fewest breaks to fit the line.

Unlike black/blue/ruff, which explode the outermost bracket first and
stack every closing bracket on its own line, Parsimony makes the
smallest change that fits an over-long line, using three complementary
strategies.

**Explode a bracket** — open the minimum number of brackets and coalesce
adjacent ones:

```python
response = authed_client().get(reverse('forwarding:detail', args=[c.id]))
```

becomes

```python
response = authed_client().get(reverse(
    'forwarding:detail',
    args=[c.id],
))
```

The same applies to the parenthesised containers in `def`/`async def` and
`class` headers — a long parameter list or base-class list is exploded one
element per line:

```python
def some_method(self, param1: str, param2: int, param3: bool = False, param4: float = 0.0):
    ...
```

becomes

```python
def some_method(
    self,
    param1: str,
    param2: int,
    param3: bool = False,
    param4: float = 0.0,
):
    ...
```

**Break a method chain** — split a long `.`-chain one segment per line.
This is done for lines that can't be shortened by exploding a bracket:

```python
queryset = SomeModel.objects.filter(active=True).order_by('-last_modified_at')
```

becomes

```python
queryset = (
    SomeModel
    .objects
    .filter(active=True)
    .order_by('-last_modified_at')
)
```

**Break a condition** — split the boolean operators of an over-long
`if`, `elif` or `while` header one per line:

```python
if some_condition_value and another_condition_value and a_third_condition_value:
    ...
```

becomes

```python
if (
    some_condition_value
    and another_condition_value
    and a_third_condition_value
):
    ...
```

Each keeps related code grouped and gives long expressions a clean
one-operation-per-line shape.


Usage
-----

Format files or directories (recurses for `*.py`):

```shell
parsimony -i src/       # rewrite in place
parsimony --check src/  # print a diff, exit 1 if anything would change
parsimony src/file.py   # print formatted output to stdout
```

Read from stdin and write to stdout:

```shell
parsimony < src/file.py
cat src/file.py | parsimony
```


Installation
------------

```shell
pip install kaapstorm-parsimony
```


Algorithm
---------

While a physical line exceeds `LINE_LENGTH`, explode one container that
intersects an over-long line, chosen by:

1. multi-item containers only (>= 2 args/elements); single-item
   containers are never opened — see "Contract" below.
2. outermost (shallowest) of those.

A "container" is a call, list, tuple, set, dict or subscript, plus the
parameter list of a `def`/`async def` and the base/keyword list of a
`class`. A function's parens are implicit and a class's are optional
(`class Bar:`), but both explode like any other bracket. A trailing comma
after `*args` / `**kwargs` is valid in a `def`, so the exploded form stays
correct.

Then re-measure and repeat. "Explode" = each element on its own line at
a +4 hanging indent, with a trailing comma, and the closing bracket
dedented to the opening line's indent. Because we only open the chosen
container and never its single-item parents, adjacent openers like
`get(reverse(` remain coalesced. Coalescing is not about depth, only
about not opening single-item wrappers.

Exploding a container shifts its children one level deeper, so any breaks
they already carry are re-indented to match — the same adjustment chain
breaking makes.

When no multi-item container intersects a remaining over-long line, and
that line is an `if`, `elif` or `while` header whose condition is a
boolean expression, wrap the condition in parentheses and put a break
before every `and` / `or`. Operands the author already parenthesised stay
on one line — a hand-written group is a grouping decision, not a joint.

When neither applies, fall back to breaking the outermost method chain on
it (>= 2 call segments, as shown above): wrap it in parentheses and put
the head plus each `.attr` on its own +4 line. Bracket explosion is
preferred — a chain is only broken when opening a bracket cannot fix the
line — which keeps breaks minimal. Any brackets already opened inside a
segment are re-indented to stay aligned under their now-deeper segment.


Contract
--------

The tool only ever adds breaks to over-long lines; it never removes or
rewrites existing breaks. This makes it idempotent, safe to re-run, and
safe to combine with hand-formatting. Preserving existing line breaks
is intentional, so it cannot be combined with `ruff format`, which would
re-flow its output back into the staircase. Pair it with ruff-as-linter
(E501 off) instead.

Multi-item-only is also a correctness guardrail: opening a single-item
subscript would turn `x[0]` into `x[0,]` (== `x[(0,)]`), which changes
meaning. Restricting to multi-item containers avoids that. It also
avoids ugly single-element splits.


Limitations
-----------

- Lines long for non-bracket, non-chain, non-condition reasons —
  ternaries, arithmetic chains, pure attribute chains (no calls), long
  string literals — are left untouched and reported, not fixed.
- Boolean expressions outside an `if`/`elif`/`while` condition — in an
  `assert`, a `return`, an assignment — are not broken. Neither is a
  negated condition (`if not (a and b):`), nor a boolean expression
  nested inside a bracket in the header (`if check(a and b and c):`).
- No "join" pass: it will not re-flow code that another tool has already
  split. It only acts on lines that are physically too long.
- Comments inside brackets and pre-existing trailing commas are not
  specially handled.
