jaymd96-pants-baseline 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,135 @@
1
+ """Rules for Ruff linting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Iterable
7
+
8
+ from pants.core.goals.lint import LintResult, LintTargetsRequest
9
+ from pants.core.util_rules.external_tool import DownloadedExternalTool, ExternalToolRequest
10
+ from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
11
+ from pants.engine.fs import Digest, MergeDigests
12
+ from pants.engine.platform import Platform
13
+ from pants.engine.process import FallibleProcessResult, Process
14
+ from pants.engine.rules import Get, MultiGet, collect_rules, rule
15
+ from pants.engine.target import FieldSet, Target
16
+ from pants.engine.unions import UnionRule
17
+ from pants.util.logging import LogLevel
18
+
19
+ from pants_baseline.subsystems.baseline import BaselineSubsystem
20
+ from pants_baseline.subsystems.ruff import RuffSubsystem
21
+ from pants_baseline.targets import BaselineSourcesField, SkipLintField
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class RuffLintFieldSet(FieldSet):
26
+ """Field set for Ruff linting."""
27
+
28
+ required_fields = (BaselineSourcesField,)
29
+
30
+ sources: BaselineSourcesField
31
+ skip_lint: SkipLintField
32
+
33
+
34
+ class RuffLintRequest(LintTargetsRequest):
35
+ """Request to run Ruff linting."""
36
+
37
+ field_set_type = RuffLintFieldSet
38
+ tool_name = "ruff"
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class RuffLintResult:
43
+ """Result of running Ruff lint."""
44
+
45
+ exit_code: int
46
+ stdout: str
47
+ stderr: str
48
+
49
+
50
+ @rule(desc="Lint with Ruff", level=LogLevel.DEBUG)
51
+ async def run_ruff_lint(
52
+ request: RuffLintRequest,
53
+ ruff_subsystem: RuffSubsystem,
54
+ baseline_subsystem: BaselineSubsystem,
55
+ platform: Platform,
56
+ ) -> LintResult:
57
+ """Run Ruff linter on Python files."""
58
+ if not baseline_subsystem.enabled:
59
+ return LintResult(
60
+ exit_code=0,
61
+ stdout="",
62
+ stderr="",
63
+ linter_name="ruff",
64
+ partition_description=None,
65
+ )
66
+
67
+ # Filter out skipped targets
68
+ field_sets = [fs for fs in request.field_sets if not fs.skip_lint.value]
69
+
70
+ if not field_sets:
71
+ return LintResult(
72
+ exit_code=0,
73
+ stdout="No targets to lint",
74
+ stderr="",
75
+ linter_name="ruff",
76
+ partition_description=None,
77
+ )
78
+
79
+ # Get source files
80
+ sources = await Get(
81
+ SourceFiles,
82
+ SourceFilesRequest(
83
+ sources_fields=[fs.sources for fs in field_sets],
84
+ for_sources_types=(BaselineSourcesField,),
85
+ ),
86
+ )
87
+
88
+ if not sources.files:
89
+ return LintResult(
90
+ exit_code=0,
91
+ stdout="No files to lint",
92
+ stderr="",
93
+ linter_name="ruff",
94
+ partition_description=None,
95
+ )
96
+
97
+ # Build Ruff command
98
+ select_args = [f"--select={','.join(ruff_subsystem.select)}"] if ruff_subsystem.select else []
99
+ ignore_args = [f"--ignore={','.join(ruff_subsystem.ignore)}"] if ruff_subsystem.ignore else []
100
+
101
+ argv = [
102
+ "ruff",
103
+ "check",
104
+ f"--target-version={baseline_subsystem.get_python_target_version()}",
105
+ f"--line-length={baseline_subsystem.line_length}",
106
+ *select_args,
107
+ *ignore_args,
108
+ "--output-format=text",
109
+ *sources.files,
110
+ ]
111
+
112
+ process = Process(
113
+ argv=argv,
114
+ input_digest=sources.snapshot.digest,
115
+ description=f"Run Ruff lint on {len(sources.files)} files",
116
+ level=LogLevel.DEBUG,
117
+ )
118
+
119
+ result = await Get(FallibleProcessResult, Process, process)
120
+
121
+ return LintResult(
122
+ exit_code=result.exit_code,
123
+ stdout=result.stdout.decode(),
124
+ stderr=result.stderr.decode(),
125
+ linter_name="ruff",
126
+ partition_description=None,
127
+ )
128
+
129
+
130
+ def rules() -> Iterable:
131
+ """Return all lint rules."""
132
+ return [
133
+ *collect_rules(),
134
+ UnionRule(LintTargetsRequest, RuffLintRequest),
135
+ ]
@@ -0,0 +1,153 @@
1
+ """Rules for pytest testing with coverage."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Iterable
7
+
8
+ from pants.core.goals.test import TestRequest, TestResult
9
+ from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
10
+ from pants.engine.process import FallibleProcessResult, Process
11
+ from pants.engine.rules import Get, collect_rules, rule
12
+ from pants.engine.target import FieldSet
13
+ from pants.engine.unions import UnionRule
14
+ from pants.util.logging import LogLevel
15
+
16
+ from pants_baseline.subsystems.baseline import BaselineSubsystem
17
+ from pants_baseline.targets import (
18
+ BaselineSourcesField,
19
+ BaselineTestSourcesField,
20
+ CoverageThresholdField,
21
+ SkipTestField,
22
+ )
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class PytestFieldSet(FieldSet):
27
+ """Field set for pytest testing."""
28
+
29
+ required_fields = (BaselineTestSourcesField,)
30
+
31
+ sources: BaselineSourcesField
32
+ test_sources: BaselineTestSourcesField
33
+ coverage_threshold: CoverageThresholdField
34
+ skip_test: SkipTestField
35
+
36
+
37
+ class PytestTestRequest(TestRequest):
38
+ """Request to run pytest tests."""
39
+
40
+ field_set_type = PytestFieldSet
41
+ tool_name = "pytest"
42
+
43
+
44
+ @rule(desc="Test with pytest", level=LogLevel.DEBUG)
45
+ async def run_pytest(
46
+ request: PytestTestRequest,
47
+ baseline_subsystem: BaselineSubsystem,
48
+ ) -> TestResult:
49
+ """Run pytest on test files with coverage."""
50
+ if not baseline_subsystem.enabled:
51
+ return TestResult(
52
+ exit_code=0,
53
+ stdout="",
54
+ stderr="",
55
+ stdout_digest=None,
56
+ stderr_digest=None,
57
+ address=None,
58
+ output_setting=None,
59
+ )
60
+
61
+ # Filter out skipped targets
62
+ field_sets = [fs for fs in request.field_sets if not fs.skip_test.value]
63
+
64
+ if not field_sets:
65
+ return TestResult(
66
+ exit_code=0,
67
+ stdout="No targets to test",
68
+ stderr="",
69
+ stdout_digest=None,
70
+ stderr_digest=None,
71
+ address=None,
72
+ output_setting=None,
73
+ )
74
+
75
+ # Get test source files
76
+ test_sources = await Get(
77
+ SourceFiles,
78
+ SourceFilesRequest(
79
+ sources_fields=[fs.test_sources for fs in field_sets],
80
+ for_sources_types=(BaselineTestSourcesField,),
81
+ ),
82
+ )
83
+
84
+ # Get source files for coverage
85
+ sources = await Get(
86
+ SourceFiles,
87
+ SourceFilesRequest(
88
+ sources_fields=[fs.sources for fs in field_sets],
89
+ for_sources_types=(BaselineSourcesField,),
90
+ ),
91
+ )
92
+
93
+ if not test_sources.files:
94
+ return TestResult(
95
+ exit_code=0,
96
+ stdout="No test files found",
97
+ stderr="",
98
+ stdout_digest=None,
99
+ stderr_digest=None,
100
+ address=None,
101
+ output_setting=None,
102
+ )
103
+
104
+ # Get coverage threshold from first field set (or use default)
105
+ coverage_threshold = (
106
+ field_sets[0].coverage_threshold.value
107
+ if field_sets
108
+ else baseline_subsystem.coverage_threshold
109
+ )
110
+
111
+ # Build pytest command with coverage
112
+ src_root = ",".join(baseline_subsystem.src_roots)
113
+
114
+ argv = [
115
+ "pytest",
116
+ "-v",
117
+ "--strict-markers",
118
+ "--strict-config",
119
+ "-ra",
120
+ "--tb=short",
121
+ f"--cov={src_root}",
122
+ "--cov-report=term-missing",
123
+ f"--cov-fail-under={coverage_threshold}",
124
+ "--cov-branch",
125
+ *test_sources.files,
126
+ ]
127
+
128
+ process = Process(
129
+ argv=argv,
130
+ input_digest=test_sources.snapshot.digest,
131
+ description=f"Run pytest on {len(test_sources.files)} test files",
132
+ level=LogLevel.DEBUG,
133
+ )
134
+
135
+ result = await Get(FallibleProcessResult, Process, process)
136
+
137
+ return TestResult(
138
+ exit_code=result.exit_code,
139
+ stdout=result.stdout.decode(),
140
+ stderr=result.stderr.decode(),
141
+ stdout_digest=None,
142
+ stderr_digest=None,
143
+ address=None,
144
+ output_setting=None,
145
+ )
146
+
147
+
148
+ def rules() -> Iterable:
149
+ """Return all test rules."""
150
+ return [
151
+ *collect_rules(),
152
+ UnionRule(TestRequest, PytestTestRequest),
153
+ ]
@@ -0,0 +1,146 @@
1
+ """Rules for ty type checking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Iterable
7
+
8
+ from pants.core.goals.check import CheckRequest, CheckResult, CheckResults
9
+ from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
10
+ from pants.engine.process import FallibleProcessResult, Process
11
+ from pants.engine.rules import Get, collect_rules, rule
12
+ from pants.engine.target import FieldSet
13
+ from pants.engine.unions import UnionRule
14
+ from pants.util.logging import LogLevel
15
+
16
+ from pants_baseline.subsystems.baseline import BaselineSubsystem
17
+ from pants_baseline.subsystems.ty import TySubsystem
18
+ from pants_baseline.targets import BaselineSourcesField, SkipTypecheckField
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class TyFieldSet(FieldSet):
23
+ """Field set for ty type checking."""
24
+
25
+ required_fields = (BaselineSourcesField,)
26
+
27
+ sources: BaselineSourcesField
28
+ skip_typecheck: SkipTypecheckField
29
+
30
+
31
+ class TyCheckRequest(CheckRequest):
32
+ """Request to run ty type checking."""
33
+
34
+ field_set_type = TyFieldSet
35
+ tool_name = "ty"
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class TyCheckResult:
40
+ """Result of running ty type check."""
41
+
42
+ exit_code: int
43
+ stdout: str
44
+ stderr: str
45
+ partition_description: str | None = None
46
+
47
+
48
+ @rule(desc="Type check with ty", level=LogLevel.DEBUG)
49
+ async def run_ty_check(
50
+ request: TyCheckRequest,
51
+ ty_subsystem: TySubsystem,
52
+ baseline_subsystem: BaselineSubsystem,
53
+ ) -> CheckResults:
54
+ """Run ty type checker on Python files."""
55
+ if not baseline_subsystem.enabled:
56
+ return CheckResults(
57
+ results=[
58
+ CheckResult(
59
+ exit_code=0,
60
+ stdout="",
61
+ stderr="",
62
+ partition_description=None,
63
+ )
64
+ ],
65
+ checker_name="ty",
66
+ )
67
+
68
+ # Filter out skipped targets
69
+ field_sets = [fs for fs in request.field_sets if not fs.skip_typecheck.value]
70
+
71
+ if not field_sets:
72
+ return CheckResults(
73
+ results=[
74
+ CheckResult(
75
+ exit_code=0,
76
+ stdout="No targets to type check",
77
+ stderr="",
78
+ partition_description=None,
79
+ )
80
+ ],
81
+ checker_name="ty",
82
+ )
83
+
84
+ # Get source files
85
+ sources = await Get(
86
+ SourceFiles,
87
+ SourceFilesRequest(
88
+ sources_fields=[fs.sources for fs in field_sets],
89
+ for_sources_types=(BaselineSourcesField,),
90
+ ),
91
+ )
92
+
93
+ if not sources.files:
94
+ return CheckResults(
95
+ results=[
96
+ CheckResult(
97
+ exit_code=0,
98
+ stdout="No files to type check",
99
+ stderr="",
100
+ partition_description=None,
101
+ )
102
+ ],
103
+ checker_name="ty",
104
+ )
105
+
106
+ # Build ty command
107
+ strict_arg = ["--strict"] if ty_subsystem.strict else []
108
+ output_format_arg = [f"--output-format={ty_subsystem.output_format}"]
109
+
110
+ argv = [
111
+ "ty",
112
+ "check",
113
+ f"--python-version={baseline_subsystem.python_version}",
114
+ *strict_arg,
115
+ *output_format_arg,
116
+ *sources.files,
117
+ ]
118
+
119
+ process = Process(
120
+ argv=argv,
121
+ input_digest=sources.snapshot.digest,
122
+ description=f"Run ty type check on {len(sources.files)} files",
123
+ level=LogLevel.DEBUG,
124
+ )
125
+
126
+ result = await Get(FallibleProcessResult, Process, process)
127
+
128
+ return CheckResults(
129
+ results=[
130
+ CheckResult(
131
+ exit_code=result.exit_code,
132
+ stdout=result.stdout.decode(),
133
+ stderr=result.stderr.decode(),
134
+ partition_description=None,
135
+ )
136
+ ],
137
+ checker_name="ty",
138
+ )
139
+
140
+
141
+ def rules() -> Iterable:
142
+ """Return all type check rules."""
143
+ return [
144
+ *collect_rules(),
145
+ UnionRule(CheckRequest, TyCheckRequest),
146
+ ]
@@ -0,0 +1,13 @@
1
+ """Subsystems for Python Baseline plugin configuration."""
2
+
3
+ from pants_baseline.subsystems.baseline import BaselineSubsystem
4
+ from pants_baseline.subsystems.ruff import RuffSubsystem
5
+ from pants_baseline.subsystems.ty import TySubsystem
6
+ from pants_baseline.subsystems.uv import UvSubsystem
7
+
8
+ __all__ = [
9
+ "BaselineSubsystem",
10
+ "RuffSubsystem",
11
+ "TySubsystem",
12
+ "UvSubsystem",
13
+ ]
@@ -0,0 +1,74 @@
1
+ """Main baseline subsystem for coordinating all quality tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pants.option.option_types import BoolOption, IntOption, StrListOption, StrOption
6
+ from pants.option.subsystem import Subsystem
7
+
8
+
9
+ class BaselineSubsystem(Subsystem):
10
+ """Configuration for the Python Baseline quality framework.
11
+
12
+ This subsystem provides global configuration for all baseline tools
13
+ including Ruff, ty, pytest, and uv audit.
14
+ """
15
+
16
+ options_scope = "python-baseline"
17
+ help = "Opinionated Python code quality baseline configuration."
18
+
19
+ # Global settings
20
+ enabled = BoolOption(
21
+ default=True,
22
+ help="Whether to enable the Python baseline checks globally.",
23
+ )
24
+
25
+ python_version = StrOption(
26
+ default="3.11",
27
+ help="Target Python version for all tools (e.g., '3.11', '3.12').",
28
+ )
29
+
30
+ line_length = IntOption(
31
+ default=120,
32
+ help="Maximum line length for formatting and linting.",
33
+ )
34
+
35
+ src_roots = StrListOption(
36
+ default=["src"],
37
+ help="Source root directories for Python code.",
38
+ )
39
+
40
+ test_roots = StrListOption(
41
+ default=["tests"],
42
+ help="Test root directories.",
43
+ )
44
+
45
+ exclude_patterns = StrListOption(
46
+ default=[
47
+ ".venv",
48
+ ".git",
49
+ "__pycache__",
50
+ "dist",
51
+ "build",
52
+ ".pytest_cache",
53
+ ".ruff_cache",
54
+ "migrations",
55
+ ],
56
+ help="Patterns to exclude from all baseline checks.",
57
+ )
58
+
59
+ # Coverage threshold
60
+ coverage_threshold = IntOption(
61
+ default=80,
62
+ help="Minimum code coverage percentage required.",
63
+ )
64
+
65
+ # Strictness level
66
+ strict_mode = BoolOption(
67
+ default=True,
68
+ help="Enable strict mode for all tools (more rigorous checks).",
69
+ )
70
+
71
+ def get_python_target_version(self) -> str:
72
+ """Return Python version in format suitable for tools (e.g., 'py311')."""
73
+ version = self.python_version.replace(".", "")
74
+ return f"py{version}"
@@ -0,0 +1,90 @@
1
+ """Ruff subsystem for linting and formatting configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pants.option.option_types import BoolOption, StrListOption, StrOption
6
+ from pants.option.subsystem import Subsystem
7
+
8
+
9
+ class RuffSubsystem(Subsystem):
10
+ """Configuration for Ruff linting and formatting.
11
+
12
+ Ruff is an extremely fast Python linter and formatter, written in Rust.
13
+ It replaces Black, isort, Flake8, and many other tools in a single binary.
14
+ """
15
+
16
+ options_scope = "ruff"
17
+ help = "Ruff linting and formatting configuration."
18
+
19
+ version = StrOption(
20
+ default="0.2.0",
21
+ help="Version of Ruff to use.",
22
+ )
23
+
24
+ # Linting configuration
25
+ select = StrListOption(
26
+ default=[
27
+ "E", # pycodestyle errors
28
+ "W", # pycodestyle warnings
29
+ "F", # pyflakes
30
+ "I", # isort
31
+ "N", # pep8-naming
32
+ "UP", # pyupgrade
33
+ "B", # flake8-bugbear
34
+ "C4", # flake8-comprehensions
35
+ "SIM", # flake8-simplify
36
+ "ASYNC", # flake8-async
37
+ "DTZ", # flake8-datetimez
38
+ "PIE", # flake8-pie
39
+ "RUF", # Ruff-specific rules
40
+ ],
41
+ help="Rule codes to enable for linting.",
42
+ )
43
+
44
+ ignore = StrListOption(
45
+ default=[
46
+ "E501", # line too long (handled by formatter)
47
+ "W292", # blank line at end of file
48
+ ],
49
+ help="Rule codes to ignore.",
50
+ )
51
+
52
+ # Formatting configuration
53
+ quote_style = StrOption(
54
+ default="double",
55
+ help="Quote style for formatting ('double' or 'single').",
56
+ )
57
+
58
+ indent_style = StrOption(
59
+ default="space",
60
+ help="Indentation style ('space' or 'tab').",
61
+ )
62
+
63
+ # Auto-fix
64
+ fix = BoolOption(
65
+ default=True,
66
+ help="Automatically fix auto-fixable issues.",
67
+ )
68
+
69
+ unsafe_fixes = BoolOption(
70
+ default=False,
71
+ help="Allow unsafe fixes that may change code behavior.",
72
+ )
73
+
74
+ # Per-file ignores (common patterns)
75
+ skip_tests_rules = StrListOption(
76
+ default=[
77
+ "F401", # unused imports OK in tests
78
+ "F811", # redefined function OK in tests
79
+ "S101", # assert OK in tests
80
+ ],
81
+ help="Rules to skip in test files.",
82
+ )
83
+
84
+ skip_init_rules = StrListOption(
85
+ default=[
86
+ "F401", # unused imports (exports)
87
+ "F403", # star imports OK for namespace
88
+ ],
89
+ help="Rules to skip in __init__.py files.",
90
+ )
@@ -0,0 +1,80 @@
1
+ """ty subsystem for type checking configuration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pants.option.option_types import BoolOption, StrListOption, StrOption
6
+ from pants.option.subsystem import Subsystem
7
+
8
+
9
+ class TySubsystem(Subsystem):
10
+ """Configuration for ty type checker.
11
+
12
+ ty is Astral's next-generation Python type checker, designed for
13
+ exceptional performance (10-60x faster than MyPy, 80x faster than
14
+ Pyright on incremental edits).
15
+
16
+ Features:
17
+ - LSP-first architecture for IDE integration
18
+ - Incremental checking optimized for real-time feedback
19
+ - First-class intersection types and reachability analysis
20
+ - Rust implementation with obsessive performance focus
21
+ """
22
+
23
+ options_scope = "ty"
24
+ help = "ty type checker configuration (Astral's next-gen type checker)."
25
+
26
+ version = StrOption(
27
+ default="0.1.0",
28
+ help="Version of ty to use.",
29
+ )
30
+
31
+ # Type checking mode
32
+ strict = BoolOption(
33
+ default=True,
34
+ help="Enable strict type checking mode.",
35
+ )
36
+
37
+ # Error reporting
38
+ report_missing_imports = BoolOption(
39
+ default=True,
40
+ help="Report errors for missing imports.",
41
+ )
42
+
43
+ report_unused_imports = BoolOption(
44
+ default=True,
45
+ help="Report errors for unused imports.",
46
+ )
47
+
48
+ report_unused_variables = BoolOption(
49
+ default=True,
50
+ help="Report errors for unused variables.",
51
+ )
52
+
53
+ # Include/exclude paths
54
+ include = StrListOption(
55
+ default=["src", "tests"],
56
+ help="Directories to include in type checking.",
57
+ )
58
+
59
+ exclude = StrListOption(
60
+ default=[
61
+ ".venv",
62
+ ".git",
63
+ "__pycache__",
64
+ "dist",
65
+ "build",
66
+ ],
67
+ help="Patterns to exclude from type checking.",
68
+ )
69
+
70
+ # Type stub handling
71
+ stub_path = StrOption(
72
+ default="",
73
+ help="Path to custom type stubs directory.",
74
+ )
75
+
76
+ # Output format
77
+ output_format = StrOption(
78
+ default="text",
79
+ help="Output format for type errors ('text', 'json', 'github').",
80
+ )