diff --git a/nox/_option_set.py b/nox/_option_set.py index 39a84f6e..0a9cef47 100644 --- a/nox/_option_set.py +++ b/nox/_option_set.py @@ -23,7 +23,7 @@ import functools from argparse import ArgumentError as ArgumentError from argparse import ArgumentParser, Namespace -from collections.abc import Callable, Sequence +from collections.abc import Callable, Iterable from typing import Any import argcomplete @@ -92,7 +92,7 @@ def __init__( | list[str] | Callable[[], bool | str | None | list[str]] = None, hidden: bool = False, - completer: Callable[..., Sequence[str]] | None = None, + completer: Callable[..., Iterable[str]] | None = None, **kwargs: Any, ) -> None: self.name = name diff --git a/nox/_options.py b/nox/_options.py index 9c06f8ad..df5e46d9 100644 --- a/nox/_options.py +++ b/nox/_options.py @@ -16,10 +16,14 @@ import argparse import functools +import itertools import os import sys +from collections.abc import Iterable from typing import Any, Callable, Sequence +import argcomplete + from nox import _option_set from nox.tasks import discover_manifest, filter_manifest, load_nox_module @@ -229,19 +233,42 @@ def _posargs_finalizer( return posargs[dash_index + 1 :] +def _python_completer( + prefix: str, parsed_args: argparse.Namespace, **kwargs: Any +) -> Iterable[str]: + module = load_nox_module(parsed_args) + manifest = discover_manifest(module, parsed_args) + return filter( + None, + ( + session.func.python # type:ignore[misc] # str sequences flattened, other non-strs falsey and filtered out + for session, _ in manifest.list_all_sessions() + ), + ) + + def _session_completer( prefix: str, parsed_args: argparse.Namespace, **kwargs: Any -) -> list[str]: - global_config = parsed_args - global_config.list_sessions = True - module = load_nox_module(global_config) - manifest = discover_manifest(module, global_config) - filtered_manifest = filter_manifest(manifest, global_config) +) -> Iterable[str]: + parsed_args.list_sessions = True + module = load_nox_module(parsed_args) + manifest = discover_manifest(module, parsed_args) + filtered_manifest = filter_manifest(manifest, parsed_args) if isinstance(filtered_manifest, int): return [] - return [ + return ( session.friendly_name for session, _ in filtered_manifest.list_all_sessions() - ] + ) + + +def _tag_completer( + prefix: str, parsed_args: argparse.Namespace, **kwargs: Any +) -> Iterable[str]: + module = load_nox_module(parsed_args) + manifest = discover_manifest(module, parsed_args) + return itertools.chain.from_iterable( + filter(None, (session.tags for session, _ in manifest.list_all_sessions())) + ) options.add_options( @@ -300,6 +327,7 @@ def _session_completer( nargs="*", default=default_env_var_list_factory("NOXPYTHON"), help="Only run sessions that use the given python interpreter versions.", + completer=_python_completer, ), _option_set.Option( "keywords", @@ -309,6 +337,7 @@ def _session_completer( noxfile=True, merge_func=functools.partial(_sessions_merge_func, "keywords"), help="Only run sessions that match the given expression.", + completer=argcomplete.completers.ChoicesCompleter(()), ), _option_set.Option( "tags", @@ -319,6 +348,7 @@ def _session_completer( merge_func=functools.partial(_sessions_merge_func, "tags"), nargs="*", help="Only run sessions with the given tags.", + completer=_tag_completer, ), _option_set.Option( "posargs", @@ -423,6 +453,7 @@ def _session_completer( merge_func=_envdir_merge_func, group=options.groups["environment"], help="Directory where Nox will store virtualenvs, this is ``.nox`` by default.", + completer=argcomplete.completers.DirectoriesCompleter(), ), _option_set.Option( "extra_pythons", @@ -432,6 +463,7 @@ def _session_completer( nargs="*", default=default_env_var_list_factory("NOXEXTRAPYTHON"), help="Additionally, run sessions using the given python interpreter versions.", + completer=_python_completer, ), _option_set.Option( "force_pythons", @@ -445,6 +477,7 @@ def _session_completer( " Noxfile. This is a shorthand for ``--python=X.Y --extra-python=X.Y``." ), finalizer_func=_force_pythons_finalizer, + completer=_python_completer, ), *_option_set.make_flag_pair( "stop_on_first_error", @@ -496,6 +529,7 @@ def _session_completer( group=options.groups["reporting"], noxfile=True, help="Output a report of all sessions to the given filename.", + completer=argcomplete.completers.FilesCompleter(("json",)), ), _option_set.Option( "non_interactive", diff --git a/tests/resources/noxfile_pythons.py b/tests/resources/noxfile_pythons.py index 63cb8d95..8dd3551a 100644 --- a/tests/resources/noxfile_pythons.py +++ b/tests/resources/noxfile_pythons.py @@ -7,3 +7,13 @@ @nox.parametrize("cheese", ["cheddar", "jack", "brie"]) def snack(unused_session, cheese): print(f"Noms, {cheese} so good!") + + +@nox.session(python=False) +def nopy(unused_session): + print("No pythons here.") + + +@nox.session(python="3.12") +def strpy(unused_session): + print("Python-in-a-str here.") diff --git a/tests/resources/noxfile_tags.py b/tests/resources/noxfile_tags.py new file mode 100644 index 00000000..bf43d993 --- /dev/null +++ b/tests/resources/noxfile_tags.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +import nox + + +@nox.session # no tags +def no_tags(unused_session): + print("Look ma, no tags!") + + +@nox.session(tags=["tag1"]) +def one_tag(unused_session): + print("Lonesome tag here.") + + +@nox.session(tags=["tag1", "tag2", "tag3"]) +def moar_tags(unused_session): + print("Some more tags here.") diff --git a/tests/test__option_set.py b/tests/test__option_set.py index 93a768a0..c474d12f 100644 --- a/tests/test__option_set.py +++ b/tests/test__option_set.py @@ -95,7 +95,7 @@ def test_session_completer(self): ) expected_sessions = ["testytest", "lintylint", "typeytype"] - assert expected_sessions == actual_sessions_from_file + assert expected_sessions == list(actual_sessions_from_file) def test_session_completer_invalid_sessions(self): parsed_args = _options.options.namespace( @@ -105,3 +105,27 @@ def test_session_completer_invalid_sessions(self): prefix=None, parsed_args=parsed_args ) assert len(all_nox_sessions) == 0 + + def test_python_completer(self): + parsed_args = _options.options.namespace( + posargs=[], + noxfile=str(RESOURCES.joinpath("noxfile_pythons.py")), + ) + actual_pythons_from_file = _options._python_completer( + prefix=None, parsed_args=parsed_args + ) + + expected_pythons = {"3.6", "3.12"} + assert expected_pythons == set(actual_pythons_from_file) + + def test_tag_completer(self): + parsed_args = _options.options.namespace( + posargs=[], + noxfile=str(RESOURCES.joinpath("noxfile_tags.py")), + ) + actual_tags_from_file = _options._tag_completer( + prefix=None, parsed_args=parsed_args + ) + + expected_tags = {"tag1", "tag2", "tag3"} + assert expected_tags == set(actual_tags_from_file)