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.
@@ -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
+ ![](https://assets.plainframework.com/docs/plain-fix.png)
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
+ ![](https://assets.plainframework.com/docs/plain-fix.png)
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.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "plain.code"
3
- version = "0.17.0"
3
+ version = "0.18.0"
4
4
  description = "Preconfigured code formatting and linting."
5
5
  authors = [{name = "Dave Gaeddert", email = "dave.gaeddert@dropseed.dev"}]
6
6
  readme = "README.md"
@@ -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
- ![](https://assets.plainframework.com/docs/plain-fix.png)
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
- ![](https://assets.plainframework.com/docs/plain-fix.png)
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