summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/tox/config/sets.py2
-rw-r--r--src/tox/execute/api.py6
-rw-r--r--src/tox/execute/local_sub_process/__init__.py2
-rw-r--r--src/tox/report.py13
-rw-r--r--src/tox/run.py8
-rw-r--r--src/tox/session/cmd/run/sequential.py25
-rw-r--r--src/tox/tox_env/api.py148
-rw-r--r--src/tox/tox_env/info.py8
-rw-r--r--src/tox/tox_env/python/api.py21
-rw-r--r--src/tox/tox_env/python/virtual_env/api.py32
-rw-r--r--tests/unit/execute/test_local_subprocess.py4
-rw-r--r--tox.ini1
12 files changed, 178 insertions, 92 deletions
diff --git a/src/tox/config/sets.py b/src/tox/config/sets.py
index fd5cfdfe..03442af5 100644
--- a/src/tox/config/sets.py
+++ b/src/tox/config/sets.py
@@ -97,7 +97,7 @@ class ConfigDynamicDefinition(ConfigDefinition[T]):
else:
value = self.default(conf, src.name) if callable(self.default) else self.default
if self.post_process is not None:
- self.post_process(value, conf) # noqa
+ value = self.post_process(value, conf) # noqa
self._cache = value
return cast(T, self._cache)
diff --git a/src/tox/execute/api.py b/src/tox/execute/api.py
index 97e77d85..f2e9501f 100644
--- a/src/tox/execute/api.py
+++ b/src/tox/execute/api.py
@@ -131,6 +131,7 @@ class Outcome:
def assert_success(self, logger: logging.Logger) -> None:
if self.exit_code != self.OK:
self._assert_fail(logger)
+ self._log_run(logging.INFO, logger)
def _assert_fail(self, logger: logging.Logger) -> NoReturn:
if self.show_on_standard is False:
@@ -140,9 +141,12 @@ class Outcome:
print(Fore.RED, file=sys.stderr, end="")
print(self.err, file=sys.stderr, end="")
print(Fore.RESET, file=sys.stderr)
- logger.critical("exit code %d for %s: %s in %s", self.exit_code, self.request.cwd, self.shell_cmd, self.elapsed)
+ self._log_run(logging.CRITICAL, logger)
raise SystemExit(self.exit_code)
+ def _log_run(self, lvl: int, logger: logging.Logger) -> None:
+ logger.log(lvl, "exit %d (%.2fs) cwd %s: %s", self.exit_code, self.elapsed, self.request.cwd, self.shell_cmd)
+
@property
def elapsed(self) -> float:
return self.end - self.start
diff --git a/src/tox/execute/local_sub_process/__init__.py b/src/tox/execute/local_sub_process/__init__.py
index b32c1d42..be2aac39 100644
--- a/src/tox/execute/local_sub_process/__init__.py
+++ b/src/tox/execute/local_sub_process/__init__.py
@@ -89,7 +89,7 @@ class LocalSubProcessExecuteInstance(ExecuteInstance):
return exit_code
@staticmethod
- def get_stream_file_no(key: str) -> Generator[int, Popen[bytes], None]:
+ def get_stream_file_no(key: str) -> Generator[int, 'Popen[bytes]', None]:
if sys.platform != "win32" and getattr(sys, key).isatty():
# on UNIX if tty is set let's forward it via a pseudo terminal
import pty
diff --git a/src/tox/report.py b/src/tox/report.py
index 8d841e65..08240704 100644
--- a/src/tox/report.py
+++ b/src/tox/report.py
@@ -1,5 +1,6 @@
"""Handle reporting from within tox"""
import logging
+import os
import sys
from colorama import Fore, Style, init
@@ -37,12 +38,16 @@ class ToxHandler(logging.StreamHandler):
color = Fore.GREEN
fmt = f"{Style.BRIGHT}{Fore.MAGENTA}%(name)s: {color}%(message)s{Style.RESET_ALL}"
if enabled_level <= logging.DEBUG:
- locate = "pathname" if enabled_level > logging.DEBUG else "module"
- fmt = f"%(levelname)s {fmt}{Style.DIM} [%(asctime)s] [%({locate})s:%(lineno)d]{Style.RESET_ALL}"
+ fmt = f"%(relativeCreated)d %(levelname)s {fmt}{Style.DIM} [%(pathname)s:%(lineno)d]{Style.RESET_ALL}"
formatter = logging.Formatter(fmt)
return formatter
def format(self, record: logging.LogRecord) -> str:
+ # shorten the pathname to start from within the site-packages folder
+ basename = os.path.dirname(record.pathname)
+ sys_path_match = sorted([p for p in sys.path if basename.startswith(p)], key=len, reverse=True)
+ record.pathname = record.pathname[len(sys_path_match[0]) + 1 :]
+
if record.levelno >= logging.ERROR:
return self.error_formatter.format(record)
if record.levelno >= logging.WARNING:
@@ -73,3 +78,7 @@ def _get_level(verbosity: int) -> int:
def _clean_handlers(log: logging.Logger) -> None:
for log_handler in list(log.handlers): # remove handlers of libraries
log.removeHandler(log_handler)
+
+
+class HandledError(RuntimeError):
+ """Error that has been handled so no need for stack trace"""
diff --git a/src/tox/run.py b/src/tox/run.py
index 854ec3fc..89518f44 100644
--- a/src/tox/run.py
+++ b/src/tox/run.py
@@ -8,6 +8,7 @@ from tox.config.cli.parse import get_options
from tox.config.main import Config
from tox.config.override import Override
from tox.config.source.ini import ToxIni
+from tox.report import HandledError
from tox.session.state import State
from tox.tox_env.builder import build_tox_envs
@@ -16,8 +17,11 @@ def run(args: Optional[Sequence[str]] = None) -> None:
try:
result = main(sys.argv[1:] if args is None else args)
except Exception as exception:
- logging.error("%s| %s", type(exception).__name__, str(exception))
- result = -2
+ if isinstance(exception, HandledError):
+ logging.error("%s| %s", type(exception).__name__, str(exception))
+ result = -2
+ else:
+ raise
except KeyboardInterrupt:
result = -2
raise SystemExit(result)
diff --git a/src/tox/session/cmd/run/sequential.py b/src/tox/session/cmd/run/sequential.py
index f117c633..ac4e4adf 100644
--- a/src/tox/session/cmd/run/sequential.py
+++ b/src/tox/session/cmd/run/sequential.py
@@ -3,6 +3,8 @@ Run tox environments in sequential order.
"""
from typing import Dict
+from colorama import Fore
+
from tox.config.cli.parser import ToxParser
from tox.execute.api import Outcome
from tox.plugin.impl import impl
@@ -26,19 +28,22 @@ def run_sequential(state: State) -> int:
for name in state.env_list:
tox_env = state.tox_envs[name]
status_codes[name] = run_one(tox_env, state.options.recreate, state.options.no_test)
- return report(status_codes, state.tox_envs)
+ return report(status_codes, state.tox_envs, state.options.is_colored)
+
+def report(status_dict: Dict[str, int], tox_envs: Dict[str, RunToxEnv], is_colored: bool) -> int: # noqa
+ def _print(color: int, msg: str) -> None:
+ print(f"{color if is_colored else ''}{msg}{Fore.RESET if is_colored else ''}")
-def report(status_dict: Dict[str, int], tox_envs: Dict[str, RunToxEnv]) -> int: # noqa
+ all_ok = True
for name, status in status_dict.items():
- if status == Outcome.OK:
- msg = "OK "
- else:
- msg = f"FAIL code {status}"
- print(f" {name}: {msg}")
- if all(value == Outcome.OK for name, value in status_dict.items()):
- print(" congratulations :)")
+ ok = status == Outcome.OK
+ msg = "OK " if ok else f"FAIL code {status}"
+ _print(Fore.GREEN if ok else Fore.RED, f" {name}: {msg}")
+ all_ok = ok and all_ok
+ if all_ok:
+ _print(Fore.GREEN, " congratulations :)")
return Outcome.OK
else:
- print(" evaluation failed :(")
+ _print(Fore.RED, " evaluation failed :(")
return -1
diff --git a/src/tox/tox_env/api.py b/src/tox/tox_env/api.py
index 25f29226..841e09c8 100644
--- a/src/tox/tox_env/api.py
+++ b/src/tox/tox_env/api.py
@@ -1,15 +1,16 @@
"""
Defines the abstract base traits of a tox environment.
"""
-import itertools
import logging
import os
+import re
import shutil
import sys
from abc import ABC, abstractmethod
from pathlib import Path
-from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Union, cast
+from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Union
+from tox.config.main import Config
from tox.config.sets import ConfigSet
from tox.execute.api import Execute, Outcome
from tox.execute.request import ExecuteRequest
@@ -19,23 +20,6 @@ from .info import Info
if TYPE_CHECKING:
from tox.config.cli.parser import Parsed
-if sys.platform == "win32":
- PASS_ENV_ALWAYS = [
- "SYSTEMDRIVE", # needed for pip6
- "SYSTEMROOT", # needed for python's crypto module
- "PATHEXT", # needed for discovering executables
- "COMSPEC", # needed for distutils cygwin compiler
- "PROCESSOR_ARCHITECTURE", # platform.machine()
- "USERPROFILE", # needed for `os.path.expanduser()`
- "MSYSTEM", # controls paths printed format
- "TEMP",
- "TMP",
- ]
-else:
- PASS_ENV_ALWAYS = [
- "TMPDIR",
- ]
-
class ToxEnv(ABC):
def __init__(self, conf: ConfigSet, core: ConfigSet, options: "Parsed"):
@@ -47,7 +31,7 @@ class ToxEnv(ABC):
self._cache = Info(self.conf["env_dir"])
self._paths: List[Path] = []
self.logger = logging.getLogger(self.conf["env_name"])
- self._env_vars: Dict[str, str] = {}
+ self._env_vars: Optional[Dict[str, str]] = None
def __repr__(self) -> str:
return f"{self.__class__.__name__}(name={self.conf['env_name']})"
@@ -63,18 +47,6 @@ class ToxEnv(ABC):
value=self.conf.name,
)
self.conf.add_config(
- keys=["set_env", "setenv"],
- of_type=Dict[str, str],
- default={},
- desc="environment variables to set when running commands in the tox environment",
- )
- self.conf.add_config(
- keys=["pass_env", "passenv"],
- of_type=List[str],
- default=[],
- desc="environment variables to pass on to the tox environment",
- )
- self.conf.add_config(
keys=["env_dir", "envdir"],
of_type=Path,
default=lambda conf, name: conf.core["work_dir"] / conf[name]["env_name"],
@@ -87,47 +59,106 @@ class ToxEnv(ABC):
desc="a folder that is always reset at the start of the run",
)
+ def set_env_post_process(values: Dict[str, str], config: Config) -> Dict[str, str]:
+ env = self.default_set_env()
+ env.update(values)
+ return env
+
+ self.conf.add_config(
+ keys=["set_env", "setenv"],
+ of_type=Dict[str, str],
+ default={},
+ desc="environment variables to set when running commands in the tox environment",
+ post_process=set_env_post_process,
+ )
+
+ def pass_env_post_process(values: List[str], config: Config) -> List[str]:
+ values.extend(self.default_pass_env())
+ return sorted(list({k: None for k in values}.keys()))
+
+ self.conf.add_config(
+ keys=["pass_env", "passenv"],
+ of_type=List[str],
+ default=[],
+ desc="environment variables to pass on to the tox environment",
+ post_process=pass_env_post_process,
+ )
+
+ def default_set_env(self) -> Dict[str, str]:
+ return {}
+
+ def default_pass_env(self) -> List[str]:
+ env = [
+ "https_proxy",
+ "http_proxy",
+ "no_proxy",
+ ]
+ if sys.platform == "win32":
+ env.extend(
+ [
+ "TEMP",
+ "TMP",
+ ]
+ )
+ else:
+ env.append("TMPDIR")
+ return env
+
def setup(self) -> None:
"""
1. env dir exists
2. contains a runner with the same type.
"""
- env_tmp_dir = cast(Path, self.conf["env_tmp_dir"])
- if env_tmp_dir.exists():
- shutil.rmtree(str(env_tmp_dir), ignore_errors=True)
- env_dir = cast(Path, self.conf["env_dir"])
+ env_dir: Path = self.conf["env_dir"]
conf = {"name": self.conf.name, "type": type(self).__name__}
- with self._cache.compare(conf, ToxEnv.__name__) as (eq, old):
- try:
- if eq is True:
- return
- # if either the name or type changed and already exists start over
- self.clean()
- finally:
- env_dir.mkdir(exist_ok=True, parents=True)
+ try:
+ with self._cache.compare(conf, ToxEnv.__name__) as (eq, old):
+ try:
+ if eq is True:
+ return
+ # if either the name or type changed and already exists start over
+ self.clean()
+ finally:
+ env_dir.mkdir(exist_ok=True, parents=True)
+ finally:
+ self._handle_env_tmp_dir()
+
+ def _handle_env_tmp_dir(self) -> None:
+ """Ensure exists and empty"""
+ env_tmp_dir: Path = self.conf["env_tmp_dir"]
+ if env_tmp_dir.exists():
+ logging.debug("removing %s", env_tmp_dir)
+ shutil.rmtree(env_tmp_dir, ignore_errors=True)
+ env_tmp_dir.mkdir(parents=True)
def clean(self) -> None:
- env_dir = self.conf["env_dir"]
+ env_dir: Path = self.conf["env_dir"]
if env_dir.exists():
- logging.info("removing %s", env_dir)
- shutil.rmtree(cast(Path, env_dir))
+ logging.info("remove tox env folder %s", env_dir)
+ shutil.rmtree(env_dir)
+ self._cache.reset()
@property
def environment_variables(self) -> Dict[str, str]:
- if self._env_vars:
+ if self._env_vars is not None:
return self._env_vars
- pass_env: List[str] = self.conf["pass_env"]
- pass_env.extend(PASS_ENV_ALWAYS)
+ result: Dict[str, str] = {}
+ pass_env: List[str] = self.conf["pass_env"]
+ glob_pass_env = [re.compile(e.replace("*", ".*")) for e in pass_env if "*" in e]
+ literal_pass_env = [e for e in pass_env if "*" not in e]
+ for env in literal_pass_env:
+ if env in os.environ:
+ result[env] = os.environ[env]
+ if glob_pass_env:
+ for env, value in os.environ.items():
+ if any(g.match(env) is not None for g in glob_pass_env):
+ result[env] = value
set_env: Dict[str, str] = self.conf["set_env"]
- for key, value in os.environ.items():
- if key in pass_env:
- set_env[key] = value
- self._env_vars.update(set_env)
- self._env_vars["PATH"] = os.pathsep.join(
- itertools.chain((str(i) for i in self._paths), os.environ.get("PATH", "").split(os.pathsep))
- )
- return self._env_vars
+ result.update(set_env)
+ result["PATH"] = os.pathsep.join([str(i) for i in self._paths] + os.environ.get("PATH", "").split(os.pathsep))
+ self._env_vars = result
+ return result
def execute(
self,
@@ -143,7 +174,6 @@ class ToxEnv(ABC):
request = ExecuteRequest(cmd, cwd, self.environment_variables, allow_stdin)
self.logger.warning("%s run => %s$ %s", self.conf["env_name"], request.cwd, request.shell_cmd)
outcome = self._executor(request=request, show_on_standard=show_on_standard, colored=self.options.colored)
- self.logger.info("done => code %d in %s for %s", outcome.exit_code, outcome.elapsed, outcome.shell_cmd)
return outcome
@staticmethod
diff --git a/src/tox/tox_env/info.py b/src/tox/tox_env/info.py
index 888b76b5..a55213ad 100644
--- a/src/tox/tox_env/info.py
+++ b/src/tox/tox_env/info.py
@@ -17,10 +17,14 @@ class Info:
value = {}
self._content = value
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}(path={self._path})"
+
@contextmanager
def compare(
self, value: Any, section: str, sub_section: Optional[str] = None
) -> Iterator[Tuple[bool, Optional[Any]]]:
+ """Cache"""
old = self._content.get(section)
if sub_section is not None and old is not None:
old = old.get(sub_section)
@@ -39,8 +43,8 @@ class Info:
self._content[section][sub_section] = value
self._write()
- def update(self, section: str, value: str) -> None:
- self._content[section] = value
+ def reset(self) -> None:
+ self._content = {}
def _write(self) -> None:
self._path.write_text(json.dumps(self._content, indent=2))
diff --git a/src/tox/tox_env/python/api.py b/src/tox/tox_env/python/api.py
index bef81e32..2fb6b794 100644
--- a/src/tox/tox_env/python/api.py
+++ b/src/tox/tox_env/python/api.py
@@ -62,6 +62,21 @@ class Python(ToxEnv, ABC):
value=lambda: self.env_site_package_dir(),
)
+ def default_pass_env(self) -> List[str]:
+ env = super().default_pass_env()
+ if sys.platform == "win32":
+ env.extend(
+ [
+ "SYSTEMROOT", # needed for python's crypto module
+ "PATHEXT", # needed for discovering executables
+ "COMSPEC", # needed for distutils cygwin compiler
+ "PROCESSOR_ARCHITECTURE", # platform.machine()
+ "USERPROFILE", # needed for `os.path.expanduser()`
+ "MSYSTEM", # controls paths printed format
+ ]
+ )
+ return env
+
def default_base_python(self, conf: "Config", env_name: str) -> List[str]:
spec = PythonSpec.from_string_spec(env_name)
if spec.implementation is not None:
@@ -98,9 +113,7 @@ class Python(ToxEnv, ABC):
if self._base_python_searched is False:
base_pythons = self.conf["base_python"]
self._base_python_searched = True
- for base_python in base_pythons:
- self._base_python = self._get_python(base_python)
- break
+ self._base_python = self._get_python(base_pythons)
if self._base_python is None:
self.no_base_python_found(base_pythons)
return cast(PythonInfo, self._base_python)
@@ -110,7 +123,7 @@ class Python(ToxEnv, ABC):
raise NotImplementedError
@abstractmethod
- def _get_python(self, base_python: str) -> PythonInfo:
+ def _get_python(self, base_python: List[str]) -> Optional[PythonInfo]:
raise NotImplementedError
def cached_install(self, deps: Deps, section: str, of_type: str) -> bool:
diff --git a/src/tox/tox_env/python/virtual_env/api.py b/src/tox/tox_env/python/virtual_env/api.py
index 8c023e81..79db2897 100644
--- a/src/tox/tox_env/python/virtual_env/api.py
+++ b/src/tox/tox_env/python/virtual_env/api.py
@@ -3,11 +3,10 @@ Declare the abstract base class for tox environments that handle the Python lang
"""
from abc import ABC
from pathlib import Path
-from typing import List, Optional, Sequence, cast
+from typing import Dict, List, Optional, Sequence, cast
from virtualenv import session_via_cli
from virtualenv.create.creator import Creator
-from virtualenv.discovery.builtin import get_interpreter
from virtualenv.run.session import Session
from tox.config.cli.parser import Parsed
@@ -19,10 +18,24 @@ from ..api import Deps, Python, PythonInfo
class VirtualEnv(Python, ABC):
+ """A python executor that uses the virtualenv project with pip"""
+
def __init__(self, conf: ConfigSet, core: ConfigSet, options: Parsed):
super().__init__(conf, core, options)
self._virtualenv_session: Optional[Session] = None # type: ignore[no-any-unimported]
+ def default_pass_env(self) -> List[str]:
+ env = super().default_pass_env()
+ env.append("PIP_*") # we use pip as installer
+ env.append("VIRTUALENV_*") # we use virtualenv as isolation creator
+ return env
+
+ def default_set_env(self) -> Dict[str, str]:
+ env = super().default_set_env()
+ env["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
+ env["VIRTUALENV_NO_PERIODIC_UPDATE"] = "1"
+ return env
+
def executor(self) -> Execute:
return LocalSubProcessExecutor()
@@ -30,12 +43,12 @@ class VirtualEnv(Python, ABC):
def session(self) -> Session: # type: ignore[no-any-unimported]
if self._virtualenv_session is None:
args = [
- "--no-periodic-update",
- "-p",
- self.base_python.executable,
"--clear",
str(cast(Path, self.conf["env_dir"])),
]
+ base_python: List[str] = self.conf["base_python"]
+ for base in base_python:
+ args.extend(["-p", base])
self._virtualenv_session = session_via_cli(args, setup_logging=False)
return self._virtualenv_session
@@ -46,9 +59,12 @@ class VirtualEnv(Python, ABC):
def create_python_env(self) -> None:
self.session.run()
- def _get_python(self, base_python: str) -> PythonInfo:
- info = get_interpreter(base_python)
- return PythonInfo(info.version_info, info.system_executable)
+ def _get_python(self, base_python: List[str]) -> Optional[PythonInfo]:
+ try:
+ return PythonInfo(self.creator.interpreter.version_info, self.creator.interpreter.system_executable)
+ except RuntimeError:
+ pass
+ return None
def paths(self) -> List[Path]:
"""Paths to add to the executable"""
diff --git a/tests/unit/execute/test_local_subprocess.py b/tests/unit/execute/test_local_subprocess.py
index 2dfd1b26..1077de24 100644
--- a/tests/unit/execute/test_local_subprocess.py
+++ b/tests/unit/execute/test_local_subprocess.py
@@ -146,8 +146,8 @@ def test_local_execute_basic_fail(caplog, capsys):
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.levelno == logging.CRITICAL
- assert record.msg == "exit code %d for %s: %s in %s"
- _code, _cwd, _cmd, _duration = record.args
+ assert record.msg == "exit %d (%.2fs) cwd %s: %s"
+ _code, _duration, _cwd, _cmd = record.args
assert _code == 3
assert _cwd == cwd
assert _cmd == request.shell_cmd
diff --git a/tox.ini b/tox.ini
index 3969b2cc..6f11e0a4 100644
--- a/tox.ini
+++ b/tox.ini
@@ -15,6 +15,7 @@ minversion = 3.14.0
[testenv]
description = run the tests with pytest
+package = wheel
passenv =
PYTEST_*
SSL_CERT_FILE