Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow setting tags on parametrized sessions #832

Merged
merged 1 commit into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading