Skip to content

Commit

Permalink
feat: allow setting tags on parametrized sessions
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
living180 committed Sep 23, 2024
1 parent 7e1f953 commit 3c82301
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 13 deletions.
57 changes: 57 additions & 0 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,63 @@ Pythons:
...
Assigning tags to parametrized sessions
---------------------------------------

Just as tags can be :ref:`assigned to normal sessions <session tags>`, they can also be assigned to parametrized sessions. The following examples are both equivalent:

.. code-block:: python
@nox.session
@nox.parametrize('dependency',
['1.0', '2.0'],
tags=[['old'], ['new']])
@nox.parametrize('database'
['postgres', 'mysql'],
tags=[['psql'], ['mysql']])
def tests(session, dependency, database):
...
.. code-block:: python
@nox.session
@nox.parametrize('dependency', [
nox.param('1.0', tags=['old']),
nox.param('2.0', tags=['new']),
])
@nox.parametrize('database', [
nox.param('postgres', tags=['psql']),
nox.param('mysql', tags=['mysql']),
])
def tests(session, dependency, database):
...
In either case, running ``nox --tags old`` will run the tests using version 1.0 of the dependency against both database backends, while running ``nox --tags psql`` will run the tests using both versions of the dependency, but only against PostgreSQL.

More sophisticated tag assignment can be performed by passing a generator to the ``@nox.parametrize`` decorator, as seen in the following example:

.. code-block:: python
def generate_params():
for dependency in ["1.0", "1.1", "2.0"]:
for database in ["sqlite", "postgresql", "mysql"]:
tags = []
if dependency == "2.0" and database == "sqlite":
tags.append("quick")
if dependency == "2.0" or database == "sqlite":
tags.append("standard")
yield nox.param((dependency, database), tags)
@nox.session
@nox.parametrize(
["dependency", "database"], generate_params(),
)
def tests(session, dependency, database):
...
In this example, the ``quick`` tag is assigned to the single combination of the latest version of the dependency along with the SQLite database backend, allowing a developer to run the tests in a single configuration as a basic sanity test. The ``standard`` tag, in contrast, selects combinations targeting either the latest version of the dependency *or* the SQLite database backend. If the developer runs ``tox --tags standard``, the tests will be run against all supported versions of the dependency with the SQLite backend, as well as against all supported database backends under the latest version of the dependency, giving much more comprehensive test coverage while using only five of the potential nine test matrix combinations.


The session object
------------------

Expand Down
2 changes: 2 additions & 0 deletions docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,8 @@ read more about parametrization and see more examples over at
.. _pytest's parametrize: https://pytest.org/latest/parametrize.html#_pytest.python.Metafunc.parametrize


.. _session tags:

Session tags
------------

Expand Down
2 changes: 1 addition & 1 deletion nox/_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 25 additions & 3 deletions nox/_parametrize.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,16 @@ 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__(
self,
*args: Any,
arg_names: Sequence[str] | None = None,
id: str | None = None,
tags: Sequence[str] | None = None,
) -> None:
self.args = args
self.id = id
Expand All @@ -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))
Expand All @@ -60,20 +68,24 @@ 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__):
return (
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
Expand All @@ -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,
) -> Callable[[Any], Any]:
"""Parametrize a session.
Expand All @@ -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]]): 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')
Expand Down Expand Up @@ -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)

Expand Down
7 changes: 7 additions & 0 deletions tests/resources/noxfile_tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,10 @@ def one_tag(unused_session):
@nox.session(tags=["tag1", "tag2", "tag3"])
def more_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.")
2 changes: 1 addition & 1 deletion tests/test__option_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
40 changes: 39 additions & 1 deletion tests/test__parametrize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 = []

Expand Down
22 changes: 15 additions & 7 deletions tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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
)
Expand All @@ -289,6 +296,7 @@ def corge():
"quux": quux,
"quuz": quuz,
"corge": corge,
"grault": grault,
},
config,
)
Expand Down

0 comments on commit 3c82301

Please sign in to comment.