summaryrefslogtreecommitdiff
path: root/tox/venv.py
diff options
context:
space:
mode:
Diffstat (limited to 'tox/venv.py')
-rw-r--r--tox/venv.py422
1 files changed, 422 insertions, 0 deletions
diff --git a/tox/venv.py b/tox/venv.py
new file mode 100644
index 0000000..eb2fadc
--- /dev/null
+++ b/tox/venv.py
@@ -0,0 +1,422 @@
+from __future__ import with_statement
+import os
+import sys
+import re
+import codecs
+import py
+import tox
+from .config import DepConfig
+
+
+class CreationConfig:
+ def __init__(self, md5, python, version, sitepackages,
+ usedevelop, deps):
+ self.md5 = md5
+ self.python = python
+ self.version = version
+ self.sitepackages = sitepackages
+ self.usedevelop = usedevelop
+ self.deps = deps
+
+ def writeconfig(self, path):
+ lines = ["%s %s" % (self.md5, self.python)]
+ lines.append("%s %d %d" % (self.version, self.sitepackages, self.usedevelop))
+ for dep in self.deps:
+ lines.append("%s %s" % dep)
+ path.ensure()
+ path.write("\n".join(lines))
+
+ @classmethod
+ def readconfig(cls, path):
+ try:
+ lines = path.readlines(cr=0)
+ value = lines.pop(0).split(None, 1)
+ md5, python = value
+ version, sitepackages, usedevelop = lines.pop(0).split(None, 3)
+ sitepackages = bool(int(sitepackages))
+ usedevelop = bool(int(usedevelop))
+ deps = []
+ for line in lines:
+ md5, depstring = line.split(None, 1)
+ deps.append((md5, depstring))
+ return CreationConfig(md5, python, version, sitepackages, usedevelop, deps)
+ except Exception:
+ return None
+
+ def matches(self, other):
+ return (other and self.md5 == other.md5
+ and self.python == other.python
+ and self.version == other.version
+ and self.sitepackages == other.sitepackages
+ and self.usedevelop == other.usedevelop
+ and self.deps == other.deps)
+
+
+class VirtualEnv(object):
+ def __init__(self, envconfig=None, session=None):
+ self.envconfig = envconfig
+ self.session = session
+ self.path = envconfig.envdir
+ self.path_config = self.path.join(".tox-config1")
+
+ @property
+ def name(self):
+ return self.envconfig.envname
+
+ def __repr__(self):
+ return "<VirtualEnv at %r>" % (self.path)
+
+ def getcommandpath(self, name=None, venv=True, cwd=None):
+ if name is None:
+ return self.envconfig.envpython
+ name = str(name)
+ if os.path.isabs(name):
+ return name
+ if os.path.split(name)[0] == ".":
+ p = cwd.join(name)
+ if p.check():
+ return str(p)
+ p = None
+ if venv:
+ p = py.path.local.sysfind(name, paths=[self.envconfig.envbindir])
+ if p is not None:
+ return p
+ p = py.path.local.sysfind(name)
+ if p is None:
+ raise tox.exception.InvocationError(
+ "could not find executable %r" % (name,))
+ # p is not found in virtualenv script/bin dir
+ if venv:
+ if not self.is_allowed_external(p):
+ self.session.report.warning(
+ "test command found but not installed in testenv\n"
+ " cmd: %s\n"
+ " env: %s\n"
+ "Maybe you forgot to specify a dependency? "
+ "See also the whitelist_externals envconfig setting." % (
+ p, self.envconfig.envdir))
+ return str(p) # will not be rewritten for reporting
+
+ def is_allowed_external(self, p):
+ tryadd = [""]
+ if sys.platform == "win32":
+ tryadd += [
+ os.path.normcase(x)
+ for x in os.environ['PATHEXT'].split(os.pathsep)
+ ]
+ p = py.path.local(os.path.normcase(str(p)))
+ for x in self.envconfig.whitelist_externals:
+ for add in tryadd:
+ if p.fnmatch(x + add):
+ return True
+ return False
+
+ def _ispython3(self):
+ return "python3" in str(self.envconfig.basepython)
+
+ def update(self, action=None):
+ """ return status string for updating actual venv to match configuration.
+ if status string is empty, all is ok.
+ """
+ if action is None:
+ action = self.session.newaction(self, "update")
+ rconfig = CreationConfig.readconfig(self.path_config)
+ if not self.envconfig.recreate and rconfig and \
+ rconfig.matches(self._getliveconfig()):
+ action.info("reusing", self.envconfig.envdir)
+ return
+ if rconfig is None:
+ action.setactivity("create", self.envconfig.envdir)
+ else:
+ action.setactivity("recreate", self.envconfig.envdir)
+ try:
+ self.create(action)
+ except tox.exception.UnsupportedInterpreter:
+ return sys.exc_info()[1]
+ except tox.exception.InterpreterNotFound:
+ return sys.exc_info()[1]
+ try:
+ self.install_deps(action)
+ except tox.exception.InvocationError:
+ v = sys.exc_info()[1]
+ return "could not install deps %s; v = %r" % (
+ self.envconfig.deps, v)
+
+ def _getliveconfig(self):
+ python = self.envconfig.python_info.executable
+ md5 = getdigest(python)
+ version = tox.__version__
+ sitepackages = self.envconfig.sitepackages
+ develop = self.envconfig.usedevelop
+ deps = []
+ for dep in self._getresolvedeps():
+ raw_dep = dep.name
+ md5 = getdigest(raw_dep)
+ deps.append((md5, raw_dep))
+ return CreationConfig(md5, python, version,
+ sitepackages, develop, deps)
+
+ def _getresolvedeps(self):
+ l = []
+ for dep in self.envconfig.deps:
+ if dep.indexserver is None:
+ res = self.session._resolve_pkg(dep.name)
+ if res != dep.name:
+ dep = dep.__class__(res)
+ l.append(dep)
+ return l
+
+ def getsupportedinterpreter(self):
+ return self.envconfig.getsupportedinterpreter()
+
+ def matching_platform(self):
+ return re.match(self.envconfig.platform, sys.platform)
+
+ def create(self, action=None):
+ # if self.getcommandpath("activate").dirpath().check():
+ # return
+ if action is None:
+ action = self.session.newaction(self, "create")
+
+ config_interpreter = self.getsupportedinterpreter()
+ args = [sys.executable, '-m', 'virtualenv']
+ if self.envconfig.sitepackages:
+ args.append('--system-site-packages')
+ # add interpreter explicitly, to prevent using
+ # default (virtualenv.ini)
+ args.extend(['--python', str(config_interpreter)])
+ # if sys.platform == "win32":
+ # f, path, _ = py.std.imp.find_module("virtualenv")
+ # f.close()
+ # args[:1] = [str(config_interpreter), str(path)]
+ # else:
+ self.session.make_emptydir(self.path)
+ basepath = self.path.dirpath()
+ basepath.ensure(dir=1)
+ args.append(self.path.basename)
+ self._pcall(args, venv=False, action=action, cwd=basepath)
+ self.just_created = True
+
+ def finish(self):
+ self._getliveconfig().writeconfig(self.path_config)
+
+ def _needs_reinstall(self, setupdir, action):
+ setup_py = setupdir.join('setup.py')
+ setup_cfg = setupdir.join('setup.cfg')
+ args = [self.envconfig.envpython, str(setup_py), '--name']
+ output = action.popen(args, cwd=setupdir, redirect=False,
+ returnout=True)
+ name = output.strip()
+ egg_info = setupdir.join('.'.join((name, 'egg-info')))
+ for conf_file in (setup_py, setup_cfg):
+ if (not egg_info.check()
+ or (conf_file.check() and conf_file.mtime() > egg_info.mtime())):
+ return True
+ return False
+
+ def developpkg(self, setupdir, action):
+ assert action is not None
+ if getattr(self, 'just_created', False):
+ action.setactivity("develop-inst", setupdir)
+ self.finish()
+ extraopts = []
+ else:
+ if not self._needs_reinstall(setupdir, action):
+ action.setactivity("develop-inst-noop", setupdir)
+ return
+ action.setactivity("develop-inst-nodeps", setupdir)
+ extraopts = ['--no-deps']
+ self._install(['-e', setupdir], extraopts=extraopts, action=action)
+
+ def installpkg(self, sdistpath, action):
+ assert action is not None
+ if getattr(self, 'just_created', False):
+ action.setactivity("inst", sdistpath)
+ self.finish()
+ extraopts = []
+ else:
+ action.setactivity("inst-nodeps", sdistpath)
+ extraopts = ['-U', '--no-deps']
+ self._install([sdistpath], extraopts=extraopts, action=action)
+
+ def install_deps(self, action=None):
+ if action is None:
+ action = self.session.newaction(self, "install_deps")
+ deps = self._getresolvedeps()
+ if deps:
+ depinfo = ", ".join(map(str, deps))
+ action.setactivity("installdeps", "%s" % depinfo)
+ self._install(deps, action=action)
+
+ def _installopts(self, indexserver):
+ l = []
+ if indexserver:
+ l += ["-i", indexserver]
+ if self.envconfig.downloadcache:
+ self.envconfig.downloadcache.ensure(dir=1)
+ l.append("--download-cache=%s" % self.envconfig.downloadcache)
+ if self.envconfig.pip_pre:
+ l.append("--pre")
+ return l
+
+ def run_install_command(self, packages, options=(),
+ indexserver=None, action=None,
+ extraenv=None):
+ argv = self.envconfig.install_command[:]
+ # use pip-script on win32 to avoid the executable locking
+ i = argv.index('{packages}')
+ argv[i:i + 1] = packages
+ if '{opts}' in argv:
+ i = argv.index('{opts}')
+ argv[i:i + 1] = list(options)
+ for x in ('PIP_RESPECT_VIRTUALENV', 'PIP_REQUIRE_VIRTUALENV',
+ '__PYVENV_LAUNCHER__'):
+ try:
+ del os.environ[x]
+ except KeyError:
+ pass
+ old_stdout = sys.stdout
+ sys.stdout = codecs.getwriter('utf8')(sys.stdout)
+ if extraenv is None:
+ extraenv = {}
+ self._pcall(argv, cwd=self.envconfig.config.toxinidir,
+ extraenv=extraenv, action=action)
+ sys.stdout = old_stdout
+
+ def _install(self, deps, extraopts=None, action=None):
+ if not deps:
+ return
+ d = {}
+ l = []
+ for dep in deps:
+ if isinstance(dep, (str, py.path.local)):
+ dep = DepConfig(str(dep), None)
+ assert isinstance(dep, DepConfig), dep
+ if dep.indexserver is None:
+ ixserver = self.envconfig.config.indexserver['default']
+ else:
+ ixserver = dep.indexserver
+ d.setdefault(ixserver, []).append(dep.name)
+ if ixserver not in l:
+ l.append(ixserver)
+ assert ixserver.url is None or isinstance(ixserver.url, str)
+
+ for ixserver in l:
+ if self.envconfig.config.option.sethome:
+ extraenv = hack_home_env(
+ homedir=self.envconfig.envtmpdir.join("pseudo-home"),
+ index_url=ixserver.url)
+ else:
+ extraenv = {}
+
+ packages = d[ixserver]
+ options = self._installopts(ixserver.url)
+ if extraopts:
+ options.extend(extraopts)
+ self.run_install_command(packages=packages, options=options,
+ action=action, extraenv=extraenv)
+
+ def _getenv(self, extraenv={}):
+ env = {}
+ for envname in self.envconfig.passenv:
+ if envname in os.environ:
+ env[envname] = os.environ[envname]
+
+ env.update(self.envconfig.setenv)
+
+ env['VIRTUAL_ENV'] = str(self.path)
+
+ env.update(extraenv)
+ return env
+
+ def test(self, redirect=False):
+ action = self.session.newaction(self, "runtests")
+ with action:
+ self.status = 0
+ self.session.make_emptydir(self.envconfig.envtmpdir)
+ cwd = self.envconfig.changedir
+ env = self._getenv()
+ # Display PYTHONHASHSEED to assist with reproducibility.
+ action.setactivity("runtests", "PYTHONHASHSEED=%r" % env.get('PYTHONHASHSEED'))
+ for i, argv in enumerate(self.envconfig.commands):
+ # have to make strings as _pcall changes argv[0] to a local()
+ # happens if the same environment is invoked twice
+ message = "commands[%s] | %s" % (i, ' '.join(
+ [str(x) for x in argv]))
+ action.setactivity("runtests", message)
+ # check to see if we need to ignore the return code
+ # if so, we need to alter the command line arguments
+ if argv[0].startswith("-"):
+ ignore_ret = True
+ if argv[0] == "-":
+ del argv[0]
+ else:
+ argv[0] = argv[0].lstrip("-")
+ else:
+ ignore_ret = False
+
+ try:
+ self._pcall(argv, cwd=cwd, action=action, redirect=redirect,
+ ignore_ret=ignore_ret)
+ except tox.exception.InvocationError as err:
+ self.session.report.error(str(err))
+ self.status = "commands failed"
+ if not self.envconfig.ignore_errors:
+ break # Don't process remaining commands
+ except KeyboardInterrupt:
+ self.status = "keyboardinterrupt"
+ self.session.report.error(self.status)
+ raise
+
+ def _pcall(self, args, venv=True, cwd=None, extraenv={},
+ action=None, redirect=True, ignore_ret=False):
+ for name in ("VIRTUALENV_PYTHON", "PYTHONDONTWRITEBYTECODE"):
+ try:
+ del os.environ[name]
+ except KeyError:
+ pass
+ assert cwd
+ cwd.ensure(dir=1)
+ old = self.patchPATH()
+ try:
+ args[0] = self.getcommandpath(args[0], venv, cwd)
+ env = self._getenv(extraenv)
+ return action.popen(args, cwd=cwd, env=env, redirect=redirect, ignore_ret=ignore_ret)
+ finally:
+ os.environ['PATH'] = old
+
+ def patchPATH(self):
+ oldPATH = os.environ['PATH']
+ bindir = str(self.envconfig.envbindir)
+ os.environ['PATH'] = os.pathsep.join([bindir, oldPATH])
+ self.session.report.verbosity2("setting PATH=%s" % os.environ["PATH"])
+ return oldPATH
+
+
+def getdigest(path):
+ path = py.path.local(path)
+ if not path.check(file=1):
+ return "0" * 32
+ return path.computehash()
+
+
+def hack_home_env(homedir, index_url=None):
+ # XXX HACK (this could also live with tox itself, consider)
+ # if tox uses pip on a package that requires setup_requires
+ # the index url set with pip is usually not recognized
+ # because it is setuptools executing very early.
+ # We therefore run the tox command in an artifical home
+ # directory and set .pydistutils.cfg and pip.conf files
+ # accordingly.
+ if not homedir.check():
+ homedir.ensure(dir=1)
+ d = dict(HOME=str(homedir))
+ if not index_url:
+ index_url = os.environ.get("TOX_INDEX_URL")
+ if index_url:
+ homedir.join(".pydistutils.cfg").write(
+ "[easy_install]\n"
+ "index_url = %s\n" % index_url)
+ d["PIP_INDEX_URL"] = index_url
+ d["TOX_INDEX_URL"] = index_url
+ return d