tqlint 0.4.2__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.
- tqlint-0.4.2/PKG-INFO +119 -0
- tqlint-0.4.2/README.md +89 -0
- tqlint-0.4.2/pyproject.toml +153 -0
- tqlint-0.4.2/src/tq/__init__.py +1 -0
- tqlint-0.4.2/src/tq/cli/__init__.py +1 -0
- tqlint-0.4.2/src/tq/cli/main.py +283 -0
- tqlint-0.4.2/src/tq/config/__init__.py +1 -0
- tqlint-0.4.2/src/tq/config/loader.py +356 -0
- tqlint-0.4.2/src/tq/config/models.py +70 -0
- tqlint-0.4.2/src/tq/discovery/__init__.py +1 -0
- tqlint-0.4.2/src/tq/discovery/filesystem.py +47 -0
- tqlint-0.4.2/src/tq/discovery/index.py +57 -0
- tqlint-0.4.2/src/tq/engine/__init__.py +1 -0
- tqlint-0.4.2/src/tq/engine/context.py +45 -0
- tqlint-0.4.2/src/tq/engine/models.py +107 -0
- tqlint-0.4.2/src/tq/engine/rule_id.py +36 -0
- tqlint-0.4.2/src/tq/engine/runner.py +82 -0
- tqlint-0.4.2/src/tq/reporting/__init__.py +1 -0
- tqlint-0.4.2/src/tq/reporting/json.py +56 -0
- tqlint-0.4.2/src/tq/reporting/terminal.py +73 -0
- tqlint-0.4.2/src/tq/rules/__init__.py +1 -0
- tqlint-0.4.2/src/tq/rules/contracts.py +29 -0
- tqlint-0.4.2/src/tq/rules/file_too_large.py +96 -0
- tqlint-0.4.2/src/tq/rules/mapping_missing_test.py +127 -0
- tqlint-0.4.2/src/tq/rules/orphaned_test.py +115 -0
- tqlint-0.4.2/src/tq/rules/qualifiers.py +47 -0
- tqlint-0.4.2/src/tq/rules/structure_mismatch.py +139 -0
tqlint-0.4.2/PKG-INFO
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tqlint
|
|
3
|
+
Version: 0.4.2
|
|
4
|
+
Summary: tq inspects a codebase's tests and enforces quality rules so tests remain discoverable, focused, actionable, and maintainable
|
|
5
|
+
Keywords: testing,lint,pytest,quality,developer-tools
|
|
6
|
+
Author: Stephen Lewis
|
|
7
|
+
Author-email: Stephen Lewis <31178492+stelewis@users.noreply.github.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Dist: click>=8.3.1
|
|
22
|
+
Requires-Dist: pyyaml>=6.0.3
|
|
23
|
+
Requires-Dist: rich>=14.3.2
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Project-URL: Homepage, https://github.com/stelewis/tq
|
|
26
|
+
Project-URL: Repository, https://github.com/stelewis/tq
|
|
27
|
+
Project-URL: Issues, https://github.com/stelewis/tq/issues
|
|
28
|
+
Project-URL: Changelog, https://github.com/stelewis/tq/blob/main/CHANGELOG.md
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# `tq` - Test Quality Toolkit
|
|
32
|
+
|
|
33
|
+
`tq` inspects a codebase's tests and enforces quality rules so tests remain discoverable, focused, actionable, and maintainable.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
PyPI distribution name: `tqlint`
|
|
38
|
+
|
|
39
|
+
Add to a project:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
uv add --dev tqlint
|
|
43
|
+
uv run tq check
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Run without installing (ephemeral):
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
uvx tqlint check
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Install as a persistent global tool:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
uv tool install tqlint
|
|
56
|
+
tq check
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Note: `uvx tq check` is not available because the `tq` package name on PyPI is owned by another project.
|
|
60
|
+
|
|
61
|
+
## Current scope
|
|
62
|
+
|
|
63
|
+
`tq` currently analyzes Python source and Python tests (`.py`) only.
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
Run checks:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
uv run tq check
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Emit machine-readable diagnostics:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
uv run tq check --output-format json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Configuration
|
|
80
|
+
|
|
81
|
+
Configure `tq` in `pyproject.toml` under `[tool.tq]`:
|
|
82
|
+
|
|
83
|
+
```toml
|
|
84
|
+
[tool.tq]
|
|
85
|
+
package = "tq"
|
|
86
|
+
source_root = "src"
|
|
87
|
+
test_root = "tests"
|
|
88
|
+
ignore_init_modules = true
|
|
89
|
+
max_test_file_non_blank_lines = 600
|
|
90
|
+
qualifier_strategy = "allowlist"
|
|
91
|
+
allowed_qualifiers = ["regression"]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Documentation
|
|
95
|
+
|
|
96
|
+
- See [docs/developer/tools/tq_check.md](docs/developer/tools/tq_check.md) for tool usage and configuration details.
|
|
97
|
+
- See [docs/developer/tools/rules.md](docs/developer/tools/rules.md) for built-in rules.
|
|
98
|
+
|
|
99
|
+
## Development
|
|
100
|
+
|
|
101
|
+
Contribution guidelines are in [CONTRIBUTING.md](CONTRIBUTING.md). For local development, follow below steps.
|
|
102
|
+
|
|
103
|
+
Install dependencies:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
uv sync
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Install pre-commit hooks (including `pre-commit`, `commit-msg`, and `pre-push`):
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
uv run prek install
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Run tests:
|
|
116
|
+
|
|
117
|
+
```sh
|
|
118
|
+
uv run pytest -q
|
|
119
|
+
```
|
tqlint-0.4.2/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# `tq` - Test Quality Toolkit
|
|
2
|
+
|
|
3
|
+
`tq` inspects a codebase's tests and enforces quality rules so tests remain discoverable, focused, actionable, and maintainable.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
PyPI distribution name: `tqlint`
|
|
8
|
+
|
|
9
|
+
Add to a project:
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
uv add --dev tqlint
|
|
13
|
+
uv run tq check
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Run without installing (ephemeral):
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
uvx tqlint check
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Install as a persistent global tool:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
uv tool install tqlint
|
|
26
|
+
tq check
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Note: `uvx tq check` is not available because the `tq` package name on PyPI is owned by another project.
|
|
30
|
+
|
|
31
|
+
## Current scope
|
|
32
|
+
|
|
33
|
+
`tq` currently analyzes Python source and Python tests (`.py`) only.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
Run checks:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
uv run tq check
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Emit machine-readable diagnostics:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
uv run tq check --output-format json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Configuration
|
|
50
|
+
|
|
51
|
+
Configure `tq` in `pyproject.toml` under `[tool.tq]`:
|
|
52
|
+
|
|
53
|
+
```toml
|
|
54
|
+
[tool.tq]
|
|
55
|
+
package = "tq"
|
|
56
|
+
source_root = "src"
|
|
57
|
+
test_root = "tests"
|
|
58
|
+
ignore_init_modules = true
|
|
59
|
+
max_test_file_non_blank_lines = 600
|
|
60
|
+
qualifier_strategy = "allowlist"
|
|
61
|
+
allowed_qualifiers = ["regression"]
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Documentation
|
|
65
|
+
|
|
66
|
+
- See [docs/developer/tools/tq_check.md](docs/developer/tools/tq_check.md) for tool usage and configuration details.
|
|
67
|
+
- See [docs/developer/tools/rules.md](docs/developer/tools/rules.md) for built-in rules.
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
Contribution guidelines are in [CONTRIBUTING.md](CONTRIBUTING.md). For local development, follow below steps.
|
|
72
|
+
|
|
73
|
+
Install dependencies:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
uv sync
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Install pre-commit hooks (including `pre-commit`, `commit-msg`, and `pre-push`):
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
uv run prek install
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Run tests:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
uv run pytest -q
|
|
89
|
+
```
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tqlint"
|
|
3
|
+
version = "0.4.2"
|
|
4
|
+
description = "tq inspects a codebase's tests and enforces quality rules so tests remain discoverable, focused, actionable, and maintainable"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
keywords = ["testing", "lint", "pytest", "quality", "developer-tools"]
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Development Status :: 3 - Alpha",
|
|
10
|
+
"Intended Audience :: Developers",
|
|
11
|
+
"License :: OSI Approved :: MIT License",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Programming Language :: Python :: 3.14",
|
|
18
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
19
|
+
"Topic :: Software Development :: Testing",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
]
|
|
22
|
+
authors = [
|
|
23
|
+
{ name = "Stephen Lewis", email = "31178492+stelewis@users.noreply.github.com" }
|
|
24
|
+
]
|
|
25
|
+
requires-python = ">=3.11"
|
|
26
|
+
dependencies = [
|
|
27
|
+
"click>=8.3.1",
|
|
28
|
+
"pyyaml>=6.0.3",
|
|
29
|
+
"rich>=14.3.2",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.scripts]
|
|
33
|
+
tq = "tq.cli.main:main"
|
|
34
|
+
tqlint = "tq.cli.main:main"
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/stelewis/tq"
|
|
38
|
+
Repository = "https://github.com/stelewis/tq"
|
|
39
|
+
Issues = "https://github.com/stelewis/tq/issues"
|
|
40
|
+
Changelog = "https://github.com/stelewis/tq/blob/main/CHANGELOG.md"
|
|
41
|
+
|
|
42
|
+
[build-system]
|
|
43
|
+
requires = ["uv_build>=0.10.4,<0.11.0"]
|
|
44
|
+
build-backend = "uv_build"
|
|
45
|
+
|
|
46
|
+
[tool.uv.build-backend]
|
|
47
|
+
module-name = "tq"
|
|
48
|
+
|
|
49
|
+
[dependency-groups]
|
|
50
|
+
dev = [
|
|
51
|
+
"bandit>=1.9.3",
|
|
52
|
+
"commitizen>=4.9.1",
|
|
53
|
+
"detect-secrets>=1.5.0",
|
|
54
|
+
"pip-audit>=2.10.0",
|
|
55
|
+
"prek>=0.3.1",
|
|
56
|
+
"pytest>=9.0.2",
|
|
57
|
+
"pytest-mock>=3.15.1",
|
|
58
|
+
"ruff>=0.15.0",
|
|
59
|
+
"ty>=0.0.14",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.ruff]
|
|
63
|
+
line-length = 88
|
|
64
|
+
src = ["src"]
|
|
65
|
+
extend-exclude = ["tmp"]
|
|
66
|
+
|
|
67
|
+
[tool.ruff.format]
|
|
68
|
+
quote-style = "double"
|
|
69
|
+
indent-style = "space"
|
|
70
|
+
docstring-code-format = true
|
|
71
|
+
|
|
72
|
+
[tool.ruff.lint]
|
|
73
|
+
select = ["ALL"]
|
|
74
|
+
ignore = ["COM812"] # Per docs guidance: recommended to ignore when using ruff format as formatter enforces consistent use of trailing commas
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint.flake8-tidy-imports]
|
|
77
|
+
ban-relative-imports = "all"
|
|
78
|
+
|
|
79
|
+
[tool.ruff.lint.per-file-ignores]
|
|
80
|
+
"tests/**/*.py" = ["S101", "PLR2004"]
|
|
81
|
+
|
|
82
|
+
[tool.ruff.lint.pylint]
|
|
83
|
+
max-returns = 10
|
|
84
|
+
|
|
85
|
+
[tool.pytest.ini_options]
|
|
86
|
+
addopts = [
|
|
87
|
+
"--import-mode=importlib",
|
|
88
|
+
"-m",
|
|
89
|
+
"not integration and not e2e",
|
|
90
|
+
]
|
|
91
|
+
testpaths = ["tests"]
|
|
92
|
+
markers = [
|
|
93
|
+
"e2e: mark end-to-end tests",
|
|
94
|
+
"golden: deterministic golden/snapshot fixture tests (offline)",
|
|
95
|
+
"integration: mark a test as an integration test",
|
|
96
|
+
"regression: mark tests that exercise known regressions",
|
|
97
|
+
"smoke: mark quick smoke tests",
|
|
98
|
+
"slow: mark slow tests that may be skipped in fast CI jobs",
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
[tool.ruff.lint.pydocstyle]
|
|
102
|
+
convention = "google"
|
|
103
|
+
|
|
104
|
+
[tool.commitizen]
|
|
105
|
+
name = "cz_conventional_commits"
|
|
106
|
+
version_provider = "uv"
|
|
107
|
+
version_scheme = "semver"
|
|
108
|
+
tag_format = "$version"
|
|
109
|
+
update_changelog_on_bump = false
|
|
110
|
+
changelog_file = "CHANGELOG.md"
|
|
111
|
+
template = "CHANGELOG.md.j2"
|
|
112
|
+
change_type_order = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"]
|
|
113
|
+
gpg_sign = true
|
|
114
|
+
major_version_zero = true
|
|
115
|
+
|
|
116
|
+
[tool.commitizen.change_type_map]
|
|
117
|
+
feat = "Added"
|
|
118
|
+
fix = "Fixed"
|
|
119
|
+
perf = "Changed"
|
|
120
|
+
refactor = "Changed"
|
|
121
|
+
docs = "Changed"
|
|
122
|
+
chore = "Changed"
|
|
123
|
+
build = "Changed"
|
|
124
|
+
ci = "Changed"
|
|
125
|
+
test = "Changed"
|
|
126
|
+
style = "Changed"
|
|
127
|
+
revert = "Changed"
|
|
128
|
+
"breaking change" = "Changed"
|
|
129
|
+
"BREAKING CHANGE" = "Changed"
|
|
130
|
+
|
|
131
|
+
[tool.bandit]
|
|
132
|
+
exclude_dirs = [
|
|
133
|
+
"tests",
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
[tool.ty.src]
|
|
137
|
+
exclude = [
|
|
138
|
+
"tmp/scripts/**",
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
[tool.tq]
|
|
142
|
+
source_root = "src"
|
|
143
|
+
test_root = "tests"
|
|
144
|
+
package = "tq"
|
|
145
|
+
ignore_init_modules = true
|
|
146
|
+
max_test_file_non_blank_lines = 600
|
|
147
|
+
qualifier_strategy = "allowlist"
|
|
148
|
+
allowed_qualifiers = [
|
|
149
|
+
"fixtures_golden",
|
|
150
|
+
"fixtures_synthetic",
|
|
151
|
+
"regression",
|
|
152
|
+
"config",
|
|
153
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""tq package."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI package for tq command surface."""
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Command-line interface for tq."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tomllib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from tq.config.loader import resolve_tq_config
|
|
13
|
+
from tq.config.models import CliOverrides, ConfigValidationError, TqConfig
|
|
14
|
+
from tq.discovery.filesystem import build_analysis_index
|
|
15
|
+
from tq.engine.context import AnalysisContext
|
|
16
|
+
from tq.engine.rule_id import RuleId
|
|
17
|
+
from tq.engine.runner import RuleEngine
|
|
18
|
+
from tq.reporting.json import print_json_report
|
|
19
|
+
from tq.reporting.terminal import print_report
|
|
20
|
+
from tq.rules.file_too_large import FileTooLargeRule
|
|
21
|
+
from tq.rules.mapping_missing_test import MappingMissingTestRule
|
|
22
|
+
from tq.rules.orphaned_test import OrphanedTestRule
|
|
23
|
+
from tq.rules.qualifiers import QualifierStrategy
|
|
24
|
+
from tq.rules.structure_mismatch import StructureMismatchRule
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from tq.rules.contracts import Rule
|
|
28
|
+
|
|
29
|
+
_BUILTIN_RULE_IDS = (
|
|
30
|
+
RuleId("mapping-missing-test"),
|
|
31
|
+
RuleId("structure-mismatch"),
|
|
32
|
+
RuleId("test-file-too-large"),
|
|
33
|
+
RuleId("orphaned-test"),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_HELP_OPTION_NAMES = {"help_option_names": ["-h", "--help"]}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@click.group(context_settings=_HELP_OPTION_NAMES)
|
|
41
|
+
def cli() -> None:
|
|
42
|
+
"""Run tq commands."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@cli.command("check", context_settings=_HELP_OPTION_NAMES)
|
|
46
|
+
@click.option(
|
|
47
|
+
"--config",
|
|
48
|
+
"config_path",
|
|
49
|
+
type=click.Path(path_type=Path, dir_okay=False, exists=True),
|
|
50
|
+
default=None,
|
|
51
|
+
help="Use this pyproject file instead of discovered configuration.",
|
|
52
|
+
)
|
|
53
|
+
@click.option(
|
|
54
|
+
"--isolated",
|
|
55
|
+
is_flag=True,
|
|
56
|
+
default=False,
|
|
57
|
+
help="Ignore discovered configuration files.",
|
|
58
|
+
)
|
|
59
|
+
@click.option("--package", type=str, default=None, help="Target package import path.")
|
|
60
|
+
@click.option("--source-root", type=str, default=None, help="Source tree root path.")
|
|
61
|
+
@click.option("--test-root", type=str, default=None, help="Test tree root path.")
|
|
62
|
+
@click.option(
|
|
63
|
+
"--max-test-file-non-blank-lines",
|
|
64
|
+
type=click.IntRange(min=1),
|
|
65
|
+
default=None,
|
|
66
|
+
help="Maximum non-blank, non-comment lines per test file.",
|
|
67
|
+
)
|
|
68
|
+
@click.option(
|
|
69
|
+
"--qualifier-strategy",
|
|
70
|
+
type=click.Choice([strategy.value for strategy in QualifierStrategy]),
|
|
71
|
+
default=None,
|
|
72
|
+
help="Module-name qualifier policy for qualified test files.",
|
|
73
|
+
)
|
|
74
|
+
@click.option(
|
|
75
|
+
"--allowed-qualifier",
|
|
76
|
+
"allowed_qualifiers",
|
|
77
|
+
multiple=True,
|
|
78
|
+
type=str,
|
|
79
|
+
help="Allowed qualifier suffix for allowlist strategy.",
|
|
80
|
+
)
|
|
81
|
+
@click.option(
|
|
82
|
+
"--ignore-init-modules",
|
|
83
|
+
"ignore_init_modules",
|
|
84
|
+
flag_value=True,
|
|
85
|
+
default=None,
|
|
86
|
+
help="Ignore __init__.py modules in mapping checks.",
|
|
87
|
+
)
|
|
88
|
+
@click.option(
|
|
89
|
+
"--no-ignore-init-modules",
|
|
90
|
+
"ignore_init_modules",
|
|
91
|
+
flag_value=False,
|
|
92
|
+
default=None,
|
|
93
|
+
help="Include __init__.py modules in mapping checks.",
|
|
94
|
+
)
|
|
95
|
+
@click.option(
|
|
96
|
+
"--select",
|
|
97
|
+
"select_rules",
|
|
98
|
+
multiple=True,
|
|
99
|
+
type=str,
|
|
100
|
+
help="Only run selected rule IDs.",
|
|
101
|
+
)
|
|
102
|
+
@click.option(
|
|
103
|
+
"--ignore",
|
|
104
|
+
"ignore_rules",
|
|
105
|
+
multiple=True,
|
|
106
|
+
type=str,
|
|
107
|
+
help="Skip listed rule IDs.",
|
|
108
|
+
)
|
|
109
|
+
@click.option(
|
|
110
|
+
"--exit-zero",
|
|
111
|
+
is_flag=True,
|
|
112
|
+
default=False,
|
|
113
|
+
help="Always exit with code 0 regardless of findings.",
|
|
114
|
+
)
|
|
115
|
+
@click.option(
|
|
116
|
+
"--show-suggestions",
|
|
117
|
+
is_flag=True,
|
|
118
|
+
default=False,
|
|
119
|
+
help="Render remediation suggestions in diagnostics output.",
|
|
120
|
+
)
|
|
121
|
+
@click.option(
|
|
122
|
+
"--output-format",
|
|
123
|
+
type=click.Choice(["text", "json"]),
|
|
124
|
+
default="text",
|
|
125
|
+
show_default=True,
|
|
126
|
+
help="Select output format.",
|
|
127
|
+
)
|
|
128
|
+
def check_command( # noqa: PLR0913
|
|
129
|
+
*,
|
|
130
|
+
config_path: Path | None,
|
|
131
|
+
isolated: bool,
|
|
132
|
+
package: str | None,
|
|
133
|
+
source_root: str | None,
|
|
134
|
+
test_root: str | None,
|
|
135
|
+
max_test_file_non_blank_lines: int | None,
|
|
136
|
+
qualifier_strategy: str | None,
|
|
137
|
+
allowed_qualifiers: tuple[str, ...],
|
|
138
|
+
ignore_init_modules: bool | None,
|
|
139
|
+
select_rules: tuple[str, ...],
|
|
140
|
+
ignore_rules: tuple[str, ...],
|
|
141
|
+
exit_zero: bool,
|
|
142
|
+
show_suggestions: bool,
|
|
143
|
+
output_format: str,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Run built-in tq quality rules against discovered modules and tests."""
|
|
146
|
+
cwd = Path.cwd()
|
|
147
|
+
console = Console(stderr=False)
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
overrides = CliOverrides(
|
|
151
|
+
package=package,
|
|
152
|
+
source_root=source_root,
|
|
153
|
+
test_root=test_root,
|
|
154
|
+
ignore_init_modules=ignore_init_modules,
|
|
155
|
+
max_test_file_non_blank_lines=max_test_file_non_blank_lines,
|
|
156
|
+
qualifier_strategy=(
|
|
157
|
+
QualifierStrategy(qualifier_strategy)
|
|
158
|
+
if qualifier_strategy is not None
|
|
159
|
+
else None
|
|
160
|
+
),
|
|
161
|
+
allowed_qualifiers=allowed_qualifiers or None,
|
|
162
|
+
select=_parse_rule_id_tuple(values=select_rules),
|
|
163
|
+
ignore=_parse_rule_id_tuple(values=ignore_rules),
|
|
164
|
+
)
|
|
165
|
+
config = resolve_tq_config(
|
|
166
|
+
cwd=cwd,
|
|
167
|
+
explicit_config_path=config_path,
|
|
168
|
+
isolated=isolated,
|
|
169
|
+
cli_overrides=overrides,
|
|
170
|
+
)
|
|
171
|
+
except (ConfigValidationError, ValueError, tomllib.TOMLDecodeError) as error:
|
|
172
|
+
raise click.UsageError(str(error)) from error
|
|
173
|
+
|
|
174
|
+
if not config.source_package_root.exists():
|
|
175
|
+
msg = (
|
|
176
|
+
"Configured source package root does not exist: "
|
|
177
|
+
f"{config.source_package_root}"
|
|
178
|
+
)
|
|
179
|
+
raise click.UsageError(
|
|
180
|
+
msg,
|
|
181
|
+
)
|
|
182
|
+
if not config.test_root.exists():
|
|
183
|
+
msg = f"Configured test root does not exist: {config.test_root}"
|
|
184
|
+
raise click.UsageError(
|
|
185
|
+
msg,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
rules = _build_rules(config=config)
|
|
189
|
+
index = build_analysis_index(
|
|
190
|
+
source_root=config.source_package_root,
|
|
191
|
+
test_root=config.test_root,
|
|
192
|
+
)
|
|
193
|
+
context = AnalysisContext.create(index=index)
|
|
194
|
+
result = RuleEngine(rules=rules).run(context=context)
|
|
195
|
+
|
|
196
|
+
if output_format == "json":
|
|
197
|
+
print_json_report(result=result, console=console, cwd=cwd)
|
|
198
|
+
else:
|
|
199
|
+
print_report(
|
|
200
|
+
result=result,
|
|
201
|
+
console=console,
|
|
202
|
+
cwd=cwd,
|
|
203
|
+
include_suggestions=show_suggestions,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if exit_zero:
|
|
207
|
+
raise click.exceptions.Exit(0)
|
|
208
|
+
|
|
209
|
+
raise click.exceptions.Exit(1 if result.has_errors else 0)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _build_rules(*, config: TqConfig) -> tuple[Rule, ...]:
|
|
213
|
+
"""Build active built-in rule set using select/ignore resolution."""
|
|
214
|
+
selected_rule_ids = _resolve_rule_selection(config=config)
|
|
215
|
+
selected_set = set(selected_rule_ids)
|
|
216
|
+
|
|
217
|
+
builtins: dict[RuleId, Rule] = {
|
|
218
|
+
RuleId("mapping-missing-test"): MappingMissingTestRule(
|
|
219
|
+
ignore_init_modules=config.ignore_init_modules,
|
|
220
|
+
qualifier_strategy=config.qualifier_strategy,
|
|
221
|
+
allowed_qualifiers=config.allowed_qualifiers,
|
|
222
|
+
),
|
|
223
|
+
RuleId("structure-mismatch"): StructureMismatchRule(),
|
|
224
|
+
RuleId("test-file-too-large"): FileTooLargeRule(
|
|
225
|
+
max_non_blank_lines=config.max_test_file_non_blank_lines,
|
|
226
|
+
),
|
|
227
|
+
RuleId("orphaned-test"): OrphanedTestRule(
|
|
228
|
+
qualifier_strategy=config.qualifier_strategy,
|
|
229
|
+
allowed_qualifiers=config.allowed_qualifiers,
|
|
230
|
+
),
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return tuple(
|
|
234
|
+
builtins[rule_id] for rule_id in _BUILTIN_RULE_IDS if rule_id in selected_set
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _resolve_rule_selection(*, config: TqConfig) -> tuple[RuleId, ...]:
|
|
239
|
+
"""Resolve active rule IDs deterministically from select/ignore."""
|
|
240
|
+
builtin_set = set(_BUILTIN_RULE_IDS)
|
|
241
|
+
|
|
242
|
+
requested_select = set(config.select)
|
|
243
|
+
requested_ignore = set(config.ignore)
|
|
244
|
+
|
|
245
|
+
unknown = (requested_select | requested_ignore) - builtin_set
|
|
246
|
+
if unknown:
|
|
247
|
+
unknown_ids = ", ".join(sorted(rule_id.value for rule_id in unknown))
|
|
248
|
+
msg = f"Unknown built-in rule ID(s): {unknown_ids}"
|
|
249
|
+
raise ConfigValidationError(msg)
|
|
250
|
+
|
|
251
|
+
if config.select:
|
|
252
|
+
selected = tuple(
|
|
253
|
+
rule_id for rule_id in _BUILTIN_RULE_IDS if rule_id in requested_select
|
|
254
|
+
)
|
|
255
|
+
else:
|
|
256
|
+
selected = _BUILTIN_RULE_IDS
|
|
257
|
+
|
|
258
|
+
return tuple(rule_id for rule_id in selected if rule_id not in requested_ignore)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _parse_rule_id_tuple(*, values: tuple[str, ...]) -> tuple[RuleId, ...] | None:
|
|
262
|
+
"""Parse optional CLI rule identifier list into RuleId values."""
|
|
263
|
+
if not values:
|
|
264
|
+
return None
|
|
265
|
+
|
|
266
|
+
rule_ids: list[RuleId] = []
|
|
267
|
+
for value in values:
|
|
268
|
+
try:
|
|
269
|
+
rule_ids.append(RuleId(value))
|
|
270
|
+
except ValueError as error:
|
|
271
|
+
msg = f"Invalid rule ID: {value}"
|
|
272
|
+
raise ConfigValidationError(msg) from error
|
|
273
|
+
|
|
274
|
+
return tuple(rule_ids)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def main() -> None:
|
|
278
|
+
"""Run the tq command group."""
|
|
279
|
+
cli()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
if __name__ == "__main__":
|
|
283
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Configuration domain for tq CLI and engine composition."""
|