plain.code 0.17.0__tar.gz → 0.18.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.
- plain_code-0.18.0/PKG-INFO +172 -0
- {plain_code-0.17.0 → plain_code-0.18.0}/plain/code/CHANGELOG.md +13 -0
- plain_code-0.18.0/plain/code/README.md +157 -0
- plain_code-0.18.0/plain/code/annotations.py +345 -0
- {plain_code-0.17.0 → plain_code-0.18.0}/plain/code/cli.py +104 -0
- plain_code-0.18.0/plain/code/skills/plain-fix/SKILL.md +25 -0
- {plain_code-0.17.0 → plain_code-0.18.0}/pyproject.toml +1 -1
- plain_code-0.17.0/PKG-INFO +0 -93
- plain_code-0.17.0/plain/code/README.md +0 -78
- plain_code-0.17.0/plain/code/skills/plain-check/SKILL.md +0 -35
- {plain_code-0.17.0 → plain_code-0.18.0}/.gitignore +0 -0
- {plain_code-0.17.0 → plain_code-0.18.0}/LICENSE +0 -0
- {plain_code-0.17.0 → plain_code-0.18.0}/README.md +0 -0
- {plain_code-0.17.0 → plain_code-0.18.0}/plain/code/__init__.py +0 -0
- {plain_code-0.17.0 → plain_code-0.18.0}/plain/code/biome.py +0 -0
- {plain_code-0.17.0 → plain_code-0.18.0}/plain/code/biome_defaults.json +0 -0
- {plain_code-0.17.0 → plain_code-0.18.0}/plain/code/entrypoints.py +0 -0
- {plain_code-0.17.0 → plain_code-0.18.0}/plain/code/ruff_defaults.toml +0 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plain.code
|
|
3
|
+
Version: 0.18.0
|
|
4
|
+
Summary: Preconfigured code formatting and linting.
|
|
5
|
+
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
|
+
License-Expression: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Requires-Dist: plain<1.0.0
|
|
10
|
+
Requires-Dist: requests>=2.0.0
|
|
11
|
+
Requires-Dist: ruff>=0.1.0
|
|
12
|
+
Requires-Dist: tomlkit>=0.11.0
|
|
13
|
+
Requires-Dist: ty>=0.0.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# plain.code
|
|
17
|
+
|
|
18
|
+
**Preconfigured code formatting and linting.**
|
|
19
|
+
|
|
20
|
+
- [Overview](#overview)
|
|
21
|
+
- [Commands](#commands)
|
|
22
|
+
- [`plain fix`](#plain-fix)
|
|
23
|
+
- [`plain code check`](#plain-code-check)
|
|
24
|
+
- [`plain code annotations`](#plain-code-annotations)
|
|
25
|
+
- [Configuration](#configuration)
|
|
26
|
+
- [FAQs](#faqs)
|
|
27
|
+
- [Installation](#installation)
|
|
28
|
+
|
|
29
|
+
## Overview
|
|
30
|
+
|
|
31
|
+
Plain.code provides comprehensive code quality tools with sensible defaults:
|
|
32
|
+
|
|
33
|
+
- **[Ruff](https://astral.sh/ruff)** - Python linting and formatting
|
|
34
|
+
- **[ty](https://astral.sh/ty)** - Python type checking
|
|
35
|
+
- **[Biome](https://biomejs.dev/)** - JavaScript, JSON, and CSS formatting
|
|
36
|
+
|
|
37
|
+
Ruff and ty are installed as Python dependencies. Biome is managed automatically as a standalone binary (npm is not required).
|
|
38
|
+
|
|
39
|
+
## Commands
|
|
40
|
+
|
|
41
|
+
### `plain fix`
|
|
42
|
+
|
|
43
|
+
The most used command is [`plain fix`](./cli.py#fix), which automatically fixes linting issues and formats your code:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
plain fix
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+

|
|
50
|
+
|
|
51
|
+
You can also apply unsafe fixes or add noqa comments to suppress errors:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Apply Ruff's unsafe fixes
|
|
55
|
+
plain fix --unsafe-fixes
|
|
56
|
+
|
|
57
|
+
# Add noqa comments instead of fixing
|
|
58
|
+
plain fix --add-noqa
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### `plain code check`
|
|
62
|
+
|
|
63
|
+
To check your code without making changes (including type checking):
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
plain code check
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
You can skip specific tools if needed:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
# Skip type checking during rapid development
|
|
73
|
+
plain code check --skip-ty
|
|
74
|
+
|
|
75
|
+
# Only run type checks
|
|
76
|
+
plain code check --skip-ruff --skip-biome
|
|
77
|
+
|
|
78
|
+
# Skip Biome checks
|
|
79
|
+
plain code check --skip-biome
|
|
80
|
+
|
|
81
|
+
# Skip annotation coverage checks
|
|
82
|
+
plain code check --skip-annotations
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
If [`plain.dev`](/plain-dev/README.md) is installed, `plain code check` will be run automatically as a part of `plain pre-commit` to help catch issues before they are committed.
|
|
86
|
+
|
|
87
|
+
### `plain code annotations`
|
|
88
|
+
|
|
89
|
+
Check the type annotation coverage of your codebase:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
plain code annotations
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
This outputs a summary like `85.2% typed (23/27 functions)`.
|
|
96
|
+
|
|
97
|
+
To see which functions are missing annotations:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
plain code annotations --details
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
You can also output the results as JSON for use in CI or other tools:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
plain code annotations --json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Configuration
|
|
110
|
+
|
|
111
|
+
Default configuration is provided by [`ruff_defaults.toml`](./ruff_defaults.toml) and [`biome_defaults.json`](./biome_defaults.json).
|
|
112
|
+
|
|
113
|
+
You can customize the behavior in your `pyproject.toml`:
|
|
114
|
+
|
|
115
|
+
```toml
|
|
116
|
+
[tool.plain.code]
|
|
117
|
+
exclude = ["path/to/exclude"]
|
|
118
|
+
|
|
119
|
+
[tool.plain.code.ty]
|
|
120
|
+
enabled = true # Set to false to disable ty
|
|
121
|
+
|
|
122
|
+
[tool.plain.code.biome]
|
|
123
|
+
enabled = true # Set to false to disable Biome
|
|
124
|
+
version = "1.5.3" # Pin to a specific version
|
|
125
|
+
|
|
126
|
+
[tool.plain.code.annotations]
|
|
127
|
+
enabled = true # Set to false to disable annotation checks
|
|
128
|
+
exclude = ["migrations"] # Exclude specific patterns
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
For more advanced configuration options, see [`get_code_config`](./cli.py#get_code_config).
|
|
132
|
+
|
|
133
|
+
Generally you won't need to change the configuration. The defaults are designed to "just work" for most projects. If you find yourself needing extensive customization, consider using the underlying tools (Ruff, ty, Biome) directly instead.
|
|
134
|
+
|
|
135
|
+
## FAQs
|
|
136
|
+
|
|
137
|
+
#### How do I install or update Biome manually?
|
|
138
|
+
|
|
139
|
+
Biome is installed automatically when you run `plain fix` or `plain code check`. If you need to manage it manually:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# Install Biome (or reinstall if corrupted)
|
|
143
|
+
plain code install
|
|
144
|
+
|
|
145
|
+
# Force reinstall even if up to date
|
|
146
|
+
plain code install --force
|
|
147
|
+
|
|
148
|
+
# Update to the latest version
|
|
149
|
+
plain code update
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
#### Why are test files excluded from annotation coverage?
|
|
153
|
+
|
|
154
|
+
Test files (`test_*.py`, `*_test.py`, and files in `tests/` or `test/` directories) are excluded by default because they typically contain many small helper functions where type annotations add noise without providing significant value. You can customize this behavior via the `exclude` option in the annotations configuration.
|
|
155
|
+
|
|
156
|
+
#### How do I check a specific directory?
|
|
157
|
+
|
|
158
|
+
All commands accept a path argument:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
plain fix path/to/directory
|
|
162
|
+
plain code check path/to/directory
|
|
163
|
+
plain code annotations path/to/directory
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Installation
|
|
167
|
+
|
|
168
|
+
Install the `plain.code` package from [PyPI](https://pypi.org/project/plain.code/):
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
uv add plain.code
|
|
172
|
+
```
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# plain-code changelog
|
|
2
2
|
|
|
3
|
+
## [0.18.0](https://github.com/dropseed/plain/releases/plain-code@0.18.0) (2026-01-13)
|
|
4
|
+
|
|
5
|
+
### What's changed
|
|
6
|
+
|
|
7
|
+
- Added `plain code annotations` command for checking type annotation coverage in Python files ([df353b8](https://github.com/dropseed/plain/commit/df353b8))
|
|
8
|
+
- Use `--details` to list untyped functions
|
|
9
|
+
- Use `--json` for machine-readable output
|
|
10
|
+
- Renamed the `plain-check` skill to `plain-lint`, then to `plain-fix` to better match the primary command ([d51294a](https://github.com/dropseed/plain/commit/d51294a), [519c5af](https://github.com/dropseed/plain/commit/519c5af))
|
|
11
|
+
|
|
12
|
+
### Upgrade instructions
|
|
13
|
+
|
|
14
|
+
- If you were using the `plain-check` or `plain-lint` AI skill, it has been renamed to `plain-fix`
|
|
15
|
+
|
|
3
16
|
## [0.17.0](https://github.com/dropseed/plain/releases/plain-code@0.17.0) (2026-01-13)
|
|
4
17
|
|
|
5
18
|
### What's changed
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# plain.code
|
|
2
|
+
|
|
3
|
+
**Preconfigured code formatting and linting.**
|
|
4
|
+
|
|
5
|
+
- [Overview](#overview)
|
|
6
|
+
- [Commands](#commands)
|
|
7
|
+
- [`plain fix`](#plain-fix)
|
|
8
|
+
- [`plain code check`](#plain-code-check)
|
|
9
|
+
- [`plain code annotations`](#plain-code-annotations)
|
|
10
|
+
- [Configuration](#configuration)
|
|
11
|
+
- [FAQs](#faqs)
|
|
12
|
+
- [Installation](#installation)
|
|
13
|
+
|
|
14
|
+
## Overview
|
|
15
|
+
|
|
16
|
+
Plain.code provides comprehensive code quality tools with sensible defaults:
|
|
17
|
+
|
|
18
|
+
- **[Ruff](https://astral.sh/ruff)** - Python linting and formatting
|
|
19
|
+
- **[ty](https://astral.sh/ty)** - Python type checking
|
|
20
|
+
- **[Biome](https://biomejs.dev/)** - JavaScript, JSON, and CSS formatting
|
|
21
|
+
|
|
22
|
+
Ruff and ty are installed as Python dependencies. Biome is managed automatically as a standalone binary (npm is not required).
|
|
23
|
+
|
|
24
|
+
## Commands
|
|
25
|
+
|
|
26
|
+
### `plain fix`
|
|
27
|
+
|
|
28
|
+
The most used command is [`plain fix`](./cli.py#fix), which automatically fixes linting issues and formats your code:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
plain fix
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+

|
|
35
|
+
|
|
36
|
+
You can also apply unsafe fixes or add noqa comments to suppress errors:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Apply Ruff's unsafe fixes
|
|
40
|
+
plain fix --unsafe-fixes
|
|
41
|
+
|
|
42
|
+
# Add noqa comments instead of fixing
|
|
43
|
+
plain fix --add-noqa
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### `plain code check`
|
|
47
|
+
|
|
48
|
+
To check your code without making changes (including type checking):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
plain code check
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
You can skip specific tools if needed:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Skip type checking during rapid development
|
|
58
|
+
plain code check --skip-ty
|
|
59
|
+
|
|
60
|
+
# Only run type checks
|
|
61
|
+
plain code check --skip-ruff --skip-biome
|
|
62
|
+
|
|
63
|
+
# Skip Biome checks
|
|
64
|
+
plain code check --skip-biome
|
|
65
|
+
|
|
66
|
+
# Skip annotation coverage checks
|
|
67
|
+
plain code check --skip-annotations
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If [`plain.dev`](/plain-dev/README.md) is installed, `plain code check` will be run automatically as a part of `plain pre-commit` to help catch issues before they are committed.
|
|
71
|
+
|
|
72
|
+
### `plain code annotations`
|
|
73
|
+
|
|
74
|
+
Check the type annotation coverage of your codebase:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
plain code annotations
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This outputs a summary like `85.2% typed (23/27 functions)`.
|
|
81
|
+
|
|
82
|
+
To see which functions are missing annotations:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
plain code annotations --details
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
You can also output the results as JSON for use in CI or other tools:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
plain code annotations --json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Configuration
|
|
95
|
+
|
|
96
|
+
Default configuration is provided by [`ruff_defaults.toml`](./ruff_defaults.toml) and [`biome_defaults.json`](./biome_defaults.json).
|
|
97
|
+
|
|
98
|
+
You can customize the behavior in your `pyproject.toml`:
|
|
99
|
+
|
|
100
|
+
```toml
|
|
101
|
+
[tool.plain.code]
|
|
102
|
+
exclude = ["path/to/exclude"]
|
|
103
|
+
|
|
104
|
+
[tool.plain.code.ty]
|
|
105
|
+
enabled = true # Set to false to disable ty
|
|
106
|
+
|
|
107
|
+
[tool.plain.code.biome]
|
|
108
|
+
enabled = true # Set to false to disable Biome
|
|
109
|
+
version = "1.5.3" # Pin to a specific version
|
|
110
|
+
|
|
111
|
+
[tool.plain.code.annotations]
|
|
112
|
+
enabled = true # Set to false to disable annotation checks
|
|
113
|
+
exclude = ["migrations"] # Exclude specific patterns
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
For more advanced configuration options, see [`get_code_config`](./cli.py#get_code_config).
|
|
117
|
+
|
|
118
|
+
Generally you won't need to change the configuration. The defaults are designed to "just work" for most projects. If you find yourself needing extensive customization, consider using the underlying tools (Ruff, ty, Biome) directly instead.
|
|
119
|
+
|
|
120
|
+
## FAQs
|
|
121
|
+
|
|
122
|
+
#### How do I install or update Biome manually?
|
|
123
|
+
|
|
124
|
+
Biome is installed automatically when you run `plain fix` or `plain code check`. If you need to manage it manually:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# Install Biome (or reinstall if corrupted)
|
|
128
|
+
plain code install
|
|
129
|
+
|
|
130
|
+
# Force reinstall even if up to date
|
|
131
|
+
plain code install --force
|
|
132
|
+
|
|
133
|
+
# Update to the latest version
|
|
134
|
+
plain code update
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
#### Why are test files excluded from annotation coverage?
|
|
138
|
+
|
|
139
|
+
Test files (`test_*.py`, `*_test.py`, and files in `tests/` or `test/` directories) are excluded by default because they typically contain many small helper functions where type annotations add noise without providing significant value. You can customize this behavior via the `exclude` option in the annotations configuration.
|
|
140
|
+
|
|
141
|
+
#### How do I check a specific directory?
|
|
142
|
+
|
|
143
|
+
All commands accept a path argument:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
plain fix path/to/directory
|
|
147
|
+
plain code check path/to/directory
|
|
148
|
+
plain code annotations path/to/directory
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Installation
|
|
152
|
+
|
|
153
|
+
Install the `plain.code` package from [PyPI](https://pypi.org/project/plain.code/):
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
uv add plain.code
|
|
157
|
+
```
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Type annotation analyzer for Python codebases.
|
|
3
|
+
|
|
4
|
+
Analyzes Python files to determine the percentage of functions/methods
|
|
5
|
+
that have complete type annotations (parameters and return types).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from fnmatch import fnmatch
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class FunctionInfo:
|
|
20
|
+
"""Information about a function/method for type checking."""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
file: str
|
|
24
|
+
line: int
|
|
25
|
+
is_method: bool = False
|
|
26
|
+
has_return_type: bool = False
|
|
27
|
+
total_params: int = 0
|
|
28
|
+
typed_params: int = 0
|
|
29
|
+
is_property: bool = False
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def is_fully_typed(self) -> bool:
|
|
33
|
+
"""Check if function has all type annotations."""
|
|
34
|
+
return self.has_return_type and (self.typed_params == self.total_params)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class FileStats:
|
|
39
|
+
"""Statistics for a single Python file."""
|
|
40
|
+
|
|
41
|
+
path: str
|
|
42
|
+
functions: list[FunctionInfo] = field(default_factory=list)
|
|
43
|
+
ignore_comments: int = 0
|
|
44
|
+
cast_calls: int = 0
|
|
45
|
+
assert_statements: int = 0
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def total_functions(self) -> int:
|
|
49
|
+
return len(self.functions)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def fully_typed_functions(self) -> int:
|
|
53
|
+
return sum(1 for f in self.functions if f.is_fully_typed)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def missing_functions(self) -> int:
|
|
57
|
+
return self.total_functions - self.fully_typed_functions
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TypeAnnotationAnalyzer(ast.NodeVisitor):
|
|
61
|
+
"""AST visitor to analyze type annotations in Python code."""
|
|
62
|
+
|
|
63
|
+
def __init__(self, file_path: str) -> None:
|
|
64
|
+
self.file_path = file_path
|
|
65
|
+
self.functions: list[FunctionInfo] = []
|
|
66
|
+
self.class_stack: list[str] = []
|
|
67
|
+
|
|
68
|
+
def visit_ClassDef(self, node: ast.ClassDef) -> None:
|
|
69
|
+
"""Track when we enter/exit a class."""
|
|
70
|
+
self.class_stack.append(node.name)
|
|
71
|
+
self.generic_visit(node)
|
|
72
|
+
self.class_stack.pop()
|
|
73
|
+
|
|
74
|
+
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
75
|
+
"""Analyze function definitions."""
|
|
76
|
+
self._analyze_function(node)
|
|
77
|
+
self.generic_visit(node)
|
|
78
|
+
|
|
79
|
+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> None:
|
|
80
|
+
"""Analyze async function definitions."""
|
|
81
|
+
self._analyze_function(node)
|
|
82
|
+
self.generic_visit(node)
|
|
83
|
+
|
|
84
|
+
def _analyze_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef) -> None:
|
|
85
|
+
"""Analyze a function/method for type annotations."""
|
|
86
|
+
# Skip __init__ return type check (it's always None implicitly)
|
|
87
|
+
is_init = node.name == "__init__"
|
|
88
|
+
|
|
89
|
+
# Check if it's a method (inside a class)
|
|
90
|
+
is_method = bool(self.class_stack)
|
|
91
|
+
|
|
92
|
+
# Check decorators
|
|
93
|
+
is_property = any(
|
|
94
|
+
(isinstance(d, ast.Name) and d.id == "property")
|
|
95
|
+
or (isinstance(d, ast.Attribute) and d.attr == "property")
|
|
96
|
+
for d in node.decorator_list
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Create function info
|
|
100
|
+
func_info = FunctionInfo(
|
|
101
|
+
name=node.name,
|
|
102
|
+
file=self.file_path,
|
|
103
|
+
line=node.lineno,
|
|
104
|
+
is_method=is_method,
|
|
105
|
+
is_property=is_property,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Check return type (not needed for __init__)
|
|
109
|
+
if not is_init:
|
|
110
|
+
func_info.has_return_type = node.returns is not None
|
|
111
|
+
else:
|
|
112
|
+
func_info.has_return_type = True
|
|
113
|
+
|
|
114
|
+
def handle_param(arg: ast.arg) -> None:
|
|
115
|
+
if is_method and arg.arg in {"self", "cls"}:
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
func_info.total_params += 1
|
|
119
|
+
if arg.annotation is not None:
|
|
120
|
+
func_info.typed_params += 1
|
|
121
|
+
|
|
122
|
+
# Analyze parameters
|
|
123
|
+
for arg in node.args.posonlyargs:
|
|
124
|
+
handle_param(arg)
|
|
125
|
+
|
|
126
|
+
for arg in node.args.args:
|
|
127
|
+
handle_param(arg)
|
|
128
|
+
|
|
129
|
+
for arg in node.args.kwonlyargs:
|
|
130
|
+
handle_param(arg)
|
|
131
|
+
|
|
132
|
+
# Check *args and **kwargs
|
|
133
|
+
if node.args.vararg:
|
|
134
|
+
func_info.total_params += 1
|
|
135
|
+
if node.args.vararg.annotation is not None:
|
|
136
|
+
func_info.typed_params += 1
|
|
137
|
+
|
|
138
|
+
if node.args.kwarg:
|
|
139
|
+
func_info.total_params += 1
|
|
140
|
+
if node.args.kwarg.annotation is not None:
|
|
141
|
+
func_info.typed_params += 1
|
|
142
|
+
|
|
143
|
+
self.functions.append(func_info)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def count_ignore_comments(content: str) -> int:
|
|
147
|
+
"""Count type: ignore comments in the file."""
|
|
148
|
+
count = 0
|
|
149
|
+
pattern = r"#\s*type:\s*ignore"
|
|
150
|
+
|
|
151
|
+
for line in content.split("\n"):
|
|
152
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
153
|
+
count += 1
|
|
154
|
+
|
|
155
|
+
return count
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def count_cast_calls(content: str) -> int:
|
|
159
|
+
"""Count cast() function calls in the file."""
|
|
160
|
+
# Match both 'cast(' and 'typing.cast('
|
|
161
|
+
patterns = [
|
|
162
|
+
r"\bcast\s*\(",
|
|
163
|
+
r"\btyping\.cast\s*\(",
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
count = 0
|
|
167
|
+
for line in content.split("\n"):
|
|
168
|
+
for pattern in patterns:
|
|
169
|
+
count += len(re.findall(pattern, line))
|
|
170
|
+
|
|
171
|
+
return count
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class AssertCounter(ast.NodeVisitor):
|
|
175
|
+
"""AST visitor to count assert statements."""
|
|
176
|
+
|
|
177
|
+
def __init__(self) -> None:
|
|
178
|
+
self.count = 0
|
|
179
|
+
|
|
180
|
+
def visit_Assert(self, node: ast.Assert) -> None:
|
|
181
|
+
self.count += 1
|
|
182
|
+
self.generic_visit(node)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def count_assert_statements(tree: ast.AST) -> int:
|
|
186
|
+
"""Count assert statements in the AST."""
|
|
187
|
+
counter = AssertCounter()
|
|
188
|
+
counter.visit(tree)
|
|
189
|
+
return counter.count
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def analyze_file(file_path: Path) -> FileStats | None:
|
|
193
|
+
"""Analyze a single Python file for type annotations."""
|
|
194
|
+
try:
|
|
195
|
+
with open(file_path, encoding="utf-8") as f:
|
|
196
|
+
content = f.read()
|
|
197
|
+
|
|
198
|
+
tree = ast.parse(content, filename=str(file_path))
|
|
199
|
+
analyzer = TypeAnnotationAnalyzer(str(file_path))
|
|
200
|
+
analyzer.visit(tree)
|
|
201
|
+
|
|
202
|
+
ignore_count = count_ignore_comments(content)
|
|
203
|
+
cast_count = count_cast_calls(content)
|
|
204
|
+
assert_count = count_assert_statements(tree)
|
|
205
|
+
|
|
206
|
+
stats = FileStats(
|
|
207
|
+
path=str(file_path),
|
|
208
|
+
functions=analyzer.functions,
|
|
209
|
+
ignore_comments=ignore_count,
|
|
210
|
+
cast_calls=cast_count,
|
|
211
|
+
assert_statements=assert_count,
|
|
212
|
+
)
|
|
213
|
+
return stats
|
|
214
|
+
|
|
215
|
+
except (SyntaxError, UnicodeDecodeError):
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def find_python_files(
|
|
220
|
+
directory: Path, exclude_patterns: list[str] | None = None
|
|
221
|
+
) -> list[Path]:
|
|
222
|
+
"""Find all Python files in a directory, excluding certain patterns."""
|
|
223
|
+
default_patterns = [
|
|
224
|
+
"__pycache__",
|
|
225
|
+
".git",
|
|
226
|
+
".venv",
|
|
227
|
+
"venv",
|
|
228
|
+
"env",
|
|
229
|
+
".tox",
|
|
230
|
+
"build",
|
|
231
|
+
"dist",
|
|
232
|
+
"*.egg-info",
|
|
233
|
+
".mypy_cache",
|
|
234
|
+
".pytest_cache",
|
|
235
|
+
# Exclude test files from annotation metrics
|
|
236
|
+
"test_*.py",
|
|
237
|
+
"*_test.py",
|
|
238
|
+
"tests",
|
|
239
|
+
"test",
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
patterns = list(default_patterns)
|
|
243
|
+
if exclude_patterns:
|
|
244
|
+
patterns.extend(exclude_patterns)
|
|
245
|
+
|
|
246
|
+
def should_exclude(path: Path) -> bool:
|
|
247
|
+
try:
|
|
248
|
+
relative = path.relative_to(directory).as_posix()
|
|
249
|
+
except ValueError:
|
|
250
|
+
relative = path.as_posix()
|
|
251
|
+
|
|
252
|
+
candidates = {relative, path.as_posix(), path.name}
|
|
253
|
+
for pattern in patterns:
|
|
254
|
+
if any(fnmatch(candidate, pattern) for candidate in candidates):
|
|
255
|
+
return True
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
python_files = []
|
|
259
|
+
|
|
260
|
+
for root, dirs, files in os.walk(directory):
|
|
261
|
+
# Filter out excluded directories
|
|
262
|
+
root_path = Path(root)
|
|
263
|
+
dirs[:] = [d for d in dirs if not should_exclude(root_path / d)]
|
|
264
|
+
|
|
265
|
+
for file in files:
|
|
266
|
+
if file.endswith(".py"):
|
|
267
|
+
file_path = root_path / file
|
|
268
|
+
# Check if file path matches excluded patterns
|
|
269
|
+
if not should_exclude(file_path):
|
|
270
|
+
python_files.append(file_path)
|
|
271
|
+
|
|
272
|
+
return python_files
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@dataclass
|
|
276
|
+
class AnnotationResult:
|
|
277
|
+
"""Result of annotation analysis."""
|
|
278
|
+
|
|
279
|
+
total_functions: int
|
|
280
|
+
fully_typed_functions: int
|
|
281
|
+
missing_count: int
|
|
282
|
+
total_ignores: int
|
|
283
|
+
total_casts: int
|
|
284
|
+
total_asserts: int
|
|
285
|
+
file_stats: list[FileStats]
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def coverage_percentage(self) -> float:
|
|
289
|
+
if self.total_functions == 0:
|
|
290
|
+
return 100.0
|
|
291
|
+
return (self.fully_typed_functions / self.total_functions) * 100
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def check_annotations(
|
|
295
|
+
path: str, exclude_patterns: list[str] | None = None
|
|
296
|
+
) -> AnnotationResult:
|
|
297
|
+
"""Check type annotations in the given path."""
|
|
298
|
+
target = Path(path)
|
|
299
|
+
|
|
300
|
+
if target.is_file():
|
|
301
|
+
if not target.suffix == ".py":
|
|
302
|
+
return AnnotationResult(
|
|
303
|
+
total_functions=0,
|
|
304
|
+
fully_typed_functions=0,
|
|
305
|
+
missing_count=0,
|
|
306
|
+
total_ignores=0,
|
|
307
|
+
total_casts=0,
|
|
308
|
+
total_asserts=0,
|
|
309
|
+
file_stats=[],
|
|
310
|
+
)
|
|
311
|
+
python_files = [target]
|
|
312
|
+
elif target.is_dir():
|
|
313
|
+
python_files = find_python_files(target, exclude_patterns)
|
|
314
|
+
else:
|
|
315
|
+
return AnnotationResult(
|
|
316
|
+
total_functions=0,
|
|
317
|
+
fully_typed_functions=0,
|
|
318
|
+
missing_count=0,
|
|
319
|
+
total_ignores=0,
|
|
320
|
+
total_casts=0,
|
|
321
|
+
total_asserts=0,
|
|
322
|
+
file_stats=[],
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
all_stats = []
|
|
326
|
+
for file_path in python_files:
|
|
327
|
+
stats = analyze_file(file_path)
|
|
328
|
+
if stats:
|
|
329
|
+
all_stats.append(stats)
|
|
330
|
+
|
|
331
|
+
total_functions = sum(s.total_functions for s in all_stats)
|
|
332
|
+
fully_typed_functions = sum(s.fully_typed_functions for s in all_stats)
|
|
333
|
+
total_ignores = sum(s.ignore_comments for s in all_stats)
|
|
334
|
+
total_casts = sum(s.cast_calls for s in all_stats)
|
|
335
|
+
total_asserts = sum(s.assert_statements for s in all_stats)
|
|
336
|
+
|
|
337
|
+
return AnnotationResult(
|
|
338
|
+
total_functions=total_functions,
|
|
339
|
+
fully_typed_functions=fully_typed_functions,
|
|
340
|
+
missing_count=total_functions - fully_typed_functions,
|
|
341
|
+
total_ignores=total_ignores,
|
|
342
|
+
total_casts=total_casts,
|
|
343
|
+
total_asserts=total_asserts,
|
|
344
|
+
file_stats=all_stats,
|
|
345
|
+
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import subprocess
|
|
4
5
|
import sys
|
|
5
6
|
import tomllib
|
|
@@ -12,6 +13,7 @@ from plain.cli import register_cli
|
|
|
12
13
|
from plain.cli.print import print_event
|
|
13
14
|
from plain.cli.runtime import common_command, without_runtime_setup
|
|
14
15
|
|
|
16
|
+
from .annotations import AnnotationResult, check_annotations
|
|
15
17
|
from .biome import Biome
|
|
16
18
|
|
|
17
19
|
DEFAULT_RUFF_CONFIG = Path(__file__).parent / "ruff_defaults.toml"
|
|
@@ -78,12 +80,14 @@ def update() -> None:
|
|
|
78
80
|
@click.option("--skip-ruff", is_flag=True, help="Skip Ruff checks")
|
|
79
81
|
@click.option("--skip-ty", is_flag=True, help="Skip ty type checks")
|
|
80
82
|
@click.option("--skip-biome", is_flag=True, help="Skip Biome checks")
|
|
83
|
+
@click.option("--skip-annotations", is_flag=True, help="Skip type annotation checks")
|
|
81
84
|
def check(
|
|
82
85
|
ctx: click.Context,
|
|
83
86
|
path: str,
|
|
84
87
|
skip_ruff: bool,
|
|
85
88
|
skip_ty: bool,
|
|
86
89
|
skip_biome: bool,
|
|
90
|
+
skip_annotations: bool,
|
|
87
91
|
) -> None:
|
|
88
92
|
"""Check for formatting and linting issues"""
|
|
89
93
|
ruff_args = ["--config", str(DEFAULT_RUFF_CONFIG)]
|
|
@@ -125,6 +129,106 @@ def check(
|
|
|
125
129
|
result = biome.invoke("check", path)
|
|
126
130
|
maybe_exit(result.returncode)
|
|
127
131
|
|
|
132
|
+
if not skip_annotations and config.get("annotations", {}).get("enabled", True):
|
|
133
|
+
print_event("annotations...", newline=False)
|
|
134
|
+
exclude_patterns = config.get("annotations", {}).get("exclude", [])
|
|
135
|
+
ann_result = check_annotations(path, exclude_patterns or None)
|
|
136
|
+
if ann_result.missing_count > 0:
|
|
137
|
+
click.secho(
|
|
138
|
+
f"{ann_result.missing_count} functions are untyped",
|
|
139
|
+
fg="red",
|
|
140
|
+
)
|
|
141
|
+
click.secho("Run 'plain code annotations --details' for details")
|
|
142
|
+
maybe_exit(1)
|
|
143
|
+
else:
|
|
144
|
+
click.secho("All functions typed!", fg="green")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@without_runtime_setup
|
|
148
|
+
@cli.command()
|
|
149
|
+
@click.argument("path", default=".")
|
|
150
|
+
@click.option("--details", is_flag=True, help="List untyped functions")
|
|
151
|
+
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
|
152
|
+
def annotations(path: str, details: bool, as_json: bool) -> None:
|
|
153
|
+
"""Check type annotation status"""
|
|
154
|
+
config = get_code_config()
|
|
155
|
+
exclude_patterns = config.get("annotations", {}).get("exclude", [])
|
|
156
|
+
result = check_annotations(path, exclude_patterns or None)
|
|
157
|
+
if as_json:
|
|
158
|
+
_print_annotations_json(result)
|
|
159
|
+
else:
|
|
160
|
+
_print_annotations_report(result, show_details=details)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _print_annotations_report(
|
|
164
|
+
result: AnnotationResult,
|
|
165
|
+
show_details: bool = False,
|
|
166
|
+
) -> None:
|
|
167
|
+
"""Print the annotation report with colors."""
|
|
168
|
+
if result.total_functions == 0:
|
|
169
|
+
click.echo("No functions found")
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
# Detailed output first (if enabled and there are untyped functions)
|
|
173
|
+
if show_details and result.missing_count > 0:
|
|
174
|
+
# Collect all untyped functions with full paths
|
|
175
|
+
untyped_items: list[tuple[str, str, int, list[str]]] = []
|
|
176
|
+
|
|
177
|
+
for stats in result.file_stats:
|
|
178
|
+
for func in stats.functions:
|
|
179
|
+
if not func.is_fully_typed:
|
|
180
|
+
issues = []
|
|
181
|
+
if not func.has_return_type:
|
|
182
|
+
issues.append("return type")
|
|
183
|
+
missing_params = func.total_params - func.typed_params
|
|
184
|
+
if missing_params > 0:
|
|
185
|
+
param_word = "param" if missing_params == 1 else "params"
|
|
186
|
+
issues.append(f"{missing_params} {param_word}")
|
|
187
|
+
untyped_items.append((stats.path, func.name, func.line, issues))
|
|
188
|
+
|
|
189
|
+
# Sort by file path, then line number
|
|
190
|
+
untyped_items.sort(key=lambda x: (x[0], x[2]))
|
|
191
|
+
|
|
192
|
+
# Print each untyped function
|
|
193
|
+
for file_path, func_name, line, issues in untyped_items:
|
|
194
|
+
location = click.style(f"{file_path}:{line}", fg="cyan")
|
|
195
|
+
issue_str = click.style(f"({', '.join(issues)})", dim=True)
|
|
196
|
+
click.echo(f"{location} {func_name} {issue_str}")
|
|
197
|
+
|
|
198
|
+
click.echo()
|
|
199
|
+
|
|
200
|
+
# Summary line
|
|
201
|
+
pct = result.coverage_percentage
|
|
202
|
+
color = "green" if result.missing_count == 0 else "red"
|
|
203
|
+
click.secho(
|
|
204
|
+
f"{pct:.1f}% typed ({result.fully_typed_functions}/{result.total_functions} functions)",
|
|
205
|
+
fg=color,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Code smell indicators (only if present)
|
|
209
|
+
smells = []
|
|
210
|
+
if result.total_ignores > 0:
|
|
211
|
+
smells.append(f"{result.total_ignores} ignore")
|
|
212
|
+
if result.total_casts > 0:
|
|
213
|
+
smells.append(f"{result.total_casts} cast")
|
|
214
|
+
if result.total_asserts > 0:
|
|
215
|
+
smells.append(f"{result.total_asserts} assert")
|
|
216
|
+
if smells:
|
|
217
|
+
click.secho(f"{', '.join(smells)}", fg="yellow")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _print_annotations_json(result: AnnotationResult) -> None:
|
|
221
|
+
"""Print the annotation report as JSON."""
|
|
222
|
+
output = {
|
|
223
|
+
"overall_coverage": result.coverage_percentage,
|
|
224
|
+
"total_functions": result.total_functions,
|
|
225
|
+
"fully_typed_functions": result.fully_typed_functions,
|
|
226
|
+
"total_ignores": result.total_ignores,
|
|
227
|
+
"total_casts": result.total_casts,
|
|
228
|
+
"total_asserts": result.total_asserts,
|
|
229
|
+
}
|
|
230
|
+
click.echo(json.dumps(output))
|
|
231
|
+
|
|
128
232
|
|
|
129
233
|
@common_command
|
|
130
234
|
@without_runtime_setup
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plain-fix
|
|
3
|
+
description: Fixes code formatting and linting issues. Use during development to clean up code as you work.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Fix Code Issues
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
uv run plain fix [path]
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Automatically fixes formatting and linting issues using ruff and biome.
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
|
|
16
|
+
- `--unsafe-fixes` - Apply ruff unsafe fixes
|
|
17
|
+
- `--add-noqa` - Add noqa comments to suppress errors
|
|
18
|
+
|
|
19
|
+
## Check Without Fixing
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
uv run plain code check [path]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Runs ruff, ty (type checking), biome, and annotation coverage checks without auto-fixing.
|
plain_code-0.17.0/PKG-INFO
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: plain.code
|
|
3
|
-
Version: 0.17.0
|
|
4
|
-
Summary: Preconfigured code formatting and linting.
|
|
5
|
-
Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
|
|
6
|
-
License-Expression: BSD-3-Clause
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Requires-Python: >=3.13
|
|
9
|
-
Requires-Dist: plain<1.0.0
|
|
10
|
-
Requires-Dist: requests>=2.0.0
|
|
11
|
-
Requires-Dist: ruff>=0.1.0
|
|
12
|
-
Requires-Dist: tomlkit>=0.11.0
|
|
13
|
-
Requires-Dist: ty>=0.0.11
|
|
14
|
-
Description-Content-Type: text/markdown
|
|
15
|
-
|
|
16
|
-
# plain.code
|
|
17
|
-
|
|
18
|
-
**Preconfigured code formatting and linting.**
|
|
19
|
-
|
|
20
|
-
- [Overview](#overview)
|
|
21
|
-
- [Configuration](#configuration)
|
|
22
|
-
- [Installation](#installation)
|
|
23
|
-
|
|
24
|
-
## Overview
|
|
25
|
-
|
|
26
|
-
The `plain code` command provides comprehensive code quality tools:
|
|
27
|
-
|
|
28
|
-
- **[Ruff](https://astral.sh/ruff)** - Python linting and formatting
|
|
29
|
-
- **[ty](https://astral.sh/ty)** - Python type checking
|
|
30
|
-
- **[Biome](https://biomejs.dev/)** - JavaScript, JSON, and CSS formatting
|
|
31
|
-
|
|
32
|
-
Ruff and ty are installed as Python dependencies. Biome is managed automatically as a standalone binary (npm is not required).
|
|
33
|
-
|
|
34
|
-
The most used command is `plain code fix`, which can be run using the alias `plain fix`:
|
|
35
|
-
|
|
36
|
-
```bash
|
|
37
|
-
plain fix
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
This will automatically fix linting issues and format your code according to the configured rules.
|
|
41
|
-
|
|
42
|
-

|
|
43
|
-
|
|
44
|
-
To check your code without making changes (including type checking):
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
plain code check
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
You can skip specific tools if needed:
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
# Skip type checking during rapid development
|
|
54
|
-
plain code check --skip-ty
|
|
55
|
-
|
|
56
|
-
# Only run type checks
|
|
57
|
-
plain code check --skip-ruff --skip-biome
|
|
58
|
-
|
|
59
|
-
# Skip Biome checks
|
|
60
|
-
plain code check --skip-biome
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
If [`plain.dev`](/plain-dev/README.md) is installed then `plain code check` will be run automatically as a part of `plain precommit` to help catch issues before they are committed.
|
|
64
|
-
|
|
65
|
-
## Configuration
|
|
66
|
-
|
|
67
|
-
Default configuration is provided by [`ruff_defaults.toml`](./ruff_defaults.toml) and [`biome_defaults.json`](./biome_defaults.json).
|
|
68
|
-
|
|
69
|
-
You can customize the behavior in your `pyproject.toml`:
|
|
70
|
-
|
|
71
|
-
```toml
|
|
72
|
-
[tool.plain.code]
|
|
73
|
-
exclude = ["path/to/exclude"]
|
|
74
|
-
|
|
75
|
-
[tool.plain.code.ty]
|
|
76
|
-
enabled = true # Set to false to disable ty
|
|
77
|
-
|
|
78
|
-
[tool.plain.code.biome]
|
|
79
|
-
enabled = true # Set to false to disable Biome
|
|
80
|
-
version = "1.5.3" # Pin to a specific version
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
For more advanced configuration options, see [`get_code_config`](./cli.py#get_code_config).
|
|
84
|
-
|
|
85
|
-
Generally it's expected that you won't change the configuration! We've tried to pick defaults that "just work" for most projects. If you find yourself needing to customize things, you should probably just move to using the tools themselves directly instead of the `plain.code` package.
|
|
86
|
-
|
|
87
|
-
## Installation
|
|
88
|
-
|
|
89
|
-
Install the `plain.code` package from [PyPI](https://pypi.org/project/plain.code/):
|
|
90
|
-
|
|
91
|
-
```bash
|
|
92
|
-
uv add plain.code
|
|
93
|
-
```
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# plain.code
|
|
2
|
-
|
|
3
|
-
**Preconfigured code formatting and linting.**
|
|
4
|
-
|
|
5
|
-
- [Overview](#overview)
|
|
6
|
-
- [Configuration](#configuration)
|
|
7
|
-
- [Installation](#installation)
|
|
8
|
-
|
|
9
|
-
## Overview
|
|
10
|
-
|
|
11
|
-
The `plain code` command provides comprehensive code quality tools:
|
|
12
|
-
|
|
13
|
-
- **[Ruff](https://astral.sh/ruff)** - Python linting and formatting
|
|
14
|
-
- **[ty](https://astral.sh/ty)** - Python type checking
|
|
15
|
-
- **[Biome](https://biomejs.dev/)** - JavaScript, JSON, and CSS formatting
|
|
16
|
-
|
|
17
|
-
Ruff and ty are installed as Python dependencies. Biome is managed automatically as a standalone binary (npm is not required).
|
|
18
|
-
|
|
19
|
-
The most used command is `plain code fix`, which can be run using the alias `plain fix`:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
plain fix
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
This will automatically fix linting issues and format your code according to the configured rules.
|
|
26
|
-
|
|
27
|
-

|
|
28
|
-
|
|
29
|
-
To check your code without making changes (including type checking):
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
plain code check
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
You can skip specific tools if needed:
|
|
36
|
-
|
|
37
|
-
```bash
|
|
38
|
-
# Skip type checking during rapid development
|
|
39
|
-
plain code check --skip-ty
|
|
40
|
-
|
|
41
|
-
# Only run type checks
|
|
42
|
-
plain code check --skip-ruff --skip-biome
|
|
43
|
-
|
|
44
|
-
# Skip Biome checks
|
|
45
|
-
plain code check --skip-biome
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
If [`plain.dev`](/plain-dev/README.md) is installed then `plain code check` will be run automatically as a part of `plain precommit` to help catch issues before they are committed.
|
|
49
|
-
|
|
50
|
-
## Configuration
|
|
51
|
-
|
|
52
|
-
Default configuration is provided by [`ruff_defaults.toml`](./ruff_defaults.toml) and [`biome_defaults.json`](./biome_defaults.json).
|
|
53
|
-
|
|
54
|
-
You can customize the behavior in your `pyproject.toml`:
|
|
55
|
-
|
|
56
|
-
```toml
|
|
57
|
-
[tool.plain.code]
|
|
58
|
-
exclude = ["path/to/exclude"]
|
|
59
|
-
|
|
60
|
-
[tool.plain.code.ty]
|
|
61
|
-
enabled = true # Set to false to disable ty
|
|
62
|
-
|
|
63
|
-
[tool.plain.code.biome]
|
|
64
|
-
enabled = true # Set to false to disable Biome
|
|
65
|
-
version = "1.5.3" # Pin to a specific version
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
For more advanced configuration options, see [`get_code_config`](./cli.py#get_code_config).
|
|
69
|
-
|
|
70
|
-
Generally it's expected that you won't change the configuration! We've tried to pick defaults that "just work" for most projects. If you find yourself needing to customize things, you should probably just move to using the tools themselves directly instead of the `plain.code` package.
|
|
71
|
-
|
|
72
|
-
## Installation
|
|
73
|
-
|
|
74
|
-
Install the `plain.code` package from [PyPI](https://pypi.org/project/plain.code/):
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
uv add plain.code
|
|
78
|
-
```
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: plain-check
|
|
3
|
-
description: Runs code quality checks including ruff, type checking, and biome. Use for linting, formatting, or preflight validation.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Code Quality
|
|
7
|
-
|
|
8
|
-
## Check for Issues
|
|
9
|
-
|
|
10
|
-
```
|
|
11
|
-
uv run plain code check [path]
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
Runs ruff, ty (type checking), and biome checks.
|
|
15
|
-
|
|
16
|
-
## Fix Issues
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
uv run plain fix [path]
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
Automatically fixes formatting and linting issues.
|
|
23
|
-
|
|
24
|
-
Options:
|
|
25
|
-
|
|
26
|
-
- `--unsafe-fixes` - Apply ruff unsafe fixes
|
|
27
|
-
- `--add-noqa` - Add noqa comments to suppress errors
|
|
28
|
-
|
|
29
|
-
## Preflight Checks
|
|
30
|
-
|
|
31
|
-
```
|
|
32
|
-
uv run plain preflight
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Validates Plain configuration.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|