Skip to content

Commit

Permalink
feat: more option argument completions (#707)
Browse files Browse the repository at this point in the history
* 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

kislyuk/argcomplete#422

* style: simplify completer functions

Co-authored-by: Stanislav Filin <[email protected]>

* fix: return type hint for completer callables (`Iterable[str]`)

kislyuk/argcomplete#422

* chore: appease mypy in completers code

* test: add python and tag completer tests

---------

Co-authored-by: Stanislav Filin <[email protected]>
  • Loading branch information
scop and stasfilin authored Feb 20, 2024
1 parent d2b02ac commit ea9ea27
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 11 deletions.
4 changes: 2 additions & 2 deletions nox/_option_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
50 changes: 42 additions & 8 deletions nox/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions tests/resources/noxfile_pythons.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
18 changes: 18 additions & 0 deletions tests/resources/noxfile_tags.py
Original file line number Diff line number Diff line change
@@ -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.")
26 changes: 25 additions & 1 deletion tests/test__option_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)

0 comments on commit ea9ea27

Please sign in to comment.