summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.hgignore1
-rw-r--r--[-rwxr-xr-x]CHANGELOG46
-rw-r--r--CONTRIBUTORS6
-rw-r--r--doc/announce/release-1.8.txt54
-rw-r--r--doc/conf.py3
-rw-r--r--doc/config-v2.txt2
-rw-r--r--doc/config.txt191
-rw-r--r--doc/example/basic.txt17
-rw-r--r--doc/example/jenkins.txt7
-rwxr-xr-xdoc/example/pytest.txt2
-rw-r--r--doc/index.txt4
-rw-r--r--setup.py6
-rw-r--r--tests/conftest.py2
-rw-r--r--tests/test_config.py194
-rw-r--r--tests/test_interpreters.py2
-rw-r--r--tests/test_quickstart.py180
-rw-r--r--tests/test_venv.py22
-rw-r--r--tests/test_z_cmdline.py35
-rw-r--r--tox.ini6
-rw-r--r--tox/__init__.py4
-rw-r--r--tox/_cmdline.py29
-rw-r--r--tox/_config.py248
-rw-r--r--tox/_quickstart.py2
-rw-r--r--tox/_venv.py23
-rw-r--r--tox/_verlib.py1
-rw-r--r--tox/interpreters.py4
26 files changed, 898 insertions, 193 deletions
diff --git a/.hgignore b/.hgignore
index cf24d86..8cacf58 100644
--- a/.hgignore
+++ b/.hgignore
@@ -14,3 +14,4 @@ dist
doc/_build/
tox.egg-info
.tox
+.cache
diff --git a/CHANGELOG b/CHANGELOG
index e6168cb..3e73166 100755..100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,49 @@
+1.8.1.dev
+-----------
+
+- fix issue190: allow setenv to be empty.
+
+
+1.8.0
+-----------
+
+- new multi-dimensional configuration support. Many thanks to
+ Alexander Schepanovski for the complete PR with docs.
+ And to Mike Bayer and others for testing and feedback.
+
+- fix issue148: remove "__PYVENV_LAUNCHER__" from os.environ when starting
+ subprocesses. Thanks Steven Myint.
+
+- fix issue152: set VIRTUAL_ENV when running test commands,
+ thanks Florian Ludwig.
+
+- better report if we can't get version_info from an interpreter
+ executable. Thanks Floris Bruynooghe.
+
+
+1.7.2
+-----------
+
+- fix issue150: parse {posargs} more like we used to do it pre 1.7.0.
+ The 1.7.0 behaviour broke a lot of OpenStack projects.
+ See PR85 and the issue discussions for (far) more details, hopefully
+ resulting in a more refined behaviour in the 1.8 series.
+ And thanks to Clark Boylan for the PR.
+
+- fix issue59: add a config variable ``skip-missing-interpreters`` as well as
+ command line option ``--skip-missing-interpreters`` which won't fail the
+ build if Python interpreters listed in tox.ini are missing. Thanks
+ Alexandre Conrad for PR104.
+
+- fix issue164: better traceback info in case of failing test commands.
+ Thanks Marc Abramowitz for PR92.
+
+- support optional env variable substitution, thanks Morgan Fainberg
+ for PR86.
+
+- limit python hashseed to 1024 on Windows to prevent possible
+ memory errors. Thanks March Schlaich for the PR90.
+
1.7.1
---------
diff --git a/CONTRIBUTORS b/CONTRIBUTORS
index 0244c2b..ef8576d 100644
--- a/CONTRIBUTORS
+++ b/CONTRIBUTORS
@@ -3,6 +3,7 @@ contributions:
Krisztian Fekete
Marc Abramowitz
+Aleaxner Schepanovski
Sridhar Ratnakumar
Barry Warsaw
Chris Rose
@@ -23,3 +24,8 @@ Matt Good
Mattieu Agopian
Asmund Grammeltwedt
Ionel Maries Cristian
+Alexandre Conrad
+Morgan Fainberg
+Marc Schlaich
+Clark Boylan
+Eugene Yunak
diff --git a/doc/announce/release-1.8.txt b/doc/announce/release-1.8.txt
new file mode 100644
index 0000000..b8a2218
--- /dev/null
+++ b/doc/announce/release-1.8.txt
@@ -0,0 +1,54 @@
+tox 1.8: Generative/combinatorial environments specs
+=============================================================================
+
+I am happy to announce tox 1.8 which implements parametrized environments.
+
+See https://tox.testrun.org/latest/config.html#generating-environments-conditional-settings
+for examples and the new backward compatible syntax in your tox.ini file.
+
+Many thanks to Alexander Schepanovski for implementing and refining
+it based on the specifcation draft.
+
+More documentation about tox in general:
+
+ http://tox.testrun.org/
+
+Installation:
+
+ pip install -U tox
+
+code hosting and issue tracking on bitbucket:
+
+ https://bitbucket.org/hpk42/tox
+
+What is tox?
+----------------
+
+tox standardizes and automates tedious test activities driven from a
+simple ``tox.ini`` file, including:
+
+* creation and management of different virtualenv environments
+ with different Python interpreters
+* packaging and installing your package into each of them
+* running your test tool of choice, be it nose, py.test or unittest2 or other tools such as "sphinx" doc checks
+* testing dev packages against each other without needing to upload to PyPI
+
+best,
+Holger Krekel, merlinux GmbH
+
+
+Changes 1.8 (compared to 1.7.2)
+---------------------------------------
+
+- new multi-dimensional configuration support. Many thanks to
+ Alexander Schepanovski for the complete PR with docs.
+ And to Mike Bayer and others for testing and feedback.
+
+- fix issue148: remove "__PYVENV_LAUNCHER__" from os.environ when starting
+ subprocesses. Thanks Steven Myint.
+
+- fix issue152: set VIRTUAL_ENV when running test commands,
+ thanks Florian Ludwig.
+
+- better report if we can't get version_info from an interpreter
+ executable. Thanks Floris Bruynooghe.
diff --git a/doc/conf.py b/doc/conf.py
index 76dc4c3..92ed895 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -48,7 +48,8 @@ copyright = u'2013, holger krekel and others'
# built documents.
#
# The short X.Y version.
-release = version = "1.7.1"
+release = "1.8"
+version = "1.8.0"
# The full version, including alpha/beta/rc tags.
# The language for content autogenerated by Sphinx. Refer to documentation
diff --git a/doc/config-v2.txt b/doc/config-v2.txt
index cf4cf3b..42973c4 100644
--- a/doc/config-v2.txt
+++ b/doc/config-v2.txt
@@ -195,7 +195,7 @@ Default settings related to environments names/variants
tox comes with predefined settings for certain variants, namely:
* ``{easy,pip}`` use easy_install or pip respectively
-* ``{py24,py25,py26,py27,py31,py32,py33,pypy19]`` use the respective
+* ``{py24,py25,py26,py27,py31,py32,py33,py34,pypy19]`` use the respective
pythonNN or PyPy interpreter
* ``{win32,linux,darwin}`` defines the according ``platform``.
diff --git a/doc/config.txt b/doc/config.txt
index 054400d..8ce64db 100644
--- a/doc/config.txt
+++ b/doc/config.txt
@@ -32,16 +32,27 @@ and will first lookup global tox settings in this section::
... # override [tox] settings for the jenkins context
# note: for jenkins distshare defaults to ``{toxworkdir}/distshare``.
+.. confval:: skip_missing_interpreters=BOOL
-envlist setting
-+++++++++++++++
+ .. versionadded:: 1.7.2
-Determining the environment list that ``tox`` is to operate on
-happens in this order:
+ Setting this to ``True`` is equivalent of passing the
+ ``--skip-missing-interpreters`` command line option, and will force ``tox`` to
+ return success even if some of the specified environments were missing. This is
+ useful for some CI systems or running on a developer box, where you might only
+ have a subset of all your supported interpreters installed but don't want to
+ mark the build as failed because of it. As expected, the command line switch
+ always overrides this setting if passed on the invokation.
+ **Default:** ``False``
-* command line option ``-eENVLIST``
-* environment variable ``TOXENV``
-* ``tox.ini`` file's ``envlist``
+.. confval:: envlist=CSV
+
+ Determining the environment list that ``tox`` is to operate on
+ happens in this order (if any is found, no further lookups are made):
+
+ * command line option ``-eENVLIST``
+ * environment variable ``TOXENV``
+ * ``tox.ini`` file's ``envlist``
Virtualenv test environment settings
@@ -284,6 +295,26 @@ then the value will be retrieved as ``os.environ['KEY']``
and raise an Error if the environment variable
does not exist.
+
+environment variable substitutions with default values
+++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+If you specify a substitution string like this::
+
+ {env:KEY:DEFAULTVALUE}
+
+then the value will be retrieved as ``os.environ['KEY']``
+and replace with DEFAULTVALUE if the environment variable does not
+exist.
+
+If you specify a substitution string like this::
+
+ {env:KEY:}
+
+then the value will be retrieved as ``os.environ['KEY']``
+and replace with and empty string if the environment variable does not
+exist.
+
.. _`command positional substitution`:
.. _`positional substitution`:
@@ -351,6 +382,152 @@ You can put default values in one section and reference them in others to avoid
{[base]deps}
+Generating environments, conditional settings
+---------------------------------------------
+
+.. versionadded:: 1.8
+
+Suppose you want to test your package against python2.6, python2.7 and against
+several versions of a dependency, say Django 1.5 and Django 1.6. You can
+accomplish that by writing down 2*2 = 4 ``[testenv:*]`` sections and then
+listing all of them in ``envlist``.
+
+However, a better approach looks like this::
+
+ [tox]
+ envlist = {py26,py27}-django{15,16}
+
+ [testenv]
+ basepython =
+ py26: python2.6
+ py27: python2.7
+ deps =
+ pytest
+ django15: Django>=1.5,<1.6
+ django16: Django>=1.6,<1.7
+ py26: unittest2
+ commands = py.test
+
+This uses two new facilities of tox-1.8:
+
+- generative envlist declarations where each envname
+ consists of environment parts or "factors"
+
+- "factor" specific settings
+
+Let's go through this step by step.
+
+
+Generative envlist
++++++++++++++++++++++++
+
+::
+
+ envlist = {py26,py27}-django{15,16}
+
+This is bash-style syntax and will create ``2*2=4`` environment names
+like this::
+
+ py26-django15
+ py26-django16
+ py27-django15
+ py27-django16
+
+You can still list environments explicitly along with generated ones::
+
+ envlist = {py26,py27}-django{15,16}, docs, flake
+
+.. note::
+
+ To help with understanding how the variants will produce section values,
+ you can ask tox to show their expansion with a new option::
+
+ $ tox -l
+ py26-django15
+ py26-django16
+ py27-django15
+ py27-django16
+ docs
+ flake
+
+
+Factors and factor-conditional settings
+++++++++++++++++++++++++++++++++++++++++
+
+Parts of an environment name delimited by hyphens are called factors and can
+be used to set values conditionally::
+
+ basepython =
+ py26: python2.6
+ py27: python2.7
+
+This conditional setting will lead to either ``python2.6`` or
+``python2.7`` used as base python, e.g. ``python2.6`` is selected if current
+environment contains ``py26`` factor.
+
+In list settings such as ``deps`` or ``commands`` you can freely intermix
+optional lines with unconditional ones::
+
+ deps =
+ pytest
+ django15: Django>=1.5,<1.6
+ django16: Django>=1.6,<1.7
+ py26: unittest2
+
+Reading it line by line:
+
+- ``pytest`` will be included unconditionally,
+- ``Django>=1.5,<1.6`` will be included for environments containing ``django15`` factor,
+- ``Django>=1.6,<1.7`` similarly depends on ``django16`` factor,
+- ``unittest`` will be loaded for Python 2.6 environments.
+
+.. note::
+
+ Tox provides good defaults for basepython setting, so the above
+ ini-file can be further reduced by omitting the ``basepython``
+ setting.
+
+
+Complex factor conditions
++++++++++++++++++++++++++
+
+Sometimes you need to specify same line for several factors or create a special
+case for a combination of factors. Here is how you do it::
+
+ [tox]
+ envlist = py{26,27,33}-django{15,16}-{sqlite,mysql}
+
+ [testenv]
+ deps =
+ py33-mysql: PyMySQL ; use if both py33 and mysql are in an env name
+ py26,py27: urllib3 ; use if any of py26 or py27 are in an env name
+ py{26,27}-sqlite: mock ; mocking sqlite in python 2.x
+
+Take a look at first ``deps`` line. It shows how you can special case something
+for a combination of factors, you just join combining factors with a hyphen.
+This particular line states that ``PyMySQL`` will be loaded for python 3.3,
+mysql environments, e.g. ``py33-django15-mysql`` and ``py33-django16-mysql``.
+
+The second line shows how you use same line for several factors - by listing
+them delimited by commas. It's possible to list not only simple factors, but
+also their combinations like ``py26-sqlite,py27-sqlite``.
+
+Finally, factor expressions are expanded the same way as envlist, so last
+example could be rewritten as ``py{26,27}-sqlite``.
+
+.. note::
+
+ Factors don't do substring matching against env name, instead every
+ hyphenated expression is split by ``-`` and if ALL the factors in an
+ expression are also factors of an env then that condition is considered
+ hold.
+
+ For example, environment ``py26-mysql``:
+
+ - could be matched with expressions ``py26``, ``py26-mysql``,
+ ``mysql-py26``,
+ - but not with ``py2`` or ``py26-sql``.
+
Other Rules and notes
=====================
diff --git a/doc/example/basic.txt b/doc/example/basic.txt
index 19a15d4..562e2bf 100644
--- a/doc/example/basic.txt
+++ b/doc/example/basic.txt
@@ -1,4 +1,3 @@
-
Basic usage
=============================================
@@ -13,6 +12,7 @@ reside next to your ``setup.py`` file::
[tox]
envlist = py26,py27
[testenv]
+ deps=pytest # or 'nose' or ...
commands=py.test # or 'nosetests' or ...
To sdist-package, install and test your project, you can
@@ -38,8 +38,10 @@ Available "default" test environments names are::
py31
py32
py33
+ py34
jython
pypy
+ pypy3
However, you can also create your own test environment names,
see some of the examples in :doc:`examples <../examples>`.
@@ -207,6 +209,10 @@ a test run when ``python setup.py test`` is issued::
import sys
class Tox(TestCommand):
+ user_options = [('tox-args=', 'a', "Arguments to pass to tox")]
+ def initialize_options(self):
+ TestCommand.initialize_options(self)
+ self.tox_args = None
def finalize_options(self):
TestCommand.finalize_options(self)
self.test_args = []
@@ -214,7 +220,8 @@ a test run when ``python setup.py test`` is issued::
def run_tests(self):
#import here, cause outside the eggs aren't loaded
import tox
- errno = tox.cmdline(self.test_args)
+ import shlex
+ errno = tox.cmdline(args=shlex.split(self.tox_args))
sys.exit(errno)
setup(
@@ -227,5 +234,9 @@ Now if you run::
python setup.py test
-this will install tox and then run tox.
+this will install tox and then run tox. You can pass arguments to ``tox``
+using the ``--tox-args`` or ``-a`` command-line options. For example::
+
+ python setup.py test -a "-epy27"
+is equivalent to running ``tox -epy27``.
diff --git a/doc/example/jenkins.txt b/doc/example/jenkins.txt
index bfdceee..2d8dc25 100644
--- a/doc/example/jenkins.txt
+++ b/doc/example/jenkins.txt
@@ -32,7 +32,9 @@ using these steps:
The last point requires that your test command creates JunitXML files,
for example with ``py.test`` it is done like this:
- commands=py.test --junitxml=junit-{envname}.xml
+.. code-block:: ini
+
+ commands = py.test --junitxml=junit-{envname}.xml
@@ -57,7 +59,7 @@ with this::
exec urllib.urlopen(url).read() in d
d['cmdline'](['--recreate'])
-The downloaded `toxbootstrap.py`_ file downloads all neccessary files to
+The downloaded `toxbootstrap.py` file downloads all neccessary files to
install ``tox`` in a virtual sub environment. Notes:
* uncomment the line containing ``USETOXDEV`` to use the latest
@@ -68,7 +70,6 @@ install ``tox`` in a virtual sub environment. Notes:
will cause tox to reinstall all virtual environments all the time
which is often what one wants in CI server contexts)
-.. _`toxbootstrap.py`: https://bitbucket.org/hpk42/tox/raw/default/toxbootstrap.py
Integrating "sphinx" documentation checks in a Jenkins job
----------------------------------------------------------------
diff --git a/doc/example/pytest.txt b/doc/example/pytest.txt
index cba8741..6f98cf3 100755
--- a/doc/example/pytest.txt
+++ b/doc/example/pytest.txt
@@ -105,7 +105,7 @@ directories; pytest will still find and import them by adding their
parent directory to ``sys.path`` but they won't be copied to
other places or be found by Python's import system outside of pytest.
-.. _`fully qualified name`: http://pytest.org/latest/goodpractises.html#package-name
+.. _`fully qualified name`: http://pytest.org/latest/goodpractises.html#test-package-name
.. include:: ../links.txt
diff --git a/doc/index.txt b/doc/index.txt
index c1baae2..17d063d 100644
--- a/doc/index.txt
+++ b/doc/index.txt
@@ -68,8 +68,8 @@ Current features
support for configuring the installer command
through :confval:`install_command=ARGV`.
-* **cross-Python compatible**: Python-2.6 up to Python-3.3,
- Jython and pypy_ support.
+* **cross-Python compatible**: CPython-2.6, 2.7, 3.2 and higher,
+ Jython and pypy_.
* **cross-platform**: Windows and Unix style environments
diff --git a/setup.py b/setup.py
index 582d315..7bd4d40 100644
--- a/setup.py
+++ b/setup.py
@@ -19,16 +19,14 @@ class Tox(TestCommand):
def main():
version = sys.version_info[:2]
install_requires = ['virtualenv>=1.11.2', 'py>=1.4.17', ]
- if version < (2, 7) or (3, 0) <= version <= (3, 1):
+ if version < (2, 7):
install_requires += ['argparse']
- if version < (2,6):
- install_requires += ["simplejson"]
setup(
name='tox',
description='virtualenv-based automation of test activities',
long_description=open("README.rst").read(),
url='http://tox.testrun.org/',
- version='1.7.1',
+ version='1.8.1.dev1',
license='http://opensource.org/licenses/MIT',
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],
author='holger krekel',
diff --git a/tests/conftest.py b/tests/conftest.py
index 1d4bdde..3e2493b 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,2 +1,2 @@
-from tox._pytestplugin import *
+from tox._pytestplugin import * # noqa
diff --git a/tests/test_config.py b/tests/test_config.py
index c4ec9cc..c823cb3 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -1,12 +1,11 @@
-import tox
-import pytest
-import os, sys
-import subprocess
+import sys
from textwrap import dedent
import py
+import pytest
+import tox
import tox._config
-from tox._config import *
+from tox._config import * # noqa
from tox._config import _split_env
@@ -110,7 +109,6 @@ class TestConfigPackage:
def test_defaults_distshare(self, tmpdir, newconfig):
config = newconfig([], "")
- envconfig = config.envconfigs['python']
assert config.distshare == config.homedir.join(".tox", "distshare")
def test_defaults_changed_dir(self, tmpdir, newconfig):
@@ -168,6 +166,7 @@ class TestIniParser:
key2={xyz}
""")
reader = IniReader(config._cfg, fallbacksections=['mydefault'])
+ assert reader is not None
py.test.raises(tox.exception.ConfigError,
'reader.getdefault("mydefault", "key2")')
@@ -242,6 +241,22 @@ class TestIniParser:
py.test.raises(tox.exception.ConfigError,
'reader.getdefault("section", "key2")')
+ def test_getdefault_environment_substitution_with_default(self, monkeypatch, newconfig):
+ monkeypatch.setenv("KEY1", "hello")
+ config = newconfig("""
+ [section]
+ key1={env:KEY1:DEFAULT_VALUE}
+ key2={env:KEY2:DEFAULT_VALUE}
+ key3={env:KEY3:}
+ """)
+ reader = IniReader(config._cfg)
+ x = reader.getdefault("section", "key1")
+ assert x == "hello"
+ x = reader.getdefault("section", "key2")
+ assert x == "DEFAULT_VALUE"
+ x = reader.getdefault("section", "key3")
+ assert x == ""
+
def test_getdefault_other_section_substitution(self, newconfig):
config = newconfig("""
[section]
@@ -278,7 +293,7 @@ class TestIniParser:
# "reader.getargvlist('section', 'key1')")
assert reader.getargvlist('section', 'key1') == []
x = reader.getargvlist("section", "key2")
- assert x == [["cmd1", "with space", "grr"],
+ assert x == [["cmd1", "with", "space", "grr"],
["cmd2", "grr"]]
def test_argvlist_windows_escaping(self, tmpdir, newconfig):
@@ -304,7 +319,7 @@ class TestIniParser:
# "reader.getargvlist('section', 'key1')")
assert reader.getargvlist('section', 'key1') == []
x = reader.getargvlist("section", "key2")
- assert x == [["cmd1", "with space", "grr"]]
+ assert x == [["cmd1", "with", "space", "grr"]]
def test_argvlist_quoting_in_command(self, tmpdir, newconfig):
@@ -346,6 +361,34 @@ class TestIniParser:
assert argvlist[0] == ["cmd1"]
assert argvlist[1] == ["cmd2", "value2", "other"]
+ def test_argvlist_quoted_posargs(self, tmpdir, newconfig):
+ config = newconfig("""
+ [section]
+ key2=
+ cmd1 --foo-args='{posargs}'
+ cmd2 -f '{posargs}'
+ cmd3 -f {posargs}
+ """)
+ reader = IniReader(config._cfg)
+ reader.addsubstitutions(["foo", "bar"])
+ assert reader.getargvlist('section', 'key1') == []
+ x = reader.getargvlist("section", "key2")
+ assert x == [["cmd1", "--foo-args=foo bar"],
+ ["cmd2", "-f", "foo bar"],
+ ["cmd3", "-f", "foo", "bar"]]
+
+ def test_argvlist_posargs_with_quotes(self, tmpdir, newconfig):
+ config = newconfig("""
+ [section]
+ key2=
+ cmd1 -f {posargs}
+ """)
+ reader = IniReader(config._cfg)
+ reader.addsubstitutions(["foo", "'bar", "baz'"])
+ assert reader.getargvlist('section', 'key1') == []
+ x = reader.getargvlist("section", "key2")
+ assert x == [["cmd1", "-f", "foo", "bar baz"]]
+
def test_positional_arguments_are_only_replaced_when_standing_alone(self,
tmpdir, newconfig):
config = newconfig("""
@@ -511,7 +554,7 @@ class TestConfigTestEnv:
envconfig = config.envconfigs['python']
assert envconfig.envpython == envconfig.envbindir.join("python")
- @pytest.mark.parametrize("bp", ["jython", "pypy"])
+ @pytest.mark.parametrize("bp", ["jython", "pypy", "pypy3"])
def test_envbindir_jython(self, tmpdir, newconfig, bp):
config = newconfig("""
[testenv]
@@ -806,6 +849,100 @@ class TestConfigTestEnv:
assert conf.changedir.basename == 'testing'
assert conf.changedir.dirpath().realpath() == tmpdir.realpath()
+ def test_factors(self, newconfig):
+ inisource="""
+ [tox]
+ envlist = a-x,b
+
+ [testenv]
+ deps=
+ dep-all
+ a: dep-a
+ b: dep-b
+ x: dep-x
+ """
+ conf = newconfig([], inisource)
+ configs = conf.envconfigs
+ assert [dep.name for dep in configs['a-x'].deps] == \
+ ["dep-all", "dep-a", "dep-x"]
+ assert [dep.name for dep in configs['b'].deps] == ["dep-all", "dep-b"]
+
+ def test_factor_ops(self, newconfig):
+ inisource="""
+ [tox]
+ envlist = {a,b}-{x,y}
+
+ [testenv]
+ deps=
+ a,b: dep-a-or-b
+ a-x: dep-a-and-x
+ {a,b}-y: dep-ab-and-y
+ """
+ configs = newconfig([], inisource).envconfigs
+ get_deps = lambda env: [dep.name for dep in configs[env].deps]
+ assert get_deps("a-x") == ["dep-a-or-b", "dep-a-and-x"]
+ assert get_deps("a-y") == ["dep-a-or-b", "dep-ab-and-y"]
+ assert get_deps("b-x") == ["dep-a-or-b"]
+ assert get_deps("b-y") == ["dep-a-or-b", "dep-ab-and-y"]
+
+ def test_default_factors(self, newconfig):
+ inisource="""
+ [tox]
+ envlist = py{26,27,33,34}-dep
+
+ [testenv]
+ deps=
+ dep: dep
+ """
+ conf = newconfig([], inisource)
+ configs = conf.envconfigs
+ for name, config in configs.items():
+ assert config.basepython == 'python%s.%s' % (name[2], name[3])
+
+ @pytest.mark.issue188
+ def test_factors_in_boolean(self, newconfig):
+ inisource="""
+ [tox]
+ envlist = py{27,33}
+
+ [testenv]
+ recreate =
+ py27: True
+ """
+ configs = newconfig([], inisource).envconfigs
+ assert configs["py27"].recreate
+ assert not configs["py33"].recreate
+
+ @pytest.mark.issue190
+ def test_factors_in_setenv(self, newconfig):
+ inisource="""
+ [tox]
+ envlist = py27,py26
+
+ [testenv]
+ setenv =
+ py27: X = 1
+ """
+ configs = newconfig([], inisource).envconfigs
+ assert configs["py27"].setenv["X"] == "1"
+ assert "X" not in configs["py26"].setenv
+
+ def test_period_in_factor(self, newconfig):
+ inisource="""
+ [tox]
+ envlist = py27-{django1.6,django1.7}
+
+ [testenv]
+ deps =
+ django1.6: Django==1.6
+ django1.7: Django==1.7
+ """
+ configs = newconfig([], inisource).envconfigs
+ assert sorted(configs) == ["py27-django1.6", "py27-django1.7"]
+ assert [d.name for d in configs["py27-django1.6"].deps] \
+ == ["Django==1.6"]
+
+
class TestGlobalOptions:
def test_notest(self, newconfig):
config = newconfig([], "")
@@ -890,7 +1027,7 @@ class TestGlobalOptions:
assert str(env.basepython) == sys.executable
def test_default_environments(self, tmpdir, newconfig, monkeypatch):
- envs = "py26,py27,py31,py32,py33,jython,pypy"
+ envs = "py26,py27,py31,py32,py33,py34,jython,pypy,pypy3"
inisource = """
[tox]
envlist = %s
@@ -902,13 +1039,30 @@ class TestGlobalOptions:
env = config.envconfigs[name]
if name == "jython":
assert env.basepython == "jython"
- elif name == "pypy":
- assert env.basepython == "pypy"
+ elif name.startswith("pypy"):
+ assert env.basepython == name
else:
assert name.startswith("py")
bp = "python%s.%s" %(name[2], name[3])
assert env.basepython == bp
+ def test_envlist_expansion(self, newconfig):
+ inisource = """
+ [tox]
+ envlist = py{26,27},docs
+ """
+ config = newconfig([], inisource)
+ assert config.envlist == ["py26", "py27", "docs"]
+
+ def test_envlist_cross_product(self, newconfig):
+ inisource = """
+ [tox]
+ envlist = py{26,27}-dep{1,2}
+ """
+ config = newconfig([], inisource)
+ assert config.envlist == \
+ ["py26-dep1", "py26-dep2", "py27-dep1", "py27-dep2"]
+
def test_minversion(self, tmpdir, newconfig, monkeypatch):
inisource = """
[tox]
@@ -917,6 +1071,22 @@ class TestGlobalOptions:
config = newconfig([], inisource)
assert config.minversion == "3.0"
+ def test_skip_missing_interpreters_true(self, tmpdir, newconfig, monkeypatch):
+ inisource = """
+ [tox]
+ skip_missing_interpreters = True
+ """
+ config = newconfig([], inisource)
+ assert config.option.skip_missing_interpreters
+
+ def test_skip_missing_interpreters_false(self, tmpdir, newconfig, monkeypatch):
+ inisource = """
+ [tox]
+ skip_missing_interpreters = False
+ """
+ config = newconfig([], inisource)
+ assert not config.option.skip_missing_interpreters
+
def test_defaultenv_commandline(self, tmpdir, newconfig, monkeypatch):
config = newconfig(["-epy24"], "")
env = config.envconfigs['py24']
diff --git a/tests/test_interpreters.py b/tests/test_interpreters.py
index 39eabc6..f06a69a 100644
--- a/tests/test_interpreters.py
+++ b/tests/test_interpreters.py
@@ -2,7 +2,7 @@ import sys
import os
import pytest
-from tox.interpreters import *
+from tox.interpreters import * # noqa
@pytest.fixture
def interpreters():
diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py
index c376d4b..df8a98f 100644
--- a/tests/test_quickstart.py
+++ b/tests/test_quickstart.py
@@ -6,6 +6,7 @@ import tox._quickstart
def cleandir(tmpdir):
tmpdir.chdir()
+
class TestToxQuickstartMain(object):
def mock_term_input_return_values(self, return_values):
@@ -23,12 +24,26 @@ class TestToxQuickstartMain(object):
return mock_term_input
- def test_quickstart_main_choose_individual_pythons_and_pytest(self,
- monkeypatch):
+ def test_quickstart_main_choose_individual_pythons_and_pytest(
+ self,
+ monkeypatch):
monkeypatch.setattr(
tox._quickstart, 'term_input',
self.get_mock_term_input(
- ['4', 'Y', 'Y', 'Y', 'Y', 'Y', 'N', 'py.test', 'pytest']))
+ [
+ '4', # Python versions: choose one by one
+ 'Y', # py26
+ 'Y', # py27
+ 'Y', # py32
+ 'Y', # py33
+ 'Y', # py34
+ 'Y', # pypy
+ 'N', # jython
+ 'py.test', # command to run tests
+ 'pytest' # test dependencies
+ ]
+ )
+ )
tox._quickstart.main(argv=['tox-quickstart'])
@@ -39,7 +54,7 @@ class TestToxQuickstartMain(object):
# and then run "tox" from this directory.
[tox]
-envlist = py26, py27, py32, py33, pypy
+envlist = py26, py27, py32, py33, py34, pypy
[testenv]
commands = py.test
@@ -49,11 +64,26 @@ deps =
result = open('tox.ini').read()
assert(result == expected_tox_ini)
- def test_quickstart_main_choose_individual_pythons_and_nose_adds_deps(self, monkeypatch):
+ def test_quickstart_main_choose_individual_pythons_and_nose_adds_deps(
+ self,
+ monkeypatch):
monkeypatch.setattr(
tox._quickstart, 'term_input',
- self.get_mock_term_input(['4', 'Y', 'Y', 'Y', 'Y', 'Y', 'N',
- 'nosetests', '']))
+ self.get_mock_term_input(
+ [
+ '4', # Python versions: choose one by one
+ 'Y', # py26
+ 'Y', # py27
+ 'Y', # py32
+ 'Y', # py33
+ 'Y', # py34
+ 'Y', # pypy
+ 'N', # jython
+ 'nosetests', # command to run tests
+ '' # test dependencies
+ ]
+ )
+ )
tox._quickstart.main(argv=['tox-quickstart'])
@@ -64,7 +94,7 @@ deps =
# and then run "tox" from this directory.
[tox]
-envlist = py26, py27, py32, py33, pypy
+envlist = py26, py27, py32, py33, py34, pypy
[testenv]
commands = nosetests
@@ -74,11 +104,26 @@ deps =
result = open('tox.ini').read()
assert(result == expected_tox_ini)
- def test_quickstart_main_choose_individual_pythons_and_trial_adds_deps(self, monkeypatch):
+ def test_quickstart_main_choose_individual_pythons_and_trial_adds_deps(
+ self,
+ monkeypatch):
monkeypatch.setattr(
tox._quickstart, 'term_input',
- self.get_mock_term_input(['4', 'Y', 'Y', 'Y', 'Y', 'Y', 'N',
- 'trial', '']))
+ self.get_mock_term_input(
+ [
+ '4', # Python versions: choose one by one
+ 'Y', # py26
+ 'Y', # py27
+ 'Y', # py32
+ 'Y', # py33
+ 'Y', # py34
+ 'Y', # pypy
+ 'N', # jython
+ 'trial', # command to run tests
+ '' # test dependencies
+ ]
+ )
+ )
tox._quickstart.main(argv=['tox-quickstart'])
@@ -89,7 +134,7 @@ deps =
# and then run "tox" from this directory.
[tox]
-envlist = py26, py27, py32, py33, pypy
+envlist = py26, py27, py32, py33, py34, pypy
[testenv]
commands = trial
@@ -99,11 +144,26 @@ deps =
result = open('tox.ini').read()
assert(result == expected_tox_ini)
- def test_quickstart_main_choose_individual_pythons_and_pytest_adds_deps(self, monkeypatch):
+ def test_quickstart_main_choose_individual_pythons_and_pytest_adds_deps(
+ self,
+ monkeypatch):
monkeypatch.setattr(
tox._quickstart, 'term_input',
- self.get_mock_term_input(['4', 'Y', 'Y', 'Y', 'Y', 'Y', 'N',
- 'py.test', '']))
+ self.get_mock_term_input(
+ [
+ '4', # Python versions: choose one by one
+ 'Y', # py26
+ 'Y', # py27
+ 'Y', # py32
+ 'Y', # py33
+ 'Y', # py34
+ 'Y', # pypy
+ 'N', # jython
+ 'py.test', # command to run tests
+ '' # test dependencies
+ ]
+ )
+ )
tox._quickstart.main(argv=['tox-quickstart'])
expected_tox_ini = """
@@ -113,7 +173,7 @@ deps =
# and then run "tox" from this directory.
[tox]
-envlist = py26, py27, py32, py33, pypy
+envlist = py26, py27, py32, py33, py34, pypy
[testenv]
commands = py.test
@@ -123,10 +183,19 @@ deps =
result = open('tox.ini').read()
assert(result == expected_tox_ini)
- def test_quickstart_main_choose_py27_and_pytest_adds_deps(self, monkeypatch):
+ def test_quickstart_main_choose_py27_and_pytest_adds_deps(
+ self,
+ monkeypatch):
monkeypatch.setattr(
tox._quickstart, 'term_input',
- self.get_mock_term_input(['1', 'py.test', '']))
+ self.get_mock_term_input(
+ [
+ '1', # py27
+ 'py.test', # command to run tests
+ '' # test dependencies
+ ]
+ )
+ )
tox._quickstart.main(argv=['tox-quickstart'])
@@ -147,10 +216,19 @@ deps =
result = open('tox.ini').read()
assert(result == expected_tox_ini)
- def test_quickstart_main_choose_py27_and_py33_and_pytest_adds_deps(self, monkeypatch):
+ def test_quickstart_main_choose_py27_and_py33_and_pytest_adds_deps(
+ self,
+ monkeypatch):
monkeypatch.setattr(
tox._quickstart, 'term_input',
- self.get_mock_term_input(['2', 'py.test', '']))
+ self.get_mock_term_input(
+ [
+ '2', # py27 and py33
+ 'py.test', # command to run tests
+ '' # test dependencies
+ ]
+ )
+ )
tox._quickstart.main(argv=['tox-quickstart'])
@@ -171,10 +249,19 @@ deps =
result = open('tox.ini').read()
assert(result == expected_tox_ini)
- def test_quickstart_main_choose_all_pythons_and_pytest_adds_deps(self, monkeypatch):
+ def test_quickstart_main_choose_all_pythons_and_pytest_adds_deps(
+ self,
+ monkeypatch):
monkeypatch.setattr(
tox._quickstart, 'term_input',
- self.get_mock_term_input(['3', 'py.test', '']))
+ self.get_mock_term_input(
+ [
+ '3', # all Python versions
+ 'py.test', # command to run tests
+ '' # test dependencies
+ ]
+ )
+ )
tox._quickstart.main(argv=['tox-quickstart'])
@@ -185,7 +272,7 @@ deps =
# and then run "tox" from this directory.
[tox]
-envlist = py26, py27, py32, py33, pypy, jython
+envlist = py26, py27, py32, py33, py34, pypy, jython
[testenv]
commands = py.test
@@ -195,10 +282,26 @@ deps =
result = open('tox.ini').read()
assert(result == expected_tox_ini)
- def test_quickstart_main_choose_individual_pythons_and_defaults(self, monkeypatch):
+ def test_quickstart_main_choose_individual_pythons_and_defaults(
+ self,
+ monkeypatch):
monkeypatch.setattr(
tox._quickstart, 'term_input',
- self.get_mock_term_input(['4', '', '', '', '', '', '', '', '', '', '']))
+ self.get_mock_term_input(
+ [
+ '4', # Python versions: choose one by one
+ '', # py26
+ '', # py27
+ '', # py32
+ '', # py33
+ '', # py34
+ '', # pypy
+ '', # jython
+ '', # command to run tests
+ '' # test dependencies
+ ]
+ )
+ )
tox._quickstart.main(argv=['tox-quickstart'])
@@ -209,7 +312,7 @@ deps =
# and then run "tox" from this directory.
[tox]
-envlist = py26, py27, py32, py33, pypy, jython
+envlist = py26, py27, py32, py33, py34, pypy, jython
[testenv]
commands = {envpython} setup.py test
@@ -228,7 +331,22 @@ deps =
monkeypatch.setattr(
tox._quickstart, 'term_input',
- self.get_mock_term_input(['4', '', '', '', '', '', '', '', '', '', '', '']))
+ self.get_mock_term_input(
+ [
+ '4', # Python versions: choose one by one
+ '', # py26
+ '', # py27
+ '', # py32
+ '', # py33
+ '', # py34
+ '', # pypy
+ '', # jython
+ '', # command to run tests
+ '', # test dependencies
+ '', # tox.ini already exists; overwrite?
+ ]
+ )
+ )
tox._quickstart.main(argv=['tox-quickstart'])
@@ -239,7 +357,7 @@ deps =
# and then run "tox" from this directory.
[tox]
-envlist = py26, py27, py32, py33, pypy, jython
+envlist = py26, py27, py32, py33, py34, pypy, jython
[testenv]
commands = {envpython} setup.py test
@@ -257,6 +375,7 @@ class TestToxQuickstart(object):
'py27': True,
'py32': True,
'py33': True,
+ 'py34': True,
'pypy': True,
'commands': 'py.test',
'deps': 'pytest',
@@ -268,7 +387,7 @@ class TestToxQuickstart(object):
# and then run "tox" from this directory.
[tox]
-envlist = py26, py27, py32, py33, pypy
+envlist = py26, py27, py32, py33, py34, pypy
[testenv]
commands = py.test
@@ -339,6 +458,7 @@ deps =
'py27': True,
'py32': True,
'py33': True,
+ 'py34': True,
'pypy': True,
'commands': 'nosetests -v',
'deps': 'nose',
@@ -350,7 +470,7 @@ deps =
# and then run "tox" from this directory.
[tox]
-envlist = py27, py32, py33, pypy
+envlist = py27, py32, py33, py34, pypy
[testenv]
commands = nosetests -v
diff --git a/tests/test_venv.py b/tests/test_venv.py
index 07b89f4..b1d1f2e 100644
--- a/tests/test_venv.py
+++ b/tests/test_venv.py
@@ -3,7 +3,8 @@ import tox
import pytest
import os, sys
import tox._config
-from tox._venv import *
+from tox._venv import * # noqa
+from tox.interpreters import NoInterpreterInfo
#def test_global_virtualenv(capfd):
# v = VirtualEnv()
@@ -34,6 +35,12 @@ def test_getsupportedinterpreter(monkeypatch, newconfig, mocksession):
monkeypatch.setattr(venv.envconfig, 'basepython', 'notexistingpython')
py.test.raises(tox.exception.InterpreterNotFound,
venv.getsupportedinterpreter)
+ monkeypatch.undo()
+ # check that we properly report when no version_info is present
+ info = NoInterpreterInfo(name=venv.name)
+ info.executable = "something"
+ monkeypatch.setattr(config.interpreters, "get_info", lambda *args: info)
+ pytest.raises(tox.exception.InvocationError, venv.getsupportedinterpreter)
def test_create(monkeypatch, mocksession, newconfig):
@@ -277,7 +284,7 @@ def test_install_command_not_installed(newmocksession, monkeypatch):
venv = mocksession.getenv('python')
venv.test()
mocksession.report.expect("warning", "*test command found but not*")
- assert venv.status == "commands failed"
+ assert venv.status == 0
def test_install_command_whitelisted(newmocksession, monkeypatch):
mocksession = newmocksession(['--recreate'], """
@@ -295,7 +302,7 @@ def test_install_command_whitelisted(newmocksession, monkeypatch):
assert venv.status == "commands failed"
@pytest.mark.skipif("not sys.platform.startswith('linux')")
-def test_install_command_not_installed(newmocksession):
+def test_install_command_not_installed_bash(newmocksession):
mocksession = newmocksession(['--recreate'], """
[testenv]
commands=
@@ -387,7 +394,7 @@ class TestCreationConfig:
[testenv]
deps={distshare}/xyz-*
""")
- xyz = config.distshare.ensure("xyz-1.2.0.zip")
+ config.distshare.ensure("xyz-1.2.0.zip")
xyz2 = config.distshare.ensure("xyz-1.2.1.zip")
envconfig = config.envconfigs['python']
venv = VirtualEnv(envconfig, session=mocksession)
@@ -485,9 +492,11 @@ class TestVenvTest:
py.test.raises(ZeroDivisionError, "venv._pcall([1,2,3])")
monkeypatch.setenv("PIP_RESPECT_VIRTUALENV", "1")
monkeypatch.setenv("PIP_REQUIRE_VIRTUALENV", "1")
+ monkeypatch.setenv("__PYVENV_LAUNCHER__", "1")
py.test.raises(ZeroDivisionError, "venv.run_install_command(['qwe'])")
assert 'PIP_RESPECT_VIRTUALENV' not in os.environ
assert 'PIP_REQUIRE_VIRTUALENV' not in os.environ
+ assert '__PYVENV_LAUNCHER__' not in os.environ
def test_setenv_added_to_pcall(tmpdir, mocksession, newconfig):
pkg = tmpdir.ensure("package.tar.gz")
@@ -507,11 +516,11 @@ def test_setenv_added_to_pcall(tmpdir, mocksession, newconfig):
l = mocksession._pcalls
assert len(l) == 2
for x in l:
- args = x.args
env = x.env
assert env is not None
assert 'ENV_VAR' in env
assert env['ENV_VAR'] == 'value'
+ assert env['VIRTUAL_ENV'] == str(venv.path)
for e in os.environ:
assert e in env
@@ -553,8 +562,6 @@ def test_run_install_command(newmocksession):
assert 'install' in l[0].args
env = l[0].env
assert env is not None
- assert 'PYTHONIOENCODING' in env
- assert env['PYTHONIOENCODING'] == 'utf_8'
def test_run_custom_install_command(newmocksession):
mocksession = newmocksession([], """
@@ -585,6 +592,7 @@ def test_command_relative_issue26(newmocksession, tmpdir, monkeypatch):
mocksession.report.not_expect("warning", "*test command found but not*")
monkeypatch.setenv("PATH", str(tmpdir))
x4 = venv.getcommandpath("x", cwd=tmpdir)
+ assert x4.endswith(os.sep + 'x')
mocksession.report.expect("warning", "*test command found but not*")
def test_sethome_only_on_option(newmocksession, monkeypatch):
diff --git a/tests/test_z_cmdline.py b/tests/test_z_cmdline.py
index 343c142..00188ac 100644
--- a/tests/test_z_cmdline.py
+++ b/tests/test_z_cmdline.py
@@ -1,7 +1,6 @@
import tox
import py
import pytest
-import sys
from tox._pytestplugin import ReportExpectMock
try:
import json
@@ -129,7 +128,6 @@ class TestSession:
})
config = parseconfig([])
session = Session(config)
- envlist = ['hello', 'world']
envs = session.venvlist
assert len(envs) == 2
env1, env2 = envs
@@ -193,6 +191,19 @@ def test_minversion(cmd, initproj):
])
assert result.ret
+def test_run_custom_install_command_error(cmd, initproj):
+ initproj("interp123-0.5", filedefs={
+ 'tox.ini': '''
+ [testenv]
+ install_command=./tox.ini {opts} {packages}
+ '''
+ })
+ result = cmd.run("tox")
+ result.stdout.fnmatch_lines([
+ "ERROR: invocation failed (errno *), args: ['*/tox.ini*",
+ ])
+ assert result.ret
+
def test_unknown_interpreter_and_env(cmd, initproj):
initproj("interp123-0.5", filedefs={
'tests': {'test_hello.py': "def test_hello(): pass"},
@@ -231,6 +242,22 @@ def test_unknown_interpreter(cmd, initproj):
"*ERROR*InterpreterNotFound*xyz_unknown_interpreter*",
])
+def test_skip_unknown_interpreter(cmd, initproj):
+ initproj("interp123-0.5", filedefs={
+ 'tests': {'test_hello.py': "def test_hello(): pass"},
+ 'tox.ini': '''
+ [testenv:python]
+ basepython=xyz_unknown_interpreter
+ [testenv]
+ changedir=tests
+ '''
+ })
+ result = cmd.run("tox", "--skip-missing-interpreters")
+ assert not result.ret
+ result.stdout.fnmatch_lines([
+ "*SKIPPED*InterpreterNotFound*xyz_unknown_interpreter*",
+ ])
+
def test_unknown_dep(cmd, initproj):
initproj("dep123-0.7", filedefs={
'tests': {'test_hello.py': "def test_hello(): pass"},
@@ -567,7 +594,7 @@ def test_sdistonly(initproj, cmd):
result.stdout.fnmatch_lines([
"*sdist-make*setup.py*",
])
- assert "virtualenv" not in result.stdout.str()
+ assert "-mvirtualenv" not in result.stdout.str()
def test_separate_sdist_no_sdistfile(cmd, initproj):
distshare = cmd.tmpdir.join("distshare")
@@ -582,6 +609,7 @@ def test_separate_sdist_no_sdistfile(cmd, initproj):
l = distshare.listdir()
assert len(l) == 1
sdistfile = l[0]
+ assert 'pkg123-0.7.zip' in str(sdistfile)
def test_separate_sdist(cmd, initproj):
distshare = cmd.tmpdir.join("distshare")
@@ -611,7 +639,6 @@ def test_sdist_latest(tmpdir, newconfig):
distshare=%s
sdistsrc={distshare}/pkg123-*
""" % distshare)
- p0 = distshare.ensure("pkg123-1.3.5.zip")
p = distshare.ensure("pkg123-1.4.5.zip")
distshare.ensure("pkg123-1.4.5a1.zip")
session = Session(config)
diff --git a/tox.ini b/tox.ini
index f2fc791..6bcca5e 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-envlist=py27,py33,py26,py32,pypy
+envlist=py27,py26,py34,py33,py32,pypy,flakes
[testenv:X]
commands=echo {posargs}
@@ -18,6 +18,10 @@ commands=
--junitxml={envlogdir}/junit-{envname}.xml \
check_sphinx.py {posargs}
+[testenv:flakes]
+deps = pytest-flakes>=0.2
+commands = py.test --flakes -m flakes tox tests
+
[testenv:py25]
setenv= PIP_INSECURE=1
diff --git a/tox/__init__.py b/tox/__init__.py
index 8f0acb1..47b85e4 100644
--- a/tox/__init__.py
+++ b/tox/__init__.py
@@ -1,5 +1,5 @@
#
-__version__ = '1.7.1'
+__version__ = '1.8.1.dev1'
class exception:
class Error(Exception):
@@ -20,4 +20,4 @@ class exception:
class MissingDependency(Error):
""" a dependency could not be found or determined. """
-from tox._cmdline import main as cmdline
+from tox._cmdline import main as cmdline # noqa
diff --git a/tox/_cmdline.py b/tox/_cmdline.py
index 7c6cd69..0df2f17 100644
--- a/tox/_cmdline.py
+++ b/tox/_cmdline.py
@@ -79,7 +79,6 @@ class Action(object):
return f
def popen(self, args, cwd=None, env=None, redirect=True, returnout=False):
- logged_command = "%s$ %s" %(cwd, " ".join(map(str, args)))
f = outpath = None
resultjson = self.session.config.option.resultjson
if resultjson or redirect:
@@ -93,8 +92,13 @@ class Action(object):
if cwd is None:
# XXX cwd = self.session.config.cwd
cwd = py.path.local()
- popen = self._popen(args, cwd, env=env,
- stdout=f, stderr=STDOUT)
+ try:
+ popen = self._popen(args, cwd, env=env,
+ stdout=f, 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
@@ -114,8 +118,8 @@ class Action(object):
if ret:
invoked = " ".join(map(str, popen.args))
if outpath:
- self.report.error("invocation failed, logfile: %s" %
- outpath)
+ self.report.error("invocation failed (exit code %d), logfile: %s" %
+ (ret, outpath))
out = outpath.read()
self.report.error(out)
if hasattr(self, "commandlog"):
@@ -223,6 +227,9 @@ class Reporter(object):
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)
@@ -411,7 +418,8 @@ class Session:
sdist_path = self._makesdist()
except tox.exception.InvocationError:
v = sys.exc_info()[1]
- self.report.error("FAIL could not package project")
+ self.report.error("FAIL could not package project - v = %r" %
+ v)
return
sdistfile = self.config.distshare.join(sdist_path.basename)
if sdistfile != sdist_path:
@@ -461,7 +469,14 @@ class Session:
retcode = 0
for venv in self.venvlist:
status = venv.status
- if status and status != "skipped tests":
+ 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 and status != "skipped tests":
msg = " %s: %s" %(venv.envconfig.envname, str(status))
self.report.error(msg)
retcode = 1
diff --git a/tox/_config.py b/tox/_config.py
index 4d54924..f0ad87e 100644
--- a/tox/_config.py
+++ b/tox/_config.py
@@ -1,14 +1,12 @@
import argparse
-import distutils.sysconfig
import os
import random
import sys
import re
import shlex
import string
-import subprocess
-import textwrap
import pkg_resources
+import itertools
from tox.interpreters import Interpreters
@@ -18,13 +16,10 @@ import tox
iswin32 = sys.platform == "win32"
-defaultenvs = {'jython': 'jython', 'pypy': 'pypy'}
-for _name in "py,py24,py25,py26,py27,py30,py31,py32,py33,py34".split(","):
- if _name == "py":
- basepython = sys.executable
- else:
- basepython = "python" + ".".join(_name[2:4])
- defaultenvs[_name] = basepython
+default_factors = {'jython': 'jython', 'pypy': 'pypy', 'pypy3': 'pypy3',
+ 'py': sys.executable}
+for version in '24,25,26,27,30,31,32,33,34'.split(','):
+ default_factors['py' + version] = 'python%s.%s' % tuple(version)
def parseconfig(args=None, pkg=None):
if args is None:
@@ -123,7 +118,8 @@ def prepare_parse(pkgname):
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 to 4294967295. "
+ "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,
@@ -132,6 +128,8 @@ def prepare_parse(pkgname):
"'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("args", nargs="*",
help="additional arguments available to command positional substitution")
@@ -186,6 +184,9 @@ class VenvConfig:
info = self.config.interpreters.get_info(self.basepython)
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")
@@ -200,12 +201,15 @@ def get_homedir():
return None
def make_hashseed():
- return str(random.randint(1, 4294967295))
+ 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 = toxinidir = config.toxinipath.dirpath()
+ config.toxinidir = config.toxinipath.dirpath()
self._cfg = py.iniconfig.IniConfig(config.toxinipath)
config._cfg = self._cfg
@@ -236,10 +240,14 @@ class parseini:
"{toxinidir}/.tox")
config.minversion = reader.getdefault(toxsection, "minversion", None)
+ if not config.option.skip_missing_interpreters:
+ config.option.skip_missing_interpreters = \
+ reader.getbool(toxsection, "skip_missing_interpreters", False)
+
# determine indexserver dictionary
config.indexserver = {'default': IndexServerConfig('default')}
prefix = "indexserver"
- for line in reader.getlist(toxsection, "indexserver"):
+ for line in reader.getlist(toxsection, prefix):
name, url = map(lambda x: x.strip(), line.split("=", 1))
config.indexserver[name] = IndexServerConfig(name, url)
@@ -273,22 +281,19 @@ class parseini:
config.sdistsrc = reader.getpath(toxsection, "sdistsrc", None)
config.setupdir = reader.getpath(toxsection, "setupdir", "{toxinidir}")
config.logdir = config.toxworkdir.join("log")
- for sectionwrapper in self._cfg:
- section = sectionwrapper.name
- if section.startswith(testenvprefix):
- name = section[len(testenvprefix):]
- envconfig = self._makeenvconfig(name, section, reader._subs,
- config)
- config.envconfigs[name] = envconfig
- if not config.envconfigs:
- config.envconfigs['python'] = \
- self._makeenvconfig("python", "_xz_9", reader._subs, config)
- config.envlist = self._getenvlist(reader, toxsection)
- for name in config.envlist:
- if name not in config.envconfigs:
- if name in defaultenvs:
- config.envconfigs[name] = \
- self._makeenvconfig(name, "_xz_9", reader._subs, config)
+
+ config.envlist, all_envs = self._getenvdata(reader, toxsection)
+
+ # configure testenvs
+ known_factors = self._list_section_factors("testenv")
+ known_factors.update(default_factors)
+ known_factors.add("python")
+ 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._makeenvconfig(name, section, reader._subs, config)
all_develop = all(name in config.envconfigs
and config.envconfigs[name].develop
@@ -296,10 +301,20 @@ class parseini:
config.skipsdist = reader.getbool(toxsection, "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 _makeenvconfig(self, name, section, subs, config):
vc = VenvConfig(envname=name)
vc.config = config
- reader = IniReader(self._cfg, fallbacksections=["testenv"])
+ factors = set(name.split('-'))
+ reader = IniReader(self._cfg, fallbacksections=["testenv"],
+ factors=factors)
reader.addsubstitutions(**subs)
vc.develop = not config.option.installpkg and \
reader.getbool(section, "usedevelop", config.option.develop)
@@ -308,10 +323,8 @@ class parseini:
if reader.getdefault(section, "python", None):
raise tox.exception.ConfigError(
"'python=' key was renamed to 'basepython='")
- if name in defaultenvs:
- bp = defaultenvs[name]
- else:
- bp = sys.executable
+ bp = next((default_factors[f] for f in factors if f in default_factors),
+ sys.executable)
vc.basepython = reader.getdefault(section, "basepython", bp)
vc._basepython_info = config.interpreters.get_info(vc.basepython)
reader.addsubstitutions(envdir=vc.envdir, envname=vc.envname,
@@ -379,20 +392,25 @@ class parseini:
"'install_command' must contain '{packages}' substitution")
return vc
- def _getenvlist(self, reader, toxsection):
- env = self.config.option.env
- if not env:
- env = os.environ.get("TOXENV", None)
- if not env:
- envlist = reader.getlist(toxsection, "envlist", sep=",")
- if not envlist:
- envlist = self.config.envconfigs.keys()
- return envlist
- envlist = _split_env(env)
- if "ALL" in envlist:
- envlist = list(self.config.envconfigs)
- envlist.sort()
- return envlist
+ def _getenvdata(self, reader, toxsection):
+ envstr = self.config.option.env \
+ or os.environ.get("TOXENV") \
+ or reader.getdefault(toxsection, "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 _replace_forced_dep(self, name, config):
"""
@@ -420,17 +438,32 @@ class parseini:
dep2_name = pkg_resources.Requirement.parse(dep2).project_name
return dep1_name == dep2_name
+
def _split_env(env):
"""if handed a list, action="append" was used for -e """
- envlist = []
if not isinstance(env, list):
env = [env]
- for to_split in env:
- for single_env in to_split.split(","):
- # "remove True or", if not allowing multiple same runs, update tests
- if True or single_env not in envlist:
- envlist.append(single_env)
- return envlist
+ 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):
@@ -461,9 +494,10 @@ RE_ITEM_REF = re.compile(
class IniReader:
- def __init__(self, cfgparser, fallbacksections=None):
+ def __init__(self, cfgparser, fallbacksections=None, factors=()):
self._cfg = cfgparser
self.fallbacksections = fallbacksections or []
+ self.factors = factors
self._subs = {}
self._subststack = []
@@ -492,6 +526,8 @@ class IniReader:
value = {}
for line in s.split(sep):
+ if not line.strip():
+ continue
name, rest = line.split('=', 1)
value[name.strip()] = rest.strip()
@@ -527,30 +563,35 @@ class IniReader:
def _processcommand(self, command):
posargs = getattr(self, "posargs", None)
- # special treat posargs which might contain multiple arguments
- # in their defaults
+ # 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.startswith("{posargs:") and word.endswith("}"):
+ if word == "{posargs}" or word == "[]":
+ if posargs:
+ newcommand += " ".join(posargs)
+ continue
+ elif word.startswith("{posargs:") and word.endswith("}"):
if posargs:
- word = "{posargs}"
+ newcommand += " ".join(posargs)
+ continue
else:
word = word[9:-1]
- newcommand += word
-
- # now we can properly parse the command
- argv = []
- for arg in shlex.split(newcommand):
- if arg in ('[]', "{posargs}"):
- if posargs:
- argv.extend(posargs)
- continue
new_arg = ""
- for word in CommandParser(arg).words():
- new_word = self._replace(word)
- new_word = self._replace(new_word)
- new_arg += new_word
- argv.append(new_arg)
+ new_word = self._replace(word)
+ new_word = self._replace(new_word)
+ 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 = ''
+ shlexer.commenters = ''
+ argv = list(shlexer)
return argv
def getargv(self, section, name, default=None, replace=True):
@@ -560,9 +601,12 @@ class IniReader:
def getbool(self, section, name, default=None):
s = self.getdefault(section, name, default)
+ if not s:
+ s = default
if s is None:
raise KeyError("no config value [%s] %s found" % (
section, name))
+
if not isinstance(s, bool):
if s.lower() == "true":
s = True
@@ -574,18 +618,19 @@ class IniReader:
return s
def getdefault(self, section, name, default=None, replace=True):
- try:
- x = self._cfg[section][name]
- except KeyError:
- for fallbacksection in self.fallbacksections:
- try:
- x = self._cfg[fallbacksection][name]
- except KeyError:
- pass
- else:
- break
- else:
- x = default
+ x = None
+ for s in [section] + 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'):
self._subststack.append((section, name))
try:
@@ -595,18 +640,39 @@ class IniReader:
#print "getdefault", section, 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_env(self, match):
- envkey = match.group('substitution_value')
- if not envkey:
+ match_value = match.group('substitution_value')
+ if not match_value:
raise tox.exception.ConfigError(
'env: requires an environment variable name')
- if not envkey in os.environ:
+ default = None
+ envkey_split = match_value.split(':', 1)
+
+ if len(envkey_split) is 2:
+ envkey, default = envkey_split
+ else:
+ envkey = match_value
+
+ if not envkey in os.environ and default is None:
raise tox.exception.ConfigError(
"substitution env:%r: unkown environment variable %r" %
(envkey, envkey))
- return os.environ[envkey]
+ return os.environ.get(envkey, default)
def _substitute_from_other_section(self, key):
if key.startswith("[") and "]" in key:
diff --git a/tox/_quickstart.py b/tox/_quickstart.py
index f66f1e2..098eb61 100644
--- a/tox/_quickstart.py
+++ b/tox/_quickstart.py
@@ -56,7 +56,7 @@ except NameError:
term_input = input
-all_envs = ['py26', 'py27', 'py32', 'py33', 'pypy', 'jython']
+all_envs = ['py26', 'py27', 'py32', 'py33', 'py34', 'pypy', 'jython']
PROMPT_PREFIX = '> '
diff --git a/tox/_venv.py b/tox/_venv.py
index d0f2e64..0c56cc0 100644
--- a/tox/_venv.py
+++ b/tox/_venv.py
@@ -1,6 +1,6 @@
from __future__ import with_statement
-import sys, os, re
-import subprocess
+import sys, os
+import codecs
import py
import tox
from tox._config import DepConfig
@@ -121,8 +121,6 @@ class VirtualEnv(object):
"""
if action is None:
action = self.session.newaction(self, "update")
- report = self.session.report
- name = self.envconfig.envname
rconfig = CreationConfig.readconfig(self.path_config)
if not self.envconfig.recreate and rconfig and \
rconfig.matches(self._getliveconfig()):
@@ -142,7 +140,8 @@ class VirtualEnv(object):
self.install_deps(action)
except tox.exception.InvocationError:
v = sys.exc_info()[1]
- return "could not install deps %s" %(self.envconfig.deps,)
+ return "could not install deps %s; v = %r" % (
+ self.envconfig.deps, v)
def _getliveconfig(self):
python = self.envconfig._basepython_info.executable
@@ -273,16 +272,19 @@ class VirtualEnv(object):
if '{opts}' in argv:
i = argv.index('{opts}')
argv[i:i+1] = list(options)
- for x in ('PIP_RESPECT_VIRTUALENV', 'PIP_REQUIRE_VIRTUALENV'):
+ for x in ('PIP_RESPECT_VIRTUALENV', 'PIP_REQUIRE_VIRTUALENV',
+ '__PYVENV_LAUNCHER__'):
try:
del os.environ[x]
except KeyError:
pass
- env = dict(PYTHONIOENCODING='utf_8')
- if extraenv is not None:
- env.update(extraenv)
+ old_stdout = sys.stdout
+ sys.stdout = codecs.getwriter('utf8')(sys.stdout)
+ if extraenv is None:
+ extraenv = {}
self._pcall(argv, cwd=self.envconfig.config.toxinidir,
- extraenv=env, action=action)
+ extraenv=extraenv, action=action)
+ sys.stdout = old_stdout
def _install(self, deps, extraopts=None, action=None):
if not deps:
@@ -322,6 +324,7 @@ class VirtualEnv(object):
setenv = self.envconfig.setenv
if setenv:
env.update(setenv)
+ env['VIRTUAL_ENV'] = str(self.path)
env.update(extraenv)
return env
diff --git a/tox/_verlib.py b/tox/_verlib.py
index 1df3645..a234176 100644
--- a/tox/_verlib.py
+++ b/tox/_verlib.py
@@ -8,7 +8,6 @@ licensed under the PSF license (i guess)
"""
-import sys
import re
class IrrationalVersionError(Exception):
diff --git a/tox/interpreters.py b/tox/interpreters.py
index e225fcc..75318c5 100644
--- a/tox/interpreters.py
+++ b/tox/interpreters.py
@@ -1,8 +1,6 @@
import sys
-import os
import py
import re
-import subprocess
import inspect
class Interpreters:
@@ -161,7 +159,7 @@ else:
# The standard executables can be found as a last resort via the
# Python launcher py.exe
if m:
- locate_via_py(*m.groups())
+ return locate_via_py(*m.groups())
def pyinfo():
import sys