codesnake 1.2.1__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CodeSnake 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.
@@ -0,0 +1,377 @@
1
+ Metadata-Version: 2.4
2
+ Name: codesnake
3
+ Version: 1.2.1
4
+ Summary: Semantic code checker for Python 3
5
+ Author: CodeSnake Contributors
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/bitWarrior/codesnake
8
+ Project-URL: Repository, https://github.com/bitWarrior/codesnake
9
+ Project-URL: Issues, https://github.com/bitWarrior/codesnake/issues
10
+ Keywords: linter,static-analysis,ast,security
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Software Development :: Quality Assurance
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Provides-Extra: tools
24
+ Requires-Dist: bandit>=1.9.3; extra == "tools"
25
+ Requires-Dist: flake8>=7.3.0; extra == "tools"
26
+ Requires-Dist: isort>=7.0.0; extra == "tools"
27
+ Requires-Dist: mypy>=1.19.1; extra == "tools"
28
+ Requires-Dist: pylint>=4.0.4; extra == "tools"
29
+ Provides-Extra: dev
30
+ Requires-Dist: codesnake[tools]; extra == "dev"
31
+ Dynamic: license-file
32
+
33
+ # CodeSnake
34
+
35
+ [![CI](https://github.com/bitWarrior/codesnake/actions/workflows/ci.yml/badge.svg)](https://github.com/bitWarrior/codesnake/actions/workflows/ci.yml)
36
+ [![PyPI](https://img.shields.io/pypi/v/codesnake.svg)](https://pypi.org/project/codesnake/)
37
+ [![Python](https://img.shields.io/pypi/pyversions/codesnake.svg)](https://pypi.org/project/codesnake/)
38
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
39
+
40
+ Semantic code checker for Python 3. It parses files into an AST, walks them, and reports security problems, common bugs, unused names, and complexity smells.
41
+
42
+ Two properties set it apart from the fast general-purpose linters:
43
+
44
+ - **It never imports or executes the code it analyzes.** Everything runs on the AST from `ast.parse`, so pointing it at untrusted Python — a fork's pull request, a submitted plugin — does not run that Python.
45
+ - **It has no runtime dependencies.** Standard library only, so it vendors cleanly, works air-gapped, and adds nothing to your supply chain.
46
+
47
+ It also does light **taint tracking**: `eval()` on a literal is `info`, `eval()` on something derived from `input()` or `request.args` is an `error`. See [how it compares](#how-it-compares) to Ruff, Bandit, and pylint.
48
+
49
+ Requires **Python 3.10+**.
50
+
51
+ ## Install
52
+
53
+ ```bash
54
+ python3 -m venv codesnake-venv
55
+ source codesnake-venv/bin/activate
56
+ pip install -e .
57
+ ```
58
+
59
+ Optional companion tools (pylint, flake8, mypy, bandit, isort):
60
+
61
+ ```bash
62
+ pip install -e ".[tools]"
63
+ ```
64
+
65
+ Or run `./setup.sh`, which creates the venv and installs from `pyproject.toml`.
66
+
67
+ After an editable install, `codesnake` is on `PATH`. You can also run:
68
+
69
+ ```bash
70
+ python -m codesnake check file.py # after install
71
+ PYTHONPATH=src python -m codesnake file.py # straight from a checkout
72
+ ./codesnake.sh file.py # creates/activates codesnake-venv/
73
+ ```
74
+
75
+ ## First run on an existing codebase
76
+
77
+ CodeSnake reports complexity, length, and unused-name findings by default, so the
78
+ first run on a mature codebase is loud — expect roughly ten warnings per file. That
79
+ is a backlog, not an emergency: only `error` severity fails the run. Start narrow and
80
+ widen when you are ready.
81
+
82
+ ```bash
83
+ # 1. What would actually fail CI. Start here.
84
+ codesnake check --severity error src/
85
+
86
+ # 2. Snapshot everything else, so CI only fails on NEW findings.
87
+ codesnake check --update-baseline .codesnake-baseline.json src/
88
+ git add .codesnake-baseline.json
89
+
90
+ # 3. From now on, this is your CI command.
91
+ codesnake check --baseline .codesnake-baseline.json src/
92
+ ```
93
+
94
+ Then tune thresholds in `.codesnake.json` and shrink the baseline as you go. The full
95
+ adoption path is in [docs/INTEGRATIONS.md](docs/INTEGRATIONS.md#adopting-codesnake-on-an-existing-codebase).
96
+
97
+ ## Usage
98
+
99
+ ```bash
100
+ # Files or directories (walks *.py, skips venvs, caches, and .gitignore)
101
+ codesnake check src/codesnake/checker.py test/example_bad_code.py
102
+ codesnake check src/
103
+
104
+ # Same thing without the subcommand
105
+ codesnake src/
106
+
107
+ # JSON for CI
108
+ codesnake check --format json --no-color src/
109
+
110
+ # Errors only
111
+ codesnake check --severity error src/
112
+
113
+ # Merge Bandit findings (needs pip install -e ".[tools]")
114
+ codesnake check --bandit src/
115
+
116
+ # Only Python files staged in git
117
+ codesnake check --staged
118
+
119
+ # Snapshot findings, then fail only on new ones
120
+ codesnake check --update-baseline .codesnake-baseline.json src/
121
+ codesnake check --baseline .codesnake-baseline.json src/
122
+
123
+ # Custom thresholds
124
+ codesnake check --config .codesnake.json src/
125
+
126
+ # Write a default config file (refuses to overwrite; pass --force to replace)
127
+ codesnake config -o .codesnake.json
128
+ ```
129
+
130
+ ### CLI flags
131
+
132
+ | Flag | Meaning |
133
+ |---|---|
134
+ | `--config PATH` | `.codesnake.json` or a `pyproject.toml` with `[tool.codesnake]` (otherwise the nearest one found walking up to the repository root, else defaults) |
135
+ | `--format text\|json\|github\|sarif` | Report format (default `text`) |
136
+ | `--severity error\|warning\|info` | Minimum severity to print |
137
+ | `--no-color` | Disable ANSI color (`NO_COLOR` also works) |
138
+ | `--bandit` | Merge Bandit results when the `bandit` executable is installed |
139
+ | `--staged` | Check `git diff --cached` Python files only |
140
+ | `--baseline FILE` | Hide issues whose fingerprint is already in the baseline |
141
+ | `--update-baseline FILE` | Write the current finding set as a baseline |
142
+ | `-j`, `--jobs N` | Worker processes (default: auto — one per CPU once 8+ files are checked; `1` disables) |
143
+
144
+ `--staged` needs no file arguments and works from any directory inside the repository (paths from git are resolved against the repo root). With no staged `.py` files it exits **0**. `--baseline` fingerprints are `filename|code|message-with-numbers-normalized|occurrence`, so line-only edits and count changes (`52 lines long` → `53 lines long`) do not re-fail CI, while a *second* identical violation in the same file still does. Version-1 baselines are read transparently; `--update-baseline` writes version 2. A missing baseline file fails closed (exit 1).
145
+
146
+ ### Output formats
147
+
148
+ | `--format` | Use |
149
+ |---|---|
150
+ | `text` (default) | Human-readable; color when stdout is a TTY; includes a one-line suggestion |
151
+ | `json` | Per-file issues plus a summary (`end_line`, `end_col`, `suggestion`, `source`) |
152
+ | `github` | GitHub Actions workflow commands (`::error file=...,line=...,col=...,title=...::message`), properly `%`-escaped |
153
+ | `sarif` | SARIF 2.1.0 with rule metadata (`helpUri`, default level, suggestion) and repo-relative URIs for code-scanning dashboards |
154
+
155
+ ### Exit codes
156
+
157
+ | Code | Meaning |
158
+ |---|---|
159
+ | `0` | No error-severity issues (or `--staged` with no staged Python files) |
160
+ | `1` | At least one error, I/O failure, syntax error, bad config/baseline, or git failure |
161
+ | `2` | CLI usage error (unknown flag, or `check` with neither files nor `--staged`) |
162
+
163
+ Missing files, empty directories, and decode failures are **IO001**. Syntax errors are **SYN001**. Those always fail the run; they are never printed as “no issues found.”
164
+
165
+ ## What it checks
166
+
167
+ | Code | Severity | What |
168
+ |---|---|---|
169
+ | **SEC001** | info / error | `eval()` / `exec()` — **info** on a constant, **error** on untrusted input |
170
+ | **SEC002** | warning | Unsafe deserialization: `pickle` / `dill` / `cloudpickle` / `jsonpickle` loads, `pickle.Unpickler(...).load()`, `marshal.load(s)`, `shelve.open`, and `yaml.load` without a safe `Loader` |
171
+ | **SEC003** | warning / error | `subprocess` with `shell=True`, or `os.system` / `os.popen` / `subprocess.getoutput` (**error** if the command is untrusted) |
172
+ | **SEC004** | warning | `subprocess` command (`run`, `call`, `Popen`, `check_call`, `check_output`) built from untrusted input |
173
+ | **BUG001** | error | Mutable default arguments (`[]`, `{}`, `set()`, `list()`, kw-only, `lambda`, `async def`) |
174
+ | **BUG002** | warning | Duplicate key in a dict literal, including tuple keys |
175
+ | **EXC001** | warning | Bare `except:` |
176
+ | **EXC002** | info | `except Exception`, including `builtins.Exception` and tuple clauses like `except (ValueError, Exception)` |
177
+ | **EXC003** | warning | Empty `except` body (`pass`) |
178
+ | **EXC004** | warning | `raise Exception()` with no message |
179
+ | **EXC005** | warning | `raise NewError(...)` inside `except` / `except*` without `from` |
180
+ | **COMP001** | warning | Too many parameters |
181
+ | **COMP002** | warning | Cyclomatic complexity too high (nested functions are not charged to the parent) |
182
+ | **COMP003** | warning | Function longer than the configured maximum |
183
+ | **COMP004** | warning | Class with too many methods |
184
+ | **COMP005** | warning | Too many `self.*` **assignments** in `__init__` (method calls are ignored) |
185
+ | **PERF001** | info | `for i in range(len(...))` |
186
+ | **STYLE001** | info | `is True` / `is False` |
187
+ | **IMP001** | warning | `from module import *` |
188
+ | **IMP002** | warning | Imported name is never used (module level or inside a function) |
189
+ | **IMP003** | error | Relative import of a name the sibling module does not define |
190
+ | **VAR001** | warning | Unused local or nested function (loop targets, tuple unpacking, bare annotations, and decorated nested functions are exempt) |
191
+ | **VAR002** | warning | Unused argument (`self` / `cls`, `_`-prefixed names, `*args` / `**kwargs`, lambda and dunder-method parameters, and abstract/stub bodies are skipped) |
192
+ | **VAR003** | info | Local name shadows an enclosing function binding |
193
+ | **REL002** | info | `assert` is stripped under `-O` (skipped in `test_*.py`, `*_test.py`, `conftest.py`, and `test(s)/` directories) |
194
+ | **RES001** | warning | `open()` used without `with` (anything inside a `with` item, `contextlib.closing(...)`, or `stack.enter_context(...)` counts as owned) |
195
+ | **ASY001** | warning | `async def` that never `await`s (stubs and `@abstractmethod` skipped) |
196
+ | **SYN001** | error | Syntax error |
197
+ | **IO001** | error | File missing, not a file, unreadable, or empty directory |
198
+ | **B###** | varies | Bandit test ids, only when `--bandit` / `use_bandit` is on (`source: bandit`) |
199
+
200
+ Call checks resolve imports (`from subprocess import call`, `import pickle as pkl`) instead of matching only the AST shape. `shell=True` is also detected via a local or module constant (`shell = True; run(..., shell=shell)`); reassigning the name invalidates the constant.
201
+
202
+ Function bodies are analyzed after the enclosing scope is fully bound, so a closure that references a variable assigned *after* the `def` does not produce a false "unused variable" warning.
203
+
204
+ Untrusted input (taint) is tracked from `input()`, `sys.argv`, `os.environ` / `os.getenv`, and `args` / `GET` / `POST` / `json` / `form`-style attributes read from a request object (`request`, `req`, `self.request`), including f-strings, `+`, `.format()`, `.get()`, and subscripts. Passing tainted data through `shlex.quote`, `int()`, `re.escape`, `html.escape`, or `urllib.parse.quote` clears the taint.
205
+
206
+ **IMP003** runs when several files are checked together. `from .foo import bar` is an error only if `foo.py` (or `foo/__init__.py`) is in the same run and does not define `bar`. Unused imports inside `if TYPE_CHECKING:` and names listed in `__all__` are not flagged as IMP002.
207
+
208
+ ### Suppressing findings
209
+
210
+ On the same line as the issue:
211
+
212
+ ```python
213
+ eval("1+1") # noqa
214
+ eval("1+1") # noqa: SEC001
215
+ import os # codesnake: ignore
216
+ import os # codesnake: ignore=IMP002
217
+ ```
218
+
219
+ `# noqa` with no codes suppresses every finding on that line.
220
+
221
+ ## Configuration
222
+
223
+ CodeSnake looks for configuration starting in the current directory and walking up to the repository root (the first directory containing `.git`). In each directory a `.codesnake.json` wins over a `pyproject.toml` `[tool.codesnake]` table. `--config PATH` (JSON or TOML) overrides discovery.
224
+
225
+ ```toml
226
+ # pyproject.toml — same keys as the JSON file (reading TOML needs Python 3.11+)
227
+ [tool.codesnake]
228
+ max_complexity = 8
229
+ check_style = false
230
+ ```
231
+
232
+ ```json
233
+ {
234
+ "max_function_length": 50,
235
+ "max_function_params": 7,
236
+ "max_complexity": 10,
237
+ "max_class_methods": 20,
238
+ "max_instance_vars": 10,
239
+ "check_security": true,
240
+ "check_bugs": true,
241
+ "check_exceptions": true,
242
+ "check_complexity": true,
243
+ "check_performance": true,
244
+ "check_imports": true,
245
+ "check_style": true,
246
+ "check_unused": true,
247
+ "check_reliability": true,
248
+ "use_bandit": false,
249
+ "report_errors": true,
250
+ "report_warnings": true,
251
+ "report_info": true
252
+ }
253
+ ```
254
+
255
+ `max_*` thresholds must be 1 or greater; anything lower is a config error. `check_*` turns whole categories off (`check_reliability` covers REL002 and ASY001). `report_*` filters by severity. `use_bandit` merges Bandit when it is installed. A stricter sample lives in `examples/strict.codesnake.json`.
256
+
257
+ ## Library API
258
+
259
+ ```python
260
+ from codesnake import CheckerConfig, SemanticChecker, check_file, run_check
261
+
262
+ issues = SemanticChecker(source, filename="app.py").analyze()
263
+ issues = check_file("app.py") # I/O failures become IO001
264
+ issues = check_file("app.py", source=text) # analyze already-read text
265
+
266
+ config = CheckerConfig(max_complexity=8, check_style=False)
267
+ rc = run_check(
268
+ ["app.py", "pkg/"],
269
+ config=config,
270
+ output_format="json",
271
+ min_severity="warning",
272
+ color=False,
273
+ staged=False,
274
+ baseline_path=".codesnake-baseline.json",
275
+ use_bandit=False,
276
+ )
277
+ ```
278
+
279
+ Each `Issue` includes `line`, `col`, `end_line`, `end_col`, `suggestion`, and `source` (`codesnake` or `bandit`). `col` / `end_col` are **0-based character offsets** (AST byte offsets are converted), and JSON output reports them as-is. The `text`, `github`, and `sarif` formats print **1-based** columns.
280
+
281
+ ## How it compares
282
+
283
+ CodeSnake is not trying to replace Ruff. Use both.
284
+
285
+ | | CodeSnake | Ruff | Bandit | pylint |
286
+ |---|---|---|---|---|
287
+ | Speed, 167 stdlib files | 1.7s | **0.14s** | 9.1s | 17.4s |
288
+ | Rules | ~25 | 800+ | ~70 security | 400+ |
289
+ | Runtime dependencies | **none** | none (Rust binary) | several | several |
290
+ | Imports the analyzed code | **never** | never | never | in some modes |
291
+ | Taint tracking | **yes** | no | limited | no |
292
+ | Autofix | no | **yes** | no | no |
293
+ | SARIF output | **yes** | no | **yes** | no |
294
+ | Baselines | **yes** | no | via `--baseline` | no |
295
+
296
+ **Ruff is roughly 13x faster and has 30x the rules.** If you want one fast
297
+ general-purpose linter with autofix, use Ruff — CodeSnake is not competing for that job.
298
+ Among the Python-implemented checkers, though, CodeSnake is the quick one: about 5x
299
+ faster than Bandit and 10x faster than pylint on the same files.
300
+
301
+ <sub>Measured on Python 3.12, best of 2–3 runs over the same 167 files from the standard
302
+ library, each tool using its own parallelism where it has any (`codesnake` auto,
303
+ `pylint -j 0`). pylint ran with `--disable=all --enable=W,E`, a reduced rule set in its
304
+ favor. Your numbers will differ; the ranking is the point, not the digits.</sub>
305
+
306
+ CodeSnake is worth adding when you want one of these:
307
+
308
+ - **Taint tracking.** `eval(x)` where `x` came from `input()` or `request.args` is an
309
+ `error`; `eval("1+1")` is `info`. The fast linters flag the call site without asking
310
+ where the data came from.
311
+ - **Analysis of untrusted code.** No import, no execution, no dependencies — safe to
312
+ run over a fork's PR or a user-submitted plugin.
313
+ - **A vendorable checker.** One pure-Python package with an empty dependency list,
314
+ auditable in an afternoon, no toolchain.
315
+ - **SARIF plus stable baselines**, for GitHub code scanning on a codebase with an
316
+ existing backlog.
317
+
318
+ Bandit has far broader security coverage; `--bandit` merges its findings into the same
319
+ report if you want both.
320
+
321
+ ## Performance
322
+
323
+ Files are analyzed in a process pool once there are 8 or more of them (one worker per CPU); pass `--jobs 1` for a strictly sequential run or `--jobs N` to pin the count. Output order is always the input order.
324
+
325
+ Roughly 100 files/second single-process on a modern laptop. CodeSnake is pure Python
326
+ doing a full AST walk per file; if analysis time dominates your CI, reach for Ruff.
327
+
328
+ ## Tests
329
+
330
+ ```bash
331
+ python -m unittest discover -s test -p 'test_*.py'
332
+ python test/test_codesnake.py # same suite, with a summary
333
+ ./codesnake-launcher.sh --no-venv --test
334
+ ```
335
+
336
+ CI (`.github/workflows/ci.yml`) runs the suite on Python 3.10–3.13 and then runs `codesnake check --format github src/` on the checker's own source.
337
+
338
+ `test/example_bad_code.py` is a fixture with intentional issues:
339
+
340
+ ```bash
341
+ codesnake check test/example_bad_code.py
342
+ ```
343
+
344
+ ## Project layout
345
+
346
+ ```
347
+ codesnake/
348
+ ├── pyproject.toml # packaging, extras, console script
349
+ ├── LICENSE # MIT
350
+ ├── .codesnake.json # default checker config
351
+ ├── setup.sh # venv + editable install
352
+ ├── codesnake.sh # simple launcher
353
+ ├── codesnake-launcher.sh # flags, --test, --create-venv, --no-venv
354
+ ├── src/codesnake/
355
+ │ ├── __init__.py # public API, __version__
356
+ │ ├── __main__.py # python -m codesnake
357
+ │ ├── _version.py # the one place the version lives
358
+ │ ├── checker.py # SemanticChecker, config, discovery, formats, run_check
359
+ │ ├── cli.py # check / config / version
360
+ │ └── banner.py
361
+ ├── test/ # unittest suite + fixture
362
+ ├── examples/ # strict.codesnake.json
363
+ ├── docs/ # INTEGRATIONS, BASH_SCRIPTS_GUIDE, PROJECT_STRUCTURE
364
+ └── .github/workflows/ci.yml
365
+ ```
366
+
367
+ Releasing: bump `__version__` in `src/codesnake/_version.py`; `pyproject.toml` reads it dynamically.
368
+
369
+ ## Further reading
370
+
371
+ - [`docs/INTEGRATIONS.md`](docs/INTEGRATIONS.md) — pre-commit hooks, GitHub Actions (annotations, SARIF upload, baselines), VS Code, Makefile, adopting CodeSnake on an existing codebase, and how it overlaps with flake8/Bandit/pylint.
372
+ - [`docs/BASH_SCRIPTS_GUIDE.md`](docs/BASH_SCRIPTS_GUIDE.md) — `setup.sh`, `codesnake.sh`, and `codesnake-launcher.sh`, and the virtual environment they manage.
373
+ - [`docs/PROJECT_STRUCTURE.md`](docs/PROJECT_STRUCTURE.md) — module map, how to add a rule, how to release.
374
+
375
+ ## License
376
+
377
+ MIT