diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/tox/_pytestplugin.py | 17 | ||||
-rwxr-xr-x | src/tox/config.py | 23 | ||||
-rw-r--r-- | src/tox/package.py | 135 | ||||
-rw-r--r-- | src/tox/session.py | 8 | ||||
-rwxr-xr-x | src/tox/venv.py | 21 |
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 |