pytest-stogger 2026.5.12__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.
- pytest_stogger-2026.5.12/.agents/impl_specs/project-wide-rules.md +194 -0
- pytest_stogger-2026.5.12/.agents/reports/quality-audit.md +322 -0
- pytest_stogger-2026.5.12/.gitignore +14 -0
- pytest_stogger-2026.5.12/.python-version +1 -0
- pytest_stogger-2026.5.12/PKG-INFO +9 -0
- pytest_stogger-2026.5.12/README.md +62 -0
- pytest_stogger-2026.5.12/docs/conf.py +85 -0
- pytest_stogger-2026.5.12/docs/config.md +79 -0
- pytest_stogger-2026.5.12/docs/dev/adr/project-wide-rules.md +39 -0
- pytest_stogger-2026.5.12/docs/fixtures.md +62 -0
- pytest_stogger-2026.5.12/docs/index.md +13 -0
- pytest_stogger-2026.5.12/docs/rules.md +50 -0
- pytest_stogger-2026.5.12/pyproject.toml +109 -0
- pytest_stogger-2026.5.12/src/pytest_stogger/__init__.py +0 -0
- pytest_stogger-2026.5.12/src/pytest_stogger/exemptions.py +48 -0
- pytest_stogger-2026.5.12/src/pytest_stogger/files.py +56 -0
- pytest_stogger-2026.5.12/src/pytest_stogger/matchers.py +205 -0
- pytest_stogger-2026.5.12/src/pytest_stogger/plugin.py +616 -0
- pytest_stogger-2026.5.12/src/pytest_stogger/report.py +66 -0
- pytest_stogger-2026.5.12/src/pytest_stogger/rules.py +1081 -0
- pytest_stogger-2026.5.12/tests/__init__.py +0 -0
- pytest_stogger-2026.5.12/tests/conftest.py +1 -0
- pytest_stogger-2026.5.12/tests/impl_spec/__init__.py +0 -0
- pytest_stogger-2026.5.12/tests/impl_spec/test_project_wide_rules.py +236 -0
- pytest_stogger-2026.5.12/tests/impl_spec/test_pytest_stogger_new_checks.py +701 -0
- pytest_stogger-2026.5.12/tests/impl_spec/test_quality_investments.py +258 -0
- pytest_stogger-2026.5.12/tests/impl_spec/test_stogger_rules_fixes.py +499 -0
- pytest_stogger-2026.5.12/tests/impl_spec/test_suppression_budget.py +121 -0
- pytest_stogger-2026.5.12/tests/test_architecture.py +63 -0
- pytest_stogger-2026.5.12/tests/test_coverage_gaps.py +819 -0
- pytest_stogger-2026.5.12/tests/test_entry_points.py +303 -0
- pytest_stogger-2026.5.12/tests/test_rule_detection.py +210 -0
- pytest_stogger-2026.5.12/uv.lock +1000 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
---
|
|
2
|
+
lifecycle:
|
|
3
|
+
requirements:
|
|
4
|
+
completed_at: "2026-05-07T00:00:00"
|
|
5
|
+
git_rev: dc19d32
|
|
6
|
+
design:
|
|
7
|
+
completed_at: "2026-05-07T00:00:00"
|
|
8
|
+
git_rev: dc19d32
|
|
9
|
+
plan:
|
|
10
|
+
completed_at: "2026-05-07T00:00:00"
|
|
11
|
+
git_rev: 08a35a6
|
|
12
|
+
workflow:
|
|
13
|
+
completed_at: "2026-05-07T00:00:00"
|
|
14
|
+
git_rev: 75a2a59
|
|
15
|
+
verify:
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Context
|
|
19
|
+
|
|
20
|
+
pytest-stogger enforces per-file AST rules (18 rules) and two cross-file meta-rules (logging-coverage, log-suppression-budget). FCIO projects exhibit project-wide anti-patterns — module-level `structlog.get_logger()` calls without `init_early_logging()`, and CLI entry points that import structlog without calling `init_logging()` — that require cross-file detection beyond per-file analysis.
|
|
21
|
+
|
|
22
|
+
## Decisions
|
|
23
|
+
|
|
24
|
+
### scope
|
|
25
|
+
|
|
26
|
+
#### Context
|
|
27
|
+
|
|
28
|
+
Three anti-patterns were identified: (1) no-early-init, (2) missing-init-in-entry, (3) init-logging-before-early.
|
|
29
|
+
|
|
30
|
+
#### Decision
|
|
31
|
+
|
|
32
|
+
Two rules: `no-early-init` and `missing-init-in-entry`. Pattern (3) is subsumed by (1) — if `init_early_logging()` is absent, `init_logging()` ordering is irrelevant.
|
|
33
|
+
|
|
34
|
+
#### Alternatives
|
|
35
|
+
|
|
36
|
+
a. All three rules — (3) adds no detection value beyond (1).
|
|
37
|
+
b. Only `no-early-init` — `missing-init-in-entry` catches a distinct failure mode.
|
|
38
|
+
|
|
39
|
+
#### Consequences
|
|
40
|
+
|
|
41
|
+
Two cross-file rules, each requiring all source files as input.
|
|
42
|
+
|
|
43
|
+
### rule-behavior
|
|
44
|
+
|
|
45
|
+
#### Context
|
|
46
|
+
|
|
47
|
+
All existing rules use error severity and are enabled by default. Consistency reduces cognitive overhead for users.
|
|
48
|
+
|
|
49
|
+
#### Decision
|
|
50
|
+
|
|
51
|
+
Both rules: error severity (test fails), enabled by default, individually disableable via `disable_rules`.
|
|
52
|
+
|
|
53
|
+
#### Alternatives
|
|
54
|
+
|
|
55
|
+
a. Warning severity for `missing-init-in-entry` due to heuristic detection — rejected. False positives suppressible via `disable_rules`.
|
|
56
|
+
|
|
57
|
+
#### Consequences
|
|
58
|
+
|
|
59
|
+
Immediate enforcement on upgrade. Projects opt out explicitly if violations exist.
|
|
60
|
+
|
|
61
|
+
### item-strategy
|
|
62
|
+
|
|
63
|
+
#### Context
|
|
64
|
+
|
|
65
|
+
Cross-file rules (logging-coverage, log-suppression-budget) each have a dedicated `pytest.Item` subclass with constructor accepting all source files. Per-file rules use `StoggerItem` with single-file context.
|
|
66
|
+
|
|
67
|
+
#### Decision
|
|
68
|
+
|
|
69
|
+
Two separate `pytest.Item` subclasses: `_NoEarlyInitItem` and `_MissingInitInEntryItem`. Each calls its rule function with all source files. Collected in `pytest_collection_modifyitems` alongside existing cross-file items. Node IDs: `stogger::no-early-init` and `stogger::missing-init-in-entry`.
|
|
70
|
+
|
|
71
|
+
#### Alternatives
|
|
72
|
+
|
|
73
|
+
a. Single shared `_ProjectWideItem` — mixes unrelated concerns, harder to disable individually.
|
|
74
|
+
|
|
75
|
+
#### Consequences
|
|
76
|
+
|
|
77
|
+
Consistent with existing cross-file pattern. Rule functions live in `rules.py`, imported by `plugin.py`. Each item independently disableable.
|
|
78
|
+
|
|
79
|
+
### detection-scope
|
|
80
|
+
|
|
81
|
+
#### Context
|
|
82
|
+
|
|
83
|
+
Module-level logger initialization varies: `log = structlog.get_logger()`, `logger = structlog.get_logger()`, bare calls, or `get_logger()` after `from structlog import get_logger`.
|
|
84
|
+
|
|
85
|
+
#### Decision
|
|
86
|
+
|
|
87
|
+
`no-early-init` detects any `get_logger()` call at module level (not inside function/class body), regardless of assignment pattern. Covers `structlog.get_logger()` and bare `get_logger()` after import.
|
|
88
|
+
|
|
89
|
+
#### Alternatives
|
|
90
|
+
|
|
91
|
+
a. Only assignment pattern (`X = structlog.get_logger()`) — misses bare calls and non-standard variable names.
|
|
92
|
+
|
|
93
|
+
#### Consequences
|
|
94
|
+
|
|
95
|
+
Broader detection, fewer false negatives. Each module-level `get_logger()` call reported with file:line reference.
|
|
96
|
+
|
|
97
|
+
### entry-point-pattern
|
|
98
|
+
|
|
99
|
+
#### Context
|
|
100
|
+
|
|
101
|
+
`missing-init-in-entry` needs to identify CLI entry points. Typer has `@app.callback()` and `@app.command()`. The callback is the init-level entry; commands register subcommands.
|
|
102
|
+
|
|
103
|
+
#### Decision
|
|
104
|
+
|
|
105
|
+
Two AST patterns identify entry points: (1) `if __name__ == "__main__":` blocks, (2) functions decorated with `@<any>.callback()` (any app variable name). `@app.command()` excluded — subcommand handlers are not entry points for init logic.
|
|
106
|
+
|
|
107
|
+
#### Alternatives
|
|
108
|
+
|
|
109
|
+
a. Include `@app.command()` — would cause false positives.
|
|
110
|
+
|
|
111
|
+
#### Consequences
|
|
112
|
+
|
|
113
|
+
Covers FCIO Typer app pattern. Rule checks these entry points for `init_logging()` calls.
|
|
114
|
+
|
|
115
|
+
### violation-output
|
|
116
|
+
|
|
117
|
+
#### Context
|
|
118
|
+
|
|
119
|
+
Users need to navigate from pytest output to the offending code. Existing violation format: `{file}:{line} — {rule}: {description}`.
|
|
120
|
+
|
|
121
|
+
#### Decision
|
|
122
|
+
|
|
123
|
+
Per-file violation lines + project-level summary. Each file with a module-level `get_logger()` call gets its own violation line. Summary identifies root cause (e.g., `init_early_logging() not found in project`).
|
|
124
|
+
|
|
125
|
+
#### Alternatives
|
|
126
|
+
|
|
127
|
+
a. Single-line summary with file list — loses line-level detail for navigation.
|
|
128
|
+
|
|
129
|
+
#### Consequences
|
|
130
|
+
|
|
131
|
+
Consistent with existing format. Per-file lines enable direct editor navigation.
|
|
132
|
+
|
|
133
|
+
### test-strategy
|
|
134
|
+
|
|
135
|
+
#### Context
|
|
136
|
+
|
|
137
|
+
Cross-file tests use direct rule function invocation (multi-file scenarios) and pytester integration (full plugin pipeline).
|
|
138
|
+
|
|
139
|
+
#### Decision
|
|
140
|
+
|
|
141
|
+
Spec validation tests in `tests/impl_spec/test_project_wide_rules.py` — direct rule function testing with multi-file scenarios. Pytester integration for collection and `disable_rules` config testing.
|
|
142
|
+
|
|
143
|
+
#### Alternatives
|
|
144
|
+
|
|
145
|
+
a. Only direct rule tests — misses collection and config integration.
|
|
146
|
+
|
|
147
|
+
#### Consequences
|
|
148
|
+
|
|
149
|
+
Multi-file test scenarios required: files with module-level `get_logger()` + files with/without `init_early_logging()`. Entry-point tests need Typer callback and `__main__` block fixtures.
|
|
150
|
+
|
|
151
|
+
## Requirements
|
|
152
|
+
|
|
153
|
+
### no-early-init detection
|
|
154
|
+
|
|
155
|
+
Scan all source files. Collect every module-level `get_logger()` call (file, line). Check if `init_early_logging()` call exists anywhere in the project. If module-level calls exist but `init_early_logging()` is absent: report violations.
|
|
156
|
+
|
|
157
|
+
### missing-init-in-entry detection
|
|
158
|
+
|
|
159
|
+
Scan all source files. Identify entry points (`if __name__ == "__main__"` blocks, `@*.callback()` decorated functions). For each entry point, check if `init_logging()` is called within its scope. If entry points exist without `init_logging()`: report violations.
|
|
160
|
+
|
|
161
|
+
### Integration
|
|
162
|
+
|
|
163
|
+
- Add rule names to `_KNOWN_RULE_NAMES` for `disable_rules` and `per-file-ignores` validation.
|
|
164
|
+
- Import new rule functions in `plugin.py`.
|
|
165
|
+
- Create items in `pytest_collection_modifyitems`, gated by `disable_rules` check.
|
|
166
|
+
- Rule functions must not import from `plugin`, `report`, `exemptions`, or `files` (architecture constraint).
|
|
167
|
+
|
|
168
|
+
## Appendix
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
id: project-wide-rules
|
|
172
|
+
description: "Two cross-file AST rules: no-early-init (module-level get_logger without init_early_logging) and missing-init-in-entry (CLI entry points without init_logging)"
|
|
173
|
+
git_rev: 08a35a6
|
|
174
|
+
created_at: "2026-05-07T00:00:00"
|
|
175
|
+
specs:
|
|
176
|
+
- .agents/impl_specs/project-wide-rules.md
|
|
177
|
+
target_tests:
|
|
178
|
+
- file: tests/impl_spec/test_project_wide_rules.py
|
|
179
|
+
tests:
|
|
180
|
+
- TestNoEarlyInit::test_no_early_init_clean_when_init_exists
|
|
181
|
+
- TestNoEarlyInit::test_no_early_init_violation_when_no_init
|
|
182
|
+
- TestNoEarlyInit::test_no_early_init_clean_when_no_get_logger
|
|
183
|
+
- TestNoEarlyInit::test_no_early_init_detects_structlog_get_logger
|
|
184
|
+
- TestNoEarlyInit::test_no_early_init_detects_bare_get_logger_after_import
|
|
185
|
+
- TestNoEarlyInit::test_no_early_init_ignores_function_level
|
|
186
|
+
- TestNoEarlyInit::test_no_early_init_reports_file_and_line
|
|
187
|
+
- TestMissingInitInEntry::test_missing_init_in_entry_clean_when_init_called
|
|
188
|
+
- TestMissingInitInEntry::test_missing_init_in_entry_violation_when_no_init
|
|
189
|
+
- TestMissingInitInEntry::test_missing_init_in_entry_clean_when_no_entry_points
|
|
190
|
+
- TestMissingInitInEntry::test_missing_init_in_entry_detects_main_block
|
|
191
|
+
- TestMissingInitInEntry::test_missing_init_in_entry_detects_typer_callback
|
|
192
|
+
- TestMissingInitInEntry::test_missing_init_in_entry_ignores_typer_command
|
|
193
|
+
- TestMissingInitInEntry::test_missing_init_in_entry_reports_file_and_line
|
|
194
|
+
```
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
# Quality Audit Report
|
|
2
|
+
|
|
3
|
+
## Human Summary
|
|
4
|
+
|
|
5
|
+
Quality meta-audit of pytest-stogger (a pytest plugin for AST-based logging convention checking). The project has clean code with modern Python patterns, a single runtime dependency, and a sociable test suite (zero mocks). Tool suppression is conservative and justified. Three small fixes were applied: PERF102 (`.items()` → `.values()`), PLW1510 (explicit `check=False`), and ERA001 (removed redundant comment). Coverage sits at 68% with significant gaps in 3 of 13 rules (including the flagship `logging-coverage` cross-file rule). Five class-based test groups violate modern pytest conventions. Overall grade: C (78/100).
|
|
6
|
+
|
|
7
|
+
## Completion Checklist
|
|
8
|
+
|
|
9
|
+
- [x] Entry point inventory + smoke test completed
|
|
10
|
+
- [x] Structural inventory completed (noqa, mock, complexity, test discovery, dependencies)
|
|
11
|
+
- [x] Quality gates collected (baseline + extreme)
|
|
12
|
+
- [x] All 4 investigation streams completed with structured review results
|
|
13
|
+
- [x] Tool tolerance audit produced with per-tool signals (ruff/ty/pytest)
|
|
14
|
+
- [x] Test collection integrity verified (all test files collected, no config hiding)
|
|
15
|
+
- [x] Skip/xfail/xpass audit completed (zero skips, zero xfails)
|
|
16
|
+
- [x] Test double strategy analyzed (mock:fake:golden:real per layer)
|
|
17
|
+
- [x] E2E coverage assessed for every entry point (PROVEN/SUSPECTED/UNKNOWN/BROKEN)
|
|
18
|
+
- [ ] Full CLI test not triggered — existing E2E evidence sufficient
|
|
19
|
+
- [x] Fixes applied for critical findings (PERF102, PLW1510, ERA001)
|
|
20
|
+
- [x] Fix loop completed (gates green after Round 1)
|
|
21
|
+
- [x] North Star generated from loaded skills
|
|
22
|
+
- [x] Course Corrections derived (Reality vs North Star diff)
|
|
23
|
+
- [ ] Git commit: pending
|
|
24
|
+
|
|
25
|
+
## Entry Point Inventory
|
|
26
|
+
|
|
27
|
+
| Entry Point | Type | Source | Smoke | E2E Status | Evidence |
|
|
28
|
+
|-------------|------|--------|-------|------------|----------|
|
|
29
|
+
| Plugin registration (pytest11) | entry-point | pyproject.toml:17-18 | PASS | PROVEN | pytester integration tests |
|
|
30
|
+
| pytest_addoption | hook | plugin.py:222 | PASS | PROVEN | Indirect via pytester |
|
|
31
|
+
| pytest_configure | hook | plugin.py:235 | PASS | PROVEN | Per-file-ignores, unknown rule warning |
|
|
32
|
+
| pytest_collection_modifyitems | hook | plugin.py:241 | PASS | PROVEN | pytester verifies items created |
|
|
33
|
+
| --stogger-source | CLI flag | plugin.py:224-227 | PASS | SUSPECTED | Config tested, flag never passed |
|
|
34
|
+
| --stogger-exclude | CLI flag | plugin.py:228-232 | PASS | SUSPECTED | Config tested, flag never passed |
|
|
35
|
+
| StoggerItem | pytest.Item | plugin.py:291 | PASS | PROVEN | Indirect via pytester |
|
|
36
|
+
| _CoverageItem / logging-coverage | pytest.Item | plugin.py:329 | PASS | UNKNOWN | No test at all |
|
|
37
|
+
| stogger_config fixture | fixture | plugin.py:370 | PASS | SUSPECTED | Never requested in test |
|
|
38
|
+
| stogger_source fixture | fixture | plugin.py:376 | PASS | SUSPECTED | Never requested in test |
|
|
39
|
+
| stogger_exclude fixture | fixture | plugin.py:382 | PASS | SUSPECTED | Never requested in test |
|
|
40
|
+
| stogger_test_dir fixture | fixture | plugin.py:388 | PASS | SUSPECTED | Never requested in test |
|
|
41
|
+
| source_files fixture | fixture | plugin.py:396 | PASS | SUSPECTED | Never requested in test |
|
|
42
|
+
| test_files fixture | fixture | plugin.py:404 | PASS | SUSPECTED | Never requested in test |
|
|
43
|
+
| 10/12 file-level rules | rules | rules.py | PASS | PROVEN | Direct + spec + parametrized |
|
|
44
|
+
| log-info-layer-restriction | rule | rules.py:385 | PASS | UNKNOWN | No direct test |
|
|
45
|
+
| logging-coverage | rule | rules.py:533 | PASS | UNKNOWN | No test at all |
|
|
46
|
+
| format_violations | function | report.py | PASS | PROVEN | 8 tests across 2 files |
|
|
47
|
+
| parse_inline_ignores | function | exemptions.py | PASS | PROVEN | 5 tests |
|
|
48
|
+
| walk_python_files | function | files.py | PASS | PROVEN | 3 tests |
|
|
49
|
+
| is_method_call / find_method_calls | functions | matchers.py | PASS | PROVEN | 2+ indirect tests |
|
|
50
|
+
|
|
51
|
+
**Summary**: 8 PROVEN, 8 SUSPECTED, 3 UNKNOWN, 0 BROKEN
|
|
52
|
+
|
|
53
|
+
## Tool Tolerance Audit
|
|
54
|
+
|
|
55
|
+
| Tool | Baseline | Extreme | Delta | Signal |
|
|
56
|
+
|------|----------|---------|-------|--------|
|
|
57
|
+
| ruff | 0 issues | **312 issues** | 312 suppressed | 🟢 green (justified) |
|
|
58
|
+
| ty | 0 errors | 0 errors | 0 | 🟢 green |
|
|
59
|
+
| pytest | 65 passed | 65 passed | 0 | 🟢 green |
|
|
60
|
+
|
|
61
|
+
### ruff Suppression Breakdown (src/ only — 72 findings)
|
|
62
|
+
|
|
63
|
+
| Category | Count | Verdict |
|
|
64
|
+
|----------|-------|---------|
|
|
65
|
+
| DOC201 (missing Returns in docstring) | 31 | Legitimate — docstrings describe behavior |
|
|
66
|
+
| CPY001 (missing copyright) | 7 | Legitimate — no copyright headers |
|
|
67
|
+
| E501 (line too long) | 13 | Legitimate — 100 char limit vs default 88 |
|
|
68
|
+
| D1xx (missing docstrings) | 8 | Legitimate — pytest hooks, __init__ |
|
|
69
|
+
| SLF001 (accessing _nodeid) | 2 | Legitimate — required by pytest.Item |
|
|
70
|
+
| ANN401 (**kw: Any) | 2 | Legitimate — required by pytest.Item subclass |
|
|
71
|
+
| **PERF102 (use .values())** | **1** | **Fixed** — plugin.py:180 |
|
|
72
|
+
| S404 (subprocess import) | 1 | Legitimate — complexipy subprocess isolation |
|
|
73
|
+
| S603 (subprocess.run) | 1 | Legitimate — runs sys.executable |
|
|
74
|
+
| **PLW1510 (no check=)** | **1** | **Fixed** — rules.py:439 |
|
|
75
|
+
| C901 (too complex) | 1 | Legitimate — flagged by complexipy in tox |
|
|
76
|
+
| RUF052 (dummy var accessed) | 1 | Legitimate — _loc_re accessed by closure |
|
|
77
|
+
| Other minor | 5 | Legitimate/questionable |
|
|
78
|
+
|
|
79
|
+
**No critical hiding detected.** All suppressed categories are legitimate project decisions.
|
|
80
|
+
|
|
81
|
+
### noqa/type:ignore density
|
|
82
|
+
|
|
83
|
+
- noqa comments: 0 (zero)
|
|
84
|
+
- type:ignore comments: 1 (rules.py:464 — intentional for json.loads return)
|
|
85
|
+
- Signal: 🟢 green
|
|
86
|
+
|
|
87
|
+
## Test Collection Integrity
|
|
88
|
+
|
|
89
|
+
| Check | Result | Signal |
|
|
90
|
+
|-------|--------|--------|
|
|
91
|
+
| Tests on disk | 3 files | — |
|
|
92
|
+
| Tests collected | 57 nodes + 8 plugin items | — |
|
|
93
|
+
| Uncollected files | 0 | 🟢 green |
|
|
94
|
+
| Collection errors | 0 | 🟢 green |
|
|
95
|
+
| Config exclusions | None (no [tool.pytest.ini_options]) | 🟢 green |
|
|
96
|
+
| conftest hooks modifying collection | None | 🟢 green |
|
|
97
|
+
|
|
98
|
+
## Skip/Xfail/Xpass Audit
|
|
99
|
+
|
|
100
|
+
| Category | Count | Signal |
|
|
101
|
+
|----------|-------|--------|
|
|
102
|
+
| @pytest.mark.skip | 0 | 🟢 green |
|
|
103
|
+
| @pytest.mark.skipif | 0 | 🟢 green |
|
|
104
|
+
| @pytest.mark.xfail | 0 | 🟢 green |
|
|
105
|
+
| XPASS | 0 | 🟢 green |
|
|
106
|
+
| Lazy skips | 0 | 🟢 green |
|
|
107
|
+
| Flaky-hidden | 0 | 🟢 green |
|
|
108
|
+
|
|
109
|
+
## Test Double Strategy
|
|
110
|
+
|
|
111
|
+
| Layer | Mock | Spec'd Mock | Fake | Golden | Real | Total |
|
|
112
|
+
|-------|------|-------------|------|--------|------|-------|
|
|
113
|
+
| Unit | 0 | 0 | 0 | 0 | 57 | 57 |
|
|
114
|
+
| Integration (pytester) | 0 | 0 | 0 | 0 | 8 | 8 |
|
|
115
|
+
|
|
116
|
+
- Tautological tests (mock theater): 0
|
|
117
|
+
- Golden file smell: 0
|
|
118
|
+
- Mock density hotspots: None
|
|
119
|
+
- Overall double strategy verdict: **Sociable test suite — zero mocks, all real AST/file I/O**
|
|
120
|
+
- RED FLAGS: 0/10
|
|
121
|
+
- Signal: 🟢 green
|
|
122
|
+
|
|
123
|
+
## Test Structure Summary
|
|
124
|
+
|
|
125
|
+
- Total tests: 65 (57 collected + 8 plugin-generated)
|
|
126
|
+
- Distribution: unit 57, integration 8, e2e 0
|
|
127
|
+
- RED FLAGS: 0/10
|
|
128
|
+
- Signal: 🟡 orange (class-based tests, coverage gaps)
|
|
129
|
+
|
|
130
|
+
### Test Architecture Violation
|
|
131
|
+
|
|
132
|
+
5 class-based test groups in `tests/impl_spec/test_stogger_rules_fixes.py`:
|
|
133
|
+
- TestBoundLogTracking
|
|
134
|
+
- TestInlineExemption
|
|
135
|
+
- TestConfigRestructure
|
|
136
|
+
- TestComplexityThreshold
|
|
137
|
+
- TestErrorCommunication
|
|
138
|
+
|
|
139
|
+
Forbidden by modern pytest conventions (python-audit: "Class-based tests → forbidden, use plain test_ functions").
|
|
140
|
+
|
|
141
|
+
## Test Coverage
|
|
142
|
+
|
|
143
|
+
| Module | Coverage | Missing Lines | Signal |
|
|
144
|
+
|--------|----------|---------------|--------|
|
|
145
|
+
| __init__.py | 100% | — | 🟢 green |
|
|
146
|
+
| exemptions.py | 73% | 12-20, 37 | 🟡 orange |
|
|
147
|
+
| files.py | 74% | 3-13, 53-54 | 🟡 orange |
|
|
148
|
+
| matchers.py | 74% | 3-12, 43, 47 | 🟡 orange |
|
|
149
|
+
| plugin.py | 56% | 21-109, 112-121, 135-406 | 🔴 red |
|
|
150
|
+
| report.py | 79% | 3-8, 40, 57, 60 | 🟡 orange |
|
|
151
|
+
| rules.py | 74% | 21-38, 54-72, 84-93, 101-593 | 🟡 orange |
|
|
152
|
+
|
|
153
|
+
- Overall coverage: 68%
|
|
154
|
+
- Modules < 50%: none (but plugin.py at 56% is close)
|
|
155
|
+
- Entry points with 0% coverage: logging-coverage, log-info-layer-restriction
|
|
156
|
+
- Signal: 🟡 orange
|
|
157
|
+
|
|
158
|
+
## Duration Anomalies
|
|
159
|
+
|
|
160
|
+
- Total suite time: 2.20s (wall time 4s)
|
|
161
|
+
- Duration stats: P50=<5ms, P90=~0.04s, P95=~0.08s, P99=0.34s
|
|
162
|
+
|
|
163
|
+
| Category | Count | Details |
|
|
164
|
+
|----------|-------|---------|
|
|
165
|
+
| EXTREME OUTLIER | 0 | — |
|
|
166
|
+
| FAKE SLOW | 0 | — |
|
|
167
|
+
| HIDDEN SLOW | 0 | — |
|
|
168
|
+
| Zero-duration | 0 | — |
|
|
169
|
+
|
|
170
|
+
- Signal: 🟢 green — no duration anomalies
|
|
171
|
+
|
|
172
|
+
## Dependency Audit
|
|
173
|
+
|
|
174
|
+
| Category | Count | Signal |
|
|
175
|
+
|----------|-------|--------|
|
|
176
|
+
| Forbidden libraries | 0 | 🟢 green |
|
|
177
|
+
| Stdlib reinvention | 0 | 🟢 green |
|
|
178
|
+
| Unused dependencies | 0 | 🟢 green |
|
|
179
|
+
| Missing blessed libraries | 0 | 🟢 green |
|
|
180
|
+
| Available but unused | 0 | 🟢 green |
|
|
181
|
+
|
|
182
|
+
- Single runtime dependency: complexipy (used in rules.py)
|
|
183
|
+
- All stdlib modules used correctly: tomllib, pathlib, collections.Counter, dataclasses
|
|
184
|
+
- Signal: 🟢 green
|
|
185
|
+
|
|
186
|
+
## E2E Coverage Assessment
|
|
187
|
+
|
|
188
|
+
- PROVEN: 8 entry points (plugin hooks, StoggerItem, 10 rules, format_violations, helpers)
|
|
189
|
+
- SUSPECTED: 8 entry points (2 CLI flags, 6 fixtures)
|
|
190
|
+
- UNKNOWN: 3 entry points (_CoverageItem, log-info-layer-restriction, logging-coverage)
|
|
191
|
+
- BROKEN: 0
|
|
192
|
+
- Full CLI test triggered: NO — existing evidence sufficient
|
|
193
|
+
- Signal: 🟡 orange
|
|
194
|
+
|
|
195
|
+
## Stream Signals
|
|
196
|
+
|
|
197
|
+
| Stream | Signal | Summary |
|
|
198
|
+
|--------|--------|---------|
|
|
199
|
+
| A: Code Architecture | 🟡 orange | Clean layers but no enforcement, 5 complexity hotspots |
|
|
200
|
+
| B: Code Quality / Tool Tolerance | 🟢 green | Zero noqa, 1 type:ignore, no forbidden libs, justified suppression |
|
|
201
|
+
| C: Test Structure | 🟡 orange | Sociable (0 mocks) but 68% coverage, class-based tests, 3 rules untested |
|
|
202
|
+
| D: E2E + Production Reality | 🟡 orange | Plugin loads and works, but flagship rules untested |
|
|
203
|
+
|
|
204
|
+
## Architectural North Star
|
|
205
|
+
|
|
206
|
+
| Dimension | True North | Source |
|
|
207
|
+
|-----------|------------|--------|
|
|
208
|
+
| Dependency Landscape | Minimal surface, blessed libs only | python-dev |
|
|
209
|
+
| Test Pyramid | 70/25/5 (unit/integration/e2e), sociable testing | python-tests |
|
|
210
|
+
| Type System | Type-first, PEP 604, PEP 695, no legacy | python-dev |
|
|
211
|
+
| Architecture | Layer boundaries enforced by pytest-archon | python-architecture |
|
|
212
|
+
| Code Patterns | dataclass(slots=True), pattern matching, no async | python-dev |
|
|
213
|
+
| Tool Config | ruff with extended rules, zero noqa | python-audit |
|
|
214
|
+
|
|
215
|
+
## Course Corrections
|
|
216
|
+
|
|
217
|
+
### NAV-1 Architecture Enforcement
|
|
218
|
+
- **Current heading:** Clean layers documented in AGENTS.md, no test_architecture.py
|
|
219
|
+
- **True north:** pytest-archon rules enforcing layer boundaries
|
|
220
|
+
- **Correction:** Introduce test_architecture.py with layer boundary rules
|
|
221
|
+
|
|
222
|
+
### NAV-2 Test Architecture
|
|
223
|
+
- **Current heading:** 5 class-based test groups (CRITICAL per python-audit)
|
|
224
|
+
- **True north:** Plain test_ functions, classes only for shared fixtures
|
|
225
|
+
- **Correction:** Convert TestBoundLogTracking, TestInlineExemption, TestConfigRestructure, TestComplexityThreshold, TestErrorCommunication to plain functions
|
|
226
|
+
|
|
227
|
+
### NAV-3 Coverage
|
|
228
|
+
- **Current heading:** 68% total, 3 of 13 rules untested including flagship logging-coverage
|
|
229
|
+
- **True north:** Critical entry points proven, 70%+ coverage
|
|
230
|
+
- **Correction:** Add tests for logging-coverage, log-info-layer-restriction, fixtures, CLI flags
|
|
231
|
+
|
|
232
|
+
### NAV-4 Complexity
|
|
233
|
+
- **Current heading:** 5 functions exceed CC 15, highest at 37
|
|
234
|
+
- **True north:** CC ≤ 15 per function
|
|
235
|
+
- **Correction:** Refactor complex rule functions to extract helpers
|
|
236
|
+
|
|
237
|
+
### NAV-5 Dependency Landscape
|
|
238
|
+
- **Current heading:** ON COURSE — single runtime dep, no forbidden libs
|
|
239
|
+
- **True north:** Minimal surface, blessed libs
|
|
240
|
+
- **Correction:** None needed
|
|
241
|
+
|
|
242
|
+
### NAV-6 Type System
|
|
243
|
+
- **Current heading:** ON COURSE — modern hints, PEP 604, ty clean
|
|
244
|
+
- **True north:** Type-first development
|
|
245
|
+
- **Correction:** None needed
|
|
246
|
+
|
|
247
|
+
### NAV-7 Tool Configuration
|
|
248
|
+
- **Current heading:** PERF102 and PLW1510 were suppressed — now fixed
|
|
249
|
+
- **True north:** Tool config catches actionable issues
|
|
250
|
+
- **Correction:** Consider enabling PERF and PLW rule categories
|
|
251
|
+
|
|
252
|
+
### NAV-8 E2E Credibility
|
|
253
|
+
- **Current heading:** Smoke test import verification had stale API names
|
|
254
|
+
- **True north:** E2E tests verify real production behavior
|
|
255
|
+
- **Correction:** Smoke test was a meta-audit artifact, not project code — no project fix needed
|
|
256
|
+
|
|
257
|
+
### NAV-9 Code Patterns
|
|
258
|
+
- **Current heading:** ON COURSE — dataclass(slots=True), modern patterns
|
|
259
|
+
- **True north:** Modern Python idioms
|
|
260
|
+
- **Correction:** None needed
|
|
261
|
+
|
|
262
|
+
- NAV-items total: 9
|
|
263
|
+
- Dimensions on course (no deviation): 4 (NAV-5, NAV-6, NAV-8, NAV-9)
|
|
264
|
+
- Dimensions off course: 5 (NAV-1, NAV-2, NAV-3, NAV-4, NAV-7)
|
|
265
|
+
- Signal: 🟡 orange
|
|
266
|
+
|
|
267
|
+
## Test Automation
|
|
268
|
+
|
|
269
|
+
- Task runner: tox (configured in pyproject.toml)
|
|
270
|
+
- Single-command gate: YES — `uv run tox -p`
|
|
271
|
+
- Default coverage: full (no excluded markers)
|
|
272
|
+
- Signal: 🟢 green
|
|
273
|
+
|
|
274
|
+
## Infrastructure Recommendations
|
|
275
|
+
|
|
276
|
+
- **Coverage pipeline**: Already configured — `tox -e cov` runs pytest with coverage
|
|
277
|
+
- **Duration regression**: Add `--durations=10` to tox test env to catch slow test regressions
|
|
278
|
+
- **Architecture enforcement**: Add `pytest-archon` with layer boundary rules
|
|
279
|
+
|
|
280
|
+
## Critical Findings Fixed
|
|
281
|
+
|
|
282
|
+
| Finding | Fix | File |
|
|
283
|
+
|---------|-----|------|
|
|
284
|
+
| PERF102: pfi.items() → pfi.values() | Changed to `.values()` | src/pytest_stogger/plugin.py:180 |
|
|
285
|
+
| PLW1510: subprocess.run without check=False | Added `check=False` | src/pytest_stogger/rules.py:439 |
|
|
286
|
+
| ERA001: Redundant comment above typed data | Removed comment | tests/impl_spec/test_quality_investments.py:47 |
|
|
287
|
+
|
|
288
|
+
## Full CLI Test Trace
|
|
289
|
+
|
|
290
|
+
Full CLI test not triggered — existing E2E evidence sufficient.
|
|
291
|
+
|
|
292
|
+
## Code Volume
|
|
293
|
+
|
|
294
|
+
| File | Change |
|
|
295
|
+
|------|--------|
|
|
296
|
+
| src/pytest_stogger/plugin.py | +1/-1 |
|
|
297
|
+
| src/pytest_stogger/rules.py | +1/-0 |
|
|
298
|
+
| tests/impl_spec/test_quality_investments.py | +0/-1 |
|
|
299
|
+
|
|
300
|
+
Total: +2/-2 across 3 files. Proportional to findings.
|
|
301
|
+
|
|
302
|
+
## Post-Fix Quality Gates
|
|
303
|
+
|
|
304
|
+
| Tool | Result |
|
|
305
|
+
|------|--------|
|
|
306
|
+
| tox | ✅ OK (py: OK) |
|
|
307
|
+
| ruff | ✅ 0 issues |
|
|
308
|
+
| ty | ✅ 0 errors |
|
|
309
|
+
| pytest | ✅ 65 passed |
|
|
310
|
+
| E2E smoke | ✅ PASS |
|
|
311
|
+
|
|
312
|
+
## Recommendations
|
|
313
|
+
|
|
314
|
+
1. **Medium priority**: Add tests for `logging-coverage` (flagship cross-file rule) and `log-info-layer-restriction`
|
|
315
|
+
2. **Medium priority**: Convert 5 class-based test groups to plain test_ functions
|
|
316
|
+
3. **Low priority**: Add test_architecture.py with pytest-archon layer boundary rules
|
|
317
|
+
4. **Low priority**: Consider enabling PERF and PLW ruff rule categories
|
|
318
|
+
5. **Low priority**: Refactor complex rule functions (CC 15+) to extract helpers
|
|
319
|
+
|
|
320
|
+
## Raw Data Location
|
|
321
|
+
|
|
322
|
+
`.agents/tmp/quality/` — inventory/, baseline/, extreme/, analysis/, e2e/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-stogger
|
|
3
|
+
Version: 2026.5.12
|
|
4
|
+
Summary: AST-based convention checking helpers for pytest.
|
|
5
|
+
Classifier: Framework :: Pytest
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
8
|
+
Requires-Python: >=3.13
|
|
9
|
+
Requires-Dist: complexipy
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# pytest-stogger
|
|
2
|
+
|
|
3
|
+
Automatic AST-based logging convention checking for pytest.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
uv add --dev pytest-stogger
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
uv run pytest
|
|
12
|
+
|
|
13
|
+
The plugin creates one test item per source file plus a cross-file logging-coverage check. No test files to write.
|
|
14
|
+
|
|
15
|
+
Example output:
|
|
16
|
+
|
|
17
|
+
src/mypackage/__init__.py::stogger PASSED
|
|
18
|
+
src/mypackage/core.py::stogger PASSED
|
|
19
|
+
stogger::logging-coverage PASSED
|
|
20
|
+
|
|
21
|
+
## Documentation
|
|
22
|
+
|
|
23
|
+
Full documentation is built with Sphinx:
|
|
24
|
+
|
|
25
|
+
tox -e docs
|
|
26
|
+
|
|
27
|
+
Open `docs/_build/html/index.html` after the build.
|
|
28
|
+
|
|
29
|
+
- **Built-in Rules** — all 13 rules with explanations
|
|
30
|
+
- **Configuration** — pyproject.toml options, CLI overrides, inline exemptions
|
|
31
|
+
- **Fixtures and Helpers** — session-scoped fixtures and utility functions for custom checks
|
|
32
|
+
- **API Reference** — auto-generated from source docstrings
|
|
33
|
+
|
|
34
|
+
## Quality Gate
|
|
35
|
+
|
|
36
|
+
tox # runs fix → test → cov sequentially
|
|
37
|
+
tox -p # runs fix → test → cov in parallel where possible
|
|
38
|
+
tox -e fix # ruff fix + format, ty check, complexipy, vulture
|
|
39
|
+
tox -e test # pytest fast run
|
|
40
|
+
tox -e cov # pytest with coverage report
|
|
41
|
+
|
|
42
|
+
## Testimonials
|
|
43
|
+
|
|
44
|
+
> We upgraded from a dev build to 2026.5.4. Previous version: 0 violations. After upgrade: 5 failures, 3 warnings. Turns out the old version simply didn't have the rules yet. Every single failure was legitimate. Total fix: 7 files, net -7 lines. The plugin literally made the codebase shorter while making it better.
|
|
45
|
+
>
|
|
46
|
+
> — nixflow maintainers, after discovering their "clean" codebase wasn't
|
|
47
|
+
|
|
48
|
+
> pytest-stogger nervt wie ein röchelnder Raucher über every damn event ID — und dann sitzt du um 3 Uhr Nacht vor einem Production-Incident und die Logs sind plötzlich lesbar. 5/5, würde mich wieder maßregeln lassen.
|
|
49
|
+
>
|
|
50
|
+
> — senior SRE, after their first on-call rotation with enforced conventions
|
|
51
|
+
|
|
52
|
+
> Rule 6 flagged three sequential `log.info()` calls repeating the same keys. My first reaction was "that's fine, it works." Then I refactored to `log.bind()` and deleted 12 lines. The plugin understood my code better than I did.
|
|
53
|
+
>
|
|
54
|
+
> — backend developer, reluctantly impressed
|
|
55
|
+
|
|
56
|
+
> We added pytest-stogger to a 40k-line codebase. 87 violations. Two hours later: 87 violations, zero remaining. The CI now enforces what code review couldn't. Our logging is structured, consistent, and actually useful in production. I'm annoyed it took us this long.
|
|
57
|
+
>
|
|
58
|
+
> — tech lead, after the third logging-related incident in one quarter
|
|
59
|
+
|
|
60
|
+
> The next diabolical idea from the pytest-stogger camp: now I'm getting errors on the meta level because I'm suppressing errors? Where does this end? Maybe I should just take logging seriously for once.
|
|
61
|
+
>
|
|
62
|
+
> — principal engineer, after discovering log-suppression-budget
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Sphinx configuration for pytest-stogger."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib.metadata import version as _get_version
|
|
6
|
+
|
|
7
|
+
# -- Project information -----------------------------------------------------
|
|
8
|
+
|
|
9
|
+
project = "pytest-stogger"
|
|
10
|
+
release = _get_version("pytest-stogger")
|
|
11
|
+
version = ".".join(release.split(".")[:2])
|
|
12
|
+
copyright = "2025, pytest-stogger contributors" # noqa: A001
|
|
13
|
+
author = "pytest-stogger contributors"
|
|
14
|
+
|
|
15
|
+
# -- Extensions --------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
extensions = [
|
|
18
|
+
"myst_parser",
|
|
19
|
+
"autoapi.extension",
|
|
20
|
+
"sphinx.ext.viewcode",
|
|
21
|
+
"sphinx_autodoc_typehints",
|
|
22
|
+
"sphinx_llm.txt",
|
|
23
|
+
"sphinx_copybutton",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
# Required: autoapi generates .rst files internally.
|
|
27
|
+
source_suffix = {".md": "markdown", ".rst": "restructuredtext"}
|
|
28
|
+
|
|
29
|
+
# -- autoapi -----------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
autoapi_type = "python"
|
|
32
|
+
autoapi_dirs = ["../src/pytest_stogger"]
|
|
33
|
+
autoapi_file_patterns = ["*.py"]
|
|
34
|
+
autoapi_generate_api_docs = True
|
|
35
|
+
autoapi_add_toctree_entry = True
|
|
36
|
+
autoapi_keep_files = True
|
|
37
|
+
|
|
38
|
+
autoapi_options = [
|
|
39
|
+
"members",
|
|
40
|
+
"undoc-members",
|
|
41
|
+
"show-inheritance",
|
|
42
|
+
"show-module-summary",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
autodoc_typehints = "description"
|
|
46
|
+
|
|
47
|
+
# Skip private members (underscore prefix) in autoapi output
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def autoapi_skip_member(
|
|
51
|
+
_app, _what: str, name: str, _obj, skip: bool, _options, # noqa: FBT001
|
|
52
|
+
) -> bool:
|
|
53
|
+
if name.startswith("_"):
|
|
54
|
+
return True
|
|
55
|
+
return skip
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def setup(app): # noqa: D103
|
|
59
|
+
app.connect("autoapi-skip-member", autoapi_skip_member)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# -- MyST --------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
myst_enable_extensions = [
|
|
65
|
+
"colon_fence",
|
|
66
|
+
"deflist",
|
|
67
|
+
"fieldlist",
|
|
68
|
+
]
|
|
69
|
+
myst_heading_anchors = 3
|
|
70
|
+
|
|
71
|
+
# -- sphinx-llm --------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
llms_txt_build_parallel = True
|
|
74
|
+
llms_txt_full_build = True
|
|
75
|
+
llms_txt_description = "pytest-stogger — AST-based logging convention checking for pytest"
|
|
76
|
+
|
|
77
|
+
# -- sphinx-copybutton -------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | "
|
|
80
|
+
copybutton_prompt_is_regexp = True
|
|
81
|
+
|
|
82
|
+
# -- Theme -------------------------------------------------------------------
|
|
83
|
+
|
|
84
|
+
html_theme = "furo"
|
|
85
|
+
html_title = "pytest-stogger"
|