1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224
|
"""Provides the tox_parameters() utility, which generates parameterized
sections for nox tests, which include tags that indicate various combinations
of those parameters in such a way that it's somewhat similar to how
we were using the tox project; where individual dash-separated tags could
be added to add more specificity to the suite configuation, or omitting them
would fall back to defaults.
"""
from __future__ import annotations
import collections
import re
import sys
from typing import Any
from typing import Callable
from typing import Sequence
import nox
OUR_PYTHON = f"{sys.version_info.major}.{sys.version_info.minor}"
def tox_parameters(
names: Sequence[str],
token_lists: Sequence[Sequence[str]],
*,
base_tag: str | None = None,
filter_: Callable[..., bool] | None = None,
always_include_in_tag: Sequence[str] | None = None,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
r"""Decorator to create a parameter/tagging structure for a nox session
function that acts to a large degree like tox's generative environments.
The output is a ``nox.parametrize()`` decorator that's built up from
individual ``nox.param()`` instances.
:param names: names of the parameters sent to the session function.
These names go straight to the first argument of ``nox.parametrize()``
and should all match argument names accepted by the decorated function
(except for ``python``, which is optional).
:param token_lists: a sequence of lists of values for each parameter. a
``nox.param()`` will be created for the full product of these values,
minus those filtered out using the ``filter_`` callable. These tokens
are used to create the args, tags, and ids of each ``nox.param()``. The
list of tags will be generated out including all values for a parameter
joined by ``-``, as well as combinations that include a subset of those
values, where the omitted elements of the tag are implicitly considered to
match the "default" value, indicated by them being first in their
collection (with the exception of "python", where the current python in
use is the default). Additionally, values that start with an underscore
are omitted from all ids and tags. Values that refer to Python versions
wlil be expanded to the full Python executable name when passed as
arguments to the session function, which is currently a workaround to
allow free-threaded python interpreters to be located.
:param base_tag: optional tag that will be appended to all tags generated,
e.g. if the decorator yields tags like ``python314-x86-windows``, a
``basetag`` value of ``all`` would yield the
tag as ``python314-x86-windows-all``.
:param filter\_: optional filtering function, must accept keyword arguments
matching the names in ``names``. Returns True or False indicating if
a certain tag combination should be included.
:param always_include_in_tag: list of names from ``names`` that indicate
parameters that should always be part of all tags, and not be omitted
as a "default"
"""
PY_RE = re.compile(r"(?:python)?([234]\.\d+(t?))")
def _is_py_version(token):
return bool(PY_RE.match(token))
def _expand_python_version(token):
"""expand pyx.y(t) tags into executable names.
Works around nox issue fixed at
https://github.com/wntrblm/nox/pull/999 by providing full executable
name
"""
if sys.platform == "win32":
return token
m = PY_RE.match(token)
# do this matching minimally so that it only happens for the
# free-threaded versions. on windows, the "pythonx.y" syntax doesn't
# work due to the use of the "py" tool
if m and m.group(2) == "t":
return f"python{m.group(1)}"
else:
return token
def _python_to_tag(token):
m = PY_RE.match(token)
if m:
return f"py{m.group(1).replace('.', '')}"
else:
return token
if always_include_in_tag:
name_to_list = dict(zip(names, token_lists))
must_be_present = [
name_to_list[name] for name in always_include_in_tag
]
else:
must_be_present = None
def _recur_param(prevtokens, prevtags, token_lists):
if not token_lists:
return
tokens = token_lists[0]
remainder = token_lists[1:]
for i, token in enumerate(tokens):
if _is_py_version(token):
is_our_python = token == OUR_PYTHON
tokentag = _python_to_tag(token)
is_default_token = is_our_python
else:
is_our_python = False
tokentag = token
is_default_token = i == 0
if is_our_python:
our_python_tags = ["py"]
else:
our_python_tags = []
if not tokentag.startswith("_"):
tags = (
prevtags
+ [tokentag]
+ [tag + "-" + tokentag for tag in prevtags]
+ our_python_tags
)
else:
tags = prevtags + our_python_tags
if remainder:
for args, newtags, ids in _recur_param(
prevtokens + [token], tags, remainder
):
if not is_default_token:
newtags = [
t
for t in newtags
if tokentag in t or t in our_python_tags
]
yield args, newtags, ids
else:
if not is_default_token:
newtags = [
t
for t in tags
if tokentag in t or t in our_python_tags
]
else:
newtags = tags
if base_tag:
newtags = [t + f"-{base_tag}" for t in newtags]
if must_be_present:
for t in list(newtags):
for required_tokens in must_be_present:
if not any(r in t for r in required_tokens):
newtags.remove(t)
break
yield prevtokens + [token], newtags, "-".join(
_python_to_tag(t)
for t in prevtokens + [token]
if not t.startswith("_")
)
params = [
nox.param(
*[_expand_python_version(a) for a in args], tags=tags, id=ids
)
for args, tags, ids in _recur_param([], [], token_lists)
if filter_ is None or filter_(**dict(zip(names, args)))
]
# for p in params:
# print(f"PARAM {'-'.join(p.args)} TAGS {p.tags}")
return nox.parametrize(names, params)
def extract_opts(posargs: list[str], *args: str) -> tuple[list[str], Any]:
"""Pop individual flag options from session.posargs.
Returns a named tuple with the individual flag options indicated,
as well the new posargs with those flags removed from the string list
so that the posargs can be forwarded onto pytest.
Basically if nox had an option for additional environmental flags that
didn't require putting them after ``--``, we wouldn't need this, but this
is probably more flexible.
"""
underscore_args = [arg.replace("-", "_") for arg in args]
return_tuple = collections.namedtuple("options", underscore_args)
look_for_args = {f"--{arg}": idx for idx, arg in enumerate(args)}
return_args = [False for arg in args]
def extract(arg: str):
if arg in look_for_args:
return_args[look_for_args[arg]] = True
return True
else:
return False
return [arg for arg in posargs if not extract(arg)], return_tuple(
*return_args
)
|