Skip to content

Commit 49fef5a

Browse files
committed
Add conditional settings
1 parent 0df06a9 commit 49fef5a

File tree

3 files changed

+100
-2
lines changed

3 files changed

+100
-2
lines changed

src/tox/config/loader/convert.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
from abc import ABC, abstractmethod
55
from collections import OrderedDict
66
from inspect import isclass
7+
from itertools import chain
78
from pathlib import Path
8-
from typing import Any, Callable, Dict, Generic, Iterator, List, Literal, Optional, Set, TypeVar, Union, cast
9+
from typing import Any, Callable, Dict, Generic, Iterable, Iterator, List, Literal, Optional, Set, TypeVar, Union, cast
910

11+
from tox.config.loader.ini.factor import find_factor_groups
1012
from tox.config.types import Command, EnvList
1113

1214
_NO_MAPPING = object()
@@ -16,6 +18,44 @@
1618
Factory = Optional[Callable[[object], T]] # note the argument is anything, due e.g. memory loader can inject anything
1719

1820

21+
class ConditionalValue(Generic[T]):
22+
"""Value with a condition."""
23+
24+
def __init__(self, value: T, condition: str | None) -> None:
25+
self.value = value
26+
self.condition = condition
27+
self._groups = tuple(find_factor_groups(condition)) if condition is not None else ()
28+
29+
def matches(self, env_name: str) -> bool:
30+
"""Return whether the value matches the environment name."""
31+
if self.condition is None:
32+
return True
33+
34+
# Split env_name to factors.
35+
env_factors = set(chain.from_iterable([(i for i, _ in a) for a in find_factor_groups(env_name)]))
36+
37+
matches = []
38+
for group in self._groups:
39+
group_matches = []
40+
for factor, negate in group:
41+
group_matches.append((factor in env_factors) ^ negate)
42+
matches.append(all(group_matches))
43+
return any(matches)
44+
45+
46+
class ConditionalSetting(Generic[T]):
47+
"""Setting whose value depends on various conditions."""
48+
49+
def __init__(self, values: typing.Iterable[ConditionalValue[T]]) -> None:
50+
self.values = tuple(values)
51+
52+
def filter(self, env_name: str) -> Iterable[T]:
53+
"""Filter values for the environment."""
54+
for value in self.values:
55+
if value.matches(env_name):
56+
yield value.value
57+
58+
1959
class Convert(ABC, Generic[T]):
2060
"""A class that converts a raw type to a given tox (python) type."""
2161

src/tox/config/loader/ini/factor.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ def expand_factors(value: str) -> Iterator[tuple[list[list[tuple[str, bool]]] |
6060

6161

6262
def find_factor_groups(value: str) -> Iterator[list[tuple[str, bool]]]:
63-
"""Transform '{py,!pi}-{a,b},c' to [{'py', 'a'}, {'py', 'b'}, {'pi', 'a'}, {'pi', 'b'}, {'c'}]."""
63+
"""Transform '{py,!pi}-{a,b},c' to [[('py', False), ('a', False)],
64+
[('py', False), ('b', False)],
65+
[('pi', True), ('a', False)],
66+
[('pi', False), ('b', True)],
67+
[('c', False)]]."""
6468
for env in expand_env_with_negation(value):
6569
result = [name_with_negate(f) for f in env.split("-")]
6670
yield result
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Iterable
4+
5+
import pytest
6+
7+
from tox.config.loader.convert import ConditionalSetting, ConditionalValue
8+
9+
10+
@pytest.mark.parametrize(
11+
("condition", "env_name", "result"),
12+
[
13+
(None, "a", True),
14+
(None, "a-b", True),
15+
("a", "a", True),
16+
("!a", "a", False),
17+
("a", "b", False),
18+
("!a", "b", True),
19+
("a", "a-b", True),
20+
("!a", "a-b", False),
21+
# or
22+
("a,b", "a", True),
23+
("a,b", "b", True),
24+
("a,b", "c", False),
25+
("a,b", "a-b", True),
26+
("!a,!b", "c", True),
27+
# and
28+
("a-b", "a", False),
29+
("a-b", "c", False),
30+
("a-b", "a-b", True),
31+
("a-!b", "a-b", False),
32+
("!a-b", "a-b", False),
33+
],
34+
)
35+
def test_conditional_value_matches(condition: str, env_name: str, result: bool) -> None:
36+
assert ConditionalValue(42, condition).matches(env_name) is result
37+
38+
39+
@pytest.mark.parametrize(
40+
("values", "env_name", "result"),
41+
[
42+
([], "a", []),
43+
([ConditionalValue(42, None)], "a", [42]),
44+
([ConditionalValue(42, None)], "b", [42]),
45+
([ConditionalValue(42, "!a")], "a", []),
46+
([ConditionalValue(42, "!a")], "b", [42]),
47+
([ConditionalValue(42, "a"), ConditionalValue(43, "!a")], "a", [42]),
48+
([ConditionalValue(42, "a"), ConditionalValue(43, "!a")], "b", [43]),
49+
([ConditionalValue(42, "a"), ConditionalValue(43, "a")], "a", [42, 43]),
50+
],
51+
)
52+
def test_conditional_setting_filter(values: Iterable[ConditionalValue], env_name: str, result: list[Any]) -> None:
53+
setting = ConditionalSetting(values)
54+
assert list(setting.filter(env_name)) == result

0 commit comments

Comments
 (0)