Metadata-Version: 2.4
Name: true-formatter
Version: 0.1.3
Summary: The uncompromising Python formatter.
Author: True Contributors
License: MIT
Keywords: formatter,code style,linter,pep8
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Provides-Extra: dev
Requires-Dist: pytest>=7.4; extra == "dev"
Requires-Dist: pytest-cov>=4.1; extra == "dev"

# True — The Uncompromising Python Formatter

[![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

**True** is an opinionated Python source-code formatter.
It enforces a consistent style so you never have to think about formatting again.

---

## Table of Contents

- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [CLI Reference](#cli-reference)
- [API Reference](#api-reference)
- [PEP 8 Rules Applied](#pep-8-rules-applied)
- [Architecture](#architecture)
- [Contributing](#contributing)

---

## Features

| Category | What True fixes |
|---|---|
| Whitespace | Spaces around operators, after commas, inside brackets |
| Indentation | Any indent style → 4 spaces |
| Blank lines | 2 before top-level def/class, 1 between methods, 2 after last def |
| Imports | Split `import os, sys` into separate lines |
| Strings | Normalise `'hello'` → `"hello"` |
| Semicolons | `x = 0; y = 0` → two lines |
| Compound statements | `if x: pass` → split onto two lines |
| Lambda assignment | `f = lambda x: x` → `def f(x): return x` |
| Class names | `class my_class` → `class MyClass` |
| Variable names | `xValue` → `x_value`, `calcArea` → `calc_area` |
| Default params | `def f(x =1)` → `def f(x=1)` |
| Annotated params | `def f(x: int=1)` → `def f(x: int = 1)` |
| Keyword args | `foo(key =1)` → `foo(key=1)` |
| Power operator | `2 ** 10` → `2**10` |
| Extra spaces | `for   i   in   range` → `for i in range` |
| Trailing whitespace | Stripped from every line |
| Final newline | Exactly one trailing newline |

Zero dependencies — pure stdlib.

---

## Installation

```bash
pip install true-formatter
```

Or from source:

```bash
git clone https://github.com/yourname/true-formatter
cd true-formatter
pip install -e .[dev]
```

---

## Quick Start

### CLI

```bash
# Format a file in-place
true my_script.py

# Check without modifying (for CI)
true --check my_script.py

# Show a unified diff
true --diff my_script.py

# Format an entire directory
true src/

# Read from stdin
echo "x=1" | true -
```

### Library

```python
import true_formatter

code = "x=1\ny=  'hello'\n"
result = true_formatter.format_str(code, mode=true_formatter.Mode())
print(result)
# x = 1
# y = "hello"
```

---

## CLI Reference

### Synopsis

```
true [OPTIONS] [FILES|DIRECTORIES]...
```

Pass `-` as a file path to read from **stdin** and write to **stdout**.

### Options

| Flag | Default | Description |
|---|---|---|
| `--check` | off | Don't write files; exit 1 if any would change |
| `--diff` | off | Print a unified diff instead of rewriting |
| `-l`, `--line-length INT` | `88` | Maximum line length |
| `-S`, `--skip-string-normalization` | off | Leave string quotes as-is |
| `-q`, `--quiet` | off | Suppress all non-error output |

### Exit Codes

| Code | Meaning |
|---|---|
| `0` | All files already formatted (or successfully reformatted) |
| `1` | `--check` mode: at least one file would be reformatted |

### Examples

```bash
# Format a single file
true app.py

# Format all Python files in a directory
true src/

# CI — fail if anything needs reformatting
true --check src/ tests/

# Preview changes before applying
true --diff app.py

# Pipe from stdin
cat messy.py | true - > clean.py

# Use a shorter line length
true -l 79 legacy_module.py

# Preserve existing string quotes
true -S my_file.py
```

### Integration

**pre-commit:**
```yaml
repos:
  - repo: local
    hooks:
      - id: true-formatter
        name: true
        entry: true
        language: python
        types: [python]
```

**Makefile:**
```makefile
fmt:
	true src/ tests/

check:
	true --check src/ tests/
```

**GitHub Actions:**
```yaml
- name: Check formatting
  run: |
    pip install true-formatter
    true --check src/ tests/
```

---

## API Reference

### Top-level Functions

#### `format_str(src_contents, *, mode) → str`

Format a Python source string and return the result.

```python
import true_formatter

result = true_formatter.format_str("x=1", mode=true_formatter.Mode())
# → "x = 1\n"
```

| Parameter | Type | Description |
|---|---|---|
| `src_contents` | `str` | Python source code to format |
| `mode` | `Mode` | Formatting configuration |

Raises `TypeError` if `src_contents` is not a `str`.
Raises `TrueFormattingError` if the source cannot be tokenized.

---

#### `format_file_contents(src_contents, *, mode) → str`

Like `format_str` but additionally guarantees a single trailing newline.
Use this when writing back to a file.

```python
formatted = true_formatter.format_file_contents(src, mode=mode)
Path("my_file.py").write_text(formatted, encoding="utf-8")
```

---

#### `check_format(src_contents, *, mode) → bool`

Return `True` if `src_contents` is already correctly formatted. No files are written.

```python
ok = true_formatter.check_format("x = 1\n", mode=true_formatter.Mode())
# → True

ok = true_formatter.check_format("x=1\n", mode=true_formatter.Mode())
# → False
```

---

### `Mode`

Dataclass that holds all formatting options.

```python
from true_formatter import Mode

mode = Mode(
    line_length=79,
    string_normalization=True,
    skip_string_normalization=False,
    magic_trailing_comma=True,
)
```

| Field | Type | Default | Description |
|---|---|---|---|
| `line_length` | `int` | `88` | Maximum line length |
| `string_normalization` | `bool` | `True` | Rewrite `'…'` as `"…"` |
| `skip_string_normalization` | `bool` | `False` | Disable string normalisation |
| `magic_trailing_comma` | `bool` | `True` | Preserve trailing commas |
| `target_versions` | `set[TargetVersion]` | `set()` | Python versions to target |
| `rules` | `RuleSet` | `DEFAULT_RULES` | Active rule set |

**Properties:**
- `mode.quote_char` — returns `'"'` or `"'"` based on normalisation settings.

---

### `TargetVersion`

Enum of supported Python versions.

```python
from true_formatter import TargetVersion, Mode

mode = Mode(target_versions={TargetVersion.PY311, TargetVersion.PY312})
```

Values: `PY38`, `PY39`, `PY310`, `PY311`, `PY312`

---

### Rules

#### `Rule` (abstract base class)

Extend this to create custom formatting rules that operate on the token stream.

```python
import tokenize
from true_formatter import Rule, Mode

class BanPassRule(Rule):
    name = "ban-pass"

    def apply(self, tokens, mode):
        return [t for t in tokens if t.string != "pass"]
```

| Attribute | Type | Description |
|---|---|---|
| `name` | `str` | Unique identifier shown in `--diff` output |

**Required method:** `apply(tokens, mode) → list[TokenInfo]`

---

#### `RuleSet`

An ordered, immutable-style collection of rules.

```python
from true_formatter import DEFAULT_RULES, Mode
from my_rules import BanPassRule

custom_rules = DEFAULT_RULES.add(BanPassRule())
mode = Mode(rules=custom_rules)
```

| Method | Returns | Description |
|---|---|---|
| `add(rule)` | `RuleSet` | New set with `rule` appended |
| `remove(name)` | `RuleSet` | New set without the named rule |
| `__len__()` | `int` | Number of rules |
| `__iter__()` | iterator | Iterate over rules in order |

---

### Exceptions

```
TrueError
├── TrueFormattingError   ← source cannot be parsed / formatted
└── TrueConfigError       ← invalid Mode configuration
```

```python
from true_formatter.exceptions import TrueFormattingError

try:
    true_formatter.format_str("def (:", mode=true_formatter.Mode())
except TrueFormattingError as e:
    print(f"Bad source: {e}")
```

---

## PEP 8 Rules Applied

| Code | Rule | Example |
|---|---|---|
| E101 | Indentation → 4 spaces | tab/2-space → 4 space |
| E201 | No space after `(` `[` `{` | `foo( x )` → `foo(x)` |
| E202 | No space before `)` `]` `}` | `foo(x )` → `foo(x)` |
| E203 | No space before `,` `;` `:` | `x , y` → `x, y` |
| E211 | No space before `(` in call | `foo (x)` → `foo(x)` |
| E225 | Spaces around operators | `x==1` → `x == 1` |
| E231 | Space after `,` `:` `;` | `[1,2]` → `[1, 2]` |
| E251 | No spaces around `=` in default params | `def f(x =1)` → `def f(x=1)` |
| E252 | Spaces around `=` in annotated params | `def f(x: int=1)` → `def f(x: int = 1)` |
| E261 | Two spaces before inline comment | `x=1 # note` → `x = 1  # note` |
| E262 | Space after `#` in comment | `#note` → `# note` |
| E271 | Single space after keyword | `for   i` → `for i` |
| E302 | Two blank lines before top-level def/class | |
| E301 | One blank line between methods in class | |
| E305 | Two blank lines after last def before code | |
| E401 | One import per line | `import os, sys` → two lines |
| E702 | No semicolons between statements | `x=0; y=0` → two lines |
| E711 | Compound statements split onto separate lines | `if x: pass` → two lines |
| E731 | Lambda assignment → def | `f = lambda x: x` → `def f(x): return x` |
| W291 | No trailing whitespace | |
| W292 | File ends with newline | |
| N801 | Class names in PascalCase | `class my_class` → `class MyClass` |
| N802 | Function/variable names in snake_case | `calcArea` → `calc_area` |

---

## Architecture

### Formatting Pipeline

```
Source string
     │
     ▼
 Tokenizer              (stdlib tokenize)
     │
     ▼
 Rule pipeline          (true_formatter.rules)
     │  each Rule: token list → token list
     ▼
 Emitter                (tokenize.untokenize)
     │
     ▼
 Text-level passes      (true_formatter.transforms)
     │
     ▼
 Formatted string
```

### Text-level Passes (in order)

| Pass | PEP 8 codes |
|---|---|
| `fix_indentation` | E101, W191 |
| `fix_multiple_imports` | E401 |
| `split_semicolons` | E702, E711 |
| `fix_lambda_assignment` | E731 |
| `fix_extra_whitespace` | E271, E272 |
| `fix_class_names` | N801 |
| `fix_variable_names` | N802 |
| `fix_pep8_whitespace` | E201, E202, E203, E211, E225, E231, E251, E252, E261, E262 |
| `fix_operator_priority_spacing` | E226 |
| `normalise_strings` | W605 |
| `fix_trailing_whitespace` | W291, W293 |
| `fix_blank_lines` | E302, E301 |
| `fix_blank_lines_after_def` | E305 |
| `ensure_final_newline` | W292 |

### Module Map

```
true_formatter/
├── __init__.py       Public API — re-exports from submodules
├── core.py           format_str / format_file_contents / check_format
├── transforms.py     Text-level formatting passes
├── rules.py          Rule base class, RuleSet, built-in rules
├── exceptions.py     TrueError hierarchy
└── cli.py            argparse CLI entry point
```

---

## Contributing

### Setup

```bash
git clone https://github.com/yourname/true-formatter
cd true-formatter
pip install -e .[dev]
```

### Running the Tests

```bash
pytest                          # all tests
pytest tests/test_transforms.py # one module
pytest -k "test_single"         # filter by name
pytest --tb=short               # compact tracebacks
```

### Writing a New Rule

Rules live in `true_formatter/rules.py` and operate on the token stream:

```python
import tokenize
from true_formatter.rules import Rule

class NoSemicolonRule(Rule):
    name = "no-semicolons"

    def apply(self, tokens, mode):
        return [t for t in tokens if t.string != ";"]
```

Add it to `DEFAULT_RULES` and write tests in `tests/test_rules.py`.

### Writing a New Text-level Transform

Transforms live in `true_formatter/transforms.py` and operate on strings:

```python
def remove_semicolons(src: str) -> str:
    return src.replace(";", "")
```

Wire it into `_text_level_format` in `core.py` in the correct position in the pipeline.

### Test Guidelines

- One test class per module under test
- Each test method tests exactly one behaviour
- Use `pytest.raises` for exception assertions
- Use `tmp_path` fixture for file I/O tests
- Name tests `test_<thing_being_tested>`

### Code Style

True is formatted with itself:

```bash
true true_formatter/ tests/
```

---

## License

MIT © True Contributors
