Browse Source

feat: Add session tags (#627)

pull/632/head
Edgar R. M 2 months ago committed by GitHub
parent
commit
1bd7f965ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      docs/config.rst
  2. 52
      docs/tutorial.rst
  3. 5
      docs/usage.rst
  4. 4
      nox/_decorators.py
  5. 9
      nox/_options.py
  6. 13
      nox/manifest.py
  7. 5
      nox/registry.py
  8. 4
      nox/sessions.py
  9. 7
      nox/tasks.py
  10. 30
      tests/test_manifest.py
  11. 8
      tests/test_registry.py
  12. 72
      tests/test_tasks.py

1
docs/config.rst

@ -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.

52
docs/tutorial.rst

@ -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
----------

5
docs/usage.rst

@ -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

4
nox/_decorators.py

@ -61,6 +61,7 @@ class Func(FunctionDecorator):
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
@ -68,6 +69,7 @@ class Func(FunctionDecorator):
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)
@ -81,6 +83,7 @@ class Func(FunctionDecorator):
self.venv_backend,
self.venv_params,
self.should_warn,
self.tags,
)
@ -109,6 +112,7 @@ class Call(Func):
func.venv_backend,
func.venv_params,
func.should_warn,
func.tags,
)
self.call_spec = call_spec
self.session_signature = session_signature

9
nox/_options.py

@ -285,6 +285,15 @@ options.add_options(
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",

13
nox/manifest.py

@ -172,9 +172,20 @@ class Manifest:
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]:

5
nox/registry.py

@ -41,6 +41,7 @@ def session_decorator(
name: str | None = ...,
venv_backend: Any = ...,
venv_params: Any = ...,
tags: list[str] | None = ...,
) -> Callable[[F], F]:
...
@ -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
@ -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:
@ -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

4
nox/sessions.py

@ -657,6 +657,10 @@ class SessionRunner:
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)

7
nox/tasks.py

@ -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:

30
tests/test_manifest.py

@ -33,8 +33,13 @@ from nox.manifest import (
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
@ -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():

8
tests/test_registry.py

@ -60,6 +60,14 @@ def test_session_decorator_list_of_pythons(cleanup_registry):
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):

72
tests/test_tasks.py

@ -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():
@ -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}

Loading…
Cancel
Save