# SlopGuard (Python): complete interface reference (agent-ingestible) slopguard-python measures complex, undertested code in Python projects. It computes a weighted CRAP (Change Risk Anti-Patterns) score combining cyclomatic and cognitive complexity with line coverage, and prints a structured report you can pipe into jq or fail CI on. It is the Python sibling of slopguard-go, slopguard-swift, slopguard-typescript and slopguard-kotlin: same formula, same JSON schema, same UX. Status: alpha (v0.1.x). The analyzer is stable and self-tested; the CLI surface and JSON schema may move before v1.0. License: MIT. Repo: https://github.com/JeevanThandi/slopguard-python ## Why this matters for coding agents Autonomous/background agents iterate by changing code and running tests. Two codebase properties decide how well that loop works: 1. Coverage: an agent editing untested code cannot tell a fix from a regression. Coverage is the agent's only feedback signal. 2. Complexity: deeply nested, many-branched methods are where agents (like humans) make mistakes. wCRAP captures both in one number per method. Use it as a CI gate (agents cannot merge new slop) or as a work queue (feed the JSON to an agent: "get every method under 30"). ## The formula wCRAP(m) = (cyc × cog) × (1 − cov/100)³ + sqrt(cyc × cog) - cyc: cyclomatic complexity (McCabe), parsed via the standard-library ast. Counts if/elif, for/while, each except, the ternary, each and/or, comprehension for/if clauses, and match cases — comparable to mccabe. - cog: cognitive complexity (SonarSource 2023 spec): penalises nesting, charges a whole match once, ignores early-exit shapes (plain return/break/continue). - cov: line coverage percentage in [0, 100], gathered by slopguard-python itself by driving the project's own test suite (pytest or unittest) under coverage.py and reading the `coverage json` report. Never user-supplied. It cannot be hand-fed or faked; the only way to lower a score without refactoring is to write tests that actually execute the code. - Semantics: cov = 100 → score collapses to sqrt(cyc × cog) (complexity floor). cov = 0 → full quadratic penalty (cyc × cog) + sqrt(cyc × cog). The cubed factor sharply rewards partial coverage: 50% coverage removes 87.5% of the risk term. - Inputs are clamped (complexity ≥ 0, coverage in [0,100]); score is always ≥ 0. - Default "crappy" threshold: 30 (matches the original CRAP paper). - Weighting rationale: a flat 50-branch dispatch (cyc≈50, cog=1) scores like a small method; a deeply nested 3-branch tangle (cyc=3, cog=12) scores like medium-complex code. ## Install pip install slopguard-python …or from source: git clone https://github.com/JeevanThandi/slopguard-python.git cd slopguard-python pip install . Requirements: Python 3.9+. Zero runtime dependencies — Python standard library only (ast, argparse, json). Run it from your project's own environment (the one your tests run in) so coverage gathering can import your package and its tests. coverage.py lives in that environment (like pytest), not in the tool. You can also run it as a module: `python -m slopguard analyze --path ./src`. ## Subcommands | Command | Purpose | |---|---| | analyze | Walk a directory of Python sources, drive the test suite (pytest/unittest under coverage.py) for coverage, emit a wCRAP report (text or JSON). Default subcommand. | | version | Print version metadata as JSON. | `analyze` is the default and `--path` defaults to the current directory; a bare `slopguard-python` in a project root just works. ## analyze: all flags | Flag | Default | Meaning | |---|---|---| | -p, --path | . | Directory of Python sources, or a single .py file. | | -t, --threshold | 30 | wCRAP score above which a method/type is flagged isCrappy. | | --runner | auto-detected | Test runner to drive for coverage. Auto-detected from the project when omitted. | | --project-dir | nearest project root | Directory the test run executes in. Walked up from --path (pyproject.toml / setup.py / setup.cfg / .git) when omitted. | | --no-coverage | off | Skip the test run; complexity only. Every method reads 0% coverage (worst-case scores). | | --coverage-file | — | A `coverage json` report to join instead of running tests. For CI that already produced one. | | --include | — | Glob(s) of files to include. Repeatable. | | --exclude | — | Extra exclude glob(s), combined with built-in defaults (.venv, __pycache__, test_*.py, *_test.py, tests/, conftest.py, generated stubs). | | --no-default-excludes | off | Start with an empty exclude list (e.g. to analyze test code itself). | | --json | off | Emit JSON to stdout (default is pretty text). | | --fail-over | — | Exit code 2 if any method's wCRAP exceeds this value. The CI gate. | | -v, --verbose | off | Stream test-runner output and subprocess chatter to stderr. | | --quiet | off | Silence all stderr progress chatter. Wins over --verbose. Stdout unaffected. | ## Exit codes and streams | Code | Meaning | |---|---| | 0 | Analysis completed; nothing exceeded --fail-over (or flag not passed). | | 1 | Analysis error (bad path, test/coverage failure, parse error). With --json a structured error envelope goes to stderr. | | 2 | Quality gate tripped: at least one method's wCRAP exceeds --fail-over. | Stream discipline: the report (text or JSON) goes to stdout; progress markers ("slopguard: running pytest under coverage…") go to stderr. Piped stdout is always clean. ## How coverage works Coverage is an artifact of the analysis, not an input, mirroring how slopguard-swift drives `xcodebuild test` and slopguard-typescript drives vitest/jest. slopguard-python drives the project's own test suite under coverage.py: 1. Project discovery. Walk up from --path to the nearest project root (pyproject.toml / setup.py / setup.cfg / .git). Override with --project-dir. 2. Runner detection. Prefer pytest when the project shows pytest signals (a pytest.ini/conftest.py, a [tool.pytest.ini_options] / [tool:pytest] block, or pytest in the deps); otherwise fall back to stdlib unittest. Override with --runner. 3. Test run. Drive the suite under coverage.py — `python -m coverage run --source= -m pytest` (or `-m unittest discover`) — into a slopguard-owned temp directory, then `coverage json`. Failing tests don't abort (partial coverage is still useful — a note is attached); a run that produces no usable coverage with a non-zero exit aborts with the output tail. 4. Join. Parse the `coverage json` report into a per-line index, resolve its file paths to disk (basename + longest-suffix fallback for CI-vs-local path mismatches), join per-method line coverage onto the parsed declarations, then delete the temp dir. A `coverage json` report is the universal Python interchange format — anything you can run under coverage produces one — so a report your CI already generated is supported via --coverage-file. ## What counts as a method Top-level functions, methods, constructors (__init__), and property getters/setters. Named nested defs get their own entry; anonymous lambdas don't — their branches count toward the enclosing method, with a cognitive nesting bump for the lambda body, per the Sonar spec. Methods attach to their lexically enclosing class (like the Swift/Kotlin/TypeScript ports), so nested classes get qualified names: Outer.Inner.method. Default excludes: .venv/, caches (__pycache__, .mypy_cache, …), test files and dirs (test_*.py, *_test.py, tests/, conftest.py), generated stubs (*_pb2.py) and anything carrying an @generated / DO NOT EDIT header. Analyze excluded code with --no-default-excludes. ## JSON report schema (schemaVersion "2", shared with the sibling ports) Top level: schemaVersion, generatedAt, coverageAvailable, summary, methods[], types[]. JSON keys are sorted alphabetically and whole floats render without a trailing .0, for diff-stable CI output. summary: - files, types, methods counts + crappy counts - avgCrap, maxCrap, average cyclomatic/cognitive/weighted complexity - weighted coverage across analyzed methods (when gathered) - coverageAvailable: false under --no-coverage or when no coverage data was found methods[], every analyzed function/method/constructor/accessor: - id: STABLE identifier "file#qualifiedName@line". Track methods across runs with this. - qualifiedName: "Type.method" for methods, the function name for free functions - file, line, endLine: location relative to analyzed root - kind: function | method | constructor | getter | setter - typeName: enclosing class name (lexical), null for free functions - complexity: cyclomatic (McCabe) - cognitiveComplexity: SonarSource 2023 - weightedComplexity: sqrt(cyc × cog) - coverage: line coverage percent 0–100 - crap: the wCRAP score - isCrappy: true when crap > threshold types[], per-class aggregation (rolled up by lexical nesting): - aggregatedCrap: formula applied to the type's totals (total burden, comparable across types) - maxCrap: worst single-method offender ("biggest fire") - isCrappy, weighted coverage, method counts Example (real output from slopguard-python analyzing its own sources, --no-coverage): { "qualifiedName": "_run_analyze", "file": "src/slopguard/cli.py", "line": 116, "kind": "function", "typeName": null, "complexity": 14, "cognitiveComplexity": 18, "weightedComplexity": 15.87, "coverage": 0, "crap": 267.87, "isCrappy": true, "id": "src/slopguard/cli.py#_run_analyze@116" } ## Quickstart commands # Zero-config: analyze the current project (auto-finds and runs its tests for coverage) slopguard-python # Scan a specific directory and print the top crappy methods slopguard-python analyze --path ./src --threshold 30 # Name the test runner instead of auto-detecting it slopguard-python analyze --path ./src --runner pytest # Full JSON for CI / downstream tooling slopguard-python analyze --path . --json | jq '.methods | sort_by(-.crap)[:10]' # Fail CI when any method's CRAP exceeds 50 slopguard-python analyze --path . --fail-over 50 # Complexity only (skip the test run) slopguard-python analyze --path ./src --no-coverage # Join coverage CI already produced (a `coverage json` report) slopguard-python analyze --path . --coverage-file coverage.json # Or run it as a module python -m slopguard analyze --path ./src ## Agent integration ### CLAUDE.md / AGENTS.md drop-in ## Quality gate: SlopGuard Before declaring any Python task complete, run: slopguard-python analyze --path . --json --quiet - The report is JSON on stdout; `.methods[]` entries with `isCrappy: true` exceed the wCRAP threshold (30). - If your change ADDED a crappy method, refactor it or cover it with tests until its `crap` score is below 30. Coverage is gathered by the tool itself by driving your test suite (pytest/unittest under coverage.py). Write real tests; the number cannot be hand-fed. - CI runs `--fail-over 50` and will reject the PR at exit code 2. - Full interface reference: https://www.slopguard.dev/llms-full.python.txt ### Recipes # Agent work queue: worst methods first, with stable IDs slopguard-python analyze --json --quiet \ | jq '[.methods[] | select(.isCrappy)] | sort_by(-.crap) | map({id, crap, coverage, file, line})' # Coverage gaps: complex code no test touches slopguard-python analyze --json --quiet \ | jq '.methods[] | select(.complexity >= 5 and .coverage <= 50)' # Regression check around an agent change before=$(slopguard-python analyze --json --quiet | jq '.summary.maxCrap') # … agent edits code … after=$(slopguard-python analyze --json --quiet | jq '.summary.maxCrap') # Fast pre-commit sweep, no test run (use a high fail-over: coverage reads 0%) slopguard-python analyze --no-coverage --quiet --fail-over 200 ### CI gate (GitHub Actions) name: slop-gate on: [pull_request] jobs: slopguard: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: { python-version: '3.12' } - name: Install deps + slopguard-python run: pip install -e . coverage pytest slopguard-python - name: Gate on wCRAP run: slopguard-python analyze --path . --fail-over 50 (coverage.py and your test runner must be importable in the same environment slopguard-python runs in.) Threshold guidance: flag at 30 (classic CRAP paper threshold); hard-fail CI at 50. Under --no-coverage all methods read 0% coverage, so use a much higher fail-over (e.g. 200) for complexity-only sweeps. ## Security / supply-chain posture - Zero runtime dependencies — Python standard library only (ast, argparse, json). - The only subprocess slopguard-python spawns is your project's own test suite under coverage.py. - No network, no telemetry, no source mutation. - MIT licensed. - Dogfooded: CI holds test coverage ≥95% and asserts the analyzer stays under its own threshold against its own sources and the sample-apps/ fixtures. ## Library use Everything the CLI does is importable: from slopguard.coverage import run, CoverageSource from slopguard import default_analysis_options, json_report report = run("./src", CoverageSource(), threshold=30.0, options=default_analysis_options()) print(json_report(report)) For complexity-only analysis with no I/O, slopguard.analyze_source(source, "file.py") returns the per-file metrics directly.