|
3 | 3 | """
|
4 | 4 | from __future__ import annotations
|
5 | 5 |
|
| 6 | +import logging |
6 | 7 | import os
|
7 | 8 | import re
|
8 | 9 | import sys
|
|
21 | 22 | from tox.config.loader.ini import IniLoader
|
22 | 23 | from tox.config.main import Config
|
23 | 24 |
|
| 25 | + |
| 26 | +LOGGER = logging.getLogger(__name__) |
| 27 | + |
| 28 | + |
24 | 29 | # split alongside :, unless it's preceded by a single capital letter (Windows drive letter in paths)
|
25 | 30 | ARG_DELIMITER = ":"
|
26 | 31 | REPLACE_START = "{"
|
27 | 32 | REPLACE_END = "}"
|
28 | 33 | BACKSLASH_ESCAPE_CHARS = ["\\", ARG_DELIMITER, REPLACE_START, REPLACE_END, "[", "]"]
|
| 34 | +MAX_REPLACE_DEPTH = 100 |
29 | 35 |
|
30 | 36 |
|
31 | 37 | MatchArg = Sequence[Union[str, "MatchExpression"]]
|
32 | 38 |
|
33 | 39 |
|
| 40 | +class MatchRecursionError(ValueError): |
| 41 | + """Could not stabalize on replacement value.""" |
| 42 | + |
| 43 | + |
| 44 | +class MatchError(Exception): |
| 45 | + """Could not find end terminator in MatchExpression.""" |
| 46 | + |
| 47 | + |
34 | 48 | def find_replace_expr(value: str) -> MatchArg:
|
35 | 49 | """Find all replaceable tokens within value."""
|
36 | 50 | return MatchExpression.parse_and_split_to_terminator(value)[0][0]
|
37 | 51 |
|
38 | 52 |
|
39 |
| -def replace(conf: Config, loader: IniLoader, value: str, args: ConfigLoadArgs) -> str: |
| 53 | +def replace(conf: Config, loader: IniLoader, value: str, args: ConfigLoadArgs, depth: int = 0) -> str: |
40 | 54 | """Replace all active tokens within value according to the config."""
|
41 |
| - return Replacer(conf, loader, conf_args=args).join(find_replace_expr(value)) |
42 |
| - |
43 |
| - |
44 |
| -class MatchError(Exception): |
45 |
| - """Could not find end terminator in MatchExpression.""" |
| 55 | + if depth > MAX_REPLACE_DEPTH: |
| 56 | + raise MatchRecursionError(f"Could not expand {value} after recursing {depth} frames") |
| 57 | + return Replacer(conf, loader, conf_args=args, depth=depth).join(find_replace_expr(value)) |
46 | 58 |
|
47 | 59 |
|
48 | 60 | class MatchExpression:
|
@@ -153,10 +165,11 @@ def _flatten_string_fragments(seq_of_str_or_other: Sequence[str | Any]) -> Seque
|
153 | 165 | class Replacer:
|
154 | 166 | """Recursively expand MatchExpression against the config and loader."""
|
155 | 167 |
|
156 |
| - def __init__(self, conf: Config, loader: IniLoader, conf_args: ConfigLoadArgs): |
| 168 | + def __init__(self, conf: Config, loader: IniLoader, conf_args: ConfigLoadArgs, depth: int = 0): |
157 | 169 | self.conf = conf
|
158 | 170 | self.loader = loader
|
159 | 171 | self.conf_args = conf_args
|
| 172 | + self.depth = depth |
160 | 173 |
|
161 | 174 | def __call__(self, value: MatchArg) -> Sequence[str]:
|
162 | 175 | return [self._replace_match(me) if isinstance(me, MatchExpression) else str(me) for me in value]
|
@@ -184,6 +197,13 @@ def _replace_match(self, value: MatchExpression) -> str:
|
184 | 197 | self.conf_args,
|
185 | 198 | )
|
186 | 199 | if replace_value is not None:
|
| 200 | + needs_expansion = any(isinstance(m, MatchExpression) for m in find_replace_expr(replace_value)) |
| 201 | + if needs_expansion: |
| 202 | + try: |
| 203 | + return replace(self.conf, self.loader, replace_value, self.conf_args, self.depth + 1) |
| 204 | + except MatchRecursionError as err: |
| 205 | + LOGGER.warning(str(err)) |
| 206 | + return replace_value |
187 | 207 | return replace_value
|
188 | 208 | # else: fall through -- when replacement is not possible, treat `{` as if escaped.
|
189 | 209 | # If we cannot replace, keep what was there, and continue looking for additional replaces
|
@@ -302,7 +322,7 @@ def replace_env(conf: Config, args: list[str], conf_args: ConfigLoadArgs) -> str
|
302 | 322 | return set_env.load(key, conf_args)
|
303 | 323 | elif conf_args.chain[-1] != new_key: # if there's a chain but only self-refers than use os.environ
|
304 | 324 | circular = ", ".join(i[4:] for i in conf_args.chain[conf_args.chain.index(new_key) :])
|
305 |
| - raise ValueError(f"circular chain between set env {circular}") |
| 325 | + raise MatchRecursionError(f"circular chain between set env {circular}") |
306 | 326 |
|
307 | 327 | if key in os.environ:
|
308 | 328 | return os.environ[key]
|
|
0 commit comments