gaze-py
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.
Requirements
- Python 3.11+
Installation
# 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
Basic usage
# 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 with gazepy crap
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.
Quality assessment with gazepy quality
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 captured output
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.
Understanding the output
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.
AI Reports
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).
Troubleshooting
| 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) |
Releasing
Releasing a new version
- 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.
Metadata
Release files for gaze-py 0.9.3
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| gaze_py-0.9.3.tar.gz | 963.9 kB | With provenance | Details for gaze_py-0.9.3.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| gaze_py-0.9.3-py3-none-any.whl | Python 3 | none | any | Details for gaze_py-0.9.3-py3-none-any.whl |
Total release size: 1.1 MB
Release files / gaze_py-0.9.3.tar.gz
| Download URL | gaze_py-0.9.3.tar.gz |
|---|---|
| Size | 963.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f2ea9b4798aac56a75cc988ee961c297a481fd9ee33ae502a2f9311526a8854f
|
|
BLAKE2b-256 checksum How to use checksums |
508568504f7645d98ea6546ee7893ddd1e7fa428d1157c85fe1122f22f4c85b3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.
Transparency logRelease files / gaze_py-0.9.3-py3-none-any.whl
| Download URL | gaze_py-0.9.3-py3-none-any.whl |
|---|---|
| Size | 173.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
78da6d9fb4ea9e02334d90215a4f155acd537bc75e1c7ed4c9af8e770e624311
|
|
BLAKE2b-256 checksum How to use checksums |
e78f4570d0eea6ac1ec9c34917dd6e635473b2316141aa9e92909c9f40b8507b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.
Transparency logSecurity for gaze-py 0.9.3
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for gaze_py-0.9.3.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 7, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.