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: Add session tags #627

Merged
merged 7 commits into from
Jun 21, 2022
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
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 @@ -432,6 +432,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"
FollowTheProcess marked this conversation as resolved.
Show resolved Hide resolved

.. _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",
FollowTheProcess marked this conversation as resolved.
Show resolved Hide resolved
[
(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