Skip to content

Commit

Permalink
feat: support setting an empty session list (#523)
Browse files Browse the repository at this point in the history
* feat: support setting an empty session list

* refactor: better error messages for empty sessions

* Apply suggestions from code review

Co-authored-by: Claudio Jolowicz <cjolowicz@gmail.com>

* Update tests/test_tasks.py

Co-authored-by: Tom Fleet <tomfleet2018@gmail.com>

* refactor: empty list printing

* refactor: simpler impl

* Remove debugging statement from noxfile.py

* Blacken

Co-authored-by: Tom Fleet <tomfleet2018@gmail.com>
Co-authored-by: Claudio Jolowicz <mail@claudiojolowicz.com>
  • Loading branch information
3 people committed Dec 27, 2021
1 parent c94b62e commit f5be2cc
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 38 deletions.
2 changes: 1 addition & 1 deletion docs/config.rst
Expand Up @@ -426,7 +426,7 @@ 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-pythons-and-keywords>`.
* ``nox.options.sessions`` is equivalent to specifying :ref:`-s or --sessions <opt-sessions-pythons-and-keywords>`. If set to an empty list, no sessions will be run if no sessions were given on the command line, and the list of available sessions will be shown instead.
* ``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.default_venv_backend`` is equivalent to specifying :ref:`-db or --default-venv-backend <opt-default-venv-backend>`.
Expand Down
14 changes: 14 additions & 0 deletions docs/tutorial.rst
Expand Up @@ -258,6 +258,20 @@ And if you run ``nox --sessions lint`` Nox will just run the lint session:
nox > ...
nox > Session lint was successful.
In the noxfile, you can specify a default set of sessions to run. If so, a plain
``nox`` call will only trigger certain sessions:

.. code-block:: python
import nox
nox.options.sessions = ["lint", "test"]
If you set this to an empty list, Nox will not run any sessions by default, and
will print a helpful message with the ``--list`` output when a user does not
specify a session to run.

There are many more ways to select and run sessions! You can read more about
invoking Nox in :doc:`usage`.

Expand Down
1 change: 0 additions & 1 deletion nox/__main__.py
Expand Up @@ -50,7 +50,6 @@ def main() -> None:
tasks.discover_manifest,
tasks.filter_manifest,
tasks.honor_list_request,
tasks.verify_manifest_nonempty,
tasks.run_manifest,
tasks.print_summary,
tasks.create_report,
Expand Down
62 changes: 33 additions & 29 deletions nox/tasks.py
Expand Up @@ -167,22 +167,33 @@ def filter_manifest(
the manifest otherwise (to be sent to the next task).
"""
# Shouldn't happen unless the noxfile is empty
if not manifest:
logger.error(f"No sessions found in {global_config.noxfile}.")
return 3

# Filter by the name of any explicit sessions.
# This can raise KeyError if a specified session does not exist;
# log this if it happens.
if global_config.sessions:
# log this if it happens. The sessions does not come from the noxfile
# if keywords is not empty.
if global_config.sessions is not None:
try:
manifest.filter_by_name(global_config.sessions)
except KeyError as exc:
logger.error("Error while collecting sessions.")
logger.error(exc.args[0])
return 3
if not manifest and not global_config.list_sessions:
print("No sessions selected. Please select a session with -s <session name>.\n")
_produce_listing(manifest, global_config)
return 0

# 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)
if not manifest and not global_config.list_sessions:
logger.error("Python version selection caused no sessions to be selected.")
return 3

# Filter by keywords.
if global_config.keywords:
Expand All @@ -198,28 +209,19 @@ def filter_manifest(
# (which is an error condition later).
manifest.filter_by_keywords(global_config.keywords)

if not manifest and not global_config.list_sessions:
logger.error("No sessions selected after filtering by keyword.")
return 3

# Return the modified manifest.
return manifest


def honor_list_request(
manifest: Manifest, global_config: Namespace
) -> Union[Manifest, int]:
"""If --list was passed, simply list the manifest and exit cleanly.
Args:
manifest (~.Manifest): The manifest of sessions to be run.
global_config (~nox.main.GlobalConfig): The global configuration.
Returns:
Union[~.Manifest,int]: ``0`` if a listing is all that is requested,
the manifest otherwise (to be sent to the next task).
"""
if not global_config.list_sessions:
return manifest

def _produce_listing(manifest: Manifest, global_config: Namespace) -> None:
# If the user just asked for a list of sessions, print that
# and any docstring specified in noxfile.py and be done.
# and any docstring specified in noxfile.py and be done. This
# can also be called if noxfile sessions is an empty list.

if manifest.module_docstring:
print(manifest.module_docstring.strip(), end="\n\n")

Expand Down Expand Up @@ -255,25 +257,27 @@ def honor_list_request(
print(
f"\nsessions marked with {selected_color}*{reset} are selected, sessions marked with {skipped_color}-{reset} are skipped."
)
return 0


def verify_manifest_nonempty(
def honor_list_request(
manifest: Manifest, global_config: Namespace
) -> Union[Manifest, int]:
"""Abort with an error code if the manifest is empty.
"""If --list was passed, simply list the manifest and exit cleanly.
Args:
manifest (~.Manifest): The manifest of sessions to be run.
global_config (~nox.main.GlobalConfig): The global configuration.
Returns:
Union[~.Manifest,int]: ``3`` on an empty manifest, the manifest
otherwise.
Union[~.Manifest,int]: ``0`` if a listing is all that is requested,
the manifest otherwise (to be sent to the next task).
"""
if not manifest:
return 3
return manifest
if not global_config.list_sessions:
return manifest

_produce_listing(manifest, global_config)

return 0


def run_manifest(manifest: Manifest, global_config: Namespace) -> List[Result]:
Expand Down
62 changes: 55 additions & 7 deletions tests/test_tasks.py
Expand Up @@ -209,7 +209,7 @@ def notasession():

def test_filter_manifest():
config = _options.options.namespace(
sessions=(), pythons=(), keywords=(), posargs=[]
sessions=None, pythons=(), keywords=(), posargs=[]
)
manifest = Manifest({"foo": session_func, "bar": session_func}, config)
return_value = tasks.filter_manifest(manifest, config)
Expand All @@ -228,7 +228,7 @@ def test_filter_manifest_not_found():

def test_filter_manifest_pythons():
config = _options.options.namespace(
sessions=(), pythons=("3.8",), keywords=(), posargs=[]
sessions=None, pythons=("3.8",), keywords=(), posargs=[]
)
manifest = Manifest(
{"foo": session_func_with_python, "bar": session_func, "baz": session_func},
Expand All @@ -239,9 +239,22 @@ def test_filter_manifest_pythons():
assert len(manifest) == 1


def test_filter_manifest_pythons_not_found(caplog):
config = _options.options.namespace(
sessions=None, pythons=("1.2",), keywords=(), posargs=[]
)
manifest = Manifest(
{"foo": session_func_with_python, "bar": session_func, "baz": session_func},
config,
)
return_value = tasks.filter_manifest(manifest, config)
assert return_value == 3
assert "Python version selection caused no sessions to be selected." in caplog.text


def test_filter_manifest_keywords():
config = _options.options.namespace(
sessions=(), pythons=(), keywords="foo or bar", posargs=[]
sessions=None, pythons=(), keywords="foo or bar", posargs=[]
)
manifest = Manifest(
{"foo": session_func, "bar": session_func, "baz": session_func}, config
Expand All @@ -251,9 +264,21 @@ def test_filter_manifest_keywords():
assert len(manifest) == 2


def test_filter_manifest_keywords_not_found(caplog):
config = _options.options.namespace(
sessions=None, pythons=(), keywords="mouse or python", posargs=[]
)
manifest = Manifest(
{"foo": session_func, "bar": session_func, "baz": session_func}, config
)
return_value = tasks.filter_manifest(manifest, config)
assert return_value == 3
assert "No sessions selected after filtering by keyword." in caplog.text


def test_filter_manifest_keywords_syntax_error():
config = _options.options.namespace(
sessions=(), pythons=(), keywords="foo:bar", posargs=[]
sessions=None, pythons=(), keywords="foo:bar", posargs=[]
)
manifest = Manifest({"foo_bar": session_func, "foo_baz": session_func}, config)
return_value = tasks.filter_manifest(manifest, config)
Expand Down Expand Up @@ -346,20 +371,43 @@ def test_honor_list_request_doesnt_print_docstring_if_not_present(capsys):
assert "Hello I'm a docstring" not in out


def test_empty_session_list_in_noxfile(capsys):
config = _options.options.namespace(noxfile="noxfile.py", sessions=(), posargs=[])
manifest = Manifest({"session": session_func}, config)
return_value = tasks.filter_manifest(manifest, global_config=config)
assert return_value == 0
assert "No sessions selected." in capsys.readouterr().out


def test_empty_session_None_in_noxfile(capsys):
config = _options.options.namespace(noxfile="noxfile.py", sessions=None, posargs=[])
manifest = Manifest({"session": session_func}, config)
return_value = tasks.filter_manifest(manifest, global_config=config)
assert return_value == manifest


def test_verify_manifest_empty():
config = _options.options.namespace(sessions=(), keywords=())
manifest = Manifest({}, config)
return_value = tasks.verify_manifest_nonempty(manifest, global_config=config)
return_value = tasks.filter_manifest(manifest, global_config=config)
assert return_value == 3


def test_verify_manifest_nonempty():
config = _options.options.namespace(sessions=(), keywords=(), posargs=[])
config = _options.options.namespace(sessions=None, keywords=(), posargs=[])
manifest = Manifest({"session": session_func}, config)
return_value = tasks.verify_manifest_nonempty(manifest, global_config=config)
return_value = tasks.filter_manifest(manifest, global_config=config)
assert return_value == manifest


def test_verify_manifest_list(capsys):
config = _options.options.namespace(sessions=(), keywords=(), posargs=[])
manifest = Manifest({"session": session_func}, config)
return_value = tasks.filter_manifest(manifest, global_config=config)
assert return_value == 0
assert "Please select a session" in capsys.readouterr().out


@pytest.mark.parametrize("with_warnings", [False, True], ids="with_warnings={}".format)
def test_run_manifest(with_warnings):
# Set up a valid manifest.
Expand Down

0 comments on commit f5be2cc

Please sign in to comment.