gaze-py is a Python-native port of gaze, the GazeCRAP analysis engine. It detects observable side effects in Python functions using AST-only static analysis (no code execution, no imports of analysed modules), classifies each effect as contractual or incidental using a five-signal confidence engine, and computes CRAP and GazeCRAP scores to surface functions that are both complex and under-tested. The output is schema-compatible with the Go gaze implementation.
Documentation — concepts, getting-started guide, CLI reference, configuration, glossary.
- Python 3.11+
# Run without installing (recommended for one-off use) uvx --from gaze-py gazepy --help # Permanent install uv tool install gaze-py # Or with pip pip install gaze-py
# Analyse a source directory (CRAP will be null — no coverage provided) gazepy analyze src/ # JSON output (default) gazepy analyze src/ --format=json # Human-readable text output (one line per function) gazepy analyze src/ --format=text # CRAP scoring — auto-runs pytest for coverage gazepy crap src/ # CRAP scoring with a pre-generated coverage report gazepy crap src/ --coverprofile cov.json # Assess test quality and compute GazeCRAP scores gazepy quality src/ # Scaffold OpenCode agent and command files into .opencode/ gazepy init
CRAP scoring requires line coverage data. The crap command can collect coverage
automatically by running pytest, or accept a pre-generated coverage.py JSON report:
# Auto-run pytest and collect coverage (requires pytest-cov) gazepy crap src/ # Use a pre-generated coverage report (recommended in CI to avoid a double test run) pytest --cov=your_package --cov-report=json:cov.json gazepy crap src/ --coverprofile cov.json # Fail CI if crapload exceeds a threshold gazepy crap src/ --max-crapload 30
When coverage is provided, the line_coverage and crap fields are populated in the
output. When omitted, those fields are null (not 0.0) — null means "not measured",
not "zero coverage".
The analyze command detects side effects only — it does not compute CRAP scores.
Use gazepy crap for CRAP scoring.
gazepy quality runs the full O1 pipeline: pairs test functions to their production
targets, detects assertion sites, maps assertions to detected side effects, and computes
GazeCRAP using contract coverage (the fraction of contractual effects covered by tests).
# Assess test quality — auto-discovers tests/ directory gazepy quality src/ # Explicit tests directory gazepy quality src/ --tests tests/ # JSON output gazepy quality src/ --format=json # Fail CI if average contract coverage drops below a threshold gazepy quality src/ --min-contract-coverage 80
Assert the returned value and emitted content separately so quality reports can
distinguish the two contracts. For example, put this function in example.py:
def run_example(): print("ready") return 0
In test_example.py, import the function explicitly so the mapper can identify
the source of the captured output:
from example import run_example def test_run_example(capsys): result = run_example() assert result == 0 stdout, _ = capsys.readouterr() assert stdout == "ready\n"
The mapper recognizes capsys and capfd fixture parameters, tuple unpacking,
saved capture results such as captured.out, directly assigned
stdout = capsys.readouterr().out, and inline captured-stream assertions.
Assertions on .out cover stdout; assertions on .err cover stderr.
For JSON output, use an unshadowed import json and
payload = json.loads(captured.out), with exactly one positional argument and
no keywords. Read-only indexing and equality or membership assertions retain
the stream provenance. Aliases of json, imported loads functions, decoding
hooks, arbitrary methods and helper transformations are outside this supported
subset. Mutating a parsed value or any nested descendant, or passing it to an
unknown helper, invalidates provenance for the entire parsed value and its aliases.
Calls need a resolvable, unshadowed import that identifies the exact target module and function. Direct function imports and supported module imports can establish that identity; a matching bare name or method name is not enough. Package identity uses the import root, not a source-path suffix. Unresolved imports and namespace packages without an authoritative import root remain uncovered.
Capture reads drain the buffer. A later read without another target call does not verify the earlier output; a saved captured value remains usable until it is overwritten. Keep the target call and capture in a clear sequence. The mapper leaves uncertain provenance uncovered, including unsupported control flow, unrelated producers, arbitrary helper transformations and nested definitions. A conditional capture read can consume output and invalidate pending attribution; it cannot establish a clean buffer. Only a definite read followed by a supported target call can restore that isolation. It analyzes syntax and never executes analyzed code.
Return bindings are checked at the assertion's position. Reassigning a return variable to a capture does not preserve return credit. A live return keeps precedence when mixed with a supported captured stream, but an assertion that mixes a return with unsupported capture provenance remains unmapped. Keep those assertions separate to make both contracts visible.
When a test uses a context manager, isolate its lifecycle from the output being tested: drain the fixture after entering the context, call the target, and save the captured output before leaving the context. Assertions can inspect that saved snapshot afterward. A fresh capture after context exit may include unrelated output and is left uncovered. Concurrent/background writers cannot be attributed by this bounded static analysis.
See Understanding the output for the measured fields and the changelog for mapping corrections.
Each function in the output includes:
| Field | Description |
|---|---|
side_effects |
List of detected observable side effects with type, tier, and classification |
complexity |
McCabe cyclomatic complexity |
line_coverage |
Fraction of lines covered (0.0–1.0), or null if not provided |
crap |
CRAP score (complexity² × (1 − coverage)³ + complexity), or null |
gaze_crap |
GazeCRAP score using contract coverage; populated by gazepy quality |
quadrant |
Q1–Q4 classification based on CRAP and GazeCRAP; populated by gazepy quality |
fix_strategy |
Recommended action: add_tests, add_assertions, decompose_and_test, or decompose |
contract_coverage |
Fraction of contractual effects covered by tests; populated by gazepy quality |
The summary section includes recommended_actions — up to 20 functions sorted by
priority (add_tests → decompose_and_test → decompose) that exceed the CRAP threshold.
gazepy report can pass the analysis JSON to a local or cloud AI provider to generate
a narrative interpretation. Configure your provider in .gaze.yaml:
ai: provider: ollama # or: vertex model: llama3.2:3b # any Ollama model, or a Vertex Claude model ID endpoint: http://localhost:11434 # Ollama only; ignored for Vertex timeout: 120 # seconds per HTTP request (default: 120)
For Google Vertex AI (Claude models):
ai: provider: vertex model: claude-sonnet-4-6 project: my-gcp-project region: us-east5
Environment variable overrides (higher precedence than config file):
| Variable | Description |
|---|---|
GAZEPY_AI_PROVIDER |
Provider name (ollama or vertex) |
GAZEPY_AI_MODEL |
Model ID (setting only this implies ollama) |
GAZEPY_AI_ENDPOINT |
Ollama base URL (default: http://localhost:11434) |
GAZEPY_AI_PROJECT |
GCP project ID (Vertex only) |
GAZEPY_AI_REGION |
GCP region (Vertex only) |
GAZEPY_AI_TIMEOUT |
HTTP timeout in seconds |
Prerequisites:
- Ollama: install from ollama.com and pull your model (
ollama pull llama3.2:3b) - Vertex: install the Google Cloud SDK and run
gcloud auth application-default login
If no provider is configured, gazepy report emits the raw analysis JSON to stdout and a tip to stderr.
Migration from --ai flag: the --ai and --ai-timeout flags have been removed.
Configure your provider in .gaze.yaml instead (see above).
| Symptom | Likely cause | Fix |
|---|---|---|
gcloud auth print-access-token failed (exit 1) |
Not authenticated | Run gcloud auth application-default login |
vertex provider requires gcloud CLI |
gcloud not on PATH | Install the Google Cloud SDK |
unexpected response format from gcloud auth print-access-token |
Outdated gcloud | Run gcloud components update |
Ollama request timed out after Ns |
Model is slow or timeout too short | Increase ai.timeout in .gaze.yaml (e.g. timeout: 300) |
Ollama request failed: Connection refused |
Ollama not running | Start Ollama: ollama serve |
Ollama returned HTTP 404 |
Model not pulled | Pull the model: ollama pull llama3.2:3b |
Vertex AI rate limited after 5 retries |
API quota exceeded | Wait and retry, or reduce request frequency |
Vertex AI returned HTTP 401 after token refresh |
Credentials expired | Run gcloud auth application-default login again |
Warning: ollama provider configured but not available |
Model not pulled or Ollama not running | Pull the model or start Ollama; report falls back to prompt-only mode |
Invalid value for GAZEPY_AI_TIMEOUT |
Non-integer or non-positive timeout | Set GAZEPY_AI_TIMEOUT to a positive integer (e.g. 120) |
- Bump
versioninpyproject.tomland__version__insrc/gaze_py/__init__.pyin a PR. Merge tomain. - Go to GitHub Actions → Release → Run workflow.
- Enter the tag matching the version (e.g.
v0.3.0). - Approve the
pypienvironment gate if configured. - The workflow validates, tags, builds, and publishes automatically.