summaryrefslogtreecommitdiff
path: root/tox
diff options
context:
space:
mode:
Diffstat (limited to 'tox')
-rw-r--r--tox/__init__.py33
-rw-r--r--tox/__main__.py4
-rw-r--r--tox/_pytestplugin.py333
-rw-r--r--tox/_quickstart.py275
-rw-r--r--tox/_verlib.py335
-rw-r--r--tox/config.py1247
-rw-r--r--tox/hookspecs.py59
-rw-r--r--tox/interpreters.py184
-rw-r--r--tox/result.py81
-rw-r--r--tox/session.py686
-rw-r--r--tox/venv.py416
11 files changed, 0 insertions, 3653 deletions
diff --git a/tox/__init__.py b/tox/__init__.py
deleted file mode 100644
index baf651b..0000000
--- a/tox/__init__.py
+++ /dev/null
@@ -1,33 +0,0 @@
-#
-__version__ = '2.4.0.dev1'
-
-from .hookspecs import hookspec, hookimpl # noqa
-
-
-class exception:
- class Error(Exception):
- def __str__(self):
- return "%s: %s" % (self.__class__.__name__, self.args[0])
-
- class ConfigError(Error):
- """ error in tox configuration. """
- class UnsupportedInterpreter(Error):
- "signals an unsupported Interpreter"
- class InterpreterNotFound(Error):
- "signals that an interpreter could not be found"
- class InvocationError(Error):
- """ an error while invoking a script. """
- class MissingFile(Error):
- """ an error while invoking a script. """
- class MissingDirectory(Error):
- """ a directory did not exist. """
- class MissingDependency(Error):
- """ a dependency could not be found or determined. """
- class MinVersionError(Error):
- """ the installed tox version is lower than requested minversion. """
-
- def __init__(self, message):
- self.message = message
- super(exception.MinVersionError, self).__init__(message)
-
-from tox.session import main as cmdline # noqa
diff --git a/tox/__main__.py b/tox/__main__.py
deleted file mode 100644
index e28d746..0000000
--- a/tox/__main__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from tox.session import main
-
-if __name__ == "__main__":
- main()
diff --git a/tox/_pytestplugin.py b/tox/_pytestplugin.py
deleted file mode 100644
index 785070d..0000000
--- a/tox/_pytestplugin.py
+++ /dev/null
@@ -1,333 +0,0 @@
-import py
-import pytest
-import tox
-import os
-import sys
-from py.builtin import _isbytes, _istext, print_
-from fnmatch import fnmatch
-import time
-from .config import parseconfig
-from .venv import VirtualEnv
-from .session import Action
-from .result import ResultLog
-
-
-def pytest_configure():
- if 'TOXENV' in os.environ:
- del os.environ['TOXENV']
- if 'HUDSON_URL' in os.environ:
- del os.environ['HUDSON_URL']
-
-
-def pytest_addoption(parser):
- parser.addoption("--no-network", action="store_true",
- dest="no_network",
- help="don't run tests requiring network")
-
-
-def pytest_report_header():
- return "tox comes from: %r" % (tox.__file__)
-
-
-@pytest.fixture
-def newconfig(request, tmpdir):
- def newconfig(args, source=None, plugins=()):
- if source is None:
- source = args
- args = []
- s = py.std.textwrap.dedent(source)
- p = tmpdir.join("tox.ini")
- p.write(s)
- old = tmpdir.chdir()
- try:
- return parseconfig(args, plugins=plugins)
- finally:
- old.chdir()
- return newconfig
-
-
-@pytest.fixture
-def cmd(request):
- if request.config.option.no_network:
- pytest.skip("--no-network was specified, test cannot run")
- return Cmd(request)
-
-
-class ReportExpectMock:
- def __init__(self, session):
- self._calls = []
- self._index = -1
- self.session = session
-
- def clear(self):
- self._calls[:] = []
-
- def __getattr__(self, name):
- if name[0] == "_":
- raise AttributeError(name)
-
- def generic_report(*args, **kwargs):
- self._calls.append((name,) + args)
- print("%s" % (self._calls[-1], ))
- return generic_report
-
- def action(self, venv, msg, *args):
- self._calls.append(("action", venv, msg))
- print("%s" % (self._calls[-1], ))
- return Action(self.session, venv, msg, args)
-
- def getnext(self, cat):
- __tracebackhide__ = True
- newindex = self._index + 1
- while newindex < len(self._calls):
- call = self._calls[newindex]
- lcat = call[0]
- if fnmatch(lcat, cat):
- self._index = newindex
- return call
- newindex += 1
- raise LookupError(
- "looking for %r, no reports found at >=%d in %r" %
- (cat, self._index + 1, self._calls))
-
- def expect(self, cat, messagepattern="*", invert=False):
- __tracebackhide__ = True
- if not messagepattern.startswith("*"):
- messagepattern = "*" + messagepattern
- while self._index < len(self._calls):
- try:
- call = self.getnext(cat)
- except LookupError:
- break
- for lmsg in call[1:]:
- lmsg = str(lmsg).replace("\n", " ")
- if fnmatch(lmsg, messagepattern):
- if invert:
- raise AssertionError("found %s(%r), didn't expect it" %
- (cat, messagepattern))
- return
- if not invert:
- raise AssertionError(
- "looking for %s(%r), no reports found at >=%d in %r" %
- (cat, messagepattern, self._index + 1, self._calls))
-
- def not_expect(self, cat, messagepattern="*"):
- return self.expect(cat, messagepattern, invert=True)
-
-
-class pcallMock:
- def __init__(self, args, cwd, env, stdout, stderr, shell):
- self.arg0 = args[0]
- self.args = args[1:]
- self.cwd = cwd
- self.env = env
- self.stdout = stdout
- self.stderr = stderr
- self.shell = shell
-
- def communicate(self):
- return "", ""
-
- def wait(self):
- pass
-
-
-@pytest.fixture
-def mocksession(request):
- from tox.session import Session
-
- class MockSession(Session):
- def __init__(self):
- self._clearmocks()
- self.config = request.getfuncargvalue("newconfig")([], "")
- self.resultlog = ResultLog()
- self._actions = []
-
- def getenv(self, name):
- return VirtualEnv(self.config.envconfigs[name], session=self)
-
- def _clearmocks(self):
- self._pcalls = []
- self._spec2pkg = {}
- self.report = ReportExpectMock(self)
-
- def make_emptydir(self, path):
- pass
-
- def popen(self, args, cwd, shell=None,
- universal_newlines=False,
- stdout=None, stderr=None, env=None):
- pm = pcallMock(args, cwd, env, stdout, stderr, shell)
- self._pcalls.append(pm)
- return pm
- return MockSession()
-
-
-@pytest.fixture
-def newmocksession(request):
- mocksession = request.getfuncargvalue("mocksession")
- newconfig = request.getfuncargvalue("newconfig")
-
- def newmocksession(args, source, plugins=()):
- mocksession.config = newconfig(args, source, plugins=plugins)
- return mocksession
- return newmocksession
-
-
-class Cmd:
- def __init__(self, request):
- self.tmpdir = request.getfuncargvalue("tmpdir")
- self.request = request
- current = py.path.local()
- self.request.addfinalizer(current.chdir)
-
- def chdir(self, target):
- target.chdir()
-
- def popen(self, argv, stdout, stderr, **kw):
- if not hasattr(py.std, 'subprocess'):
- py.test.skip("no subprocess module")
- env = os.environ.copy()
- env['PYTHONPATH'] = ":".join(filter(None, [
- str(os.getcwd()), env.get('PYTHONPATH', '')]))
- kw['env'] = env
- # print "env", env
- return py.std.subprocess.Popen(argv, stdout=stdout, stderr=stderr, **kw)
-
- def run(self, *argv):
- if argv[0] == "tox" and sys.version_info[:2] < (2, 7):
- pytest.skip("can not run tests involving calling tox on python2.6. "
- "(and python2.6 is about to be deprecated anyway)")
- argv = [str(x) for x in argv]
- assert py.path.local.sysfind(str(argv[0])), argv[0]
- p1 = self.tmpdir.join("stdout")
- p2 = self.tmpdir.join("stderr")
- print("%s$ %s" % (os.getcwd(), " ".join(argv)))
- f1 = p1.open("wb")
- f2 = p2.open("wb")
- now = time.time()
- popen = self.popen(argv, stdout=f1, stderr=f2,
- close_fds=(sys.platform != "win32"))
- ret = popen.wait()
- f1.close()
- f2.close()
- out = p1.read("rb")
- out = getdecoded(out).splitlines()
- err = p2.read("rb")
- err = getdecoded(err).splitlines()
-
- def dump_lines(lines, fp):
- try:
- for line in lines:
- py.builtin.print_(line, file=fp)
- except UnicodeEncodeError:
- print("couldn't print to %s because of encoding" % (fp,))
- dump_lines(out, sys.stdout)
- dump_lines(err, sys.stderr)
- return RunResult(ret, out, err, time.time() - now)
-
-
-def getdecoded(out):
- try:
- return out.decode("utf-8")
- except UnicodeDecodeError:
- return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % (
- py.io.saferepr(out),)
-
-
-class RunResult:
- def __init__(self, ret, outlines, errlines, duration):
- self.ret = ret
- self.outlines = outlines
- self.errlines = errlines
- self.stdout = LineMatcher(outlines)
- self.stderr = LineMatcher(errlines)
- self.duration = duration
-
-
-class LineMatcher:
- def __init__(self, lines):
- self.lines = lines
-
- def str(self):
- return "\n".join(self.lines)
-
- def fnmatch_lines(self, lines2):
- if isinstance(lines2, str):
- lines2 = py.code.Source(lines2)
- if isinstance(lines2, py.code.Source):
- lines2 = lines2.strip().lines
-
- from fnmatch import fnmatch
- lines1 = self.lines[:]
- nextline = None
- extralines = []
- __tracebackhide__ = True
- for line in lines2:
- nomatchprinted = False
- while lines1:
- nextline = lines1.pop(0)
- if line == nextline:
- print_("exact match:", repr(line))
- break
- elif fnmatch(nextline, line):
- print_("fnmatch:", repr(line))
- print_(" with:", repr(nextline))
- break
- else:
- if not nomatchprinted:
- print_("nomatch:", repr(line))
- nomatchprinted = True
- print_(" and:", repr(nextline))
- extralines.append(nextline)
- else:
- assert line == nextline
-
-
-@pytest.fixture
-def initproj(request, tmpdir):
- """ create a factory function for creating example projects. """
- def initproj(nameversion, filedefs=None):
- if filedefs is None:
- filedefs = {}
- if _istext(nameversion) or _isbytes(nameversion):
- parts = nameversion.split("-")
- if len(parts) == 1:
- parts.append("0.1")
- name, version = parts
- else:
- name, version = nameversion
- base = tmpdir.ensure(name, dir=1)
- create_files(base, filedefs)
- if 'setup.py' not in filedefs:
- create_files(base, {'setup.py': '''
- from setuptools import setup
- setup(
- name='%(name)s',
- description='%(name)s project',
- version='%(version)s',
- license='MIT',
- platforms=['unix', 'win32'],
- packages=['%(name)s', ],
- )
- ''' % locals()})
- if name not in filedefs:
- create_files(base, {
- name: {'__init__.py': '__version__ = %r' % version}
- })
- manifestlines = []
- for p in base.visit(lambda x: x.check(file=1)):
- manifestlines.append("include %s" % p.relto(base))
- create_files(base, {"MANIFEST.in": "\n".join(manifestlines)})
- print("created project in %s" % (base,))
- base.chdir()
- return initproj
-
-
-def create_files(base, filedefs):
- for key, value in filedefs.items():
- if isinstance(value, dict):
- create_files(base.ensure(key, dir=1), value)
- elif isinstance(value, str):
- s = py.std.textwrap.dedent(value)
- base.join(key).write(s)
diff --git a/tox/_quickstart.py b/tox/_quickstart.py
deleted file mode 100644
index 59ee48e..0000000
--- a/tox/_quickstart.py
+++ /dev/null
@@ -1,275 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
- tox._quickstart
- ~~~~~~~~~~~~~~~~~
-
- Command-line script to quickly setup tox.ini for a Python project
-
- This file was heavily inspired by and uses code from ``sphinx-quickstart``
- in the BSD-licensed `Sphinx project`_.
-
- .. Sphinx project_: http://sphinx.pocoo.org/
-
- License for Sphinx
- ==================
-
- Copyright (c) 2007-2011 by the Sphinx team (see AUTHORS file).
- All rights reserved.
-
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions are
- met:
-
- * Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
-
- * Redistributions in binary form must reproduce the above copyright
- notice, this list of conditions and the following disclaimer in the
- documentation and/or other materials provided with the distribution.
-
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-"""
-
-import sys
-from os import path
-from codecs import open
-
-TERM_ENCODING = getattr(sys.stdin, 'encoding', None)
-
-from tox import __version__
-
-# function to get input from terminal -- overridden by the test suite
-try:
- # this raw_input is not converted by 2to3
- term_input = raw_input
-except NameError:
- term_input = input
-
-
-all_envs = ['py26', 'py27', 'py32', 'py33', 'py34', 'py35', 'pypy', 'jython']
-
-PROMPT_PREFIX = '> '
-
-QUICKSTART_CONF = '''\
-# Tox (http://tox.testrun.org/) is a tool for running tests
-# in multiple virtualenvs. This configuration file will run the
-# test suite on all supported python versions. To use it, "pip install tox"
-# and then run "tox" from this directory.
-
-[tox]
-envlist = %(envlist)s
-
-[testenv]
-commands = %(commands)s
-deps = %(deps)s
-'''
-
-
-class ValidationError(Exception):
- """Raised for validation errors."""
-
-
-def nonempty(x):
- if not x:
- raise ValidationError("Please enter some text.")
- return x
-
-
-def choice(*l):
- def val(x):
- if x not in l:
- raise ValidationError('Please enter one of %s.' % ', '.join(l))
- return x
- return val
-
-
-def boolean(x):
- if x.upper() not in ('Y', 'YES', 'N', 'NO'):
- raise ValidationError("Please enter either 'y' or 'n'.")
- return x.upper() in ('Y', 'YES')
-
-
-def suffix(x):
- if not (x[0:1] == '.' and len(x) > 1):
- raise ValidationError("Please enter a file suffix, "
- "e.g. '.rst' or '.txt'.")
- return x
-
-
-def ok(x):
- return x
-
-
-def do_prompt(d, key, text, default=None, validator=nonempty):
- while True:
- if default:
- prompt = PROMPT_PREFIX + '%s [%s]: ' % (text, default)
- else:
- prompt = PROMPT_PREFIX + text + ': '
- x = term_input(prompt)
- if default and not x:
- x = default
- if sys.version_info < (3, ) and not isinstance(x, unicode): # noqa
- # for Python 2.x, try to get a Unicode string out of it
- if x.decode('ascii', 'replace').encode('ascii', 'replace') != x:
- if TERM_ENCODING:
- x = x.decode(TERM_ENCODING)
- else:
- print('* Note: non-ASCII characters entered '
- 'and terminal encoding unknown -- assuming '
- 'UTF-8 or Latin-1.')
- try:
- x = x.decode('utf-8')
- except UnicodeDecodeError:
- x = x.decode('latin1')
- try:
- x = validator(x)
- except ValidationError:
- err = sys.exc_info()[1]
- print('* ' + str(err))
- continue
- break
- d[key] = x
-
-
-def ask_user(d):
- """Ask the user for quickstart values missing from *d*.
-
- """
-
- print('Welcome to the Tox %s quickstart utility.' % __version__)
- print('''
-This utility will ask you a few questions and then generate a simple tox.ini
-file to help get you started using tox.
-
-Please enter values for the following settings (just press Enter to
-accept a default value, if one is given in brackets).''')
-
- sys.stdout.write('\n')
-
- print('''
-What Python versions do you want to test against? Choices:
- [1] py27
- [2] py27, py33
- [3] (All versions) %s
- [4] Choose each one-by-one''' % ', '.join(all_envs))
- do_prompt(d, 'canned_pyenvs', 'Enter the number of your choice',
- '3', choice('1', '2', '3', '4'))
-
- if d['canned_pyenvs'] == '1':
- d['py27'] = True
- elif d['canned_pyenvs'] == '2':
- for pyenv in ('py27', 'py33'):
- d[pyenv] = True
- elif d['canned_pyenvs'] == '3':
- for pyenv in all_envs:
- d[pyenv] = True
- elif d['canned_pyenvs'] == '4':
- for pyenv in all_envs:
- if pyenv not in d:
- do_prompt(d, pyenv, 'Test your project with %s (Y/n)' % pyenv, 'Y', boolean)
-
- print('''
-What command should be used to test your project -- examples:
- - py.test
- - python setup.py test
- - nosetests package.module
- - trial package.module''')
- do_prompt(d, 'commands', 'Command to run to test project', '{envpython} setup.py test')
-
- default_deps = ' '
- if 'py.test' in d['commands']:
- default_deps = 'pytest'
- if 'nosetests' in d['commands']:
- default_deps = 'nose'
- if 'trial' in d['commands']:
- default_deps = 'twisted'
-
- print('''
-What extra dependencies do your tests have?''')
- do_prompt(d, 'deps', 'Comma-separated list of dependencies', default_deps)
-
-
-def process_input(d):
- d['envlist'] = ', '.join([env for env in all_envs if d.get(env) is True])
- d['deps'] = '\n' + '\n'.join([
- ' %s' % dep.strip()
- for dep in d['deps'].split(',')])
-
- return d
-
-
-def rtrim_right(text):
- lines = []
- for line in text.split("\n"):
- lines.append(line.rstrip())
- return "\n".join(lines)
-
-
-def generate(d, overwrite=True, silent=False):
- """Generate project based on values in *d*."""
-
- conf_text = QUICKSTART_CONF % d
- conf_text = rtrim_right(conf_text)
-
- def write_file(fpath, mode, content):
- print('Creating file %s.' % fpath)
- f = open(fpath, mode, encoding='utf-8')
- try:
- f.write(content)
- finally:
- f.close()
-
- sys.stdout.write('\n')
-
- fpath = 'tox.ini'
-
- if path.isfile(fpath) and not overwrite:
- print('File %s already exists.' % fpath)
- do_prompt(d, 'fpath', 'Alternative path to write tox.ini contents to', 'tox-generated.ini')
- fpath = d['fpath']
-
- write_file(fpath, 'w', conf_text)
-
- if silent:
- return
- sys.stdout.write('\n')
- print('Finished: A tox.ini file has been created. For information on this file, '
- 'see http://tox.testrun.org/latest/config.html')
- print('''
-Execute `tox` to test your project.
-''')
-
-
-def main(argv=sys.argv):
- d = {}
-
- if len(argv) > 3:
- print('Usage: tox-quickstart [root]')
- sys.exit(1)
- elif len(argv) == 2:
- d['path'] = argv[1]
-
- try:
- ask_user(d)
- except (KeyboardInterrupt, EOFError):
- print()
- print('[Interrupted.]')
- return
-
- d = process_input(d)
- generate(d, overwrite=False)
-
-
-if __name__ == '__main__':
- main()
diff --git a/tox/_verlib.py b/tox/_verlib.py
deleted file mode 100644
index a2e7471..0000000
--- a/tox/_verlib.py
+++ /dev/null
@@ -1,335 +0,0 @@
-"""
-
-PEP386-version comparison algorithm.
-
-(c) Tarek Ziade and others
-extracted unmodified from https://bitbucket.org/tarek/distutilsversion
-licensed under the PSF license (i guess)
-
-"""
-
-import re
-
-
-class IrrationalVersionError(Exception):
- """This is an irrational version."""
- pass
-
-
-class HugeMajorVersionNumError(IrrationalVersionError):
- """An irrational version because the major version number is huge
- (often because a year or date was used).
-
- See `error_on_huge_major_num` option in `NormalizedVersion` for details.
- This guard can be disabled by setting that option False.
- """
- pass
-
-# A marker used in the second and third parts of the `parts` tuple, for
-# versions that don't have those segments, to sort properly. An example
-# of versions in sort order ('highest' last):
-# 1.0b1 ((1,0), ('b',1), ('f',))
-# 1.0.dev345 ((1,0), ('f',), ('dev', 345))
-# 1.0 ((1,0), ('f',), ('f',))
-# 1.0.post256.dev345 ((1,0), ('f',), ('f', 'post', 256, 'dev', 345))
-# 1.0.post345 ((1,0), ('f',), ('f', 'post', 345, 'f'))
-# ^ ^ ^
-# 'b' < 'f' ---------------------/ | |
-# | |
-# 'dev' < 'f' < 'post' -------------------/ |
-# |
-# 'dev' < 'f' ----------------------------------------------/
-# Other letters would do, but 'f' for 'final' is kind of nice.
-FINAL_MARKER = ('f',)
-
-VERSION_RE = re.compile(r'''
- ^
- (?P<version>\d+\.\d+) # minimum 'N.N'
- (?P<extraversion>(?:\.\d+)*) # any number of extra '.N' segments
- (?:
- (?P<prerel>[abc]|rc) # 'a'=alpha, 'b'=beta, 'c'=release candidate
- # 'rc'= alias for release candidate
- (?P<prerelversion>\d+(?:\.\d+)*)
- )?
- (?P<postdev>(\.post(?P<post>\d+))?(\.dev(?P<dev>\d+))?)?
- $''', re.VERBOSE)
-
-
-class NormalizedVersion(object):
- """A rational version.
-
- Good:
- 1.2 # equivalent to "1.2.0"
- 1.2.0
- 1.2a1
- 1.2.3a2
- 1.2.3b1
- 1.2.3c1
- 1.2.3.4
- TODO: fill this out
-
- Bad:
- 1 # mininum two numbers
- 1.2a # release level must have a release serial
- 1.2.3b
- """
- def __init__(self, s, error_on_huge_major_num=True):
- """Create a NormalizedVersion instance from a version string.
-
- @param s {str} The version string.
- @param error_on_huge_major_num {bool} Whether to consider an
- apparent use of a year or full date as the major version number
- an error. Default True. One of the observed patterns on PyPI before
- the introduction of `NormalizedVersion` was version numbers like this:
- 2009.01.03
- 20040603
- 2005.01
- This guard is here to strongly encourage the package author to
- use an alternate version, because a release deployed into PyPI
- and, e.g. downstream Linux package managers, will forever remove
- the possibility of using a version number like "1.0" (i.e.
- where the major number is less than that huge major number).
- """
- self._parse(s, error_on_huge_major_num)
-
- @classmethod
- def from_parts(cls, version, prerelease=FINAL_MARKER,
- devpost=FINAL_MARKER):
- return cls(cls.parts_to_str((version, prerelease, devpost)))
-
- def _parse(self, s, error_on_huge_major_num=True):
- """Parses a string version into parts."""
- match = VERSION_RE.search(s)
- if not match:
- raise IrrationalVersionError(s)
-
- groups = match.groupdict()
- parts = []
-
- # main version
- block = self._parse_numdots(groups['version'], s, False, 2)
- extraversion = groups.get('extraversion')
- if extraversion not in ('', None):
- block += self._parse_numdots(extraversion[1:], s)
- parts.append(tuple(block))
-
- # prerelease
- prerel = groups.get('prerel')
- if prerel is not None:
- block = [prerel]
- block += self._parse_numdots(groups.get('prerelversion'), s,
- pad_zeros_length=1)
- parts.append(tuple(block))
- else:
- parts.append(FINAL_MARKER)
-
- # postdev
- if groups.get('postdev'):
- post = groups.get('post')
- dev = groups.get('dev')
- postdev = []
- if post is not None:
- postdev.extend([FINAL_MARKER[0], 'post', int(post)])
- if dev is None:
- postdev.append(FINAL_MARKER[0])
- if dev is not None:
- postdev.extend(['dev', int(dev)])
- parts.append(tuple(postdev))
- else:
- parts.append(FINAL_MARKER)
- self.parts = tuple(parts)
- if error_on_huge_major_num and self.parts[0][0] > 1980:
- raise HugeMajorVersionNumError(
- "huge major version number, %r, "
- "which might cause future problems: %r" % (self.parts[0][0], s))
-
- def _parse_numdots(self, s, full_ver_str, drop_trailing_zeros=True,
- pad_zeros_length=0):
- """Parse 'N.N.N' sequences, return a list of ints.
-
- @param s {str} 'N.N.N..." sequence to be parsed
- @param full_ver_str {str} The full version string from which this
- comes. Used for error strings.
- @param drop_trailing_zeros {bool} Whether to drop trailing zeros
- from the returned list. Default True.
- @param pad_zeros_length {int} The length to which to pad the
- returned list with zeros, if necessary. Default 0.
- """
- nums = []
- for n in s.split("."):
- if len(n) > 1 and n[0] == '0':
- raise IrrationalVersionError(
- "cannot have leading zero in "
- "version number segment: '%s' in %r" % (n, full_ver_str))
- nums.append(int(n))
- if drop_trailing_zeros:
- while nums and nums[-1] == 0:
- nums.pop()
- while len(nums) < pad_zeros_length:
- nums.append(0)
- return nums
-
- def __str__(self):
- return self.parts_to_str(self.parts)
-
- @classmethod
- def parts_to_str(cls, parts):
- """Transforms a version expressed in tuple into its string
- representation."""
- # XXX This doesn't check for invalid tuples
- main, prerel, postdev = parts
- s = '.'.join(str(v) for v in main)
- if prerel is not FINAL_MARKER:
- s += prerel[0]
- s += '.'.join(str(v) for v in prerel[1:])
- if postdev and postdev is not FINAL_MARKER:
- if postdev[0] == 'f':
- postdev = postdev[1:]
- i = 0
- while i < len(postdev):
- if i % 2 == 0:
- s += '.'
- s += str(postdev[i])
- i += 1
- return s
-
- def __repr__(self):
- return "%s('%s')" % (self.__class__.__name__, self)
-
- def _cannot_compare(self, other):
- raise TypeError("cannot compare %s and %s"
- % (type(self).__name__, type(other).__name__))
-
- def __eq__(self, other):
- if not isinstance(other, NormalizedVersion):
- self._cannot_compare(other)
- return self.parts == other.parts
-
- def __lt__(self, other):
- if not isinstance(other, NormalizedVersion):
- self._cannot_compare(other)
- return self.parts < other.parts
-
- def __ne__(self, other):
- return not self.__eq__(other)
-
- def __gt__(self, other):
- return not (self.__lt__(other) or self.__eq__(other))
-
- def __le__(self, other):
- return self.__eq__(other) or self.__lt__(other)
-
- def __ge__(self, other):
- return self.__eq__(other) or self.__gt__(other)
-
-
-def suggest_normalized_version(s):
- """Suggest a normalized version close to the given version string.
-
- If you have a version string that isn't rational (i.e. NormalizedVersion
- doesn't like it) then you might be able to get an equivalent (or close)
- rational version from this function.
-
- This does a number of simple normalizations to the given string, based
- on observation of versions currently in use on PyPI. Given a dump of
- those version during PyCon 2009, 4287 of them:
- - 2312 (53.93%) match NormalizedVersion without change
- - with the automatic suggestion
- - 3474 (81.04%) match when using this suggestion method
-
- @param s {str} An irrational version string.
- @returns A rational version string, or None, if couldn't determine one.
- """
- try:
- NormalizedVersion(s)
- return s # already rational
- except IrrationalVersionError:
- pass
-
- rs = s.lower()
-
- # part of this could use maketrans
- for orig, repl in (('-alpha', 'a'), ('-beta', 'b'), ('alpha', 'a'),
- ('beta', 'b'), ('rc', 'c'), ('-final', ''),
- ('-pre', 'c'),
- ('-release', ''), ('.release', ''), ('-stable', ''),
- ('+', '.'), ('_', '.'), (' ', ''), ('.final', ''),
- ('final', '')):
- rs = rs.replace(orig, repl)
-
- # if something ends with dev or pre, we add a 0
- rs = re.sub(r"pre$", r"pre0", rs)
- rs = re.sub(r"dev$", r"dev0", rs)
-
- # if we have something like "b-2" or "a.2" at the end of the
- # version, that is pobably beta, alpha, etc
- # let's remove the dash or dot
- rs = re.sub(r"([abc|rc])[\-\.](\d+)$", r"\1\2", rs)
-
- # 1.0-dev-r371 -> 1.0.dev371
- # 0.1-dev-r79 -> 0.1.dev79
- rs = re.sub(r"[\-\.](dev)[\-\.]?r?(\d+)$", r".\1\2", rs)
-
- # Clean: 2.0.a.3, 2.0.b1, 0.9.0~c1
- rs = re.sub(r"[.~]?([abc])\.?", r"\1", rs)
-
- # Clean: v0.3, v1.0
- if rs.startswith('v'):
- rs = rs[1:]
-
- # Clean leading '0's on numbers.
- # TODO: unintended side-effect on, e.g., "2003.05.09"
- # PyPI stats: 77 (~2%) better
- rs = re.sub(r"\b0+(\d+)(?!\d)", r"\1", rs)
-
- # Clean a/b/c with no version. E.g. "1.0a" -> "1.0a0". Setuptools infers
- # zero.
- # PyPI stats: 245 (7.56%) better
- rs = re.sub(r"(\d+[abc])$", r"\g<1>0", rs)
-
- # the 'dev-rNNN' tag is a dev tag
- rs = re.sub(r"\.?(dev-r|dev\.r)\.?(\d+)$", r".dev\2", rs)
-
- # clean the - when used as a pre delimiter
- rs = re.sub(r"-(a|b|c)(\d+)$", r"\1\2", rs)
-
- # a terminal "dev" or "devel" can be changed into ".dev0"
- rs = re.sub(r"[\.\-](dev|devel)$", r".dev0", rs)
-
- # a terminal "dev" can be changed into ".dev0"
- rs = re.sub(r"(?![\.\-])dev$", r".dev0", rs)
-
- # a terminal "final" or "stable" can be removed
- rs = re.sub(r"(final|stable)$", "", rs)
-
- # The 'r' and the '-' tags are post release tags
- # 0.4a1.r10 -> 0.4a1.post10
- # 0.9.33-17222 -> 0.9.3.post17222
- # 0.9.33-r17222 -> 0.9.3.post17222
- rs = re.sub(r"\.?(r|-|-r)\.?(\d+)$", r".post\2", rs)
-
- # Clean 'r' instead of 'dev' usage:
- # 0.9.33+r17222 -> 0.9.3.dev17222
- # 1.0dev123 -> 1.0.dev123
- # 1.0.git123 -> 1.0.dev123
- # 1.0.bzr123 -> 1.0.dev123
- # 0.1a0dev.123 -> 0.1a0.dev123
- # PyPI stats: ~150 (~4%) better
- rs = re.sub(r"\.?(dev|git|bzr)\.?(\d+)$", r".dev\2", rs)
-
- # Clean '.pre' (normalized from '-pre' above) instead of 'c' usage:
- # 0.2.pre1 -> 0.2c1
- # 0.2-c1 -> 0.2c1
- # 1.0preview123 -> 1.0c123
- # PyPI stats: ~21 (0.62%) better
- rs = re.sub(r"\.?(pre|preview|-c)(\d+)$", r"c\g<2>", rs)
-
- # Tcl/Tk uses "px" for their post release markers
- rs = re.sub(r"p(\d+)$", r".post\1", rs)
-
- try:
- NormalizedVersion(rs)
- return rs # already rational
- except IrrationalVersionError:
- pass
- return None
diff --git a/tox/config.py b/tox/config.py
deleted file mode 100644
index ad453f6..0000000
--- a/tox/config.py
+++ /dev/null
@@ -1,1247 +0,0 @@
-import argparse
-import os
-import random
-from fnmatch import fnmatchcase
-import sys
-import re
-import shlex
-import string
-import pkg_resources
-import itertools
-import pluggy
-
-import tox.interpreters
-from tox import hookspecs
-from tox._verlib import NormalizedVersion
-
-import py
-
-import tox
-
-iswin32 = sys.platform == "win32"
-
-default_factors = {'jython': 'jython', 'pypy': 'pypy', 'pypy3': 'pypy3',
- 'py': sys.executable}
-for version in '26,27,32,33,34,35,36'.split(','):
- default_factors['py' + version] = 'python%s.%s' % tuple(version)
-
-hookimpl = pluggy.HookimplMarker("tox")
-
-_dummy = object()
-
-
-def get_plugin_manager(plugins=()):
- # initialize plugin manager
- import tox.venv
- pm = pluggy.PluginManager("tox")
- pm.add_hookspecs(hookspecs)
- pm.register(tox.config)
- pm.register(tox.interpreters)
- pm.register(tox.venv)
- pm.register(tox.session)
- pm.load_setuptools_entrypoints("tox")
- for plugin in plugins:
- pm.register(plugin)
- pm.check_pending()
- return pm
-
-
-class Parser:
- """ command line and ini-parser control object. """
-
- def __init__(self):
- self.argparser = argparse.ArgumentParser(
- description="tox options", add_help=False)
- self._testenv_attr = []
-
- def add_argument(self, *args, **kwargs):
- """ add argument to command line parser. This takes the
- same arguments that ``argparse.ArgumentParser.add_argument``.
- """
- return self.argparser.add_argument(*args, **kwargs)
-
- def add_testenv_attribute(self, name, type, help, default=None, postprocess=None):
- """ add an ini-file variable for "testenv" section.
-
- Types are specified as strings like "bool", "line-list", "string", "argv", "path",
- "argvlist".
-
- The ``postprocess`` function will be called for each testenv
- like ``postprocess(testenv_config=testenv_config, value=value)``
- where ``value`` is the value as read from the ini (or the default value)
- and ``testenv_config`` is a :py:class:`tox.config.TestenvConfig` instance
- which will receive all ini-variables as object attributes.
-
- Any postprocess function must return a value which will then be set
- as the final value in the testenv section.
- """
- self._testenv_attr.append(VenvAttribute(name, type, default, help, postprocess))
-
- def add_testenv_attribute_obj(self, obj):
- """ add an ini-file variable as an object.
-
- This works as the ``add_testenv_attribute`` function but expects
- "name", "type", "help", and "postprocess" attributes on the object.
- """
- assert hasattr(obj, "name")
- assert hasattr(obj, "type")
- assert hasattr(obj, "help")
- assert hasattr(obj, "postprocess")
- self._testenv_attr.append(obj)
-
- def _parse_args(self, args):
- return self.argparser.parse_args(args)
-
- def _format_help(self):
- return self.argparser.format_help()
-
-
-class VenvAttribute:
- def __init__(self, name, type, default, help, postprocess):
- self.name = name
- self.type = type
- self.default = default
- self.help = help
- self.postprocess = postprocess
-
-
-class DepOption:
- name = "deps"
- type = "line-list"
- help = "each line specifies a dependency in pip/setuptools format."
- default = ()
-
- def postprocess(self, testenv_config, value):
- deps = []
- config = testenv_config.config
- for depline in value:
- m = re.match(r":(\w+):\s*(\S+)", depline)
- if m:
- iname, name = m.groups()
- ixserver = config.indexserver[iname]
- else:
- name = depline.strip()
- ixserver = None
- name = self._replace_forced_dep(name, config)
- deps.append(DepConfig(name, ixserver))
- return deps
-
- def _replace_forced_dep(self, name, config):
- """
- Override the given dependency config name taking --force-dep-version
- option into account.
-
- :param name: dep config, for example ["pkg==1.0", "other==2.0"].
- :param config: Config instance
- :return: the new dependency that should be used for virtual environments
- """
- if not config.option.force_dep:
- return name
- for forced_dep in config.option.force_dep:
- if self._is_same_dep(forced_dep, name):
- return forced_dep
- return name
-
- @classmethod
- def _is_same_dep(cls, dep1, dep2):
- """
- Returns True if both dependency definitions refer to the
- same package, even if versions differ.
- """
- dep1_name = pkg_resources.Requirement.parse(dep1).project_name
- try:
- dep2_name = pkg_resources.Requirement.parse(dep2).project_name
- except pkg_resources.RequirementParseError:
- # we couldn't parse a version, probably a URL
- return False
- return dep1_name == dep2_name
-
-
-class PosargsOption:
- name = "args_are_paths"
- type = "bool"
- default = True
- help = "treat positional args in commands as paths"
-
- def postprocess(self, testenv_config, value):
- config = testenv_config.config
- args = config.option.args
- if args:
- if value:
- args = []
- for arg in config.option.args:
- if arg:
- origpath = config.invocationcwd.join(arg, abs=True)
- if origpath.check():
- arg = testenv_config.changedir.bestrelpath(origpath)
- args.append(arg)
- testenv_config._reader.addsubstitutions(args)
- return value
-
-
-class InstallcmdOption:
- name = "install_command"
- type = "argv"
- default = "pip install {opts} {packages}"
- help = "install command for dependencies and package under test."
-
- def postprocess(self, testenv_config, value):
- if '{packages}' not in value:
- raise tox.exception.ConfigError(
- "'install_command' must contain '{packages}' substitution")
- return value
-
-
-def parseconfig(args=None, plugins=()):
- """
- :param list[str] args: Optional list of arguments.
- :type pkg: str
- :rtype: :class:`Config`
- :raise SystemExit: toxinit file is not found
- """
-
- pm = get_plugin_manager(plugins)
-
- if args is None:
- args = sys.argv[1:]
-
- # prepare command line options
- parser = Parser()
- pm.hook.tox_addoption(parser=parser)
-
- # parse command line options
- option = parser._parse_args(args)
- interpreters = tox.interpreters.Interpreters(hook=pm.hook)
- config = Config(pluginmanager=pm, option=option, interpreters=interpreters)
- config._parser = parser
- config._testenv_attr = parser._testenv_attr
-
- # parse ini file
- basename = config.option.configfile
- if os.path.isabs(basename):
- inipath = py.path.local(basename)
- else:
- for path in py.path.local().parts(reverse=True):
- inipath = path.join(basename)
- if inipath.check():
- break
- else:
- inipath = py.path.local().join('setup.cfg')
- if not inipath.check():
- feedback("toxini file %r not found" % (basename), sysexit=True)
-
- try:
- parseini(config, inipath)
- except tox.exception.InterpreterNotFound:
- exn = sys.exc_info()[1]
- # Use stdout to match test expectations
- py.builtin.print_("ERROR: " + str(exn))
-
- # post process config object
- pm.hook.tox_configure(config=config)
-
- return config
-
-
-def feedback(msg, sysexit=False):
- py.builtin.print_("ERROR: " + msg, file=sys.stderr)
- if sysexit:
- raise SystemExit(1)
-
-
-class VersionAction(argparse.Action):
- def __call__(self, argparser, *args, **kwargs):
- version = tox.__version__
- py.builtin.print_("%s imported from %s" % (version, tox.__file__))
- raise SystemExit(0)
-
-
-class CountAction(argparse.Action):
- def __call__(self, parser, namespace, values, option_string=None):
- if hasattr(namespace, self.dest):
- setattr(namespace, self.dest, int(getattr(namespace, self.dest)) + 1)
- else:
- setattr(namespace, self.dest, 0)
-
-
-class SetenvDict:
- def __init__(self, dict, reader):
- self.reader = reader
- self.definitions = dict
- self.resolved = {}
- self._lookupstack = []
-
- def __contains__(self, name):
- return name in self.definitions
-
- def get(self, name, default=None):
- try:
- return self.resolved[name]
- except KeyError:
- try:
- if name in self._lookupstack:
- raise KeyError(name)
- val = self.definitions[name]
- except KeyError:
- return os.environ.get(name, default)
- self._lookupstack.append(name)
- try:
- self.resolved[name] = res = self.reader._replace(val)
- finally:
- self._lookupstack.pop()
- return res
-
- def __getitem__(self, name):
- x = self.get(name, _dummy)
- if x is _dummy:
- raise KeyError(name)
- return x
-
- def keys(self):
- return self.definitions.keys()
-
- def __setitem__(self, name, value):
- self.definitions[name] = value
- self.resolved[name] = value
-
-
-@hookimpl
-def tox_addoption(parser):
- # formatter_class=argparse.ArgumentDefaultsHelpFormatter)
- parser.add_argument("--version", nargs=0, action=VersionAction,
- dest="version",
- help="report version information to stdout.")
- parser.add_argument("-h", "--help", action="store_true", dest="help",
- help="show help about options")
- parser.add_argument("--help-ini", "--hi", action="store_true", dest="helpini",
- help="show help about ini-names")
- parser.add_argument("-v", nargs=0, action=CountAction, default=0,
- dest="verbosity",
- help="increase verbosity of reporting output.")
- parser.add_argument("--showconfig", action="store_true",
- help="show configuration information for all environments. ")
- parser.add_argument("-l", "--listenvs", action="store_true",
- dest="listenvs", help="show list of test environments")
- parser.add_argument("-c", action="store", default="tox.ini",
- dest="configfile",
- help="use the specified config file name.")
- parser.add_argument("-e", action="append", dest="env",
- metavar="envlist",
- help="work against specified environments (ALL selects all).")
- parser.add_argument("--notest", action="store_true", dest="notest",
- help="skip invoking test commands.")
- parser.add_argument("--sdistonly", action="store_true", dest="sdistonly",
- help="only perform the sdist packaging activity.")
- parser.add_argument("--installpkg", action="store", default=None,
- metavar="PATH",
- help="use specified package for installation into venv, instead of "
- "creating an sdist.")
- parser.add_argument("--develop", action="store_true", dest="develop",
- help="install package in the venv using 'setup.py develop' via "
- "'pip -e .'")
- parser.add_argument('-i', action="append",
- dest="indexurl", metavar="URL",
- help="set indexserver url (if URL is of form name=url set the "
- "url for the 'name' indexserver, specifically)")
- parser.add_argument("--pre", action="store_true", dest="pre",
- help="install pre-releases and development versions of dependencies. "
- "This will pass the --pre option to install_command "
- "(pip by default).")
- parser.add_argument("-r", "--recreate", action="store_true",
- dest="recreate",
- help="force recreation of virtual environments")
- parser.add_argument("--result-json", action="store",
- dest="resultjson", metavar="PATH",
- help="write a json file with detailed information "
- "about all commands and results involved.")
-
- # We choose 1 to 4294967295 because it is the range of PYTHONHASHSEED.
- parser.add_argument("--hashseed", action="store",
- metavar="SEED", default=None,
- help="set PYTHONHASHSEED to SEED before running commands. "
- "Defaults to a random integer in the range [1, 4294967295] "
- "([1, 1024] on Windows). "
- "Passing 'noset' suppresses this behavior.")
- parser.add_argument("--force-dep", action="append",
- metavar="REQ", default=None,
- help="Forces a certain version of one of the dependencies "
- "when configuring the virtual environment. REQ Examples "
- "'pytest<2.7' or 'django>=1.6'.")
- parser.add_argument("--sitepackages", action="store_true",
- help="override sitepackages setting to True in all envs")
- parser.add_argument("--skip-missing-interpreters", action="store_true",
- help="don't fail tests for missing interpreters")
- parser.add_argument("--workdir", action="store",
- dest="workdir", metavar="PATH", default=None,
- help="tox working directory")
-
- parser.add_argument("args", nargs="*",
- help="additional arguments available to command positional substitution")
-
- parser.add_testenv_attribute(
- name="envdir", type="path", default="{toxworkdir}/{envname}",
- help="venv directory")
-
- # add various core venv interpreter attributes
- def setenv(testenv_config, value):
- setenv = value
- config = testenv_config.config
- if "PYTHONHASHSEED" not in setenv and config.hashseed is not None:
- setenv['PYTHONHASHSEED'] = config.hashseed
- return setenv
-
- parser.add_testenv_attribute(
- name="setenv", type="dict_setenv", postprocess=setenv,
- help="list of X=Y lines with environment variable settings")
-
- def basepython_default(testenv_config, value):
- if value is None:
- for f in testenv_config.factors:
- if f in default_factors:
- return default_factors[f]
- return sys.executable
- return str(value)
-
- parser.add_testenv_attribute(
- name="basepython", type="string", default=None, postprocess=basepython_default,
- help="executable name or path of interpreter used to create a "
- "virtual test environment.")
-
- parser.add_testenv_attribute(
- name="envtmpdir", type="path", default="{envdir}/tmp",
- help="venv temporary directory")
-
- parser.add_testenv_attribute(
- name="envlogdir", type="path", default="{envdir}/log",
- help="venv log directory")
-
- def downloadcache(testenv_config, value):
- if value:
- # env var, if present, takes precedence
- downloadcache = os.environ.get("PIP_DOWNLOAD_CACHE", value)
- return py.path.local(downloadcache)
-
- parser.add_testenv_attribute(
- name="downloadcache", type="string", default=None, postprocess=downloadcache,
- help="(deprecated) set PIP_DOWNLOAD_CACHE.")
-
- parser.add_testenv_attribute(
- name="changedir", type="path", default="{toxinidir}",
- help="directory to change to when running commands")
-
- parser.add_testenv_attribute_obj(PosargsOption())
-
- parser.add_testenv_attribute(
- name="skip_install", type="bool", default=False,
- help="Do not install the current package. This can be used when "
- "you need the virtualenv management but do not want to install "
- "the current package")
-
- parser.add_testenv_attribute(
- name="ignore_errors", type="bool", default=False,
- help="if set to True all commands will be executed irrespective of their "
- "result error status.")
-
- def recreate(testenv_config, value):
- if testenv_config.config.option.recreate:
- return True
- return value
-
- parser.add_testenv_attribute(
- name="recreate", type="bool", default=False, postprocess=recreate,
- help="always recreate this test environment.")
-
- def passenv(testenv_config, value):
- # Flatten the list to deal with space-separated values.
- value = list(
- itertools.chain.from_iterable(
- [x.split(' ') for x in value]))
-
- passenv = set(["PATH", "PIP_INDEX_URL", "LANG", "LD_LIBRARY_PATH"])
-
- # read in global passenv settings
- p = os.environ.get("TOX_TESTENV_PASSENV", None)
- if p is not None:
- passenv.update(x for x in p.split() if x)
-
- # we ensure that tmp directory settings are passed on
- # we could also set it to the per-venv "envtmpdir"
- # but this leads to very long paths when run with jenkins
- # so we just pass it on by default for now.
- if sys.platform == "win32":
- passenv.add("SYSTEMDRIVE") # needed for pip6
- passenv.add("SYSTEMROOT") # needed for python's crypto module
- passenv.add("PATHEXT") # needed for discovering executables
- passenv.add("TEMP")
- passenv.add("TMP")
- else:
- passenv.add("TMPDIR")
- for spec in value:
- for name in os.environ:
- if fnmatchcase(name.upper(), spec.upper()):
- passenv.add(name)
- return passenv
-
- parser.add_testenv_attribute(
- name="passenv", type="line-list", postprocess=passenv,
- help="environment variables needed during executing test commands "
- "(taken from invocation environment). Note that tox always "
- "passes through some basic environment variables which are "
- "needed for basic functioning of the Python system. "
- "See --showconfig for the eventual passenv setting.")
-
- parser.add_testenv_attribute(
- name="whitelist_externals", type="line-list",
- help="each lines specifies a path or basename for which tox will not warn "
- "about it coming from outside the test environment.")
-
- parser.add_testenv_attribute(
- name="platform", type="string", default=".*",
- help="regular expression which must match against ``sys.platform``. "
- "otherwise testenv will be skipped.")
-
- def sitepackages(testenv_config, value):
- return testenv_config.config.option.sitepackages or value
-
- parser.add_testenv_attribute(
- name="sitepackages", type="bool", default=False, postprocess=sitepackages,
- help="Set to ``True`` if you want to create virtual environments that also "
- "have access to globally installed packages.")
-
- def pip_pre(testenv_config, value):
- return testenv_config.config.option.pre or value
-
- parser.add_testenv_attribute(
- name="pip_pre", type="bool", default=False, postprocess=pip_pre,
- help="If ``True``, adds ``--pre`` to the ``opts`` passed to "
- "the install command. ")
-
- def develop(testenv_config, value):
- option = testenv_config.config.option
- return not option.installpkg and (value or option.develop)
-
- parser.add_testenv_attribute(
- name="usedevelop", type="bool", postprocess=develop, default=False,
- help="install package in develop/editable mode")
-
- parser.add_testenv_attribute_obj(InstallcmdOption())
-
- parser.add_testenv_attribute(
- name="list_dependencies_command",
- type="argv",
- default="pip freeze",
- help="list dependencies for a virtual environment")
-
- parser.add_testenv_attribute_obj(DepOption())
-
- parser.add_testenv_attribute(
- name="commands", type="argvlist", default="",
- help="each line specifies a test command and can use substitution.")
-
- parser.add_testenv_attribute(
- "ignore_outcome", type="bool", default=False,
- help="if set to True a failing result of this testenv will not make "
- "tox fail, only a warning will be produced")
-
-
-class Config(object):
- """ Global Tox config object. """
- def __init__(self, pluginmanager, option, interpreters):
- #: dictionary containing envname to envconfig mappings
- self.envconfigs = {}
- self.invocationcwd = py.path.local()
- self.interpreters = interpreters
- self.pluginmanager = pluginmanager
- #: option namespace containing all parsed command line options
- self.option = option
-
- @property
- def homedir(self):
- homedir = get_homedir()
- if homedir is None:
- homedir = self.toxinidir # XXX good idea?
- return homedir
-
-
-class TestenvConfig:
- """ Testenv Configuration object.
- In addition to some core attributes/properties this config object holds all
- per-testenv ini attributes as attributes, see "tox --help-ini" for an overview.
- """
- def __init__(self, envname, config, factors, reader):
- #: test environment name
- self.envname = envname
- #: global tox config object
- self.config = config
- #: set of factors
- self.factors = factors
- self._reader = reader
-
- def get_envbindir(self):
- """ path to directory where scripts/binaries reside. """
- if (sys.platform == "win32"
- and "jython" not in self.basepython
- and "pypy" not in self.basepython):
- return self.envdir.join("Scripts")
- else:
- return self.envdir.join("bin")
-
- @property
- def envbindir(self):
- return self.get_envbindir()
-
- @property
- def envpython(self):
- """ path to python executable. """
- return self.get_envpython()
-
- def get_envpython(self):
- """ path to python/jython executable. """
- if "jython" in str(self.basepython):
- name = "jython"
- else:
- name = "python"
- return self.envbindir.join(name)
-
- def get_envsitepackagesdir(self):
- """ return sitepackagesdir of the virtualenv environment.
- (only available during execution, not parsing)
- """
- x = self.config.interpreters.get_sitepackagesdir(
- info=self.python_info,
- envdir=self.envdir)
- return x
-
- @property
- def python_info(self):
- """ return sitepackagesdir of the virtualenv environment. """
- return self.config.interpreters.get_info(envconfig=self)
-
- def getsupportedinterpreter(self):
- if sys.platform == "win32" and self.basepython and \
- "jython" in self.basepython:
- raise tox.exception.UnsupportedInterpreter(
- "Jython/Windows does not support installing scripts")
- info = self.config.interpreters.get_info(envconfig=self)
- if not info.executable:
- raise tox.exception.InterpreterNotFound(self.basepython)
- if not info.version_info:
- raise tox.exception.InvocationError(
- 'Failed to get version_info for %s: %s' % (info.name, info.err))
- if info.version_info < (2, 6):
- raise tox.exception.UnsupportedInterpreter(
- "python2.5 is not supported anymore, sorry")
- return info.executable
-
-
-testenvprefix = "testenv:"
-
-
-def get_homedir():
- try:
- return py.path.local._gethomedir()
- except Exception:
- return None
-
-
-def make_hashseed():
- max_seed = 4294967295
- if sys.platform == 'win32':
- max_seed = 1024
- return str(random.randint(1, max_seed))
-
-
-class parseini:
- def __init__(self, config, inipath):
- config.toxinipath = inipath
- config.toxinidir = config.toxinipath.dirpath()
-
- self._cfg = py.iniconfig.IniConfig(config.toxinipath)
- config._cfg = self._cfg
- self.config = config
-
- if inipath.basename == 'setup.cfg':
- prefix = 'tox'
- else:
- prefix = None
- ctxname = getcontextname()
- if ctxname == "jenkins":
- reader = SectionReader("tox:jenkins", self._cfg, prefix=prefix,
- fallbacksections=['tox'])
- distshare_default = "{toxworkdir}/distshare"
- elif not ctxname:
- reader = SectionReader("tox", self._cfg, prefix=prefix)
- distshare_default = "{homedir}/.tox/distshare"
- else:
- raise ValueError("invalid context")
-
- if config.option.hashseed is None:
- hashseed = make_hashseed()
- elif config.option.hashseed == 'noset':
- hashseed = None
- else:
- hashseed = config.option.hashseed
- config.hashseed = hashseed
-
- reader.addsubstitutions(toxinidir=config.toxinidir,
- homedir=config.homedir)
- # As older versions of tox may have bugs or incompatabilities that
- # prevent parsing of tox.ini this must be the first thing checked.
- config.minversion = reader.getstring("minversion", None)
- if config.minversion:
- minversion = NormalizedVersion(self.config.minversion)
- toxversion = NormalizedVersion(tox.__version__)
- if toxversion < minversion:
- raise tox.exception.MinVersionError(
- "tox version is %s, required is at least %s" % (
- toxversion, minversion))
- if config.option.workdir is None:
- config.toxworkdir = reader.getpath("toxworkdir", "{toxinidir}/.tox")
- else:
- config.toxworkdir = config.toxinidir.join(config.option.workdir, abs=True)
-
- if not config.option.skip_missing_interpreters:
- config.option.skip_missing_interpreters = \
- reader.getbool("skip_missing_interpreters", False)
-
- # determine indexserver dictionary
- config.indexserver = {'default': IndexServerConfig('default')}
- prefix = "indexserver"
- for line in reader.getlist(prefix):
- name, url = map(lambda x: x.strip(), line.split("=", 1))
- config.indexserver[name] = IndexServerConfig(name, url)
-
- override = False
- if config.option.indexurl:
- for urldef in config.option.indexurl:
- m = re.match(r"\W*(\w+)=(\S+)", urldef)
- if m is None:
- url = urldef
- name = "default"
- else:
- name, url = m.groups()
- if not url:
- url = None
- if name != "ALL":
- config.indexserver[name].url = url
- else:
- override = url
- # let ALL override all existing entries
- if override:
- for name in config.indexserver:
- config.indexserver[name] = IndexServerConfig(name, override)
-
- reader.addsubstitutions(toxworkdir=config.toxworkdir)
- config.distdir = reader.getpath("distdir", "{toxworkdir}/dist")
- reader.addsubstitutions(distdir=config.distdir)
- config.distshare = reader.getpath("distshare", distshare_default)
- reader.addsubstitutions(distshare=config.distshare)
- config.sdistsrc = reader.getpath("sdistsrc", None)
- config.setupdir = reader.getpath("setupdir", "{toxinidir}")
- config.logdir = config.toxworkdir.join("log")
-
- config.envlist, all_envs = self._getenvdata(reader)
-
- # factors used in config or predefined
- known_factors = self._list_section_factors("testenv")
- known_factors.update(default_factors)
- known_factors.add("python")
-
- # factors stated in config envlist
- stated_envlist = reader.getstring("envlist", replace=False)
- if stated_envlist:
- for env in _split_env(stated_envlist):
- known_factors.update(env.split('-'))
-
- # configure testenvs
- for name in all_envs:
- section = testenvprefix + name
- factors = set(name.split('-'))
- if section in self._cfg or factors <= known_factors:
- config.envconfigs[name] = \
- self.make_envconfig(name, section, reader._subs, config)
-
- all_develop = all(name in config.envconfigs
- and config.envconfigs[name].usedevelop
- for name in config.envlist)
-
- config.skipsdist = reader.getbool("skipsdist", all_develop)
-
- def _list_section_factors(self, section):
- factors = set()
- if section in self._cfg:
- for _, value in self._cfg[section].items():
- exprs = re.findall(r'^([\w{}\.,-]+)\:\s+', value, re.M)
- factors.update(*mapcat(_split_factor_expr, exprs))
- return factors
-
- def make_envconfig(self, name, section, subs, config):
- factors = set(name.split('-'))
- reader = SectionReader(section, self._cfg, fallbacksections=["testenv"],
- factors=factors)
- vc = TestenvConfig(config=config, envname=name, factors=factors, reader=reader)
- reader.addsubstitutions(**subs)
- reader.addsubstitutions(envname=name)
- reader.addsubstitutions(envbindir=vc.get_envbindir,
- envsitepackagesdir=vc.get_envsitepackagesdir,
- envpython=vc.get_envpython)
-
- for env_attr in config._testenv_attr:
- atype = env_attr.type
- if atype in ("bool", "path", "string", "dict", "dict_setenv", "argv", "argvlist"):
- meth = getattr(reader, "get" + atype)
- res = meth(env_attr.name, env_attr.default)
- elif atype == "space-separated-list":
- res = reader.getlist(env_attr.name, sep=" ")
- elif atype == "line-list":
- res = reader.getlist(env_attr.name, sep="\n")
- else:
- raise ValueError("unknown type %r" % (atype,))
-
- if env_attr.postprocess:
- res = env_attr.postprocess(testenv_config=vc, value=res)
- setattr(vc, env_attr.name, res)
-
- if atype == "path":
- reader.addsubstitutions(**{env_attr.name: res})
-
- return vc
-
- def _getenvdata(self, reader):
- envstr = self.config.option.env \
- or os.environ.get("TOXENV") \
- or reader.getstring("envlist", replace=False) \
- or []
- envlist = _split_env(envstr)
-
- # collect section envs
- all_envs = set(envlist) - set(["ALL"])
- for section in self._cfg:
- if section.name.startswith(testenvprefix):
- all_envs.add(section.name[len(testenvprefix):])
- if not all_envs:
- all_envs.add("python")
-
- if not envlist or "ALL" in envlist:
- envlist = sorted(all_envs)
-
- return envlist, all_envs
-
-
-def _split_env(env):
- """if handed a list, action="append" was used for -e """
- if not isinstance(env, list):
- if '\n' in env:
- env = ','.join(env.split('\n'))
- env = [env]
- return mapcat(_expand_envstr, env)
-
-
-def _split_factor_expr(expr):
- partial_envs = _expand_envstr(expr)
- return [set(e.split('-')) for e in partial_envs]
-
-
-def _expand_envstr(envstr):
- # split by commas not in groups
- tokens = re.split(r'((?:\{[^}]+\})+)|,', envstr)
- envlist = [''.join(g).strip()
- for k, g in itertools.groupby(tokens, key=bool) if k]
-
- def expand(env):
- tokens = re.split(r'\{([^}]+)\}', env)
- parts = [token.split(',') for token in tokens]
- return [''.join(variant) for variant in itertools.product(*parts)]
-
- return mapcat(expand, envlist)
-
-
-def mapcat(f, seq):
- return list(itertools.chain.from_iterable(map(f, seq)))
-
-
-class DepConfig:
- def __init__(self, name, indexserver=None):
- self.name = name
- self.indexserver = indexserver
-
- def __str__(self):
- if self.indexserver:
- if self.indexserver.name == "default":
- return self.name
- return ":%s:%s" % (self.indexserver.name, self.name)
- return str(self.name)
- __repr__ = __str__
-
-
-class IndexServerConfig:
- def __init__(self, name, url=None):
- self.name = name
- self.url = url
-
-
-#: Check value matches substitution form
-#: of referencing value from other section. E.g. {[base]commands}
-is_section_substitution = re.compile("{\[[^{}\s]+\]\S+?}").match
-
-
-class SectionReader:
- def __init__(self, section_name, cfgparser, fallbacksections=None,
- factors=(), prefix=None):
- if prefix is None:
- self.section_name = section_name
- else:
- self.section_name = "%s:%s" % (prefix, section_name)
- self._cfg = cfgparser
- self.fallbacksections = fallbacksections or []
- self.factors = factors
- self._subs = {}
- self._subststack = []
- self._setenv = None
-
- def get_environ_value(self, name):
- if self._setenv is None:
- return os.environ.get(name)
- return self._setenv.get(name)
-
- def addsubstitutions(self, _posargs=None, **kw):
- self._subs.update(kw)
- if _posargs:
- self.posargs = _posargs
-
- def getpath(self, name, defaultpath):
- toxinidir = self._subs['toxinidir']
- path = self.getstring(name, defaultpath)
- if path is not None:
- return toxinidir.join(path, abs=True)
-
- def getlist(self, name, sep="\n"):
- s = self.getstring(name, None)
- if s is None:
- return []
- return [x.strip() for x in s.split(sep) if x.strip()]
-
- def getdict(self, name, default=None, sep="\n"):
- value = self.getstring(name, None)
- return self._getdict(value, default=default, sep=sep)
-
- def getdict_setenv(self, name, default=None, sep="\n"):
- value = self.getstring(name, None, replace=True, crossonly=True)
- definitions = self._getdict(value, default=default, sep=sep)
- self._setenv = SetenvDict(definitions, reader=self)
- return self._setenv
-
- def _getdict(self, value, default, sep):
- if value is None:
- return default or {}
-
- d = {}
- for line in value.split(sep):
- if line.strip():
- name, rest = line.split('=', 1)
- d[name.strip()] = rest.strip()
-
- return d
-
- def getbool(self, name, default=None):
- s = self.getstring(name, default)
- if not s:
- s = default
- if s is None:
- raise KeyError("no config value [%s] %s found" % (
- self.section_name, name))
-
- if not isinstance(s, bool):
- if s.lower() == "true":
- s = True
- elif s.lower() == "false":
- s = False
- else:
- raise tox.exception.ConfigError(
- "boolean value %r needs to be 'True' or 'False'")
- return s
-
- def getargvlist(self, name, default=""):
- s = self.getstring(name, default, replace=False)
- return _ArgvlistReader.getargvlist(self, s)
-
- def getargv(self, name, default=""):
- return self.getargvlist(name, default)[0]
-
- def getstring(self, name, default=None, replace=True, crossonly=False):
- x = None
- for s in [self.section_name] + self.fallbacksections:
- try:
- x = self._cfg[s][name]
- break
- except KeyError:
- continue
-
- if x is None:
- x = default
- else:
- x = self._apply_factors(x)
-
- if replace and x and hasattr(x, 'replace'):
- x = self._replace(x, name=name, crossonly=crossonly)
- # print "getstring", self.section_name, name, "returned", repr(x)
- return x
-
- def _apply_factors(self, s):
- def factor_line(line):
- m = re.search(r'^([\w{}\.,-]+)\:\s+(.+)', line)
- if not m:
- return line
-
- expr, line = m.groups()
- if any(fs <= self.factors for fs in _split_factor_expr(expr)):
- return line
-
- lines = s.strip().splitlines()
- return '\n'.join(filter(None, map(factor_line, lines)))
-
- def _replace(self, value, name=None, section_name=None, crossonly=False):
- if '{' not in value:
- return value
-
- section_name = section_name if section_name else self.section_name
- self._subststack.append((section_name, name))
- try:
- return Replacer(self, crossonly=crossonly).do_replace(value)
- finally:
- assert self._subststack.pop() == (section_name, name)
-
-
-class Replacer:
- RE_ITEM_REF = re.compile(
- r'''
- (?<!\\)[{]
- (?:(?P<sub_type>[^[:{}]+):)? # optional sub_type for special rules
- (?P<substitution_value>[^{}]*) # substitution key
- [}]
- ''', re.VERBOSE)
-
- def __init__(self, reader, crossonly=False):
- self.reader = reader
- self.crossonly = crossonly
-
- def do_replace(self, x):
- return self.RE_ITEM_REF.sub(self._replace_match, x)
-
- def _replace_match(self, match):
- g = match.groupdict()
- sub_value = g['substitution_value']
- if self.crossonly:
- if sub_value.startswith("["):
- return self._substitute_from_other_section(sub_value)
- # in crossonly we return all other hits verbatim
- start, end = match.span()
- return match.string[start:end]
-
- # special case: opts and packages. Leave {opts} and
- # {packages} intact, they are replaced manually in
- # _venv.VirtualEnv.run_install_command.
- if sub_value in ('opts', 'packages'):
- return '{%s}' % sub_value
-
- try:
- sub_type = g['sub_type']
- except KeyError:
- raise tox.exception.ConfigError(
- "Malformed substitution; no substitution type provided")
-
- if sub_type == "env":
- return self._replace_env(match)
- if sub_type is not None:
- raise tox.exception.ConfigError(
- "No support for the %s substitution type" % sub_type)
- return self._replace_substitution(match)
-
- def _replace_env(self, match):
- match_value = match.group('substitution_value')
- if not match_value:
- raise tox.exception.ConfigError(
- 'env: requires an environment variable name')
-
- default = None
- envkey_split = match_value.split(':', 1)
-
- if len(envkey_split) is 2:
- envkey, default = envkey_split
- else:
- envkey = match_value
-
- envvalue = self.reader.get_environ_value(envkey)
- if envvalue is None:
- if default is None:
- raise tox.exception.ConfigError(
- "substitution env:%r: unknown environment variable %r "
- " or recursive definition." %
- (envkey, envkey))
- return default
- return envvalue
-
- def _substitute_from_other_section(self, key):
- if key.startswith("[") and "]" in key:
- i = key.find("]")
- section, item = key[1:i], key[i + 1:]
- cfg = self.reader._cfg
- if section in cfg and item in cfg[section]:
- if (section, item) in self.reader._subststack:
- raise ValueError('%s already in %s' % (
- (section, item), self.reader._subststack))
- x = str(cfg[section][item])
- return self.reader._replace(x, name=item, section_name=section,
- crossonly=self.crossonly)
-
- raise tox.exception.ConfigError(
- "substitution key %r not found" % key)
-
- def _replace_substitution(self, match):
- sub_key = match.group('substitution_value')
- val = self.reader._subs.get(sub_key, None)
- if val is None:
- val = self._substitute_from_other_section(sub_key)
- if py.builtin.callable(val):
- val = val()
- return str(val)
-
-
-class _ArgvlistReader:
- @classmethod
- def getargvlist(cls, reader, value):
- """Parse ``commands`` argvlist multiline string.
-
- :param str name: Key name in a section.
- :param str value: Content stored by key.
-
- :rtype: list[list[str]]
- :raise :class:`tox.exception.ConfigError`:
- line-continuation ends nowhere while resolving for specified section
- """
- commands = []
- current_command = ""
- for line in value.splitlines():
- line = line.rstrip()
- if not line:
- continue
- if line.endswith("\\"):
- current_command += " " + line[:-1]
- continue
- current_command += line
-
- if is_section_substitution(current_command):
- replaced = reader._replace(current_command, crossonly=True)
- commands.extend(cls.getargvlist(reader, replaced))
- else:
- commands.append(cls.processcommand(reader, current_command))
- current_command = ""
- else:
- if current_command:
- raise tox.exception.ConfigError(
- "line-continuation ends nowhere while resolving for [%s] %s" %
- (reader.section_name, "commands"))
- return commands
-
- @classmethod
- def processcommand(cls, reader, command):
- posargs = getattr(reader, "posargs", None)
-
- # Iterate through each word of the command substituting as
- # appropriate to construct the new command string. This
- # string is then broken up into exec argv components using
- # shlex.
- newcommand = ""
- for word in CommandParser(command).words():
- if word == "{posargs}" or word == "[]":
- if posargs:
- newcommand += " ".join(posargs)
- continue
- elif word.startswith("{posargs:") and word.endswith("}"):
- if posargs:
- newcommand += " ".join(posargs)
- continue
- else:
- word = word[9:-1]
- new_arg = ""
- new_word = reader._replace(word)
- new_word = reader._replace(new_word)
- new_word = new_word.replace('\\{', '{').replace('\\}', '}')
- new_arg += new_word
- newcommand += new_arg
-
- # Construct shlex object that will not escape any values,
- # use all values as is in argv.
- shlexer = shlex.shlex(newcommand, posix=True)
- shlexer.whitespace_split = True
- shlexer.escape = ''
- argv = list(shlexer)
- return argv
-
-
-class CommandParser(object):
-
- class State(object):
- def __init__(self):
- self.word = ''
- self.depth = 0
- self.yield_words = []
-
- def __init__(self, command):
- self.command = command
-
- def words(self):
- ps = CommandParser.State()
-
- def word_has_ended():
- return ((cur_char in string.whitespace and ps.word and
- ps.word[-1] not in string.whitespace) or
- (cur_char == '{' and ps.depth == 0 and not ps.word.endswith('\\')) or
- (ps.depth == 0 and ps.word and ps.word[-1] == '}') or
- (cur_char not in string.whitespace and ps.word and
- ps.word.strip() == ''))
-
- def yield_this_word():
- yieldword = ps.word
- ps.word = ''
- if yieldword:
- ps.yield_words.append(yieldword)
-
- def yield_if_word_ended():
- if word_has_ended():
- yield_this_word()
-
- def accumulate():
- ps.word += cur_char
-
- def push_substitution():
- ps.depth += 1
-
- def pop_substitution():
- ps.depth -= 1
-
- for cur_char in self.command:
- if cur_char in string.whitespace:
- if ps.depth == 0:
- yield_if_word_ended()
- accumulate()
- elif cur_char == '{':
- yield_if_word_ended()
- accumulate()
- push_substitution()
- elif cur_char == '}':
- accumulate()
- pop_substitution()
- else:
- yield_if_word_ended()
- accumulate()
-
- if ps.word.strip():
- yield_this_word()
- return ps.yield_words
-
-
-def getcontextname():
- if any(env in os.environ for env in ['JENKINS_URL', 'HUDSON_URL']):
- return 'jenkins'
- return None
diff --git a/tox/hookspecs.py b/tox/hookspecs.py
deleted file mode 100644
index 22e231d..0000000
--- a/tox/hookspecs.py
+++ /dev/null
@@ -1,59 +0,0 @@
-""" Hook specifications for tox.
-
-"""
-
-from pluggy import HookspecMarker, HookimplMarker
-
-hookspec = HookspecMarker("tox")
-hookimpl = HookimplMarker("tox")
-
-
-@hookspec
-def tox_addoption(parser):
- """ add command line options to the argparse-style parser object."""
-
-
-@hookspec
-def tox_configure(config):
- """ called after command line options have been parsed and the ini-file has
- been read. Please be aware that the config object layout may change as its
- API was not designed yet wrt to providing stability (it was an internal
- thing purely before tox-2.0). """
-
-
-@hookspec(firstresult=True)
-def tox_get_python_executable(envconfig):
- """ return a python executable for the given python base name.
- The first plugin/hook which returns an executable path will determine it.
-
- ``envconfig`` is the testenv configuration which contains
- per-testenv configuration, notably the ``.envname`` and ``.basepython``
- setting.
- """
-
-
-@hookspec
-def tox_testenv_create(venv, action):
- """ [experimental] perform creation action for this venv.
- """
-
-
-@hookspec
-def tox_testenv_install_deps(venv, action):
- """ [experimental] perform install dependencies action for this venv. """
-
-
-@hookspec
-def tox_runtest_pre(venv):
- """ [experimental] perform arbitrary action before running tests for this venv.
-
- This could be used to indicate that tests for a given venv have started, for instance.
- """
-
-
-@hookspec
-def tox_runtest_post(venv):
- """ [experimental] perform arbitrary action after running tests for this venv.
-
- This could be used to have per-venv test reporting of pass/fail status.
- """
diff --git a/tox/interpreters.py b/tox/interpreters.py
deleted file mode 100644
index e049f1d..0000000
--- a/tox/interpreters.py
+++ /dev/null
@@ -1,184 +0,0 @@
-import sys
-import py
-import re
-import inspect
-from tox import hookimpl
-
-
-class Interpreters:
- def __init__(self, hook):
- self.name2executable = {}
- self.executable2info = {}
- self.hook = hook
-
- def get_executable(self, envconfig):
- """ return path object to the executable for the given
- name (e.g. python2.6, python2.7, python etc.)
- if name is already an existing path, return name.
- If an interpreter cannot be found, return None.
- """
- try:
- return self.name2executable[envconfig.envname]
- except KeyError:
- exe = self.hook.tox_get_python_executable(envconfig=envconfig)
- self.name2executable[envconfig.envname] = exe
- return exe
-
- def get_info(self, envconfig):
- executable = self.get_executable(envconfig)
- name = envconfig.basepython
- if not executable:
- return NoInterpreterInfo(name=name)
- try:
- return self.executable2info[executable]
- except KeyError:
- info = run_and_get_interpreter_info(name, executable)
- self.executable2info[executable] = info
- return info
-
- def get_sitepackagesdir(self, info, envdir):
- if not info.executable:
- return ""
- envdir = str(envdir)
- try:
- res = exec_on_interpreter(info.executable,
- [inspect.getsource(sitepackagesdir),
- "print(sitepackagesdir(%r))" % envdir])
- except ExecFailed:
- val = sys.exc_info()[1]
- print("execution failed: %s -- %s" % (val.out, val.err))
- return ""
- else:
- return res["dir"]
-
-
-def run_and_get_interpreter_info(name, executable):
- assert executable
- try:
- result = exec_on_interpreter(executable,
- [inspect.getsource(pyinfo), "print(pyinfo())"])
- except ExecFailed:
- val = sys.exc_info()[1]
- return NoInterpreterInfo(name, executable=val.executable,
- out=val.out, err=val.err)
- else:
- return InterpreterInfo(name, executable, **result)
-
-
-def exec_on_interpreter(executable, source):
- if isinstance(source, list):
- source = "\n".join(source)
- from subprocess import Popen, PIPE
- args = [str(executable)]
- popen = Popen(args, stdin=PIPE, stdout=PIPE, stderr=PIPE)
- popen.stdin.write(source.encode("utf8"))
- out, err = popen.communicate()
- if popen.returncode:
- raise ExecFailed(executable, source, out, err)
- try:
- result = eval(out.strip())
- except Exception:
- raise ExecFailed(executable, source, out,
- "could not decode %r" % out)
- return result
-
-
-class ExecFailed(Exception):
- def __init__(self, executable, source, out, err):
- self.executable = executable
- self.source = source
- self.out = out
- self.err = err
-
-
-class InterpreterInfo:
- runnable = True
-
- def __init__(self, name, executable, version_info, sysplatform):
- assert executable and version_info
- self.name = name
- self.executable = executable
- self.version_info = version_info
- self.sysplatform = sysplatform
-
- def __str__(self):
- return "<executable at %s, version_info %s>" % (
- self.executable, self.version_info)
-
-
-class NoInterpreterInfo:
- runnable = False
-
- def __init__(self, name, executable=None,
- out=None, err="not found"):
- self.name = name
- self.executable = executable
- self.version_info = None
- self.out = out
- self.err = err
-
- def __str__(self):
- if self.executable:
- return "<executable at %s, not runnable>"
- else:
- return "<executable not found for: %s>" % self.name
-
-if sys.platform != "win32":
- @hookimpl
- def tox_get_python_executable(envconfig):
- return py.path.local.sysfind(envconfig.basepython)
-
-else:
- @hookimpl
- def tox_get_python_executable(envconfig):
- name = envconfig.basepython
- p = py.path.local.sysfind(name)
- if p:
- return p
- actual = None
- # Is this a standard PythonX.Y name?
- m = re.match(r"python(\d)\.(\d)", name)
- if m:
- # The standard names are in predictable places.
- actual = r"c:\python%s%s\python.exe" % m.groups()
- if not actual:
- actual = win32map.get(name, None)
- if actual:
- actual = py.path.local(actual)
- if actual.check():
- return actual
- # The standard executables can be found as a last resort via the
- # Python launcher py.exe
- if m:
- return locate_via_py(*m.groups())
-
- # Exceptions to the usual windows mapping
- win32map = {
- 'python': sys.executable,
- 'jython': "c:\jython2.5.1\jython.bat",
- }
-
- def locate_via_py(v_maj, v_min):
- ver = "-%s.%s" % (v_maj, v_min)
- script = "import sys; print(sys.executable)"
- py_exe = py.path.local.sysfind('py')
- if py_exe:
- try:
- exe = py_exe.sysexec(ver, '-c', script).strip()
- except py.process.cmdexec.Error:
- exe = None
- if exe:
- exe = py.path.local(exe)
- if exe.check():
- return exe
-
-
-def pyinfo():
- import sys
- return dict(version_info=tuple(sys.version_info),
- sysplatform=sys.platform)
-
-
-def sitepackagesdir(envdir):
- from distutils.sysconfig import get_python_lib
- return dict(dir=get_python_lib(prefix=envdir))
diff --git a/tox/result.py b/tox/result.py
deleted file mode 100644
index bad8cc3..0000000
--- a/tox/result.py
+++ /dev/null
@@ -1,81 +0,0 @@
-import sys
-import py
-from tox import __version__ as toxver
-import json
-
-
-class ResultLog:
-
- def __init__(self, dict=None):
- if dict is None:
- dict = {}
- self.dict = dict
- self.dict.update({"reportversion": "1", "toxversion": toxver})
- self.dict["platform"] = sys.platform
- self.dict["host"] = py.std.socket.getfqdn()
-
- def set_header(self, installpkg):
- """
- :param py.path.local installpkg: Path ot the package.
- """
- self.dict["installpkg"] = dict(
- md5=installpkg.computehash("md5"),
- sha256=installpkg.computehash("sha256"),
- basename=installpkg.basename,
- )
-
- def get_envlog(self, name):
- testenvs = self.dict.setdefault("testenvs", {})
- d = testenvs.setdefault(name, {})
- return EnvLog(self, name, d)
-
- def dumps_json(self):
- return json.dumps(self.dict, indent=2)
-
- @classmethod
- def loads_json(cls, data):
- return cls(json.loads(data))
-
-
-class EnvLog:
- def __init__(self, reportlog, name, dict):
- self.reportlog = reportlog
- self.name = name
- self.dict = dict
-
- def set_python_info(self, pythonexecutable):
- pythonexecutable = py.path.local(pythonexecutable)
- out = pythonexecutable.sysexec("-c",
- "import sys; "
- "print(sys.executable);"
- "print(list(sys.version_info)); "
- "print(sys.version)")
- lines = out.splitlines()
- executable = lines.pop(0)
- version_info = eval(lines.pop(0))
- version = "\n".join(lines)
- self.dict["python"] = dict(
- executable=executable,
- version_info=version_info,
- version=version)
-
- def get_commandlog(self, name):
- l = self.dict.setdefault(name, [])
- return CommandLog(self, l)
-
- def set_installed(self, packages):
- self.dict["installed_packages"] = packages
-
-
-class CommandLog:
- def __init__(self, envlog, list):
- self.envlog = envlog
- self.list = list
-
- def add_command(self, argv, output, retcode):
- d = {}
- self.list.append(d)
- d["command"] = argv
- d["output"] = output
- d["retcode"] = str(retcode)
- return d
diff --git a/tox/session.py b/tox/session.py
deleted file mode 100644
index 7b0ca3e..0000000
--- a/tox/session.py
+++ /dev/null
@@ -1,686 +0,0 @@
-"""
-Automatically package and test a Python project against configurable
-Python2 and Python3 based virtual environments. Environments are
-setup by using virtualenv. Configuration is generally done through an
-INI-style "tox.ini" file.
-"""
-from __future__ import with_statement
-
-import tox
-import py
-import os
-import sys
-import subprocess
-from tox._verlib import NormalizedVersion, IrrationalVersionError
-from tox.venv import VirtualEnv
-from tox.config import parseconfig
-from tox.result import ResultLog
-from subprocess import STDOUT
-
-
-def now():
- return py.std.time.time()
-
-
-def prepare(args):
- config = parseconfig(args)
- if config.option.help:
- show_help(config)
- raise SystemExit(0)
- elif config.option.helpini:
- show_help_ini(config)
- raise SystemExit(0)
- return config
-
-
-def main(args=None):
- try:
- config = prepare(args)
- retcode = Session(config).runcommand()
- raise SystemExit(retcode)
- except KeyboardInterrupt:
- raise SystemExit(2)
- except tox.exception.MinVersionError as e:
- r = Reporter(None)
- r.error(e.message)
- raise SystemExit(1)
-
-
-def show_help(config):
- tw = py.io.TerminalWriter()
- tw.write(config._parser._format_help())
- tw.line()
- tw.line("Environment variables", bold=True)
- tw.line("TOXENV: comma separated list of environments "
- "(overridable by '-e')")
- tw.line("TOX_TESTENV_PASSENV: space-separated list of extra "
- "environment variables to be passed into test command "
- "environments")
-
-
-def show_help_ini(config):
- tw = py.io.TerminalWriter()
- tw.sep("-", "per-testenv attributes")
- for env_attr in config._testenv_attr:
- tw.line("%-15s %-8s default: %s" %
- (env_attr.name, "<" + env_attr.type + ">", env_attr.default), bold=True)
- tw.line(env_attr.help)
- tw.line()
-
-
-class Action(object):
- def __init__(self, session, venv, msg, args):
- self.venv = venv
- self.msg = msg
- self.activity = msg.split(" ", 1)[0]
- self.session = session
- self.report = session.report
- self.args = args
- self.id = venv and venv.envconfig.envname or "tox"
- self._popenlist = []
- if self.venv:
- self.venvname = self.venv.name
- else:
- self.venvname = "GLOB"
- if msg == "runtests":
- cat = "test"
- else:
- cat = "setup"
- envlog = session.resultlog.get_envlog(self.venvname)
- self.commandlog = envlog.get_commandlog(cat)
-
- def __enter__(self):
- self.report.logaction_start(self)
-
- def __exit__(self, *args):
- self.report.logaction_finish(self)
-
- def setactivity(self, name, msg):
- self.activity = name
- self.report.verbosity0("%s %s: %s" % (self.venvname, name, msg), bold=True)
-
- def info(self, name, msg):
- self.report.verbosity1("%s %s: %s" % (self.venvname, name, msg), bold=True)
-
- def _initlogpath(self, actionid):
- if self.venv:
- logdir = self.venv.envconfig.envlogdir
- else:
- logdir = self.session.config.logdir
- try:
- l = logdir.listdir("%s-*" % actionid)
- except py.error.ENOENT:
- logdir.ensure(dir=1)
- l = []
- num = len(l)
- path = logdir.join("%s-%s.log" % (actionid, num))
- f = path.open('w')
- f.flush()
- return f
-
- def popen(self, args, cwd=None, env=None, redirect=True, returnout=False, ignore_ret=False):
- stdout = outpath = None
- resultjson = self.session.config.option.resultjson
- if resultjson or redirect:
- fout = self._initlogpath(self.id)
- fout.write("actionid: %s\nmsg: %s\ncmdargs: %r\nenv: %s\n\n" % (
- self.id, self.msg, args, env))
- fout.flush()
- self.popen_outpath = outpath = py.path.local(fout.name)
- fin = outpath.open()
- fin.read() # read the header, so it won't be written to stdout
- stdout = fout
- elif returnout:
- stdout = subprocess.PIPE
- if cwd is None:
- # XXX cwd = self.session.config.cwd
- cwd = py.path.local()
- try:
- popen = self._popen(args, cwd, env=env,
- stdout=stdout, stderr=STDOUT)
- except OSError as e:
- self.report.error("invocation failed (errno %d), args: %s, cwd: %s" %
- (e.errno, args, cwd))
- raise
- popen.outpath = outpath
- popen.args = [str(x) for x in args]
- popen.cwd = cwd
- popen.action = self
- self._popenlist.append(popen)
- try:
- self.report.logpopen(popen, env=env)
- try:
- if resultjson and not redirect:
- assert popen.stderr is None # prevent deadlock
- out = None
- last_time = now()
- while 1:
- fin_pos = fin.tell()
- # we have to read one byte at a time, otherwise there
- # might be no output for a long time with slow tests
- data = fin.read(1)
- if data:
- sys.stdout.write(data)
- if '\n' in data or (now() - last_time) > 1:
- # we flush on newlines or after 1 second to
- # provide quick enough feedback to the user
- # when printing a dot per test
- sys.stdout.flush()
- last_time = now()
- elif popen.poll() is not None:
- if popen.stdout is not None:
- popen.stdout.close()
- break
- else:
- py.std.time.sleep(0.1)
- fin.seek(fin_pos)
- fin.close()
- else:
- out, err = popen.communicate()
- except KeyboardInterrupt:
- self.report.keyboard_interrupt()
- popen.wait()
- raise KeyboardInterrupt()
- ret = popen.wait()
- finally:
- self._popenlist.remove(popen)
- if ret and not ignore_ret:
- invoked = " ".join(map(str, popen.args))
- if outpath:
- self.report.error("invocation failed (exit code %d), logfile: %s" %
- (ret, outpath))
- out = outpath.read()
- self.report.error(out)
- if hasattr(self, "commandlog"):
- self.commandlog.add_command(popen.args, out, ret)
- raise tox.exception.InvocationError(
- "%s (see %s)" % (invoked, outpath), ret)
- else:
- raise tox.exception.InvocationError("%r" % (invoked, ), ret)
- if not out and outpath:
- out = outpath.read()
- if hasattr(self, "commandlog"):
- self.commandlog.add_command(popen.args, out, ret)
- return out
-
- def _rewriteargs(self, cwd, args):
- newargs = []
- for arg in args:
- if sys.platform != "win32" and isinstance(arg, py.path.local):
- arg = cwd.bestrelpath(arg)
- newargs.append(str(arg))
-
- # subprocess does not always take kindly to .py scripts
- # so adding the interpreter here.
- if sys.platform == "win32":
- ext = os.path.splitext(str(newargs[0]))[1].lower()
- if ext == '.py' and self.venv:
- newargs = [str(self.venv.envconfig.envpython)] + newargs
-
- return newargs
-
- def _popen(self, args, cwd, stdout, stderr, env=None):
- args = self._rewriteargs(cwd, args)
- if env is None:
- env = os.environ.copy()
- return self.session.popen(args, shell=False, cwd=str(cwd),
- universal_newlines=True,
- stdout=stdout, stderr=stderr, env=env)
-
-
-class Reporter(object):
- actionchar = "-"
-
- def __init__(self, session):
- self.tw = py.io.TerminalWriter()
- self.session = session
- self._reportedlines = []
- # self.cumulated_time = 0.0
-
- def _get_verbosity(self):
- if self.session:
- return self.session.config.option.verbosity
- else:
- return 2
-
- def logpopen(self, popen, env):
- """ log information about the action.popen() created process. """
- cmd = " ".join(map(str, popen.args))
- if popen.outpath:
- self.verbosity1(" %s$ %s >%s" % (popen.cwd, cmd, popen.outpath,))
- else:
- self.verbosity1(" %s$ %s " % (popen.cwd, cmd))
-
- def logaction_start(self, action):
- msg = action.msg + " " + " ".join(map(str, action.args))
- self.verbosity2("%s start: %s" % (action.venvname, msg), bold=True)
- assert not hasattr(action, "_starttime")
- action._starttime = now()
-
- def logaction_finish(self, action):
- duration = now() - action._starttime
- # self.cumulated_time += duration
- self.verbosity2("%s finish: %s after %.2f seconds" % (
- action.venvname, action.msg, duration), bold=True)
- delattr(action, '_starttime')
-
- def startsummary(self):
- self.tw.sep("_", "summary")
-
- def info(self, msg):
- if self._get_verbosity() >= 2:
- self.logline(msg)
-
- def using(self, msg):
- if self._get_verbosity() >= 1:
- self.logline("using %s" % (msg,), bold=True)
-
- def keyboard_interrupt(self):
- self.error("KEYBOARDINTERRUPT")
-
-# def venv_installproject(self, venv, pkg):
-# self.logline("installing to %s: %s" % (venv.envconfig.envname, pkg))
-
- def keyvalue(self, name, value):
- if name.endswith(":"):
- name += " "
- self.tw.write(name, bold=True)
- self.tw.write(value)
- self.tw.line()
-
- def line(self, msg, **opts):
- self.logline(msg, **opts)
-
- def good(self, msg):
- self.logline(msg, green=True)
-
- def warning(self, msg):
- self.logline("WARNING:" + msg, red=True)
-
- def error(self, msg):
- self.logline("ERROR: " + msg, red=True)
-
- def skip(self, msg):
- self.logline("SKIPPED:" + msg, yellow=True)
-
- def logline(self, msg, **opts):
- self._reportedlines.append(msg)
- self.tw.line("%s" % msg, **opts)
-
- def verbosity0(self, msg, **opts):
- if self._get_verbosity() >= 0:
- self.logline("%s" % msg, **opts)
-
- def verbosity1(self, msg, **opts):
- if self._get_verbosity() >= 1:
- self.logline("%s" % msg, **opts)
-
- def verbosity2(self, msg, **opts):
- if self._get_verbosity() >= 2:
- self.logline("%s" % msg, **opts)
-
- # def log(self, msg):
- # py.builtin.print_(msg, file=sys.stderr)
-
-
-class Session:
- """ (unstable API). the session object that ties
- together configuration, reporting, venv creation, testing. """
-
- def __init__(self, config, popen=subprocess.Popen, Report=Reporter):
- self.config = config
- self.popen = popen
- self.resultlog = ResultLog()
- self.report = Report(self)
- self.make_emptydir(config.logdir)
- config.logdir.ensure(dir=1)
- # self.report.using("logdir %s" %(self.config.logdir,))
- self.report.using("tox.ini: %s" % (self.config.toxinipath,))
- self._spec2pkg = {}
- self._name2venv = {}
- try:
- self.venvlist = [
- self.getvenv(x)
- for x in self.config.envlist
- ]
- except LookupError:
- raise SystemExit(1)
- self._actions = []
-
- @property
- def hook(self):
- return self.config.pluginmanager.hook
-
- def _makevenv(self, name):
- envconfig = self.config.envconfigs.get(name, None)
- if envconfig is None:
- self.report.error("unknown environment %r" % name)
- raise LookupError(name)
- elif envconfig.envdir == self.config.toxinidir:
- self.report.error(
- "venv in %s would delete project" % envconfig.envdir)
- raise tox.exception.ConfigError('envdir must not equal toxinidir')
- venv = VirtualEnv(envconfig=envconfig, session=self)
- self._name2venv[name] = venv
- return venv
-
- def getvenv(self, name):
- """ return a VirtualEnv controler object for the 'name' env. """
- try:
- return self._name2venv[name]
- except KeyError:
- return self._makevenv(name)
-
- def newaction(self, venv, msg, *args):
- action = Action(self, venv, msg, args)
- self._actions.append(action)
- return action
-
- def runcommand(self):
- self.report.using("tox-%s from %s" % (tox.__version__, tox.__file__))
- if self.config.option.showconfig:
- self.showconfig()
- elif self.config.option.listenvs:
- self.showenvs()
- else:
- return self.subcommand_test()
-
- def _copyfiles(self, srcdir, pathlist, destdir):
- for relpath in pathlist:
- src = srcdir.join(relpath)
- if not src.check():
- self.report.error("missing source file: %s" % (src,))
- raise SystemExit(1)
- target = destdir.join(relpath)
- target.dirpath().ensure(dir=1)
- src.copy(target)
-
- def _makesdist(self):
- setup = self.config.setupdir.join("setup.py")
- if not setup.check():
- raise tox.exception.MissingFile(setup)
- action = self.newaction(None, "packaging")
- with action:
- action.setactivity("sdist-make", setup)
- self.make_emptydir(self.config.distdir)
- action.popen([sys.executable, setup, "sdist", "--formats=zip",
- "--dist-dir", self.config.distdir, ],
- cwd=self.config.setupdir)
- try:
- return self.config.distdir.listdir()[0]
- except py.error.ENOENT:
- # check if empty or comment only
- data = []
- with open(str(setup)) as fp:
- for line in fp:
- if line and line[0] == '#':
- continue
- data.append(line)
- if not ''.join(data).strip():
- self.report.error(
- 'setup.py is empty'
- )
- raise SystemExit(1)
- self.report.error(
- 'No dist directory found. Please check setup.py, e.g with:\n'
- ' python setup.py sdist'
- )
- raise SystemExit(1)
-
- def make_emptydir(self, path):
- if path.check():
- self.report.info(" removing %s" % path)
- py.std.shutil.rmtree(str(path), ignore_errors=True)
- path.ensure(dir=1)
-
- def setupenv(self, venv):
- if not venv.matching_platform():
- venv.status = "platform mismatch"
- return # we simply omit non-matching platforms
- action = self.newaction(venv, "getenv", venv.envconfig.envdir)
- with action:
- venv.status = 0
- envlog = self.resultlog.get_envlog(venv.name)
- try:
- status = venv.update(action=action)
- except tox.exception.InvocationError:
- status = sys.exc_info()[1]
- if status:
- commandlog = envlog.get_commandlog("setup")
- commandlog.add_command(["setup virtualenv"], str(status), 1)
- venv.status = status
- self.report.error(str(status))
- return False
- commandpath = venv.getcommandpath("python")
- envlog.set_python_info(commandpath)
- return True
-
- def finishvenv(self, venv):
- action = self.newaction(venv, "finishvenv")
- with action:
- venv.finish()
- return True
-
- def developpkg(self, venv, setupdir):
- action = self.newaction(venv, "developpkg", setupdir)
- with action:
- try:
- venv.developpkg(setupdir, action)
- return True
- except tox.exception.InvocationError:
- venv.status = sys.exc_info()[1]
- return False
-
- def installpkg(self, venv, path):
- """Install package in the specified virtual environment.
-
- :param :class:`tox.config.VenvConfig`: Destination environment
- :param str path: Path to the distribution package.
- :return: True if package installed otherwise False.
- :rtype: bool
- """
- self.resultlog.set_header(installpkg=py.path.local(path))
- action = self.newaction(venv, "installpkg", path)
- with action:
- try:
- venv.installpkg(path, action)
- return True
- except tox.exception.InvocationError:
- venv.status = sys.exc_info()[1]
- return False
-
- def get_installpkg_path(self):
- """
- :return: Path to the distribution
- :rtype: py.path.local
- """
- if not self.config.option.sdistonly and (self.config.sdistsrc or
- self.config.option.installpkg):
- path = self.config.option.installpkg
- if not path:
- path = self.config.sdistsrc
- path = self._resolve_pkg(path)
- self.report.info("using package %r, skipping 'sdist' activity " %
- str(path))
- else:
- try:
- path = self._makesdist()
- except tox.exception.InvocationError:
- v = sys.exc_info()[1]
- self.report.error("FAIL could not package project - v = %r" %
- v)
- return
- sdistfile = self.config.distshare.join(path.basename)
- if sdistfile != path:
- self.report.info("copying new sdistfile to %r" %
- str(sdistfile))
- try:
- sdistfile.dirpath().ensure(dir=1)
- except py.error.Error:
- self.report.warning("could not copy distfile to %s" %
- sdistfile.dirpath())
- else:
- path.copy(sdistfile)
- return path
-
- def subcommand_test(self):
- if self.config.skipsdist:
- self.report.info("skipping sdist step")
- path = None
- else:
- path = self.get_installpkg_path()
- if not path:
- return 2
- if self.config.option.sdistonly:
- return
- for venv in self.venvlist:
- if self.setupenv(venv):
- if venv.envconfig.usedevelop:
- self.developpkg(venv, self.config.setupdir)
- elif self.config.skipsdist or venv.envconfig.skip_install:
- self.finishvenv(venv)
- else:
- self.installpkg(venv, path)
-
- # write out version dependency information
- action = self.newaction(venv, "envreport")
- with action:
- args = venv.envconfig.list_dependencies_command
- output = venv._pcall(args,
- cwd=self.config.toxinidir,
- action=action)
- # the output contains a mime-header, skip it
- output = output.split("\n\n")[-1]
- packages = output.strip().split("\n")
- action.setactivity("installed", ",".join(packages))
- envlog = self.resultlog.get_envlog(venv.name)
- envlog.set_installed(packages)
-
- self.runtestenv(venv)
- retcode = self._summary()
- return retcode
-
- def runtestenv(self, venv, redirect=False):
- if not self.config.option.notest:
- if venv.status:
- return
- self.hook.tox_runtest_pre(venv=venv)
- venv.test(redirect=redirect)
- self.hook.tox_runtest_post(venv=venv)
- else:
- venv.status = "skipped tests"
-
- def _summary(self):
- self.report.startsummary()
- retcode = 0
- for venv in self.venvlist:
- status = venv.status
- if isinstance(status, tox.exception.InterpreterNotFound):
- msg = " %s: %s" % (venv.envconfig.envname, str(status))
- if self.config.option.skip_missing_interpreters:
- self.report.skip(msg)
- else:
- retcode = 1
- self.report.error(msg)
- elif status == "platform mismatch":
- msg = " %s: %s" % (venv.envconfig.envname, str(status))
- self.report.skip(msg)
- elif status and status == "ignored failed command":
- msg = " %s: %s" % (venv.envconfig.envname, str(status))
- self.report.good(msg)
- elif status and status != "skipped tests":
- msg = " %s: %s" % (venv.envconfig.envname, str(status))
- self.report.error(msg)
- retcode = 1
- else:
- if not status:
- status = "commands succeeded"
- self.report.good(" %s: %s" % (venv.envconfig.envname, status))
- if not retcode:
- self.report.good(" congratulations :)")
-
- path = self.config.option.resultjson
- if path:
- path = py.path.local(path)
- path.write(self.resultlog.dumps_json())
- self.report.line("wrote json report at: %s" % path)
- return retcode
-
- def showconfig(self):
- self.info_versions()
- self.report.keyvalue("config-file:", self.config.option.configfile)
- self.report.keyvalue("toxinipath: ", self.config.toxinipath)
- self.report.keyvalue("toxinidir: ", self.config.toxinidir)
- self.report.keyvalue("toxworkdir: ", self.config.toxworkdir)
- self.report.keyvalue("setupdir: ", self.config.setupdir)
- self.report.keyvalue("distshare: ", self.config.distshare)
- self.report.keyvalue("skipsdist: ", self.config.skipsdist)
- self.report.tw.line()
- for envconfig in self.config.envconfigs.values():
- self.report.line("[testenv:%s]" % envconfig.envname, bold=True)
- for attr in self.config._parser._testenv_attr:
- self.report.line(" %-15s = %s"
- % (attr.name, getattr(envconfig, attr.name)))
-
- def showenvs(self):
- for env in self.config.envlist:
- self.report.line("%s" % env)
-
- def info_versions(self):
- versions = ['tox-%s' % tox.__version__]
- try:
- version = py.process.cmdexec("virtualenv --version")
- except py.process.cmdexec.Error:
- versions.append("virtualenv-1.9.1 (vendored)")
- else:
- versions.append("virtualenv-%s" % version.strip())
- self.report.keyvalue("tool-versions:", " ".join(versions))
-
- def _resolve_pkg(self, pkgspec):
- try:
- return self._spec2pkg[pkgspec]
- except KeyError:
- self._spec2pkg[pkgspec] = x = self._resolvepkg(pkgspec)
- return x
-
- def _resolvepkg(self, pkgspec):
- if not os.path.isabs(str(pkgspec)):
- return pkgspec
- p = py.path.local(pkgspec)
- if p.check():
- return p
- if not p.dirpath().check(dir=1):
- raise tox.exception.MissingDirectory(p.dirpath())
- self.report.info("determining %s" % p)
- candidates = p.dirpath().listdir(p.basename)
- if len(candidates) == 0:
- raise tox.exception.MissingDependency(pkgspec)
- if len(candidates) > 1:
- items = []
- for x in candidates:
- ver = getversion(x.basename)
- if ver is not None:
- items.append((ver, x))
- else:
- self.report.warning("could not determine version of: %s" %
- str(x))
- items.sort()
- if not items:
- raise tox.exception.MissingDependency(pkgspec)
- return items[-1][1]
- else:
- return candidates[0]
-
-
-_rex_getversion = py.std.re.compile("[\w_\-\+\.]+-(.*)(\.zip|\.tar.gz)")
-
-
-def getversion(basename):
- m = _rex_getversion.match(basename)
- if m is None:
- return None
- version = m.group(1)
- try:
- return NormalizedVersion(version)
- except IrrationalVersionError:
- return None
diff --git a/tox/venv.py b/tox/venv.py
deleted file mode 100644
index 0461fc7..0000000
--- a/tox/venv.py
+++ /dev/null
@@ -1,416 +0,0 @@
-from __future__ import with_statement
-import os
-import sys
-import re
-import codecs
-import py
-import tox
-from .config import DepConfig, hookimpl
-
-
-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
-
- @property
- def hook(self):
- return self.envconfig.config.pluginmanager.hook
-
- @property
- def path(self):
- """ Path to environment base dir. """
- return self.envconfig.envdir
-
- @property
- def path_config(self):
- return self.path.join(".tox-config1")
-
- @property
- def name(self):
- """ test environment name. """
- return self.envconfig.envname
-
- def __repr__(self):
- return "<VirtualEnv at %r>" % (self.path)
-
- def getcommandpath(self, name, venv=True, cwd=None):
- """ Return absolute path (str or localpath) for specified command name.
- - If it's a local path we will rewrite it as as a relative path.
- - If venv is True we will check if the command is coming from the venv
- or is whitelisted to come from external.
- """
- name = str(name)
- if os.path.isabs(name):
- return name
- if os.path.split(name)[0] == ".":
- path = cwd.join(name)
- if path.check():
- return str(path)
-
- if venv:
- path = self._venv_lookup_and_check_external_whitelist(name)
- else:
- path = self._normal_lookup(name)
-
- if path is None:
- raise tox.exception.InvocationError(
- "could not find executable %r" % (name,))
-
- return str(path) # will not be rewritten for reporting
-
- def _venv_lookup_and_check_external_whitelist(self, name):
- path = self._venv_lookup(name)
- if path is None:
- path = self._normal_lookup(name)
- if path is not None:
- self._check_external_allowed_and_warn(path)
- return path
-
- def _venv_lookup(self, name):
- return py.path.local.sysfind(name, paths=[self.envconfig.envbindir])
-
- def _normal_lookup(self, name):
- return py.path.local.sysfind(name)
-
- def _check_external_allowed_and_warn(self, path):
- if not self.is_allowed_external(path):
- 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." % (
- path, self.envconfig.envdir))
-
- 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):
- """ return status string for updating actual venv to match configuration.
- 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()):
- 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.hook.tox_testenv_create(action=action, venv=self)
- self.just_created = True
- except tox.exception.UnsupportedInterpreter:
- return sys.exc_info()[1]
- except tox.exception.InterpreterNotFound:
- 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 %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 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 _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, action, options=()):
- 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__'):
- os.environ.pop(x, None)
-
- old_stdout = sys.stdout
- sys.stdout = codecs.getwriter('utf8')(sys.stdout)
- self._pcall(argv, cwd=self.envconfig.config.toxinidir, 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:
- packages = d[ixserver]
- options = self._installopts(ixserver.url)
- if extraopts:
- options.extend(extraopts)
- self.run_install_command(packages=packages, options=options,
- action=action)
-
- def _getenv(self, testcommand=False):
- if testcommand:
- # for executing tests we construct a clean environment
- env = {}
- for envname in self.envconfig.passenv:
- if envname in os.environ:
- env[envname] = os.environ[envname]
- else:
- # for executing non-test commands we use the full
- # invocation environment
- env = os.environ.copy()
-
- # in any case we honor per-testenv setenv configuration
- env.update(self.envconfig.setenv)
-
- env['VIRTUAL_ENV'] = str(self.path)
- 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(testcommand=True)
- # 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, testcommand=True)
- except tox.exception.InvocationError as err:
- if self.envconfig.ignore_outcome:
- self.session.report.warning(
- "command failed but result from testenv is ignored\n"
- " cmd: %s" % (str(err),))
- self.status = "ignored failed command"
- continue # keep processing commands
-
- 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, cwd, venv=True, testcommand=False,
- action=None, redirect=True, ignore_ret=False):
- for name in ("VIRTUALENV_PYTHON", "PYTHONDONTWRITEBYTECODE"):
- os.environ.pop(name, None)
-
- cwd.ensure(dir=1)
- args[0] = self.getcommandpath(args[0], venv, cwd)
- env = self._getenv(testcommand=testcommand)
- bindir = str(self.envconfig.envbindir)
- env['PATH'] = p = os.pathsep.join([bindir, os.environ["PATH"]])
- self.session.report.verbosity2("setting PATH=%s" % p)
- return action.popen(args, cwd=cwd, env=env,
- redirect=redirect, ignore_ret=ignore_ret)
-
-
-def getdigest(path):
- path = py.path.local(path)
- if not path.check(file=1):
- return "0" * 32
- return path.computehash()
-
-
-@hookimpl
-def tox_testenv_create(venv, action):
- # if self.getcommandpath("activate").dirpath().check():
- # return
- config_interpreter = venv.getsupportedinterpreter()
- args = [sys.executable, '-m', 'virtualenv']
- if venv.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:
- venv.session.make_emptydir(venv.path)
- basepath = venv.path.dirpath()
- basepath.ensure(dir=1)
- args.append(venv.path.basename)
- venv._pcall(args, venv=False, action=action, cwd=basepath)
-
-
-@hookimpl
-def tox_testenv_install_deps(venv, action):
- deps = venv._getresolvedeps()
- if deps:
- depinfo = ", ".join(map(str, deps))
- action.setactivity("installdeps", "%s" % depinfo)
- venv._install(deps, action=action)