From 3e9584ccaf770d459fe4ced346ef9977d7fffdec Mon Sep 17 00:00:00 2001 From: Daniel Harding Date: Wed, 29 May 2024 09:35:26 +0300 Subject: [PATCH] feat: allow setting tags on parametrized sessions To allow more fine-grained session selection, allow tags to be set on individual parametrized sessions via either a tags argument to the @nox.parametrize() decorator, or a tags argument to nox.param() (similar to how parametrized session IDs can be specified). Any tags specified this way will be added to any tags passed to the @nox.session() decorator. --- nox/_decorators.py | 2 +- nox/_parametrize.py | 28 ++++++++++++++++++++--- tests/resources/noxfile_tags.py | 7 ++++++ tests/test__option_set.py | 2 +- tests/test__parametrize.py | 40 ++++++++++++++++++++++++++++++++- tests/test_tasks.py | 22 ++++++++++++------ 6 files changed, 88 insertions(+), 13 deletions(-) diff --git a/nox/_decorators.py b/nox/_decorators.py index 25f13de6..c21f1669 100644 --- a/nox/_decorators.py +++ b/nox/_decorators.py @@ -128,7 +128,7 @@ def __init__(self, func: Func, param_spec: Param) -> None: func.venv_backend, func.venv_params, func.should_warn, - func.tags, + func.tags + param_spec.tags, default=func.default, ) self.call_spec = call_spec diff --git a/nox/_parametrize.py b/nox/_parametrize.py index 865cb19b..134dda01 100644 --- a/nox/_parametrize.py +++ b/nox/_parametrize.py @@ -29,6 +29,8 @@ class Param: arg_names (Sequence[str]): The names of the args. id (str): An optional ID for this set of parameters. If unspecified, it will be generated from the parameters. + tags (Sequence[str]): Optional tags to associate with this set of + parameters. """ def __init__( @@ -36,6 +38,7 @@ def __init__( *args: Any, arg_names: Sequence[str] | None = None, id: str | None = None, + tags: Sequence[str] | None = None, ) -> None: self.args = args self.id = id @@ -45,6 +48,11 @@ def __init__( self.arg_names = tuple(arg_names) + if tags is None: + tags = [] + + self.tags = list(tags) + @property def call_spec(self) -> dict[str, Any]: return dict(zip(self.arg_names, self.args)) @@ -60,13 +68,16 @@ def __str__(self) -> str: __repr__ = __str__ def copy(self) -> Param: - new = self.__class__(*self.args, arg_names=self.arg_names, id=self.id) + new = self.__class__( + *self.args, arg_names=self.arg_names, id=self.id, tags=self.tags + ) return new def update(self, other: Param) -> None: self.id = ", ".join([str(self), str(other)]) self.args = self.args + other.args self.arg_names = self.arg_names + other.arg_names + self.tags = self.tags + other.tags def __eq__(self, other: object) -> bool: if isinstance(other, self.__class__): @@ -74,6 +85,7 @@ def __eq__(self, other: object) -> bool: self.args == other.args and self.arg_names == other.arg_names and self.id == other.id + and self.tags == other.tags ) elif isinstance(other, dict): return dict(zip(self.arg_names, self.args)) == other @@ -95,6 +107,7 @@ def parametrize_decorator( arg_names: str | Sequence[str], arg_values_list: Iterable[ArgValue] | ArgValue, ids: Iterable[str | None] | None = None, + tags: Iterable[Sequence[str] | None] | None = None, ) -> Callable[[Any], Any]: """Parametrize a session. @@ -114,6 +127,8 @@ def parametrize_decorator( argument name, for example ``[(1, 'a'), (2, 'b')]``. ids (Sequence[str]): Optional sequence of test IDs to use for the parametrized arguments. + tags (Iterable[Sequence[str] | None]): Optional iterable of tags to + associate with the parametrized arguments. """ # Allow args names to be specified as any of 'arg', 'arg,arg2' or ('arg', 'arg2') @@ -143,14 +158,21 @@ def parametrize_decorator( if not ids: ids = [] + if tags is None: + tags = [] + # Generate params for each item in the param_args_values list. param_specs: list[Param] = [] - for param_arg_values, param_id in itertools.zip_longest(_arg_values_list, ids): + for param_arg_values, param_id, param_tags in itertools.zip_longest( + _arg_values_list, ids, tags + ): if isinstance(param_arg_values, Param): param_spec = param_arg_values param_spec.arg_names = tuple(arg_names) else: - param_spec = Param(*param_arg_values, arg_names=arg_names, id=param_id) + param_spec = Param( + *param_arg_values, arg_names=arg_names, id=param_id, tags=param_tags + ) param_specs.append(param_spec) diff --git a/tests/resources/noxfile_tags.py b/tests/resources/noxfile_tags.py index bf43d993..07d392bf 100644 --- a/tests/resources/noxfile_tags.py +++ b/tests/resources/noxfile_tags.py @@ -16,3 +16,10 @@ def one_tag(unused_session): @nox.session(tags=["tag1", "tag2", "tag3"]) def moar_tags(unused_session): print("Some more tags here.") + + +@nox.session(tags=["tag4"]) +@nox.parametrize("foo", [nox.param(1, tags=["tag5", "tag6"])]) +@nox.parametrize("bar", [2, 3], tags=[["tag7"]]) +def parametrized_tags(unused_session): + print("Parametrized tags here.") diff --git a/tests/test__option_set.py b/tests/test__option_set.py index c474d12f..73f7affb 100644 --- a/tests/test__option_set.py +++ b/tests/test__option_set.py @@ -127,5 +127,5 @@ def test_tag_completer(self): prefix=None, parsed_args=parsed_args ) - expected_tags = {"tag1", "tag2", "tag3"} + expected_tags = {f"tag{n}" for n in range(1, 8)} assert expected_tags == set(actual_tags_from_file) diff --git a/tests/test__parametrize.py b/tests/test__parametrize.py index 41ea4236..a09c6e11 100644 --- a/tests/test__parametrize.py +++ b/tests/test__parametrize.py @@ -197,7 +197,7 @@ def test_generate_calls_simple(): def test_generate_calls_multiple_args(): - f = mock.Mock(should_warn=None, tags=None) + f = mock.Mock(should_warn=None, tags=[]) f.__name__ = "f" arg_names = ("foo", "abc") @@ -244,6 +244,44 @@ def test_generate_calls_ids(): f.assert_called_with(foo=2) +def test_generate_calls_tags(): + f = mock.Mock(should_warn={}, tags=[]) + f.__name__ = "f" + + arg_names = ("foo",) + call_specs = [ + _parametrize.Param(1, arg_names=arg_names, tags=["tag3"]), + _parametrize.Param(1, arg_names=arg_names), + _parametrize.Param(2, arg_names=arg_names, tags=["tag4", "tag5"]), + ] + + calls = _decorators.Call.generate_calls(f, call_specs) + + assert len(calls) == 3 + assert calls[0].tags == ["tag3"] + assert calls[1].tags == [] + assert calls[2].tags == ["tag4", "tag5"] + + +def test_generate_calls_merge_tags(): + f = mock.Mock(should_warn={}, tags=["tag1", "tag2"]) + f.__name__ = "f" + + arg_names = ("foo",) + call_specs = [ + _parametrize.Param(1, arg_names=arg_names, tags=["tag3"]), + _parametrize.Param(1, arg_names=arg_names), + _parametrize.Param(2, arg_names=arg_names, tags=["tag4", "tag5"]), + ] + + calls = _decorators.Call.generate_calls(f, call_specs) + + assert len(calls) == 3 + assert calls[0].tags == ["tag1", "tag2", "tag3"] + assert calls[1].tags == ["tag1", "tag2"] + assert calls[2].tags == ["tag1", "tag2", "tag4", "tag5"] + + def test_generate_calls_session_python(): called_with = [] diff --git a/tests/test_tasks.py b/tests/test_tasks.py index 122cc0e8..fdfad628 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -254,13 +254,14 @@ def test_filter_manifest_keywords_syntax_error(): @pytest.mark.parametrize( "tags,session_count", [ - (None, 4), - (["foo"], 3), - (["bar"], 3), - (["baz"], 1), - (["foo", "bar"], 4), - (["foo", "baz"], 3), - (["foo", "bar", "baz"], 4), + (None, 8), + (["foo"], 7), + (["bar"], 5), + (["baz"], 3), + (["foo", "bar"], 8), + (["foo", "baz"], 7), + (["bar", "baz"], 6), + (["foo", "bar", "baz"], 8), ], ) def test_filter_manifest_tags(tags, session_count): @@ -280,6 +281,12 @@ def quuz(): def corge(): pass + @nox.session(tags=["foo"]) + @nox.parametrize("a", [1, nox.param(2, tags=["bar"])]) + @nox.parametrize("b", [3, 4], tags=[["baz"]]) + def grault(): + pass + config = _options.options.namespace( sessions=None, pythons=(), posargs=[], tags=tags ) @@ -289,6 +296,7 @@ def corge(): "quux": quux, "quuz": quuz, "corge": corge, + "grault": grault, }, config, )