pyprojkit 0.3.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.
- pyprojkit/__init__.py +104 -0
- pyprojkit/_paths.py +43 -0
- pyprojkit/_run.py +36 -0
- pyprojkit/cli.py +58 -0
- pyprojkit/config/__init__.py +79 -0
- pyprojkit/config/base.py +84 -0
- pyprojkit/config/profiles.py +97 -0
- pyprojkit/config/project.py +219 -0
- pyprojkit/config/tools.py +168 -0
- pyprojkit/discovery.py +51 -0
- pyprojkit/sessions.py +71 -0
- pyprojkit/sync.py +243 -0
- pyprojkit/tasks.py +393 -0
- pyprojkit/versions.py +115 -0
- pyprojkit-0.3.0.dist-info/METADATA +311 -0
- pyprojkit-0.3.0.dist-info/RECORD +19 -0
- pyprojkit-0.3.0.dist-info/WHEEL +4 -0
- pyprojkit-0.3.0.dist-info/entry_points.txt +3 -0
- pyprojkit-0.3.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Project-wide configuration, declared in a project's `pyprojconf.py`.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Sequence
|
|
10
|
+
|
|
11
|
+
from .. import _paths
|
|
12
|
+
from ..versions import PythonVersions
|
|
13
|
+
from .base import BaseFormatterConfig
|
|
14
|
+
from .tools import (
|
|
15
|
+
CoverageReportConfig,
|
|
16
|
+
CoverageRunConfig,
|
|
17
|
+
DoitConfig,
|
|
18
|
+
MypyConfig,
|
|
19
|
+
NoxConfig,
|
|
20
|
+
PytestConfig,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"ProjectConfig",
|
|
25
|
+
"ToolsConfig",
|
|
26
|
+
"FormattingConfig",
|
|
27
|
+
"TestConfig",
|
|
28
|
+
"DocConfig",
|
|
29
|
+
"MkinitConfig",
|
|
30
|
+
"SphinxConfig",
|
|
31
|
+
"AnalysisConfig",
|
|
32
|
+
"PublishConfig",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(kw_only=True)
|
|
37
|
+
class FormattingConfig:
|
|
38
|
+
"""
|
|
39
|
+
Formatting configuration: which formatters run (in order) and their settings.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
formatters: tuple[BaseFormatterConfig, ...]
|
|
43
|
+
"""
|
|
44
|
+
Formatters in run order.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def default(cls) -> FormattingConfig:
|
|
49
|
+
"""
|
|
50
|
+
Get the default formatting profile.
|
|
51
|
+
"""
|
|
52
|
+
from .profiles import get_formatting_profile
|
|
53
|
+
|
|
54
|
+
return get_formatting_profile("default")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(kw_only=True)
|
|
58
|
+
class TestConfig:
|
|
59
|
+
"""
|
|
60
|
+
Testing configuration (pytest + coverage).
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
pytest: PytestConfig = field(default_factory=PytestConfig)
|
|
64
|
+
coverage_run: CoverageRunConfig = field(default_factory=CoverageRunConfig)
|
|
65
|
+
coverage_report: CoverageReportConfig = field(default_factory=CoverageReportConfig)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(kw_only=True)
|
|
69
|
+
class MkinitConfig:
|
|
70
|
+
"""
|
|
71
|
+
Configuration for generating `__init__.py` files using mkinit.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
args: tuple[str, ...] = (
|
|
75
|
+
"--recursive",
|
|
76
|
+
"--nomods",
|
|
77
|
+
"--relative",
|
|
78
|
+
"-i",
|
|
79
|
+
"--source-order",
|
|
80
|
+
)
|
|
81
|
+
"""
|
|
82
|
+
Arguments passed to mkinit.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(kw_only=True)
|
|
87
|
+
class SphinxConfig:
|
|
88
|
+
"""
|
|
89
|
+
Configuration for building documentation using sphinx.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
source_dir: str = "doc"
|
|
93
|
+
"""
|
|
94
|
+
Directory containing sphinx sources.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
copy_env_var: str | None = None
|
|
98
|
+
"""
|
|
99
|
+
Name of environment variable (or `.env` entry) giving a directory to which to copy
|
|
100
|
+
built documentation when the doc task is run with `--copy`.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass(kw_only=True)
|
|
105
|
+
class DocConfig:
|
|
106
|
+
"""
|
|
107
|
+
Documentation tool configurations.
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
mkinit: MkinitConfig | None = field(default_factory=MkinitConfig)
|
|
111
|
+
"""
|
|
112
|
+
Enables the `init` task; enabled by default.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
sphinx: SphinxConfig | None = None
|
|
116
|
+
"""
|
|
117
|
+
Enables the `doc` task; opt-in.
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass(kw_only=True)
|
|
122
|
+
class AnalysisConfig:
|
|
123
|
+
"""
|
|
124
|
+
Static analysis configuration.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
mypy: MypyConfig | None = field(default_factory=MypyConfig)
|
|
128
|
+
pyright: bool = True
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(kw_only=True)
|
|
132
|
+
class PublishConfig:
|
|
133
|
+
"""
|
|
134
|
+
Configuration for building and publishing via uv.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
out_dir: str = str(_paths.UV_PATH)
|
|
138
|
+
"""
|
|
139
|
+
Directory for build artifacts; cleaned before each build so stale artifacts are
|
|
140
|
+
never published.
|
|
141
|
+
"""
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass(kw_only=True)
|
|
145
|
+
class ToolsConfig:
|
|
146
|
+
"""
|
|
147
|
+
Development tool configurations, grouped by broad tool category.
|
|
148
|
+
|
|
149
|
+
A "tools profile" is simply a pre-canned `ToolsConfig` factory; see
|
|
150
|
+
`pyprojkit.config.profiles`. The default instance corresponds to the default
|
|
151
|
+
profile.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
formatting: FormattingConfig = field(default_factory=FormattingConfig.default)
|
|
155
|
+
test: TestConfig | None = field(default_factory=TestConfig)
|
|
156
|
+
doit: DoitConfig = field(default_factory=DoitConfig)
|
|
157
|
+
nox: NoxConfig = field(default_factory=NoxConfig)
|
|
158
|
+
doc: DocConfig = field(default_factory=DocConfig)
|
|
159
|
+
analysis: AnalysisConfig | None = None
|
|
160
|
+
publish: PublishConfig = field(default_factory=PublishConfig)
|
|
161
|
+
|
|
162
|
+
tool_overrides: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
163
|
+
"""
|
|
164
|
+
Escape hatch: extra entries merged last into managed tables, keyed by table path,
|
|
165
|
+
e.g. `{"tool.pytest.ini_options": {"addopts": "..."}}`.
|
|
166
|
+
|
|
167
|
+
A key not otherwise managed creates a new managed table.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
@classmethod
|
|
171
|
+
def default(cls) -> ToolsConfig:
|
|
172
|
+
"""
|
|
173
|
+
Get the default tools profile.
|
|
174
|
+
"""
|
|
175
|
+
from .profiles import get_tools_profile
|
|
176
|
+
|
|
177
|
+
return get_tools_profile("default")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass(kw_only=True)
|
|
181
|
+
class ProjectConfig:
|
|
182
|
+
"""
|
|
183
|
+
Top-level project configuration.
|
|
184
|
+
|
|
185
|
+
A project's `pyprojconf.py` must define a module-level instance named `config`.
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
package: str
|
|
189
|
+
"""
|
|
190
|
+
Import name of the package, e.g. `"trilium_alchemy"`.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
python: PythonVersions
|
|
194
|
+
"""
|
|
195
|
+
Supported Python versions.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
packages_dir: str = "src"
|
|
199
|
+
"""
|
|
200
|
+
Directory containing packages, relative to the project root.
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
format_paths: Sequence[str] = ("src", "test", "doc", "examples")
|
|
204
|
+
"""
|
|
205
|
+
Directories to format (those which exist), in addition to `*.py` and `*.toml` files
|
|
206
|
+
at the project root.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
tools: ToolsConfig = field(default_factory=ToolsConfig.default)
|
|
210
|
+
"""
|
|
211
|
+
Tool configurations.
|
|
212
|
+
"""
|
|
213
|
+
|
|
214
|
+
@property
|
|
215
|
+
def package_path(self) -> Path:
|
|
216
|
+
"""
|
|
217
|
+
Path to the package directory, relative to the project root.
|
|
218
|
+
"""
|
|
219
|
+
return Path(self.packages_dir) / self.package
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configurations for all supported tools.
|
|
3
|
+
|
|
4
|
+
Formatters are tools which additionally run as part of the `format` task.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
11
|
+
|
|
12
|
+
from .. import _paths
|
|
13
|
+
from .base import BaseFormatterConfig, BaseToolConfig
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .project import ProjectConfig
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"AutoflakeConfig",
|
|
20
|
+
"IsortConfig",
|
|
21
|
+
"BlackConfig",
|
|
22
|
+
"DocfmtConfig",
|
|
23
|
+
"TomlSortConfig",
|
|
24
|
+
"PytestConfig",
|
|
25
|
+
"CoverageRunConfig",
|
|
26
|
+
"CoverageReportConfig",
|
|
27
|
+
"DoitConfig",
|
|
28
|
+
"NoxConfig",
|
|
29
|
+
"MypyConfig",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# --- formatters (run order defined by FormattingConfig) ---
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(kw_only=True)
|
|
37
|
+
class AutoflakeConfig(BaseFormatterConfig):
|
|
38
|
+
table_path: ClassVar[str] = "tool.autoflake"
|
|
39
|
+
|
|
40
|
+
in_place: bool | None = True
|
|
41
|
+
recursive: bool | None = True
|
|
42
|
+
remove_all_unused_imports: bool | None = field(
|
|
43
|
+
default=True, metadata={"toml": "remove-all-unused-imports"}
|
|
44
|
+
)
|
|
45
|
+
remove_unused_variables: bool | None = field(
|
|
46
|
+
default=True, metadata={"toml": "remove-unused-variables"}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
|
|
50
|
+
return ["autoflake"] + py_paths
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(kw_only=True)
|
|
54
|
+
class IsortConfig(BaseFormatterConfig):
|
|
55
|
+
table_path: ClassVar[str] = "tool.isort"
|
|
56
|
+
|
|
57
|
+
profile: str | None = "black"
|
|
58
|
+
quiet: bool | None = True
|
|
59
|
+
|
|
60
|
+
def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
|
|
61
|
+
return ["isort"] + py_paths
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(kw_only=True)
|
|
65
|
+
class BlackConfig(BaseFormatterConfig):
|
|
66
|
+
table_path: ClassVar[str] = "tool.black"
|
|
67
|
+
|
|
68
|
+
quiet: bool | None = True
|
|
69
|
+
|
|
70
|
+
def extra_toml(self, project: ProjectConfig) -> dict[str, Any]:
|
|
71
|
+
return {"target-version": project.python.black_targets}
|
|
72
|
+
|
|
73
|
+
def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
|
|
74
|
+
return ["black"] + py_paths
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(kw_only=True)
|
|
78
|
+
class DocfmtConfig(BaseFormatterConfig):
|
|
79
|
+
table_path: ClassVar[str] = "tool.docfmt"
|
|
80
|
+
|
|
81
|
+
# docfmt returns 0 when it writes changes in-place; 1 means "would change"
|
|
82
|
+
# in check mode and 2 is an error, so only 0 is expected here
|
|
83
|
+
expect_rc: ClassVar[frozenset[int]] = frozenset({0})
|
|
84
|
+
|
|
85
|
+
# line length is left unset: docfmt picks it up from `[tool.black]`
|
|
86
|
+
in_place: bool | None = field(default=True, metadata={"toml": "in-place"})
|
|
87
|
+
recursive: bool | None = True
|
|
88
|
+
summary_on_own_line: bool | None = field(
|
|
89
|
+
default=True, metadata={"toml": "summary-on-own-line"}
|
|
90
|
+
)
|
|
91
|
+
force_reflow: bool | None = field(default=True, metadata={"toml": "force-reflow"})
|
|
92
|
+
|
|
93
|
+
def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
|
|
94
|
+
return ["docfmt"] + py_paths
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(kw_only=True)
|
|
98
|
+
class TomlSortConfig(BaseFormatterConfig):
|
|
99
|
+
table_path: ClassVar[str] = "tool.tomlsort"
|
|
100
|
+
|
|
101
|
+
sort_first: tuple[str, ...] | None = (
|
|
102
|
+
"project",
|
|
103
|
+
"dependency-groups",
|
|
104
|
+
"build-system",
|
|
105
|
+
)
|
|
106
|
+
sort_table_keys: bool | None = True
|
|
107
|
+
|
|
108
|
+
# explicit since the toml-sort CLI and library defaults disagree; must be
|
|
109
|
+
# pinned so the sync engine's normalization and the format task agree
|
|
110
|
+
spaces_before_inline_comment: int | None = 2
|
|
111
|
+
|
|
112
|
+
def command(self, py_paths: list[str], toml_paths: list[str]) -> list[str]:
|
|
113
|
+
return ["toml-sort", "-i"] + toml_paths
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# --- non-formatter tools ---
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(kw_only=True)
|
|
120
|
+
class PytestConfig(BaseToolConfig):
|
|
121
|
+
table_path: ClassVar[str] = "tool.pytest.ini_options"
|
|
122
|
+
|
|
123
|
+
addopts: str | None = "--import-mode=importlib -s -v -rA"
|
|
124
|
+
cache_dir: str | None = str(_paths.PYTEST_CACHE_PATH)
|
|
125
|
+
testpaths: str | None = "test"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(kw_only=True)
|
|
129
|
+
class CoverageRunConfig(BaseToolConfig):
|
|
130
|
+
table_path: ClassVar[str] = "tool.coverage.run"
|
|
131
|
+
|
|
132
|
+
data_file: str | None = str(_paths.COVERAGE_DATA_PATH)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@dataclass(kw_only=True)
|
|
136
|
+
class CoverageReportConfig(BaseToolConfig):
|
|
137
|
+
table_path: ClassVar[str] = "tool.coverage.report"
|
|
138
|
+
|
|
139
|
+
exclude_lines: tuple[str, ...] | None = (
|
|
140
|
+
"if TYPE_CHECKING:",
|
|
141
|
+
"\\.\\.\\.$",
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass(kw_only=True)
|
|
146
|
+
class DoitConfig(BaseToolConfig):
|
|
147
|
+
table_path: ClassVar[str] = "tool.doit"
|
|
148
|
+
|
|
149
|
+
dep_file: str | None = str(_paths.DOIT_DB_PATH)
|
|
150
|
+
verbosity: int | None = 2
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass(kw_only=True)
|
|
154
|
+
class NoxConfig(BaseToolConfig):
|
|
155
|
+
table_path: ClassVar[str] = "tool.nox"
|
|
156
|
+
|
|
157
|
+
default_venv_backend: str | None = "uv"
|
|
158
|
+
envdir: str | None = str(_paths.NOX_ENVDIR_PATH)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(kw_only=True)
|
|
162
|
+
class MypyConfig(BaseToolConfig):
|
|
163
|
+
table_path: ClassVar[str] = "tool.mypy"
|
|
164
|
+
|
|
165
|
+
ignore_missing_imports: bool | None = True
|
|
166
|
+
|
|
167
|
+
def extra_toml(self, project: ProjectConfig) -> dict[str, Any]:
|
|
168
|
+
return {"python_version": project.python.mypy_version}
|
pyprojkit/discovery.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Discovery of a project's `pyprojconf.py`.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import importlib.util
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from .config.base import ConfigError
|
|
12
|
+
from .config.project import ProjectConfig
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"load_config",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
CONF_FILENAME = "pyprojconf.py"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_config(root: Path | str | None = None) -> ProjectConfig:
|
|
22
|
+
"""
|
|
23
|
+
Load `pyprojconf.py` from the given project root (defaulting to the current
|
|
24
|
+
directory) and return its `config`.
|
|
25
|
+
"""
|
|
26
|
+
root_path = Path(root) if root else Path.cwd()
|
|
27
|
+
conf_path = root_path / CONF_FILENAME
|
|
28
|
+
|
|
29
|
+
if not conf_path.is_file():
|
|
30
|
+
raise ConfigError(f"'{conf_path}' not found")
|
|
31
|
+
|
|
32
|
+
module_name = f"_pyprojconf_{abs(hash(str(conf_path.resolve())))}"
|
|
33
|
+
spec = importlib.util.spec_from_file_location(module_name, conf_path)
|
|
34
|
+
assert spec and spec.loader
|
|
35
|
+
|
|
36
|
+
module = importlib.util.module_from_spec(spec)
|
|
37
|
+
sys.modules[module_name] = module
|
|
38
|
+
try:
|
|
39
|
+
spec.loader.exec_module(module)
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
raise ConfigError(f"Failed to import '{conf_path}': {exc}") from exc
|
|
42
|
+
finally:
|
|
43
|
+
sys.modules.pop(module_name, None)
|
|
44
|
+
|
|
45
|
+
config = getattr(module, "config", None)
|
|
46
|
+
if not isinstance(config, ProjectConfig):
|
|
47
|
+
raise ConfigError(
|
|
48
|
+
f"'{conf_path}' must define a module-level `config: ProjectConfig`"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return config
|
pyprojkit/sessions.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""
|
|
2
|
+
`nox` session factories.
|
|
3
|
+
|
|
4
|
+
Usage in a project's `noxfile.py`:
|
|
5
|
+
|
|
6
|
+
```python
|
|
7
|
+
from pyprojkit import NoxFactory
|
|
8
|
+
|
|
9
|
+
test = NoxFactory().create_test_session()
|
|
10
|
+
```
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Callable
|
|
17
|
+
|
|
18
|
+
import nox
|
|
19
|
+
from nox import Session
|
|
20
|
+
|
|
21
|
+
from .config.base import ConfigError
|
|
22
|
+
from .config.project import ProjectConfig
|
|
23
|
+
from .discovery import load_config
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"NoxFactory",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class NoxFactory:
|
|
31
|
+
"""
|
|
32
|
+
Factory for `nox` sessions, configured by the project's `pyprojconf.py`.
|
|
33
|
+
|
|
34
|
+
Sessions are registered with nox upon creation, so `create_*_session()` must be
|
|
35
|
+
called at `noxfile.py` import time.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
_config: ProjectConfig
|
|
39
|
+
_registered: set[str]
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
config: ProjectConfig | None = None,
|
|
44
|
+
root: Path | str | None = None,
|
|
45
|
+
):
|
|
46
|
+
self._config = config or load_config(root)
|
|
47
|
+
self._registered = set()
|
|
48
|
+
nox.options.envdir = self._config.tools.nox.envdir
|
|
49
|
+
|
|
50
|
+
def create_test_session(self, *, group: str = "dev") -> Callable[[Session], None]:
|
|
51
|
+
"""
|
|
52
|
+
Create and register `test` session which runs pytest over all supported Python
|
|
53
|
+
versions, installing dependencies from the given dependency group via uv.
|
|
54
|
+
"""
|
|
55
|
+
if "test" in self._registered:
|
|
56
|
+
raise ConfigError("Session 'test' already created")
|
|
57
|
+
self._registered.add("test")
|
|
58
|
+
|
|
59
|
+
@nox.session(python=self._config.python.pins, name="test")
|
|
60
|
+
def test(session: Session):
|
|
61
|
+
session.run_install(
|
|
62
|
+
"uv",
|
|
63
|
+
"sync",
|
|
64
|
+
f"--group={group}",
|
|
65
|
+
"--frozen",
|
|
66
|
+
f"--python={session.python}", # explicitly pin the version
|
|
67
|
+
env={"UV_PROJECT_ENVIRONMENT": session.virtualenv.location},
|
|
68
|
+
)
|
|
69
|
+
session.run("pytest", *session.posargs)
|
|
70
|
+
|
|
71
|
+
return test
|