Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to specify which Python versions to run with from the command line #304

Merged
merged 5 commits into from
Mar 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,6 @@ target/

# pyenv
.python-version

# PyCharm
.idea/
7 changes: 4 additions & 3 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ If you only want to run one of the parametrized sessions, see :ref:`running_para
Giving friendly names to parametrized sessions
----------------------------------------------

The automatically generated names for parametrized sessions, such as ``tests(django='1.9', database='postgres')``, can be long and unwieldy to work with even with using :ref:`keyword filtering <opt-sessions-and-keywords>`. You can give parametrized sessions custom IDs to help in this scenario. These two examples are equivalent:
The automatically generated names for parametrized sessions, such as ``tests(django='1.9', database='postgres')``, can be long and unwieldy to work with even with using :ref:`keyword filtering <opt-sessions-pythons-and-keywords>`. You can give parametrized sessions custom IDs to help in this scenario. These two examples are equivalent:

.. code-block:: python

Expand Down Expand Up @@ -372,8 +372,9 @@ Or, if you wanted to provide a set of sessions that are run by default:
The following options can be specified in the Noxfile:

* ``nox.options.envdir`` is equivalent to specifying :ref:`--envdir <opt-envdir>`.
* ``nox.options.sessions`` is equivalent to specifying :ref:`-s or --sessions <opt-sessions-and-keywords>`.
* ``nox.options.keywords`` is equivalent to specifying :ref:`-k or --keywords <opt-sessions-and-keywords>`.
* ``nox.options.sessions`` is equivalent to specifying :ref:`-s or --sessions <opt-sessions-pythons-and-keywords>`.
* ``nox.options.pythons`` is equivalent to specifying :ref:`-p or --pythons <opt-sessions-pythons-and-keywords>`.
* ``nox.options.keywords`` is equivalent to specifying :ref:`-k or --keywords <opt-sessions-pythons-and-keywords>`.
* ``nox.options.reuse_existing_virtualenvs`` is equivalent to specifying :ref:`--reuse-existing-virtualenvs <opt-reuse-existing-virtualenvs>`. You can force this off by specifying ``--no-reuse-existing-virtualenvs`` during invocation.
* ``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.
Expand Down
9 changes: 8 additions & 1 deletion docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ You can run every session by just executing ``nox`` without any arguments:
The order that sessions are executed is the order that they appear in the Noxfile.


.. _opt-sessions-and-keywords:
.. _opt-sessions-pythons-and-keywords:

Specifying one or more sessions
-------------------------------
Expand All @@ -67,6 +67,13 @@ You can also use the ``NOXSESSION`` environment variable:

Nox will run these sessions in the same order they are specified.

If you have a :ref:`configured session's virtualenv <virtualenv config>`, you can choose to run only sessions with given Python versions:

.. code-block:: console

nox --python 3.8
nox -p 3.7 3.8

You can also use `pytest-style keywords`_ to filter test sessions:

.. code-block:: console
Expand Down
27 changes: 19 additions & 8 deletions nox/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,21 +39,21 @@
)


def _sessions_and_keywords_merge_func(
def _session_filters_merge_func(
key: str, command_args: argparse.Namespace, noxfile_args: argparse.Namespace
) -> List[str]:
"""Only return the Noxfile value for sessions/keywords if neither sessions
or keywords are specified on the command-line.
"""Only return the Noxfile value for sessions/pythons/keywords if neither sessions,
pythons or keywords are specified on the command-line.

Args:
key (str): This function is used for both the "sessions" and "keywords"
key (str): This function is used for the "sessions", "pythons" and "keywords"
options, this allows using ``funtools.partial`` to pass the
same function for both options.
command_args (_option_set.Namespace): The options specified on the
command-line.
noxfile_Args (_option_set.Namespace): The options specified in the
noxfile_args (_option_set.Namespace): The options specified in the
Noxfile."""
if not command_args.sessions and not command_args.keywords:
if not any((command_args.sessions, command_args.pythons, command_args.keywords)):
return getattr(noxfile_args, key)
return getattr(command_args, key)

Expand Down Expand Up @@ -176,18 +176,29 @@ def _session_completer(
"--session",
group="primary",
noxfile=True,
merge_func=functools.partial(_sessions_and_keywords_merge_func, "sessions"),
merge_func=functools.partial(_session_filters_merge_func, "sessions"),
nargs="*",
default=_sessions_default,
help="Which sessions to run. By default, all sessions will run.",
completer=_session_completer,
),
_option_set.Option(
"pythons",
"-p",
"--pythons",
"--python",
group="primary",
noxfile=True,
merge_func=functools.partial(_session_filters_merge_func, "pythons"),
nargs="*",
help="Only run sessions that use the given python interpreter versions.",
),
_option_set.Option(
"keywords",
"-k",
"--keywords",
noxfile=True,
merge_func=functools.partial(_sessions_and_keywords_merge_func, "keywords"),
merge_func=functools.partial(_session_filters_merge_func, "keywords"),
help="Only run sessions that match the given expression.",
),
_option_set.Option(
Expand Down
12 changes: 11 additions & 1 deletion nox/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import argparse
import collections.abc
import itertools
from typing import Any, Iterable, Iterator, List, Mapping, Set, Tuple, Union
from typing import Any, Iterable, Iterator, List, Mapping, Sequence, Set, Tuple, Union

from nox._decorators import Call, Func
from nox.sessions import Session, SessionRunner
Expand Down Expand Up @@ -132,6 +132,16 @@ def filter_by_name(self, specified_sessions: Iterable[str]) -> None:
if missing_sessions:
raise KeyError("Sessions not found: {}".format(", ".join(missing_sessions)))

def filter_by_python_interpreter(self, specified_pythons: Sequence[str]) -> None:
"""Filter sessions in the queue based on the user-specified
python interpreter versions.

Args:
specified_pythons (Sequence[str]): A list of specified
python interpreter versions.
"""
self._queue = [x for x in self._queue if x.func.python in specified_pythons]

def filter_by_keywords(self, keywords: str) -> None:
"""Filter sessions using pytest-like keyword expressions.

Expand Down
6 changes: 6 additions & 0 deletions nox/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,12 @@ def filter_manifest(
logger.error(exc.args[0])
return 3

# Filter by python interpreter versions.
# This function never errors, but may cause an empty list of sessions
# (which is an error condition later).
if global_config.pythons:
manifest.filter_by_python_interpreter(global_config.pythons)

# Filter by keywords.
# This function never errors, but may cause an empty list of sessions
# (which is an error condition later).
Expand Down
10 changes: 10 additions & 0 deletions tests/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ def test_filter_by_name_not_found():
manifest.filter_by_name(("baz",))


def test_filter_by_python_interpreter():
sessions = create_mock_sessions()
manifest = Manifest(sessions, mock.sentinel.CONFIG)
manifest["foo"].func.python = "3.8"
manifest["bar"].func.python = "3.7"
manifest.filter_by_python_interpreter(("3.8",))
assert "foo" in manifest
assert "bar" not in manifest


def test_filter_by_keyword():
sessions = create_mock_sessions()
manifest = Manifest(sessions, mock.sentinel.CONFIG)
Expand Down
24 changes: 21 additions & 3 deletions tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ def session_func():
session_func.python = None


def session_func_with_python():
pass


session_func_with_python.python = "3.8"


def test_load_nox_module():
config = _options.options.namespace(noxfile=os.path.join(RESOURCES, "noxfile.py"))
noxfile_module = tasks.load_nox_module(config)
Expand Down Expand Up @@ -91,22 +98,33 @@ def notasession():


def test_filter_manifest():
config = _options.options.namespace(sessions=(), keywords=())
config = _options.options.namespace(sessions=(), pythons=(), keywords=())
manifest = Manifest({"foo": session_func, "bar": session_func}, config)
return_value = tasks.filter_manifest(manifest, config)
assert return_value is manifest
assert len(manifest) == 2


def test_filter_manifest_not_found():
config = _options.options.namespace(sessions=("baz",), keywords=())
config = _options.options.namespace(sessions=("baz",), pythons=(), keywords=())
manifest = Manifest({"foo": session_func, "bar": session_func}, config)
return_value = tasks.filter_manifest(manifest, config)
assert return_value == 3


def test_filter_manifest_pythons():
config = _options.options.namespace(sessions=(), pythons=("3.8",), keywords=())
manifest = Manifest(
{"foo": session_func_with_python, "bar": session_func, "baz": session_func},
config,
)
return_value = tasks.filter_manifest(manifest, config)
assert return_value is manifest
assert len(manifest) == 1


def test_filter_manifest_keywords():
config = _options.options.namespace(sessions=(), keywords="foo or bar")
config = _options.options.namespace(sessions=(), pythons=(), keywords="foo or bar")
manifest = Manifest(
{"foo": session_func, "bar": session_func, "baz": session_func}, config
)
Expand Down