Skip to content

Commit

Permalink
Require nox.needs_version to be specified statically
Browse files Browse the repository at this point in the history
Drop the two-phase approach and support only version requirements that can be
determined by parsing the AST of the user's Noxfile.
  • Loading branch information
cjolowicz committed Feb 20, 2021
1 parent 966a39f commit 3b879a8
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 39 deletions.
9 changes: 5 additions & 4 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ When invoking ``nox``, any options specified on the command line take precedence
Nox version requirements
------------------------

Nox version requirements can be specified in your ``noxfile.py`` by setting
Nox version requirements can be specified in your Noxfile by setting
``nox.needs_version``. If the Nox version does not satisfy the requirements, Nox
exits with a friendly error message. For example:

Expand All @@ -412,9 +412,10 @@ exits with a friendly error message. For example:
def pytest(session):
session.run("pytest")
Any of the version specifiers defined in `PEP 440`_ can be used.

Any of the version specifiers defined in `PEP 440`_ can be used. If you assign a
string literal like in the example above, Nox is able to check the version
without importing the Noxfile.
**Important**: Version requirements *must* be specified as a string literal,
using a simple assignment to ``nox.needs_version`` at the module level. This
allows Nox to check the version without importing the Noxfile.

.. _PEP 440: https://www.python.org/dev/peps/pep-0440/
14 changes: 5 additions & 9 deletions nox/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,25 +93,21 @@ def _check_nox_version_satisfies(needs_version: str) -> None:
)


def check_nox_version(filename: Optional[str] = None) -> None:
def check_nox_version(filename: str) -> None:
"""Check if ``nox.needs_version`` in the user's noxfile is satisfied.
Args:
filename: The location of the user's noxfile. When specified, read
``nox.needs_version`` from the noxfile by parsing the AST.
Otherwise, assume that the noxfile was already imported, and
use ``nox.needs_version`` directly.
filename: The location of the user's noxfile. ``nox.needs_version`` is
read from the noxfile by parsing the AST.
Raises:
VersionCheckFailed: The Nox version does not satisfy what
``nox.needs_version`` specifies.
InvalidVersionSpecifier: The ``nox.needs_version`` specifier cannot be
parsed.
"""
from nox import needs_version

if filename is not None:
needs_version = _read_needs_version(filename) # noqa: F811
needs_version = _read_needs_version(filename)

if needs_version is not None:
_check_nox_version_satisfies(needs_version)
7 changes: 1 addition & 6 deletions nox/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,10 @@ def load_nox_module(global_config: Namespace) -> Union[types.ModuleType, int]:
# import-time path resolutions work the way the Noxfile author would
# guess.
os.chdir(os.path.realpath(os.path.dirname(global_config.noxfile)))
module = importlib.machinery.SourceFileLoader(
return importlib.machinery.SourceFileLoader(
"user_nox_module", global_config.noxfile
).load_module() # type: ignore

# Check ``nox.needs_version`` as set by the Noxfile.
check_nox_version()

return module

except (VersionCheckFailed, InvalidVersionSpecifier) as error:
logger.error(str(error))
return 2
Expand Down
52 changes: 32 additions & 20 deletions tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import json
import os
import platform
from pathlib import Path
from textwrap import dedent
from unittest import mock

Expand Down Expand Up @@ -81,31 +80,44 @@ def test_load_nox_module_not_found():
assert tasks.load_nox_module(config) == 2


@pytest.mark.parametrize(
"text",
[
dedent(
"""
import nox
nox.needs_version = ">=9999.99.99"
"""
),
dedent(
"""
import nox
NOX_NEEDS_VERSION = ">=9999.99.99"
nox.needs_version = NOX_NEEDS_VERSION
"""
),
],
)
def test_load_nox_module_needs_version(text: str, tmp_path: Path):
@pytest.fixture
def reset_needs_version():
"""Do not leak ``nox.needs_version`` between tests."""
try:
yield
finally:
nox.needs_version = None


def test_load_nox_module_needs_version_static(reset_needs_version, tmp_path):
text = dedent(
"""
import nox
nox.needs_version = ">=9999.99.99"
"""
)
noxfile = tmp_path / "noxfile.py"
noxfile.write_text(text)
config = _options.options.namespace(noxfile=str(noxfile))
assert tasks.load_nox_module(config) == 2


def test_load_nox_module_needs_version_dynamic(reset_needs_version, tmp_path):
text = dedent(
"""
import nox
NOX_NEEDS_VERSION = ">=9999.99.99"
nox.needs_version = NOX_NEEDS_VERSION
"""
)
noxfile = tmp_path / "noxfile.py"
noxfile.write_text(text)
config = _options.options.namespace(noxfile=str(noxfile))
tasks.load_nox_module(config)
# Dynamic version requirements are not checked.
assert nox.needs_version == ">=9999.99.99"


def test_discover_session_functions_decorator():
# Define sessions using the decorator.
@nox.session
Expand Down

0 comments on commit 3b879a8

Please sign in to comment.