Structural Python search and lightweight AST-based rule checking
Project description
pyastq
pyastq is a structural Python searcher and lightweight rule runner. It searches
the Python AST rather than raw text, making it suitable for local scripts,
pre-commit hooks, and CI checks.
Build
cargo build --release
The binary is written to target/release/pyastq.
Install
pyastq is a Rust executable distributed as the pyastq Python package. Install
it as an isolated command-line tool:
uv tool install pyastq
# or
pipx install pyastq
For local development:
uv tool install .
pyastq --help
The PyPI package is only a distribution mechanism for the executable; it does not provide an importable Python module.
Versions
Release tags are the source of truth for published package versions. To publish a new release:
- Tag the commit, for example
git tag v0.2.0. - Push the tag with
git push origin v0.2.0.
The release workflow validates the tag, changes the package version in its
temporary checkout, builds wheels for Linux, macOS, and Windows, publishes them
to PyPI, and creates a GitHub release. Cargo.toml may therefore still show the
development version from the tagged commit.
An existing tag can be released from GitHub Actions by running the Release
workflow manually and entering the tag.
PyPI retains previously published versions, so users can select one explicitly:
uv tool install 'pyastq==0.1.0'
pipx install 'pyastq==0.1.0'
Uploading another build with the same version is not a replacement mechanism.
Fixes require a new version such as 0.1.1.
Structural Search
pyastq find src 'call:eval'
pyastq find src 'class:* -> function:regex:^[A-Z]'
pyastq find src 'call:request AND argument:timeout:>30'
pyastq find src 'function:* AND descendant(call:open) AND NOT descendant(call:close)'
pyastq find src 'call:print AND ancestor(function:*)'
pyastq find src 'call:$target' --var target=eval
pyastq find src 'call:eval AND argument:0:$expr' \
--replace 'json.loads($expr)' --change-label 'replace eval'
The first pattern is the node reported as the finding. Conditions inspect its structure:
patternanddescendant(pattern)search all nested nodes.child(pattern)searches direct AST children.ancestor(pattern)andinside(pattern)search enclosing nodes.->is shorthand forAND descendant.AND,OR,NOT, and parentheses compose conditions.
Supported node patterns:
call:<value>
class:<value>
function:<value>
import:<value>
argument:<key>:<value>
Argument keys are keyword names, zero-based positional indexes, or *:
argument:timeout:30
argument:0:"input.txt"
argument:*:None
Value predicates:
* any value
User exact value
exact:User exact value
contains:User substring
starts_with:test_ prefix
ends_with:_unsafe suffix
regex:^[A-Z] regular expression
>3 >=3 <10 <=10 numeric comparison
Quote a complete query when invoking it from a shell. Quotes inside a value
allow spaces, for example argument:0:"hello world".
Query Variables
Use $name or ${name} anywhere a complete value is accepted:
pyastq find src 'call:$target' --var target=requests.get
Defined variables are parsed as query values, so values such as regex:^safe_,
contains:token, or >30 keep their normal meaning. Variables may also be used
inside value predicates:
pyastq find src 'function:regex:$pattern' --var pattern='^[A-Z]'
pyastq find src 'call:request AND argument:timeout:>$limit' --var limit=30
An undefined variable becomes a capture for one candidate match. Reusing the same capture requires the same source value each time:
pyastq find src 'call:* AND argument:0:$x AND argument:1:$x'
That query reports calls whose first two positional arguments are the same.
Changes
find can apply a labelled replacement to every matched AST node:
pyastq find src 'call:eval AND argument:0:$expr' \
--replace 'json.loads($expr)' \
--change-label 'replace eval with json.loads'
Replacement templates can use defined variables and captures. Use $$ for a
literal dollar sign. Unsafe one-off changes must be labelled and explicitly
allowed:
pyastq find src 'call:eval AND argument:0:$expr' \
--replace 'json.loads($expr)' \
--change-label 'replace eval with json.loads' \
--unsafe-change --allow-unsafe
Python Name Resolution
Call patterns follow Python imports and aliases within each file:
import requests as r
from requests import post as send
r.get(url)
send(url)
call:requests.get matches r.get(url), and call:requests.post matches
send(url). Resolution respects lexical scopes and known shadowing by
parameters, assignments, definitions, loop targets, and similar bindings.
Literal call matching remains available when no import relationship is known.
Automation
find returns 0 unless parsing or execution fails. Use --fail-on-match to
make matches return 1:
pyastq find . 'call:eval' --fail-on-match --quiet
Exit codes:
0: successful and clean1: findings were detected when failure-on-match applies2: invalid query, configuration, or execution error
Output and filtering options:
pyastq find . 'call:eval' --format json
pyastq find . 'call:eval' --format jsonl
pyastq find . 'call:eval' --format sarif
pyastq find . 'call:eval' --include 'src/**/*.py' --exclude '**/generated/**'
pyastq find . 'call:eval' --changed --max-matches 10
pyastq find . 'call:eval' --no-cache
pyastq find . 'call:eval' --num-workers 4
pyastq find . 'call:$target' --var target=eval
pyastq find . 'call:eval AND argument:0:$expr' \
--replace 'json.loads($expr)' --change-label 'replace eval'
--changed includes staged, unstaged, and untracked Python files reported by
Git.
Directory searches store one content hash per file and findings per query or
rule in .pyastq-cache.json. Unchanged files reuse cached findings. Changed files
are read, hashed, and parsed once, then all applicable rules run against the
same syntax tree. Use --no-cache to force a full scan. Cache failures fall
back to a full scan, and --changed or --max-matches searches do not use the
cache. File processing uses one worker by default; --num-workers enables
parallel processing.
Rule Files
Rules use TOML. See the examples under examples/:
examples/check/demonstrates rules without replacements.examples/replace/demonstrates safe and unsafe replacements with expected output files.
exclude = ["**/generated/**"]
variables = { dangerous = "eval" }
[[rules]]
id = "no-eval"
query = "call:$dangerous AND argument:0:$expr"
message = "Avoid eval(); parse the expected input explicitly."
severity = "error"
change = { label = "replace eval with json.loads", replace = "json.loads($expr)", safety = "unsafe" }
include = ["src/**/*.py"]
valid = ["parse(value)"]
invalid = ["eval(value)"]
[[rules]]
id = "method-name-case"
query = "class:* -> function:regex:^[A-Z]"
message = "Method names must start with a lowercase letter."
severity = "warning"
Top-level rule variables apply to every rule. A rule may define its own
variables = { ... } table to override or add values for that rule only.
Variables that are not defined in TOML are treated as captures, just like
find queries.
Rules may also define a labelled change. Safe changes run with check --fix;
unsafe changes are skipped unless --allow-unsafe is also passed.
Run rules:
pyastq check . --rules pyastq.toml
pyastq check . --rules pyastq.toml --format json --changed
pyastq check . --rules pyastq.toml --fix
pyastq check . --rules pyastq.toml --fix --allow-unsafe
pyastq test-rules --rules pyastq.toml
Run the bundled check-only example:
pyastq check examples/check/example.py --rules examples/check/pyastq.toml
pyastq test-rules --rules examples/check/pyastq.toml
Run the bundled replacement example and compare it to the expected output:
cp examples/replace/example.py /tmp/pyastq-replace-example.py
pyastq check /tmp/pyastq-replace-example.py \
--rules examples/replace/pyastq.toml --fix
diff -u examples/replace/expected.py /tmp/pyastq-replace-example.py
To apply unsafe replacements too:
cp examples/replace/example.py /tmp/pyastq-replace-example.py
pyastq check /tmp/pyastq-replace-example.py \
--rules examples/replace/pyastq.toml --fix --allow-unsafe
diff -u examples/replace/expected-unsafe.py /tmp/pyastq-replace-example.py
check returns 1 when any rule matches. test-rules verifies that each
valid example does not match and each invalid example does.
Rules can also live in pyproject.toml:
[tool.pyastq]
exclude = ["generated/**", "migrations/**"]
[[tool.pyastq.rules]]
id = "no-eval"
query = "call:eval"
message = "Avoid eval(); parse the expected input explicitly."
severity = "error"
valid = ["parse(value)"]
invalid = ["eval(value)"]
Alternatively, reference a standalone rule file:
[tool.pyastq]
rules-file = "config/pyastq.toml"
exclude = ["build/**"]
rules-file is resolved relative to pyproject.toml. External and inline
rules may be used together: external rules are loaded first, inline rules are
appended, and exclusions from both configurations are combined. Rule IDs must
remain unique across both sources.
When --rules is omitted, check searches from the analyzed path toward the
filesystem root for a pyproject.toml containing [tool.pyastq]:
pyastq check .
pyastq test-rules
Passing --rules continues to support standalone rule files and explicit
pyproject.toml files:
pyastq check . --rules pyastq.toml
pyastq check . --rules pyproject.toml
SARIF 2.1.0 output is suitable for code-scanning systems:
pyastq check . --format sarif > pyastq.sarif
Suppressions
Suppress one line, the following line, or an entire file:
eval(value) # pyastq: ignore no-eval
# pyastq: ignore no-eval
eval(value)
# pyastq: ignore-file no-eval
Omitting the rule ID suppresses every rule at that location. Multiple IDs can be separated by spaces or commas.
Pre-commit Example
repos:
- repo: local
hooks:
- id: pyastq
name: pyastq structural rules
entry: target/release/pyastq check . --rules pyastq.toml --changed
language: system
pass_filenames: false
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyastq-0.3.0-py3-none-win_amd64.whl.
File metadata
- Download URL: pyastq-0.3.0-py3-none-win_amd64.whl
- Upload date:
- Size: 1.5 MB
- Tags: Python 3, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
28646d1869de2d05a4e98c254e070c47e604f7e5a44ac7bf8fa85dba1af75ea1
|
|
| MD5 |
9751efa0e1cd7f4e0fa3a07e8eee6614
|
|
| BLAKE2b-256 |
b9b6c025ca06ff11fe3582adbb1e3de119000cefa395b54bad102476c61053a5
|
Provenance
The following attestation bundles were made for pyastq-0.3.0-py3-none-win_amd64.whl:
Publisher:
release.yml on PetarMishov/pyastq
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyastq-0.3.0-py3-none-win_amd64.whl -
Subject digest:
28646d1869de2d05a4e98c254e070c47e604f7e5a44ac7bf8fa85dba1af75ea1 - Sigstore transparency entry: 1849398428
- Sigstore integration time:
-
Permalink:
PetarMishov/pyastq@8d0893b38bcb47fa4e4518eb3036b186e723f7b2 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/PetarMishov
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8d0893b38bcb47fa4e4518eb3036b186e723f7b2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyastq-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: pyastq-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 1.6 MB
- Tags: Python 3, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d80455b127aabb4365f9c1f4ed66066d706dc4d405ecd1e03bc40a3998f04343
|
|
| MD5 |
a5d0212289aa02f262d8b12ee63a9e02
|
|
| BLAKE2b-256 |
17a2590f02c8a9eaaaf13acd1d5a551fc609e765f4710cba3aedf73e025681a6
|
Provenance
The following attestation bundles were made for pyastq-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:
Publisher:
release.yml on PetarMishov/pyastq
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyastq-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
d80455b127aabb4365f9c1f4ed66066d706dc4d405ecd1e03bc40a3998f04343 - Sigstore transparency entry: 1849398325
- Sigstore integration time:
-
Permalink:
PetarMishov/pyastq@8d0893b38bcb47fa4e4518eb3036b186e723f7b2 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/PetarMishov
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8d0893b38bcb47fa4e4518eb3036b186e723f7b2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyastq-0.3.0-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: pyastq-0.3.0-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.5 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b41434bb1cbc06b1a5e58b28e18a8ee2198ca1a41fdabd8239bfd8d383be63b8
|
|
| MD5 |
08ad0b97ce2cce94e9d7a581671120fa
|
|
| BLAKE2b-256 |
499b7915ce151539ed9694600101554b674ca05cf66966248474362b099a2ce4
|
Provenance
The following attestation bundles were made for pyastq-0.3.0-py3-none-macosx_11_0_arm64.whl:
Publisher:
release.yml on PetarMishov/pyastq
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyastq-0.3.0-py3-none-macosx_11_0_arm64.whl -
Subject digest:
b41434bb1cbc06b1a5e58b28e18a8ee2198ca1a41fdabd8239bfd8d383be63b8 - Sigstore transparency entry: 1849398195
- Sigstore integration time:
-
Permalink:
PetarMishov/pyastq@8d0893b38bcb47fa4e4518eb3036b186e723f7b2 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/PetarMishov
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8d0893b38bcb47fa4e4518eb3036b186e723f7b2 -
Trigger Event:
push
-
Statement type:
File details
Details for the file pyastq-0.3.0-py3-none-macosx_10_12_x86_64.whl.
File metadata
- Download URL: pyastq-0.3.0-py3-none-macosx_10_12_x86_64.whl
- Upload date:
- Size: 1.5 MB
- Tags: Python 3, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
62a672308ed45e9f136764b4fa4fa574cd49b64ebcd62cd84ef3235b483419a6
|
|
| MD5 |
e650f229a05fa9245bd0a4ccf585b82a
|
|
| BLAKE2b-256 |
726ee8351016e095ab7fe68ba3583a9604c778a2ccd9a21569e7d0ce5219fd44
|
Provenance
The following attestation bundles were made for pyastq-0.3.0-py3-none-macosx_10_12_x86_64.whl:
Publisher:
release.yml on PetarMishov/pyastq
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyastq-0.3.0-py3-none-macosx_10_12_x86_64.whl -
Subject digest:
62a672308ed45e9f136764b4fa4fa574cd49b64ebcd62cd84ef3235b483419a6 - Sigstore transparency entry: 1849398529
- Sigstore integration time:
-
Permalink:
PetarMishov/pyastq@8d0893b38bcb47fa4e4518eb3036b186e723f7b2 -
Branch / Tag:
refs/tags/v0.3.0 - Owner: https://github.com/PetarMishov
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@8d0893b38bcb47fa4e4518eb3036b186e723f7b2 -
Trigger Event:
push
-
Statement type: