Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
43ed78d
Add --download-python python option
saucoide Jul 7, 2025
d242525
add kwargs to condaenv
saucoide Jul 7, 2025
511768c
change removeprefix since its not supported in 3.8
saucoide Jul 7, 2025
0f4cb3f
linting
saucoide Jul 7, 2025
d71f32c
pass 'never' to expected failure on windows
saucoide Jul 7, 2025
bce383a
allow none
saucoide Jul 7, 2025
0d6b216
handle windows install layout
saucoide Jul 7, 2025
01a5c3e
fix windows test
saucoide Jul 7, 2025
8c4a3e3
fix buggy version matching
saucoide Jul 8, 2025
08d15cf
make mypy happy
saucoide Jul 9, 2025
1491d94
Merge branch 'wntrblm:main' into nox-download-python
saucoide Jul 22, 2025
f6686c8
silence http loggers
saucoide Jul 22, 2025
dd024ab
add new dependencies to the cli test
saucoide Jul 26, 2025
49dac51
rearrange to ensure windows is already fully checked before installing
saucoide Jul 26, 2025
33fdf9b
remove todos
saucoide Jul 26, 2025
0898ea9
do not let the test download python
saucoide Jul 26, 2025
847cb17
which is called multiple times on windows
saucoide Jul 30, 2025
28d2374
add documentation
saucoide Jul 30, 2025
99b09f1
whitespace
saucoide Jul 31, 2025
6b394e9
require pbs_installer during conda tests
saucoide Jul 31, 2025
33ae716
undo
saucoide Jul 31, 2025
d547a02
rename package
saucoide Jul 31, 2025
c4a791e
reorder
saucoide Jul 31, 2025
65f2918
add httpx to conda test reqs
saucoide Aug 4, 2025
d80a90d
add test to cover successful install finds new python
saucoide Aug 5, 2025
bcb73b4
test the success cases
saucoide Aug 5, 2025
cf38f58
patch uv too
saucoide Aug 5, 2025
6cd9257
test bad version & failed install for uv
saucoide Aug 5, 2025
c2d5a80
try to clarify tests
saucoide Aug 6, 2025
81bf241
fix tests
saucoide Aug 6, 2025
9138b38
test the auto case in the failed_install test
saucoide Aug 7, 2025
7760208
remove redudant test
saucoide Aug 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ repos:
- jinja2
- orjson # Faster mypy
- packaging
- pbs_installer
- pytest
- importlib_metadata
- importlib_resources
Expand Down
3 changes: 3 additions & 0 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ When you provide a version number, Nox automatically prepends python to determin
def tests(session):
pass

If the specified python interpreter is not found, Nox can automatically download it when ``--download-python`` is set to ``auto`` or ``always``.

When collecting your sessions, Nox will create a separate session for each interpreter. You can see these sessions when running ``nox --list``. For example this Noxfile:

.. code-block:: python
Expand Down Expand Up @@ -511,6 +513,7 @@ The following options can be specified in the Noxfile:
* ``nox.options.stop_on_first_error`` is equivalent to specifying :ref:`--stop-on-first-error <opt-stop-on-first-error>`. You can force this off by specifying ``--no-stop-on-first-error`` during invocation.
* ``nox.options.error_on_missing_interpreters`` is equivalent to specifying :ref:`--error-on-missing-interpreters <opt-error-on-missing-interpreters>`. You can force this off by specifying ``--no-error-on-missing-interpreters`` during invocation.
* ``nox.options.error_on_external_run`` is equivalent to specifying :ref:`--error-on-external-run <opt-error-on-external-run>`. You can force this off by specifying ``--no-error-on-external-run`` during invocation.
* ``nox.options.download_python`` is equivalent to specifying ``--download-python``.
* ``nox.options.report`` is equivalent to specifying :ref:`--report <opt-report>`.


Expand Down
15 changes: 15 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,21 @@ using the ``python`` specified for the current ``PATH``:

NOXFORCEPYTHON=python NOXSESSION=lint nox

Downloading Python interpreters
-------------------------------

Nox can download Python interpreters, either via uv or directly from
python-build-standalone, by using ``--download-python``:

.. code-block:: console

nox --download-python auto # Download if interpreter not found (default)
nox --download-python never # Never download interpreters
nox --download-python always # Always download interpreters

You can also set this option with the ``NOX_DOWNLOAD_PYTHON`` environment
variable.

.. _opt-stop-on-first-error:

Stopping if any session fails
Expand Down
32 changes: 29 additions & 3 deletions nox/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import sys
import urllib.parse
from pathlib import Path
from typing import TYPE_CHECKING, Any, NoReturn
from typing import TYPE_CHECKING, Any, Literal, NoReturn, cast

import packaging.requirements
import packaging.utils
Expand All @@ -32,7 +32,7 @@
import nox.virtualenv
from nox import _options, tasks, workflow
from nox._version import get_nox_version
from nox.logger import setup_logging
from nox.logger import logger, setup_logging
from nox.project import load_toml

if TYPE_CHECKING:
Expand Down Expand Up @@ -137,12 +137,18 @@ def check_url_dependency(dep_url: str, dist: importlib.metadata.Distribution) ->


def run_script_mode(
envdir: Path, *, reuse: bool, dependencies: list[str], venv_backend: str
envdir: Path,
*,
reuse: bool,
dependencies: list[str],
venv_backend: str,
download_python: Literal["auto", "never", "always"],
) -> NoReturn:
envdir.mkdir(exist_ok=True)
noxenv = envdir.joinpath("_nox_script_mode")
venv = nox.virtualenv.get_virtualenv(
*venv_backend.split("|"),
download_python=download_python,
reuse_existing=reuse,
envdir=str(noxenv),
)
Expand Down Expand Up @@ -208,12 +214,32 @@ def main() -> None:
)
)

download_python = (
os.environ.get("NOX_SCRIPT_DOWNLOAD_PYTHON")
or (
toml_config.get("tool", {})
.get("nox", {})
.get("script-download-python", "auto")
)
or args.download_python
)

if download_python not in ("auto", "never", "always"):
logger.warning(
f"Invalid parameter for {download_python=}. Defaulting to 'auto'"
)
download_python = "auto"
download_python = cast(
"Literal['auto', 'never', 'always']", download_python
)

envdir = Path(args.envdir or ".nox")
run_script_mode(
envdir,
reuse=nox_script_mode == "reuse",
dependencies=dependencies,
venv_backend=venv_backend,
download_python=download_python,
)

exit_code = execute_workflow(args)
Expand Down
6 changes: 5 additions & 1 deletion nox/_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import functools
import inspect
import types
from typing import TYPE_CHECKING, Any, Callable, TypeVar, cast
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, cast

if TYPE_CHECKING:
from collections.abc import Iterable, Mapping, Sequence
Expand Down Expand Up @@ -78,6 +78,7 @@ def __init__(
*,
default: bool = True,
requires: Sequence[str] | None = None,
download_python: Literal["auto", "never", "always"] | None = None,
) -> None:
self.func = func
self.python = python
Expand All @@ -89,6 +90,7 @@ def __init__(
self.tags = list(tags or [])
self.default = default
self.requires = list(requires or [])
self.download_python = download_python

def __repr__(self) -> str:
return f"{self.__class__.__name__}(name={self.name!r})"
Expand All @@ -110,6 +112,7 @@ def copy(self, name: str | None = None) -> Func:
self.tags,
default=self.default,
requires=self._requires,
download_python=self.download_python,
)

@property
Expand Down Expand Up @@ -165,6 +168,7 @@ def __init__(self, func: Func, param_spec: Param) -> None:
func.tags + param_spec.tags,
default=func.default,
requires=func.requires,
download_python=func.download_python,
)
self.call_spec = call_spec
self.session_signature = session_signature
Expand Down
3 changes: 3 additions & 0 deletions nox/_option_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ def __dir__() -> list[str]:
@attrs.define(slots=True, kw_only=True)
class NoxOptions:
default_venv_backend: None | str = attrs.field(validator=av_opt_str)
download_python: None | Literal["auto", "never", "always"] = attrs.field(
default=None, validator=av.optional(av.in_(["auto", "never", "always"]))
)
envdir: None | str | os.PathLike[str] = attrs.field(validator=av_opt_path)
error_on_external_run: bool = attrs.field(validator=av_bool)
error_on_missing_interpreters: bool = attrs.field(validator=av_bool)
Expand Down
14 changes: 14 additions & 0 deletions nox/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,20 @@ def _tag_completer(
help="Directory where Nox will store virtualenvs, this is ``.nox`` by default.",
completer=argcomplete.completers.DirectoriesCompleter(), # type: ignore[no-untyped-call]
),
_option_set.Option(
"download_python",
"--download-python",
"--download-python",
noxfile=True,
group=options.groups["python"],
default=lambda: os.getenv("NOX_DOWNLOAD_PYTHON"),
help=(
"When should nox download python standalone builds to run the sessions,"
" defaults to 'auto' which will download when the version requested can't"
" be found in the running environment."
),
choices=["auto", "never", "always"],
),
_option_set.Option(
"extra_pythons",
"--extra-pythons",
Expand Down
2 changes: 2 additions & 0 deletions nox/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,5 @@ def setup_logging(

# Silence noisy loggers
logging.getLogger("sh").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
6 changes: 5 additions & 1 deletion nox/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import copy
import functools
from typing import TYPE_CHECKING, Any, Callable, overload
from typing import TYPE_CHECKING, Any, Callable, Literal, overload

from ._decorators import Func

Expand Down Expand Up @@ -55,6 +55,7 @@ def session_decorator(
*,
default: bool = ...,
requires: Sequence[str] | None = ...,
download_python: Literal["auto", "never", "always"] | None = None,
) -> Callable[[RawFunc | Func], Func]: ...


Expand All @@ -71,6 +72,7 @@ def session_decorator(
*,
default: bool = True,
requires: Sequence[str] | None = None,
download_python: Literal["auto", "never", "always"] | None = None,
) -> Func | Callable[[RawFunc | Func], Func]:
"""Designate the decorated function as a session."""
# If `func` is provided, then this is the decorator call with the function
Expand All @@ -92,6 +94,7 @@ def session_decorator(
tags=tags,
default=default,
requires=requires,
download_python=download_python,
)

if py is not None and python is not None:
Expand All @@ -116,6 +119,7 @@ def session_decorator(
tags=tags,
default=default,
requires=requires,
download_python=download_python,
)
_REGISTRY[name or func.__name__] = fn
return fn
Expand Down
7 changes: 6 additions & 1 deletion nox/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1017,10 +1017,15 @@ def _create_venv(self) -> None:
or "virtualenv"
).split("|")

download_python = (
self.global_config.download_python or self.func.download_python or "auto"
)

self.venv = get_virtualenv(
*backends,
reuse_existing=reuse_existing,
download_python=download_python,
envdir=self.envdir,
reuse_existing=reuse_existing,
interpreter=self.func.python,
venv_params=self.func.venv_params,
)
Expand Down
Loading
Loading