stifle 1.0.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,5 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ .pytest_cache/
stifle-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 stifle contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
stifle-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,254 @@
1
+ Metadata-Version: 2.5
2
+ Name: stifle
3
+ Version: 1.0.0
4
+ Summary: Strip comments (and optionally docstrings) from Python code — fast, and provably incapable of touching anything else.
5
+ Project-URL: Repository, https://github.com/byzantime/stifle
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: cleanup,comments,docstrings,source,strip
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Software Development :: Quality Assurance
17
+ Requires-Python: >=3.12
18
+ Description-Content-Type: text/markdown
19
+
20
+ # stifle
21
+
22
+ Delete comments — and optionally docstrings — from a Python codebase.
23
+ Fast, and built so it provably cannot touch anything the program can see.
24
+
25
+ ```console
26
+ $ stifle format path/to/project # strip comments and orphan strings, in place
27
+ $ stifle check src/ # CI gate: exit 1 if anything would change
28
+ $ stifle check --diff --delete docstrings pkg/ # preview without writing
29
+ ```
30
+
31
+ Zero runtime dependencies, pure stdlib, Python 3.12+.
32
+
33
+ ## What gets deleted
34
+
35
+ Deletions are selected per category with `--delete CAT` (repeatable) and
36
+ subtracted from with `--skip CAT` (repeatable):
37
+
38
+ | Category | Meaning |
39
+ |---|---|
40
+ | own-line | comments that occupy their own line |
41
+ | trailing | comments after code on the same line |
42
+ | orphan-strings | a bare string statement after an assignment |
43
+ | docstrings | module/class/function docstrings |
44
+
45
+ The default is equivalent to
46
+ `--delete own-line --delete trailing --delete orphan-strings`; docstrings
47
+ are only ever deleted when explicitly selected. `--delete` replaces the
48
+ default selection entirely (ruff select-style); `--skip` keeps a category
49
+ despite the selection.
50
+
51
+ When docstrings are selected, a class or function whose body is *only* a
52
+ docstring gets a `pass` at the same indentation so the code still parses.
53
+ A docstring that shares its line with other code (`def f(): "doc"`) is
54
+ conservatively kept.
55
+
56
+ ### Orphan strings
57
+
58
+ An orphan string is a string literal sitting on its own as a statement
59
+ after an assignment — a comment wearing a docstring's clothes:
60
+
61
+ ```python
62
+ RETIRING = frozenset({DONE, CLOSED})
63
+ """Which statuses retire a task. <- deleted: not a docstring
64
+
65
+ Prose that a docstring-length cap would have caught, parked one
66
+ statement below the only place it could have been enforced."""
67
+ ```
68
+
69
+ Python evaluates and discards it, so removing it cannot change the
70
+ program. A real docstring is `body[0]`, so no assignment can precede it
71
+ and none of them match. Indentation is no escape: the rule applies inside
72
+ `if`/`for`/`while`/`with`/`try` bodies too, and to a whole run of strings,
73
+ not just the first. A string sharing its line with code (`x = 1; "doc"`)
74
+ is left alone, since deletion works by whole lines.
75
+
76
+ **This deletes PEP 224 / Sphinx `autodoc` attribute docstrings**, which
77
+ use exactly the same syntax and are indistinguishable from the pattern
78
+ above. If your package documents module or class attributes that way, opt
79
+ out with `--skip orphan-strings` or `skip = ["orphan-strings"]`.
80
+
81
+ Selecting this category (or `docstrings`) means the file must *parse*, not
82
+ merely tokenize: a source that the running interpreter's `ast` rejects —
83
+ one using newer syntax, say — is skipped and reported rather than
84
+ comment-stripped.
85
+
86
+ ## What is always preserved
87
+
88
+ - **Shebang** (`#!...` on line 1) and **PEP 263 coding declarations**
89
+ (lines 1–2) — deleting these can break execution or the file's encoding,
90
+ so they survive every selection, even `--no-default-keeps`.
91
+ - **Tool pragmas**: comments starting with `# noqa`, `# fmt:`,
92
+ `# isort:`, `# ruff:`, `# mypy:`, `# type:`, `# pyright:`, `# pragma:`
93
+ (case-insensitive, flexible spacing). Pass `--no-default-keeps` if you
94
+ really do want "literally all comments".
95
+ - Anything matching your own `--keep REGEX` (matched against the comment
96
+ text, including the `#`).
97
+
98
+ ## The safety guarantee
99
+
100
+ `stifle` never regenerates code. The only edits it makes are deleting whole
101
+ physical lines and cutting a line at the start column of a trailing comment,
102
+ so every surviving line is byte-for-byte identical to the input — formatting,
103
+ quotes, escapes, encodings, BOMs and CRLF line endings included.
104
+
105
+ On top of that, every rewrite must pass an independent verification gate
106
+ before the file is touched:
107
+
108
+ - **Comment deletions** re-tokenize input and output and require the token
109
+ streams — minus `COMMENT`/`NL` tokens — to be identical. Equal significant
110
+ token streams mean the compiler sees the exact same program.
111
+ - **Docstring and orphan-string deletions** parse both sides and require the
112
+ ASTs to match after the removed string statements are normalized out of the
113
+ input. An AST cannot see comments, so this path additionally requires every
114
+ comment on the preserve-list to survive. A file from which no string
115
+ statement was actually removed is verified by tokens, as before.
116
+
117
+ Any mismatch, or any internal error at all, leaves the file untouched and is
118
+ reported (exit code 2). Files that cannot be tokenized/parsed, decoded, or
119
+ that use lone-CR line endings are skipped the same way. Writes are atomic
120
+ (temp file + `os.replace`), so a crash can never leave a half-written file.
121
+
122
+ The stdlib of the running interpreter is used as a test corpus: every target
123
+ set must verify and be idempotent on every file.
124
+
125
+ ## CLI
126
+
127
+ stifle uses two subcommands, so the invocations you already know from
128
+ `ruff` and `black` do what you'd expect:
129
+
130
+ ```text
131
+ stifle check PATH... [--fix] [--delete CAT]... [--skip CAT]... [options]
132
+ stifle format PATH... [--check] [--delete CAT]... [--skip CAT]... [options]
133
+ ```
134
+
135
+ - `check` reports what would change and never writes; add `--fix`
136
+ (ruff-style) to rewrite in place. Exits 1 if anything would change.
137
+ - `format` (alias `strip`) rewrites in place (black-style); add
138
+ `--check` to only report instead.
139
+
140
+ Shared options:
141
+
142
+ ```text
143
+ --delete CAT delete this category instead of the default
144
+ (orphan-strings, own-line, trailing); one of
145
+ docstrings, orphan-strings, own-line, trailing;
146
+ repeatable to select several
147
+ --skip CAT keep this category despite the selection (repeatable)
148
+ --diff print unified diffs instead of writing (never writes)
149
+ --max-doc-lines N report docstrings longer than N content lines; exit 1
150
+ if any (composes with every selection)
151
+ --keep REGEX also keep comments matching REGEX (repeatable)
152
+ --default-keeps / --no-default-keeps
153
+ keep the built-in pragma preserve-list (default: true)
154
+ --exclude GLOB skip matching paths (repeatable; matches basename
155
+ or full path)
156
+ --jobs N worker processes (default: CPU count)
157
+ --config PATH read configuration from this pyproject.toml instead
158
+ of discovering one
159
+ --isolated ignore any pyproject.toml configuration
160
+ ```
161
+
162
+ Running `stifle` without a command is a usage error (exit 2); nothing is
163
+ written.
164
+
165
+ ## Configuration
166
+
167
+ `stifle` reads settings from a `[tool.stifle]` table in a `pyproject.toml`,
168
+ the same way `black` and `ruff` do:
169
+
170
+ ```toml
171
+ [tool.stifle]
172
+ delete = ["own-line"] # replaces the default (own-line, trailing,
173
+ # orphan-strings)
174
+ skip = ["trailing"] # subtracted from the selection
175
+ keep = ["^# KEEP"] # list of regexes for comments to preserve
176
+ default-keeps = true # built-in pragma preserve-list (# noqa etc.)
177
+ exclude = ["migrations/*"]
178
+ ```
179
+
180
+ Discovery is black-style: starting from the common ancestor of the input
181
+ paths, stifle walks up until it finds a `pyproject.toml` with a
182
+ `[tool.stifle]` table — stopping at the project root (a directory holding
183
+ `.git` or `.hg`). Pass `--config PATH` to read an explicit file (it may be
184
+ a normal pyproject or carry a bare top-level `[stifle]` table), or
185
+ `--isolated` to ignore configuration entirely.
186
+
187
+ Precedence follows the black convention: **configuration supplies defaults;
188
+ any explicit CLI flag wins outright** (`check`, `format`, `diff`, `jobs` and
189
+ paths are CLI-only and cannot be set in the file).
190
+
191
+ ## CI gate
192
+
193
+ ```console
194
+ $ stifle check src/
195
+ would strip comments from: src/pkg/mod.py
196
+ stifle: 1 files contain comments stifle would delete.
197
+ stifle: to fix, run: stifle format src/
198
+ stifle: (rewrites in place; run with --diff first to preview the deletions)
199
+ $ echo $?
200
+ 1
201
+ ```
202
+
203
+ Directories are searched recursively for `*.py`; `.git`, `.venv`, `venv`,
204
+ `__pycache__`, `build`, `dist`, `.tox`, `.nox`, `.eggs` and common tool
205
+ caches are skipped. Explicitly named files are processed as-is.
206
+
207
+ Exit codes: `0` success, `1` changes needed (`stifle check` without `--fix`, `stifle format --check`, or docstring
208
+ violations from `--max-doc-lines`), `2` any file skipped or failed (every
209
+ such file is listed on stderr, and left untouched).
210
+
211
+ ## Docstring length cap
212
+
213
+ ```console
214
+ $ stifle check --max-doc-lines 20 src/ # CI gate for oversized docstrings
215
+ ```
216
+
217
+ `--max-doc-lines N` reports every module/class/function docstring whose
218
+ content spans more than *N* lines, as
219
+ `path:lineno: docstring of 'name' has M lines (limit N)`. It is a check,
220
+ never a rewrite: docstrings are never modified by this flag, and it composes
221
+ with every selection. The count covers the docstring's own text — interior
222
+ blank lines do not count, the quote-only opening/closing lines do not. Any
223
+ violation makes the exit code 1.
224
+
225
+ The cap only inspects real docstrings (`body[0]`), so prose relocated one
226
+ statement below the thing it describes is invisible to it. That is the hole
227
+ `orphan-strings` closes: such a string is deleted outright rather than
228
+ measured.
229
+
230
+ Performance: files are processed in parallel with a process pool; stripping
231
+ plus verifying runs at roughly 8 MB of source per second per core (the
232
+ whole 15 MB CPython stdlib takes about two seconds on two cores).
233
+
234
+ ## Library use
235
+
236
+ ```python
237
+ from stifle import DOCSTRINGS, strip_source, verify
238
+
239
+ stripped = strip_source(
240
+ src, {"own-line", "docstrings"}, keep=re.compile(r"KEEP")
241
+ )
242
+ assert verify(src, stripped, {"own-line", "docstrings"}) # do before persisting
243
+ ```
244
+
245
+ `strip_source` is pure (`str -> str`) and raises `ValueError` on unknown
246
+ category names and `SyntaxError` / `tokenize.TokenError` on source it
247
+ cannot process. `verify` recomputes equivalence from scratch; treat a
248
+ `False` (or any exception) as "do not use the result".
249
+
250
+ ## Development
251
+
252
+ ```console
253
+ $ uv run --group dev pytest
254
+ ```
stifle-1.0.0/README.md ADDED
@@ -0,0 +1,235 @@
1
+ # stifle
2
+
3
+ Delete comments — and optionally docstrings — from a Python codebase.
4
+ Fast, and built so it provably cannot touch anything the program can see.
5
+
6
+ ```console
7
+ $ stifle format path/to/project # strip comments and orphan strings, in place
8
+ $ stifle check src/ # CI gate: exit 1 if anything would change
9
+ $ stifle check --diff --delete docstrings pkg/ # preview without writing
10
+ ```
11
+
12
+ Zero runtime dependencies, pure stdlib, Python 3.12+.
13
+
14
+ ## What gets deleted
15
+
16
+ Deletions are selected per category with `--delete CAT` (repeatable) and
17
+ subtracted from with `--skip CAT` (repeatable):
18
+
19
+ | Category | Meaning |
20
+ |---|---|
21
+ | own-line | comments that occupy their own line |
22
+ | trailing | comments after code on the same line |
23
+ | orphan-strings | a bare string statement after an assignment |
24
+ | docstrings | module/class/function docstrings |
25
+
26
+ The default is equivalent to
27
+ `--delete own-line --delete trailing --delete orphan-strings`; docstrings
28
+ are only ever deleted when explicitly selected. `--delete` replaces the
29
+ default selection entirely (ruff select-style); `--skip` keeps a category
30
+ despite the selection.
31
+
32
+ When docstrings are selected, a class or function whose body is *only* a
33
+ docstring gets a `pass` at the same indentation so the code still parses.
34
+ A docstring that shares its line with other code (`def f(): "doc"`) is
35
+ conservatively kept.
36
+
37
+ ### Orphan strings
38
+
39
+ An orphan string is a string literal sitting on its own as a statement
40
+ after an assignment — a comment wearing a docstring's clothes:
41
+
42
+ ```python
43
+ RETIRING = frozenset({DONE, CLOSED})
44
+ """Which statuses retire a task. <- deleted: not a docstring
45
+
46
+ Prose that a docstring-length cap would have caught, parked one
47
+ statement below the only place it could have been enforced."""
48
+ ```
49
+
50
+ Python evaluates and discards it, so removing it cannot change the
51
+ program. A real docstring is `body[0]`, so no assignment can precede it
52
+ and none of them match. Indentation is no escape: the rule applies inside
53
+ `if`/`for`/`while`/`with`/`try` bodies too, and to a whole run of strings,
54
+ not just the first. A string sharing its line with code (`x = 1; "doc"`)
55
+ is left alone, since deletion works by whole lines.
56
+
57
+ **This deletes PEP 224 / Sphinx `autodoc` attribute docstrings**, which
58
+ use exactly the same syntax and are indistinguishable from the pattern
59
+ above. If your package documents module or class attributes that way, opt
60
+ out with `--skip orphan-strings` or `skip = ["orphan-strings"]`.
61
+
62
+ Selecting this category (or `docstrings`) means the file must *parse*, not
63
+ merely tokenize: a source that the running interpreter's `ast` rejects —
64
+ one using newer syntax, say — is skipped and reported rather than
65
+ comment-stripped.
66
+
67
+ ## What is always preserved
68
+
69
+ - **Shebang** (`#!...` on line 1) and **PEP 263 coding declarations**
70
+ (lines 1–2) — deleting these can break execution or the file's encoding,
71
+ so they survive every selection, even `--no-default-keeps`.
72
+ - **Tool pragmas**: comments starting with `# noqa`, `# fmt:`,
73
+ `# isort:`, `# ruff:`, `# mypy:`, `# type:`, `# pyright:`, `# pragma:`
74
+ (case-insensitive, flexible spacing). Pass `--no-default-keeps` if you
75
+ really do want "literally all comments".
76
+ - Anything matching your own `--keep REGEX` (matched against the comment
77
+ text, including the `#`).
78
+
79
+ ## The safety guarantee
80
+
81
+ `stifle` never regenerates code. The only edits it makes are deleting whole
82
+ physical lines and cutting a line at the start column of a trailing comment,
83
+ so every surviving line is byte-for-byte identical to the input — formatting,
84
+ quotes, escapes, encodings, BOMs and CRLF line endings included.
85
+
86
+ On top of that, every rewrite must pass an independent verification gate
87
+ before the file is touched:
88
+
89
+ - **Comment deletions** re-tokenize input and output and require the token
90
+ streams — minus `COMMENT`/`NL` tokens — to be identical. Equal significant
91
+ token streams mean the compiler sees the exact same program.
92
+ - **Docstring and orphan-string deletions** parse both sides and require the
93
+ ASTs to match after the removed string statements are normalized out of the
94
+ input. An AST cannot see comments, so this path additionally requires every
95
+ comment on the preserve-list to survive. A file from which no string
96
+ statement was actually removed is verified by tokens, as before.
97
+
98
+ Any mismatch, or any internal error at all, leaves the file untouched and is
99
+ reported (exit code 2). Files that cannot be tokenized/parsed, decoded, or
100
+ that use lone-CR line endings are skipped the same way. Writes are atomic
101
+ (temp file + `os.replace`), so a crash can never leave a half-written file.
102
+
103
+ The stdlib of the running interpreter is used as a test corpus: every target
104
+ set must verify and be idempotent on every file.
105
+
106
+ ## CLI
107
+
108
+ stifle uses two subcommands, so the invocations you already know from
109
+ `ruff` and `black` do what you'd expect:
110
+
111
+ ```text
112
+ stifle check PATH... [--fix] [--delete CAT]... [--skip CAT]... [options]
113
+ stifle format PATH... [--check] [--delete CAT]... [--skip CAT]... [options]
114
+ ```
115
+
116
+ - `check` reports what would change and never writes; add `--fix`
117
+ (ruff-style) to rewrite in place. Exits 1 if anything would change.
118
+ - `format` (alias `strip`) rewrites in place (black-style); add
119
+ `--check` to only report instead.
120
+
121
+ Shared options:
122
+
123
+ ```text
124
+ --delete CAT delete this category instead of the default
125
+ (orphan-strings, own-line, trailing); one of
126
+ docstrings, orphan-strings, own-line, trailing;
127
+ repeatable to select several
128
+ --skip CAT keep this category despite the selection (repeatable)
129
+ --diff print unified diffs instead of writing (never writes)
130
+ --max-doc-lines N report docstrings longer than N content lines; exit 1
131
+ if any (composes with every selection)
132
+ --keep REGEX also keep comments matching REGEX (repeatable)
133
+ --default-keeps / --no-default-keeps
134
+ keep the built-in pragma preserve-list (default: true)
135
+ --exclude GLOB skip matching paths (repeatable; matches basename
136
+ or full path)
137
+ --jobs N worker processes (default: CPU count)
138
+ --config PATH read configuration from this pyproject.toml instead
139
+ of discovering one
140
+ --isolated ignore any pyproject.toml configuration
141
+ ```
142
+
143
+ Running `stifle` without a command is a usage error (exit 2); nothing is
144
+ written.
145
+
146
+ ## Configuration
147
+
148
+ `stifle` reads settings from a `[tool.stifle]` table in a `pyproject.toml`,
149
+ the same way `black` and `ruff` do:
150
+
151
+ ```toml
152
+ [tool.stifle]
153
+ delete = ["own-line"] # replaces the default (own-line, trailing,
154
+ # orphan-strings)
155
+ skip = ["trailing"] # subtracted from the selection
156
+ keep = ["^# KEEP"] # list of regexes for comments to preserve
157
+ default-keeps = true # built-in pragma preserve-list (# noqa etc.)
158
+ exclude = ["migrations/*"]
159
+ ```
160
+
161
+ Discovery is black-style: starting from the common ancestor of the input
162
+ paths, stifle walks up until it finds a `pyproject.toml` with a
163
+ `[tool.stifle]` table — stopping at the project root (a directory holding
164
+ `.git` or `.hg`). Pass `--config PATH` to read an explicit file (it may be
165
+ a normal pyproject or carry a bare top-level `[stifle]` table), or
166
+ `--isolated` to ignore configuration entirely.
167
+
168
+ Precedence follows the black convention: **configuration supplies defaults;
169
+ any explicit CLI flag wins outright** (`check`, `format`, `diff`, `jobs` and
170
+ paths are CLI-only and cannot be set in the file).
171
+
172
+ ## CI gate
173
+
174
+ ```console
175
+ $ stifle check src/
176
+ would strip comments from: src/pkg/mod.py
177
+ stifle: 1 files contain comments stifle would delete.
178
+ stifle: to fix, run: stifle format src/
179
+ stifle: (rewrites in place; run with --diff first to preview the deletions)
180
+ $ echo $?
181
+ 1
182
+ ```
183
+
184
+ Directories are searched recursively for `*.py`; `.git`, `.venv`, `venv`,
185
+ `__pycache__`, `build`, `dist`, `.tox`, `.nox`, `.eggs` and common tool
186
+ caches are skipped. Explicitly named files are processed as-is.
187
+
188
+ Exit codes: `0` success, `1` changes needed (`stifle check` without `--fix`, `stifle format --check`, or docstring
189
+ violations from `--max-doc-lines`), `2` any file skipped or failed (every
190
+ such file is listed on stderr, and left untouched).
191
+
192
+ ## Docstring length cap
193
+
194
+ ```console
195
+ $ stifle check --max-doc-lines 20 src/ # CI gate for oversized docstrings
196
+ ```
197
+
198
+ `--max-doc-lines N` reports every module/class/function docstring whose
199
+ content spans more than *N* lines, as
200
+ `path:lineno: docstring of 'name' has M lines (limit N)`. It is a check,
201
+ never a rewrite: docstrings are never modified by this flag, and it composes
202
+ with every selection. The count covers the docstring's own text — interior
203
+ blank lines do not count, the quote-only opening/closing lines do not. Any
204
+ violation makes the exit code 1.
205
+
206
+ The cap only inspects real docstrings (`body[0]`), so prose relocated one
207
+ statement below the thing it describes is invisible to it. That is the hole
208
+ `orphan-strings` closes: such a string is deleted outright rather than
209
+ measured.
210
+
211
+ Performance: files are processed in parallel with a process pool; stripping
212
+ plus verifying runs at roughly 8 MB of source per second per core (the
213
+ whole 15 MB CPython stdlib takes about two seconds on two cores).
214
+
215
+ ## Library use
216
+
217
+ ```python
218
+ from stifle import DOCSTRINGS, strip_source, verify
219
+
220
+ stripped = strip_source(
221
+ src, {"own-line", "docstrings"}, keep=re.compile(r"KEEP")
222
+ )
223
+ assert verify(src, stripped, {"own-line", "docstrings"}) # do before persisting
224
+ ```
225
+
226
+ `strip_source` is pure (`str -> str`) and raises `ValueError` on unknown
227
+ category names and `SyntaxError` / `tokenize.TokenError` on source it
228
+ cannot process. `verify` recomputes equivalence from scratch; treat a
229
+ `False` (or any exception) as "do not use the result".
230
+
231
+ ## Development
232
+
233
+ ```console
234
+ $ uv run --group dev pytest
235
+ ```
@@ -0,0 +1,89 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "stifle"
7
+ version = "1.0.0"
8
+ description = "Strip comments (and optionally docstrings) from Python code — fast, and provably incapable of touching anything else."
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = "MIT"
12
+ keywords = ["comments", "docstrings", "strip", "cleanup", "source"]
13
+ classifiers = [
14
+ "Development Status :: 5 - Production/Stable",
15
+ "Environment :: Console",
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Topic :: Software Development :: Quality Assurance",
22
+ ]
23
+ [project.urls]
24
+ Repository = "https://github.com/byzantime/stifle"
25
+
26
+ [project.scripts]
27
+ stifle = "stifle._cli:main"
28
+
29
+ [dependency-groups]
30
+ dev = ["pytest>=7", "black", "ruff>=0.4"]
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/stifle"]
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
37
+
38
+ [tool.black]
39
+ line-length = 80
40
+ target-version = ["py312"]
41
+
42
+ [tool.ruff]
43
+ line-length = 80
44
+ target-version = "py312"
45
+
46
+ [tool.ruff.lint]
47
+ # E/F/I/C90 are the baseline. The rest are named rule-by-rule rather than by
48
+ # family: each one below was measured against this codebase, every finding it
49
+ # had was fixed, and it earns its place by catching a way agent-written code
50
+ # goes wrong that review reliably misses.
51
+ select = [
52
+ "E",
53
+ "F",
54
+ "I",
55
+ "C90",
56
+ # Detached asyncio task nobody holds a reference to. The garbage collector
57
+ # may cancel it mid-flight; this is the same bug class as the post-run write
58
+ # race, and it is invisible until a task silently doesn't happen.
59
+ "RUF006",
60
+ # Closure over a loop variable. Reads correct, runs with the last
61
+ # iteration's value.
62
+ "B023",
63
+ # try/except/pass with no logging, i.e. precisely where a failure needs to
64
+ # be findable.
65
+ "S110",
66
+ # Re-raise inside except without `from`, which drops the original traceback.
67
+ "B904",
68
+ # Module-level global rebound at runtime. In a suite that shares one process
69
+ # across the whole session, this is where cross-test leaks come from.
70
+ "PLW0603",
71
+ # Mutable class attribute with no ClassVar: shared state that reads as a
72
+ # per-instance default.
73
+ "RUF012",
74
+ # zip() with no strict=, so a length mismatch silently truncates.
75
+ "B905",
76
+ # noqa that no longer suppresses anything — a stale claim about the code.
77
+ "RUF100",
78
+ # try/except/pass that contextlib.suppress states more directly.
79
+ "SIM105",
80
+ ]
81
+ ignore = ["F402"]
82
+ dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
83
+
84
+ [tool.ruff.lint.mccabe]
85
+ max-complexity = 10
86
+
87
+ [tool.ruff.lint.isort]
88
+ known-first-party = ["stifle"]
89
+ force-single-line = true
@@ -0,0 +1,38 @@
1
+ """stifle — delete comments (and optionally docstrings) from Python code.
2
+
3
+ Library use::
4
+
5
+ from stifle import ALL_TARGETS, strip_source, verify
6
+
7
+ stripped = strip_source(src, {"own-line"})
8
+ assert verify(src, stripped, {"own-line"})
9
+ """
10
+
11
+ from stifle._core import ALL_TARGETS
12
+ from stifle._core import DEFAULT_KEEPS
13
+ from stifle._core import DOCSTRINGS
14
+ from stifle._core import ORPHAN_STRINGS
15
+ from stifle._core import OWN_LINE
16
+ from stifle._core import TARGETS
17
+ from stifle._core import TRAILING
18
+ from stifle._core import DocstringViolation
19
+ from stifle._core import docstring_violations
20
+ from stifle._core import strip_source
21
+ from stifle._core import verify
22
+
23
+ __version__ = "1.0.0"
24
+
25
+ __all__ = [
26
+ "OWN_LINE",
27
+ "TRAILING",
28
+ "DOCSTRINGS",
29
+ "ORPHAN_STRINGS",
30
+ "ALL_TARGETS",
31
+ "TARGETS",
32
+ "strip_source",
33
+ "verify",
34
+ "DEFAULT_KEEPS",
35
+ "DocstringViolation",
36
+ "docstring_violations",
37
+ "__version__",
38
+ ]
@@ -0,0 +1,4 @@
1
+ from stifle._cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())