Skip to content

Commit f2be46a

Browse files
authored
feat: allow setting tags on parametrized sessions (#832)
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.
1 parent 7e1f953 commit f2be46a

File tree

8 files changed

+147
-13
lines changed

8 files changed

+147
-13
lines changed

docs/config.rst

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,63 @@ Pythons:
397397
...
398398
399399
400+
Assigning tags to parametrized sessions
401+
---------------------------------------
402+
403+
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:
404+
405+
.. code-block:: python
406+
407+
@nox.session
408+
@nox.parametrize('dependency',
409+
['1.0', '2.0'],
410+
tags=[['old'], ['new']])
411+
@nox.parametrize('database'
412+
['postgres', 'mysql'],
413+
tags=[['psql'], ['mysql']])
414+
def tests(session, dependency, database):
415+
...
416+
417+
.. code-block:: python
418+
419+
@nox.session
420+
@nox.parametrize('dependency', [
421+
nox.param('1.0', tags=['old']),
422+
nox.param('2.0', tags=['new']),
423+
])
424+
@nox.parametrize('database', [
425+
nox.param('postgres', tags=['psql']),
426+
nox.param('mysql', tags=['mysql']),
427+
])
428+
def tests(session, dependency, database):
429+
...
430+
431+
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.
432+
433+
More sophisticated tag assignment can be performed by passing a generator to the ``@nox.parametrize`` decorator, as seen in the following example:
434+
435+
.. code-block:: python
436+
437+
def generate_params():
438+
for dependency in ["1.0", "1.1", "2.0"]:
439+
for database in ["sqlite", "postgresql", "mysql"]:
440+
tags = []
441+
if dependency == "2.0" and database == "sqlite":
442+
tags.append("quick")
443+
if dependency == "2.0" or database == "sqlite":
444+
tags.append("standard")
445+
yield nox.param((dependency, database), tags)
446+
447+
@nox.session
448+
@nox.parametrize(
449+
["dependency", "database"], generate_params(),
450+
)
451+
def tests(session, dependency, database):
452+
...
453+
454+
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.
455+
456+
400457
The session object
401458
------------------
402459

docs/tutorial.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,8 @@ read more about parametrization and see more examples over at
496496
.. _pytest's parametrize: https://pytest.org/latest/parametrize.html#_pytest.python.Metafunc.parametrize
497497

498498

499+
.. _session tags:
500+
499501
Session tags
500502
------------
501503

nox/_decorators.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ def __init__(self, func: Func, param_spec: Param) -> None:
128128
func.venv_backend,
129129
func.venv_params,
130130
func.should_warn,
131-
func.tags,
131+
func.tags + param_spec.tags,
132132
default=func.default,
133133
)
134134
self.call_spec = call_spec

nox/_parametrize.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,16 @@ class Param:
2929
arg_names (Sequence[str]): The names of the args.
3030
id (str): An optional ID for this set of parameters. If unspecified,
3131
it will be generated from the parameters.
32+
tags (Sequence[str]): Optional tags to associate with this set of
33+
parameters.
3234
"""
3335

3436
def __init__(
3537
self,
3638
*args: Any,
3739
arg_names: Sequence[str] | None = None,
3840
id: str | None = None,
41+
tags: Sequence[str] | None = None,
3942
) -> None:
4043
self.args = args
4144
self.id = id
@@ -45,6 +48,11 @@ def __init__(
4548

4649
self.arg_names = tuple(arg_names)
4750

51+
if tags is None:
52+
tags = []
53+
54+
self.tags = list(tags)
55+
4856
@property
4957
def call_spec(self) -> dict[str, Any]:
5058
return dict(zip(self.arg_names, self.args))
@@ -60,20 +68,24 @@ def __str__(self) -> str:
6068
__repr__ = __str__
6169

6270
def copy(self) -> Param:
63-
new = self.__class__(*self.args, arg_names=self.arg_names, id=self.id)
71+
new = self.__class__(
72+
*self.args, arg_names=self.arg_names, id=self.id, tags=self.tags
73+
)
6474
return new
6575

6676
def update(self, other: Param) -> None:
6777
self.id = ", ".join([str(self), str(other)])
6878
self.args = self.args + other.args
6979
self.arg_names = self.arg_names + other.arg_names
80+
self.tags = self.tags + other.tags
7081

7182
def __eq__(self, other: object) -> bool:
7283
if isinstance(other, self.__class__):
7384
return (
7485
self.args == other.args
7586
and self.arg_names == other.arg_names
7687
and self.id == other.id
88+
and self.tags == other.tags
7789
)
7890
elif isinstance(other, dict):
7991
return dict(zip(self.arg_names, self.args)) == other
@@ -95,6 +107,7 @@ def parametrize_decorator(
95107
arg_names: str | Sequence[str],
96108
arg_values_list: Iterable[ArgValue] | ArgValue,
97109
ids: Iterable[str | None] | None = None,
110+
tags: Iterable[Sequence[str]] | None = None,
98111
) -> Callable[[Any], Any]:
99112
"""Parametrize a session.
100113
@@ -114,6 +127,8 @@ def parametrize_decorator(
114127
argument name, for example ``[(1, 'a'), (2, 'b')]``.
115128
ids (Sequence[str]): Optional sequence of test IDs to use for the
116129
parametrized arguments.
130+
tags (Iterable[Sequence[str]]): Optional iterable of tags to associate
131+
with the parametrized arguments.
117132
"""
118133

119134
# Allow args names to be specified as any of 'arg', 'arg,arg2' or ('arg', 'arg2')
@@ -143,14 +158,21 @@ def parametrize_decorator(
143158
if not ids:
144159
ids = []
145160

161+
if tags is None:
162+
tags = []
163+
146164
# Generate params for each item in the param_args_values list.
147165
param_specs: list[Param] = []
148-
for param_arg_values, param_id in itertools.zip_longest(_arg_values_list, ids):
166+
for param_arg_values, param_id, param_tags in itertools.zip_longest(
167+
_arg_values_list, ids, tags
168+
):
149169
if isinstance(param_arg_values, Param):
150170
param_spec = param_arg_values
151171
param_spec.arg_names = tuple(arg_names)
152172
else:
153-
param_spec = Param(*param_arg_values, arg_names=arg_names, id=param_id)
173+
param_spec = Param(
174+
*param_arg_values, arg_names=arg_names, id=param_id, tags=param_tags
175+
)
154176

155177
param_specs.append(param_spec)
156178

tests/resources/noxfile_tags.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,10 @@ def one_tag(unused_session):
1616
@nox.session(tags=["tag1", "tag2", "tag3"])
1717
def more_tags(unused_session):
1818
print("Some more tags here.")
19+
20+
21+
@nox.session(tags=["tag4"])
22+
@nox.parametrize("foo", [nox.param(1, tags=["tag5", "tag6"])])
23+
@nox.parametrize("bar", [2, 3], tags=[["tag7"]])
24+
def parametrized_tags(unused_session):
25+
print("Parametrized tags here.")

tests/test__option_set.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,5 +127,5 @@ def test_tag_completer(self):
127127
prefix=None, parsed_args=parsed_args
128128
)
129129

130-
expected_tags = {"tag1", "tag2", "tag3"}
130+
expected_tags = {f"tag{n}" for n in range(1, 8)}
131131
assert expected_tags == set(actual_tags_from_file)

tests/test__parametrize.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def test_generate_calls_simple():
197197

198198

199199
def test_generate_calls_multiple_args():
200-
f = mock.Mock(should_warn=None, tags=None)
200+
f = mock.Mock(should_warn=None, tags=[])
201201
f.__name__ = "f"
202202

203203
arg_names = ("foo", "abc")
@@ -244,6 +244,44 @@ def test_generate_calls_ids():
244244
f.assert_called_with(foo=2)
245245

246246

247+
def test_generate_calls_tags():
248+
f = mock.Mock(should_warn={}, tags=[])
249+
f.__name__ = "f"
250+
251+
arg_names = ("foo",)
252+
call_specs = [
253+
_parametrize.Param(1, arg_names=arg_names, tags=["tag3"]),
254+
_parametrize.Param(1, arg_names=arg_names),
255+
_parametrize.Param(2, arg_names=arg_names, tags=["tag4", "tag5"]),
256+
]
257+
258+
calls = _decorators.Call.generate_calls(f, call_specs)
259+
260+
assert len(calls) == 3
261+
assert calls[0].tags == ["tag3"]
262+
assert calls[1].tags == []
263+
assert calls[2].tags == ["tag4", "tag5"]
264+
265+
266+
def test_generate_calls_merge_tags():
267+
f = mock.Mock(should_warn={}, tags=["tag1", "tag2"])
268+
f.__name__ = "f"
269+
270+
arg_names = ("foo",)
271+
call_specs = [
272+
_parametrize.Param(1, arg_names=arg_names, tags=["tag3"]),
273+
_parametrize.Param(1, arg_names=arg_names),
274+
_parametrize.Param(2, arg_names=arg_names, tags=["tag4", "tag5"]),
275+
]
276+
277+
calls = _decorators.Call.generate_calls(f, call_specs)
278+
279+
assert len(calls) == 3
280+
assert calls[0].tags == ["tag1", "tag2", "tag3"]
281+
assert calls[1].tags == ["tag1", "tag2"]
282+
assert calls[2].tags == ["tag1", "tag2", "tag4", "tag5"]
283+
284+
247285
def test_generate_calls_session_python():
248286
called_with = []
249287

tests/test_tasks.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -254,13 +254,14 @@ def test_filter_manifest_keywords_syntax_error():
254254
@pytest.mark.parametrize(
255255
"tags,session_count",
256256
[
257-
(None, 4),
258-
(["foo"], 3),
259-
(["bar"], 3),
260-
(["baz"], 1),
261-
(["foo", "bar"], 4),
262-
(["foo", "baz"], 3),
263-
(["foo", "bar", "baz"], 4),
257+
(None, 8),
258+
(["foo"], 7),
259+
(["bar"], 5),
260+
(["baz"], 3),
261+
(["foo", "bar"], 8),
262+
(["foo", "baz"], 7),
263+
(["bar", "baz"], 6),
264+
(["foo", "bar", "baz"], 8),
264265
],
265266
)
266267
def test_filter_manifest_tags(tags, session_count):
@@ -280,6 +281,12 @@ def quuz():
280281
def corge():
281282
pass
282283

284+
@nox.session(tags=["foo"])
285+
@nox.parametrize("a", [1, nox.param(2, tags=["bar"])])
286+
@nox.parametrize("b", [3, 4], tags=[["baz"]])
287+
def grault():
288+
pass
289+
283290
config = _options.options.namespace(
284291
sessions=None, pythons=(), posargs=[], tags=tags
285292
)
@@ -289,6 +296,7 @@ def corge():
289296
"quux": quux,
290297
"quuz": quuz,
291298
"corge": corge,
299+
"grault": grault,
292300
},
293301
config,
294302
)

0 commit comments

Comments
 (0)