Metadata-Version: 2.4
Name: docfmt
Version: 0.2.0
Summary: Reliable docstring formatter: AST-located, surgically spliced, self-verifying
Author: mm21
Author-email: mm21 <mm21.dev@gmail.com>
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12, <3.15
Project-URL: homepage, https://github.com/mm21/docfmt
Description-Content-Type: text/markdown

# docfmt

Reliable docstring formatter for Python

[![Python versions](https://img.shields.io/pypi/pyversions/docfmt.svg)](https://pypi.org/project/docfmt)
[![PyPI](https://img.shields.io/pypi/v/docfmt?color=%2334D058&label=pypi%20package)](https://pypi.org/project/docfmt)
[![Tests](./badges/tests.svg?dummy=8484744)]()
[![Coverage](./badges/cov.svg?dummy=8484744)]()
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

- [docfmt](#docfmt)
  - [Motivation](#motivation)
  - [Behavior](#behavior)
    - [MyST directives](#myst-directives)
    - [Attribute docstrings](#attribute-docstrings)
  - [Usage](#usage)
  - [Exit codes](#exit-codes)

## Motivation

Docstring formatters that rebuild the file from a token stream are prone to a
particular class of bug: edits computed against one version of the token list get
applied to another, so behavior depends on how much unrelated code precedes the
docstring being formatted. The symptom is formatting that changes when you add or
remove an unrelated class elsewhere in the file.

docfmt is built so that cannot happen:

- **Docstrings are located by AST structure**, never by token heuristics or by
  matching line contents.
- **The file is never regenerated.** Formatting produces a set of non-overlapping
  character-range replacements against the original source; everything outside a
  docstring is preserved byte-for-byte.
- **Results are verified before they are written.** Every formatted file is
  checked for AST equivalence (nothing changed but docstring contents) and for
  idempotency (`format(format(x)) == format(x)`). A failure leaves the file
  untouched and reports an internal error rather than writing a bad result.

## Behavior

docfmt normalizes docstring *layout* and never mutates author text. Adding a
trailing period is the one exception, and it is opt-in
(`--add-summary-period`).

Blank lines around docstrings are **preserved** by default. Normalization is
opt-in per position under `[tool.docfmt.blank-lines]`.

Structured content is copied verbatim and never rewrapped: fenced code blocks,
MyST directives, doctests, reST directives and field lists, tables, and lists.

### MyST directives

MyST directives nest by widening the fence, so fence *length* is significant:

````
```{note}
Test note
```
````

A block closes only on a fence of the same character with at least as many
markers and no info string, so a narrower inner fence is content:

`````
````{note}
```{warning}
Test warning
```
````
`````

Inline constructs such as `` `False`{l=python} `` are treated as unbreakable
atoms, so wrapping never splits an inline code span from its role attribute.

### Attribute docstrings

A string statement immediately following an assignment at module or class level
is an attribute docstring, including annotation-only attributes:

```python
package: str
"""
Import name of the package.
"""
```

## Usage

```
docfmt --in-place --recursive src test
docfmt --check src        # exit 1 if anything would change
docfmt --diff src
```

Configure via `[tool.docfmt]` in `pyproject.toml`:

```toml
[tool.docfmt]
in-place = true
recursive = true
summary-on-own-line = true
```

| Key | Default | Meaning |
| --- | --- | --- |
| `line-length` | black's, else 88 | wrap width; 0 disables wrapping |
| `summary-on-own-line` | `false` | put the summary below the opening quotes |
| `blank-after-description` | `false` | blank line before the closing quotes |
| `force-reflow` | `false` | refill prose that already fits |
| `add-summary-period` | `false` | append a period to summaries |
| `exclude` | `[]` | path fragments to skip when recursing |
| `in-place` | `false` | write changes instead of printing a diff |
| `check` | `false` | only report files which would change |
| `diff` | `false` | print a unified diff even in in-place or check mode |
| `recursive` | `false` | recurse into directories given as arguments |

An unrecognized key in `[tool.docfmt]` or `[tool.docfmt.blank-lines]` is an
error (exit code 2), so a typo cannot silently do nothing.

Command-line flags win over the config file. `--in-place` and `--check` select
their mode outright, so either overrides a mode set in the config file; setting
both in the config file is an error.

`line-length` is left unset by default and picked up from
`[tool.black] line-length` when black is configured, falling back to 88.

Blank-line rules live in their own table. Each is a count, or `"preserve"` to
leave the author's spacing alone:

```toml
[tool.docfmt.blank-lines]
before-class = "preserve"
after-module = 1
after-class = 1
after-function = "preserve"
after-attribute = 1
```

## Exit codes

| Code | Meaning |
| --- | --- |
| 0 | Nothing to do, or changes written in in-place mode |
| 1 | Check mode: files would change |
| 2 | Error; no file was modified |
