Skip to content

Commit 1bd7f96

Browse files
feat: Add session tags (#627)
1 parent d1bdf09 commit 1bd7f96

12 files changed

Lines changed: 205 additions & 5 deletions

docs/config.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,7 @@ The following options can be specified in the Noxfile:
429429
* ``nox.options.sessions`` is equivalent to specifying :ref:`-s or --sessions <opt-sessions-pythons-and-keywords>`. If set to an empty list, no sessions will be run if no sessions were given on the command line, and the list of available sessions will be shown instead.
430430
* ``nox.options.pythons`` is equivalent to specifying :ref:`-p or --pythons <opt-sessions-pythons-and-keywords>`.
431431
* ``nox.options.keywords`` is equivalent to specifying :ref:`-k or --keywords <opt-sessions-pythons-and-keywords>`.
432+
* ``nox.options.tags`` is equivalent to specifying :ref:`-t or --tags <opt-sessions-pythons-and-keywords>`.
432433
* ``nox.options.default_venv_backend`` is equivalent to specifying :ref:`-db or --default-venv-backend <opt-default-venv-backend>`.
433434
* ``nox.options.force_venv_backend`` is equivalent to specifying :ref:`-fb or --force-venv-backend <opt-force-venv-backend>`.
434435
* ``nox.options.reuse_existing_virtualenvs`` is equivalent to specifying :ref:`--reuse-existing-virtualenvs <opt-reuse-existing-virtualenvs>`. You can force this off by specifying ``--no-reuse-existing-virtualenvs`` during invocation.

docs/tutorial.rst

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

450450

451+
Session tags
452+
------------
453+
454+
You can add tags to your sessions to help you organize your development tasks:
455+
456+
.. code-block:: python
457+
458+
@nox.session(tags=["style", "fix"])
459+
def black(session):
460+
session.install("black")
461+
session.run("black", "my_package")
462+
463+
@nox.session(tags=["style", "fix"])
464+
def isort(session):
465+
session.install("isort")
466+
session.run("isort", "my_package")
467+
468+
@nox.session(tags=["style"])
469+
def flake8(session):
470+
session.install("flake8")
471+
session.run("flake8", "my_package")
472+
473+
474+
If you run ``nox -t style``, Nox will run all three sessions:
475+
476+
.. code-block:: console
477+
478+
* black
479+
* isort
480+
* flake8
481+
482+
483+
If you run ``nox -t fix``, Nox will only run the ``black`` and ``isort``
484+
sessions:
485+
486+
.. code-block:: console
487+
488+
* black
489+
* isort
490+
- flake8
491+
492+
493+
If you run ``nox -t style fix``, Nox will all sessions that match *any* of
494+
the tags, so all three sessions:
495+
496+
.. code-block:: console
497+
498+
* black
499+
* isort
500+
* flake8
501+
502+
451503
Next steps
452504
----------
453505

docs/usage.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,15 @@ If you have a :ref:`configured session's virtualenv <virtualenv config>`, you ca
7474
nox --python 3.8
7575
nox -p 3.7 3.8
7676
77-
You can also use `pytest-style keywords`_ to filter test sessions:
77+
You can also use `pytest-style keywords`_ using ``-k`` or ``--keywords``, and
78+
tags using ``-t`` or ``--tags`` to filter test sessions:
7879

7980
.. code-block:: console
8081
8182
nox -k "not lint"
8283
nox -k "tests and not lint"
84+
nox -k "not my_tag"
85+
nox -t "my_tag" "my_other_tag"
8386
8487
.. _pytest-style keywords: https://docs.pytest.org/en/latest/usage.html#specifying-tests-selecting-tests
8588

nox/_decorators.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,15 @@ def __init__(
6161
venv_backend: Any = None,
6262
venv_params: Any = None,
6363
should_warn: dict[str, Any] | None = None,
64+
tags: list[str] | None = None,
6465
):
6566
self.func = func
6667
self.python = python
6768
self.reuse_venv = reuse_venv
6869
self.venv_backend = venv_backend
6970
self.venv_params = venv_params
7071
self.should_warn = should_warn or dict()
72+
self.tags = tags or []
7173

7274
def __call__(self, *args: Any, **kwargs: Any) -> Any:
7375
return self.func(*args, **kwargs)
@@ -81,6 +83,7 @@ def copy(self, name: str | None = None) -> Func:
8183
self.venv_backend,
8284
self.venv_params,
8385
self.should_warn,
86+
self.tags,
8487
)
8588

8689

@@ -109,6 +112,7 @@ def __init__(self, func: Func, param_spec: Param) -> None:
109112
func.venv_backend,
110113
func.venv_params,
111114
func.should_warn,
115+
func.tags,
112116
)
113117
self.call_spec = call_spec
114118
self.session_signature = session_signature

nox/_options.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,15 @@ def _session_completer(
285285
merge_func=functools.partial(_sessions_and_keywords_merge_func, "keywords"),
286286
help="Only run sessions that match the given expression.",
287287
),
288+
_option_set.Option(
289+
"tags",
290+
"-t",
291+
"--tags",
292+
group=options.groups["sessions"],
293+
noxfile=True,
294+
nargs="*",
295+
help="Only run sessions with the given tags.",
296+
),
288297
_option_set.Option(
289298
"posargs",
290299
"posargs",

nox/manifest.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,9 +172,20 @@ def filter_by_keywords(self, keywords: str) -> None:
172172
session names are checked against.
173173
"""
174174
self._queue = [
175-
x for x in self._queue if keyword_match(keywords, x.signatures + [x.name])
175+
x
176+
for x in self._queue
177+
if keyword_match(keywords, x.signatures + x.tags + [x.name])
176178
]
177179

180+
def filter_by_tags(self, tags: list[str]) -> None:
181+
"""Filter sessions by their tags.
182+
183+
Args:
184+
tags (list[str]): A list of tags which session names
185+
are checked against.
186+
"""
187+
self._queue = [x for x in self._queue if set(x.tags).intersection(tags)]
188+
178189
def make_session(
179190
self, name: str, func: Func, multi: bool = False
180191
) -> list[SessionRunner]:

nox/registry.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def session_decorator(
4141
name: str | None = ...,
4242
venv_backend: Any = ...,
4343
venv_params: Any = ...,
44+
tags: list[str] | None = ...,
4445
) -> Callable[[F], F]:
4546
...
4647

@@ -53,6 +54,7 @@ def session_decorator(
5354
name: str | None = None,
5455
venv_backend: Any = None,
5556
venv_params: Any = None,
57+
tags: list[str] | None = None,
5658
) -> F | Callable[[F], F]:
5759
"""Designate the decorated function as a session."""
5860
# If `func` is provided, then this is the decorator call with the function
@@ -71,6 +73,7 @@ def session_decorator(
7173
name=name,
7274
venv_backend=venv_backend,
7375
venv_params=venv_params,
76+
tags=tags,
7477
)
7578

7679
if py is not None and python is not None:
@@ -82,7 +85,7 @@ def session_decorator(
8285
if python is None:
8386
python = py
8487

85-
fn = Func(func, python, reuse_venv, name, venv_backend, venv_params)
88+
fn = Func(func, python, reuse_venv, name, venv_backend, venv_params, tags=tags)
8689
_REGISTRY[name or func.__name__] = fn
8790
return fn
8891

nox/sessions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,10 @@ def __str__(self) -> str:
657657
def friendly_name(self) -> str:
658658
return self.signatures[0] if self.signatures else self.name
659659

660+
@property
661+
def tags(self) -> list[str]:
662+
return self.func.tags
663+
660664
@property
661665
def envdir(self) -> str:
662666
return _normalize_path(self.global_config.envdir, self.friendly_name)

nox/tasks.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,13 @@ def filter_manifest(manifest: Manifest, global_config: Namespace) -> Manifest |
191191
logger.error("Python version selection caused no sessions to be selected.")
192192
return 3
193193

194+
# Filter by tags.
195+
if global_config.tags is not None:
196+
manifest.filter_by_tags(global_config.tags)
197+
if not manifest and not global_config.list_sessions:
198+
logger.error("Tag selection caused no sessions to be selected.")
199+
return 3
200+
194201
# Filter by keywords.
195202
if global_config.keywords:
196203
try:

tests/test_manifest.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,13 @@
3333

3434
def create_mock_sessions():
3535
sessions = collections.OrderedDict()
36-
sessions["foo"] = mock.Mock(spec=(), python=None, venv_backend=None)
37-
sessions["bar"] = mock.Mock(spec=(), python=None, venv_backend=None)
36+
sessions["foo"] = mock.Mock(spec=(), python=None, venv_backend=None, tags=["baz"])
37+
sessions["bar"] = mock.Mock(
38+
spec=(),
39+
python=None,
40+
venv_backend=None,
41+
tags=["baz", "qux"],
42+
)
3843
return sessions
3944

4045

@@ -190,6 +195,27 @@ def test_filter_by_keyword():
190195
assert len(manifest) == 2
191196
manifest.filter_by_keywords("foo")
192197
assert len(manifest) == 1
198+
# Match tags
199+
manifest.filter_by_keywords("not baz")
200+
assert len(manifest) == 0
201+
202+
203+
@pytest.mark.parametrize(
204+
"tags,session_count",
205+
[
206+
(["baz", "qux"], 2),
207+
(["baz"], 2),
208+
(["qux"], 1),
209+
(["missing"], 0),
210+
(["baz", "missing"], 2),
211+
],
212+
)
213+
def test_filter_by_tags(tags: list[str], session_count: int):
214+
sessions = create_mock_sessions()
215+
manifest = Manifest(sessions, create_mock_config())
216+
assert len(manifest) == 2
217+
manifest.filter_by_tags(tags)
218+
assert len(manifest) == session_count
193219

194220

195221
def test_list_all_sessions_with_filter():

0 commit comments

Comments
 (0)