summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/tox/_pytestplugin.py17
-rwxr-xr-xsrc/tox/config.py23
-rw-r--r--src/tox/package.py135
-rw-r--r--src/tox/session.py8
-rwxr-xr-xsrc/tox/venv.py21
5 files changed, 185 insertions, 19 deletions
diff --git a/src/tox/_pytestplugin.py b/src/tox/_pytestplugin.py
index 321d511b..9e218cde 100644
--- a/src/tox/_pytestplugin.py
+++ b/src/tox/_pytestplugin.py
@@ -277,7 +277,7 @@ def initproj(tmpdir):
setup.py
"""
- def initproj_(nameversion, filedefs=None, src_root="."):
+ def initproj_(nameversion, filedefs=None, src_root=".", add_missing_setup_py=True):
if filedefs is None:
filedefs = {}
if not src_root:
@@ -297,7 +297,7 @@ def initproj(tmpdir):
base.ensure(dir=1)
create_files(base, filedefs)
- if not _filedefs_contains(base, filedefs, "setup.py"):
+ if not _filedefs_contains(base, filedefs, "setup.py") and add_missing_setup_py:
create_files(
base,
{
@@ -319,7 +319,18 @@ def initproj(tmpdir):
)
if not _filedefs_contains(base, filedefs, src_root_path.join(name)):
create_files(
- src_root_path, {name: {"__init__.py": "__version__ = {!r}".format(version)}}
+ src_root_path,
+ {
+ name: {
+ "__init__.py": textwrap.dedent(
+ '''
+ """ module {} """
+ __version__ = {!r}'''
+ )
+ .strip()
+ .format(name, version)
+ }
+ },
)
manifestlines = [
"include {}".format(p.relto(base)) for p in base.visit(lambda x: x.check(file=1))
diff --git a/src/tox/config.py b/src/tox/config.py
index 891eb0a9..70dcb425 100755
--- a/src/tox/config.py
+++ b/src/tox/config.py
@@ -974,7 +974,8 @@ class parseini:
config.logdir = config.toxworkdir.join("log")
self._make_thread_safe_path(config, "logdir", unique_id)
- config.envlist, all_envs = self._getenvdata(reader)
+ self.parse_build_isolation(config, reader)
+ config.envlist, all_envs = self._getenvdata(reader, config)
# factors used in config or predefined
known_factors = self._list_section_factors("testenv")
@@ -1006,6 +1007,16 @@ class parseini:
config.skipsdist = reader.getbool("skipsdist", all_develop)
+ def parse_build_isolation(self, config, reader):
+ config.isolated_build = reader.getbool("isolated_build", False)
+ config.isolated_build_env = reader.getstring("isolated_build_env", ".package")
+ if config.isolated_build is True:
+ name = config.isolated_build_env
+ if name not in config.envconfigs:
+ config.envconfigs[name] = self.make_envconfig(
+ name, testenvprefix + name, reader._subs, config
+ )
+
def _make_thread_safe_path(self, config, attr, unique_id):
if config.option.parallel_safe_build:
path = getattr(config, attr)
@@ -1069,7 +1080,7 @@ class parseini:
reader.addsubstitutions(**{env_attr.name: res})
return tc
- def _getenvdata(self, reader):
+ def _getenvdata(self, reader, config):
candidates = (
self.config.option.env,
os.environ.get("TOXENV"),
@@ -1086,9 +1097,17 @@ class parseini:
if not all_envs:
all_envs.add("python")
+ package_env = config.isolated_build_env
+ if config.isolated_build is True and package_env in all_envs:
+ all_envs.remove(package_env)
+
if not env_list or "ALL" in env_list:
env_list = sorted(all_envs)
+ if config.isolated_build is True and package_env in env_list:
+ msg = "isolated_build_env {} cannot be part of envlist".format(package_env)
+ raise tox.exception.ConfigError(msg)
+
return env_list, all_envs
diff --git a/src/tox/package.py b/src/tox/package.py
index b23cb1a3..54e6558b 100644
--- a/src/tox/package.py
+++ b/src/tox/package.py
@@ -1,8 +1,17 @@
+import json
import sys
+import textwrap
+from collections import namedtuple
+import pkg_resources
import py
+import six
+import toml
import tox
+from tox.config import DepConfig
+
+BuildInfo = namedtuple("BuildInfo", ["requires", "backend_module", "backend_object"])
@tox.hookimpl
@@ -27,10 +36,9 @@ def get_package(session):
report.info("using package {!r}, skipping 'sdist' activity ".format(str(path)))
else:
try:
- path = make_sdist(report, config, session)
- except tox.exception.InvocationError:
- v = sys.exc_info()[1]
- report.error("FAIL could not package project - v = {!r}".format(v))
+ path = build_package(config, report, session)
+ except tox.exception.InvocationError as exception:
+ report.error("FAIL could not package project - v = {!r}".format(exception))
return None
sdist_file = config.distshare.join(path.basename)
if sdist_file != path:
@@ -44,7 +52,14 @@ def get_package(session):
return path
-def make_sdist(report, config, session):
+def build_package(config, report, session):
+ if not config.isolated_build:
+ return make_sdist_legacy(report, config, session)
+ else:
+ return build_isolated(config, report, session)
+
+
+def make_sdist_legacy(report, config, session):
setup = config.setupdir.join("setup.py")
if not setup.check():
report.error(
@@ -83,3 +98,113 @@ def make_sdist(report, config, session):
" python setup.py sdist"
)
raise SystemExit(1)
+
+
+def build_isolated(config, report, session):
+ build_info = get_build_info(config.setupdir, report)
+ package_venv = session.getvenv(config.isolated_build_env)
+ package_venv.envconfig.deps_matches_subset = True
+
+ # we allow user specified dependencies so the users can write extensions to
+ # install additional type of dependencies (e.g. binary)
+ user_specified_deps = package_venv.envconfig.deps
+ package_venv.envconfig.deps = [DepConfig(r, None) for r in build_info.requires]
+ package_venv.envconfig.deps.extend(user_specified_deps)
+
+ if not session.setupenv(package_venv):
+ session.finishvenv(package_venv)
+
+ build_requires = get_build_requires(build_info, package_venv, session)
+ # we need to filter out requirements already specified in pyproject.toml or user deps
+ base_build_deps = {pkg_resources.Requirement(r.name).key for r in package_venv.envconfig.deps}
+ build_requires_dep = [
+ DepConfig(r, None)
+ for r in build_requires
+ if pkg_resources.Requirement(r).key not in base_build_deps
+ ]
+ if build_requires_dep:
+ with session.newaction(
+ package_venv, "build_requires", package_venv.envconfig.envdir
+ ) as action:
+ package_venv.run_install_command(packages=build_requires_dep, action=action)
+ session.finishvenv(package_venv)
+ return perform_isolated_build(build_info, package_venv, session, config)
+
+
+def get_build_info(folder, report):
+ toml_file = folder.join("pyproject.toml")
+
+ # as per https://www.python.org/dev/peps/pep-0517/
+
+ def abort(message):
+ report.error("{} inside {}".format(message, toml_file))
+ raise SystemExit(1)
+
+ if not toml_file.exists():
+ report.error("missing {}".format(toml_file))
+ raise SystemExit(1)
+
+ with open(str(toml_file)) as file_handler:
+ config_data = toml.load(file_handler)
+
+ if "build-system" not in config_data:
+ abort("build-system section missing")
+
+ build_system = config_data["build-system"]
+
+ if "requires" not in build_system:
+ abort("missing requires key at build-system section")
+ if "build-backend" not in build_system:
+ abort("missing build-backend key at build-system section")
+
+ requires = build_system["requires"]
+ if not isinstance(requires, list) or not all(isinstance(i, six.text_type) for i in requires):
+ abort("requires key at build-system section must be a list of string")
+
+ backend = build_system["build-backend"]
+ if not isinstance(backend, six.text_type):
+ abort("build-backend key at build-system section must be a string")
+
+ args = backend.split(":")
+ module = args[0]
+ obj = "" if len(args) == 1 else ".{}".format(args[1])
+
+ return BuildInfo(requires, module, "{}{}".format(module, obj))
+
+
+def perform_isolated_build(build_info, package_venv, session, config):
+ with session.newaction(
+ package_venv, "perform isolated build", package_venv.envconfig.envdir
+ ) as action:
+ script = textwrap.dedent(
+ """
+ import sys
+ import {}
+ basename = {}.build_{}({!r}, {{ "--global-option": ["--formats=gztar"]}})
+ print(basename)""".format(
+ build_info.backend_module, build_info.backend_object, "sdist", str(config.distdir)
+ )
+ )
+ config.distdir.ensure_dir()
+ result = action.popen([package_venv.envconfig.envpython, "-c", script], returnout=True)
+ return config.distdir.join(result.split("\n")[-2])
+
+
+def get_build_requires(build_info, package_venv, session):
+ with session.newaction(
+ package_venv, "get build requires", package_venv.envconfig.envdir
+ ) as action:
+ script = textwrap.dedent(
+ """
+ import {}
+ import json
+
+ backend = {}
+ for_build_requires = backend.get_requires_for_build_{}(None)
+ print(json.dumps(for_build_requires))
+ """.format(
+ build_info.backend_module, build_info.backend_object, "sdist"
+ )
+ ).strip()
+ result = action.popen([package_venv.envconfig.envpython, "-c", script], returnout=True)
+ return json.loads(result.split("\n")[-2])
diff --git a/src/tox/session.py b/src/tox/session.py
index 12db45cf..ae6eb283 100644
--- a/src/tox/session.py
+++ b/src/tox/session.py
@@ -263,7 +263,7 @@ class Reporter(object):
def __init__(self, session):
self.tw = py.io.TerminalWriter()
self.session = session
- self._reportedlines = []
+ self.reported_lines = []
@property
def verbosity(self):
@@ -338,7 +338,7 @@ class Reporter(object):
self.logline("SKIPPED: {}".format(msg), yellow=True)
def logline(self, msg, **opts):
- self._reportedlines.append(msg)
+ self.reported_lines.append(msg)
self.tw.line("{}".format(msg), **opts)
def verbosity0(self, msg, **opts):
@@ -619,7 +619,9 @@ class Session:
def showenvs(self, all_envs=False, description=False):
env_conf = self.config.envconfigs # this contains all environments
default = self.config.envlist # this only the defaults
- extra = sorted(e for e in env_conf if e not in default) if all_envs else []
+ ignore = {self.config.isolated_build_env}.union(default)
+ extra = sorted(e for e in env_conf if e not in ignore) if all_envs else []
+
if description:
self.report.line("default environments:")
max_length = max(len(env) for env in (default + extra))
diff --git a/src/tox/venv.py b/src/tox/venv.py
index f93b9445..978ff5df 100755
--- a/src/tox/venv.py
+++ b/src/tox/venv.py
@@ -53,7 +53,7 @@ class CreationConfig:
except Exception:
return None
- def matches(self, other):
+ def matches(self, other, deps_matches_subset=False):
return (
other
and self.md5 == other.md5
@@ -62,7 +62,11 @@ class CreationConfig:
and self.sitepackages == other.sitepackages
and self.usedevelop == other.usedevelop
and self.alwayscopy == other.alwayscopy
- and self.deps == other.deps
+ and (
+ all(d in self.deps for d in other.deps)
+ if deps_matches_subset is True
+ else self.deps == other.deps
+ )
)
@@ -159,7 +163,13 @@ class VirtualEnv(object):
if status string is empty, all is ok.
"""
rconfig = CreationConfig.readconfig(self.path_config)
- if not self.envconfig.recreate and rconfig and rconfig.matches(self._getliveconfig()):
+ if (
+ not self.envconfig.recreate
+ and rconfig
+ and rconfig.matches(
+ self._getliveconfig(), getattr(self.envconfig, "deps_matches_subset", False)
+ )
+ ):
action.info("reusing", self.envconfig.envdir)
return
if rconfig is None:
@@ -173,9 +183,8 @@ class VirtualEnv(object):
return sys.exc_info()[1]
try:
self.hook.tox_testenv_install_deps(action=action, venv=self)
- except tox.exception.InvocationError:
- v = sys.exc_info()[1]
- return "could not install deps {}; v = {!r}".format(self.envconfig.deps, v)
+ except tox.exception.InvocationError as exception:
+ return "could not install deps {}; v = {!r}".format(self.envconfig.deps, exception)
def _getliveconfig(self):
python = self.envconfig.python_info.executable