pyrigor 0.1.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.
pyrigor-0.1.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jarl Hoyem (@jarl-hoyem)
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.
pyrigor-0.1.1/PKG-INFO ADDED
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrigor
3
+ Version: 0.1.1
4
+ Summary: A Python coding discipline guideline collection and (eventually) linter.
5
+ Author: Jarl Hoyem (@jarl-hoyem)
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8; extra == "dev"
17
+ Requires-Dist: pytest-cov>=5; extra == "dev"
18
+ Requires-Dist: mypy>=1.10; extra == "dev"
19
+ Requires-Dist: pyright>=1.1.411; extra == "dev"
20
+ Requires-Dist: ty>=0.0.69; extra == "dev"
21
+ Requires-Dist: ruff>=0.6; extra == "dev"
22
+ Requires-Dist: mutmut>=3; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ # pyrigor
26
+
27
+ Disciplined Python patterns for catching bugs that type checkers and
28
+ standard linters miss — inspired by safety-critical coding guidelines from
29
+ other languages, adapted for a language and ecosystem they were not written
30
+ for.
31
+
32
+ ## What this is
33
+
34
+ Python’s failure modes are often silent: implicit type coercion,
35
+ positional-argument swaps between same-typed parameters, mutable default
36
+ arguments, float equality checks, and tuple-unpacking that "type-checks"
37
+ while being semantically wrong are all real, tool-catchable classes of
38
+ bugs that slip past mypy, pylint, and ruff’s default rule sets.
39
+
40
+ `pyrigor` collects a set of guidelines — and, over time, tooling to enforce
41
+ them — aimed at closing those gaps.
42
+
43
+ ## Status
44
+
45
+ Early stage. As of 2026-08, a set of documented guidelines. AST-based checks are
46
+ in progress. Plugin integration (pylint, possibly ruff) is a future goal,
47
+ not a current feature.
48
+
49
+ - [x] Guideline documentation
50
+ - [x] Standalone AST-based checkers (pre-commit local hooks) — PYR402 implemented.
51
+ PYR201, PYR202, PYR401, PYR403 documented but not yet enforced.
52
+ - [ ] pylint plugin
53
+ - [ ] ruff plugin (stretch goal — contingent on learning Rust)
54
+
55
+ ## Usage
56
+
57
+ ```bash
58
+ pip install pyrigor
59
+ pyrigor path/to/file.py [path/to/another.py ...]
60
+ ```
61
+
62
+ Only PYR402 is enforced today. A violation exits non-zero and prints
63
+ `path:line:col: PYR402 message (keyword-only-arguments)`.
64
+
65
+ To suppress a specific violation, add a same-line comment with a
66
+ reason:
67
+
68
+ ```python
69
+ def f(weight, bias): # pyrigor: PYR402 # matches a fixed external API
70
+ ...
71
+ ```
72
+
73
+ Codes may be given as the full code (`PYR402`), the bare number
74
+ (`402`), or the rule’s symbolic name (`keyword-only-arguments`).
75
+ Multiple codes: `# pyrigor: 402,403 # reason`. A suppression comment
76
+ without a reason is ignored, and a warning is printed.
77
+
78
+ ## Guidelines
79
+
80
+ See [`guidelines/`](./guidelines) for the full list. Each guideline has a
81
+ rule ID, rationale, example, and — once implemented — a link to its
82
+ enforcing check.
83
+
84
+ Guidelines documented so far:
85
+
86
+ | ID | Rule | Enforced by |
87
+ |--------|-----------------------------------------------------------------------|---------------------------------|
88
+ | PYR401 | Use `NamedTuple` for any function returning more than one value | Not yet implemented |
89
+ | PYR201 | Use `NewType` for same-typed values at risk of being swapped | Not yet implemented |
90
+ | PYR202 | Use `Enum` instead of magic strings, ints, or bools for closed states | Not yet implemented |
91
+ | PYR402 | Force keyword-only arguments for 2+ function parameters (bare `*`) | `pyrigor` CLI (pre-commit hook) |
92
+ | PYR403 | Force keyword-only arguments for single-parameter functions | Not yet implemented |
93
+
94
+ ## Philosophy
95
+
96
+ Prefer explicit over implicit. Make illegal states unrepresentable. Do not
97
+ rely on convention or code review where a tool can enforce correctness
98
+ instead.
99
+
100
+ ## Contributing
101
+
102
+ See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the setup and workflow.
103
+
104
+
105
+ ## License
106
+
107
+ MIT
@@ -0,0 +1,83 @@
1
+ # pyrigor
2
+
3
+ Disciplined Python patterns for catching bugs that type checkers and
4
+ standard linters miss — inspired by safety-critical coding guidelines from
5
+ other languages, adapted for a language and ecosystem they were not written
6
+ for.
7
+
8
+ ## What this is
9
+
10
+ Python’s failure modes are often silent: implicit type coercion,
11
+ positional-argument swaps between same-typed parameters, mutable default
12
+ arguments, float equality checks, and tuple-unpacking that "type-checks"
13
+ while being semantically wrong are all real, tool-catchable classes of
14
+ bugs that slip past mypy, pylint, and ruff’s default rule sets.
15
+
16
+ `pyrigor` collects a set of guidelines — and, over time, tooling to enforce
17
+ them — aimed at closing those gaps.
18
+
19
+ ## Status
20
+
21
+ Early stage. As of 2026-08, a set of documented guidelines. AST-based checks are
22
+ in progress. Plugin integration (pylint, possibly ruff) is a future goal,
23
+ not a current feature.
24
+
25
+ - [x] Guideline documentation
26
+ - [x] Standalone AST-based checkers (pre-commit local hooks) — PYR402 implemented.
27
+ PYR201, PYR202, PYR401, PYR403 documented but not yet enforced.
28
+ - [ ] pylint plugin
29
+ - [ ] ruff plugin (stretch goal — contingent on learning Rust)
30
+
31
+ ## Usage
32
+
33
+ ```bash
34
+ pip install pyrigor
35
+ pyrigor path/to/file.py [path/to/another.py ...]
36
+ ```
37
+
38
+ Only PYR402 is enforced today. A violation exits non-zero and prints
39
+ `path:line:col: PYR402 message (keyword-only-arguments)`.
40
+
41
+ To suppress a specific violation, add a same-line comment with a
42
+ reason:
43
+
44
+ ```python
45
+ def f(weight, bias): # pyrigor: PYR402 # matches a fixed external API
46
+ ...
47
+ ```
48
+
49
+ Codes may be given as the full code (`PYR402`), the bare number
50
+ (`402`), or the rule’s symbolic name (`keyword-only-arguments`).
51
+ Multiple codes: `# pyrigor: 402,403 # reason`. A suppression comment
52
+ without a reason is ignored, and a warning is printed.
53
+
54
+ ## Guidelines
55
+
56
+ See [`guidelines/`](./guidelines) for the full list. Each guideline has a
57
+ rule ID, rationale, example, and — once implemented — a link to its
58
+ enforcing check.
59
+
60
+ Guidelines documented so far:
61
+
62
+ | ID | Rule | Enforced by |
63
+ |--------|-----------------------------------------------------------------------|---------------------------------|
64
+ | PYR401 | Use `NamedTuple` for any function returning more than one value | Not yet implemented |
65
+ | PYR201 | Use `NewType` for same-typed values at risk of being swapped | Not yet implemented |
66
+ | PYR202 | Use `Enum` instead of magic strings, ints, or bools for closed states | Not yet implemented |
67
+ | PYR402 | Force keyword-only arguments for 2+ function parameters (bare `*`) | `pyrigor` CLI (pre-commit hook) |
68
+ | PYR403 | Force keyword-only arguments for single-parameter functions | Not yet implemented |
69
+
70
+ ## Philosophy
71
+
72
+ Prefer explicit over implicit. Make illegal states unrepresentable. Do not
73
+ rely on convention or code review where a tool can enforce correctness
74
+ instead.
75
+
76
+ ## Contributing
77
+
78
+ See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the setup and workflow.
79
+
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,75 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "pyrigor"
7
+ version = "0.1.1"
8
+ description = "A Python coding discipline guideline collection and (eventually) linter."
9
+ readme = "README.md"
10
+ requires-python = ">=3.11"
11
+ license = { text = "MIT" }
12
+ authors = [
13
+ { name = "Jarl Hoyem (@jarl-hoyem)" }
14
+ ]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ ]
22
+ dependencies = []
23
+
24
+ [project.scripts]
25
+ pyrigor = "pyrigor.checkers.cli:run"
26
+
27
+ [project.optional-dependencies]
28
+ dev = [
29
+ "pytest>=8",
30
+ "pytest-cov>=5",
31
+ "mypy>=1.10",
32
+ "pyright>=1.1.411",
33
+ "ty>=0.0.69",
34
+ "ruff>=0.6",
35
+ "mutmut>=3",
36
+ ]
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["."]
40
+ include = ["pyrigor*"]
41
+
42
+ [tool.mypy]
43
+ python_version = "3.11"
44
+ strict = true
45
+
46
+ [tool.ruff]
47
+ target-version = "py311"
48
+ line-length = 120
49
+
50
+ [tool.pytest.ini_options]
51
+ markers = [
52
+ "slow: marks tests as slow (deselect with '-m \"not slow\"')",
53
+ ]
54
+ addopts = [
55
+ "-m", "not slow",
56
+ "--cov=pyrigor",
57
+ "--cov-branch",
58
+ "--cov-report=html",
59
+ "--cov-report=term-missing",
60
+ "--cov-fail-under=100",
61
+ "--strict-markers",
62
+ "-W", "error",
63
+ ]
64
+
65
+ [tool.coverage.run]
66
+ source = ["pyrigor"]
67
+
68
+ [tool.pydocstyle]
69
+ convention = "google"
70
+
71
+ [tool.mutmut]
72
+ source_paths = "pyrigor/"
73
+
74
+ [tool.pylint.format]
75
+ max-line-length = 120
@@ -0,0 +1 @@
1
+ """pyrigor: a Python coding-discipline guideline collection (and eventually a linter)."""
@@ -0,0 +1,6 @@
1
+ """AST-based checkers for pyrigor's guidelines."""
2
+
3
+ # pyrigor/checkers/__init__.py
4
+ from pyrigor.checkers.pyr402_keyword_only_arguments import find_violations as find_pyr402_violations
5
+
6
+ __all__ = ["find_pyr402_violations"]
@@ -0,0 +1,40 @@
1
+ """Command-line entry point for pyrigor's checkers."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ from pyrigor.checkers import find_pyr402_violations
7
+ from pyrigor.suppression import filter_suppressed
8
+
9
+
10
+ def main(paths: list[str]) -> int:
11
+ """Run all checkers against the given file paths.
12
+
13
+ Args:
14
+ paths: File paths to check.
15
+
16
+ Returns:
17
+ 0 if no violations were found, 1 otherwise.
18
+ """
19
+ exit_code = 0
20
+
21
+ for path in paths:
22
+ source = Path(path).read_text(encoding="utf-8")
23
+ violations = find_pyr402_violations(source)
24
+ violations = filter_suppressed(violations=violations, source=source)
25
+
26
+ for violation in violations:
27
+ location = f"{path}:{violation.line}:{violation.column}"
28
+ print(f"{location}: {violation.rule.name} {violation.message} ({violation.rule.value})")
29
+ exit_code = 1
30
+
31
+ return exit_code
32
+
33
+
34
+ def run() -> None:
35
+ """Console-script entry point: parse argv and run main()."""
36
+ sys.exit(main(paths=sys.argv[1:]))
37
+
38
+
39
+ if __name__ == "__main__": # pragma: no cover
40
+ run()
@@ -0,0 +1,68 @@
1
+ """PYR402 checker: flag functions with parameters before a bare `*`."""
2
+
3
+ import ast
4
+ from typing import NamedTuple
5
+
6
+ from pyrigor.rules import Rule
7
+
8
+
9
+ class Violation(NamedTuple):
10
+ """A single rule violation found by one of pyrigor's checkers."""
11
+
12
+ line: int
13
+ column: int
14
+ function_name: str
15
+ rule: Rule
16
+ message: str
17
+
18
+
19
+ def _has_violation(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
20
+ """Check whether a function definition violates PYR402.
21
+
22
+ Args:
23
+ node: The function definition to check.
24
+
25
+ Returns:
26
+ True if the function has two or more parameters with at least
27
+ one positional (beyond an optional leading `self`/`cls`).
28
+ Single-parameter functions are exempt — see PYR403.
29
+ """
30
+ positional_args = list(node.args.posonlyargs) + list(node.args.args)
31
+ if positional_args and positional_args[0].arg in ("self", "cls"):
32
+ positional_args = positional_args[1:]
33
+
34
+ total_params = len(positional_args) + len(node.args.kwonlyargs)
35
+ if total_params < 2:
36
+ return False
37
+
38
+ return bool(positional_args)
39
+
40
+
41
+ def find_violations(source: str) -> list[Violation]:
42
+ """Find PYR402 violations in a source string.
43
+
44
+ PYR402: all parameters should be keyword-only.
45
+
46
+ Args:
47
+ source: Python source code to check.
48
+
49
+ Returns:
50
+ A list of violations found, one per offending function.
51
+ """
52
+ tree = ast.parse(source)
53
+ violations = []
54
+
55
+ for node in ast.walk(tree):
56
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and _has_violation(node):
57
+ violations.append(
58
+ Violation(
59
+ line=node.lineno,
60
+ column=node.col_offset + 1,
61
+ function_name=node.name,
62
+ rule=Rule.PYR402,
63
+ message=f"Function '{node.name}' has positional parameters; "
64
+ f"all parameters should be keyword-only (PYR402).",
65
+ )
66
+ )
67
+
68
+ return violations
File without changes
@@ -0,0 +1,15 @@
1
+ """Registry of all pyrigor rules.
2
+
3
+ Each Rule member's name is the rule's code (for example, PYR402), and its
4
+ value is the rule's symbolic name (for example, "keyword-only-arguments") —
5
+ matching the pylint-style dual-identifier convention pyrigor's own
6
+ output and suppression comments use.
7
+ """
8
+
9
+ from enum import Enum
10
+
11
+
12
+ class Rule(Enum):
13
+ """All pyrigor rules implemented or planned."""
14
+
15
+ PYR402 = "keyword-only-arguments"
@@ -0,0 +1,108 @@
1
+ """Suppression-comment mechanism for pyrigor's checkers.
2
+
3
+ Recognizes `# pyrigor: CODE[, CODE...]` comments on the same line as a
4
+ violation, where CODE may be a rule's full code ("PYR402"), its
5
+ numeric shorthand ("402"), or its symbolic name
6
+ ("keyword-only-arguments"). Whitespace around the colon and commas is
7
+ tolerated.
8
+ """
9
+
10
+ import re
11
+ import sys
12
+ from typing import NamedTuple
13
+
14
+ from pyrigor.checkers.pyr402_keyword_only_arguments import Violation
15
+
16
+
17
+ class _SuppressionInfo(NamedTuple):
18
+ """Parsed contents of a `# pyrigor:` suppression comment."""
19
+
20
+ tokens: set[str]
21
+ reason: str | None
22
+
23
+
24
+ _SUPPRESSION_PATTERN = re.compile(r"#\s*pyrigor\s*:\s*(?P<tokens>.+)$")
25
+ _NEAR_MISS_PATTERN = re.compile(r"#.*pyrigor", re.IGNORECASE)
26
+
27
+
28
+ def _suppressed_tokens(*, line: str) -> _SuppressionInfo:
29
+ """Get the suppression tokens and optional reason from a source line.
30
+
31
+ Args:
32
+ line: One line of source code.
33
+
34
+ Returns:
35
+ The suppression information is found on this line, or an empty
36
+ _SuppressionInfo if there is no suppression comment. If a
37
+ comment mentions "pyrigor" but doesn't match the expected
38
+ pattern, a warning is printed.
39
+ """
40
+ match = _SUPPRESSION_PATTERN.search(line)
41
+ if match is None:
42
+ if _NEAR_MISS_PATTERN.search(line):
43
+ print(
44
+ f"Warning: comment mentions 'pyrigor' but doesn't match "
45
+ f"'# pyrigor: CODE[,CODE] # reason' -- ignoring: {line.strip()}",
46
+ file=sys.stderr,
47
+ )
48
+ return _SuppressionInfo(tokens=set(), reason=None)
49
+
50
+ body = match.group("tokens")
51
+ codes_part, _, reason_part = body.partition("#")
52
+
53
+ tokens = {token.strip() for token in codes_part.split(",")}
54
+ reason = reason_part.strip() or None
55
+
56
+ return _SuppressionInfo(tokens=tokens, reason=reason)
57
+
58
+
59
+ def _matches_suppression(*, violation: Violation, suppression: _SuppressionInfo) -> bool:
60
+ """Check whether a violation is suppressed by the given suppression information.
61
+
62
+ Args:
63
+ violation: The violation to check.
64
+ suppression: Suppression information parsed from the violation's line.
65
+
66
+ Returns:
67
+ True if the violation's rule code, numeric shorthand, or
68
+ symbolic name is present in suppression tokens, and a reason
69
+ is present. Suppression with codes but no reason does not
70
+ suppress. Instead, a warning is printed.
71
+ """
72
+ code = violation.rule.name
73
+ shorthand = code.removeprefix("PYR")
74
+ name = violation.rule.value
75
+
76
+ code_matches = bool(suppression.tokens & {code, shorthand, name})
77
+
78
+ if code_matches and suppression.reason is None:
79
+ print(
80
+ f"Warning: suppression on line {violation.line} for {code} is missing required reason, ignoring.",
81
+ file=sys.stderr,
82
+ )
83
+ return False
84
+
85
+ return code_matches
86
+
87
+
88
+ def filter_suppressed(*, violations: list[Violation], source: str) -> list[Violation]:
89
+ """Remove violations suppressed by a same-line `# pyrigor:` comment.
90
+
91
+ Args:
92
+ violations: Violations to filter.
93
+ source: The source code the violations were found in.
94
+
95
+ Returns:
96
+ Violations that are not suppressed.
97
+ """
98
+ lines = source.splitlines()
99
+
100
+ result = []
101
+ for violation in violations:
102
+ line_text = lines[violation.line - 1] if 0 < violation.line <= len(lines) else ""
103
+ suppression = _suppressed_tokens(line=line_text)
104
+
105
+ if not _matches_suppression(violation=violation, suppression=suppression):
106
+ result.append(violation)
107
+
108
+ return result
@@ -0,0 +1,107 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyrigor
3
+ Version: 0.1.1
4
+ Summary: A Python coding discipline guideline collection and (eventually) linter.
5
+ Author: Jarl Hoyem (@jarl-hoyem)
6
+ License: MIT
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.11
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Requires-Python: >=3.11
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest>=8; extra == "dev"
17
+ Requires-Dist: pytest-cov>=5; extra == "dev"
18
+ Requires-Dist: mypy>=1.10; extra == "dev"
19
+ Requires-Dist: pyright>=1.1.411; extra == "dev"
20
+ Requires-Dist: ty>=0.0.69; extra == "dev"
21
+ Requires-Dist: ruff>=0.6; extra == "dev"
22
+ Requires-Dist: mutmut>=3; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ # pyrigor
26
+
27
+ Disciplined Python patterns for catching bugs that type checkers and
28
+ standard linters miss — inspired by safety-critical coding guidelines from
29
+ other languages, adapted for a language and ecosystem they were not written
30
+ for.
31
+
32
+ ## What this is
33
+
34
+ Python’s failure modes are often silent: implicit type coercion,
35
+ positional-argument swaps between same-typed parameters, mutable default
36
+ arguments, float equality checks, and tuple-unpacking that "type-checks"
37
+ while being semantically wrong are all real, tool-catchable classes of
38
+ bugs that slip past mypy, pylint, and ruff’s default rule sets.
39
+
40
+ `pyrigor` collects a set of guidelines — and, over time, tooling to enforce
41
+ them — aimed at closing those gaps.
42
+
43
+ ## Status
44
+
45
+ Early stage. As of 2026-08, a set of documented guidelines. AST-based checks are
46
+ in progress. Plugin integration (pylint, possibly ruff) is a future goal,
47
+ not a current feature.
48
+
49
+ - [x] Guideline documentation
50
+ - [x] Standalone AST-based checkers (pre-commit local hooks) — PYR402 implemented.
51
+ PYR201, PYR202, PYR401, PYR403 documented but not yet enforced.
52
+ - [ ] pylint plugin
53
+ - [ ] ruff plugin (stretch goal — contingent on learning Rust)
54
+
55
+ ## Usage
56
+
57
+ ```bash
58
+ pip install pyrigor
59
+ pyrigor path/to/file.py [path/to/another.py ...]
60
+ ```
61
+
62
+ Only PYR402 is enforced today. A violation exits non-zero and prints
63
+ `path:line:col: PYR402 message (keyword-only-arguments)`.
64
+
65
+ To suppress a specific violation, add a same-line comment with a
66
+ reason:
67
+
68
+ ```python
69
+ def f(weight, bias): # pyrigor: PYR402 # matches a fixed external API
70
+ ...
71
+ ```
72
+
73
+ Codes may be given as the full code (`PYR402`), the bare number
74
+ (`402`), or the rule’s symbolic name (`keyword-only-arguments`).
75
+ Multiple codes: `# pyrigor: 402,403 # reason`. A suppression comment
76
+ without a reason is ignored, and a warning is printed.
77
+
78
+ ## Guidelines
79
+
80
+ See [`guidelines/`](./guidelines) for the full list. Each guideline has a
81
+ rule ID, rationale, example, and — once implemented — a link to its
82
+ enforcing check.
83
+
84
+ Guidelines documented so far:
85
+
86
+ | ID | Rule | Enforced by |
87
+ |--------|-----------------------------------------------------------------------|---------------------------------|
88
+ | PYR401 | Use `NamedTuple` for any function returning more than one value | Not yet implemented |
89
+ | PYR201 | Use `NewType` for same-typed values at risk of being swapped | Not yet implemented |
90
+ | PYR202 | Use `Enum` instead of magic strings, ints, or bools for closed states | Not yet implemented |
91
+ | PYR402 | Force keyword-only arguments for 2+ function parameters (bare `*`) | `pyrigor` CLI (pre-commit hook) |
92
+ | PYR403 | Force keyword-only arguments for single-parameter functions | Not yet implemented |
93
+
94
+ ## Philosophy
95
+
96
+ Prefer explicit over implicit. Make illegal states unrepresentable. Do not
97
+ rely on convention or code review where a tool can enforce correctness
98
+ instead.
99
+
100
+ ## Contributing
101
+
102
+ See [`CONTRIBUTING.md`](./CONTRIBUTING.md) for the setup and workflow.
103
+
104
+
105
+ ## License
106
+
107
+ MIT
@@ -0,0 +1,18 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ pyrigor/__init__.py
5
+ pyrigor/py.typed
6
+ pyrigor/rules.py
7
+ pyrigor/suppression.py
8
+ pyrigor.egg-info/PKG-INFO
9
+ pyrigor.egg-info/SOURCES.txt
10
+ pyrigor.egg-info/dependency_links.txt
11
+ pyrigor.egg-info/entry_points.txt
12
+ pyrigor.egg-info/requires.txt
13
+ pyrigor.egg-info/top_level.txt
14
+ pyrigor/checkers/__init__.py
15
+ pyrigor/checkers/cli.py
16
+ pyrigor/checkers/pyr402_keyword_only_arguments.py
17
+ tests/test_rules.py
18
+ tests/test_suppression.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyrigor = pyrigor.checkers.cli:run
@@ -0,0 +1,9 @@
1
+
2
+ [dev]
3
+ pytest>=8
4
+ pytest-cov>=5
5
+ mypy>=1.10
6
+ pyright>=1.1.411
7
+ ty>=0.0.69
8
+ ruff>=0.6
9
+ mutmut>=3
@@ -0,0 +1 @@
1
+ pyrigor
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,9 @@
1
+ """Tests for pyrigor's Rule registry."""
2
+
3
+ from pyrigor.rules import Rule
4
+
5
+
6
+ def test_pyr402_rule_has_correct_code_and_name() -> None:
7
+ """Rule PYR402's enum name/value should match the expected code and symbolic name."""
8
+ assert Rule.PYR402.name == "PYR402"
9
+ assert Rule.PYR402.value == "keyword-only-arguments"
@@ -0,0 +1,120 @@
1
+ """Tests for pyrigor's suppression-comment mechanism."""
2
+
3
+ from pytest import CaptureFixture
4
+
5
+ from pyrigor.checkers.pyr402_keyword_only_arguments import Violation
6
+ from pyrigor.rules import Rule
7
+
8
+ # noinspection PyProtectedMember
9
+ from pyrigor.suppression import (
10
+ _suppressed_tokens, # pylint: disable=protected-access
11
+ filter_suppressed,
12
+ )
13
+
14
+
15
+ def test_suppressed_violation_is_filtered_out() -> None:
16
+ """A violation on a line with a matching # pyrigor: CODE # reason comment should be removed."""
17
+ source = "def apply_correction(weight, bias): # pyrigor: PYR402 # positional swap risk\n ...\n"
18
+ violations = [Violation(line=1, column=1, function_name="apply_correction", rule=Rule.PYR402, message="...")]
19
+
20
+ result = filter_suppressed(violations=violations, source=source)
21
+
22
+ assert not result
23
+
24
+
25
+ def test_unsuppressed_violation_is_kept() -> None:
26
+ """A violation on a line with no suppression comment should be kept."""
27
+ source = "def apply_correction(weight, bias):\n ...\n"
28
+ violations = [Violation(line=1, column=1, function_name="apply_correction", rule=Rule.PYR402, message="...")]
29
+
30
+ result = filter_suppressed(violations=violations, source=source)
31
+
32
+ assert result == violations
33
+
34
+
35
+ def test_multiple_suppressed_codes_on_one_line() -> None:
36
+ """Multiple codes in one # pyrigor: comment should each suppress their matching violation."""
37
+ source = "def apply_correction(weight, bias): # pyrigor: 402,201 # some reason\n ...\n"
38
+ violations = [
39
+ Violation(line=1, column=1, function_name="apply_correction", rule=Rule.PYR402, message="..."),
40
+ ]
41
+
42
+ result = filter_suppressed(violations=violations, source=source)
43
+
44
+ assert not result
45
+
46
+
47
+ def test_symbolic_name_suppresses_violation() -> None:
48
+ """A suppression comment using the symbolic name should work, as well as the code."""
49
+ source = "def apply_correction(weight, bias): # pyrigor: keyword-only-arguments # some reason\n ...\n"
50
+ violations = [
51
+ Violation(line=1, column=1, function_name="apply_correction", rule=Rule.PYR402, message="..."),
52
+ ]
53
+
54
+ result = filter_suppressed(violations=violations, source=source)
55
+
56
+ assert not result
57
+
58
+
59
+ def test_whitespace_around_colon_and_commas_is_tolerated() -> None:
60
+ """Irregular spacing around the colon and commas should still parse correctly."""
61
+ source = "def apply_correction(weight, bias): #pyrigor: 402 , 201 # some reason\n ...\n"
62
+ violations = [
63
+ Violation(line=1, column=1, function_name="apply_correction", rule=Rule.PYR402, message="..."),
64
+ ]
65
+
66
+ result = filter_suppressed(violations=violations, source=source)
67
+
68
+ assert not result
69
+
70
+
71
+ def test_suppression_comment_with_reason_is_parsed() -> None:
72
+ """A # pyrigor: CODE # reason comment should suppress and capture the reason text."""
73
+ source = (
74
+ "def apply_correction(weight, bias): # pyrigor: 402 # pytest fixture injection is positional-only\n ...\n"
75
+ )
76
+ violations = [
77
+ Violation(line=1, column=1, function_name="apply_correction", rule=Rule.PYR402, message="..."),
78
+ ]
79
+
80
+ result = filter_suppressed(violations=violations, source=source)
81
+
82
+ assert not result
83
+
84
+
85
+ def test_suppression_comment_reason_is_parsed_correctly() -> None:
86
+ """The free-text reason after the second # should be captured verbatim."""
87
+ line = "def apply_correction(weight, bias): # pyrigor: 402 # positional injection required by pytest"
88
+
89
+ info = _suppressed_tokens(line=line)
90
+
91
+ assert info.tokens == {"402"}
92
+ assert info.reason == "positional injection required by pytest"
93
+
94
+
95
+ def test_suppression_without_reason_does_not_suppress(capsys: CaptureFixture[str]) -> None:
96
+ """A suppression comment with no reason should not suppress and should warn."""
97
+ source = "def apply_correction(weight, bias): # pyrigor: 402\n ...\n"
98
+ violations = [
99
+ Violation(line=1, column=1, function_name="apply_correction", rule=Rule.PYR402, message="..."),
100
+ ]
101
+
102
+ result = filter_suppressed(violations=violations, source=source)
103
+
104
+ captured = capsys.readouterr()
105
+ assert result == violations
106
+ assert "missing required reason" in captured.err
107
+
108
+
109
+ def test_near_miss_comment_warns(capsys: CaptureFixture[str]) -> None:
110
+ """A comment mentioning 'pyrigor' that doesn't match the suppression pattern should warn."""
111
+ source = "def apply_correction(weight, bias): # pyrigor 402 missing colon\n ...\n"
112
+ violations = [
113
+ Violation(line=1, column=1, function_name="apply_correction", rule=Rule.PYR402, message="..."),
114
+ ]
115
+
116
+ result = filter_suppressed(violations=violations, source=source)
117
+
118
+ captured = capsys.readouterr()
119
+ assert result == violations
120
+ assert "doesn't match" in captured.err