pytest-eo 0.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. pytest_eo-0.5.1/.claude/hooks/eo-gate.sh +53 -0
  2. pytest_eo-0.5.1/.claude/settings.json +29 -0
  3. pytest_eo-0.5.1/.github/workflows/codecov.yml +20 -0
  4. pytest_eo-0.5.1/.github/workflows/pypi.yml +19 -0
  5. pytest_eo-0.5.1/.gitignore +10 -0
  6. pytest_eo-0.5.1/.rultor.yml +17 -0
  7. pytest_eo-0.5.1/CLAUDE.md +101 -0
  8. pytest_eo-0.5.1/LICENSE.txt +21 -0
  9. pytest_eo-0.5.1/Makefile +9 -0
  10. pytest_eo-0.5.1/PKG-INFO +177 -0
  11. pytest_eo-0.5.1/README.md +157 -0
  12. pytest_eo-0.5.1/pyproject.toml +54 -0
  13. pytest_eo-0.5.1/src/pytest_eo/__init__.py +1 -0
  14. pytest_eo-0.5.1/src/pytest_eo/angry_tests.py +44 -0
  15. pytest_eo-0.5.1/src/pytest_eo/assertion.py +30 -0
  16. pytest_eo-0.5.1/src/pytest_eo/assigned.py +32 -0
  17. pytest_eo-0.5.1/src/pytest_eo/blank_lines.py +42 -0
  18. pytest_eo-0.5.1/src/pytest_eo/bodies.py +36 -0
  19. pytest_eo-0.5.1/src/pytest_eo/bundled.py +20 -0
  20. pytest_eo-0.5.1/src/pytest_eo/callee.py +23 -0
  21. pytest_eo-0.5.1/src/pytest_eo/capacity.py +50 -0
  22. pytest_eo-0.5.1/src/pytest_eo/check.py +17 -0
  23. pytest_eo-0.5.1/src/pytest_eo/command.py +13 -0
  24. pytest_eo-0.5.1/src/pytest_eo/comment.py +42 -0
  25. pytest_eo-0.5.1/src/pytest_eo/comments.py +42 -0
  26. pytest_eo-0.5.1/src/pytest_eo/config/flake8.ini +23 -0
  27. pytest_eo-0.5.1/src/pytest_eo/config/mypy.ini +5 -0
  28. pytest_eo-0.5.1/src/pytest_eo/config/ruff.toml +43 -0
  29. pytest_eo-0.5.1/src/pytest_eo/edges.py +28 -0
  30. pytest_eo-0.5.1/src/pytest_eo/encapsulation.py +30 -0
  31. pytest_eo-0.5.1/src/pytest_eo/ending.py +30 -0
  32. pytest_eo-0.5.1/src/pytest_eo/eo_item.py +41 -0
  33. pytest_eo-0.5.1/src/pytest_eo/eo_table.py +29 -0
  34. pytest_eo-0.5.1/src/pytest_eo/excludes.py +25 -0
  35. pytest_eo-0.5.1/src/pytest_eo/extension.py +12 -0
  36. pytest_eo-0.5.1/src/pytest_eo/fields.py +40 -0
  37. pytest_eo-0.5.1/src/pytest_eo/files.py +12 -0
  38. pytest_eo-0.5.1/src/pytest_eo/final_assertion.py +46 -0
  39. pytest_eo-0.5.1/src/pytest_eo/flake_check.py +49 -0
  40. pytest_eo-0.5.1/src/pytest_eo/heading.py +35 -0
  41. pytest_eo-0.5.1/src/pytest_eo/inline_comments.py +44 -0
  42. pytest_eo-0.5.1/src/pytest_eo/layout.py +15 -0
  43. pytest_eo-0.5.1/src/pytest_eo/margin.py +42 -0
  44. pytest_eo-0.5.1/src/pytest_eo/message.py +60 -0
  45. pytest_eo-0.5.1/src/pytest_eo/module_constants.py +32 -0
  46. pytest_eo-0.5.1/src/pytest_eo/mypy_check.py +43 -0
  47. pytest_eo-0.5.1/src/pytest_eo/narrow_suppressions.py +72 -0
  48. pytest_eo-0.5.1/src/pytest_eo/negative_messages.py +40 -0
  49. pytest_eo-0.5.1/src/pytest_eo/output.py +37 -0
  50. pytest_eo-0.5.1/src/pytest_eo/owned.py +28 -0
  51. pytest_eo-0.5.1/src/pytest_eo/place.py +21 -0
  52. pytest_eo-0.5.1/src/pytest_eo/plugin.py +45 -0
  53. pytest_eo-0.5.1/src/pytest_eo/printout.py +18 -0
  54. pytest_eo-0.5.1/src/pytest_eo/profile.py +13 -0
  55. pytest_eo-0.5.1/src/pytest_eo/project.py +37 -0
  56. pytest_eo-0.5.1/src/pytest_eo/project_files.py +39 -0
  57. pytest_eo-0.5.1/src/pytest_eo/python_module.py +36 -0
  58. pytest_eo-0.5.1/src/pytest_eo/raising.py +27 -0
  59. pytest_eo-0.5.1/src/pytest_eo/remark.py +25 -0
  60. pytest_eo-0.5.1/src/pytest_eo/ruff_check.py +50 -0
  61. pytest_eo-0.5.1/src/pytest_eo/ruff_format_check.py +46 -0
  62. pytest_eo-0.5.1/src/pytest_eo/rule.py +12 -0
  63. pytest_eo-0.5.1/src/pytest_eo/shared.py +35 -0
  64. pytest_eo-0.5.1/src/pytest_eo/span.py +28 -0
  65. pytest_eo-0.5.1/src/pytest_eo/strict.py +112 -0
  66. pytest_eo-0.5.1/src/pytest_eo/style_violations_error.py +15 -0
  67. pytest_eo-0.5.1/src/pytest_eo/suppression_check.py +44 -0
  68. pytest_eo-0.5.1/src/pytest_eo/table.py +11 -0
  69. pytest_eo-0.5.1/src/pytest_eo/text.py +28 -0
  70. pytest_eo-0.5.1/src/pytest_eo/tool_failed_error.py +15 -0
  71. pytest_eo-0.5.1/src/pytest_eo/trial.py +37 -0
  72. pytest_eo-0.5.1/src/pytest_eo/verified.py +28 -0
  73. pytest_eo-0.5.1/src/pytest_eo/violation.py +24 -0
  74. pytest_eo-0.5.1/src/pytest_eo/wide_suppressions.py +45 -0
  75. pytest_eo-0.5.1/tests/__init__.py +1 -0
  76. pytest_eo-0.5.1/tests/conftest.py +3 -0
  77. pytest_eo-0.5.1/tests/it/__init__.py +1 -0
  78. pytest_eo-0.5.1/tests/it/test_eo_gate.py +30 -0
  79. pytest_eo-0.5.1/tests/it/test_plugin.py +469 -0
  80. pytest_eo-0.5.1/tests/support/__init__.py +1 -0
  81. pytest_eo-0.5.1/tests/support/fake_command.py +22 -0
  82. pytest_eo-0.5.1/tests/support/fake_files.py +22 -0
  83. pytest_eo-0.5.1/tests/support/fake_table.py +22 -0
  84. pytest_eo-0.5.1/tests/test_angry_tests.py +293 -0
  85. pytest_eo-0.5.1/tests/test_assertion.py +37 -0
  86. pytest_eo-0.5.1/tests/test_assigned.py +29 -0
  87. pytest_eo-0.5.1/tests/test_blank_lines.py +69 -0
  88. pytest_eo-0.5.1/tests/test_bodies.py +37 -0
  89. pytest_eo-0.5.1/tests/test_bundled.py +13 -0
  90. pytest_eo-0.5.1/tests/test_callee.py +21 -0
  91. pytest_eo-0.5.1/tests/test_capacity.py +88 -0
  92. pytest_eo-0.5.1/tests/test_comment.py +38 -0
  93. pytest_eo-0.5.1/tests/test_comments.py +46 -0
  94. pytest_eo-0.5.1/tests/test_edges.py +23 -0
  95. pytest_eo-0.5.1/tests/test_encapsulation.py +22 -0
  96. pytest_eo-0.5.1/tests/test_ending.py +28 -0
  97. pytest_eo-0.5.1/tests/test_eo_table.py +63 -0
  98. pytest_eo-0.5.1/tests/test_excludes.py +24 -0
  99. pytest_eo-0.5.1/tests/test_fields.py +46 -0
  100. pytest_eo-0.5.1/tests/test_final_assertion.py +29 -0
  101. pytest_eo-0.5.1/tests/test_flake_check.py +52 -0
  102. pytest_eo-0.5.1/tests/test_heading.py +44 -0
  103. pytest_eo-0.5.1/tests/test_inline_comments.py +61 -0
  104. pytest_eo-0.5.1/tests/test_margin.py +45 -0
  105. pytest_eo-0.5.1/tests/test_message.py +53 -0
  106. pytest_eo-0.5.1/tests/test_module_constants.py +38 -0
  107. pytest_eo-0.5.1/tests/test_mypy_check.py +70 -0
  108. pytest_eo-0.5.1/tests/test_narrow_suppressions.py +82 -0
  109. pytest_eo-0.5.1/tests/test_negative_messages.py +37 -0
  110. pytest_eo-0.5.1/tests/test_output.py +37 -0
  111. pytest_eo-0.5.1/tests/test_owned.py +31 -0
  112. pytest_eo-0.5.1/tests/test_place.py +11 -0
  113. pytest_eo-0.5.1/tests/test_project.py +51 -0
  114. pytest_eo-0.5.1/tests/test_project_files.py +46 -0
  115. pytest_eo-0.5.1/tests/test_raising.py +34 -0
  116. pytest_eo-0.5.1/tests/test_ruff_check.py +74 -0
  117. pytest_eo-0.5.1/tests/test_ruff_format_check.py +49 -0
  118. pytest_eo-0.5.1/tests/test_shared.py +49 -0
  119. pytest_eo-0.5.1/tests/test_span.py +39 -0
  120. pytest_eo-0.5.1/tests/test_strict.py +29 -0
  121. pytest_eo-0.5.1/tests/test_style_violations_error.py +23 -0
  122. pytest_eo-0.5.1/tests/test_suppression_check.py +320 -0
  123. pytest_eo-0.5.1/tests/test_text.py +29 -0
  124. pytest_eo-0.5.1/tests/test_tool_failed_error.py +11 -0
  125. pytest_eo-0.5.1/tests/test_trial.py +39 -0
  126. pytest_eo-0.5.1/tests/test_verified.py +23 -0
  127. pytest_eo-0.5.1/tests/test_violation.py +12 -0
  128. pytest_eo-0.5.1/tests/test_wide_suppressions.py +78 -0
  129. pytest_eo-0.5.1/third_party/yegor256-prompt-LICENSE.txt +21 -0
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+ # Claude Code hook: run `pytest --eo` and, when it is red, give the
3
+ # findings back to Claude (exit code 2) so it fixes them before going on.
4
+ # Used for PostToolUse (after editing a Python or config file) and Stop.
5
+ set -u
6
+
7
+ payload=$(cat)
8
+ root="${CLAUDE_PROJECT_DIR:-$PWD}"
9
+ cd "$root" || exit 0
10
+ python=".venv/bin/python"
11
+ if [ ! -x "$python" ]; then
12
+ echo "pytest --eo gate skipped: no .venv here, run python3 -m venv .venv && .venv/bin/pip install -e ." >&2
13
+ exit 1
14
+ fi
15
+
16
+ field() {
17
+ printf '%s' "$payload" | "$python" -I -c '
18
+ import json, sys
19
+ value = json.load(sys.stdin)
20
+ for key in sys.argv[1].split("."):
21
+ value = value.get(key, "") if isinstance(value, dict) else ""
22
+ print(value)
23
+ ' "$1"
24
+ }
25
+
26
+ event=$(field hook_event_name)
27
+ if [ "$event" = "PostToolUse" ]; then
28
+ case "$(field tool_input.file_path)" in
29
+ *.py|*.toml|*.ini|*.cfg) ;;
30
+ *) exit 0 ;;
31
+ esac
32
+ fi
33
+
34
+ report=$("$python" -m pytest --eo -q --tb=short 2>&1)
35
+ if [ $? -eq 0 ]; then
36
+ exit 0
37
+ fi
38
+
39
+ findings=$(printf '%s\n' "$report" | tail -n 120)
40
+ summary=$(printf '%s\n' "$report" | tail -n 1)
41
+
42
+ if [ "$event" = "Stop" ] && [ "$(field stop_hook_active)" = "True" ]; then
43
+ printf '{"systemMessage": %s}\n' \
44
+ "$(printf 'pytest --eo is still red after a retry: %s' "$summary" | "$python" -I -c 'import json, sys; print(json.dumps(sys.stdin.read()))')"
45
+ exit 0
46
+ fi
47
+
48
+ {
49
+ echo "pytest --eo is red; fix every finding below, never suppress it without a reason"
50
+ printf '%s\n' "$findings"
51
+ echo "$summary"
52
+ } >&2
53
+ exit 2
@@ -0,0 +1,29 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Edit|Write|MultiEdit",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/eo-gate.sh",
10
+ "timeout": 180,
11
+ "statusMessage": "pytest --eo"
12
+ }
13
+ ]
14
+ }
15
+ ],
16
+ "Stop": [
17
+ {
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/eo-gate.sh",
22
+ "timeout": 180,
23
+ "statusMessage": "pytest --eo"
24
+ }
25
+ ]
26
+ }
27
+ ]
28
+ }
29
+ }
@@ -0,0 +1,20 @@
1
+ name: codecov
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ jobs:
7
+ codecov:
8
+ runs-on: ubuntu-24.04
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: actions/setup-python@v5
12
+ with:
13
+ python-version: "3.11"
14
+ - run: python3 -m venv .venv
15
+ - run: .venv/bin/pip install -e '.[dev]'
16
+ - run: make coverage
17
+ - uses: codecov/codecov-action@v5
18
+ with:
19
+ token: ${{ secrets.CODECOV_TOKEN }}
20
+ files: coverage.xml
@@ -0,0 +1,19 @@
1
+ name: pypi
2
+ on:
3
+ push:
4
+ tags: ["*.*.*"]
5
+ jobs:
6
+ pypi:
7
+ runs-on: ubuntu-24.04
8
+ environment: pypi
9
+ permissions:
10
+ id-token: write
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.11"
16
+ - run: grep -q "^version = \"${GITHUB_REF_NAME}\"$" pyproject.toml || { echo "Tag ${GITHUB_REF_NAME} is not the version in pyproject.toml"; exit 1; }
17
+ - run: python3 -m pip install build==1.3.0
18
+ - run: python3 -m build
19
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ __pycache__/
3
+ .mypy_cache/
4
+ .ruff_cache/
5
+ .pytest_cache/
6
+ *.egg-info/
7
+ dist/
8
+ .coverage
9
+ .coverage.*
10
+ coverage.xml
@@ -0,0 +1,17 @@
1
+ ---
2
+ docker:
3
+ image: python:3.11
4
+ merge:
5
+ script: |-
6
+ python3 -m venv .venv
7
+ .venv/bin/pip install -e '.[dev]'
8
+ make check
9
+ release:
10
+ script: |-
11
+ [[ "${tag}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "Tag ${tag} is not a version like 1.2.3"; exit 1; }
12
+ sed -i -E "s/^version = \"[^\"]*\"$/version = \"${tag}\"/" pyproject.toml
13
+ grep -q "^version = \"${tag}\"$" pyproject.toml || { echo "Cannot set version ${tag} in pyproject.toml"; exit 1; }
14
+ python3 -m venv .venv
15
+ .venv/bin/pip install -e '.[dev]'
16
+ make check
17
+ git commit -am "${tag}"
@@ -0,0 +1,101 @@
1
+ <!-- Source: https://github.com/yegor256/prompt (README.txt) at commit 83ec21442f7349179a291445d0d369361273c891.
2
+ MIT License, Copyright (c) 2025 Yegor Bugayenko; see third_party/yegor256-prompt-LICENSE.txt.
3
+ Changed for Linux: gsed, gfind, ggrep, gcat are written as sed, find, grep, cat. -->
4
+
5
+ # Be Quality Dogmatic
6
+
7
+ Keep chat replies and your answers easy to comprehend.
8
+
9
+ ## Tools
10
+
11
+ Use GNU (GNU's Not Unix) tools such as `sed`, `find`, `grep`, and `cat`.
12
+ Commit changes with `courier:commit-changes-to-git` skill.
13
+ Submit pull requests with `courier:submit-a-pull-request` skill.
14
+ File issues with `bugscribe:submit-an-issue` skill.
15
+
16
+ ## Discipline
17
+
18
+ Write concise, direct, human prose.
19
+ Speak plain English in full sentences, avoiding jargon, slang, and shorthand.
20
+ Push back on technical mistakes, deferring to users on vision and architecture.
21
+ Stay on scope, refactoring only what tasks require.
22
+ Design top-down, whole before parts, composition before ingredients.
23
+ Never work around design obstacles, stopping to ask me instead.
24
+
25
+ ## Workflow
26
+
27
+ Pull from Git before changes, push back only with explicit user permission.
28
+ Name each branch after its GitHub issue integer, asking when unsure.
29
+ Always practice TDD (test-driven development), reproducing bugs before fixing.
30
+ Invest in unit tests until bugs reproduce, trusting intuition over debugging.
31
+ Log extensively when problems stay hard.
32
+
33
+ ## Changes
34
+
35
+ Tolerate upstream bugs, suggesting issues and waiting for fixes.
36
+ Keep changes focused and minimal.
37
+ Leave puzzles for later, following Puzzle Driven Development.
38
+ Flag smells and refactoring, suggesting issues rather than fixing silently.
39
+ Fix style violations in code, trusting checkers rather than suppressing them.
40
+
41
+ ## Code
42
+
43
+ Favor aesthetics over functionality.
44
+ Skip inline comments.
45
+ Prepend classes with plain-text docblocks explaining purpose, not usage.
46
+ Name variables as single nouns.
47
+ Inline single-use expressions, dropping unnecessary local variables.
48
+ Name methods as single nouns or verbs, separating commands from queries.
49
+ Pack method bodies without blank lines.
50
+
51
+ ## Style
52
+
53
+ Keep error and log messages to one sentence with context.
54
+ Strip trailing periods from messages.
55
+ Follow "Paired Brackets" and "Monotonic Indentation".
56
+
57
+ ## Objects
58
+
59
+ Honor Elegant Objects and SOLID (single, open, Liskov, interface, dependency).
60
+ Follow DDD (domain-driven design) and YAGNI (you arent gonna need it).
61
+ Apply SRP (single-responsibility) and DRY (dont repeat yourself).
62
+ Favor failing immediately over failing silently.
63
+ Allow only assignments in one primary constructor, delegating from secondaries.
64
+ Compose objects instead of inheriting implementation.
65
+ Reject -er names and utility classes.
66
+ Make every class final, holding one to four attributes.
67
+
68
+ ## Methods
69
+
70
+ Build only immutable objects, freezing attributes after assignment.
71
+ Drop static methods, public static literals, setters, and getters.
72
+ Declare methods in interfaces first, exposing nothing public outside them.
73
+ Reject null returns and arguments.
74
+ Skip type introspection, casting, and reflection.
75
+
76
+ ## Tests
77
+
78
+ Follow "Angry Tests".
79
+ Place one assertion per test, as its last statement.
80
+ Phrase each failure message negatively.
81
+ Keep tests short, verifying one behavior with one statement.
82
+ Map test files one-to-one with feature files, naming tests as sentences.
83
+ Spell "cannot" and "dont" without apostrophes.
84
+ Isolate tests from shared attributes, setUp, tearDown, constants, and cleanup.
85
+
86
+ ## Fixtures
87
+
88
+ Provide custom arguments to tested objects, not defaults.
89
+ Skip irrelevant code, side effects, accessors, constructors, and errors.
90
+ Favor fakes and stubs over mocks.
91
+ Use Hamcrest matchers with irregular, random inputs.
92
+ Inline brief fixtures, generating bulky ones at runtime.
93
+ Create fixture objects against duplication, using temporary directories.
94
+ Keep filesystem, sockets, and memory unmocked.
95
+
96
+ ## Runtime
97
+
98
+ Disable logging in tests.
99
+ Bound every wait with timeouts.
100
+ Test concurrency, retrying flaky blocks.
101
+ Assume no Internet, using ephemeral ports.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RaulIQ
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,9 @@
1
+ .PHONY: check coverage
2
+ check:
3
+ .venv/bin/pytest --eo
4
+
5
+ coverage:
6
+ .venv/bin/coverage run -m pytest
7
+ .venv/bin/coverage combine --quiet
8
+ .venv/bin/coverage report
9
+ .venv/bin/coverage xml
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.5
2
+ Name: pytest-eo
3
+ Version: 0.5.1
4
+ Summary: pytest --eo: strict style gate over ruff, mypy --strict and pyeo
5
+ Author: RaulIQ
6
+ License-Expression: MIT
7
+ License-File: LICENSE.txt
8
+ Requires-Python: >=3.11
9
+ Requires-Dist: eo-styleguide==0.2.0
10
+ Requires-Dist: flake8-override==0.1.1
11
+ Requires-Dist: flake8==7.4.1
12
+ Requires-Dist: mypy==2.4.0
13
+ Requires-Dist: pyhamcrest>=2.1
14
+ Requires-Dist: pytest>=8
15
+ Requires-Dist: ruff==0.17.1
16
+ Requires-Dist: typing-extensions>=4.4
17
+ Provides-Extra: dev
18
+ Requires-Dist: coverage>=7.10; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # pytest-eo
22
+
23
+ [![codecov](https://codecov.io/gh/RaulIQ/pytest-eo/graph/badge.svg)](https://codecov.io/gh/RaulIQ/pytest-eo)
24
+
25
+ `pytest --eo` is a strict quality gate in the spirit of
26
+ [Qulice](https://github.com/yegor256/qulice), built from ready-made tools.
27
+ The plugin runs the tools, reads their reports, and shows each tool as a
28
+ separate test; its own rules cover only tests and suppressions.
29
+
30
+ | Test | Tool |
31
+ |-------------------|------------------------------------------------|
32
+ | `eo::ruff` | `ruff check`, all rules (`select = ["ALL"]`) |
33
+ | `eo::ruff-format` | `ruff format --check` |
34
+ | `eo::mypy` | `mypy --strict` |
35
+ | `eo::flake8` | `flake8` with [pyeo](https://pypi.org/project/eo-styleguide/) (Elegant Objects rules) |
36
+ | `eo::suppressions`| `# noqa` and `# ruff: ignore` comments |
37
+
38
+ The Elegant Objects rules enabled so far, most of them from
39
+ [pyeo](https://github.com/blablatdinov/pyeo) and the rest from its sibling
40
+ packages:
41
+
42
+ | Code | Rule | Package |
43
+ |----------|----------------------------------------------|---------|
44
+ | `FIN100` | every class is `@final` | pyeo |
45
+ | `PEO101` | `__init__` contains only assignments | pyeo |
46
+ | `PEO102` | `@classmethod` contains only a `cls()` call | pyeo |
47
+ | `PEO200` | every class is frozen (immutable) | pyeo |
48
+ | `PEO300` | no class names ending in "-er" | pyeo |
49
+ | `PEO301` | no public attributes | pyeo |
50
+ | `PEO400` | no static methods | pyeo |
51
+ | `PEO500` | no `@property`, a getter in disguise | pyeo |
52
+ | `PEO601` | no methods starting with `get` | pyeo |
53
+ | `PEO602` | no getter methods | pyeo |
54
+ | `PEO700` | no introspection, casting, or reflection | pyeo |
55
+ | `NPM100` | no private functions or methods | [flake8-no-private-methods](https://github.com/blablatdinov/flake8-no-private-methods) |
56
+ | `OVR100` | every public method is an `@override` of an interface method | [flake8-override](https://github.com/blablatdinov/flake8-override) |
57
+ | `FOC100` | one public class per module | [flake8-one-class](https://github.com/blablatdinov/flake8-one-class) |
58
+
59
+ mypy runs with `explicit-override` and `warn_unreachable` on top of
60
+ `--strict`: an `@override` must override a real method, every overriding
61
+ method must say so, and code that can never run is an error.
62
+ Python 3.11 has no `typing.override`; import it from `typing_extensions`.
63
+
64
+ The package also brings a flake8 plugin of its own, with rules in the spirit
65
+ of "Angry Tests" for the files pytest collects (`test_*.py`, `*_test.py`):
66
+
67
+ | Code | Rule |
68
+ |----------|----------------------------------------------------------|
69
+ | `TST100` | a test ends with its only assertion, or with `pytest.raises` |
70
+ | `TST200` | a test module keeps no constants, except `pytest_plugins` |
71
+ | `TST300` | an assertion carries a negative message, such as `"x doesnt y"` |
72
+
73
+ More rules of its own cover every file, tests or not:
74
+
75
+ | Code | Rule |
76
+ |----------|----------------------------------------------------------|
77
+ | `ENC100` | a class encapsulates one to four attributes; a `Protocol` is exempt |
78
+ | `BOD100` | a function body holds no blank lines, except around a nested `def` or `class` |
79
+ | `BOD200` | a function body holds no comments, except suppressions |
80
+
81
+ As in Qulice, which rejects plain JUnit assertions, a test asserts through a
82
+ Hamcrest matcher, never through a plain `assert`; ruff's `S101` stays on in
83
+ tests too. PyHamcrest comes with the package:
84
+
85
+ ```python
86
+ from hamcrest import assert_that, equal_to
87
+
88
+
89
+ def test_checks_the_whole_directory_by_default(tmp_path: Path) -> None:
90
+ assert_that(
91
+ Project(EoTable(tmp_path / "pyproject.toml"), tmp_path).paths(),
92
+ equal_to((".",)),
93
+ "Project doesnt check the whole directory by default",
94
+ )
95
+ ```
96
+
97
+ A rule may be suppressed on one line only, with a reason:
98
+ `# noqa: PEO200 - pytest.Item is mutable by design`, or
99
+ `# ruff: ignore[unused-import] - re-exported for plugins`.
100
+ `eo::suppressions` enforces it:
101
+
102
+ | Code | Rule |
103
+ |----------|----------------------------------------------------------|
104
+ | `SUP001` | a suppression gives a reason after ` - ` |
105
+ | `SUP002` | a suppression never names `TID251` (`banned-api`), the ban on mocks |
106
+ | `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 |
107
+
108
+ ruff already rejects a bare `# noqa` (`PGH004`), a bare `# type: ignore`
109
+ (`PGH003`) and an unused `# noqa` for its own codes (`RUF100`).
110
+
111
+ The configuration is bundled with the package (`src/pytest_eo/config/`)
112
+ and cannot be overridden by a project. A project may only say what to
113
+ check and what to skip:
114
+
115
+ ```toml
116
+ [tool.eo]
117
+ paths = ["src", "tests"] # default: src and tests, if they exist
118
+ exclude = ["generated/"]
119
+ ```
120
+
121
+ ## Usage
122
+
123
+ ```sh
124
+ pip install 'pytest-eo==0.5.1'
125
+ pytest --eo
126
+ ```
127
+
128
+ Without `--eo` the plugin does nothing. Pin an exact version: every version
129
+ of the rules is a version of the package, so pip installs new rules only
130
+ when the version in the requirement changes.
131
+
132
+ ## Claude Code hooks
133
+
134
+ This repository runs the gate on every Claude Code session that works in it:
135
+ after Claude edits a `.py`, `.toml`, `.ini` or `.cfg` file, and again before
136
+ it finishes. When the gate is red, the findings go back to Claude and it has
137
+ to fix them. If the gate is still red after one retry, Claude may stop, and
138
+ you see a warning instead.
139
+
140
+ To get the same in another project:
141
+
142
+ 1. Install pytest-eo into the project's `.venv`.
143
+ 2. Copy `.claude/hooks/eo-gate.sh` into the same place in that project and
144
+ make it executable.
145
+ 3. Copy the `hooks` block of `.claude/settings.json` into that project's
146
+ `.claude/settings.json`, merging it with what is already there.
147
+ 4. Commit both files, so everyone who opens the project with Claude Code gets
148
+ the gate, and run `/hooks` once in an open session to load them.
149
+
150
+ ## Development
151
+
152
+ ```sh
153
+ python3 -m venv .venv
154
+ .venv/bin/pip install -e '.[dev]'
155
+ make check
156
+ ```
157
+
158
+ `make check` runs the tests and `pytest --eo` on the plugin itself; Rultor
159
+ merges on it. `make coverage` measures lines and branches, counting the
160
+ pytest subprocesses of the end-to-end tests too, and writes `coverage.xml`,
161
+ which GitHub Actions sends to Codecov on every push and pull request.
162
+
163
+ `pyproject.toml` on `main` always says `version = "0.0.0"`; a real version
164
+ exists only in a tag. To release, comment in any issue, with the version in
165
+ backticks, since Rultor reads nothing else:
166
+
167
+ ```text
168
+ @rultor release, tag is `0.3.0`
169
+ ```
170
+
171
+ Rultor writes the version into `pyproject.toml`, runs
172
+ `make check`, commits the result on a side branch and pushes only the tag.
173
+ GitHub Actions builds that tag and uploads it to PyPI through Trusted
174
+ Publishing, so no token is stored anywhere.
175
+ Every change to the rules or the bundled configuration ships in a release:
176
+ raise the minor number for a new or stricter rule and the patch number for a
177
+ fix.
@@ -0,0 +1,157 @@
1
+ # pytest-eo
2
+
3
+ [![codecov](https://codecov.io/gh/RaulIQ/pytest-eo/graph/badge.svg)](https://codecov.io/gh/RaulIQ/pytest-eo)
4
+
5
+ `pytest --eo` is a strict quality gate in the spirit of
6
+ [Qulice](https://github.com/yegor256/qulice), built from ready-made tools.
7
+ The plugin runs the tools, reads their reports, and shows each tool as a
8
+ separate test; its own rules cover only tests and suppressions.
9
+
10
+ | Test | Tool |
11
+ |-------------------|------------------------------------------------|
12
+ | `eo::ruff` | `ruff check`, all rules (`select = ["ALL"]`) |
13
+ | `eo::ruff-format` | `ruff format --check` |
14
+ | `eo::mypy` | `mypy --strict` |
15
+ | `eo::flake8` | `flake8` with [pyeo](https://pypi.org/project/eo-styleguide/) (Elegant Objects rules) |
16
+ | `eo::suppressions`| `# noqa` and `# ruff: ignore` comments |
17
+
18
+ The Elegant Objects rules enabled so far, most of them from
19
+ [pyeo](https://github.com/blablatdinov/pyeo) and the rest from its sibling
20
+ packages:
21
+
22
+ | Code | Rule | Package |
23
+ |----------|----------------------------------------------|---------|
24
+ | `FIN100` | every class is `@final` | pyeo |
25
+ | `PEO101` | `__init__` contains only assignments | pyeo |
26
+ | `PEO102` | `@classmethod` contains only a `cls()` call | pyeo |
27
+ | `PEO200` | every class is frozen (immutable) | pyeo |
28
+ | `PEO300` | no class names ending in "-er" | pyeo |
29
+ | `PEO301` | no public attributes | pyeo |
30
+ | `PEO400` | no static methods | pyeo |
31
+ | `PEO500` | no `@property`, a getter in disguise | pyeo |
32
+ | `PEO601` | no methods starting with `get` | pyeo |
33
+ | `PEO602` | no getter methods | pyeo |
34
+ | `PEO700` | no introspection, casting, or reflection | pyeo |
35
+ | `NPM100` | no private functions or methods | [flake8-no-private-methods](https://github.com/blablatdinov/flake8-no-private-methods) |
36
+ | `OVR100` | every public method is an `@override` of an interface method | [flake8-override](https://github.com/blablatdinov/flake8-override) |
37
+ | `FOC100` | one public class per module | [flake8-one-class](https://github.com/blablatdinov/flake8-one-class) |
38
+
39
+ mypy runs with `explicit-override` and `warn_unreachable` on top of
40
+ `--strict`: an `@override` must override a real method, every overriding
41
+ method must say so, and code that can never run is an error.
42
+ Python 3.11 has no `typing.override`; import it from `typing_extensions`.
43
+
44
+ The package also brings a flake8 plugin of its own, with rules in the spirit
45
+ of "Angry Tests" for the files pytest collects (`test_*.py`, `*_test.py`):
46
+
47
+ | Code | Rule |
48
+ |----------|----------------------------------------------------------|
49
+ | `TST100` | a test ends with its only assertion, or with `pytest.raises` |
50
+ | `TST200` | a test module keeps no constants, except `pytest_plugins` |
51
+ | `TST300` | an assertion carries a negative message, such as `"x doesnt y"` |
52
+
53
+ More rules of its own cover every file, tests or not:
54
+
55
+ | Code | Rule |
56
+ |----------|----------------------------------------------------------|
57
+ | `ENC100` | a class encapsulates one to four attributes; a `Protocol` is exempt |
58
+ | `BOD100` | a function body holds no blank lines, except around a nested `def` or `class` |
59
+ | `BOD200` | a function body holds no comments, except suppressions |
60
+
61
+ As in Qulice, which rejects plain JUnit assertions, a test asserts through a
62
+ Hamcrest matcher, never through a plain `assert`; ruff's `S101` stays on in
63
+ tests too. PyHamcrest comes with the package:
64
+
65
+ ```python
66
+ from hamcrest import assert_that, equal_to
67
+
68
+
69
+ def test_checks_the_whole_directory_by_default(tmp_path: Path) -> None:
70
+ assert_that(
71
+ Project(EoTable(tmp_path / "pyproject.toml"), tmp_path).paths(),
72
+ equal_to((".",)),
73
+ "Project doesnt check the whole directory by default",
74
+ )
75
+ ```
76
+
77
+ A rule may be suppressed on one line only, with a reason:
78
+ `# noqa: PEO200 - pytest.Item is mutable by design`, or
79
+ `# ruff: ignore[unused-import] - re-exported for plugins`.
80
+ `eo::suppressions` enforces it:
81
+
82
+ | Code | Rule |
83
+ |----------|----------------------------------------------------------|
84
+ | `SUP001` | a suppression gives a reason after ` - ` |
85
+ | `SUP002` | a suppression never names `TID251` (`banned-api`), the ban on mocks |
86
+ | `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 |
87
+
88
+ ruff already rejects a bare `# noqa` (`PGH004`), a bare `# type: ignore`
89
+ (`PGH003`) and an unused `# noqa` for its own codes (`RUF100`).
90
+
91
+ The configuration is bundled with the package (`src/pytest_eo/config/`)
92
+ and cannot be overridden by a project. A project may only say what to
93
+ check and what to skip:
94
+
95
+ ```toml
96
+ [tool.eo]
97
+ paths = ["src", "tests"] # default: src and tests, if they exist
98
+ exclude = ["generated/"]
99
+ ```
100
+
101
+ ## Usage
102
+
103
+ ```sh
104
+ pip install 'pytest-eo==0.5.1'
105
+ pytest --eo
106
+ ```
107
+
108
+ Without `--eo` the plugin does nothing. Pin an exact version: every version
109
+ of the rules is a version of the package, so pip installs new rules only
110
+ when the version in the requirement changes.
111
+
112
+ ## Claude Code hooks
113
+
114
+ This repository runs the gate on every Claude Code session that works in it:
115
+ after Claude edits a `.py`, `.toml`, `.ini` or `.cfg` file, and again before
116
+ it finishes. When the gate is red, the findings go back to Claude and it has
117
+ to fix them. If the gate is still red after one retry, Claude may stop, and
118
+ you see a warning instead.
119
+
120
+ To get the same in another project:
121
+
122
+ 1. Install pytest-eo into the project's `.venv`.
123
+ 2. Copy `.claude/hooks/eo-gate.sh` into the same place in that project and
124
+ make it executable.
125
+ 3. Copy the `hooks` block of `.claude/settings.json` into that project's
126
+ `.claude/settings.json`, merging it with what is already there.
127
+ 4. Commit both files, so everyone who opens the project with Claude Code gets
128
+ the gate, and run `/hooks` once in an open session to load them.
129
+
130
+ ## Development
131
+
132
+ ```sh
133
+ python3 -m venv .venv
134
+ .venv/bin/pip install -e '.[dev]'
135
+ make check
136
+ ```
137
+
138
+ `make check` runs the tests and `pytest --eo` on the plugin itself; Rultor
139
+ merges on it. `make coverage` measures lines and branches, counting the
140
+ pytest subprocesses of the end-to-end tests too, and writes `coverage.xml`,
141
+ which GitHub Actions sends to Codecov on every push and pull request.
142
+
143
+ `pyproject.toml` on `main` always says `version = "0.0.0"`; a real version
144
+ exists only in a tag. To release, comment in any issue, with the version in
145
+ backticks, since Rultor reads nothing else:
146
+
147
+ ```text
148
+ @rultor release, tag is `0.3.0`
149
+ ```
150
+
151
+ Rultor writes the version into `pyproject.toml`, runs
152
+ `make check`, commits the result on a side branch and pushes only the tag.
153
+ GitHub Actions builds that tag and uploads it to PyPI through Trusted
154
+ Publishing, so no token is stored anywhere.
155
+ Every change to the rules or the bundled configuration ships in a release:
156
+ raise the minor number for a new or stricter rule and the patch number for a
157
+ fix.
@@ -0,0 +1,54 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "pytest-eo"
7
+ version = "0.5.1"
8
+ description = "pytest --eo: strict style gate over ruff, mypy --strict and pyeo"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE.txt"]
12
+ authors = [{ name = "RaulIQ" }]
13
+ requires-python = ">=3.11"
14
+ dependencies = [
15
+ "pytest>=8",
16
+ "ruff==0.17.1",
17
+ "mypy==2.4.0",
18
+ "flake8==7.4.1",
19
+ "eo-styleguide==0.2.0",
20
+ "flake8-override==0.1.1",
21
+ "typing_extensions>=4.4",
22
+ "PyHamcrest>=2.1",
23
+ ]
24
+
25
+ [project.optional-dependencies]
26
+ dev = ["coverage>=7.10"]
27
+
28
+ [project.entry-points.pytest11]
29
+ eo = "pytest_eo.plugin"
30
+
31
+ [project.entry-points."flake8.extension"]
32
+ TST = "pytest_eo.angry_tests:AngryTests"
33
+ ENC = "pytest_eo.encapsulation:Encapsulation"
34
+ BOD = "pytest_eo.bodies:Bodies"
35
+
36
+ [tool.hatch.build.targets.wheel]
37
+ packages = ["src/pytest_eo"]
38
+
39
+ [tool.eo]
40
+ paths = ["src", "tests"]
41
+
42
+ [tool.pytest.ini_options]
43
+ pythonpath = ["src"]
44
+ testpaths = ["tests"]
45
+
46
+ [tool.coverage.run]
47
+ branch = true
48
+ source = ["pytest_eo"]
49
+ patch = ["subprocess"]
50
+ parallel = true
51
+
52
+ [tool.coverage.report]
53
+ show_missing = true
54
+ skip_covered = true
@@ -0,0 +1 @@
1
+ """pytest --eo: a strict style gate that aggregates ready-made tools."""