summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBernát Gábor <bgabor8@bloomberg.net>2020-11-26 18:00:28 +0000
committerBernát Gábor <bgabor8@bloomberg.net>2020-11-27 17:47:57 +0000
commit39c07bb676411f75adc1dab697c5917b6a803c49 (patch)
treec2b3ffe6efc7611dea154581dff5818377a46ccf
parent8b526dc6bd10b2bff87c667ce14fba6cc54c0d1f (diff)
downloadtox-git-39c07bb676411f75adc1dab697c5917b6a803c49.tar.gz
Fix provisioning support
Signed-off-by: Bernát Gábor <bgabor8@bloomberg.net>
-rw-r--r--.gitignore30
-rw-r--r--setup.cfg2
-rw-r--r--src/tox/config/loader/convert.py17
-rw-r--r--src/tox/config/loader/memory.py8
-rw-r--r--src/tox/config/loader/str_convert.py15
-rw-r--r--src/tox/journal/__init__.py13
-rw-r--r--src/tox/plugin/manager.py2
-rw-r--r--src/tox/provision.py106
-rw-r--r--src/tox/provision/__init__.py80
-rw-r--r--src/tox/pytest.py215
-rw-r--r--src/tox/run.py4
-rw-r--r--src/tox/session/cmd/legacy.py2
-rw-r--r--src/tox/session/cmd/run/sequential.py7
-rw-r--r--src/tox/session/cmd/run/single.py11
-rw-r--r--src/tox/tox_env/api.py22
-rw-r--r--src/tox/tox_env/package.py39
-rw-r--r--src/tox/tox_env/python/api.py16
-rw-r--r--src/tox/tox_env/python/package.py8
-rw-r--r--src/tox/tox_env/python/runner.py56
-rw-r--r--src/tox/tox_env/python/virtual_env/api.py4
-rw-r--r--src/tox/tox_env/python/virtual_env/package/artifact/dev.py6
-rw-r--r--src/tox/tox_env/runner.py26
-rw-r--r--tests/config/cli/test_cli_env_var.py2
-rw-r--r--tests/config/cli/test_cli_ini.py2
-rw-r--r--tests/config/loader/ini/replace/test_replace_tox_env.py2
-rw-r--r--tests/config/test_main.py9
-rw-r--r--tests/conftest.py17
-rw-r--r--tests/demo_pkg_inline/build.py53
-rw-r--r--tests/demo_pkg_inline/pyproject.toml4
-rw-r--r--tests/demo_pkg_setuptools/demo_pkg_setuptools/__init__.py2
-rw-r--r--tests/demo_pkg_setuptools/pyproject.toml3
-rw-r--r--tests/demo_pkg_setuptools/setup.cfg6
-rw-r--r--tests/pytest_/test_init.py12
-rw-r--r--tests/session/cmd/run/test_int_setuptools.py52
-rw-r--r--tests/session/cmd/test_show_config.py1
-rw-r--r--tests/test_provision.py106
-rw-r--r--tests/tox_env/python/test_python_api.py18
-rw-r--r--tests/tox_env/python/test_python_runner.py10
-rw-r--r--tests/tox_env/python/virtual_env/setuptools/__init__.py0
-rw-r--r--tests/tox_env/python/virtual_env/setuptools/package/__init__.py0
-rw-r--r--tests/tox_env/python/virtual_env/setuptools/package/test_wheel_build.py87
-rw-r--r--tests/tox_env/python/virtual_env/test_setuptools.py62
-rw-r--r--tox.ini4
43 files changed, 747 insertions, 394 deletions
diff --git a/.gitignore b/.gitignore
index 126cbb96..93a6c293 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,36 +1,16 @@
-# python
*.pyc
*.pyo
*.swp
__pycache__
.eggs
-
-
-# packaging folders
/src/tox/version.py
-/build/
-/dist/
-/src/tox.egg-info
-
-# tox working folder
-/.tox
-/.tox4
-
-# IDEs
-/.idea
-/.vscode
-
-# tools
+build
+dist
+*.egg-info
+.tox
+.tox4
/.*_cache
.dmypy.json
-
-# documentation
/docs/_draft.rst
-
-# release
-credentials.json
-
pip-wheel-metadata
-.DS_Store
-.coverage.*
Dockerfile
diff --git a/setup.cfg b/setup.cfg
index 7f696a49..8e0d372e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -61,6 +61,8 @@ docs =
towncrier>=19.9.0rc1
testing =
covdefaults>=1.2
+ devpi-client>=5
+ devpi-server>=5
freezegun>=1
psutil>=5.7
pytest>=5.4.1
diff --git a/src/tox/config/loader/convert.py b/src/tox/config/loader/convert.py
index 27426651..0143e138 100644
--- a/src/tox/config/loader/convert.py
+++ b/src/tox/config/loader/convert.py
@@ -21,7 +21,6 @@ class Convert(ABC, Generic[T]):
"""A class that converts a raw type to a given tox (python) type"""
def to(self, raw: T, of_type: Type[V]) -> V:
-
from_module = getattr(of_type, "__module__", None)
if from_module in ("typing", "typing_extensions"):
return self._to_typing(raw, of_type) # type: ignore[return-value]
@@ -35,6 +34,8 @@ class Convert(ABC, Generic[T]):
return self.to_env_list(raw) # type: ignore[return-value]
elif issubclass(of_type, str):
return self.to_str(raw) # type: ignore[return-value]
+ elif isinstance(raw, of_type):
+ return raw
elif issubclass(of_type, Enum):
return cast(V, getattr(of_type, str(raw)))
return of_type(raw) # type: ignore[call-arg]
@@ -44,13 +45,15 @@ class Convert(ABC, Generic[T]):
result: Any = _NO_MAPPING
if origin in (list, List):
entry_type = of_type.__args__[0] # type: ignore[attr-defined]
- result = [self.to(i, entry_type) for i in self.to_list(raw)]
+ result = [self.to(i, entry_type) for i in self.to_list(raw, entry_type)]
elif origin in (set, Set):
entry_type = of_type.__args__[0] # type: ignore[attr-defined]
- result = {self.to(i, entry_type) for i in self.to_set(raw)}
+ result = {self.to(i, entry_type) for i in self.to_set(raw, entry_type)}
elif origin in (dict, Dict):
key_type, value_type = of_type.__args__[0], of_type.__args__[1] # type: ignore[attr-defined]
- result = OrderedDict((self.to(k, key_type), self.to(v, value_type)) for k, v in self.to_dict(raw))
+ result = OrderedDict(
+ (self.to(k, key_type), self.to(v, value_type)) for k, v in self.to_dict(raw, (key_type, value_type))
+ )
elif origin == Union: # handle Optional values
args: List[Type[Any]] = of_type.__args__ # type: ignore[attr-defined]
none = type(None)
@@ -81,17 +84,17 @@ class Convert(ABC, Generic[T]):
@staticmethod
@abstractmethod
- def to_list(value: T) -> Iterator[T]:
+ def to_list(value: T, of_type: Type[Any]) -> Iterator[T]:
raise NotImplementedError
@staticmethod
@abstractmethod
- def to_set(value: T) -> Iterator[T]:
+ def to_set(value: T, of_type: Type[Any]) -> Iterator[T]:
raise NotImplementedError
@staticmethod
@abstractmethod
- def to_dict(value: T) -> Iterator[Tuple[T, T]]:
+ def to_dict(value: T, of_type: Tuple[Type[Any], Type[Any]]) -> Iterator[Tuple[T, T]]:
raise NotImplementedError
@staticmethod
diff --git a/src/tox/config/loader/memory.py b/src/tox/config/loader/memory.py
index 874a2d76..2c5abe3c 100644
--- a/src/tox/config/loader/memory.py
+++ b/src/tox/config/loader/memory.py
@@ -1,5 +1,5 @@
from pathlib import Path
-from typing import Any, Dict, Iterator, Optional, Set, Tuple, cast
+from typing import Any, Dict, Iterator, Optional, Set, Tuple, Type, cast
from tox.config.loader.convert import T
from tox.config.main import Config
@@ -28,15 +28,15 @@ class MemoryLoader(Loader[Any]):
return value # type: ignore[no-any-return]
@staticmethod
- def to_list(value: Any) -> Iterator[T]:
+ def to_list(value: Any, of_type: Type[Any]) -> Iterator[T]:
return value # type: ignore[no-any-return]
@staticmethod
- def to_set(value: Any) -> Iterator[T]:
+ def to_set(value: Any, of_type: Type[Any]) -> Iterator[T]:
return iter(value) # type: ignore[no-any-return]
@staticmethod
- def to_dict(value: Any) -> Iterator[Tuple[T, T]]:
+ def to_dict(value: Any, of_type: Tuple[Type[Any], Type[Any]]) -> Iterator[Tuple[T, T]]:
return value.items() # type: ignore[no-any-return]
@staticmethod
diff --git a/src/tox/config/loader/str_convert.py b/src/tox/config/loader/str_convert.py
index 98ef5107..75960d7a 100644
--- a/src/tox/config/loader/str_convert.py
+++ b/src/tox/config/loader/str_convert.py
@@ -2,7 +2,7 @@
import shlex
from itertools import chain
from pathlib import Path
-from typing import Iterator, Tuple
+from typing import Any, Iterator, Tuple, Type
from tox.config.loader.convert import Convert
from tox.config.types import Command, EnvList
@@ -20,8 +20,8 @@ class StrConvert(Convert[str]):
return Path(value)
@staticmethod
- def to_list(value: str) -> Iterator[str]:
- splitter = "\n" if "\n" in value else ","
+ def to_list(value: str, of_type: Type[Any]) -> Iterator[str]:
+ splitter = "\n" if issubclass(of_type, Command) or "\n" in value else ","
splitter = splitter.replace("\r", "")
for token in value.split(splitter):
value = token.strip()
@@ -29,12 +29,12 @@ class StrConvert(Convert[str]):
yield value
@staticmethod
- def to_set(value: str) -> Iterator[str]:
- for value in StrConvert.to_list(value):
+ def to_set(value: str, of_type: Type[Any]) -> Iterator[str]:
+ for value in StrConvert.to_list(value, of_type):
yield value
@staticmethod
- def to_dict(value: str) -> Iterator[Tuple[str, str]]:
+ def to_dict(value: str, of_type: Tuple[Type[Any], Type[Any]]) -> Iterator[Tuple[str, str]]:
for row in value.split("\n"):
row = row.strip()
if row:
@@ -49,7 +49,8 @@ class StrConvert(Convert[str]):
@staticmethod
def to_command(value: str) -> Command:
- return Command(shlex.split(value))
+ args = shlex.split(value, comments=True)
+ return Command(args)
@staticmethod
def to_env_list(value: str) -> EnvList:
diff --git a/src/tox/journal/__init__.py b/src/tox/journal/__init__.py
index f292dd45..6ce9aa47 100644
--- a/src/tox/journal/__init__.py
+++ b/src/tox/journal/__init__.py
@@ -1,8 +1,21 @@
"""This module handles collecting and persisting in json format a tox session"""
+import json
+from pathlib import Path
+from typing import Optional
+
from .env import EnvJournal
from .main import Journal
+
+def write_journal(path: Optional[Path], journal: Journal) -> None:
+ if path is None:
+ return
+ with open(path, "wt") as file_handler:
+ json.dump(journal.content, file_handler, indent=2, ensure_ascii=False)
+
+
__all__ = (
"Journal",
"EnvJournal",
+ "write_journal",
)
diff --git a/src/tox/plugin/manager.py b/src/tox/plugin/manager.py
index 9d7bb5ed..5cb3c93f 100644
--- a/src/tox/plugin/manager.py
+++ b/src/tox/plugin/manager.py
@@ -10,6 +10,7 @@ from tox.config.sets import ConfigSet
from tox.session import state
from tox.session.cmd import devenv, legacy, list_env, quickstart, show_config, version_flag
from tox.session.cmd.run import parallel, sequential
+from tox.tox_env import package as package_api
from tox.tox_env.api import ToxEnv
from tox.tox_env.python.virtual_env import runner
from tox.tox_env.python.virtual_env.package.artifact import dev, sdist, wheel
@@ -38,6 +39,7 @@ class Plugin:
list_env,
parallel,
sequential,
+ package_api,
)
for plugin in internal_plugins:
diff --git a/src/tox/provision.py b/src/tox/provision.py
new file mode 100644
index 00000000..6c1a90a4
--- /dev/null
+++ b/src/tox/provision.py
@@ -0,0 +1,106 @@
+"""
+This package handles provisioning an appropriate tox version per requirements.
+"""
+import logging
+import sys
+from argparse import ArgumentParser
+from typing import List, Tuple, Union, cast
+
+from packaging.requirements import Requirement
+from packaging.utils import canonicalize_name
+from packaging.version import Version
+
+from tox.config.loader.memory import MemoryLoader
+from tox.config.main import Config
+from tox.config.sets import ConfigSet
+from tox.plugin.impl import impl
+from tox.session.state import State
+from tox.tox_env.python.api import PythonDep
+from tox.tox_env.python.runner import PythonRun
+from tox.tox_env.python.virtual_env.package.api import PackageType
+from tox.version import __version__ as current_version
+
+if sys.version_info >= (3, 8): # pragma: no cover (py38+)
+ from importlib.metadata import PackageNotFoundError, distribution
+else: # pragma: no cover (py38+)
+ from importlib_metadata import PackageNotFoundError, distribution # noqa
+
+
+@impl
+def tox_add_option(parser: ArgumentParser) -> None:
+ parser.add_argument(
+ "--no-recreate-provision",
+ dest="recreate",
+ help="if recreate is set do not recreate provision tox environment",
+ action="store_true",
+ )
+
+
+@impl
+def tox_add_core_config(core: ConfigSet) -> None:
+ core.add_config(
+ keys=["min_version", "minversion"],
+ of_type=Version,
+ default=Version(current_version),
+ desc="Define the minimal tox version required to run",
+ )
+ core.add_config(
+ keys="provision_tox_env",
+ of_type=str,
+ default=".tox",
+ desc="Name of the virtual environment used to provision a tox.",
+ )
+
+ def add_tox_requires_min_version(requires: List[Requirement], conf: Config) -> List[Requirement]:
+ min_version: Version = conf.core["min_version"]
+ requires.append(Requirement(f"tox >= {min_version}"))
+ return requires
+
+ core.add_config(
+ keys="requires",
+ of_type=List[Requirement],
+ default=[],
+ desc="Name of the virtual environment used to provision a tox.",
+ post_process=add_tox_requires_min_version,
+ )
+
+
+def provision(state: State) -> Union[int, bool]:
+ requires: List[Requirement] = state.conf.core["requires"]
+ missing: List[Tuple[Requirement, str]] = []
+ for package in requires:
+ package_name = canonicalize_name(package.name)
+ try:
+ dist = distribution(package_name)
+ if not package.specifier.contains(dist.version, prereleases=True):
+ missing.append((package, dist.version))
+ except PackageNotFoundError:
+ missing.append((package, "N/A"))
+ if not missing:
+ return False
+ deps = ", ".join(f"{p} ({ver})" for p, ver in missing)
+ logging.warning(
+ "will run in automatically provisioned tox, host %s is missing [requires (has)]: %s", sys.executable, deps
+ )
+ return run_provision(requires, state)
+
+
+def run_provision(deps: List[Requirement], state: State) -> int: # noqa
+ """"""
+ loader = MemoryLoader( # these configuration values are loaded from in-memory always (no file conf)
+ base=[], # disable inheritance for provision environments
+ package=PackageType.skip, # no packaging for this please
+ deps=[PythonDep(d) for d in deps], # use our own dependency specification
+ pass_env=["*"], # do not filter environment variables, will be handled by provisioned tox
+ )
+ provision_tox_env: str = state.conf.core["provision_tox_env"]
+ state.conf.get_env(provision_tox_env, loaders=[loader])
+ tox_env = cast(PythonRun, state.tox_env(provision_tox_env))
+ env_python = tox_env.env_python()
+ logging.info("will run in a automatically provisioned python environment under %s", env_python)
+ recreate = state.options.no_recreate_provision is False if state.options.recreate else False
+ tox_env.ensure_setup(recreate=recreate)
+ args: List[str] = [str(env_python), "-m", "tox"]
+ args.extend(state.args)
+ outcome = tox_env.execute(cmd=args, allow_stdin=True, show_on_standard=True, run_id="provision")
+ return outcome.exit_code
diff --git a/src/tox/provision/__init__.py b/src/tox/provision/__init__.py
deleted file mode 100644
index 013558bc..00000000
--- a/src/tox/provision/__init__.py
+++ /dev/null
@@ -1,80 +0,0 @@
-"""
-This package handles provisioning an appropriate tox version per requirements.
-"""
-import sys
-from typing import List
-
-from packaging.requirements import Requirement
-from packaging.utils import canonicalize_name
-from packaging.version import Version
-
-from tox.config.main import Config
-from tox.config.sets import ConfigSet
-from tox.plugin.impl import impl
-from tox.session.state import State
-from tox.tox_env.api import ToxEnv
-from tox.version import __version__ as current_version
-
-if sys.version_info >= (3, 8): # pragma: no cover (py38+)
- from importlib.metadata import distribution
-else: # pragma: no cover (py38+)
- from importlib_metadata import distribution # noqa
-
-
-def add_tox_requires_min_version(requires: List[Requirement], conf: Config) -> List[Requirement]:
- min_version = conf.core["min_version"]
- requires.append(Requirement(f"tox >= {min_version}"))
- return requires
-
-
-def provision(state: State) -> None:
- core = state.conf.core
- provision_tox_env = core["provision_tox_env"]
- requires = core["requires"]
-
- exists = set()
- missing = []
- for package in requires:
- package_name = canonicalize_name(package.name)
- if package_name not in exists:
- exists.add(package_name)
- dist = distribution(package.name)
- if not package.specifier.contains(dist.version, prereleases=True):
- missing.append(package)
- if missing:
- for package in missing:
- print(package)
- run_provision(requires, state.tox_env(provision_tox_env))
-
-
-@impl
-def tox_add_core_config(core: ConfigSet) -> None:
- core.add_config(
- keys=["min_version", "minversion"],
- of_type=Version,
- default=Version(current_version),
- desc="Define the minimal tox version required to run",
- )
- core.add_config(
- keys="provision_tox_env",
- of_type=str,
- default=".tox",
- desc="Name of the virtual environment used to provision a tox.",
- )
- core.add_config(
- keys="requires",
- of_type=List[Requirement],
- default=[],
- desc="Name of the virtual environment used to provision a tox.",
- post_process=add_tox_requires_min_version,
- )
- core.add_config(
- keys=["no_package", "skipsdist"],
- of_type=bool,
- default=False,
- desc="Is there any packaging involved in this project.",
- )
-
-
-def run_provision(deps: List[Requirement], tox_env: ToxEnv) -> None: # noqa
- """"""
diff --git a/src/tox/pytest.py b/src/tox/pytest.py
index d6b0be86..22265062 100644
--- a/src/tox/pytest.py
+++ b/src/tox/pytest.py
@@ -3,13 +3,20 @@ A pytest plugin useful to test tox itself (and its plugins).
"""
import os
+import random
import re
+import shutil
+import socket
+import string
import sys
import textwrap
import warnings
-from contextlib import contextmanager
+from contextlib import closing, contextmanager
from pathlib import Path
-from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Optional, Sequence
+from subprocess import PIPE, Popen, check_call
+from threading import Thread
+from types import TracebackType
+from typing import IO, TYPE_CHECKING, Any, Dict, Iterator, List, Optional, Sequence, Tuple, Type, cast
import pytest
from _pytest.capture import CaptureFixture as _CaptureFixture
@@ -18,6 +25,9 @@ from _pytest.config.argparsing import Parser
from _pytest.logging import LogCaptureFixture
from _pytest.monkeypatch import MonkeyPatch
from _pytest.python import Function
+from _pytest.tmpdir import TempPathFactory
+from virtualenv.discovery.py_info import PythonInfo
+from virtualenv.info import IS_WIN
import tox.run
from tox.execute.api import Outcome
@@ -28,11 +38,20 @@ from tox.run import setup_state as previous_setup_state
from tox.session.cmd.run.parallel import ENV_VAR_KEY
from tox.session.state import State
+if sys.version_info >= (3, 8): # pragma: no cover (py38+)
+ from typing import Protocol
+else: # pragma: no cover (<py38)
+ from typing_extensions import Protocol # noqa
+
if TYPE_CHECKING:
CaptureFixture = _CaptureFixture[str]
else:
CaptureFixture = _CaptureFixture
+os.environ["PIP_DISABLE_PIP_VERSION_CHECK"] = "1"
+os.environ["PIP_NO_PYTHON_VERSION_WARNING"] = "1"
+os.environ["PIP_USE_FEATURE"] = "2020-resolver"
+
@pytest.fixture(autouse=True)
def ensure_logging_framework_not_altered() -> Iterator[None]:
@@ -86,6 +105,7 @@ class ToxProject:
def __init__(
self,
files: Dict[str, Any],
+ base: Optional[Path],
path: Path,
capsys: CaptureFixture,
monkeypatch: MonkeyPatch,
@@ -93,17 +113,20 @@ class ToxProject:
self.path: Path = path
self.monkeypatch: MonkeyPatch = monkeypatch
self._capsys = capsys
- self._setup_files(self.path, files)
+ self._setup_files(self.path, base, files)
@staticmethod
- def _setup_files(dest: Path, content: Dict[str, Any]) -> None:
+ def _setup_files(dest: Path, base: Optional[Path], content: Dict[str, Any]) -> None:
+ if base is not None:
+ shutil.copytree(str(base), str(dest))
+ dest.mkdir(exist_ok=True)
for key, value in content.items():
if not isinstance(key, str):
raise TypeError(f"{key!r} at {dest}") # pragma: no cover
at_path = dest / key
if isinstance(value, dict):
at_path.mkdir(exist_ok=True)
- ToxProject._setup_files(at_path, value)
+ ToxProject._setup_files(at_path, None, value)
elif isinstance(value, str):
at_path.write_text(textwrap.dedent(value))
else:
@@ -149,6 +172,9 @@ class ToxProject:
m.setattr(sys, "argv", [sys.executable, "-m", "tox"] + list(args))
m.setenv("VIRTUALENV_SYMLINK_APP_DATA", "1")
m.setenv("VIRTUALENV_SYMLINKS", "1")
+ m.setenv("VIRTUALENV_PIP", "embed")
+ m.setenv("VIRTUALENV_WHEEL", "embed")
+ m.setenv("VIRTUALENV_SETUPTOOLS", "embed")
try:
tox_run(args)
except SystemExit as exception:
@@ -186,6 +212,9 @@ class ToxRunOutcome:
def assert_success(self) -> None:
assert self.success, repr(self)
+ def assert_failed(self) -> None:
+ assert not self.success, repr(self)
+
def __repr__(self) -> str:
return "\n".join(
"{}{}{}".format(k, "\n" if "\n" in v else ": ", v)
@@ -230,16 +259,18 @@ class ToxRunOutcome:
assert Matches(pattern, flags=flags) == text
-ToxProjectCreator = Callable[[Dict[str, Any]], ToxProject]
+class ToxProjectCreator(Protocol):
+ def __call__(self, files: Dict[str, Any], base: Optional[Path] = None) -> ToxProject:
+ ...
@pytest.fixture(name="tox_project")
def init_fixture(tmp_path: Path, capsys: CaptureFixture, monkeypatch: MonkeyPatch) -> ToxProjectCreator:
- def _init(files: Dict[str, Any]) -> ToxProject:
+ def _init(files: Dict[str, Any], base: Optional[Path] = None) -> ToxProject:
"""create tox projects"""
- return ToxProject(files, tmp_path, capsys, monkeypatch)
+ return ToxProject(files, base, tmp_path / "p", capsys, monkeypatch)
- return _init
+ return _init # noqa
@pytest.fixture()
@@ -277,12 +308,178 @@ def pytest_collection_modifyitems(config: PyTestConfig, items: List[Function]) -
items.sort(key=lambda i: 1 if is_integration(i) else 0)
+class Index:
+ def __init__(self, base_url: str, name: str, client_cmd_base: List[str]) -> None:
+ self._client_cmd_base = client_cmd_base
+ self._server_url = base_url
+ self.name = name
+
+ @property
+ def url(self) -> str:
+ return f"{self._server_url}/{self.name}/+simple"
+
+ def upload(self, files: Sequence[Path]) -> None:
+ check_call(self._client_cmd_base + ["upload", "--index", self.name] + [str(i) for i in files])
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}(url={self.url})" # pragma: no cover
+
+ def use(self, monkeypatch: MonkeyPatch) -> None:
+ enable_pypi_server(monkeypatch, self.url)
+
+
+def enable_pypi_server(monkeypatch: MonkeyPatch, url: Optional[str]) -> None:
+ if url is None: # pragma: no cover # only one of the branches can be hit depending on env
+ monkeypatch.delenv("PIP_INDEX_URL", raising=False)
+ else: # pragma: no cover
+ monkeypatch.setenv("PIP_INDEX_URL", url)
+ monkeypatch.setenv("PIP_RETRIES", str(5))
+ monkeypatch.setenv("PIP_TIMEOUT", str(2))
+
+
+def _find_free_port() -> int:
+ with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as socket_handler:
+ socket_handler.bind(("", 0))
+ return cast(int, socket_handler.getsockname()[1])
+
+
+class IndexServer:
+ def __init__(self, path: Path) -> None:
+ self.path = path
+
+ self.host, self.port = "localhost", _find_free_port()
+ self._passwd = "".join(random.choice(string.ascii_letters) for _ in range(8))
+
+ def _exe(name: str) -> str:
+ return str(Path(scripts_dir) / f"{name}{'.exe' if IS_WIN else ''}")
+
+ scripts_dir = PythonInfo.current().sysconfig_path("scripts")
+ self._init: str = _exe("devpi-init")
+ self._server: str = _exe("devpi-server")
+ self._client: str = _exe("devpi")
+
+ self._server_dir = self.path / "server"
+ self._client_dir = self.path / "client"
+ self._indexes: Dict[str, Index] = {}
+ self._process: Optional["Popen[str]"] = None
+ self._has_use = False
+ self._stdout_drain: Optional[Thread] = None
+
+ def __enter__(self) -> "IndexServer":
+ self._create_and_start_server()
+ self._setup_client()
+ return self
+
+ def _create_and_start_server(self) -> None:
+ self._server_dir.mkdir(exist_ok=True)
+ server_at = str(self._server_dir)
+ # 1. create the server
+ cmd = [self._init, "--serverdir", server_at]
+ cmd.extend(("--no-root-pypi", "--role", "standalone", "--root-passwd", self._passwd))
+ check_call(cmd, stdout=PIPE, stderr=PIPE)
+ # 2. start the server
+ cmd = [self._server, "--serverdir", server_at, "--port", str(self.port), "--offline-mode"]
+ self._process = Popen(cmd, stdout=PIPE, universal_newlines=True)
+ stdout = self._drain_stdout()
+ for line in stdout: # pragma: no branch # will always loop at least once
+ if "serving at url" in line:
+
+ def _keep_draining() -> None:
+ for _ in stdout:
+ pass
+
+ # important to keep draining the stdout, otherwise once the buffer is full Windows blocks the processg s
+ self._stdout_drain = Thread(target=_keep_draining)
+ self._stdout_drain.start()
+ break
+
+ def _drain_stdout(self) -> Iterator[str]:
+ process = cast("Popen[str]", self._process)
+ stdout = cast(IO[str], process.stdout)
+ while True:
+ if process.poll() is not None: # pragma: no cover
+ raise RuntimeError(f"devpi server with pid {process.pid} at {self._server_dir} died")
+ yield stdout.readline()
+
+ def _setup_client(self) -> None:
+ """create a user on the server and authenticate it"""
+ self._client_dir.mkdir(exist_ok=True)
+ base = ["--clientdir", str(self._client_dir)]
+ check_call([self._client, "use"] + base + [self.url], stdout=PIPE, stderr=PIPE)
+ check_call([self._client, "login"] + base + ["root", "--password", self._passwd], stdout=PIPE, stderr=PIPE)
+
+ def create_index(self, name: str, *args: str) -> Index:
+ if name in self._indexes: # pragma: no cover
+ raise ValueError(f"index {name} already exists")
+ base = [self._client, "--clientdir", str(self._client_dir)]
+ check_call(base + ["index", "-c", name, *args], stdout=PIPE, stderr=PIPE)
+ index = Index(f"{self.url}/root", name, base)
+ if not self._has_use:
+ self._has_use = True
+ check_call(base + ["use", f"root/{name}"], stdout=PIPE, stderr=PIPE)
+ self._indexes[name] = index
+ return index
+
+ def __exit__(
+ self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]
+ ) -> None:
+ if self._process is not None: # pragma: no cover # defend against devpi startup fail
+ self._process.terminate()
+ if self._stdout_drain is not None and self._stdout_drain.is_alive(): # pragma: no cover # devpi startup fail
+ self._stdout_drain.join()
+
+ @property
+ def url(self) -> str:
+ return f"http://{self.host}:{self.port}"
+
+ def __repr__(self) -> str:
+ return f"{self.__class__.__name__}(url={self.url}, indexes={list(self._indexes)})" # pragma: no cover
+
+
+@pytest.fixture(scope="session")
+def pypi_server(tmp_path_factory: TempPathFactory) -> Iterator[IndexServer]:
+ # takes around 2.5s
+ path = tmp_path_factory.mktemp("pypi")
+ with IndexServer(path) as server:
+ server.create_index("empty", "volatile=False")
+ yield server
+
+
+@pytest.fixture(scope="session")
+def _invalid_index_fake_port() -> int:
+ return _find_free_port()
+
+
+@pytest.fixture(autouse=True)
+def disable_pip_pypi_access(_invalid_index_fake_port: int, monkeypatch: MonkeyPatch) -> Tuple[str, Optional[str]]:
+ """set a fake pip index url, tests that want to use a pypi server should create and overwrite this"""
+ previous_url = os.environ.get("PIP_INDEX_URL")
+ new_url = f"http://localhost:{_invalid_index_fake_port}/bad-pypi-server"
+ monkeypatch.setenv("PIP_INDEX_URL", new_url)
+ monkeypatch.setenv("PIP_RETRIES", str(0))
+ monkeypatch.setenv("PIP_TIMEOUT", str(0.001))
+ return new_url, previous_url
+
+
+@pytest.fixture(name="enable_pip_pypi_access")
+def enable_pip_pypi_access_fixture(
+ disable_pip_pypi_access: Tuple[str, Optional[str]], monkeypatch: MonkeyPatch
+) -> Optional[str]:
+ """set a fake pip index url, tests that want to use a pypi server should create and overwrite this"""
+ _, previous_url = disable_pip_pypi_access
+ enable_pypi_server(monkeypatch, previous_url)
+ return previous_url
+
+
__all__ = (
"CaptureFixture",
"LogCaptureFixture",
+ "TempPathFactory",
"MonkeyPatch",
"ToxRunOutcome",
"ToxProject",
"ToxProjectCreator",
"check_os_environ",
+ "IndexServer",
+ "Index",
)
diff --git a/src/tox/run.py b/src/tox/run.py
index 2f3788eb..69b6fdf6 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.cli.parser import Parsed
from tox.config.main import Config
from tox.config.source.tox_ini import ToxIni
+from tox.provision import provision
from tox.report import HandledError
from tox.session.state import State
@@ -28,6 +29,9 @@ def run(args: Optional[Sequence[str]] = None) -> None:
def main(args: Sequence[str]) -> int:
state = setup_state(args)
+ result = provision(state)
+ if result is not False:
+ return result
command = state.options.command
handler = state.handlers[command]
result = handler(state)
diff --git a/src/tox/session/cmd/legacy.py b/src/tox/session/cmd/legacy.py
index 706c9049..f2bbf57d 100644
--- a/src/tox/session/cmd/legacy.py
+++ b/src/tox/session/cmd/legacy.py
@@ -87,6 +87,8 @@ def tox_add_option(parser: ToxParser) -> None:
def legacy(state: State) -> int:
option = state.options
if option.show_config:
+ state.options.list_keys_only = []
+ state.options.show_core = True
return show_config(state)
if option.list_envs or option.list_envs_all:
option.list_no_description = option.verbosity <= DEFAULT_VERBOSITY
diff --git a/src/tox/session/cmd/run/sequential.py b/src/tox/session/cmd/run/sequential.py
index 7738ac01..39d9ab35 100644
--- a/src/tox/session/cmd/run/sequential.py
+++ b/src/tox/session/cmd/run/sequential.py
@@ -1,7 +1,6 @@
"""
Run tox environments in sequential order.
"""
-import json
from datetime import datetime
from typing import Dict, List, Tuple
@@ -9,6 +8,7 @@ from colorama import Fore
from tox.config.cli.parser import ToxParser
from tox.execute.api import Outcome
+from tox.journal import write_journal
from tox.plugin.impl import impl
from tox.session.common import env_list_flag
from tox.session.state import State
@@ -32,10 +32,7 @@ def run_sequential(state: State) -> int:
code, outcomes = run_one(tox_env, state.options.recreate, state.options.no_test)
duration = (datetime.now() - start_one).total_seconds()
status_codes[name] = code, duration, [o.elapsed for o in outcomes]
- result_json = getattr(state.options, "result_json", None)
- if result_json is not None:
- with open(result_json, "wt") as file_handler:
- json.dump(state.journal.content, file_handler, indent=2, ensure_ascii=False)
+ write_journal(getattr(state.options, "result_json", None), state.journal)
return report(state.options.start, status_codes, state.options.is_colored)
diff --git a/src/tox/session/cmd/run/single.py b/src/tox/session/cmd/run/single.py
index d83d8dbc..5ee6ac44 100644
--- a/src/tox/session/cmd/run/single.py
+++ b/src/tox/session/cmd/run/single.py
@@ -6,20 +6,11 @@ from typing import List, Tuple, cast
from tox.config.types import Command
from tox.execute.api import Outcome
from tox.tox_env.api import ToxEnv
-from tox.tox_env.errors import Recreate
from tox.tox_env.runner import RunToxEnv
def run_one(tox_env: RunToxEnv, recreate: bool, no_test: bool) -> Tuple[int, List[Outcome]]:
- if recreate:
- tox_env.clean(package_env=recreate)
- try:
- tox_env.setup()
- except Recreate:
- tox_env.clean(package_env=False) # restart creation once, no package please
- tox_env.setup()
- tox_env.setup_done()
-
+ tox_env.ensure_setup(recreate=recreate)
code, outcomes = run_commands(tox_env, no_test)
return code, outcomes
diff --git a/src/tox/tox_env/api.py b/src/tox/tox_env/api.py
index 7a506c85..07167c91 100644
--- a/src/tox/tox_env/api.py
+++ b/src/tox/tox_env/api.py
@@ -15,6 +15,7 @@ from tox.config.sets import ConfigSet
from tox.execute.api import Execute, Outcome
from tox.execute.request import ExecuteRequest
from tox.journal import EnvJournal
+from tox.tox_env.errors import Recreate
from .info import Info
@@ -34,6 +35,8 @@ class ToxEnv(ABC):
self._paths: List[Path] = []
self.logger = logging.getLogger(self.conf["env_name"])
self._env_vars: Optional[Dict[str, str]] = None
+ self.setup_done = False
+ self.clean_done = False
def __repr__(self) -> str:
return f"{self.__class__.__name__}(name={self.conf['env_name']})"
@@ -126,8 +129,22 @@ class ToxEnv(ABC):
env_dir.mkdir(exist_ok=True, parents=True)
finally:
self._handle_env_tmp_dir()
+ self.setup_done, self.clean_done = True, False
- def setup_done(self) -> None:
+ def ensure_setup(self, recreate: bool = False) -> None:
+ if self.setup_done is True:
+ return
+ if recreate:
+ self.clean()
+ try:
+ self.setup()
+ except Recreate:
+ if not recreate:
+ self.clean()
+ self.setup()
+ self.setup_has_been_done()
+
+ def setup_has_been_done(self) -> None:
"""called when setup is done"""
def _handle_env_tmp_dir(self) -> None:
@@ -139,11 +156,14 @@ class ToxEnv(ABC):
env_tmp_dir.mkdir(parents=True)
def clean(self) -> None:
+ if self.clean_done is True:
+ return
env_dir: Path = self.conf["env_dir"]
if env_dir.exists():
logging.info("remove tox env folder %s", env_dir)
shutil.rmtree(env_dir)
self._cache.reset()
+ self.setup_done, self.clean_done = False, True
@property
def environment_variables(self) -> Dict[str, str]:
diff --git a/src/tox/tox_env/package.py b/src/tox/tox_env/package.py
index b6ac8181..ef151eb7 100644
--- a/src/tox/tox_env/package.py
+++ b/src/tox/tox_env/package.py
@@ -2,6 +2,7 @@
A tox environment that can build packages.
"""
from abc import ABC, abstractmethod
+from argparse import ArgumentParser
from pathlib import Path
from typing import TYPE_CHECKING, List, Optional, Set
@@ -9,23 +10,19 @@ from packaging.requirements import Requirement
from tox.config.sets import ConfigSet
from tox.journal import EnvJournal
-from tox.tox_env.errors import Recreate
+from tox.plugin.impl import impl
from .api import ToxEnv
if TYPE_CHECKING:
from tox.config.cli.parser import Parsed
- from tox.tox_env.python.api import Deps
+ from tox.tox_env.python.api import PythonDeps
class PackageToxEnv(ToxEnv, ABC):
def __init__(self, conf: ConfigSet, core: ConfigSet, options: "Parsed", journal: EnvJournal) -> None:
super().__init__(conf, core, options, journal)
- self._cleaned = False
- self._setup_done = False
-
- def register_config(self) -> None:
- super().register_config()
+ self.recreate_package = options.no_recreate_pkg is False if options.recreate else False
@abstractmethod
def get_package_dependencies(self, extras: Optional[Set[str]] = None) -> List[Requirement]:
@@ -35,21 +32,19 @@ class PackageToxEnv(ToxEnv, ABC):
def perform_packaging(self) -> List[Path]:
raise NotImplementedError
+ def package_deps(self) -> "PythonDeps":
+ return []
+
def clean(self) -> None:
- # package environments may be shared clean only once
- if self._cleaned is False:
- self._cleaned = True
+ if self.recreate_package: # only recreate if user did not opt out
super().clean()
- def ensure_setup(self) -> None:
- if self._setup_done is False:
- try:
- self.setup()
- except Recreate:
- self.clean()
- self.setup()
- self.setup_done()
- self._setup_done = True
-
- def package_deps(self) -> "Deps":
- return []
+
+@impl
+def tox_add_option(parser: ArgumentParser) -> None:
+ parser.add_argument(
+ "--no-recreate-pkg",
+ dest="no_recreate_pkg",
+ help="if recreate is set do not recreate packaging tox environment(s)",
+ action="store_true",
+ )
diff --git a/src/tox/tox_env/python/api.py b/src/tox/tox_env/python/api.py
index cbb79765..c459130f 100644
--- a/src/tox/tox_env/python/api.py
+++ b/src/tox/tox_env/python/api.py
@@ -35,7 +35,7 @@ class PythonInfo(NamedTuple):
extra_version_info: Optional[str]
-class Dep:
+class PythonDep:
def __init__(self, value: Union[Path, Requirement]) -> None:
self._value = value
@@ -53,7 +53,7 @@ class Dep:
return not (self == other)
-Deps = Sequence[Dep]
+PythonDeps = Sequence[PythonDep]
class Python(ToxEnv, ABC):
@@ -125,9 +125,9 @@ class Python(ToxEnv, ABC):
self.create_python_env()
self._paths = self.paths()
- def setup_done(self) -> None:
+ def setup_has_been_done(self) -> None:
"""called when setup is done"""
- super().setup_done()
+ super().setup_has_been_done()
if self.journal:
outcome = self.get_installed_packages()
self.journal["installed_packages"] = outcome
@@ -172,19 +172,19 @@ class Python(ToxEnv, ABC):
def _get_python(self, base_python: List[str]) -> Optional[PythonInfo]:
raise NotImplementedError
- def cached_install(self, deps: Deps, section: str, of_type: str) -> bool:
+ def cached_install(self, deps: PythonDeps, section: str, of_type: str) -> bool:
conf_deps: List[str] = [str(i) for i in deps]
with self._cache.compare(conf_deps, section, of_type) as (eq, old):
if eq is True:
return True
if old is None:
old = []
- missing = [Dep(Requirement(i)) for i in (set(old) - set(conf_deps))]
+ missing = [PythonDep(Requirement(i)) for i in (set(old) - set(conf_deps))]
if missing: # no way yet to know what to uninstall here (transitive dependencies?)
# bail out and force recreate
raise Recreate()
new_deps_str = set(conf_deps) - set(old)
- new_deps = [Dep(Requirement(i)) for i in new_deps_str]
+ new_deps = [PythonDep(Requirement(i)) for i in new_deps_str]
self.install_python_packages(packages=new_deps)
return False
@@ -197,7 +197,7 @@ class Python(ToxEnv, ABC):
raise NotImplementedError
@abstractmethod
- def install_python_packages(self, packages: Deps, no_deps: bool = False) -> None:
+ def install_python_packages(self, packages: PythonDeps, no_deps: bool = False) -> None:
raise NotImplementedError
diff --git a/src/tox/tox_env/python/package.py b/src/tox/tox_env/python/package.py
index 21e32508..267b5ceb 100644
--- a/src/tox/tox_env/python/package.py
+++ b/src/tox/tox_env/python/package.py
@@ -10,20 +10,22 @@ from packaging.requirements import Requirement
from tox.config.main import Config
from ..package import PackageToxEnv
-from .api import Dep, NoInterpreter, Python
+from .api import NoInterpreter, Python, PythonDep
class PythonPackage(Python, PackageToxEnv, ABC):
def setup(self) -> None:
"""setup the tox environment"""
super().setup()
- fresh_requires = self.cached_install([Dep(i) for i in self.requires()], PythonPackage.__name__, "requires")
+ fresh_requires = self.cached_install(
+ [PythonDep(i) for i in self.requires()], PythonPackage.__name__, "requires"
+ )
if not fresh_requires:
build_requirements: List[Union[str, Requirement]] = []
with self._cache.compare(build_requirements, PythonPackage.__name__, "build-requires") as (eq, old):
if eq is False and old is None:
build_requirements.extend(self.build_requires())
- new_deps = [Dep(Requirement(i) if isinstance(i, str) else i) for i in set(build_requirements)]
+ new_deps = [PythonDep(Requirement(i) if isinstance(i, str) else i) for i in set(build_requirements)]
self.install_python_packages(packages=new_deps)
def no_base_python_found(self, base_pythons: List[str]) -> NoReturn:
diff --git a/src/tox/tox_env/python/runner.py b/src/tox/tox_env/python/runner.py
index 1bfaae88..bc454a24 100644
--- a/src/tox/tox_env/python/runner.py
+++ b/src/tox/tox_env/python/runner.py
@@ -8,30 +8,40 @@ from typing import Any, Dict, List, NoReturn, Union, cast
from packaging.requirements import Requirement
+from tox.config.cli.parser import Parsed
+from tox.config.sets import ConfigSet
from tox.journal import EnvJournal
from tox.tox_env.errors import Skip
from ..runner import RunToxEnv
-from .api import Dep, NoInterpreter, Python
+from .api import NoInterpreter, Python, PythonDep
class PythonRun(Python, RunToxEnv, ABC):
+ def __init__(self, conf: ConfigSet, core: ConfigSet, options: Parsed, journal: EnvJournal):
+ super().__init__(conf, core, options, journal)
+ self._packages: List[PythonDep] = []
+
def register_config(self) -> None:
super().register_config()
outer_self = self
- class _Dep(Dep):
- def __init__(self, raw: Any) -> None:
- if not raw.startswith("-r"):
- val: Union[Path, Requirement] = Requirement(raw)
+ class _PythonDep(PythonDep):
+ def __init__(self, raw: Union[PythonDep, str]) -> None:
+ if isinstance(raw, str):
+ if raw.startswith("-r"):
+ val: Union[Path, Requirement] = Path(raw[2:])
+ if not cast(Path, val).is_absolute():
+ val = outer_self.core["toxinidir"] / val
+ else:
+ val = Requirement(raw)
else:
- path = Path(raw[2:])
- val = path if path.is_absolute() else cast(Path, outer_self.core["toxinidir"]) / path
+ val = raw.value
super().__init__(val)
self.conf.add_config(
keys="deps",
- of_type=List[_Dep],
+ of_type=List[_PythonDep],
default=[],
desc="Name of the python dependencies as specified by PEP-440",
)
@@ -51,26 +61,38 @@ class PythonRun(Python, RunToxEnv, ABC):
"""setup the tox environment"""
super().setup()
self.install_deps()
-
if self.package_env is not None:
package_deps = self.package_env.get_package_dependencies(self.conf["extras"])
- self.cached_install([Dep(p) for p in package_deps], PythonRun.__name__, "package_deps")
+ self.cached_install([PythonDep(p) for p in package_deps], PythonRun.__name__, "package_deps")
self.install_package()
def install_deps(self) -> None:
self.cached_install(self.conf["deps"], PythonRun.__name__, "deps")
def install_package(self) -> None:
- if self.package_env is not None:
- package: List[Dep] = [Dep(p) for p in self.package_env.perform_packaging()]
- else:
- package = [Dep(d) for d in self.get_pkg_no_env()] if self.has_package else []
+ package = self.get_package()
if package:
self.install_python_packages(package, **self.install_package_args()) # type: ignore[no-untyped-call]
self.handle_journal_package(self.journal, package)
+ def get_package(self) -> List[PythonDep]:
+ if self.package_env is not None:
+ package: List[PythonDep] = [PythonDep(p) for p in self.package_env.perform_packaging()]
+ else:
+ package = [PythonDep(d) for d in self.get_pkg_no_env()] if self.has_package else []
+ self._packages = package
+ return package
+
+ @abstractmethod
+ def install_package_args(self) -> Dict[str, Any]:
+ raise NotImplementedError
+
+ @property
+ def packages(self) -> List[str]:
+ return [str(d.value) for d in self._packages]
+
@staticmethod
- def handle_journal_package(journal: EnvJournal, package: List[Dep]) -> None:
+ def handle_journal_package(journal: EnvJournal, package: List[PythonDep]) -> None:
if not journal:
return
installed_meta = []
@@ -88,7 +110,3 @@ class PythonRun(Python, RunToxEnv, ABC):
def get_pkg_no_env(self) -> List[Path]:
# by default in Python just forward the root folder to the installer
return [cast(Path, self.core["tox_root"])]
-
- @abstractmethod
- def install_package_args(self) -> Dict[str, Any]:
- raise NotImplementedError
diff --git a/src/tox/tox_env/python/virtual_env/api.py b/src/tox/tox_env/python/virtual_env/api.py
index d6f865e5..27b0023d 100644
--- a/src/tox/tox_env/python/virtual_env/api.py
+++ b/src/tox/tox_env/python/virtual_env/api.py
@@ -15,7 +15,7 @@ from tox.execute.api import Execute, Outcome
from tox.execute.local_sub_process import LocalSubProcessExecutor
from tox.journal import EnvJournal
-from ..api import Deps, Python, PythonInfo
+from ..api import Python, PythonDeps, PythonInfo
class VirtualEnv(Python, ABC):
@@ -89,7 +89,7 @@ class VirtualEnv(Python, ABC):
def install_python_packages(
self,
- packages: Deps,
+ packages: PythonDeps,
no_deps: bool = False,
develop: bool = False,
force_reinstall: bool = False,
diff --git a/src/tox/tox_env/python/virtual_env/package/artifact/dev.py b/src/tox/tox_env/python/virtual_env/package/artifact/dev.py
index 60218da4..fc4aed26 100644
--- a/src/tox/tox_env/python/virtual_env/package/artifact/dev.py
+++ b/src/tox/tox_env/python/virtual_env/package/artifact/dev.py
@@ -2,7 +2,7 @@ from pathlib import Path
from typing import List, cast
from tox.plugin.impl import impl
-from tox.tox_env.python.api import Dep, Deps
+from tox.tox_env.python.api import PythonDep, PythonDeps
from tox.tox_env.register import ToxEnvRegister
from ..api import Pep517VirtualEnvPackage
@@ -19,9 +19,9 @@ class LegacyDevVirtualEnvPackage(Pep517VirtualEnvPackage):
"""The root folder itself is the package to install"""
return [cast(Path, self.core["tox_root"])]
- def package_deps(self) -> Deps:
+ def package_deps(self) -> PythonDeps:
"""Install requirement from pyproject.toml table"""
- return [Dep(i) for i in self._requires]
+ return [PythonDep(i) for i in self._requires]
@impl
diff --git a/src/tox/tox_env/runner.py b/src/tox/tox_env/runner.py
index f5a00e70..9abb1d48 100644
--- a/src/tox/tox_env/runner.py
+++ b/src/tox/tox_env/runner.py
@@ -1,6 +1,7 @@
-from abc import ABC
+import os
+from abc import ABC, abstractmethod
from pathlib import Path
-from typing import TYPE_CHECKING, Generator, List, Optional, Tuple, cast
+from typing import TYPE_CHECKING, Dict, Generator, List, Optional, Tuple, cast
from tox.config.sets import ConfigSet
from tox.config.types import Command, EnvList
@@ -67,6 +68,12 @@ class RunToxEnv(ToxEnv, ABC):
def add_package_conf(self) -> bool:
"""If this returns True package_env and package_tox_env_type configurations must be defined"""
+ self.core.add_config(
+ keys=["no_package", "skipsdist"],
+ of_type=bool,
+ default=False,
+ desc="Is there any packaging involved in this project.",
+ )
core_no_package: bool = self.core["no_package"]
if core_no_package is True:
return False
@@ -93,3 +100,18 @@ class RunToxEnv(ToxEnv, ABC):
super().clean()
if self.package_env:
self.package_env.clean()
+
+ @property
+ def environment_variables(self) -> Dict[str, str]:
+ environment_variables = super().environment_variables
+ if self.has_package: # if package(s) have been built insert them as environment variable
+ packages = self.packages
+ if packages:
+ environment_variables["TOX_PACKAGE"] = os.pathsep.join(packages)
+ return environment_variables
+
+ @property
+ @abstractmethod
+ def packages(self) -> List[str]:
+ """:returns: a list of packages installed in the environment"""
+ raise NotImplementedError
diff --git a/tests/config/cli/test_cli_env_var.py b/tests/config/cli/test_cli_env_var.py
index 957c983b..a6d989de 100644
--- a/tests/config/cli/test_cli_env_var.py
+++ b/tests/config/cli/test_cli_env_var.py
@@ -36,6 +36,7 @@ def test_verbose_no_test(monkeypatch: MonkeyPatch) -> None:
"override": [],
"show_config": False,
"list_envs_all": False,
+ "no_recreate_pkg": False,
"list_envs": False,
"devenv_path": None,
"config_file": "",
@@ -86,6 +87,7 @@ def test_env_var_exhaustive_parallel_values(
"installpkg": None,
"list_envs": False,
"list_envs_all": False,
+ "no_recreate_pkg": False,
"no_test": True,
"override": [Override("a=b"), Override("c=d")],
"package_only": False,
diff --git a/tests/config/cli/test_cli_ini.py b/tests/config/cli/test_cli_ini.py
index 374db8e7..5d3afde4 100644
--- a/tests/config/cli/test_cli_ini.py
+++ b/tests/config/cli/test_cli_ini.py
@@ -89,6 +89,7 @@ def default_options() -> Dict[str, Any]:
"result_json": None,
"skip_missing_interpreters": "config",
"verbose": 2,
+ "no_recreate_pkg": False,
"work_dir": Path.cwd().absolute(),
}
@@ -107,6 +108,7 @@ def test_ini_exhaustive_parallel_values(exhaustive_ini: Path, core_handlers: Dic
"no_test": True,
"override": [Override("a=b"), Override("c=d")],
"package_only": False,
+ "no_recreate_pkg": False,
"parallel": 3,
"parallel_live": True,
"quiet": 1,
diff --git a/tests/config/loader/ini/replace/test_replace_tox_env.py b/tests/config/loader/ini/replace/test_replace_tox_env.py
index de3562a4..75791f59 100644
--- a/tests/config/loader/ini/replace/test_replace_tox_env.py
+++ b/tests/config/loader/ini/replace/test_replace_tox_env.py
@@ -135,4 +135,4 @@ def test_replace_from_section_bad_type(tox_ini_conf: ToxIniCreator) -> None:
def test_replace_from_tox_section_registered(tox_ini_conf: ToxIniCreator, tmp_path: Path) -> None:
conf_a = tox_ini_conf("[testenv:a]\nx = {[tox]tox_root}").get_env("a")
conf_a.add_config(keys="x", of_type=Path, default=Path.cwd() / "magic", desc="d")
- assert conf_a["x"] == tmp_path
+ assert conf_a["x"] == (tmp_path / "c")
diff --git a/tests/config/test_main.py b/tests/config/test_main.py
index 81ed88fa..8976d236 100644
--- a/tests/config/test_main.py
+++ b/tests/config/test_main.py
@@ -5,7 +5,6 @@ from tox.config.loader.api import Override
from tox.config.loader.memory import MemoryLoader
from tox.config.main import Config
from tox.config.sets import ConfigSet
-from tox.pytest import ToxProject
@pytest.fixture
@@ -13,13 +12,9 @@ def empty_config(tox_ini_conf: ToxIniCreator) -> Config:
return tox_ini_conf("")
-def test_empty_config_root(empty_config: Config, empty_project: ToxProject) -> None:
- assert empty_config.core["tox_root"] == empty_project.path
-
-
-def test_empty_config_repr(empty_config: Config, empty_project: ToxProject) -> None:
+def test_empty_config_repr(empty_config: Config) -> None:
text = repr(empty_config)
- assert str(empty_project.path) in text
+ assert str(empty_config.core["tox_root"]) in text
assert "config_source=ToxIni" in text
diff --git a/tests/conftest.py b/tests/conftest.py
index d445332a..1bdd429a 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -11,6 +11,7 @@ from tox.config.main import Config
from tox.run import make_config
pytest_plugins = "tox.pytest"
+HERE = Path(__file__).absolute().parent
@pytest.fixture(scope="session")
@@ -35,9 +36,21 @@ class ToxIniCreator(Protocol):
@pytest.fixture
def tox_ini_conf(tmp_path: Path, monkeypatch: MonkeyPatch) -> ToxIniCreator:
def func(conf: str, override: Optional[Sequence[Override]] = None) -> Config:
- (tmp_path / "tox.ini").write_bytes(conf.encode("utf-8"))
+ dest = tmp_path / "c"
+ dest.mkdir()
+ (dest / "tox.ini").write_bytes(conf.encode("utf-8"))
with monkeypatch.context() as context:
context.chdir(tmp_path)
- return make_config(Parsed(work_dir=tmp_path, override=override or []), pos_args=[])
+ return make_config(Parsed(work_dir=dest, override=override or []), pos_args=[])
return func
+
+
+@pytest.fixture(scope="session")
+def demo_pkg_setuptools() -> Path:
+ return HERE / "demo_pkg_setuptools"
+
+
+@pytest.fixture(scope="session")
+def demo_pkg_inline() -> Path:
+ return HERE / "demo_pkg_inline"
diff --git a/tests/demo_pkg_inline/build.py b/tests/demo_pkg_inline/build.py
new file mode 100644
index 00000000..7515fc0b
--- /dev/null
+++ b/tests/demo_pkg_inline/build.py
@@ -0,0 +1,53 @@
+import sys
+from pathlib import Path
+from textwrap import dedent
+from zipfile import ZipFile
+
+name = "demo_pkg_inline"
+pkg_name = name.replace("_", "-")
+
+version = "1.0.0"
+dist_info = f"{name}-{version}.dist-info"
+
+content = {
+ f"{name}/__init__.py": f"def do():\nprint('greetings from {name}')",
+ f"{dist_info}/METADATA": f"""
+ Metadata-Version: 2.1
+ Name: {pkg_name}
+ Version: {version}
+ Summary: UNKNOWN
+ Home-page: UNKNOWN
+ Author: UNKNOWN
+ Author-email: UNKNOWN
+ License: UNKNOWN
+ Platform: UNKNOWN
+
+ UNKNOWN
+ """,
+ f"{dist_info}/WHEEL": f"""
+ Wheel-Version: 1.0
+ Generator: {name}-{version}
+ Root-Is-Purelib: true
+ Tag: py3-none-any
+ """,
+ f"{dist_info}/top_level.txt": name,
+ f"{dist_info}/RECORD": f"""
+ {name}/__init__.py,,
+ {dist_info}/METADATA,,
+ {dist_info}/WHEEL,,
+ {dist_info}/top_level.txt,,
+ {dist_info}/RECORD,,
+ """,
+}
+
+
+def build_wheel(wheel_directory, metadata_directory=None, config_settings=None):
+ path = Path(wheel_directory) / f"{name}-{version}-py{sys.version_info.major}-none-any.whl"
+ with ZipFile(str(path), "w") as zip_file_handler:
+ for arc_name, data in content.items():
+ zip_file_handler.writestr(zinfo_or_arcname=arc_name, data=dedent(data).strip())
+ print(f"created wheel {path}")
+
+
+def get_requires_for_build_wheel(config_settings):
+ return []
diff --git a/tests/demo_pkg_inline/pyproject.toml b/tests/demo_pkg_inline/pyproject.toml
new file mode 100644
index 00000000..a28fa49a
--- /dev/null
+++ b/tests/demo_pkg_inline/pyproject.toml
@@ -0,0 +1,4 @@
+[build-system]
+requires=[]
+build-backend="build"
+backend-path=["."]
diff --git a/tests/demo_pkg_setuptools/demo_pkg_setuptools/__init__.py b/tests/demo_pkg_setuptools/demo_pkg_setuptools/__init__.py
new file mode 100644
index 00000000..694740b1
--- /dev/null
+++ b/tests/demo_pkg_setuptools/demo_pkg_setuptools/__init__.py
@@ -0,0 +1,2 @@
+def do():
+ print("greetings from demo_pkg_setuptools")
diff --git a/tests/demo_pkg_setuptools/pyproject.toml b/tests/demo_pkg_setuptools/pyproject.toml
new file mode 100644
index 00000000..7fd4fb51
--- /dev/null
+++ b/tests/demo_pkg_setuptools/pyproject.toml
@@ -0,0 +1,3 @@
+[build-system]
+requires = ["setuptools>=45", "wheel>=0.33"]
+build-backend = 'setuptools.build_meta'
diff --git a/tests/demo_pkg_setuptools/setup.cfg b/tests/demo_pkg_setuptools/setup.cfg
new file mode 100644
index 00000000..80e42548
--- /dev/null
+++ b/tests/demo_pkg_setuptools/setup.cfg
@@ -0,0 +1,6 @@
+[metadata]
+name = demo_pkg_setuptools
+version = 1.2.3
+
+[options]
+packages = find:
diff --git a/tests/pytest_/test_init.py b/tests/pytest_/test_init.py
index b615c799..08e14165 100644
--- a/tests/pytest_/test_init.py
+++ b/tests/pytest_/test_init.py
@@ -1,6 +1,7 @@
import os
import sys
from itertools import chain, combinations
+from pathlib import Path
from textwrap import dedent
from typing import List, Sequence
@@ -12,7 +13,7 @@ from tox.pytest import MonkeyPatch, ToxProjectCreator, check_os_environ
from tox.report import HandledError
-def test_init_base(tox_project: ToxProjectCreator) -> None:
+def test_tox_project_no_base(tox_project: ToxProjectCreator) -> None:
project = tox_project(
{
"tox.ini": "[tox]",
@@ -27,6 +28,14 @@ def test_init_base(tox_project: ToxProjectCreator) -> None:
}
+def test_tox_project_base(tmp_path: Path, tox_project: ToxProjectCreator) -> None:
+ base = tmp_path / "base"
+ base.mkdir()
+ (base / "out").write_text("a")
+ project = tox_project({"tox.ini": "[tox]"}, base=base)
+ assert project.structure
+
+
COMB = list(chain.from_iterable(combinations(["DIFF", "MISS", "EXTRA"], i) for i in range(4)))
@@ -98,7 +107,6 @@ def test_tox_run_outcome_repr(tox_project: ToxProjectCreator) -> None:
min_version = {__version__}
provision_tox_env = .tox
requires = tox>={__version__}
- no_package = False
"""
).lstrip()
assert repr(outcome) == exp
diff --git a/tests/session/cmd/run/test_int_setuptools.py b/tests/session/cmd/run/test_int_setuptools.py
deleted file mode 100644
index 2201284d..00000000
--- a/tests/session/cmd/run/test_int_setuptools.py
+++ /dev/null
@@ -1,52 +0,0 @@
-import os
-
-import pytest
-
-from tox.pytest import ToxProjectCreator
-
-
-@pytest.mark.timeout(20)
-@pytest.mark.integration
-def test_setuptools_package_py_project(tox_project: ToxProjectCreator) -> None:
- project = tox_project(
- {
- "tox.ini": """
- [tox]
- env_list = py
-
- [testenv]
- commands_pre =
- python -c 'import sys; print("start", sys.executable)'
- commands =
- python -c 'import magic; print(magic.__version__)'
- commands_post =
- python -c 'import sys; print("end", sys.executable)'
- package = wheel
- """,
- "setup.cfg": """
- [metadata]
- name = magic
- version = 1.2.3
- [options]
- packages = find:
- package_dir =
- =src
- [options.packages.find]
- where = src
- [bdist_wheel]
- universal = 1
- """,
- "pyproject.toml": """
- [build-system]
- requires = [
- "setuptools >= 40.0.4",
- "wheel >= 0.29.0",
- ]
- build-backend = 'setuptools.build_meta'
- """,
- "src": {"magic": {"__init__.py": """__version__ = "1.2.3" """}},
- },
- )
- outcome = project.run("-vv", "r", "-e", "py")
- outcome.assert_success()
- assert f"\n1.2.3{os.linesep}" in outcome.out
diff --git a/tests/session/cmd/test_show_config.py b/tests/session/cmd/test_show_config.py
index d8fd5ee2..610e05ee 100644
--- a/tests/session/cmd/test_show_config.py
+++ b/tests/session/cmd/test_show_config.py
@@ -24,7 +24,6 @@ def test_list_empty(tox_project: ToxProjectCreator) -> None:
min_version = {__version__}
provision_tox_env = .tox
requires = tox>={__version__}
- no_package = False
""",
).lstrip()
assert outcome.out == expected
diff --git a/tests/test_provision.py b/tests/test_provision.py
new file mode 100644
index 00000000..680927f5
--- /dev/null
+++ b/tests/test_provision.py
@@ -0,0 +1,106 @@
+import json
+import os
+import sys
+from pathlib import Path
+from subprocess import check_call
+from typing import List, Optional
+from zipfile import ZipFile
+
+import pytest
+from packaging.requirements import Requirement
+
+from tox.pytest import Index, IndexServer, MonkeyPatch, TempPathFactory, ToxProjectCreator
+
+if sys.version_info >= (3, 8): # pragma: no cover (py38+)
+ from importlib.metadata import Distribution # type: ignore[attr-defined]
+else: # pragma: no cover (<py38)
+ from importlib_metadata import Distribution # noqa
+
+ROOT = Path(__file__).parents[1]
+
+
+@pytest.fixture(scope="session")
+def tox_wheel(tmp_path_factory: TempPathFactory) -> Path:
+ # takes around 3.2s
+ package: Optional[Path] = None
+ if "TOX_PACKAGE" in os.environ:
+ env_tox_pkg = Path(os.environ["TOX_PACKAGE"])
+ if env_tox_pkg.exists() and env_tox_pkg.suffix == ".whl":
+ package = env_tox_pkg
+ if package is None: # pragma: no cover
+ # when we don't get a wheel path injected, build it (for example when running from an IDE)
+ package = build_wheel(tmp_path_factory.mktemp("dist"), Path(__file__).parents[1])
+ return package
+
+
+@pytest.fixture(scope="session")
+def tox_wheels(tox_wheel: Path, tmp_path_factory: TempPathFactory) -> List[Path]:
+ # takes around 1.5s if already cached
+ result: List[Path] = [tox_wheel]
+ info = tmp_path_factory.mktemp("info")
+ with ZipFile(str(tox_wheel), "r") as zip_file:
+ zip_file.extractall(path=info)
+ dist_info = next((i for i in info.iterdir() if i.suffix == ".dist-info"), None)
+ if dist_info is None: # pragma: no cover
+ raise RuntimeError(f"no tox.dist-info inside {tox_wheel}")
+ distribution = Distribution.at(dist_info)
+ wheel_cache = ROOT / ".wheel_cache" / f"{sys.version_info.major}.{sys.version_info.minor}"
+ wheel_cache.mkdir(parents=True, exist_ok=True)
+ cmd = [sys.executable, "-I", "-m", "pip", "--disable-pip-version-check", "download", "-d", str(wheel_cache)]
+ for req in distribution.requires:
+ requirement = Requirement(req)
+ if not requirement.extras: # pragma: no branch # we don't need to install any extras (tests/docs/etc)
+ cmd.append(req)
+ check_call(cmd)
+ result.extend(wheel_cache.iterdir())
+ return result
+
+
+@pytest.fixture(scope="session")
+def demo_pkg_inline_wheel(tmp_path_factory: TempPathFactory, demo_pkg_inline: Path) -> Path:
+ return build_wheel(tmp_path_factory.mktemp("dist"), demo_pkg_inline)
+
+
+def build_wheel(dist_dir: Path, of: Path) -> Path:
+ from build.__main__ import build_package # noqa
+
+ build_package(str(of), str(dist_dir), distributions=["wheel"])
+ package = next(dist_dir.iterdir())
+ return package
+
+
+@pytest.fixture(scope="session")
+def pypi_index_self(pypi_server: IndexServer, tox_wheels: List[Path], demo_pkg_inline_wheel: Path) -> Index:
+ # takes around 1s
+ self_index = pypi_server.create_index("self", "volatile=False")
+ self_index.upload(tox_wheels + [demo_pkg_inline_wheel])
+ return self_index
+
+
+def test_provision_requires_nok(tox_project: ToxProjectCreator) -> None:
+ ini = "[tox]\nrequires = pkg-does-not-exist\n setuptools==1\nskipsdist=true\n"
+ outcome = tox_project({"tox.ini": ini}).run("c", "-e", "py")
+ outcome.assert_failed()
+ outcome.assert_out_err(
+ r".*will run in automatically provisioned tox, host .* is missing \[requires \(has\)\]:"
+ r" pkg-does-not-exist \(N/A\), setuptools==1 \(.*\).*",
+ r".*",
+ regex=True,
+ )
+
+
+@pytest.mark.integration
+@pytest.mark.timeout(60)
+def test_provision_requires_ok(
+ tox_project: ToxProjectCreator, pypi_index_self: Index, monkeypatch: MonkeyPatch, tmp_path: Path
+) -> None:
+ log = tmp_path / "out.log"
+ pypi_index_self.use(monkeypatch)
+ ini = "[tox]\nrequires = demo-pkg-inline\n setuptools \n[testenv]\npackage=skip"
+
+ outcome = tox_project({"tox.ini": ini}).run("r", "-e", "py", "--result-json", str(log))
+
+ outcome.assert_success()
+ with log.open("rt") as file_handler:
+ log_report = json.load(file_handler)
+ assert "py" in log_report["testenvs"]
diff --git a/tests/tox_env/python/test_python_api.py b/tests/tox_env/python/test_python_api.py
index 8ce76e48..6f66756d 100644
--- a/tests/tox_env/python/test_python_api.py
+++ b/tests/tox_env/python/test_python_api.py
@@ -2,28 +2,28 @@ from pathlib import Path
from packaging.requirements import Requirement
-from tox.tox_env.python.api import Dep
+from tox.tox_env.python.api import PythonDep
def test_deps_path_eq() -> None:
- dep_1 = Dep(Path.cwd())
- dep_2 = Dep(Path.cwd())
+ dep_1 = PythonDep(Path.cwd())
+ dep_2 = PythonDep(Path.cwd())
assert dep_1 == dep_2
def test_deps_path_ne() -> None:
- dep_1 = Dep(Path.cwd())
- dep_2 = Dep(Path.cwd() / "a")
+ dep_1 = PythonDep(Path.cwd())
+ dep_2 = PythonDep(Path.cwd() / "a")
assert dep_1 != dep_2
def test_deps_req_eq() -> None:
- dep_1 = Dep(Requirement("pytest"))
- dep_2 = Dep(Requirement("pytest"))
+ dep_1 = PythonDep(Requirement("pytest"))
+ dep_2 = PythonDep(Requirement("pytest"))
assert dep_1 == dep_2
def test_deps_req_ne() -> None:
- dep_1 = Dep(Requirement("pytest"))
- dep_2 = Dep(Requirement("tox"))
+ dep_1 = PythonDep(Requirement("pytest"))
+ dep_2 = PythonDep(Requirement("tox"))
assert dep_1 != dep_2
diff --git a/tests/tox_env/python/test_python_runner.py b/tests/tox_env/python/test_python_runner.py
index 3507f74d..0f3662a0 100644
--- a/tests/tox_env/python/test_python_runner.py
+++ b/tests/tox_env/python/test_python_runner.py
@@ -4,7 +4,7 @@ from packaging.requirements import Requirement
from tox.journal import EnvJournal
from tox.pytest import ToxProjectCreator
-from tox.tox_env.python.api import Dep
+from tox.tox_env.python.api import PythonDep
from tox.tox_env.python.runner import PythonRun
@@ -33,7 +33,7 @@ def test_journal_one_wheel_file(tmp_path: Path) -> None:
wheel.write_bytes(b"magical")
journal = EnvJournal(enabled=True, name="a")
- PythonRun.handle_journal_package(journal, [Dep(wheel)])
+ PythonRun.handle_journal_package(journal, [PythonDep(wheel)])
content = journal.content
assert content == {
@@ -52,7 +52,7 @@ def test_journal_multiple_wheel_file(tmp_path: Path) -> None:
wheel_2.write_bytes(b"magic")
journal = EnvJournal(enabled=True, name="a")
- PythonRun.handle_journal_package(journal, [Dep(wheel_1), Dep(wheel_2)])
+ PythonRun.handle_journal_package(journal, [PythonDep(wheel_1), PythonDep(wheel_2)])
content = journal.content
assert content == {
@@ -74,7 +74,7 @@ def test_journal_multiple_wheel_file(tmp_path: Path) -> None:
def test_journal_packge_dir(tmp_path: Path) -> None:
journal = EnvJournal(enabled=True, name="a")
- PythonRun.handle_journal_package(journal, [Dep(tmp_path)])
+ PythonRun.handle_journal_package(journal, [PythonDep(tmp_path)])
content = journal.content
assert content == {
@@ -88,7 +88,7 @@ def test_journal_packge_dir(tmp_path: Path) -> None:
def test_journal_package_requirement(tmp_path: Path) -> None:
journal = EnvJournal(enabled=True, name="a")
- PythonRun.handle_journal_package(journal, [Dep(Requirement("pytest"))])
+ PythonRun.handle_journal_package(journal, [PythonDep(Requirement("pytest"))])
content = journal.content
assert content == {}
diff --git a/tests/tox_env/python/virtual_env/setuptools/__init__.py b/tests/tox_env/python/virtual_env/setuptools/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/tests/tox_env/python/virtual_env/setuptools/__init__.py
+++ /dev/null
diff --git a/tests/tox_env/python/virtual_env/setuptools/package/__init__.py b/tests/tox_env/python/virtual_env/setuptools/package/__init__.py
deleted file mode 100644
index e69de29b..00000000
--- a/tests/tox_env/python/virtual_env/setuptools/package/__init__.py
+++ /dev/null
diff --git a/tests/tox_env/python/virtual_env/setuptools/package/test_wheel_build.py b/tests/tox_env/python/virtual_env/setuptools/package/test_wheel_build.py
deleted file mode 100644
index c147af23..00000000
--- a/tests/tox_env/python/virtual_env/setuptools/package/test_wheel_build.py
+++ /dev/null
@@ -1,87 +0,0 @@
-import sys
-from pathlib import Path
-from typing import List, Sequence
-
-import pytest
-import setuptools
-import wheel
-
-from tox.execute.api import Outcome
-from tox.execute.request import ExecuteRequest
-from tox.pytest import MonkeyPatch, ToxProjectCreator
-from tox.tox_env.python.virtual_env.api import VirtualEnv
-from tox.tox_env.python.virtual_env.package.artifact.wheel import Pep517VirtualEnvPackageWheel
-
-
-@pytest.fixture()
-def use_host_virtualenv(monkeypatch: MonkeyPatch) -> None:
- # disable install
- def perform_install(self: VirtualEnv, install_command: Sequence[str]) -> Outcome:
- install_command = ("python", "-c", "import sys; print(sys.argv)") + tuple(install_command)
- return old_cmd(self, install_command)
-
- old_cmd = VirtualEnv.perform_install
- monkeypatch.setattr(VirtualEnv, "perform_install", perform_install)
-
- # return hots path
- def paths(self: VirtualEnv) -> List[Path]:
- return [Path(sys.executable).parent]
-
- monkeypatch.setattr(VirtualEnv, "paths", paths)
-
- # return hots path
- def create_python_env(self: VirtualEnv) -> Outcome:
- return Outcome(ExecuteRequest(["a"], Path(), {}, False), False, Outcome.OK, "", "", 0, 1.0, ["a"])
-
- monkeypatch.setattr(VirtualEnv, "create_python_env", create_python_env)
-
-
-def test_setuptools_package_wheel_universal(tox_project: ToxProjectCreator, use_host_virtualenv: None) -> None:
- project = tox_project(
- {
- "tox.ini": """
- [tox]
- env_list = py
-
- [testenv]
- package = wheel
- package_env = .package
- """,
- "setup.cfg": """
- [metadata]
- name = magic
- version = 1.2.3
- [options]
- packages = find:
- package_dir =
- =src
- [options.packages.find]
- where = src
- """,
- "pyproject.toml": f"""
- [build-system]
- requires = [
- "setuptools >= {setuptools.__version__}",
- "wheel >= {wheel.__version__}",
- ]
- build-backend = 'setuptools.build_meta'
- """,
- "src": {"magic": {"__init__.py": """__version__ = "1.2.3" """}},
- },
- )
- outcome = project.run("r")
- tox_env = outcome.state.tox_env("py")
- package_env = tox_env.package_env
- assert isinstance(package_env, Pep517VirtualEnvPackageWheel)
- packages = package_env.perform_packaging()
- assert len(packages) == 1
- package = packages[0]
- assert package.name == "magic-1.2.3-py3-none-any.whl"
-
- result = outcome.out.split("\n")
- py_messages = [i for i in result if "py: " in i]
- assert len(py_messages) == 2 # 1 install wheel + 1 report
-
- package_messages = [i for i in result if ".package: " in i]
- # 1 install requires + 1 build requires + 1 build meta + 1 build isolated
- assert len(package_messages) == 4
diff --git a/tests/tox_env/python/virtual_env/test_setuptools.py b/tests/tox_env/python/virtual_env/test_setuptools.py
index 8dcd0022..b18035b1 100644
--- a/tests/tox_env/python/virtual_env/test_setuptools.py
+++ b/tests/tox_env/python/virtual_env/test_setuptools.py
@@ -1,24 +1,48 @@
-from tox.pytest import ToxProjectCreator
+import os
+import sys
+from pathlib import Path
+from typing import Optional
+
+import pytest
+from tox.pytest import ToxProjectCreator
+from tox.tox_env.python.virtual_env.package.artifact.wheel import Pep517VirtualEnvPackageWheel
-def test_setuptools_project_no_package(tox_project: ToxProjectCreator) -> None:
- project = tox_project(
- {
- "tox.ini": """
- [tox]
- env_list = py
- no_package = true
+@pytest.mark.timeout(30)
+@pytest.mark.integration
+def test_setuptools_package(
+ tox_project: ToxProjectCreator,
+ demo_pkg_setuptools: Path,
+ enable_pip_pypi_access: Optional[str], # noqa
+) -> None:
+ tox_ini = """
[testenv]
- deps = pip
- commands_pre =
- python -c 'import sys; print("start", sys.executable)'
- commands =
- python -c 'import sys; print("do", sys.executable)'
- commands_post =
- python -c 'import sys; print("end", sys.executable)'
- """,
- },
- )
- outcome = project.run("-e", "py")
+ package = wheel
+ package_env = .package
+ commands_pre = python -c 'import sys; print("start", sys.executable)'
+ commands = python -c 'from demo_pkg_setuptools import do; do()'
+ commands_post = python -c 'import sys; print("end", sys.executable)'
+ """
+ project = tox_project({"tox.ini": tox_ini}, base=demo_pkg_setuptools)
+
+ outcome = project.run("r", "-e", "py")
+
outcome.assert_success()
+ assert f"\ngreetings from demo_pkg_setuptools{os.linesep}" in outcome.out
+ tox_env = outcome.state.tox_env("py")
+
+ package_env = tox_env.package_env
+ assert isinstance(package_env, Pep517VirtualEnvPackageWheel)
+ packages = package_env.perform_packaging()
+ assert len(packages) == 1
+ package = packages[0]
+ assert package.name == f"demo_pkg_setuptools-1.2.3-py{sys.version_info.major}-none-any.whl"
+
+ result = outcome.out.split("\n")
+ py_messages = [i for i in result if "py: " in i]
+ assert len(py_messages) == 5, "\n".join(py_messages) # 1 install wheel + 3 command + 1 reports
+
+ package_messages = [i for i in result if ".package: " in i]
+ # 1 install requires + 1 build requires + 1 build meta + 1 build isolated
+ assert len(package_messages) == 4, "\n".join(package_messages)
diff --git a/tox.ini b/tox.ini
index f1cea781..1d9ff58c 100644
--- a/tox.ini
+++ b/tox.ini
@@ -11,7 +11,7 @@ envlist =
pkg_meta
isolated_build = true
skip_missing_interpreters = true
-minversion = 3.14.0
+minversion = 3.14
[testenv]
description = run the tests with pytest
@@ -32,7 +32,7 @@ commands =
--cov-report html:{envtmpdir}/htmlcov \
--cov-report xml:{toxworkdir}/coverage.{envname}.xml \
-n={env:PYTEST_XDIST_PROC_NR:auto} \
- tests --timeout 10 --durations 5 --run-integration}
+ tests --timeout 20 --durations 5 --run-integration}
[testenv:fix]
description = format the code base to adhere to our styles, and complain about what we cannot do automatically