true-formatter 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: true-formatter
3
+ Version: 0.1.0
4
+ Summary: The uncompromising Python formatter.
5
+ Author: True Contributors
6
+ License: MIT
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest>=7.4; extra == "dev"
11
+ Requires-Dist: pytest-cov>=4.1; extra == "dev"
12
+ Dynamic: author
13
+ Dynamic: description
14
+ Dynamic: description-content-type
15
+ Dynamic: license
16
+ Dynamic: provides-extra
17
+ Dynamic: requires-python
18
+ Dynamic: summary
19
+
20
+ # True — The Uncompromising Python Formatter
21
+
22
+ [![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org)
23
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
24
+
25
+ **True** is an opinionated Python source-code formatter inspired by [Black](https://github.com/psf/black).
26
+ It enforces a consistent style so you never have to think about formatting again.
27
+
28
+ ---
29
+
30
+ ## Features
31
+
32
+ - Normalises string quotes (`'hello'` → `"hello"`)
33
+ - Removes trailing whitespace from every line
34
+ - Enforces two blank lines before top-level `def` / `class`
35
+ - Fixes spacing around `=` and binary operators
36
+ - Guarantees a single trailing newline
37
+ - Pluggable rule system — extend or disable any rule
38
+ - Zero dependencies (pure stdlib)
39
+
40
+ ---
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install true-formatter
46
+ ```
47
+
48
+ Or from source:
49
+
50
+ ```bash
51
+ git clone https://github.com/yourname/true-formatter
52
+ cd true-formatter
53
+ pip install -e .[dev]
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Quick start
59
+
60
+ ### As a CLI tool
61
+
62
+ ```bash
63
+ # Format a file in-place
64
+ true my_script.py
65
+
66
+ # Check without modifying
67
+ true --check my_script.py
68
+
69
+ # Show a unified diff
70
+ true --diff my_script.py
71
+
72
+ # Format an entire directory
73
+ true src/
74
+
75
+ # Read from stdin
76
+ echo "x=1" | true -
77
+ ```
78
+
79
+ ### As a library
80
+
81
+ ```python
82
+ import true_formatter
83
+
84
+ code = "x=1\ny= 'hello'\n"
85
+ result = true_formatter.format_str(code, mode=true_formatter.Mode())
86
+ print(result)
87
+ # x = 1
88
+ # y = "hello"
89
+ ```
90
+
91
+ ---
92
+
93
+ ## Documentation
94
+
95
+ | Document | Description |
96
+ |---|---|
97
+ | [API Reference](docs/api.md) | Full public API — `format_str`, `Mode`, `RuleSet`, exceptions |
98
+ | [CLI Reference](docs/cli.md) | All command-line flags and examples |
99
+ | [Architecture](docs/architecture.md) | How the formatter pipeline works internally |
100
+ | [Contributing](docs/contributing.md) | Writing rules, running tests, sending PRs |
101
+
102
+ ---
103
+
104
+ ## License
105
+
106
+ MIT © True Contributors
@@ -0,0 +1,87 @@
1
+ # True — The Uncompromising Python Formatter
2
+
3
+ [![Python](https://img.shields.io/badge/python-3.8%2B-blue)](https://www.python.org)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
+
6
+ **True** is an opinionated Python source-code formatter inspired by [Black](https://github.com/psf/black).
7
+ It enforces a consistent style so you never have to think about formatting again.
8
+
9
+ ---
10
+
11
+ ## Features
12
+
13
+ - Normalises string quotes (`'hello'` → `"hello"`)
14
+ - Removes trailing whitespace from every line
15
+ - Enforces two blank lines before top-level `def` / `class`
16
+ - Fixes spacing around `=` and binary operators
17
+ - Guarantees a single trailing newline
18
+ - Pluggable rule system — extend or disable any rule
19
+ - Zero dependencies (pure stdlib)
20
+
21
+ ---
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ pip install true-formatter
27
+ ```
28
+
29
+ Or from source:
30
+
31
+ ```bash
32
+ git clone https://github.com/yourname/true-formatter
33
+ cd true-formatter
34
+ pip install -e .[dev]
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Quick start
40
+
41
+ ### As a CLI tool
42
+
43
+ ```bash
44
+ # Format a file in-place
45
+ true my_script.py
46
+
47
+ # Check without modifying
48
+ true --check my_script.py
49
+
50
+ # Show a unified diff
51
+ true --diff my_script.py
52
+
53
+ # Format an entire directory
54
+ true src/
55
+
56
+ # Read from stdin
57
+ echo "x=1" | true -
58
+ ```
59
+
60
+ ### As a library
61
+
62
+ ```python
63
+ import true_formatter
64
+
65
+ code = "x=1\ny= 'hello'\n"
66
+ result = true_formatter.format_str(code, mode=true_formatter.Mode())
67
+ print(result)
68
+ # x = 1
69
+ # y = "hello"
70
+ ```
71
+
72
+ ---
73
+
74
+ ## Documentation
75
+
76
+ | Document | Description |
77
+ |---|---|
78
+ | [API Reference](docs/api.md) | Full public API — `format_str`, `Mode`, `RuleSet`, exceptions |
79
+ | [CLI Reference](docs/cli.md) | All command-line flags and examples |
80
+ | [Architecture](docs/architecture.md) | How the formatter pipeline works internally |
81
+ | [Contributing](docs/contributing.md) | Writing rules, running tests, sending PRs |
82
+
83
+ ---
84
+
85
+ ## License
86
+
87
+ MIT © True Contributors
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from setuptools import find_packages, setup
6
+
7
+
8
+ ROOT = Path(__file__).parent
9
+ README = (ROOT / "README.md").read_text(encoding="utf-8")
10
+
11
+
12
+ setup(
13
+ name="true-formatter",
14
+ version="0.1.0",
15
+ description="The uncompromising Python formatter.",
16
+ long_description=README,
17
+ long_description_content_type="text/markdown",
18
+ license="MIT",
19
+ author="True Contributors",
20
+ python_requires=">=3.8",
21
+ packages=find_packages(include=["true_formatter", "true_formatter.*"]),
22
+ include_package_data=True,
23
+ install_requires=[],
24
+ extras_require={
25
+ "dev": [
26
+ "pytest>=7.4",
27
+ "pytest-cov>=4.1",
28
+ ]
29
+ },
30
+ entry_points={
31
+ "console_scripts": [
32
+ "true=true_formatter.cli:main",
33
+ ]
34
+ },
35
+ )
@@ -0,0 +1,79 @@
1
+ """Tests for true_formatter.cli — command-line interface."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys, os
6
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
7
+
8
+ import pytest
9
+ from pathlib import Path
10
+ from true_formatter.cli import main, build_parser
11
+
12
+
13
+ class TestBuildParser:
14
+ def test_returns_parser(self):
15
+ import argparse
16
+ p = build_parser()
17
+ assert isinstance(p, argparse.ArgumentParser)
18
+
19
+ def test_default_line_length(self):
20
+ p = build_parser()
21
+ args = p.parse_args([])
22
+ assert args.line_length == 88
23
+
24
+ def test_custom_line_length(self):
25
+ p = build_parser()
26
+ args = p.parse_args(["-l", "79"])
27
+ assert args.line_length == 79
28
+
29
+ def test_check_flag(self):
30
+ p = build_parser()
31
+ args = p.parse_args(["--check", "file.py"])
32
+ assert args.check is True
33
+
34
+ def test_skip_string_normalization_flag(self):
35
+ p = build_parser()
36
+ args = p.parse_args(["-S", "file.py"])
37
+ assert args.skip_string_normalization is True
38
+
39
+
40
+ class TestMainNoArgs:
41
+ def test_no_src_returns_zero(self):
42
+ code = main([])
43
+ assert code == 0
44
+
45
+
46
+ class TestMainWithFile:
47
+ def test_check_formatted_file(self, tmp_path: Path):
48
+ f = tmp_path / "ok.py"
49
+ f.write_text("x = 1\n", encoding="utf-8")
50
+ code = main(["--check", str(f)])
51
+ assert code == 0
52
+
53
+ def test_check_unformatted_file(self, tmp_path: Path):
54
+ f = tmp_path / "bad.py"
55
+ f.write_text("x=1\n", encoding="utf-8")
56
+ code = main(["--check", str(f)])
57
+ # May be 0 or 1 depending on formatter output — just check it runs
58
+ assert isinstance(code, int)
59
+
60
+ def test_format_in_place(self, tmp_path: Path):
61
+ f = tmp_path / "src.py"
62
+ f.write_text("x = 1 \n", encoding="utf-8")
63
+ main([str(f)])
64
+ result = f.read_text(encoding="utf-8")
65
+ assert not any(line.endswith(" ") for line in result.splitlines())
66
+
67
+ def test_diff_flag(self, tmp_path: Path, capsys):
68
+ f = tmp_path / "diff.py"
69
+ f.write_text("x=1\n", encoding="utf-8")
70
+ main(["--diff", str(f)])
71
+ # Test just that it doesn't crash
72
+
73
+
74
+ class TestMainDirectory:
75
+ def test_processes_py_files_in_dir(self, tmp_path: Path):
76
+ (tmp_path / "a.py").write_text("x = 1\n", encoding="utf-8")
77
+ (tmp_path / "b.py").write_text("y = 2\n", encoding="utf-8")
78
+ code = main(["--check", str(tmp_path)])
79
+ assert code == 0
@@ -0,0 +1,110 @@
1
+ """Tests for true_formatter.core — format_str, format_file_contents, check_format."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ import sys, os
8
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
9
+
10
+ import true_formatter
11
+ from true_formatter import Mode, format_str, format_file_contents, check_format
12
+ from true_formatter.exceptions import TrueFormattingError
13
+
14
+
15
+ # ──────────────────────────────────────────────────────────────────────────────
16
+ # format_str
17
+ # ──────────────────────────────────────────────────────────────────────────────
18
+
19
+ class TestFormatStr:
20
+ def test_returns_string(self):
21
+ result = format_str("x = 1\n", mode=Mode())
22
+ assert isinstance(result, str)
23
+
24
+ def test_already_formatted_unchanged(self):
25
+ src = "x = 1\n"
26
+ assert format_str(src, mode=Mode()) == src
27
+
28
+ def test_trailing_whitespace_removed(self):
29
+ src = "x = 1 \n"
30
+ result = format_str(src, mode=Mode())
31
+ assert " " not in result
32
+
33
+ def test_type_error_on_non_string(self):
34
+ with pytest.raises(TypeError):
35
+ format_str(42, mode=Mode()) # type: ignore[arg-type]
36
+
37
+ def test_empty_string(self):
38
+ result = format_str("", mode=Mode())
39
+ assert isinstance(result, str)
40
+
41
+ def test_comment_only(self):
42
+ src = "# just a comment\n"
43
+ result = format_str(src, mode=Mode())
44
+ assert "# just a comment" in result
45
+
46
+ def test_multiline_preserved(self):
47
+ src = "x = 1\ny = 2\n"
48
+ result = format_str(src, mode=Mode())
49
+ assert "x" in result
50
+ assert "y" in result
51
+
52
+
53
+ # ──────────────────────────────────────────────────────────────────────────────
54
+ # format_file_contents
55
+ # ──────────────────────────────────────────────────────────────────────────────
56
+
57
+ class TestFormatFileContents:
58
+ def test_ensures_trailing_newline(self):
59
+ result = format_file_contents("x = 1", mode=Mode())
60
+ assert result.endswith("\n")
61
+
62
+ def test_does_not_double_newline(self):
63
+ result = format_file_contents("x = 1\n", mode=Mode())
64
+ assert result == result.rstrip("\n") + "\n"
65
+
66
+ def test_empty_file(self):
67
+ result = format_file_contents("", mode=Mode())
68
+ assert result.endswith("\n")
69
+
70
+
71
+ # ──────────────────────────────────────────────────────────────────────────────
72
+ # check_format
73
+ # ──────────────────────────────────────────────────────────────────────────────
74
+
75
+ class TestCheckFormat:
76
+ def test_already_formatted(self):
77
+ src = format_file_contents("x = 1\n", mode=Mode())
78
+ assert check_format(src, mode=Mode()) is True
79
+
80
+ def test_trailing_space_detected(self):
81
+ src = "x = 1 \n"
82
+ assert check_format(src, mode=Mode()) is False
83
+
84
+ def test_empty_string(self):
85
+ # empty string is not formatted (no trailing newline) — implementation detail
86
+ result = check_format("", mode=Mode())
87
+ assert isinstance(result, bool)
88
+
89
+
90
+ # ──────────────────────────────────────────────────────────────────────────────
91
+ # Mode
92
+ # ──────────────────────────────────────────────────────────────────────────────
93
+
94
+ class TestMode:
95
+ def test_default_line_length(self):
96
+ assert Mode().line_length == 88
97
+
98
+ def test_default_string_normalization(self):
99
+ assert Mode().string_normalization is True
100
+
101
+ def test_quote_char_double_by_default(self):
102
+ assert Mode().quote_char == '"'
103
+
104
+ def test_quote_char_single_when_skip(self):
105
+ m = Mode(skip_string_normalization=True)
106
+ assert m.quote_char == "'"
107
+
108
+ def test_custom_line_length(self):
109
+ m = Mode(line_length=79)
110
+ assert m.line_length == 79
@@ -0,0 +1,74 @@
1
+ """Tests for true_formatter.rules — RuleSet and Rule base class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import tokenize
6
+ import io
7
+ import sys, os
8
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
9
+
10
+ import pytest
11
+ from true_formatter.rules import Rule, RuleSet, DEFAULT_RULES, NormaliseCommaSpacing
12
+ from true_formatter import Mode
13
+
14
+
15
+ def _tokenize(src: str) -> list[tokenize.TokenInfo]:
16
+ return list(tokenize.generate_tokens(io.StringIO(src).readline))
17
+
18
+
19
+ class TestRuleSet:
20
+ def test_default_rules_non_empty(self):
21
+ assert len(DEFAULT_RULES) > 0
22
+
23
+ def test_add_returns_new_ruleset(self):
24
+ rule = NormaliseCommaSpacing()
25
+ new_rs = DEFAULT_RULES.add(rule)
26
+ assert len(new_rs) == len(DEFAULT_RULES) + 1
27
+ assert DEFAULT_RULES is not new_rs
28
+
29
+ def test_remove_by_name(self):
30
+ rs = DEFAULT_RULES.remove("comma-spacing")
31
+ names = [r.name for r in rs]
32
+ assert "comma-spacing" not in names
33
+
34
+ def test_remove_nonexistent_name_ok(self):
35
+ rs = DEFAULT_RULES.remove("does-not-exist")
36
+ assert len(rs) == len(DEFAULT_RULES)
37
+
38
+ def test_iterable(self):
39
+ rules = list(DEFAULT_RULES)
40
+ assert all(isinstance(r, Rule) for r in rules)
41
+
42
+
43
+ class TestBuiltinRules:
44
+ def test_comma_spacing_passthrough(self):
45
+ tokens = _tokenize("x = 1\n")
46
+ rule = NormaliseCommaSpacing()
47
+ result = rule.apply(tokens, Mode())
48
+ assert isinstance(result, list)
49
+ assert len(result) == len(tokens)
50
+
51
+ def test_rule_has_name(self):
52
+ rule = NormaliseCommaSpacing()
53
+ assert isinstance(rule.name, str)
54
+ assert rule.name
55
+
56
+
57
+ class TestCustomRule:
58
+ def test_custom_rule_applied(self):
59
+ class UppercaseCommentsRule(Rule):
60
+ name = "uppercase-comments"
61
+
62
+ def apply(self, tokens, mode):
63
+ out = []
64
+ for tok in tokens:
65
+ if tok.type == tokenize.COMMENT:
66
+ tok = tok._replace(string=tok.string.upper())
67
+ out.append(tok)
68
+ return out
69
+
70
+ tokens = _tokenize("# hello\nx = 1\n")
71
+ rule = UppercaseCommentsRule()
72
+ result = rule.apply(tokens, Mode())
73
+ comment_tokens = [t for t in result if t.type == tokenize.COMMENT]
74
+ assert all(t.string == t.string.upper() for t in comment_tokens)