11from __future__ import annotations
22
3- import os
3+ import functools
44import pathlib
55import shutil
6- import sys
7- from functools import partial
86
97import nox
108
119# Control factors for finding pieces of the module
1210MODULE_NAME = "module_name"
13- TESTS_PATH = "tests"
14- COVERAGE_FAIL_UNDER = 50
15- VENV_PATH = "./.venv"
11+ COVERAGE_FAIL_UNDER = "100"
1612LINT_PATH = "./src"
17- REQUIREMENTS_PATH = "./requirements"
18-
19-
20- LINTING_COMMANDS = (
21- (
22- "isort" ,
23- "--verbose" ,
24- "--force-single-line-imports" ,
25- "--profile" ,
26- "black" ,
27- "--add-import" ,
28- "from __future__ import annotations" ,
29- LINT_PATH ,
30- TESTS_PATH ,
31- ),
32- ("black" , "--verbose" , LINT_PATH , TESTS_PATH ),
33- ("flake8" , "--show-source" , "--verbose" , LINT_PATH , TESTS_PATH ),
34- ("mypy" , "--no-incremental" , "--package" , MODULE_NAME ),
35- ("mypy" , "--no-incremental" , TESTS_PATH ),
36- )
13+ TESTS_PATH = "./tests"
3714
3815# What we allowed to clean (delete)
3916CLEANABLE_TARGETS = [
4320 "./.coverage" ,
4421 "./.coverage.*" ,
4522 "./coverage.json" ,
23+ "./htmlcov" ,
4624 "./**/.mypy_cache" ,
4725 "./**/.pytest_cache" ,
4826 "./**/__pycache__" ,
5129]
5230
5331# Define the default sessions run when `nox` is called on the CLI
54- nox .options .default_venv_backend = "virtualenv "
32+ nox .options .default_venv_backend = "uv "
5533nox .options .sessions = ["lint" , "test" ]
5634
35+ # All linters are run with `uv run --active`
36+ # Ordering matters. Formatters should run before static checks.
37+ LINTERS : list [tuple [str , ...]] = [
38+ (
39+ "isort" ,
40+ "--verbose" ,
41+ "--force-single-line-imports" ,
42+ "--profile" ,
43+ "black" ,
44+ "--add-import" ,
45+ "from __future__ import annotations" ,
46+ LINT_PATH ,
47+ TESTS_PATH ,
48+ ),
49+ ("black" , "--verbose" , LINT_PATH , TESTS_PATH ),
50+ ("flake8" , "--verbose" , "--show-source" , LINT_PATH , TESTS_PATH ),
51+ ("mypy" , "--pretty" , "--no-incremental" , "--package" , MODULE_NAME ),
52+ ("mypy" , "--pretty" , "--no-incremental" , TESTS_PATH ),
53+ ]
5754
58- @nox .session ()
59- def dev (session : nox .Session ) -> None :
60- """Setup a development environment by creating the venv and installs dependencies."""
61- # Use the active environement if it exists, otherwise create a new one
62- venv_path = os .environ .get ("VIRTUAL_ENV" , VENV_PATH )
63-
64- if sys .platform == "win32" :
65- venv_path = f"{ venv_path } /Scripts"
66- activate_command = f"{ venv_path } /activate"
67- else :
68- venv_path = f"{ venv_path } /bin"
69- activate_command = f"source { venv_path } /activate"
70-
71- if not os .path .exists (VENV_PATH ):
72- session .run ("python" , "-m" , "venv" , VENV_PATH , "--upgrade-deps" )
73-
74- python = partial (session .run , f"{ venv_path } /python" , "-m" )
75- contraint = ("--constraint" , f"{ REQUIREMENTS_PATH } /constraints.txt" )
7655
77- python ("pip" , "install" , "--editable" , ".[dev,test]" , * contraint , external = True )
56+ @nox .session (name = "dev" , python = False )
57+ def dev_session (session : nox .Session ) -> None :
58+ """Create a development environment. Optionally: Provide the python version to use."""
59+ python_version : list [str ] = []
60+ if session .posargs :
61+ python_version = ["--python" , session .posargs [0 ]]
7862
79- if not os . environ . get ( "VIRTUAL_ENV" ):
80- session .log ( f" \n \n Run ' { activate_command } ' to enter the virtual environment. \n " )
63+ session . run_install ( "uv" , "sync" , "--frozen" , "--all-groups" , * python_version , external = True )
64+ session .run_install ( "uv" , "run" , "pre-commit" , "install" , external = True )
8165
8266
8367@nox .session (name = "test" )
8468def run_tests_with_coverage (session : nox .Session ) -> None :
85- """Run pytest with coverage, outputs console report and json ."""
69+ """Run pytest in isolated environment, display coverage. Extra arguements passed to pytest ."""
8670 print_standard_logs (session )
8771
88- contraint = ("--constraint" , f"{ REQUIREMENTS_PATH } /constraints.txt" )
72+ partial = "partial-coverage" in session .posargs
73+ extra : list [str ] = []
74+ if "no-config" in session .posargs :
75+ session .posargs .remove ("no-config" )
76+ extra = ["--no-config" ]
8977
90- session .install ( ".[ test] " , * contraint )
78+ session .run_install ( "uv" , "sync" , "--frozen" , "--active" , "--group" , " test" , * extra )
9179
92- coverage = partial (session .run , "python " , "-m" , "coverage" )
80+ coverage = functools . partial (session .run , "uv " , "run" , "--active" , * extra , "coverage" )
9381
9482 coverage ("erase" )
9583
96- if "partial-coverage" in session .posargs :
97- coverage ("run" , "--parallel-mode" , "--module" , "pytest" , TESTS_PATH )
84+ if partial :
85+ session .posargs .remove ("partial-coverage" )
86+ coverage ("run" , "--parallel-mode" , "--module" , "pytest" , * session .posargs )
9887 else :
99- coverage ("run" , "--module" , "pytest" , TESTS_PATH )
88+ coverage ("run" , "--module" , "pytest" , * session . posargs )
10089 coverage ("report" , "--show-missing" , f"--fail-under={ COVERAGE_FAIL_UNDER } " )
101- coverage ("json " )
90+ coverage ("html " )
10291
10392
104- @nox .session ()
105- def coverage_combine (session : nox .Session ) -> None :
106- """CI: Combine parallel-mode coverage files and produce reports."""
93+ @nox .session (name = "combine" )
94+ def combine_coverage (session : nox .Session ) -> None :
95+ """Combine parallel-mode coverage files and produce reports."""
10796 print_standard_logs (session )
10897
109- contraint = ( "--constraint " , f" { REQUIREMENTS_PATH } /constraints.txt " )
98+ session . run_install ( "uv" , "sync" , "--frozen " , "--active" , "--group" , "test " )
11099
111- session . install ( "-r " , f" { REQUIREMENTS_PATH } /requirements-test.txt " , * contraint )
100+ coverage = functools . partial ( session . run , "uv " , "run" , "--active " , "coverage" )
112101
113- coverage = partial (session .run , "python" , "-m" , "coverage" )
114102 coverage ("combine" )
115103 coverage ("report" , "--show-missing" , f"--fail-under={ COVERAGE_FAIL_UNDER } " )
116- coverage ("json " )
104+ coverage ("html " )
117105
118106
119107@nox .session (name = "lint" )
120108def run_linters_and_formatters (session : nox .Session ) -> None :
121109 """Run code formatters, linters, and type checking against all files."""
122110 print_standard_logs (session )
123111
124- contraint = ("--constraint" , f"{ REQUIREMENTS_PATH } /constraints.txt" )
125- session .install (".[dev,test]" , * contraint )
126-
127- python = partial (session .run , "python" , "-m" )
112+ session .run_install ("uv" , "sync" , "--frozen" , "--active" , "--group" , "test" , "--group" , "lint" )
128113
129- for linter_command in LINTING_COMMANDS :
130- python ( * linter_command )
114+ for linter_args in LINTERS :
115+ session . run ( "uv" , "run" , "--active" , * linter_args )
131116
132117
133- @nox .session ()
134- def build (session : nox .Session ) -> None :
135- """Build distribution files ."""
118+ @nox .session (name = "build" )
119+ def build_artifacts (session : nox .Session ) -> None :
120+ """Build a sdist and wheel ."""
136121 print_standard_logs (session )
137122
138- session .install ("build" )
139- session .run ("python" , "-m" , "build" )
123+ session .run ("uv" , "build" )
140124
141125
142- @nox .session (name = "update-deps " )
143- def update_deps (session : nox .Session ) -> None :
144- """Process requirement*.txt files, updating only additions/removals ."""
126+ @nox .session (name = "upgrade " )
127+ def upgrade_dependencies (session : nox .Session ) -> None :
128+ """Upgrade all versions of all dependencies ."""
145129 print_standard_logs (session )
146130
147- session .install ("pip-tools" )
148- session .run (
149- "pip-compile" ,
150- "--strip-extras" ,
151- "--no-annotate" ,
152- "--no-emit-index-url" ,
153- "--output-file" ,
154- f"{ REQUIREMENTS_PATH } /constraints.txt" ,
155- * get_requirement_files (),
156- )
157-
158-
159- @nox .session (name = "upgrade-deps" )
160- def upgrade_deps (session : nox .Session ) -> None :
161- """Process requirement*.txt files and upgrade all libraries as possible."""
131+ session .run ("uv" , "lock" , "--upgrade" )
132+
133+
134+ @nox .session (name = "upgrade-package" )
135+ def upgrade_specific_package (session : nox .Session ) -> None :
136+ """Upgrade specific package name given in extra args."""
162137 print_standard_logs (session )
163138
164- session .install ("pip-tools" )
165- session .run (
166- "pip-compile" ,
167- "--strip-extras" ,
168- "--no-annotate" ,
169- "--no-emit-index-url" ,
170- "--upgrade" ,
171- "--output-file" ,
172- f"{ REQUIREMENTS_PATH } /constraints.txt" ,
173- * get_requirement_files (),
174- )
139+ if not session .posargs :
140+ session .log ("No package name provided, nothing to do." )
141+
142+ else :
143+ session .run ("uv" , "lock" , "--upgrade-package" , * session .posargs )
175144
176145
177146@nox .session (python = False )
178- def clean (_ : nox .Session ) -> None :
179- """Clean cache, .pyc, .pyo, and test/ build artifact files from project."""
147+ def clean (session : nox .Session ) -> None :
148+ """Clean cache, .pyc, .pyo, and build artifact files from project."""
180149 count = 0
181150 for searchpath in CLEANABLE_TARGETS :
182151 for filepath in pathlib .Path ("." ).glob (searchpath ):
@@ -186,17 +155,11 @@ def clean(_: nox.Session) -> None:
186155 filepath .unlink ()
187156 count += 1
188157
189- print (f"{ count } files cleaned." )
158+ session . log (f"{ count } files cleaned." )
190159
191160
192161def print_standard_logs (session : nox .Session ) -> None :
193162 """Reusable output for monitoring environment factors."""
194163 version = session .run ("python" , "--version" , silent = True )
195164 session .log (f"Running from: { session .bin } " )
196165 session .log (f"Running with: { version } " )
197-
198-
199- def get_requirement_files () -> list [pathlib .Path ]:
200- """Get a list of requirement files matching "requirements*.txt"."""
201- glob = pathlib .Path (REQUIREMENTS_PATH ).glob ("requirements*.txt" )
202- return [path for path in glob ]
0 commit comments