pytest-eo
pytest --eo is a strict quality gate in the spirit of
Qulice, built from ready-made tools.
The plugin runs the tools, reads their reports, and shows each tool as a
separate test; its own rules cover only tests and suppressions.
| Test | Tool |
|---|---|
eo::ruff |
ruff check, all rules (select = ["ALL"]) |
eo::ruff-format |
ruff format --check |
eo::mypy |
mypy --strict |
eo::flake8 |
flake8 with pyeo (Elegant Objects rules) |
eo::suppressions |
# noqa and # ruff: ignore comments |
The Elegant Objects rules enabled so far, most of them from pyeo and the rest from its sibling packages:
| Code | Rule | Package |
|---|---|---|
FIN100 |
every class is @final |
pyeo |
PEO101 |
__init__ contains only assignments |
pyeo |
PEO102 |
@classmethod contains only a cls() call |
pyeo |
PEO200 |
every class is frozen (immutable) | pyeo |
PEO300 |
no class names ending in "-er" | pyeo |
PEO301 |
no public attributes | pyeo |
PEO400 |
no static methods | pyeo |
PEO500 |
no @property, a getter in disguise |
pyeo |
PEO601 |
no methods starting with get |
pyeo |
PEO602 |
no getter methods | pyeo |
PEO700 |
no introspection, casting, or reflection | pyeo |
NPM100 |
no private functions or methods | flake8-no-private-methods |
OVR100 |
every public method is an @override of an interface method |
flake8-override |
FOC100 |
one public class per module | flake8-one-class |
mypy runs with explicit-override and warn_unreachable on top of
--strict: an @override must override a real method, every overriding
method must say so, and code that can never run is an error.
Python 3.11 has no typing.override; import it from typing_extensions.
The package also brings a flake8 plugin of its own, with rules in the spirit
of "Angry Tests" for the files pytest collects (test_*.py, *_test.py):
| Code | Rule |
|---|---|
TST100 |
a test ends with its only assertion, or with pytest.raises |
TST200 |
a test module keeps no constants, except pytest_plugins |
TST300 |
an assertion carries a negative message, such as "x doesnt y" |
More rules of its own cover every file, tests or not:
| Code | Rule |
|---|---|
ENC100 |
a class encapsulates one to four attributes; a Protocol is exempt |
BOD100 |
a function body holds no blank lines, except around a nested def or class |
BOD200 |
a function body holds no comments, except suppressions |
As in Qulice, which rejects plain JUnit assertions, a test asserts through a
Hamcrest matcher, never through a plain assert; ruff's S101 stays on in
tests too. PyHamcrest comes with the package:
from hamcrest import assert_that, equal_to
def test_checks_the_whole_directory_by_default(tmp_path: Path) -> None:
assert_that(
Project(EoTable(tmp_path / "pyproject.toml"), tmp_path).paths(),
equal_to((".",)),
"Project doesnt check the whole directory by default",
)
A rule may be suppressed on one line only, with a reason:
# noqa: PEO200 - pytest.Item is mutable by design, or
# ruff: ignore[unused-import] - re-exported for plugins.
eo::suppressions enforces it:
| Code | Rule |
|---|---|
SUP001 |
a suppression gives a reason after - |
SUP002 |
a suppression never names TID251 (banned-api), the ban on mocks |
SUP003 |
nothing suppresses past one line: no # ruff: noqa, file-ignore or disable, no # flake8: noqa, no # mypy: comment, no # type: ignore above the code |
ruff already rejects a bare # noqa (PGH004), a bare # type: ignore
(PGH003) and an unused # noqa for its own codes (RUF100).
The configuration is bundled with the package (src/pytest_eo/config/)
and cannot be overridden by a project. A project may only say what to
check and what to skip:
[tool.eo]
paths = ["src", "tests"] # default: src and tests, if they exist
exclude = ["generated/"]
Usage
pip install 'pytest-eo==0.5.1'
pytest --eo
Without --eo the plugin does nothing. Pin an exact version: every version
of the rules is a version of the package, so pip installs new rules only
when the version in the requirement changes.
Claude Code hooks
This repository runs the gate on every Claude Code session that works in it:
after Claude edits a .py, .toml, .ini or .cfg file, and again before
it finishes. When the gate is red, the findings go back to Claude and it has
to fix them. If the gate is still red after one retry, Claude may stop, and
you see a warning instead.
To get the same in another project:
- Install pytest-eo into the project's
.venv. - Copy
.claude/hooks/eo-gate.shinto the same place in that project and make it executable. - Copy the
hooksblock of.claude/settings.jsoninto that project's.claude/settings.json, merging it with what is already there. - Commit both files, so everyone who opens the project with Claude Code gets
the gate, and run
/hooksonce in an open session to load them.
Development
python3 -m venv .venv
.venv/bin/pip install -e '.[dev]'
make check
make check runs the tests and pytest --eo on the plugin itself; Rultor
merges on it. make coverage measures lines and branches, counting the
pytest subprocesses of the end-to-end tests too, and writes coverage.xml,
which GitHub Actions sends to Codecov on every push and pull request.
pyproject.toml on main always says version = "0.0.0"; a real version
exists only in a tag. To release, comment in any issue, with the version in
backticks, since Rultor reads nothing else:
@rultor release, tag is `0.3.0`
Rultor writes the version into pyproject.toml, runs
make check, commits the result on a side branch and pushes only the tag.
GitHub Actions builds that tag and uploads it to PyPI through Trusted
Publishing, so no token is stored anywhere.
Every change to the rules or the bundled configuration ships in a release:
raise the minor number for a new or stricter rule and the patch number for a
fix.
Metadata
Release files for pytest-eo 0.5.1
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 | |
|---|---|---|---|---|
| pytest_eo-0.5.1.tar.gz | 40.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_eo-0.5.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 82.6 kB
Release files / pytest_eo-0.5.1.tar.gz
| Download URL | pytest_eo-0.5.1.tar.gz |
|---|---|
| Size | 40.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a3701bb2628e682c898b8fbd40d77af2f5673c22d8434d08040913287acdff90
|
|
BLAKE2b-256 checksum How to use checksums |
d099c88015d9e7441515843681060f912ecb5e206b695988c5ab5d4cb6818f78
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Oct 22, 2026.
Transparency logRelease files / pytest_eo-0.5.1-py3-none-any.whl
| Download URL | pytest_eo-0.5.1-py3-none-any.whl |
|---|---|
| Size | 42.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
af20fd42c85a6df5c87243b12949a50ec3f97f7af1b2fdff02508b43d0eb8b84
|
|
BLAKE2b-256 checksum How to use checksums |
74557bb2a5b245e22fca76dd2f502e4f2ebe15addac2e3d383307cb3e7c72502
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
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 Oct 22, 2026.
Transparency logSecurity for pytest-eo 0.5.1
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 pytest_eo-0.5.1.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 Oct 22, 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.