Metadata-Version: 2.5
Name: anti-slop-py
Version: 0.1.0
Summary: Opinionated, readable lint rules that reject low-evidence Python patterns
Project-URL: Repository, https://github.com/infoslack/anti-slop-py
Project-URL: Issues, https://github.com/infoslack/anti-slop-py/issues
Author: Daniel Romero
License-Expression: MIT
License-File: LICENSE
Keywords: ai-slop,fastapi,flake8,lint,mypy,pydantic,ruff,type-safety
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Flake8
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.10
Requires-Dist: flake8>=7.0
Description-Content-Type: text/markdown

# anti-slop-py

[![skills.sh](https://skills.sh/b/infoslack/anti-slop-py)](https://skills.sh/infoslack/anti-slop-py)

Opinionated lint rules that reject low-evidence, low-signal Python patterns. A port of [anti-slop](https://github.com/dmmulroy/anti-slop) (Oxlint/TypeScript) to the Python ecosystem.

Vendor this project instead of pinning it as a dependency. Copy the rules into your repository, read them, and change them until they say what your team actually believes. The bundled agent skill does the first copy and wiring; after that the files are yours to maintain.

## Architecture: three layers

Ruff still has no API for custom rules, so anti-slop-py can't be a single plugin. It's one opinionated configuration spread across three tools, each covering the part it already handles well:

1. **Ruff** (`configs/ruff-anti-slop.toml`), for the rules that already exist: `ANN401` (no `Any` in signatures), `PGH003` (no blanket `type: ignore`), `B009`/`B010` (no constant-name `getattr`/`setattr`), and `TID251` with a banned-API list that includes `unittest.mock.patch`.
2. **mypy strict flags** (`configs/mypy-anti-slop.ini`). `disallow_any_explicit` and friends catch `Any` in the places Ruff can't see: nested generics, aliases, plain variables.
3. **A Flake8 plugin** (`src/anti_slop/`, vendored as a [local plugin](https://flake8.pycqa.org/en/latest/user/configuration.html)) carrying the ten custom `ASP` rules below. It runs next to Ruff, scoped with `select = ASP`.

## Install with an agent skill

Clone the repository and point your coding agent at the bundled skill:

```bash
git clone https://github.com/infoslack/anti-slop-py
```

Then, from the target repository, ask the agent to install anti-slop-py following `<clone>/skills/install-anti-slop-py/SKILL.md`. The skill copies the plugin to `tools/flake8/anti_slop/`, registers it as a Flake8 local plugin, merges the Ruff and mypy layers into whatever configuration already exists, and validates the result. When `pydantic` or `fastapi` shows up as a direct dependency, it also enables the matching framework group.

If you have Node available, the [skills.sh](https://skills.sh) CLI does the fetch in one step and registers the skill with your agent:

```bash
npx skills add infoslack/anti-slop-py --skill install-anti-slop-py
```

Claude Code users can install it as a plugin instead:

```
/plugin marketplace add infoslack/anti-slop-py
/plugin install anti-slop-py@anti-slop-py
```

## Scan without installing

The rules also run straight from a clone, reporting findings without adding a single file to the target project. Flake8 resolves the plugin relative to the config file, so from the target repository:

```bash
flake8 --config <clone>/configs/flake8-scan.ini src tests
```

Pair it with CLI-only flags for the other layers (`ruff check --extend-select ANN401,PGH003,B009,B010`, `mypy --disallow-any-explicit`) and nothing in the repo changes. With the package published, the clone becomes optional too: `uvx --with anti-slop-py flake8 --enable-extensions=ASD,ASF src tests`. This is the audit mode; the two installation paths below are for repositories that want the rules enforced in CI for every developer.

## Install as a package

The lighter enforcement path:

```bash
uv add --dev anti-slop-py
```

The entry points register the `ASP` rules with Flake8 automatically; a fresh `flake8 src tests` already reports them. The framework groups ship off by default and turn on per project:

```ini
[flake8]
select = ASP
# with pydantic/fastapi as direct dependencies:
# select = ASP,ASD,ASF
# enable-extensions = ASD,ASF
```

Then merge `configs/ruff-anti-slop.toml` into your Ruff configuration and `configs/mypy-anti-slop.ini` into your mypy configuration (both ship inside the sdist as reference).

## Vendored installation

For teams that want to own and edit the rules rather than track releases: copy `src/anti_slop/` into the target repository (say, at `tools/flake8/anti_slop/`), install `flake8` as a development dependency, and register the local plugin in `.flake8`, since Flake8 doesn't read `pyproject.toml`:

```ini
[flake8]
select = ASP

[flake8:local-plugins]
extension =
    ASP = anti_slop.checker:AntiSlopChecker
paths =
    ./tools/flake8
```

The Ruff and mypy layers merge the same way as in the package path. Avoid running the vendored copy and the installed package at once; each finding gets reported twice.

## Rules

Each rule documents which original anti-slop rule it ports and names the ready-made tool covering the complementary half.

| Code | Ports | Rejects |
|---|---|---|
| `ASP001` | `require-safety-comment-for-type-assertion` | `typing.cast(...)` or `# type: ignore` with no `# SAFETY:` comment documenting the checked invariant |
| `ASP002` | `no-runtime-typeof` | ad hoc `isinstance`/`hasattr`/`type(x) is` narrowing; parse at the boundary instead. `--anti-slop-allow-typeguards` permits checks inside `TypeGuard`/`TypeIs` functions |
| `ASP003` | `no-module-mocking` | `mock.patch(...)`, `mocker.patch(...)`, `monkeypatch.setattr(...)`; inject dependencies through real seams |
| `ASP004` | `no-chained-type-assertions` | `cast(User, cast(object, value))` |
| `ASP005` | `no-known-value-widening` | `handlers: dict[str, Handler] = {"start": ...}`, where the broad annotation throws away known keys; use inference, `Final`, or a `TypedDict` |
| `ASP006` | `no-reflect-get` / `no-reflect-apply` | `getattr(owner, dynamic_name)`; Ruff B009/B010 cover the constant-name forms |
| `ASP007` | `no-shape-in-symbol-names` | `shape` in class, function, parameter, or variable names |
| `ASP008` | `no-object-parameters` / `no-unknown-*` | parameters or returns typed bare `object`, Python's safe top type; dunder methods and `cause` parameters are exempt |
| `ASP009` | `no-unsafe-dictionary-type` | `dict[str, Any]`, `Mapping[str, object]`, and friends; define a `TypedDict`, dataclass, or model |
| `ASP010` | `no-unknown-type-aliases` | `type ExternalValue = Any` plus the `TypeAlias` and bare-assignment spellings |

Not ported: `no-conditional-empty-object-spread`, because the JS idiom barely exists in Python, and `no-widen-then-assert`, which needs local flow analysis and stays on the roadmap. The Effect rule `no-service-constructor-imports` maps to [import-linter](https://github.com/seddonym/import-linter) contracts rather than a lint rule.

## Framework groups (opt-in)

Framework policy lives in separate rule groups, the same split the original makes with `anti-slop-effect`. The groups are `off_by_default` Flake8 plugins: enable one only when its framework is a direct dependency, with `enable-extensions = ASD,ASF` next to `select` (package path) or together with the extension lines in `[flake8:local-plugins]` (vendored path). Each group repeats the three-layer split, with ready-made rules first and custom `ASD`/`ASF` rules only for the gaps.

### Pydantic (`ASD`)

Ready-made layer: [flake8-pydantic](https://github.com/Viicos/flake8-pydantic) for `PYD` hygiene rules, plus the official Pydantic mypy plugin (`configs/mypy-anti-slop-pydantic.ini`).

| Code | Rejects |
|---|---|
| `ASD001` | `model_construct()` or `construct()` without a `# SAFETY:` comment. Both skip validation; the docs allow them only for data you already trust |
| `ASD002` | `extra="allow"` in `ConfigDict` or a legacy `class Config`, which stores unvalidated keys without a contract |
| `ASD003` | model fields typed `Any`, bare or nested (`list[Any]`), opting the field out of validation |
| `ASD004` | `f(**model.model_dump())` and `x: dict[...] = model.model_dump()`: re-widening a validated model into an untyped dict, the Pydantic version of `no-widen-then-assert` |
| `ASD005` | `TypeAdapter(...)` inside a function; official performance guidance says build it once at module scope |

### FastAPI (`ASF`)

Ready-made layer: Ruff `FAST` (redundant response_model, non-`Annotated` `Depends`, unused path params) and `ASYNC` (blocking I/O inside async routes), via `configs/ruff-anti-slop-fastapi.toml`.

| Code | Rejects |
|---|---|
| `ASF001` | endpoints with no typed response contract: no return annotation and no `response_model`, or a return annotated `Any`/`dict` |
| `ASF002` | `await request.json()`, `.body()`, or `.form()` inside endpoints; declare a body model parameter and let the framework parse at the boundary |
| `ASF003` | `JSONResponse(content={...})` with a dict literal inside endpoints; return a model. Exception handlers and middleware stay unflagged |

Detection is syntactic. Models are found by base-class name (same-module subclasses included), endpoints by `@<receiver>.<http-verb>(...)` decorators; cross-module inheritance and aliased imports are known misses.

## Development

```bash
uv run --group dev pytest
uv run flake8 src tests scripts   # the project's own rules, on itself
uv run ruff check src tests scripts
uv run mypy
```

The repo dogfoods all three layers on its own source through the installed entry points; CI enforces it. One documented deviation lives in `.flake8`: ASP002 targets application boundaries, and an AST linter discriminating the closed `ast.*` sum type is that boundary, so the rule does not apply to this codebase.

`src/` is canonical. After changing production source, run `scripts/sync_skill_assets.py` so the skill's bundled copy stays identical; `--check` verifies without writing. Releases: bump `__version__` in `src/anti_slop/__init__.py`, tag `v*`, and the publish workflow ships to PyPI via trusted publishing.

## License

MIT
