From ea9ea27bcf60e38fee8a6298ab39d819cd1945b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 20 Feb 2024 17:19:01 +0200 Subject: [PATCH] feat: more option argument completions (#707) * feat: add envdir and report completions * feat: add python completions * feat: add tag completion * feat: add keywords non-completion This will actually only prevent completions with bash if the completion was registered using `register-python-argcomplete --complete-arguments -- nox` (i.e. with _all_ fallback completions disabled, not just the readline ones). But it does not hurt if registered without doing so. * refactor: return Iterables from completers https://github.com/kislyuk/argcomplete/pull/422 * style: simplify completer functions Co-authored-by: Stanislav Filin * fix: return type hint for completer callables (`Iterable[str]`) https://github.com/kislyuk/argcomplete/pull/422 * chore: appease mypy in completers code * test: add python and tag completer tests --------- Co-authored-by: Stanislav Filin --- nox/_option_set.py | 4 +-- nox/_options.py | 50 +++++++++++++++++++++++++----- tests/resources/noxfile_pythons.py | 10 ++++++ tests/resources/noxfile_tags.py | 18 +++++++++++ tests/test__option_set.py | 26 +++++++++++++++- 5 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 tests/resources/noxfile_tags.py diff --git a/nox/_option_set.py b/nox/_option_set.py index 0ca3793d..e85c5746 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 @@ -90,7 +90,7 @@ def __init__( bool | str | None | 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 d2d335a8..39ebd4c5 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 @@ -227,19 +231,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( @@ -298,6 +325,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", @@ -307,6 +335,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", @@ -317,6 +346,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", @@ -421,6 +451,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", @@ -430,6 +461,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( " It will also work on sessions that don't have any interpreter parametrized." ), 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)