Skip to content

Commit

Permalink
feat: Add session tags (#627)
Browse files Browse the repository at this point in the history
  • Loading branch information
edgarrmondragon authored Jun 21, 2022
1 parent d1bdf09 commit 1bd7f96
Show file tree
Hide file tree
Showing 12 changed files with 205 additions and 5 deletions.
1 change: 1 addition & 0 deletions docs/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ The following options can be specified in the Noxfile:
* ``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.
* ``nox.options.pythons`` is equivalent to specifying :ref:`-p or --pythons <opt-sessions-pythons-and-keywords>`.
* ``nox.options.keywords`` is equivalent to specifying :ref:`-k or --keywords <opt-sessions-pythons-and-keywords>`.
* ``nox.options.tags`` is equivalent to specifying :ref:`-t or --tags <opt-sessions-pythons-and-keywords>`.
* ``nox.options.default_venv_backend`` is equivalent to specifying :ref:`-db or --default-venv-backend <opt-default-venv-backend>`.
* ``nox.options.force_venv_backend`` is equivalent to specifying :ref:`-fb or --force-venv-backend <opt-force-venv-backend>`.
* ``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.
Expand Down
52 changes: 52 additions & 0 deletions docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,58 @@ 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
------------

You can add tags to your sessions to help you organize your development tasks:

.. code-block:: python
@nox.session(tags=["style", "fix"])
def black(session):
session.install("black")
session.run("black", "my_package")
@nox.session(tags=["style", "fix"])
def isort(session):
session.install("isort")
session.run("isort", "my_package")
@nox.session(tags=["style"])
def flake8(session):
session.install("flake8")
session.run("flake8", "my_package")
If you run ``nox -t style``, Nox will run all three sessions:

.. code-block:: console
* black
* isort
* flake8
If you run ``nox -t fix``, Nox will only run the ``black`` and ``isort``
sessions:

.. code-block:: console
* black
* isort
- flake8
If you run ``nox -t style fix``, Nox will all sessions that match *any* of
the tags, so all three sessions:

.. code-block:: console
* black
* isort
* flake8
Next steps
----------

Expand Down
5 changes: 4 additions & 1 deletion docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,15 @@ If you have a :ref:`configured session's virtualenv <virtualenv config>`, you ca
nox --python 3.8
nox -p 3.7 3.8
You can also use `pytest-style keywords`_ to filter test sessions:
You can also use `pytest-style keywords`_ using ``-k`` or ``--keywords``, and
tags using ``-t`` or ``--tags`` to filter test sessions:

.. code-block:: console
nox -k "not lint"
nox -k "tests and not lint"
nox -k "not my_tag"
nox -t "my_tag" "my_other_tag"
.. _pytest-style keywords: https://docs.pytest.org/en/latest/usage.html#specifying-tests-selecting-tests

Expand Down
4 changes: 4 additions & 0 deletions nox/_decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,15 @@ def __init__(
venv_backend: Any = None,
venv_params: Any = None,
should_warn: dict[str, Any] | None = None,
tags: list[str] | None = None,
):
self.func = func
self.python = python
self.reuse_venv = reuse_venv
self.venv_backend = venv_backend
self.venv_params = venv_params
self.should_warn = should_warn or dict()
self.tags = tags or []

def __call__(self, *args: Any, **kwargs: Any) -> Any:
return self.func(*args, **kwargs)
Expand All @@ -81,6 +83,7 @@ def copy(self, name: str | None = None) -> Func:
self.venv_backend,
self.venv_params,
self.should_warn,
self.tags,
)


Expand Down Expand Up @@ -109,6 +112,7 @@ def __init__(self, func: Func, param_spec: Param) -> None:
func.venv_backend,
func.venv_params,
func.should_warn,
func.tags,
)
self.call_spec = call_spec
self.session_signature = session_signature
Expand Down
9 changes: 9 additions & 0 deletions nox/_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,15 @@ def _session_completer(
merge_func=functools.partial(_sessions_and_keywords_merge_func, "keywords"),
help="Only run sessions that match the given expression.",
),
_option_set.Option(
"tags",
"-t",
"--tags",
group=options.groups["sessions"],
noxfile=True,
nargs="*",
help="Only run sessions with the given tags.",
),
_option_set.Option(
"posargs",
"posargs",
Expand Down
13 changes: 12 additions & 1 deletion nox/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,20 @@ def filter_by_keywords(self, keywords: str) -> None:
session names are checked against.
"""
self._queue = [
x for x in self._queue if keyword_match(keywords, x.signatures + [x.name])
x
for x in self._queue
if keyword_match(keywords, x.signatures + x.tags + [x.name])
]

def filter_by_tags(self, tags: list[str]) -> None:
"""Filter sessions by their tags.
Args:
tags (list[str]): A list of tags which session names
are checked against.
"""
self._queue = [x for x in self._queue if set(x.tags).intersection(tags)]

def make_session(
self, name: str, func: Func, multi: bool = False
) -> list[SessionRunner]:
Expand Down
5 changes: 4 additions & 1 deletion nox/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def session_decorator(
name: str | None = ...,
venv_backend: Any = ...,
venv_params: Any = ...,
tags: list[str] | None = ...,
) -> Callable[[F], F]:
...

Expand All @@ -53,6 +54,7 @@ def session_decorator(
name: str | None = None,
venv_backend: Any = None,
venv_params: Any = None,
tags: list[str] | None = None,
) -> F | Callable[[F], F]:
"""Designate the decorated function as a session."""
# If `func` is provided, then this is the decorator call with the function
Expand All @@ -71,6 +73,7 @@ def session_decorator(
name=name,
venv_backend=venv_backend,
venv_params=venv_params,
tags=tags,
)

if py is not None and python is not None:
Expand All @@ -82,7 +85,7 @@ def session_decorator(
if python is None:
python = py

fn = Func(func, python, reuse_venv, name, venv_backend, venv_params)
fn = Func(func, python, reuse_venv, name, venv_backend, venv_params, tags=tags)
_REGISTRY[name or func.__name__] = fn
return fn

Expand Down
4 changes: 4 additions & 0 deletions nox/sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,10 @@ def __str__(self) -> str:
def friendly_name(self) -> str:
return self.signatures[0] if self.signatures else self.name

@property
def tags(self) -> list[str]:
return self.func.tags

@property
def envdir(self) -> str:
return _normalize_path(self.global_config.envdir, self.friendly_name)
Expand Down
7 changes: 7 additions & 0 deletions nox/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,13 @@ def filter_manifest(manifest: Manifest, global_config: Namespace) -> Manifest |
logger.error("Python version selection caused no sessions to be selected.")
return 3

# Filter by tags.
if global_config.tags is not None:
manifest.filter_by_tags(global_config.tags)
if not manifest and not global_config.list_sessions:
logger.error("Tag selection caused no sessions to be selected.")
return 3

# Filter by keywords.
if global_config.keywords:
try:
Expand Down
30 changes: 28 additions & 2 deletions tests/test_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,13 @@

def create_mock_sessions():
sessions = collections.OrderedDict()
sessions["foo"] = mock.Mock(spec=(), python=None, venv_backend=None)
sessions["bar"] = mock.Mock(spec=(), python=None, venv_backend=None)
sessions["foo"] = mock.Mock(spec=(), python=None, venv_backend=None, tags=["baz"])
sessions["bar"] = mock.Mock(
spec=(),
python=None,
venv_backend=None,
tags=["baz", "qux"],
)
return sessions


Expand Down Expand Up @@ -190,6 +195,27 @@ def test_filter_by_keyword():
assert len(manifest) == 2
manifest.filter_by_keywords("foo")
assert len(manifest) == 1
# Match tags
manifest.filter_by_keywords("not baz")
assert len(manifest) == 0


@pytest.mark.parametrize(
"tags,session_count",
[
(["baz", "qux"], 2),
(["baz"], 2),
(["qux"], 1),
(["missing"], 0),
(["baz", "missing"], 2),
],
)
def test_filter_by_tags(tags: list[str], session_count: int):
sessions = create_mock_sessions()
manifest = Manifest(sessions, create_mock_config())
assert len(manifest) == 2
manifest.filter_by_tags(tags)
assert len(manifest) == session_count


def test_list_all_sessions_with_filter():
Expand Down
8 changes: 8 additions & 0 deletions tests/test_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,14 @@ def unit_tests(session):
assert unit_tests.python == ["3.5", "3.6"]


def test_session_decorator_tags(cleanup_registry):
@registry.session_decorator(tags=["tag-1", "tag-2"])
def unit_tests(session):
pass

assert unit_tests.tags == ["tag-1", "tag-2"]


def test_session_decorator_py_alias(cleanup_registry):
@registry.session_decorator(py=["3.5", "3.6"])
def unit_tests(session):
Expand Down
72 changes: 72 additions & 0 deletions tests/test_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ def session_func():
session_func.python = None
session_func.venv_backend = None
session_func.should_warn = dict()
session_func.tags = []


def session_func_with_python():
Expand Down Expand Up @@ -243,6 +244,77 @@ def test_filter_manifest_keywords_syntax_error():
assert return_value == 3


@pytest.mark.parametrize(
"tags,session_count",
[
(None, 4),
(["foo"], 3),
(["bar"], 3),
(["baz"], 1),
(["foo", "bar"], 4),
(["foo", "baz"], 3),
(["foo", "bar", "baz"], 4),
],
)
def test_filter_manifest_tags(tags, session_count):
@nox.session(tags=["foo"])
def qux():
pass

@nox.session(tags=["bar"])
def quux():
pass

@nox.session(tags=["foo", "bar"])
def quuz():
pass

@nox.session(tags=["foo", "bar", "baz"])
def corge():
pass

config = _options.options.namespace(
sessions=None, pythons=(), posargs=[], tags=tags
)
manifest = Manifest(
{
"qux": qux,
"quux": quux,
"quuz": quuz,
"corge": corge,
},
config,
)
return_value = tasks.filter_manifest(manifest, config)
assert return_value is manifest
assert len(manifest) == session_count


@pytest.mark.parametrize(
"tags",
[
["Foo"],
["not-found"],
],
ids=[
"tags-are-case-insensitive",
"tag-does-not-exist",
],
)
def test_filter_manifest_tags_not_found(tags, caplog):
@nox.session(tags=["foo"])
def quux():
pass

config = _options.options.namespace(
sessions=None, pythons=(), posargs=[], tags=tags
)
manifest = Manifest({"quux": quux}, config)
return_value = tasks.filter_manifest(manifest, config)
assert return_value == 3
assert "Tag selection caused no sessions to be selected." in caplog.text


def test_honor_list_request_noop():
config = _options.options.namespace(list_sessions=False)
manifest = {"thing": mock.sentinel.THING}
Expand Down

0 comments on commit 1bd7f96

Please sign in to comment.