diff --git a/docs/config.rst b/docs/config.rst index 7773da18..4e720f1c 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -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 `. -* ``nox.options.sessions`` is equivalent to specifying :ref:`-s or --sessions `. +* ``nox.options.sessions`` is equivalent to specifying :ref:`-s or --sessions `. 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 `. * ``nox.options.keywords`` is equivalent to specifying :ref:`-k or --keywords `. * ``nox.options.default_venv_backend`` is equivalent to specifying :ref:`-db or --default-venv-backend `. diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 3441036e..f4aac4aa 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -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`. diff --git a/nox/__main__.py b/nox/__main__.py index d6043ac0..34786a69 100644 --- a/nox/__main__.py +++ b/nox/__main__.py @@ -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, diff --git a/nox/tasks.py b/nox/tasks.py index 9b149af7..a33f3138 100644 --- a/nox/tasks.py +++ b/nox/tasks.py @@ -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 .\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: @@ -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") @@ -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]: diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 906ef247..50f7f86b 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -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) @@ -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}, @@ -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 @@ -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) @@ -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.