diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 27c00920a..41d395a29 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,11 +7,6 @@ repos: - id: check-yaml - id: debug-statements - id: flake8 -- repo: https://github.com/asottile/reorder_python_imports - sha: v0.3.5 - hooks: - - id: reorder-python-imports - language_version: python3.6 - repo: https://github.com/asottile/pyupgrade sha: v1.2.0 hooks: diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a703d8a22..643787959 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,7 +18,7 @@ on Github: - `2.8 series of releases `_ .. - Everything below here is generated by `towncrier `_. + Everything below here is generated by `towncrier `_. It is generated once as part of the release process rendering fragments from the `changelog` folder. If necessary, the generated text can be edited afterwards to e.g. merge rc changes into the final release notes. @@ -781,7 +781,7 @@ Improved Documentation location ({envtmpdir}/pseudo-home). If an index url was specified a .pydistutils.cfg file will be written with an index_url setting so that packages defining ``setup_requires`` dependencies will not - silently use your HOME-directory settings or https://pypi.python.org/pypi. + silently use your HOME-directory settings or PyPi. - fix `#1 `_: empty setup files are properly detected, thanks Anthon van der Neuth diff --git a/HOWTORELEASE.rst b/HOWTORELEASE.rst index 0a53d5e6f..444aea043 100644 --- a/HOWTORELEASE.rst +++ b/HOWTORELEASE.rst @@ -38,7 +38,6 @@ If you want to use the scripts in `task/` you need a `.pypirc` with a correctly .. code-block:: ini [pypi] - ;repository=https://pypi.python.org/pypi ;repository=https://upload.pypi.io/legacy/ username= password= diff --git a/MANIFEST.in b/MANIFEST.in index e7995530b..0a9d6ae96 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,3 +6,6 @@ include setup.py include tox.ini graft doc graft tests + +global-exclude __pycache__ +global-exclude *.py[cod] diff --git a/changelog/754.misc.rst b/changelog/754.misc.rst new file mode 100644 index 000000000..90c0d6f45 --- /dev/null +++ b/changelog/754.misc.rst @@ -0,0 +1 @@ +filter out unwanted files in package - by @obestwalter diff --git a/changelog/797.doc.rst b/changelog/797.doc.rst new file mode 100644 index 000000000..ad4d96935 --- /dev/null +++ b/changelog/797.doc.rst @@ -0,0 +1 @@ +extend the plugin documentation and make lot of small fixes and improvements - by @obestwalter diff --git a/changelog/798.feature.rst b/changelog/798.feature.rst new file mode 100644 index 000000000..87caabf30 --- /dev/null +++ b/changelog/798.feature.rst @@ -0,0 +1 @@ +introduce a constants module to be used internally and as experimental API - by @obestwalter diff --git a/changelog/799.doc.rst b/changelog/799.doc.rst new file mode 100644 index 000000000..c3031d682 --- /dev/null +++ b/changelog/799.doc.rst @@ -0,0 +1 @@ +tidy up tests - remove unused fixtures, update old cinstructs, etc. - by @obestwalter diff --git a/changelog/800.misc.rst b/changelog/800.misc.rst new file mode 100644 index 000000000..6fddb9740 --- /dev/null +++ b/changelog/800.misc.rst @@ -0,0 +1 @@ +make the already existing implicit API explicit - by @obestwalter diff --git a/changelog/801.misc.rst b/changelog/801.misc.rst new file mode 100644 index 000000000..8f148d676 --- /dev/null +++ b/changelog/801.misc.rst @@ -0,0 +1 @@ +improve tox quickstart and corresponding tests - @obestwalter diff --git a/changelog/examples.rst b/changelog/examples.rst index b5fb730b4..746d86de1 100644 --- a/changelog/examples.rst +++ b/changelog/examples.rst @@ -1,10 +1,18 @@ +.. examples for changelog entries adding to your Pull Requests + +file ``544.doc.rst``:: + + explain everything much better - by @passionate_technicalwriter + file ``544.feature.rst``:: - ``tox --version`` now shows information about all registered plugins - by @obestwalter. + ``tox --version`` now shows information about all registered plugins - by @obestwalter file ``571.bugfix.rst``:: ``skip_install`` overrides ``usedevelop`` (``usedevelop`` is an option to choose the installation type if the package is installed and `skip_install` determines if it should be - installed at all) - by @ferdonline. + installed at all) - by @ferdonline + +.. see tox/pyproject.toml for all available categories diff --git a/doc/example/basic.rst b/doc/example/basic.rst index cd3872da4..5a74f7fd8 100644 --- a/doc/example/basic.rst +++ b/doc/example/basic.rst @@ -88,7 +88,7 @@ configuration: /bin/bash -.. _virtualenv: https://pypi.python.org/pypi/virtualenv +.. _virtualenv: https://pypi.org/project/virtualenv .. _multiindex: @@ -175,8 +175,8 @@ like this: .. code-block:: shell - tox -i DEV=http://pypi.python.org/simple # changes :DEV: package URLs - tox -i http://pypi.python.org/simple # changes default + tox -i DEV=http://pypi.org/simple # changes :DEV: package URLs + tox -i http://pypi.org/simple # changes default further customizing installation --------------------------------- diff --git a/doc/example/pytest.rst b/doc/example/pytest.rst index f871090d7..2f49f0919 100644 --- a/doc/example/pytest.rst +++ b/doc/example/pytest.rst @@ -5,8 +5,6 @@ It is easy to integrate `pytest`_ runs with tox. If you encounter issues, please check if they are `listed as a known issue`_ and/or use the :doc:`support channels <../support>`. -.. _`pytest`: https://docs.pytest.org/en/latest/ - Basic example -------------------------- diff --git a/doc/example/unittest.rst b/doc/example/unittest.rst index 860b73d2b..2d43af5ab 100644 --- a/doc/example/unittest.rst +++ b/doc/example/unittest.rst @@ -4,11 +4,9 @@ unittest2, discover and tox Running unittests with 'discover' ------------------------------------------ -.. _Pygments: https://pypi.python.org/pypi/Pygments - The discover_ project allows to discover and run unittests and we can easily integrate it in a ``tox`` run. As an example, -perform a checkout of Pygments_: +perform a checkout of `Pygments `_: .. code-block:: shell @@ -34,7 +32,6 @@ Running unittest2 and sphinx tests in one go ----------------------------------------------------- .. _`Michael Foord`: http://www.voidspace.org.uk/ -.. _tox.ini: https://github.com/testing-cabal/mock/blob/master/tox.ini `Michael Foord`_ has contributed a ``tox.ini`` file that allows you to run all tests for his mock_ project, @@ -45,7 +42,8 @@ its repository with: git clone https://github.com/testing-cabal/mock.git -the checkout has a tox.ini_ that looks like this: +The checkout has a `tox.ini file `_ +that looks like this: .. code-block:: ini diff --git a/doc/index.rst b/doc/index.rst index 6ca95f2b4..1ffe3ab32 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -33,9 +33,12 @@ right next to your ``setup.py`` file:: # content of: tox.ini , put in same dir as setup.py [tox] envlist = py27,py36 + [testenv] - deps=pytest # install pytest in the venvs - commands=pytest # or 'nosetests' or ... + deps = pytest # install pytest in the virtualenv where commands will be executed + commands = + # whatever extra steps before testing might be necessary + pytest # or any other test runner that you might use You can also try generating a ``tox.ini`` file automatically, by running ``tox-quickstart`` and then answering a few simple questions. @@ -63,13 +66,12 @@ Current features - test-tool agnostic: runs pytest, nose or unittests in a uniform manner -* :doc:`(new in 2.0) plugin system ` to modify tox execution with simple hooks. +* :doc:`plugin system ` to modify tox execution with simple hooks. * uses pip_ and setuptools_ by default. Support for configuring the installer command through :confval:`install_command=ARGV`. -* **cross-Python compatible**: CPython-2.7, 3.4 and higher, - Jython and pypy_. +* **cross-Python compatible**: CPython-2.7, 3.4 and higher, Jython and pypy_. * **cross-platform**: Windows and Unix style environments @@ -92,10 +94,6 @@ Current features * supports :ref:`using different / multiple PyPI index servers ` -.. _pypy: http://pypy.org - -.. _`tox.ini`: :doc:configfile - .. toctree:: :hidden: diff --git a/doc/links.rst b/doc/links.rst index 933b03503..79a1996de 100644 --- a/doc/links.rst +++ b/doc/links.rst @@ -1,20 +1,24 @@ - +.. _`Cookiecutter`: https://cookiecutter.readthedocs.io +.. _`pluggy`: https://pluggy.readthedocs.io +.. _`cookiecutter-tox-plugin`: https://github.com/tox-dev/cookiecutter-tox-plugin .. _devpi: http://doc.devpi.net .. _Python: http://www.python.org -.. _virtualenv: https://pypi.python.org/pypi/virtualenv -.. _virtualenv3: https://pypi.python.org/pypi/virtualenv3 -.. _virtualenv5: https://pypi.python.org/pypi/virtualenv5 -.. _`py.test`: http://pytest.org +.. _virtualenv: https://pypi.org/project/virtualenv +.. _`pytest`: https://pytest.org .. _nosetests: -.. _`nose`: https://pypi.python.org/pypi/nose +.. _`nose`: https://pypi.org/project/nose .. _`Holger Krekel`: https://twitter.com/hpk42 -.. _`pytest-xdist`: https://pypi.python.org/pypi/pytest-xdist +.. _`pytest-xdist`: https://pypi.org/project/pytest-xdist .. _`easy_install`: http://peak.telecommunity.com/DevCenter/EasyInstall -.. _pip: https://pypi.python.org/pypi/pip -.. _setuptools: https://pypi.python.org/pypi/setuptools +.. _pip: https://pypi.org/project/pip +.. _setuptools: https://pypi.org/project/setuptools .. _`jenkins`: https://jenkins.io/index.html -.. _sphinx: https://pypi.python.org/pypi/Sphinx -.. _discover: https://pypi.python.org/pypi/discover -.. _unittest2: https://pypi.python.org/pypi/unittest2 -.. _mock: https://pypi.python.org/pypi/mock/ +.. _sphinx: https://pypi.org/project/Sphinx +.. _discover: https://pypi.org/project/discover +.. _unittest2: https://pypi.org/project/unittest2 +.. _mock: https://pypi.org/project/mock/ +.. _pypy: http://pypy.org + +.. _`Python Packaging Guide`: https://packaging.python.org/tutorials/distributing-packages/#packaging-your-project +.. _`tox.ini`: :doc:configfile diff --git a/doc/plugins.rst b/doc/plugins.rst index 9c6d432db..013e7fdfe 100644 --- a/doc/plugins.rst +++ b/doc/plugins.rst @@ -5,77 +5,181 @@ tox plugins .. versionadded:: 2.0 -With tox-2.0 a few aspects of tox running can be experimentally modified -by writing hook functions. The list of of available hook function is -to grow over time on a per-need basis. +A growing number of hooks make tox modifiable in different phases of execution by writing plugins. +tox - like `pytest`_ and `devpi`_ - uses `pluggy`_ to provide an extension mechanism for pip-installable internal or devpi/PyPi-published plugins. -writing a setuptools entrypoints plugin ---------------------------------------- +Using plugins +------------- -If you have a ``tox_MYPLUGIN.py`` module you could use the following -rough ``setup.py`` to make it into a package which you can upload to the -Python packaging index: +To start using a plugin you need to install it in the same environment where the tox host +is installed. -.. code-block:: python +e.g.: - # content of setup.py - from setuptools import setup +.. code-block:: shell - if __name__ == "__main__": - setup( - name='tox-MYPLUGIN', - description='tox plugin decsription', - license="MIT license", - version='0.1', - py_modules=['tox_MYPLUGIN'], - entry_points={'tox': ['MYPLUGIN = tox_MYPLUGIN']}, - install_requires=['tox>=2.0'], - ) + $ pip install tox-travis + +You can search for available plugins on PyPi by typing ``pip search tox`` and filter for +packages that are prefixed `tox-` or contain the "plugin" in the description. +You will get some output similar to this:: + + tox-pipenv (1.4.1) - A pipenv plugin for tox + tox-pyenv (1.1.0) - tox plugin that makes tox use `pyenv which` to find + python executables + tox-globinterpreter (0.3) - tox plugin to allow specification of interpreter + locationspaths to use + tox-venv (0.2.0) - Use python3 venvs for python3 tox testenvs + tox-cmake (0.1.1) - Build CMake projects using Tox + tox-travis (0.10) - Seamless integration of Tox into Travis CI + tox-py-backwards (0.1) - tox plugin for py-backwards + tox-pytest-summary (0.1.2) - Tox + Py.test summary + tox-envreport (0.2.0) - A tox-plugin to document the setup of used virtual + environments. + tox-no-internet (0.1.0) - Workarounds for using tox with no internet connection + tox-virtualenv-no-download (1.0.2) - Disable virtualenv's download-by-default in tox + tox-run-command (0.4) - tox plugin to run arbitrary commands in a virtualenv + tox-pip-extensions (1.2.1) - Augment tox with different installation methods via + progressive enhancement. + tox-run-before (0.1) - Tox plugin to run shell commands before the test + environments are created. + tox-docker (1.0.0) - Launch a docker instance around test runs + tox-bitbucket-status (1.0) - Update bitbucket status for each env + tox-pipenv-install (1.0.3) - Install packages from Pipfile + + +There might also be some plugins not (yet) available from PyPi that could be installed directly +fom source hosters like Github or Bitbucket (or from a local clone). See the + +To see what is installed you can call ``tox --version`` to get the version of the host and names +and locations of all installed plugins:: + + 3.0.0 imported from /home/ob/.virtualenvs/tmp/lib/python3.6/site-packages/tox/__init__.py + registered plugins: + tox-travis-0.10 at /home/ob/.virtualenvs/tmp/lib/python3.6/site-packages/tox_travis/hooks.py + detox-0.12 at /home/ob/.virtualenvs/tmp/lib/python3.6/site-packages/detox/tox_proclimit.py + + +Creating a plugin +----------------- + +Start from a template + +You can create a new tox plugin with all the bells and whistles via a `Cookiecutter`_ template +(see `cookiecutter-tox-plugin`_ - this will create a complete pypi-releasable, documented +project with license, documentation and CI. -If installed, the ``entry_points`` part will make tox see and integrate -your plugin during startup. +.. code-block:: shell -You can install the plugin for development ("in-place") via: + $ pip install -U cookiecutter + $ cookiecutter gh:tox-dev/cookiecutter-tox-plugin -.. code-block:: shell - pip install -e . +Tutorial: a minimal tox plugin +------------------------------ -and later publish it via something like: +.. note:: -.. code-block:: shell + This is the minimal implementation to demonstrate what is absolutely necessary to have a + working plugin for internal use. To move from something like this to a publishable plugin + you could apply `cookiecutter -f cookiecutter-tox-plugin` and adapt the code to the + package based structure used in the cookiecutter. + +Let us consider you want to extend tox behaviour by displaying fireworks at the end of a +successful tox run (we won't go into the details of how to display fireworks though). - python setup.py sdist register upload +To create a working plugin you need at least a python project with a tox entry point and a python +module implementing one or more of the pluggy based hooks tox specifies (using the +``@tox.hookimpl`` decorator as marker). +minimal structure: -Writing hook implementations ----------------------------- +.. code-block:: shell -A plugin module defines one or more hook implementation functions -by decorating them with tox's ``hookimpl`` marker: + $ mkdir tox-fireworks + $ cd tox-fireworks + $ touch tox_fireworks.py + $ touch setup.py + +contents of ``tox_fireworks.py``: .. code-block:: python - from tox import hookimpl + import pluggy + + hookimpl = pluggy.HookimplMarker("tox") @hookimpl def tox_addoption(parser): - # add your own command line options - + """Add command line option to display fireworks on request.""" @hookimpl def tox_configure(config): - # post process tox configuration after cmdline/ini file have - # been parsed + """Post process config after parsing.""" + + @hookimpl + def tox_runenvreport(config): + """Display fireworks if all was fine and requested.""" + +.. note:: + + See :ref:`toxHookSpecsApi` for details -If you put this into a module and make it pypi-installable with the ``tox`` -entry point you'll get your code executed as part of a tox run. +contents of ``setup.py``: +.. code-block:: python + + from setuptools import setup + setup(name='tox-fireworks', py_modules=['tox_fireworks'], + entry_points={'tox': ['fireworks = tox_fireworks']} + classifiers=['Framework:: tox']) -tox hook specifications and related API ---------------------------------------- +Using the **tox-** prefix in ``tox-fireworks`` is an established convention to be able to +see from the project name that this is a plugin for tox. It also makes it easier to find with +e.g. ``pip search 'tox-'`` once it is released on PyPi. + +To make your new plugin discoverable by tox, you need to install it. During development you should +install it with ``-e`` or ``--editable``, so that changes to the code are immediately active: + +.. code-block:: shell + + $ pip install -e + + +Publish your plugin to PyPi +--------------------------- + +If you think the rest of the world could profit using your plugin you can publish it to PyPi. + +You need to add some more meta data to ``setup.py`` (see `cookiecutter-tox-plugin`_ for a complete +example or consult the `setup.py docs `_). + + +.. note:: + + Make sure your plugin project name is prefixed by `tox-` to be easy to find via e.g. + `pip search tox-` + +You can and publish it like: + +.. code-block:: shell + + $ cd + $ python setup.py sdist bdist_wheel upload + +.. note:: + + You could also use `twine `_ for secure uploads. + + For more information about packaging and deploying Python projects see the + `Python Packaging Guide`_. + +.. _toxHookSpecsApi: + +Hook specifications and related API +----------------------------------- .. automodule:: tox.hookspecs :members: @@ -94,3 +198,5 @@ tox hook specifications and related API .. autoclass:: tox.session.Session() :members: + +.. include:: links.rst diff --git a/setup.py b/setup.py index d0af0d771..4ae2ad0c6 100644 --- a/setup.py +++ b/setup.py @@ -20,8 +20,8 @@ def has_environment_marker_support(): try: v = pkg_resources.parse_version(setuptools.__version__) return v >= pkg_resources.parse_version('0.7.2') - except Exception as exc: - sys.stderr.write("Could not test setuptool's version: %s\n" % exc) + except Exception as e: + sys.stderr.write("Could not test setuptool's version: %s\n" % e) return False @@ -43,7 +43,7 @@ def main(): author='holger krekel', author_email='holger@merlinux.eu', packages=['tox'], - entry_points={'console_scripts': ['tox=tox.session:run_main', + entry_points={'console_scripts': ['tox=tox:cmdline', 'tox-quickstart=tox._quickstart:main']}, python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', setup_requires=['setuptools_scm'], diff --git a/tests/conftest.py b/tests/conftest.py index 8b36b3b62..c30387771 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1 +1,4 @@ +# FIXME this seems unnecessary +# TODO move fixtures here and only keep helper functions/classes in the plugin +# TODO _pytest_helpers might be a better name than _pytestplugin then? from tox._pytestplugin import * # noqa diff --git a/tests/test_config.py b/tests/test_config.py index 61d106bfe..6c1b67402 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -8,15 +8,9 @@ from pluggy import PluginManager import tox -import tox.config -from tox.config import CommandParser -from tox.config import DepOption -from tox.config import get_homedir -from tox.config import get_version_info -from tox.config import getcontextname -from tox.config import is_section_substitution -from tox.config import parseconfig -from tox.config import SectionReader +from tox.config import ( + CommandParser, DepOption, SectionReader, get_homedir, get_version_info, + getcontextname, is_section_substitution, parseconfig) from tox.venv import VirtualEnv @@ -65,7 +59,7 @@ def test_envdir_set_manually(self, tmpdir, newconfig): envconfig = config.envconfigs['devenv'] assert envconfig.envdir == tmpdir.join('devenv') - def test_envdir_set_manually_with_substitutions(self, tmpdir, newconfig): + def test_envdir_set_manually_with_substitutions(self, newconfig): config = newconfig([], """ [testenv:devenv] envdir = {toxworkdir}/foobar @@ -107,7 +101,7 @@ def test_force_dep_with_url(self, initproj): [testenv] deps= dep1==1.0 - https://pypi.python.org/xyz/pkg1.tar.gz + https://pypi.org/xyz/pkg1.tar.gz ''' }) config = parseconfig( @@ -116,7 +110,7 @@ def test_force_dep_with_url(self, initproj): 'dep1==1.5' ] assert [str(x) for x in config.envconfigs['python'].deps] == [ - 'dep1==1.5', 'https://pypi.python.org/xyz/pkg1.tar.gz' + 'dep1==1.5', 'https://pypi.org/xyz/pkg1.tar.gz' ] def test_process_deps(self, newconfig): @@ -174,8 +168,7 @@ def test_config_parse_platform_rex(self, newconfig, mocksession, monkeypatch): assert not venv.matching_platform() @pytest.mark.parametrize("plat", ["win", "lin", "osx"]) - def test_config_parse_platform_with_factors(self, newconfig, plat, monkeypatch): - monkeypatch.setattr(sys, "platform", "win32") + def test_config_parse_platform_with_factors(self, newconfig, plat): config = newconfig([], """ [tox] envlist = py27-{win, lin,osx } @@ -200,7 +193,7 @@ def test_defaults(self, tmpdir, newconfig): assert not envconfig.recreate assert not envconfig.pip_pre - def test_defaults_distshare(self, tmpdir, newconfig): + def test_defaults_distshare(self, newconfig): config = newconfig([], "") assert config.distshare == config.homedir.join(".tox", "distshare") @@ -385,7 +378,7 @@ def test_regression_issue595(self, newconfig): class TestIniParser: - def test_getstring_single(self, tmpdir, newconfig): + def test_getstring_single(self, newconfig): config = newconfig(""" [section] key=value @@ -397,7 +390,7 @@ def test_getstring_single(self, tmpdir, newconfig): x = reader.getstring("hello", "world") assert x == "world" - def test_missing_substitution(self, tmpdir, newconfig): + def test_missing_substitution(self, newconfig): config = newconfig(""" [mydefault] key2={xyz} @@ -407,7 +400,7 @@ def test_missing_substitution(self, tmpdir, newconfig): with pytest.raises(tox.exception.ConfigError): reader.getstring("key2") - def test_getstring_fallback_sections(self, tmpdir, newconfig): + def test_getstring_fallback_sections(self, newconfig): config = newconfig(""" [mydefault] key2=value2 @@ -422,7 +415,7 @@ def test_getstring_fallback_sections(self, tmpdir, newconfig): x = reader.getstring("key3", "world") assert x == "world" - def test_getstring_substitution(self, tmpdir, newconfig): + def test_getstring_substitution(self, newconfig): config = newconfig(""" [mydefault] key2={value2} @@ -438,7 +431,7 @@ def test_getstring_substitution(self, tmpdir, newconfig): x = reader.getstring("key3", "{value2}") assert x == "newvalue2" - def test_getlist(self, tmpdir, newconfig): + def test_getlist(self, newconfig): config = newconfig(""" [section] key2= @@ -450,7 +443,7 @@ def test_getlist(self, tmpdir, newconfig): x = reader.getlist("key2") assert x == ['item1', 'grr'] - def test_getdict(self, tmpdir, newconfig): + def test_getdict(self, newconfig): config = newconfig(""" [section] key2= @@ -518,7 +511,7 @@ def test_getstring_other_section_substitution(self, newconfig): x = reader.getstring("key") assert x == "true" - def test_argvlist(self, tmpdir, newconfig): + def test_argvlist(self, newconfig): config = newconfig(""" [section] key2= @@ -532,7 +525,7 @@ def test_argvlist(self, tmpdir, newconfig): assert x == [["cmd1", "with", "space", "grr"], ["cmd2", "grr"]] - def test_argvlist_windows_escaping(self, tmpdir, newconfig): + def test_argvlist_windows_escaping(self, newconfig): config = newconfig(""" [section] comm = pytest {posargs} @@ -542,7 +535,7 @@ def test_argvlist_windows_escaping(self, tmpdir, newconfig): argv = reader.getargv("comm") assert argv == ["pytest", "hello\\this"] - def test_argvlist_multiline(self, tmpdir, newconfig): + def test_argvlist_multiline(self, newconfig): config = newconfig(""" [section] key2= @@ -555,7 +548,7 @@ def test_argvlist_multiline(self, tmpdir, newconfig): x = reader.getargvlist("key2") assert x == [["cmd1", "with", "space", "grr"]] - def test_argvlist_quoting_in_command(self, tmpdir, newconfig): + def test_argvlist_quoting_in_command(self, newconfig): config = newconfig(""" [section] key1= @@ -566,7 +559,7 @@ def test_argvlist_quoting_in_command(self, tmpdir, newconfig): x = reader.getargvlist("key1") assert x == [["cmd1", "part one", "part two"]] - def test_argvlist_comment_after_command(self, tmpdir, newconfig): + def test_argvlist_comment_after_command(self, newconfig): config = newconfig(""" [section] key1= @@ -576,7 +569,7 @@ def test_argvlist_comment_after_command(self, tmpdir, newconfig): x = reader.getargvlist("key1") assert x == [["cmd1", "--flag"]] - def test_argvlist_command_contains_hash(self, tmpdir, newconfig): + def test_argvlist_command_contains_hash(self, newconfig): config = newconfig(""" [section] key1= @@ -586,7 +579,7 @@ def test_argvlist_command_contains_hash(self, tmpdir, newconfig): x = reader.getargvlist("key1") assert x == [["cmd1", "--re", "use the # symbol for an arg"]] - def test_argvlist_positional_substitution(self, tmpdir, newconfig): + def test_argvlist_positional_substitution(self, newconfig): config = newconfig(""" [section] key2= @@ -609,7 +602,7 @@ def test_argvlist_positional_substitution(self, tmpdir, newconfig): assert argvlist[0] == ["cmd1"] assert argvlist[1] == ["cmd2", "value2", "other"] - def test_argvlist_quoted_posargs(self, tmpdir, newconfig): + def test_argvlist_quoted_posargs(self, newconfig): config = newconfig(""" [section] key2= @@ -625,7 +618,7 @@ def test_argvlist_quoted_posargs(self, tmpdir, newconfig): ["cmd2", "-f", "foo bar"], ["cmd3", "-f", "foo", "bar"]] - def test_argvlist_posargs_with_quotes(self, tmpdir, newconfig): + def test_argvlist_posargs_with_quotes(self, newconfig): config = newconfig(""" [section] key2= @@ -637,7 +630,7 @@ def test_argvlist_posargs_with_quotes(self, tmpdir, newconfig): x = reader.getargvlist("key2") assert x == [["cmd1", "-f", "foo", "bar baz"]] - def test_positional_arguments_are_only_replaced_when_standing_alone(self, tmpdir, newconfig): + def test_positional_arguments_are_only_replaced_when_standing_alone(self, newconfig): config = newconfig(""" [section] key= @@ -701,7 +694,7 @@ def test_getpath(self, tmpdir, newconfig): x = reader.getpath("path1", tmpdir) assert x == tmpdir.join("mypath") - def test_getbool(self, tmpdir, newconfig): + def test_getbool(self, newconfig): config = newconfig(""" [section] key1=True @@ -715,12 +708,14 @@ def test_getbool(self, tmpdir, newconfig): assert reader.getbool("key1a") is True assert reader.getbool("key2") is False assert reader.getbool("key2a") is False - pytest.raises(KeyError, 'reader.getbool("key3")') - pytest.raises(tox.exception.ConfigError, 'reader.getbool("key5")') + with pytest.raises(KeyError): + reader.getbool("key3") + with pytest.raises(tox.exception.ConfigError): + reader.getbool("key5") class TestIniParserPrefix: - def test_basic_section_access(self, tmpdir, newconfig): + def test_basic_section_access(self, newconfig): config = newconfig(""" [p:section] key=value @@ -732,7 +727,7 @@ def test_basic_section_access(self, tmpdir, newconfig): x = reader.getstring("hello", "world") assert x == "world" - def test_fallback_sections(self, tmpdir, newconfig): + def test_fallback_sections(self, newconfig): config = newconfig(""" [p:mydefault] key2=value2 @@ -769,7 +764,7 @@ def test_other_section_substitution(self, newconfig): class TestConfigTestEnv: - def test_commentchars_issue33(self, tmpdir, newconfig): + def test_commentchars_issue33(self, newconfig): config = newconfig(""" [testenv] # hello deps = http://abc#123 @@ -780,7 +775,7 @@ def test_commentchars_issue33(self, tmpdir, newconfig): assert envconfig.deps[0].name == "http://abc#123" assert envconfig.commands[0] == ["python", "-c", "x ; y"] - def test_defaults(self, tmpdir, newconfig): + def test_defaults(self, newconfig): config = newconfig(""" [testenv] commands= @@ -803,7 +798,7 @@ def test_defaults(self, tmpdir, newconfig): assert int_hashseed > 0 assert envconfig.ignore_outcome is False - def test_sitepackages_switch(self, tmpdir, newconfig): + def test_sitepackages_switch(self, newconfig): config = newconfig(["--sitepackages"], "") envconfig = config.envconfigs['python'] assert envconfig.sitepackages is True @@ -815,7 +810,7 @@ def test_installpkg_tops_develop(self, newconfig): """) assert not config.envconfigs["python"].usedevelop - def test_specific_command_overrides(self, tmpdir, newconfig): + def test_specific_command_overrides(self, newconfig): config = newconfig(""" [testenv] commands=xyz @@ -826,7 +821,7 @@ def test_specific_command_overrides(self, tmpdir, newconfig): envconfig = config.envconfigs['py'] assert envconfig.commands == [["abc"]] - def test_whitelist_externals(self, tmpdir, newconfig): + def test_whitelist_externals(self, newconfig): config = newconfig(""" [testenv] whitelist_externals = xyz @@ -844,7 +839,7 @@ def test_whitelist_externals(self, tmpdir, newconfig): envconfig = config.envconfigs['x'] assert envconfig.whitelist_externals == ["xyz"] - def test_changedir(self, tmpdir, newconfig): + def test_changedir(self, newconfig): config = newconfig(""" [testenv] changedir=xyz @@ -854,7 +849,7 @@ def test_changedir(self, tmpdir, newconfig): assert envconfig.changedir.basename == "xyz" assert envconfig.changedir == config.toxinidir.join("xyz") - def test_ignore_errors(self, tmpdir, newconfig): + def test_ignore_errors(self, newconfig): config = newconfig(""" [testenv] ignore_errors=True @@ -863,7 +858,7 @@ def test_ignore_errors(self, tmpdir, newconfig): envconfig = config.envconfigs['python'] assert envconfig.ignore_errors is True - def test_envbindir(self, tmpdir, newconfig): + def test_envbindir(self, newconfig): config = newconfig(""" [testenv] basepython=python @@ -873,7 +868,7 @@ def test_envbindir(self, tmpdir, newconfig): assert envconfig.envpython == envconfig.envbindir.join("python") @pytest.mark.parametrize("bp", ["jython", "pypy", "pypy3"]) - def test_envbindir_jython(self, tmpdir, newconfig, bp): + def test_envbindir_jython(self, newconfig, bp): config = newconfig(""" [testenv] basepython=%s @@ -886,8 +881,8 @@ def test_envbindir_jython(self, tmpdir, newconfig, bp): assert envconfig.envpython == envconfig.envbindir.join(bp) @pytest.mark.parametrize("plat", ["win32", "linux2"]) - def test_passenv_as_multiline_list(self, tmpdir, newconfig, monkeypatch, plat): - monkeypatch.setattr(sys, "platform", plat) + def test_passenv_as_multiline_list(self, newconfig, monkeypatch, plat): + monkeypatch.setattr(tox.INFO, 'IS_WIN', plat == 'win32') monkeypatch.setenv("A123A", "a") monkeypatch.setenv("A123B", "b") monkeypatch.setenv("BX23", "0") @@ -922,8 +917,8 @@ def test_passenv_as_multiline_list(self, tmpdir, newconfig, monkeypatch, plat): assert "A123B" in envconfig.passenv @pytest.mark.parametrize("plat", ["win32", "linux2"]) - def test_passenv_as_space_separated_list(self, tmpdir, newconfig, monkeypatch, plat): - monkeypatch.setattr(sys, "platform", plat) + def test_passenv_as_space_separated_list(self, newconfig, monkeypatch, plat): + monkeypatch.setattr(tox.INFO, 'IS_WIN', plat == 'win32') monkeypatch.setenv("A123A", "a") monkeypatch.setenv("A123B", "b") monkeypatch.setenv("BX23", "0") @@ -950,7 +945,7 @@ def test_passenv_as_space_separated_list(self, tmpdir, newconfig, monkeypatch, p assert "A123A" in envconfig.passenv assert "A123B" in envconfig.passenv - def test_passenv_with_factor(self, tmpdir, newconfig, monkeypatch): + def test_passenv_with_factor(self, newconfig, monkeypatch): monkeypatch.setenv("A123A", "a") monkeypatch.setenv("A123B", "b") monkeypatch.setenv("A123C", "c") @@ -987,7 +982,7 @@ def test_passenv_with_factor(self, tmpdir, newconfig, monkeypatch): assert "CB21" not in config.envconfigs["x2"].passenv assert "BX23" not in config.envconfigs["x2"].passenv - def test_passenv_from_global_env(self, tmpdir, newconfig, monkeypatch): + def test_passenv_from_global_env(self, newconfig, monkeypatch): monkeypatch.setenv("A1", "a1") monkeypatch.setenv("A2", "a2") monkeypatch.setenv("TOX_TESTENV_PASSENV", "A1") @@ -999,7 +994,7 @@ def test_passenv_from_global_env(self, tmpdir, newconfig, monkeypatch): assert "A1" in env.passenv assert "A2" in env.passenv - def test_passenv_glob_from_global_env(self, tmpdir, newconfig, monkeypatch): + def test_passenv_glob_from_global_env(self, newconfig, monkeypatch): monkeypatch.setenv("A1", "a1") monkeypatch.setenv("A2", "a2") monkeypatch.setenv("TOX_TESTENV_PASSENV", "A*") @@ -1010,7 +1005,7 @@ def test_passenv_glob_from_global_env(self, tmpdir, newconfig, monkeypatch): assert "A1" in env.passenv assert "A2" in env.passenv - def test_changedir_override(self, tmpdir, newconfig): + def test_changedir_override(self, newconfig): config = newconfig(""" [testenv] changedir=xyz @@ -1033,10 +1028,8 @@ def test_install_command_setting(self, newconfig): 'some_install', '{packages}'] def test_install_command_must_contain_packages(self, newconfig): - pytest.raises(tox.exception.ConfigError, newconfig, """ - [testenv] - install_command=pip install - """) + with pytest.raises(tox.exception.ConfigError): + newconfig("[testenv]\ninstall_command=pip install") def test_install_command_substitutions(self, newconfig): config = newconfig(""" @@ -1067,7 +1060,7 @@ def test_pip_pre_cmdline_override(self, newconfig): envconfig = config.envconfigs['python'] assert envconfig.pip_pre - def test_simple(tmpdir, newconfig): + def test_simple(self, newconfig): config = newconfig(""" [testenv:py36] basepython=python3.6 @@ -1078,13 +1071,11 @@ def test_simple(tmpdir, newconfig): assert "py36" in config.envconfigs assert "py27" in config.envconfigs - def test_substitution_error(tmpdir, newconfig): - pytest.raises(tox.exception.ConfigError, newconfig, """ - [testenv:py27] - basepython={xyz} - """) + def test_substitution_error(self, newconfig): + with pytest.raises(tox.exception.ConfigError): + newconfig("[testenv:py27]\nbasepython={xyz}") - def test_substitution_defaults(tmpdir, newconfig): + def test_substitution_defaults(self, newconfig): config = newconfig(""" [testenv:py27] commands = @@ -1110,7 +1101,7 @@ def test_substitution_defaults(tmpdir, newconfig): assert argv[7][0] == config.homedir.join(".tox", "distshare") assert argv[8][0] == conf.envlogdir - def test_substitution_notfound_issue246(tmpdir, newconfig): + def test_substitution_notfound_issue246(self, newconfig): config = newconfig(""" [testenv:py27] setenv = @@ -1122,7 +1113,7 @@ def test_substitution_notfound_issue246(tmpdir, newconfig): assert 'FOO' in env assert 'BAR' in env - def test_substitution_notfound_issue515(tmpdir, newconfig): + def test_substitution_notfound_issue515(self, newconfig): config = newconfig(""" [tox] envlist = standard-greeting @@ -1142,7 +1133,7 @@ def test_substitution_notfound_issue515(tmpdir, newconfig): ['python', '-c', 'print("Hello, world!")'] ] - def test_substitution_nested_env_defaults(tmpdir, newconfig, monkeypatch): + def test_substitution_nested_env_defaults(self, newconfig, monkeypatch): monkeypatch.setenv("IGNORE_STATIC_DEFAULT", "env") monkeypatch.setenv("IGNORE_DYNAMIC_DEFAULT", "env") config = newconfig(""" @@ -1211,7 +1202,7 @@ def test_substitution_double(self, newconfig): argv = conf.commands assert argv[0] == ['echo', 'bah'] - def test_posargs_backslashed_or_quoted(self, tmpdir, newconfig): + def test_posargs_backslashed_or_quoted(self, newconfig): inisource = r""" [testenv:py27] commands = @@ -1383,7 +1374,8 @@ def test_recursive_substitution_cycle_fails(self, newconfig): deps= {[testing:pytest]deps} """ - pytest.raises(ValueError, newconfig, [], inisource) + with pytest.raises(ValueError): + newconfig([], inisource) def test_single_value_from_other_secton(self, newconfig, tmpdir): inisource = """ @@ -1597,8 +1589,7 @@ def test_quiet(self, args, expected, newconfig): config = newconfig(args, "") assert config.option.quiet_level == expected - def test_substitution_jenkins_default(self, tmpdir, - monkeypatch, newconfig): + def test_substitution_jenkins_default(self, monkeypatch, newconfig): monkeypatch.setenv("HUDSON_URL", "xyz") config = newconfig(""" [testenv:py27] @@ -1625,7 +1616,7 @@ def test_substitution_jenkins_context(self, tmpdir, monkeypatch, newconfig): assert argv[0][0] == config.distshare assert config.distshare == tmpdir.join("hello") - def test_sdist_specification(self, tmpdir, newconfig): + def test_sdist_specification(self, newconfig): config = newconfig(""" [tox] sdistsrc = {distshare}/xyz.zip @@ -1634,7 +1625,7 @@ def test_sdist_specification(self, tmpdir, newconfig): config = newconfig([], "") assert not config.sdistsrc - def test_env_selection(self, tmpdir, newconfig, monkeypatch): + def test_env_selection(self, newconfig, monkeypatch): inisource = """ [tox] envlist = py36 @@ -1660,19 +1651,14 @@ def test_env_selection(self, tmpdir, newconfig, monkeypatch): config = newconfig(['-espam'], inisource) assert config.envlist == ["spam"] - def test_py_venv(self, tmpdir, newconfig, monkeypatch): + def test_py_venv(self, newconfig): config = newconfig(["-epy"], "") env = config.envconfigs['py'] assert str(env.basepython) == sys.executable - def test_default_environments(self, tmpdir, newconfig, monkeypatch): - envs = "py27,py34,py35,py36,py37,jython,pypy,pypy3,py2,py3" - inisource = """ - [tox] - envlist = %s - """ % envs - config = newconfig([], inisource) - envlist = envs.split(",") + def test_default_factors(self, newconfig): + envlist = list(tox.PYTHON.DEFAULT_FACTORS.keys()) + config = newconfig([], "[tox]\nenvlist=%s" % ", ".join(envlist)) assert config.envlist == envlist for name in config.envlist: env = config.envconfigs[name] @@ -1682,10 +1668,11 @@ def test_default_environments(self, tmpdir, newconfig, monkeypatch): assert env.basepython == name elif name in ("py2", "py3"): assert env.basepython == 'python' + name[-1] + elif name == 'py': + assert 'python' in env.basepython else: assert name.startswith("py") - bp = "python%s.%s" % (name[2], name[3]) - assert env.basepython == bp + assert env.basepython == "python%s.%s" % (name[2], name[3]) def test_envlist_expansion(self, newconfig): inisource = """ @@ -1714,7 +1701,7 @@ def test_envlist_multiline(self, newconfig): config = newconfig([], inisource) assert config.envlist == ["py27", "py34"] - def test_minversion(self, tmpdir, newconfig, monkeypatch): + def test_minversion(self, newconfig): inisource = """ [tox] minversion = 10.0 @@ -1722,7 +1709,7 @@ def test_minversion(self, tmpdir, newconfig, monkeypatch): with pytest.raises(tox.exception.MinVersionError): newconfig([], inisource) - def test_skip_missing_interpreters_true(self, tmpdir, newconfig, monkeypatch): + def test_skip_missing_interpreters_true(self, newconfig): inisource = """ [tox] skip_missing_interpreters = True @@ -1730,7 +1717,7 @@ def test_skip_missing_interpreters_true(self, tmpdir, newconfig, monkeypatch): config = newconfig([], inisource) assert config.option.skip_missing_interpreters - def test_skip_missing_interpreters_false(self, tmpdir, newconfig, monkeypatch): + def test_skip_missing_interpreters_false(self, newconfig): inisource = """ [tox] skip_missing_interpreters = False @@ -1738,13 +1725,13 @@ def test_skip_missing_interpreters_false(self, tmpdir, newconfig, monkeypatch): config = newconfig([], inisource) assert not config.option.skip_missing_interpreters - def test_defaultenv_commandline(self, tmpdir, newconfig, monkeypatch): + def test_defaultenv_commandline(self, newconfig): config = newconfig(["-epy27"], "") env = config.envconfigs['py27'] assert env.basepython == "python2.7" assert not env.commands - def test_defaultenv_partial_override(self, tmpdir, newconfig, monkeypatch): + def test_defaultenv_partial_override(self, newconfig): inisource = """ [tox] envlist = py27 @@ -1758,7 +1745,6 @@ def test_defaultenv_partial_override(self, tmpdir, newconfig, monkeypatch): class TestHashseedOption: - def _get_envconfigs(self, newconfig, args=None, tox_ini=None, make_hashseed=None): if args is None: @@ -1791,22 +1777,22 @@ def _check_testenv(self, newconfig, expected, args=None, tox_ini=None): envconfig = self._get_envconfig(newconfig, args=args, tox_ini=tox_ini) self._check_hashseed(envconfig, expected) - def test_default(self, tmpdir, newconfig): + def test_default(self, newconfig): self._check_testenv(newconfig, '123456789') - def test_passing_integer(self, tmpdir, newconfig): + def test_passing_integer(self, newconfig): args = ['--hashseed', '1'] self._check_testenv(newconfig, '1', args=args) - def test_passing_string(self, tmpdir, newconfig): + def test_passing_string(self, newconfig): args = ['--hashseed', 'random'] self._check_testenv(newconfig, 'random', args=args) - def test_passing_empty_string(self, tmpdir, newconfig): + def test_passing_empty_string(self, newconfig): args = ['--hashseed', ''] self._check_testenv(newconfig, '', args=args) - def test_passing_no_argument(self, tmpdir, newconfig): + def test_passing_no_argument(self, newconfig): """Test that passing no arguments to --hashseed is not allowed.""" args = ['--hashseed'] try: @@ -1817,7 +1803,7 @@ def test_passing_no_argument(self, tmpdir, newconfig): return assert False # getting here means we failed the test. - def test_setenv(self, tmpdir, newconfig): + def test_setenv(self, newconfig): """Check that setenv takes precedence.""" tox_ini = """ [testenv] @@ -1828,12 +1814,12 @@ def test_setenv(self, tmpdir, newconfig): args = ['--hashseed', '1'] self._check_testenv(newconfig, '2', args=args, tox_ini=tox_ini) - def test_noset(self, tmpdir, newconfig): + def test_noset(self, newconfig): args = ['--hashseed', 'noset'] envconfig = self._get_envconfig(newconfig, args=args) assert not envconfig.setenv.definitions - def test_noset_with_setenv(self, tmpdir, newconfig): + def test_noset_with_setenv(self, newconfig): tox_ini = """ [testenv] setenv = @@ -1842,7 +1828,7 @@ def test_noset_with_setenv(self, tmpdir, newconfig): args = ['--hashseed', 'noset'] self._check_testenv(newconfig, '2', args=args, tox_ini=tox_ini) - def test_one_random_hashseed(self, tmpdir, newconfig): + def test_one_random_hashseed(self, newconfig): """Check that different testenvs use the same random seed.""" tox_ini = """ [testenv:hash1] @@ -1864,7 +1850,7 @@ def make_hashseed(): # Check that hash2's value is not '1003', for example. self._check_hashseed(envconfigs["hash2"], '1002') - def test_setenv_in_one_testenv(self, tmpdir, newconfig): + def test_setenv_in_one_testenv(self, newconfig): """Check using setenv in one of multiple testenvs.""" tox_ini = """ [testenv:hash1] @@ -1878,7 +1864,7 @@ def test_setenv_in_one_testenv(self, tmpdir, newconfig): class TestSetenv: - def test_getdict_lazy(self, tmpdir, newconfig, monkeypatch): + def test_getdict_lazy(self, newconfig, monkeypatch): monkeypatch.setenv("X", "2") config = newconfig(""" [testenv:X] @@ -1891,7 +1877,7 @@ def test_getdict_lazy(self, tmpdir, newconfig, monkeypatch): assert val["key1"] == "2" assert val["key2"] == "1" - def test_getdict_lazy_update(self, tmpdir, newconfig, monkeypatch): + def test_getdict_lazy_update(self, newconfig, monkeypatch): monkeypatch.setenv("X", "2") config = newconfig(""" [testenv:X] @@ -1905,7 +1891,7 @@ def test_getdict_lazy_update(self, tmpdir, newconfig, monkeypatch): d.update(val) assert d == {"key1": "2", "key2": "1"} - def test_setenv_uses_os_environ(self, tmpdir, newconfig, monkeypatch): + def test_setenv_uses_os_environ(self, newconfig, monkeypatch): monkeypatch.setenv("X", "1") config = newconfig(""" [testenv:env1] @@ -1914,7 +1900,7 @@ def test_setenv_uses_os_environ(self, tmpdir, newconfig, monkeypatch): """) assert config.envconfigs["env1"].setenv["X"] == "1" - def test_setenv_default_os_environ(self, tmpdir, newconfig, monkeypatch): + def test_setenv_default_os_environ(self, newconfig, monkeypatch): monkeypatch.delenv("X", raising=False) config = newconfig(""" [testenv:env1] @@ -1923,7 +1909,7 @@ def test_setenv_default_os_environ(self, tmpdir, newconfig, monkeypatch): """) assert config.envconfigs["env1"].setenv["X"] == "2" - def test_setenv_uses_other_setenv(self, tmpdir, newconfig): + def test_setenv_uses_other_setenv(self, newconfig): config = newconfig(""" [testenv:env1] setenv = @@ -1932,7 +1918,7 @@ def test_setenv_uses_other_setenv(self, tmpdir, newconfig): """) assert config.envconfigs["env1"].setenv["X"] == "5" - def test_setenv_recursive_direct(self, tmpdir, newconfig): + def test_setenv_recursive_direct(self, newconfig): config = newconfig(""" [testenv:env1] setenv = @@ -1940,7 +1926,7 @@ def test_setenv_recursive_direct(self, tmpdir, newconfig): """) assert config.envconfigs["env1"].setenv["X"] == "3" - def test_setenv_overrides(self, tmpdir, newconfig): + def test_setenv_overrides(self, newconfig): config = newconfig(""" [testenv] setenv = @@ -1954,7 +1940,7 @@ def test_setenv_overrides(self, tmpdir, newconfig): assert envconfig.setenv['PYTHONPATH'] == 'something' assert envconfig.setenv['ANOTHER_VAL'] == 'else' - def test_setenv_with_envdir_and_basepython(self, tmpdir, newconfig): + def test_setenv_with_envdir_and_basepython(self, newconfig): config = newconfig(""" [testenv] setenv = @@ -1967,7 +1953,7 @@ def test_setenv_with_envdir_and_basepython(self, tmpdir, newconfig): assert envconfig.setenv['VAL'] == envconfig.envdir assert envconfig.basepython == envconfig.envdir - def test_setenv_ordering_1(self, tmpdir, newconfig): + def test_setenv_ordering_1(self, newconfig): config = newconfig(""" [testenv] setenv= @@ -2026,7 +2012,7 @@ def test_setenv_cross_section_mixed(self, monkeypatch, newconfig): class TestIndexServer: - def test_indexserver(self, tmpdir, newconfig): + def test_indexserver(self, newconfig): config = newconfig(""" [tox] indexserver = @@ -2066,7 +2052,7 @@ def test_multiple_homedir_relative_local_indexservers(self, newconfig): default = file://{homedir}/.pip/downloads/simple local1 = file://{homedir}/.pip/downloads/simple local2 = file://{toxinidir}/downloads/simple - pypi = http://pypi.python.org/simple + pypi = http://pypi.org/simple """ config = newconfig([], inisource) expected = "file://%s/.pip/downloads/simple" % config.homedir @@ -2097,7 +2083,6 @@ def test_pathsep_regex(self): class TestParseEnv: - def test_parse_recreate(self, newconfig): inisource = "" config = newconfig([], inisource) @@ -2124,8 +2109,7 @@ def test_help(self, cmd): def test_version_simple(self, cmd): result = cmd("--version") assert not result.ret - from tox import __version__ - assert "{} imported from".format(__version__) in result.out + assert "{} imported from".format(tox.__version__) in result.out def test_version_no_plugins(self): pm = PluginManager('fakeprject') @@ -2297,7 +2281,7 @@ def test_no_tox_ini(self, cmd, initproj): assert result.out == '' assert result.err == "ERROR: toxini file 'tox.ini' not found\n" - def test_override_workdir(self, tmpdir, cmd, initproj): + def test_override_workdir(self, cmd, initproj): baddir = "badworkdir-123" gooddir = "overridden-234" initproj("overrideworkdir-0.5", filedefs={ @@ -2333,9 +2317,7 @@ def test_showconfig_with_force_dep_version(self, cmd, initproj): assert result.ret == 0 assert any(re.match(r'.*deps.*dep1, dep2==5.0.*', l) for l in result.outlines) - @pytest.mark.xfail( - "'pypy' not in sys.executable", - reason='Upstream bug. See #203') + @pytest.mark.xfail("'pypy' not in sys.executable", reason='Upstream bug. See #203') def test_colon_symbol_in_directory_name(self, cmd, initproj): initproj('colon:_symbol_in_dir_name', filedefs={ 'tox.ini': ''' @@ -2363,7 +2345,6 @@ def test_env_spec(cmdline, envlist): class TestCommandParser: - def test_command_parser_for_word(self): p = CommandParser('word') assert list(p.words()) == ['word'] @@ -2417,7 +2398,7 @@ def test_command_parsing_for_issue_10(self): '--with-doctest', ' ', '[]' ] - @pytest.mark.skipif("sys.platform != 'win32'") + # @mark_dont_run_on_windows def test_commands_with_backslash(self, newconfig): config = newconfig([r"hello\world"], """ [testenv:py36] diff --git a/tests/test_interpreters.py b/tests/test_interpreters.py index 8d3cdcaf4..83dfab5fd 100644 --- a/tests/test_interpreters.py +++ b/tests/test_interpreters.py @@ -6,24 +6,21 @@ import py import pytest +import tox +from tox._pytestplugin import mark_dont_run_on_posix from tox.config import get_plugin_manager -from tox.interpreters import ExecFailed -from tox.interpreters import InterpreterInfo -from tox.interpreters import Interpreters -from tox.interpreters import NoInterpreterInfo -from tox.interpreters import pyinfo -from tox.interpreters import run_and_get_interpreter_info -from tox.interpreters import sitepackagesdir -from tox.interpreters import tox_get_python_executable - - -@pytest.fixture -def interpreters(): +from tox.interpreters import ( + ExecFailed, InterpreterInfo, Interpreters, NoInterpreterInfo, pyinfo, + run_and_get_interpreter_info, sitepackagesdir, tox_get_python_executable) + + +@pytest.fixture(name="interpreters") +def create_interpreters_instance(): pm = get_plugin_manager() return Interpreters(hook=pm.hook) -@pytest.mark.skipif("sys.platform != 'win32'") +@mark_dont_run_on_posix def test_locate_via_py(monkeypatch): from tox.interpreters import locate_via_py @@ -50,7 +47,6 @@ def communicate(): return proc - # Monkeypatch modules to return our faked value monkeypatch.setattr(distutils.spawn, 'find_executable', fake_find_exe) monkeypatch.setattr(subprocess, 'Popen', fake_popen) assert locate_via_py('3', '2') == sys.executable @@ -63,10 +59,10 @@ class envconfig: p = tox_get_python_executable(envconfig) assert p == py.path.local(sys.executable) - for ver in "2.7 3.4 3.5 3.6".split(): - name = "python%s" % ver - if sys.platform == "win32": - pydir = "python%s" % ver.replace(".", "") + for major, minor in tox.PYTHON.CPYTHON_VERSION_TUPLES: + name = "python%s.%s" % (major, minor) + if tox.INFO.IS_WIN: + pydir = "python%s%s" % (major, minor) x = py.path.local(r"c:\%s" % pydir) print(x) if not x.check(): @@ -81,12 +77,13 @@ class envconfig: stdout=subprocess.PIPE) stdout, stderr = popen.communicate() assert not stdout or not stderr - assert ver in stderr.decode('ascii') or ver in stdout.decode('ascii') + all_output = stderr.decode('ascii') + stdout.decode('ascii') + assert "%s.%s" % (major, minor) in all_output def test_find_executable_extra(monkeypatch): @staticmethod - def sysfind(x): + def sysfind(_): return "hello" monkeypatch.setattr(py.path.local, "sysfind", sysfind) @@ -108,7 +105,6 @@ def test_run_and_get_interpreter_info(): class TestInterpreters: - def test_get_executable(self, interpreters): class envconfig: basepython = sys.executable @@ -175,8 +171,8 @@ def test_exec_failed(): class TestInterpreterInfo: - - def info(self, name="my-name", executable="my-executable", + @staticmethod + def info(name="my-name", executable="my-executable", version_info="my-version-info", sysplatform="my-sys-platform"): return InterpreterInfo(name, executable, version_info, sysplatform) @@ -201,7 +197,6 @@ def test_str(self): class TestNoInterpreterInfo: - def test_runnable(self): assert not NoInterpreterInfo("foo").runnable assert not NoInterpreterInfo("foo", executable=sys.executable).runnable @@ -215,8 +210,8 @@ def test_default_data(self): assert x.err == "not found" def test_set_data(self): - x = NoInterpreterInfo("migraine", executable="my-executable", - out="my-out", err="my-err") + x = NoInterpreterInfo( + "migraine", executable="my-executable", out="my-out", err="my-err") assert x.name == "migraine" assert x.executable == "my-executable" assert x.version_info is None diff --git a/tests/test_pytest_plugins.py b/tests/test_pytest_plugins.py index 3eb1a12f1..126aa2d01 100644 --- a/tests/test_pytest_plugins.py +++ b/tests/test_pytest_plugins.py @@ -6,8 +6,7 @@ import py.path import pytest -from tox._pytestplugin import _filedefs_contains -from tox._pytestplugin import _path_parts +from tox._pytestplugin import _filedefs_contains, _path_parts class TestInitProj: diff --git a/tests/test_quickstart.py b/tests/test_quickstart.py index 8ee9d484a..666ae18fc 100644 --- a/tests/test_quickstart.py +++ b/tests/test_quickstart.py @@ -1,580 +1,234 @@ +import os + import pytest -import tox._quickstart - - -@pytest.fixture(autouse=True) -def cleandir(tmpdir): - tmpdir.chdir() - - -class TestToxQuickstartMain(object): - - def mock_term_input_return_values(self, return_values): - for return_val in return_values: - yield return_val - - def get_mock_term_input(self, return_values): - generator = self.mock_term_input_return_values(return_values) - - def mock_term_input(prompt): - try: - return next(generator) - except NameError: - return generator.next() # noqa - - return mock_term_input - - def test_quickstart_main_choose_individual_pythons_and_pytest( - self, - monkeypatch): - monkeypatch.setattr( - tox._quickstart, 'term_input', - self.get_mock_term_input( - [ - '4', # Python versions: choose one by one - 'Y', # py27 - 'Y', # py34 - 'Y', # py35 - 'Y', # py36 - 'Y', # pypy - 'N', # jython - 'py.test', # command to run tests - 'pytest' # test dependencies - ] - ) - ) - - tox._quickstart.main(argv=['tox-quickstart']) - - expected_tox_ini = """ -# tox (https://tox.readthedocs.io/) 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 = py27, py34, py35, py36, pypy - -[testenv] -commands = py.test -deps = - pytest -""".lstrip() - result = read_tox() - assert (result == expected_tox_ini) - - 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', # Python versions: choose one by one - 'Y', # py27 - 'Y', # py34 - 'Y', # py35 - 'Y', # py36 - 'Y', # pypy - 'N', # jython - 'nosetests', # command to run tests - '' # test dependencies - ] - ) - ) - - tox._quickstart.main(argv=['tox-quickstart']) - - expected_tox_ini = """ -# tox (https://tox.readthedocs.io/) 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 = py27, py34, py35, py36, pypy - -[testenv] -commands = nosetests -deps = - nose -""".lstrip() - result = read_tox() - assert (result == expected_tox_ini) - - 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', # Python versions: choose one by one - 'Y', # py27 - 'Y', # py34 - 'Y', # py35 - 'Y', # py36 - 'Y', # pypy - 'N', # jython - 'trial', # command to run tests - '' # test dependencies - ] - ) - ) - - tox._quickstart.main(argv=['tox-quickstart']) - - expected_tox_ini = """ -# tox (https://tox.readthedocs.io/) 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 = py27, py34, py35, py36, pypy - -[testenv] -commands = trial -deps = - twisted -""".lstrip() - result = read_tox() - assert (result == expected_tox_ini) - - 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', # Python versions: choose one by one - 'Y', # py27 - 'Y', # py34 - 'Y', # py35 - 'Y', # py36 - 'Y', # pypy - 'N', # jython - 'py.test', # command to run tests - '' # test dependencies - ] - ) - ) - tox._quickstart.main(argv=['tox-quickstart']) - - expected_tox_ini = """ -# tox (https://tox.readthedocs.io/) 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 = py27, py34, py35, py36, pypy - -[testenv] -commands = py.test -deps = - pytest -""".lstrip() - result = read_tox() - assert (result == expected_tox_ini) - - def test_quickstart_main_choose_py27_and_pytest_adds_deps( - self, - monkeypatch): - monkeypatch.setattr( - tox._quickstart, 'term_input', - self.get_mock_term_input( - [ - '1', # py27 - 'py.test', # command to run tests - '' # test dependencies - ] - ) - ) - - tox._quickstart.main(argv=['tox-quickstart']) - - expected_tox_ini = """ -# tox (https://tox.readthedocs.io/) 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 = py27 - -[testenv] -commands = py.test -deps = - pytest -""".lstrip() - result = read_tox() - assert (result == expected_tox_ini) - - def test_quickstart_main_choose_py27_and_py34_and_pytest_adds_deps( - self, - monkeypatch): - monkeypatch.setattr( - tox._quickstart, 'term_input', - self.get_mock_term_input( - [ - '2', # py27 and py36 - 'py.test', # command to run tests - '' # test dependencies - ] - ) - ) - - tox._quickstart.main(argv=['tox-quickstart']) - - expected_tox_ini = """ -# tox (https://tox.readthedocs.io/) 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 = py27, py36 - -[testenv] -commands = py.test -deps = - pytest -""".lstrip() - result = read_tox() - assert (result == expected_tox_ini) - - 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', # all Python versions - 'py.test', # command to run tests - '' # test dependencies - ] - ) - ) - - tox._quickstart.main(argv=['tox-quickstart']) - - expected_tox_ini = """ -# tox (https://tox.readthedocs.io/) 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 = py27, py34, py35, py36, pypy, jython - -[testenv] -commands = py.test -deps = - pytest -""".lstrip() - result = read_tox() - assert (result == expected_tox_ini) - - def test_quickstart_main_choose_individual_pythons_and_defaults( - self, - monkeypatch): - monkeypatch.setattr( - tox._quickstart, 'term_input', - self.get_mock_term_input( - [ - '4', # Python versions: choose one by one - '', # py27 - '', # py34 - '', # py35 - '', # py36 - '', # pypy - '', # jython - '', # command to run tests - '' # test dependencies - ] - ) - ) - - tox._quickstart.main(argv=['tox-quickstart']) - - expected_tox_ini = """ -# tox (https://tox.readthedocs.io/) 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 = py27, py34, py35, py36, pypy, jython - -[testenv] -commands = {envpython} setup.py test -deps = - -""".lstrip() - result = read_tox() - assert (result == expected_tox_ini) - - def test_quickstart_main_existing_tox_ini(self, monkeypatch): +import tox +from tox._quickstart import ( + ALTERNATIVE_CONFIG_NAME, list_modificator, main, post_process_input, prepare_content, + QUICKSTART_CONF) + +ALL_PY_ENVS_AS_STRING = ', '.join(tox.PYTHON.QUICKSTART_PY_ENVS) +ALL_PY_ENVS_WO_LAST_AS_STRING = ', '.join(tox.PYTHON.QUICKSTART_PY_ENVS[:-1]) +SIGNS_OF_SANITY = ( + 'tox.readthedocs.io', '[tox]', '[testenv]', 'envlist = ', 'deps =', 'commands =') +"""A bunch of elements to be expected in the generated config as marker for basic sanity""" + + +class _answers: + """Simulate a series of terminal inputs by popping them from a list if called.""" + + def __init__(self, inputs): + self._inputs = [str(i) for i in inputs] + + def extend(self, items): + self._inputs.extend(items) + + def __str__(self): + return "|".join(self._inputs) + + def __call__(self, prompt): + print("prompt: '%s'" % prompt) try: - f = open('tox.ini', 'w') - f.write('foo bar\n') - finally: - f.close() - - monkeypatch.setattr( - tox._quickstart, 'term_input', - self.get_mock_term_input( - [ - '4', # Python versions: choose one by one - '', # py27 - '', # py34 - '', # py35 - '', # py36 - '', # pypy - '', # jython - '', # command to run tests - '', # test dependencies - '', # tox.ini already exists; overwrite? - ] - ) - ) - - tox._quickstart.main(argv=['tox-quickstart']) - - expected_tox_ini = """ -# tox (https://tox.readthedocs.io/) 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 = py27, py34, py35, py36, pypy, jython - -[testenv] -commands = {envpython} setup.py test -deps = - -""".lstrip() - result = read_tox('tox-generated.ini') - assert (result == expected_tox_ini) - - def test_quickstart_main_tox_ini_location_can_be_overridden( - self, - tmpdir, - monkeypatch): - monkeypatch.setattr( - tox._quickstart, 'term_input', - self.get_mock_term_input( - [ - '1', # py27 and py34 - 'py.test', # command to run tests - '', # test dependencies - ] - ) - ) - - root_dir = tmpdir.mkdir('alt-root') - tox_ini_path = root_dir.join('tox.ini') - - tox._quickstart.main(argv=['tox-quickstart', root_dir.basename]) - - assert tox_ini_path.isfile() - - expected_tox_ini = """ -# tox (https://tox.readthedocs.io/) 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 = py27 - -[testenv] -commands = py.test -deps = - pytest -""".lstrip() - result = read_tox(fname=tox_ini_path.strpath) - assert (result == expected_tox_ini) - - def test_quickstart_main_custom_tox_ini_location_with_existing_tox_ini( - self, - tmpdir, - monkeypatch): - monkeypatch.setattr( - tox._quickstart, 'term_input', - self.get_mock_term_input( - [ - '1', # py27 and py34 - 'py.test', # command to run tests - '', # test dependencies - '', # tox.ini already exists; overwrite? - ] - ) - ) - - root_dir = tmpdir.mkdir('alt-root') - tox_ini_path = root_dir.join('tox.ini') - tox_ini_path.write('foo\nbar\n') - - tox._quickstart.main(argv=['tox-quickstart', root_dir.basename]) - tox_ini_path = root_dir.join('tox-generated.ini') - - assert tox_ini_path.isfile() - - expected_tox_ini = """ -# tox (https://tox.readthedocs.io/) 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 = py27 - -[testenv] -commands = py.test -deps = - pytest -""".lstrip() - result = read_tox(fname=tox_ini_path.strpath) - assert (result == expected_tox_ini) - - def test_quickstart_main_custom_nonexistent_tox_ini_location( - self, - tmpdir, - monkeypatch): - monkeypatch.setattr( - tox._quickstart, 'term_input', - self.get_mock_term_input( - [ - '1', # py27 and py34 - 'py.test', # command to run tests - '', # test dependencies - ] - ) - ) - - root_dir = tmpdir.join('nonexistent-root') - - assert tox._quickstart.main(argv=['tox-quickstart', root_dir.basename]) == 2 - - -class TestToxQuickstart(object): - def test_pytest(self): - d = { - 'py27': True, - 'py34': True, - 'pypy': True, - 'commands': 'py.test', - 'deps': 'pytest', - } - expected_tox_ini = """ -# tox (https://tox.readthedocs.io/) 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 = py27, py34, pypy - -[testenv] -commands = py.test -deps = - pytest -""".lstrip() - d = tox._quickstart.process_input(d) - tox._quickstart.generate(d) - result = read_tox() - # print(result) - assert (result == expected_tox_ini) - - def test_setup_py_test(self): - d = { - 'py36': True, - 'py27': True, - 'commands': 'python setup.py test', - 'deps': '', - } - expected_tox_ini = """ -# tox (https://tox.readthedocs.io/) 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 = py27, py36 - -[testenv] -commands = python setup.py test -deps = - -""".lstrip() - d = tox._quickstart.process_input(d) - tox._quickstart.generate(d) - result = read_tox() - # print(result) - assert (result == expected_tox_ini) - - def test_trial(self): - d = { - 'py27': True, - 'commands': 'trial', - 'deps': 'Twisted', - } - expected_tox_ini = """ -# tox (https://tox.readthedocs.io/) 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 = py27 - -[testenv] -commands = trial -deps = - Twisted -""".lstrip() - d = tox._quickstart.process_input(d) - tox._quickstart.generate(d) - result = read_tox() - # print(result) - assert (result == expected_tox_ini) - - def test_nosetests(self): - d = { - 'py27': True, - 'py34': True, - 'py35': True, - 'pypy': True, - 'commands': 'nosetests -v', - 'deps': 'nose', - } - expected_tox_ini = """ -# tox (https://tox.readthedocs.io/) 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 = py27, py34, py35, pypy - -[testenv] -commands = nosetests -v -deps = - nose -""".lstrip() - d = tox._quickstart.process_input(d) - tox._quickstart.generate(d) - result = read_tox() - # print(result) - assert (result == expected_tox_ini) - - -def read_tox(fname='tox.ini'): - with open(fname) as f: - return f.read() + answer = self._inputs.pop(0) + print("user answer: '%s'" % answer) + return answer + except IndexError: + pytest.fail("missing user answer for '%s'" % prompt) + + +class _cnf: + """Handle files and args for different test scenarios.""" + SOME_CONTENT = 'dontcare' + + def __init__(self, exists=False, names=None, pass_path=False): + self.original_name = tox.INFO.DEFAULT_CONFIG_NAME + self.names = names or [ALTERNATIVE_CONFIG_NAME] + self.exists = exists + self.pass_path = pass_path + + def __str__(self): + return self.original_name if not self.exists else str(self.names) + + @property + def argv(self): + argv = ['tox-quickstart'] + if self.pass_path: + argv.append(os.getcwd()) + return argv + + @property + def dpath(self): + return os.getcwd() if self.pass_path else '' + + def create(self): + paths_to_create = {self._original_path} + for name in self.names[:-1]: + paths_to_create.add(os.path.join(self.dpath, name)) + for path in paths_to_create: + with open(path, 'w') as f: + f.write(self.SOME_CONTENT) + + @property + def generated_content(self): + return self._alternative_content if self.exists else self._original_content + + @property + def already_existing_content(self): + if not self.exists: + if os.path.exists(self._alternative_path): + pytest.fail("alternative path should never exist here") + pytest.fail("checking for already existing content makes not sense here") + return self._original_content + + @property + def path_to_generated(self): + return os.path.join(os.getcwd(), self.names[-1] if self.exists else self.original_name) + + @property + def _original_path(self): + return os.path.join(self.dpath, self.original_name) + + @property + def _alternative_path(self): + return os.path.join(self.dpath, self.names[-1]) + + @property + def _original_content(self): + with open(self._original_path) as f: + return f.read() + + @property + def _alternative_content(self): + with open(self._alternative_path) as f: + return f.read() + + +class _exp: + """Holds test expectations and a user scenario description.""" + STANDARD_EPECTATIONS = [ALL_PY_ENVS_AS_STRING, 'pytest', 'pytest'] + + def __init__(self, name, exp=None): + self.name = name + exp = exp or self.STANDARD_EPECTATIONS + # NOTE extra mangling here ensures formatting is the same in file and exp + map_ = {'deps': list_modificator(exp[1]), 'commands': list_modificator(exp[2])} + post_process_input(map_) + map_['envlist'] = exp[0] + self.content = prepare_content(QUICKSTART_CONF.format(**map_)) + + def __str__(self): + return self.name + + +@pytest.mark.usefixtures('work_in_clean_dir') +@pytest.mark.parametrize(argnames='answers, exp, cnf', ids=lambda param: str(param), argvalues=( + ( + _answers([4, 'Y', 'Y', 'Y', 'Y', 'Y', 'N', 'pytest', 'pytest']), + _exp('choose versions individually and use pytest', + [ALL_PY_ENVS_WO_LAST_AS_STRING, 'pytest', 'pytest']), + _cnf(), + ), + ( + _answers([4, 'Y', 'Y', 'Y', 'Y', 'Y', 'N', 'py.test', '']), + _exp('choose versions individually and use old fashioned py.test', + [ALL_PY_ENVS_WO_LAST_AS_STRING, 'pytest', 'py.test']), + _cnf(), + ), + ( + _answers([1, 'pytest', '']), + _exp('choose current release Python and pytest with defaut deps', + [tox.PYTHON.CURRENT_RELEASE_ENV, 'pytest', 'pytest']), + _cnf(), + ), + ( + _answers([1, 'pytest -n auto', 'pytest-xdist']), + _exp('choose current release Python and pytest with xdist and some args', + [tox.PYTHON.CURRENT_RELEASE_ENV, 'pytest, pytest-xdist', + 'pytest -n auto']), + _cnf(), + ), + ( + _answers([2, 'pytest', '']), + _exp('choose py27, current release Python and pytest with defaut deps', + ['py27, %s' % tox.PYTHON.CURRENT_RELEASE_ENV, 'pytest', 'pytest']), + _cnf(), + ), + ( + _answers([3, 'pytest', '']), + _exp('choose all supported version and pytest with defaut deps'), + _cnf(), + ), + ( + _answers([4, 'Y', 'Y', 'Y', 'Y', 'Y', 'N', 'py.test', '']), + _exp('choose versions individually and use old fashioned py.test', + [ALL_PY_ENVS_WO_LAST_AS_STRING, 'pytest', 'py.test']), + _cnf(), + ), + ( + _answers([4, '', '', '', '', '', '', '', '']), + _exp('choose no version individually and defaults'), + _cnf(), + ), + ( + _answers([4, 'Y', 'Y', 'Y', 'Y', 'Y', 'N', 'python -m unittest discover', '']), + _exp('choose versions individually and use nose with default deps', + [ALL_PY_ENVS_WO_LAST_AS_STRING, '', 'python -m unittest discover']), + _cnf(), + ), + ( + _answers([4, 'Y', 'Y', 'Y', 'Y', 'Y', 'N', 'nosetests', 'nose']), + _exp('choose versions individually and use nose with default deps', + [ALL_PY_ENVS_WO_LAST_AS_STRING, 'nose', 'nosetests']), + _cnf(), + ), + ( + _answers([4, 'Y', 'Y', 'Y', 'Y', 'Y', 'N', 'trial', '']), + _exp('choose versions individually and use twisted tests with default deps', + [ALL_PY_ENVS_WO_LAST_AS_STRING, 'twisted', 'trial']), + _cnf(), + ), + ( + _answers([4, '', '', '', '', '', '', '', '']), + _exp('existing not overriden, generated to alternative with default name'), + _cnf(exists=True), + ), + ( + _answers([4, '', '', '', '', '', '', '', '']), + _exp('existing not overriden, generated to alternative with custom name'), + _cnf(exists=True, names=['some-other.ini']), + ), + ( + _answers([4, '', '', '', '', '', '', '', '']), + _exp('existing not override, generated to alternative'), + _cnf(exists=True, names=['tox.ini', 'some-other.ini']), + ), + ( + _answers([4, '', '', '', '', '', '', '', '']), + _exp('existing alternatives are not overriden, generated to alternative'), + _cnf(exists=True, names=['tox.ini', 'setup.py', 'some-other.ini']), + ), +)) +def test_quickstart(answers, cnf, exp, monkeypatch): + """Test quickstart script using some little helpers. + + :param _answers answers: user interaction simulation + :param _cnf cnf: helper for args and config file paths and contents + :param _exp exp: expectation helper + """ + monkeypatch.setattr('six.moves.input', answers) + monkeypatch.setattr('sys.argv', cnf.argv) + if cnf.exists: + answers.extend(cnf.names) + cnf.create() + main() + print("generated config at %s:\n%s\n" % (cnf.path_to_generated, cnf.generated_content)) + check_basic_sanity(cnf.generated_content, SIGNS_OF_SANITY) + assert cnf.generated_content == exp.content + if cnf.exists: + assert cnf.already_existing_content == cnf.SOME_CONTENT + + +def check_basic_sanity(content, signs): + for sign in signs: + if sign not in content: + pytest.fail("%s not in\n%s" % (sign, content)) diff --git a/tests/test_result.py b/tests/test_result.py index 515fb7e88..924193fde 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -10,14 +10,14 @@ from tox.result import ResultLog -@pytest.fixture -def pkg(tmpdir): - p = tmpdir.join("hello-1.0.tar.gz") - p.write("whatever") - return p +@pytest.fixture(name="pkg") +def create_fake_pkg(tmpdir): + pkg = tmpdir.join("hello-1.0.tar.gz") + pkg.write("whatever") + return pkg -def test_pre_set_header(pkg): +def test_pre_set_header(): replog = ResultLog() d = replog.dict assert replog.dict == d @@ -26,7 +26,7 @@ def test_pre_set_header(pkg): assert replog.dict["platform"] == sys.platform assert replog.dict["host"] == socket.getfqdn() data = replog.dumps_json() - replog2 = ResultLog.loads_json(data) + replog2 = ResultLog(data) assert replog2.dict == replog.dict @@ -44,7 +44,7 @@ def test_set_header(pkg): "md5": pkg.computehash("md5"), "sha256": pkg.computehash("sha256")} data = replog.dumps_json() - replog2 = ResultLog.loads_json(data) + replog2 = ResultLog(data) assert replog2.dict == replog.dict @@ -77,29 +77,20 @@ def test_get_commandlog(pkg): @pytest.mark.parametrize('os_name', ['posix', 'nt']) def test_invocation_error(exit_code, os_name, mocker, monkeypatch): monkeypatch.setattr(os, 'name', value=os_name) - mocker.spy(tox, '_exit_code_str') + mocker.spy(tox.exception, 'exit_code_str') + result = str(tox.exception.InvocationError("", exit_code=exit_code)) + # check that mocker works, because it will be our only test in + # test_z_cmdline.py::test_exit_code needs the mocker.spy above + assert tox.exception.exit_code_str.call_count == 1 + assert tox.exception.exit_code_str.call_args == mocker.call( + 'InvocationError', "", exit_code) if exit_code is None: - exception = tox.exception.InvocationError("") + assert "(exited with code" not in result else: - exception = tox.exception.InvocationError("", exit_code) - result = str(exception) - # check that mocker works, - # because it will be our only test in test_z_cmdline.py::test_exit_code - # need the mocker.spy above - assert tox._exit_code_str.call_count == 1 - assert tox._exit_code_str.call_args == mocker.call('InvocationError', "", exit_code) - if exit_code is None: - needle = "(exited with code" - assert needle not in result - else: - needle = "(exited with code %d)" % exit_code - assert needle in result - note = ("Note: this might indicate a fatal error signal") + assert "(exited with code %d)" % exit_code in result + note = "Note: this might indicate a fatal error signal" if (os_name == 'posix') and (exit_code == 128 + signal.SIGTERM): assert note in result - number = signal.SIGTERM - name = "SIGTERM" - signal_str = "({} - 128 = {}: {})".format(exit_code, number, name) - assert signal_str in result + assert "({} - 128 = {}: SIGTERM)".format(exit_code, signal.SIGTERM) in result else: assert note not in result diff --git a/tests/test_venv.py b/tests/test_venv.py index 073c96c3b..5ff601b0c 100644 --- a/tests/test_venv.py +++ b/tests/test_venv.py @@ -5,23 +5,9 @@ import pytest import tox -import tox.config -from tox.hookspecs import hookimpl from tox.interpreters import NoInterpreterInfo -from tox.venv import CreationConfig -from tox.venv import getdigest -from tox.venv import tox_testenv_create -from tox.venv import tox_testenv_install_deps -from tox.venv import VirtualEnv - - -# def test_global_virtualenv(capfd): -# v = VirtualEnv() -# assert v.list() -# out, err = capfd.readouterr() -# assert not out -# assert not err -# +from tox.venv import ( + CreationConfig, VirtualEnv, getdigest, tox_testenv_create, tox_testenv_install_deps) def test_getdigest(tmpdir): @@ -37,22 +23,25 @@ def test_getsupportedinterpreter(monkeypatch, newconfig, mocksession): interp = venv.getsupportedinterpreter() # realpath needed for debian symlinks assert py.path.local(interp).realpath() == py.path.local(sys.executable).realpath() - monkeypatch.setattr(sys, 'platform', "win32") + monkeypatch.setattr(tox.INFO, 'IS_WIN', True) monkeypatch.setattr(venv.envconfig, 'basepython', 'jython') - pytest.raises(tox.exception.UnsupportedInterpreter, venv.getsupportedinterpreter) + with pytest.raises(tox.exception.UnsupportedInterpreter): + venv.getsupportedinterpreter() monkeypatch.undo() monkeypatch.setattr(venv.envconfig, "envname", "py1") monkeypatch.setattr(venv.envconfig, 'basepython', 'notexistingpython') - pytest.raises(tox.exception.InterpreterNotFound, venv.getsupportedinterpreter) + with pytest.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, **kw: info) - pytest.raises(tox.exception.InvocationError, venv.getsupportedinterpreter) + with pytest.raises(tox.exception.InvocationError): + venv.getsupportedinterpreter() -def test_create(monkeypatch, mocksession, newconfig): +def test_create(mocksession, newconfig): config = newconfig([], """ [testenv:py123] """) @@ -66,7 +55,7 @@ def test_create(monkeypatch, mocksession, newconfig): assert len(pcalls) >= 1 args = pcalls[0].args assert "virtualenv" == str(args[2]) - if sys.platform != "win32": + if not tox.INFO.IS_WIN: # realpath is needed for stuff like the debian symlinks assert py.path.local(sys.executable).realpath() == py.path.local(args[0]).realpath() # assert Envconfig.toxworkdir in args @@ -76,9 +65,7 @@ def test_create(monkeypatch, mocksession, newconfig): assert venv.path_config.check(exists=False) -@pytest.mark.skipif("sys.platform == 'win32'") -def test_commandpath_venv_precedence(tmpdir, monkeypatch, - mocksession, newconfig): +def test_commandpath_venv_precedence(tmpdir, monkeypatch, mocksession, newconfig): config = newconfig([], """ [testenv:py123] """) @@ -91,7 +78,7 @@ def test_commandpath_venv_precedence(tmpdir, monkeypatch, assert py.path.local(p).relto(envconfig.envbindir), p -def test_create_sitepackages(monkeypatch, mocksession, newconfig): +def test_create_sitepackages(mocksession, newconfig): config = newconfig([], """ [testenv:site] sitepackages=True @@ -303,20 +290,15 @@ def test_env_variables_added_to_needs_reinstall(tmpdir, mocksession, newconfig, assert env["TEMP_NOPASS_VAR"] == "456" -def test_test_hashseed_is_in_output(newmocksession): - original_make_hashseed = tox.config.make_hashseed - tox.config.make_hashseed = lambda: '123456789' - try: - mocksession = newmocksession([], ''' - [testenv] - ''') - finally: - tox.config.make_hashseed = original_make_hashseed +def test_test_hashseed_is_in_output(newmocksession, monkeypatch): + seed = '123456789' + monkeypatch.setattr('tox.config.make_hashseed', lambda: seed) + mocksession = newmocksession([], "") venv = mocksession.getenv('python') action = mocksession.newaction(venv, "update") venv.update(action) venv.test() - mocksession.report.expect("verbosity0", "python runtests: PYTHONHASHSEED='123456789'") + mocksession.report.expect("verbosity0", "runtests: PYTHONHASHSEED='{}'".format(seed)) def test_test_runtests_action_command_is_in_output(newmocksession): @@ -331,7 +313,7 @@ def test_test_runtests_action_command_is_in_output(newmocksession): mocksession.report.expect("verbosity0", "*runtests*commands?0? | echo foo bar") -def test_install_error(newmocksession, monkeypatch): +def test_install_error(newmocksession): mocksession = newmocksession(['--recreate'], """ [testenv] deps=xyz @@ -344,7 +326,7 @@ def test_install_error(newmocksession, monkeypatch): assert venv.status == "commands failed" -def test_install_command_not_installed(newmocksession, monkeypatch): +def test_install_command_not_installed(newmocksession): mocksession = newmocksession(['--recreate'], """ [testenv] commands= @@ -356,7 +338,7 @@ def test_install_command_not_installed(newmocksession, monkeypatch): assert venv.status == 0 -def test_install_command_whitelisted(newmocksession, monkeypatch): +def test_install_command_whitelisted(newmocksession): mocksession = newmocksession(['--recreate'], """ [testenv] whitelist_externals = pytest @@ -372,7 +354,6 @@ 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_bash(newmocksession): mocksession = newmocksession(['--recreate'], """ [testenv] @@ -384,7 +365,7 @@ def test_install_command_not_installed_bash(newmocksession): mocksession.report.expect("warning", "*test command found but not*") -def test_install_python3(tmpdir, newmocksession): +def test_install_python3(newmocksession): if not py.path.local.sysfind('python3'): pytest.skip("needs python3") mocksession = newmocksession([], """ @@ -412,7 +393,6 @@ def test_install_python3(tmpdir, newmocksession): class TestCreationConfig: - def test_basic(self, newconfig, mocksession, tmpdir): config = newconfig([], "") envconfig = config.envconfigs['python'] @@ -533,7 +513,6 @@ def test_develop_recreation(self, newconfig, mocksession): class TestVenvTest: - def test_envbindir_path(self, newmocksession, monkeypatch): monkeypatch.setenv("PIP_RESPECT_VIRTUALENV", "1") mocksession = newmocksession([], """ @@ -737,12 +716,16 @@ def test_tox_testenv_create(newmocksession): log = [] class Plugin: - @hookimpl + @tox.hookimpl def tox_testenv_create(self, action, venv): + assert isinstance(action, tox.session.Action) + assert isinstance(venv, VirtualEnv) log.append(1) - @hookimpl + @tox.hookimpl def tox_testenv_install_deps(self, action, venv): + assert isinstance(action, tox.session.Action) + assert isinstance(venv, VirtualEnv) log.append(2) mocksession = newmocksession([], """ @@ -760,12 +743,12 @@ def test_tox_testenv_pre_post(newmocksession): log = [] class Plugin: - @hookimpl - def tox_runtest_pre(self, venv): + @tox.hookimpl + def tox_runtest_pre(self): log.append('started') - @hookimpl - def tox_runtest_post(self, venv): + @tox.hookimpl + def tox_runtest_post(self): log.append('finished') mocksession = newmocksession([], """ diff --git a/tests/test_z_cmdline.py b/tests/test_z_cmdline.py index 469b30661..18bcc98b8 100644 --- a/tests/test_z_cmdline.py +++ b/tests/test_z_cmdline.py @@ -1,3 +1,4 @@ +import json import os import platform import re @@ -9,17 +10,12 @@ import tox from tox._pytestplugin import ReportExpectMock - -try: - import json -except ImportError: - import simplejson as json +from tox.config import parseconfig +from tox.exception import MissingDependency, MissingDirectory +from tox.session import Session pytest_plugins = "pytester" -from tox.session import Session # noqa #E402 module level import not at top of file -from tox.config import parseconfig # noqa #E402 module level import not at top of file - def test_report_protocol(newconfig): config = newconfig([], """ @@ -37,8 +33,7 @@ def communicate(self): def wait(self): pass - session = Session(config, popen=Popen, - Report=ReportExpectMock) + session = Session(config, popen=Popen, Report=ReportExpectMock) report = session.report report.expect("using") venv = session.getvenv("mypython") @@ -50,12 +45,13 @@ def wait(self): def test__resolve_pkg(tmpdir, mocksession): distshare = tmpdir.join("distshare") spec = distshare.join("pkg123-*") - pytest.raises(tox.exception.MissingDirectory, 'mocksession._resolve_pkg(spec)') + with pytest.raises(MissingDirectory): + mocksession._resolve_pkg(spec) distshare.ensure(dir=1) - pytest.raises(tox.exception.MissingDependency, 'mocksession._resolve_pkg(spec)') + with pytest.raises(MissingDependency): + mocksession._resolve_pkg(spec) distshare.ensure("pkg123-1.3.5.zip") p = distshare.ensure("pkg123-1.4.5.zip") - mocksession.report.clear() result = mocksession._resolve_pkg(spec) assert result == p @@ -154,7 +150,7 @@ def test_summary_status(self, initproj, capfd): exp = "%s: commands succeeded" % env2.envconfig.envname assert exp in out - def test_getvenv(self, initproj, capfd): + def test_getvenv(self, initproj): initproj("logexample123-0.5", filedefs={ 'tests': {'test_hello.py': "def test_hello(): pass"}, 'tox.ini': ''' @@ -170,22 +166,8 @@ def test_getvenv(self, initproj, capfd): venv1 = session.getvenv("world") venv2 = session.getvenv("world") assert venv1 is venv2 - pytest.raises(LookupError, lambda: session.getvenv("qwe")) - - -# not sure we want this option ATM -def XXX_test_package(cmd, initproj): - initproj("myproj-0.6", filedefs={ - 'tests': {'test_hello.py': "def test_hello(): pass"}, - 'MANIFEST.in': """ - include doc - include myproj - """, - 'tox.ini': '' - }) - result = cmd("package") - assert not result.ret - assert any(re.match(r'.*created sdist package at.*', l) for l in result.outlines) + with pytest.raises(LookupError): + session.getvenv("qwe") def test_minversion(cmd, initproj): @@ -908,7 +890,7 @@ def test_tox_cmdline_no_args(monkeypatch): tox.cmdline() -def test_tox_cmdline_args(monkeypatch): +def test_tox_cmdline_args(): with pytest.raises(SystemExit): tox.cmdline(['caller_script', '--help']) @@ -917,14 +899,15 @@ def test_tox_cmdline_args(monkeypatch): def test_exit_code(initproj, cmd, exit_code, mocker): """ Check for correct InvocationError, with exit code, except for zero exit code """ - mocker.spy(tox, '_exit_code_str') + import tox.exception + mocker.spy(tox.exception, 'exit_code_str') tox_ini_content = "[testenv:foo]\ncommands=python -c 'import sys; sys.exit(%d)'" % exit_code initproj("foo", filedefs={'tox.ini': tox_ini_content}) cmd() if exit_code: # need mocker.spy above - assert tox._exit_code_str.call_count == 1 - (args, kwargs) = tox._exit_code_str.call_args + assert tox.exception.exit_code_str.call_count == 1 + (args, kwargs) = tox.exception.exit_code_str.call_args assert kwargs == {} (call_error_name, call_command, call_exit_code) = args assert call_error_name == 'InvocationError' @@ -935,4 +918,4 @@ def test_exit_code(initproj, cmd, exit_code, mocker): assert call_exit_code == exit_code else: # need mocker.spy above - assert tox._exit_code_str.call_count == 0 + assert tox.exception.exit_code_str.call_count == 0 diff --git a/tox.ini b/tox.ini index 386090047..877668cff 100644 --- a/tox.ini +++ b/tox.ini @@ -18,11 +18,13 @@ extras = testing commands = pytest {posargs:--cov-config="{toxinidir}/tox.ini" --cov="{envsitepackagesdir}/tox" --timeout=180 tests} [testenv:docs] -description = invoke sphinx-build to build the HTML docs, check that URIs are valid +description = invoke sphinx-build to build the HTML docs and check that all links are valid +whitelist_externals = sphinx-build basepython = python3.6 extras = docs -commands = sphinx-build -d "{toxworkdir}/docs_doctree" doc "{toxworkdir}/docs_out" --color -W -bhtml - sphinx-build -d "{toxworkdir}/docs_doctree" doc "{toxworkdir}/docs_out" --color -W -blinkcheck +commands = + sphinx-build -d "{toxworkdir}/docs_doctree" doc "{toxworkdir}/docs_out" --color -W -bhtml + sphinx-build -d "{toxworkdir}/docs_doctree" doc "{toxworkdir}/docs_out" --color -W -blinkcheck [testenv:fix-lint] basepython = python3.6 @@ -65,6 +67,7 @@ skip_install = True commands = python3.6 -c "import sys; sys.exit(139)" [testenv:pra] +platform = linux passenv = * description = "personal release assistant" - see HOWTORELEASE.rst extras = publish, docs diff --git a/tox/__init__.py b/tox/__init__.py index fd2b86c89..c7560c825 100644 --- a/tox/__init__.py +++ b/tox/__init__.py @@ -1,89 +1,40 @@ -import os -import signal +"""Everything made explicitly available via `__all__` can be considered as part of the tox API. -from pkg_resources import DistributionNotFound -from pkg_resources import get_distribution +We will emit deprecation warnings for one minor release before making changes to these objects. -from .hookspecs import hookimpl -from .hookspecs import hookspec - -try: - _full_version = get_distribution(__name__).version - __version__ = _full_version.split('+', 1)[0] -except DistributionNotFound: - __version__ = '0.0.0.dev0' - - -# separate function because pytest-mock `spy` does not work on Exceptions -# can use neither a class method nor a static because of -# https://bugs.python.org/issue23078 -# even a normal method failed with -# TypeError: descriptor '__getattribute__' requires a 'BaseException' object but received a 'type' -def _exit_code_str(exception_name, command, exit_code): - """ string representation for an InvocationError, with exit code """ - str_ = "%s for command %s" % (exception_name, command) - if exit_code is not None: - str_ += " (exited with code %d)" % (exit_code) - if (os.name == 'posix') and (exit_code > 128): - signals = {number: name - for name, number in vars(signal).items() - if name.startswith("SIG")} - number = exit_code - 128 - name = signals.get(number) - if name: - str_ += ("\nNote: this might indicate a fatal error signal " - "({} - 128 = {}: {})".format(number+128, number, name)) - return str_ - - -class exception: - class Error(Exception): - def __str__(self): - return "%s: %s" % (self.__class__.__name__, self.args[0]) +If objects are marked experimental they might change between minor versions. - class MissingSubstitution(Error): - FLAG = 'TOX_MISSING_SUBSTITUTION' - """placeholder for debugging configurations""" +To override/modify tox behaviour via plugins see `tox.hookspec` and its use with pluggy. +""" +from pkg_resources import DistributionNotFound, get_distribution - def __init__(self, name): - self.name = name +import pluggy - 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. """ - def __init__(self, command, exit_code=None): - super(exception.Error, self).__init__(command, exit_code) - self.command = command - self.exit_code = exit_code - - def __str__(self): - return _exit_code_str(self.__class__.__name__, self.command, self.exit_code) - - class MissingFile(Error): - """ an error while invoking a script. """ +from . import exception +from .constants import INFO, PIP, PYTHON +from .hookspecs import hookspec - class MissingDirectory(Error): - """ a directory did not exist. """ +__all__ = ( + '__version__', # tox version + 'cmdline', # run tox as part of another program/IDE (same behaviour as called standalone) + 'hookimpl', # Hook implementation marker to be imported by plugins + 'exception', # tox specific exceptions - class MissingDependency(Error): - """ a dependency could not be found or determined. """ + # EXPERIMENTAL CONSTANTS API + 'PYTHON', 'INFO', 'PIP', - class MinVersionError(Error): - """ the installed tox version is lower than requested minversion. """ + # DEPRECATED - will be removed from API in tox 4 + 'hookspec', +) - def __init__(self, message): - self.message = message - super(exception.MinVersionError, self).__init__(message) +hookimpl = pluggy.HookimplMarker("tox") +try: + _full_version = get_distribution(__name__).version + __version__ = _full_version.split('+', 1)[0] +except DistributionNotFound: + __version__ = '0.0.0.dev0' -from .session import run_main as cmdline # noqa -__all__ = ('hookspec', 'hookimpl', 'cmdline', 'exception', '__version__') +# NOTE: must come last due to circular import +from .session import cmdline # noqa diff --git a/tox/__main__.py b/tox/__main__.py index aadd3d2d7..7d3c3cc15 100644 --- a/tox/__main__.py +++ b/tox/__main__.py @@ -1,4 +1,4 @@ -from tox.session import run_main +import tox if __name__ == '__main__': - run_main() + tox.cmdline() diff --git a/tox/_pytestplugin.py b/tox/_pytestplugin.py index 4baa45922..71d1f45ac 100644 --- a/tox/_pytestplugin.py +++ b/tox/_pytestplugin.py @@ -1,5 +1,4 @@ -from __future__ import print_function -from __future__ import unicode_literals +from __future__ import print_function, unicode_literals import os import textwrap @@ -11,10 +10,13 @@ import six import tox -from .config import parseconfig -from .result import ResultLog -from .session import main -from .venv import VirtualEnv +from tox.config import parseconfig +from tox.result import ResultLog +from tox.session import Session, main +from tox.venv import VirtualEnv + +mark_dont_run_on_windows = pytest.mark.skipif(os.name == 'nt', reason="non windows test") +mark_dont_run_on_posix = pytest.mark.skipif(os.name == 'posix', reason="non posix test") def pytest_configure(): @@ -25,8 +27,7 @@ def pytest_configure(): def pytest_addoption(parser): - parser.addoption("--no-network", action="store_true", - dest="no_network", + parser.addoption("--no-network", action="store_true", dest="no_network", help="don't run tests requiring network") @@ -35,21 +36,23 @@ def pytest_report_header(): @pytest.fixture -def newconfig(request, tmpdir): - def newconfig(args, source=None, plugins=()): +def work_in_clean_dir(tmpdir): + with tmpdir.as_cwd(): + yield + + +@pytest.fixture(name="newconfig") +def create_new_config_file(tmpdir): + def create_new_config_file_(args, source=None, plugins=()): if source is None: source = args args = [] s = textwrap.dedent(source) p = tmpdir.join("tox.ini") p.write(s) - old = tmpdir.chdir() - try: + with tmpdir.as_cwd(): return parseconfig(args, plugins=plugins) - finally: - old.chdir() - - return newconfig + return create_new_config_file_ @pytest.fixture @@ -71,7 +74,6 @@ def run(*argv): except OSError as e: result.ret = e.errno return result - yield run @@ -119,7 +121,7 @@ def __getattr__(self, name): # FIXME: special case for property on Reporter class, may it be generalized? return 0 - def generic_report(*args, **kwargs): + def generic_report(*args, **_): self._calls.append((name,) + args) print("%s" % (self._calls[-1],)) @@ -174,17 +176,16 @@ def __init__(self, args, cwd, env, stdout, stderr, shell): self.stderr = stderr self.shell = shell - def communicate(self): + @staticmethod + def communicate(): return "", "" def wait(self): pass -@pytest.fixture -def mocksession(request): - from tox.session import Session - +@pytest.fixture(name="mocksession") +def create_mocksession(request): class MockSession(Session): def __init__(self): self._clearmocks() @@ -203,26 +204,19 @@ def _clearmocks(self): def make_emptydir(self, path): pass - def popen(self, args, cwd, shell=None, - universal_newlines=False, - stdout=None, stderr=None, env=None): + def popen(self, args, cwd, shell=None, 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.getfixturevalue("mocksession") - newconfig = request.getfixturevalue("newconfig") - - def newmocksession(args, source, plugins=()): +def newmocksession(mocksession, newconfig): + def newmocksession_(args, source, plugins=()): mocksession.config = newconfig(args, source, plugins=plugins) return mocksession - - return newmocksession + return newmocksession_ def getdecoded(out): @@ -234,8 +228,8 @@ def getdecoded(out): @pytest.fixture -def initproj(request, tmpdir): - """Create a factory function for creating example projects +def initproj(tmpdir): + """Create a factory function for creating example projects. Constructed folder/file hierarchy examples: @@ -257,10 +251,8 @@ def initproj(request, tmpdir): __init__.py name.egg-info/ # created later on package build setup.py - """ - - def initproj(nameversion, filedefs=None, src_root="."): + def initproj_(nameversion, filedefs=None, src_root="."): if filedefs is None: filedefs = {} if not src_root: @@ -272,18 +264,15 @@ def initproj(nameversion, filedefs=None, src_root="."): name, version = parts else: name, version = nameversion - base = tmpdir.join(name) src_root_path = _path_join(base, src_root) assert base == src_root_path or src_root_path.relto(base), ( '`src_root` must be the constructed project folder or its direct ' 'or indirect subfolder') - base.ensure(dir=1) create_files(base, filedefs) - if not _filedefs_contains(base, filedefs, 'setup.py'): - create_files(base, {'setup.py': ''' + create_files(base, {'setup.py': """ from setuptools import setup, find_packages setup( name='%(name)s', @@ -294,22 +283,16 @@ def initproj(nameversion, filedefs=None, src_root="."): packages=find_packages('%(src_root)s'), package_dir={'':'%(src_root)s'}, ) - ''' % locals()}) - + """ % locals()}) if not _filedefs_contains(base, filedefs, src_root_path.join(name)): - create_files(src_root_path, { - name: {'__init__.py': '__version__ = %r' % version} - }) - + create_files(src_root_path, {name: {'__init__.py': '__version__ = %r' % version}}) manifestlines = ["include %s" % p.relto(base) for p in base.visit(lambda x: x.check(file=1))] create_files(base, {"MANIFEST.in": "\n".join(manifestlines)}) - - print("created project in %s" % (base,)) + print("created project in %s" % base) base.chdir() return base - - return initproj + return initproj_ def _path_parts(path): diff --git a/tox/_quickstart.py b/tox/_quickstart.py index d685f12aa..01269677f 100644 --- a/tox/_quickstart.py +++ b/tox/_quickstart.py @@ -3,7 +3,7 @@ tox._quickstart ~~~~~~~~~~~~~~~~~ - Command-line script to quickly setup tox.ini for a Python project + Command-line script to quickly setup a configuration for a Python project This file was heavily inspired by and uses code from ``sphinx-quickstart`` in the BSD-licensed `Sphinx project`_. @@ -40,25 +40,15 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ import argparse +import codecs +import os import sys -from codecs import open -from os import path -TERM_ENCODING = getattr(sys.stdin, 'encoding', None) +import six -from tox import __version__ # noqa #E402 module level import not at top of file - -# 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 = ['py27', 'py34', 'py35', 'py36', 'pypy', 'jython'] - -PROMPT_PREFIX = '> ' +import tox +ALTERNATIVE_CONFIG_NAME = 'tox-generated.ini' QUICKSTART_CONF = '''\ # tox (https://tox.readthedocs.io/) is a tool for running tests # in multiple virtualenvs. This configuration file will run the @@ -66,11 +56,13 @@ # and then run "tox" from this directory. [tox] -envlist = %(envlist)s +envlist = {envlist} [testenv] -commands = %(commands)s -deps = %(deps)s +deps = + {deps} +commands = + {commands} ''' @@ -110,186 +102,148 @@ def ok(x): return x -def do_prompt(d, key, text, default=None, validator=nonempty): +def list_modificator(answer, existing=None): + if not existing: + existing = [] + if not isinstance(existing, list): + existing = [existing] + if not answer: + return existing + existing.extend([t.strip() for t in answer.split(',') if t.strip()]) + return existing + + +def do_prompt(map_, key, text, default=None, validator=nonempty, modificator=None): 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 + prompt = '> %s [%s]: ' % (text, default) if default else '> %s: ' % text + answer = six.moves.input(prompt) + if default and not answer: + answer = default + # FIXME use six instead of self baked solution + if sys.version_info < (3,) and not isinstance(answer, 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) + if answer.decode('ascii', 'replace').encode('ascii', 'replace') != answer: + term_encoding = getattr(sys.stdin, 'encoding', None) + if term_encoding: + answer = answer.decode(term_encoding) else: - print('* Note: non-ASCII characters entered ' - 'and terminal encoding unknown -- assuming ' - 'UTF-8 or Latin-1.') + print('* Note: non-ASCII characters entered but terminal encoding unknown ' + '-> assuming UTF-8 or Latin-1.') try: - x = x.decode('utf-8') + answer = answer.decode('utf-8') except UnicodeDecodeError: - x = x.decode('latin1') - try: - x = validator(x) - except ValidationError: - err = sys.exc_info()[1] - print('* ' + str(err)) - continue + answer = answer.decode('latin1') + if validator: + try: + answer = validator(answer) + 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, py36 - [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', 'py36'): - 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: - - pytest - - 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 any(c in d['commands'] for c in ['pytest', 'py.test']): - 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): + map_[key] = modificator(answer, map_.get(key)) if modificator else answer + + +def ask_user(map_): + """modify *map_* in place by getting info from the user.""" + print('Welcome to the tox %s quickstart utility.' % tox.__version__) + print('This utility will ask you a few questions and then generate a simple configuration ' + 'file to help get you started using tox.\n' + 'Please enter values for the following settings (just press Enter to accept a ' + 'default value, if one is given in brackets).\n') + print('What Python versions do you want to test against?\n' + ' [1] %s\n' + ' [2] py27, %s\n' + ' [3] (All versions) %s\n' + ' [4] Choose each one-by-one' % ( + tox.PYTHON.CURRENT_RELEASE_ENV, tox.PYTHON.CURRENT_RELEASE_ENV, + ', '.join(tox.PYTHON.QUICKSTART_PY_ENVS))) + do_prompt(map_, 'canned_pyenvs', 'Enter the number of your choice', + default='3', validator=choice('1', '2', '3', '4')) + if map_['canned_pyenvs'] == '1': + map_[tox.PYTHON.CURRENT_RELEASE_ENV] = True + elif map_['canned_pyenvs'] == '2': + for pyenv in ('py27', tox.PYTHON.CURRENT_RELEASE_ENV): + map_[pyenv] = True + elif map_['canned_pyenvs'] == '3': + for pyenv in tox.PYTHON.QUICKSTART_PY_ENVS: + map_[pyenv] = True + elif map_['canned_pyenvs'] == '4': + for pyenv in tox.PYTHON.QUICKSTART_PY_ENVS: + if pyenv not in map_: + do_prompt(map_, pyenv, 'Test your project with %s (Y/n)' % pyenv, 'Y', + validator=boolean) + print('What command should be used to test your project? Examples:\n' + ' - pytest\n' + ' - python -m unittest discover\n' + ' - python setup.py test\n' + ' - trial package.module\n') + do_prompt(map_, 'commands', 'Type the command to run your tests', + default='pytest', modificator=list_modificator) + print('What extra dependencies do your tests have?') + map_['deps'] = get_default_deps(map_['commands']) + if map_['deps']: + print("default dependencies are: %s" % map_['deps']) + do_prompt(map_, 'deps', 'Comma-separated list of dependencies', + validator=None, modificator=list_modificator) + + +def get_default_deps(commands): + if commands and any(c in str(commands) for c in ['pytest', 'py.test']): + return ['pytest'] + if 'trial' in commands: + return ['twisted'] + return [] + + +def post_process_input(map_): + envlist = [env for env in tox.PYTHON.QUICKSTART_PY_ENVS if map_.get(env) is True] + map_['envlist'] = ', '.join(envlist) + map_['commands'] = '\n '.join([cmd.strip() for cmd in map_['commands']]) + map_['deps'] = '\n '.join([dep.strip() for dep in set(map_['deps'])]) + + +def generate(map_): """Generate project based on values in *d*.""" + dpath = map_.get('path', os.getcwd()) + altpath = os.path.join(dpath, ALTERNATIVE_CONFIG_NAME) + while True: + name = map_.get('name', tox.INFO.DEFAULT_CONFIG_NAME) + targetpath = os.path.join(dpath, name) + if not os.path.isfile(targetpath): + break + do_prompt(map_, 'name', '%s exists - choose an alternative' % targetpath, altpath) + with codecs.open(targetpath, 'w', encoding='utf-8') as f: + f.write(prepare_content(QUICKSTART_CONF.format(**map_))) + print('Finished: %s has been created. For information on this file, ' + 'see https://tox.readthedocs.io/en/latest/config.html\n' + 'Execute `tox` to test your project.' % targetpath) - conf_text = QUICKSTART_CONF % d - conf_text = rtrim_right(conf_text) - - def write_file(fpath, mode, content): - print('Creating file %s.' % fpath) - try: - with open(fpath, mode, encoding='utf-8') as f: - f.write(content) - except IOError: - print('Error writing file.') - raise - - sys.stdout.write('\n') - - fpath = path.join(d.get('path', ''), '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', - path.join(d.get('path', ''), '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 https://tox.readthedocs.io/en/latest/config.html') - print(''' -Execute `tox` to test your project. -''') +def prepare_content(content): + return '\n'.join([line.rstrip() for line in content.split("\n")]) -def parse_args(argv): +def parse_args(): parser = argparse.ArgumentParser( - description='Command-line script to quickly setup tox.ini for a Python project.' - ) + description='Command-line script to quickly tox config file for a Python project.') parser.add_argument( 'root', type=str, nargs='?', default='.', - help='Custom root directory to write tox.ini to. Defaults to current directory.' - ) - parser.add_argument('--version', action='version', version='%(prog)s ' + __version__) - - args = argv[1:] - return parser.parse_args(args) - + help='Custom root directory to write config to. Defaults to current directory.') + parser.add_argument('--version', action='version', version='%(prog)s ' + tox.__version__) + return parser.parse_args() -def main(argv=sys.argv): - args = parse_args(argv) - - d = {} - d['path'] = args.root +def main(): + args = parse_args() + map_ = {'path': args.root} try: - ask_user(d) + ask_user(map_) except (KeyboardInterrupt, EOFError): - print() - print('[Interrupted.]') - return - - d = process_input(d) - try: - generate(d, overwrite=False) - except Exception: - return 2 - - return 0 + print('\n[Interrupted.]') + return 1 + post_process_input(map_) + generate(map_) if __name__ == '__main__': diff --git a/tox/_verlib.py b/tox/_verlib.py index 79f9a11e4..39294695a 100644 --- a/tox/_verlib.py +++ b/tox/_verlib.py @@ -175,9 +175,8 @@ def __str__(self): @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 + """Transform a version expressed in tuple into its string representation.""" + # FIXME 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: diff --git a/tox/config.py b/tox/config.py index f8cb9ff47..67d3fbd7b 100755 --- a/tox/config.py +++ b/tox/config.py @@ -16,39 +16,28 @@ import py import tox -import tox.interpreters -from tox import hookspecs +from tox.interpreters import Interpreters from tox._verlib import NormalizedVersion -iswin32 = sys.platform == "win32" -default_factors = {'jython': 'jython', 'pypy': 'pypy', 'pypy3': 'pypy3', - 'py': sys.executable, 'py2': 'python2', 'py3': 'python3'} -for version in '27,34,35,36,37'.split(','): - default_factors['py' + version] = 'python%s.%s' % tuple(version) +hookimpl = tox.hookimpl +"""DEPRECATED - REMOVE - this is left for compatibility with plugins importing this from here. -hookimpl = pluggy.HookimplMarker("tox") +Instead create a hookimpl in your code with: -_dummy = object() + import pluggy + hookimpl = pluggy.HookimplMarker("tox") +""" -PIP_INSTALL_SHORT_OPTIONS_ARGUMENT = ['-{}'.format(option) for option in [ - 'c', 'e', 'r', 'b', 't', 'd', -]] - -PIP_INSTALL_LONG_OPTIONS_ARGUMENT = ['--{}'.format(option) for option in [ - 'constraint', 'editable', 'requirement', 'build', 'target', 'download', - 'src', 'upgrade-strategy', 'install-options', 'global-option', - 'root', 'prefix', 'no-binary', 'only-binary', 'index-url', - 'extra-index-url', 'find-links', 'proxy', 'retries', 'timeout', - 'exists-action', 'trusted-host', 'client-cert', 'cache-dir', -]] +default_factors = tox.PYTHON.DEFAULT_FACTORS +"""DEPRECATED MOVE - please update to new location.""" def get_plugin_manager(plugins=()): # initialize plugin manager import tox.venv pm = pluggy.PluginManager("tox") - pm.add_hookspecs(hookspecs) + pm.add_hookspecs(tox.hookspecs) pm.register(tox.config) pm.register(tox.interpreters) pm.register(tox.venv) @@ -61,8 +50,7 @@ def get_plugin_manager(plugins=()): class Parser: - """ command line and ini-parser control object. """ - + """Command line and ini-parser control object.""" def __init__(self): self.argparser = argparse.ArgumentParser( description="tox options", add_help=False) @@ -136,35 +124,25 @@ def postprocess(self, testenv_config, value): else: name = depline.strip() ixserver = None - # we need to process options, in case they contain a space, # as the subprocess call to pip install will otherwise fail. - # in case of a short option, we remove the space - for option in PIP_INSTALL_SHORT_OPTIONS_ARGUMENT: + for option in tox.PIP.INSTALL_SHORT_OPTIONS_ARGUMENT: if name.startswith(option): - name = '{}{}'.format( - option, name[len(option):].strip() - ) - + name = '%s%s' % (option, name[len(option):].strip()) # in case of a long option, we add an equal sign - for option in PIP_INSTALL_LONG_OPTIONS_ARGUMENT: + for option in tox.PIP.INSTALL_LONG_OPTIONS_ARGUMENT: if name.startswith(option + ' '): - name = '{}={}'.format( - option, name[len(option):].strip() - ) - + name = '%s=%s' % (option, name[len(option):].strip()) 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. + """Override given dependency config name. Take ``--force-dep-version`` option into account. :param name: dep config, for example ["pkg==1.0", "other==2.0"]. - :param config: Config instance + :param config: ``Config`` instance :return: the new dependency that should be used for virtual environments """ if not config.option.force_dep: @@ -176,10 +154,7 @@ def _replace_forced_dep(self, name, config): @classmethod def _is_same_dep(cls, dep1, dep2): - """ - Returns True if both dependency definitions refer to the - same package, even if versions differ. - """ + """Definitions are the same if they 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 @@ -225,21 +200,20 @@ def postprocess(self, testenv_config, value): def parseconfig(args, plugins=()): - """ + """Parse the configuration file and create a Config object. + + :param plugins: :param list[str] args: list of arguments. - :type pkg: str :rtype: :class:`Config` :raise SystemExit: toxinit file is not found """ - pm = get_plugin_manager(plugins) # 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) + interpreters = Interpreters(hook=pm.hook) config = Config(pluginmanager=pm, option=option, interpreters=interpreters) config._parser = parser config._testenv_attr = parser._testenv_attr @@ -299,6 +273,8 @@ def get_version_info(pm): class SetenvDict(object): + _DUMMY = object() + def __init__(self, definitions, reader): self.definitions = definitions self.reader = reader @@ -329,8 +305,8 @@ def get(self, name, default=None): return res def __getitem__(self, name): - x = self.get(name, _dummy) - if x is _dummy: + x = self.get(name, self._DUMMY) + if x is self._DUMMY: raise KeyError(name) return x @@ -342,7 +318,7 @@ def __setitem__(self, name, value): self.resolved[name] = value -@hookimpl +@tox.hookimpl def tox_addoption(parser): # formatter_class=argparse.ArgumentDefaultsHelpFormatter) parser.add_argument("--version", action="store_true", dest="version", @@ -442,9 +418,9 @@ def setenv(testenv_config, value): 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] + for factor in testenv_config.factors: + if factor in tox.PYTHON.DEFAULT_FACTORS: + return tox.PYTHON.DEFAULT_FACTORS[factor] return sys.executable return str(value) @@ -520,15 +496,14 @@ def passenv(testenv_config, value): # 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": + if tox.INFO.IS_WIN: passenv.add("SYSTEMDRIVE") # needed for pip6 passenv.add("SYSTEMROOT") # needed for python's crypto module passenv.add("PATHEXT") # needed for discovering executables passenv.add("COMSPEC") # needed for distutils cygwincompiler passenv.add("TEMP") passenv.add("TMP") - # for `multiprocessing.cpu_count()` on Windows - # (prior to Python 3.4). + # for `multiprocessing.cpu_count()` on Windows (prior to Python 3.4). passenv.add("NUMBER_OF_PROCESSORS") passenv.add("PROCESSOR_ARCHITECTURE") # platform.machine() passenv.add("USERPROFILE") # needed for `os.path.expanduser()` @@ -617,31 +592,30 @@ def develop(testenv_config, value): class Config(object): - """ Global Tox config object. """ - + """Global Tox config object.""" def __init__(self, pluginmanager, option, interpreters): - #: dictionary containing envname to envconfig mappings self.envconfigs = {} + """Mapping envname -> envconfig""" self.invocationcwd = py.path.local() self.interpreters = interpreters self.pluginmanager = pluginmanager - #: option namespace containing all parsed command line options self.option = option + """option namespace containing all parsed command line options""" @property def homedir(self): homedir = get_homedir() if homedir is None: - homedir = self.toxinidir # XXX good idea? + homedir = self.toxinidir # FIXME XXX good idea? return homedir class TestenvConfig: - """ Testenv Configuration object. + """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 @@ -659,9 +633,10 @@ def __init__(self, envname, config, factors, 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: + """Path to directory where scripts/binaries reside.""" + if (tox.INFO.IS_WIN and + "jython" not in self.basepython and + "pypy" not in self.basepython): return self.envdir.join("Scripts") else: return self.envdir.join("bin") @@ -672,7 +647,7 @@ def envbindir(self): @property def envpython(self): - """ path to python executable. """ + """Path to python executable.""" return self.get_envpython() def get_envpython(self): @@ -684,8 +659,9 @@ def get_envpython(self): return self.envbindir.join(name) def get_envsitepackagesdir(self): - """ return sitepackagesdir of the virtualenv environment. - (only available during execution, not parsing) + """Return sitepackagesdir of the virtualenv environment. + + NOTE: Only available during execution, not during parsing. """ x = self.config.interpreters.get_sitepackagesdir( info=self.python_info, @@ -694,12 +670,11 @@ def get_envsitepackagesdir(self): @property def python_info(self): - """ return sitepackagesdir of the virtualenv environment. """ + """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: + if tox.INFO.IS_WIN 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) @@ -723,7 +698,7 @@ def get_homedir(): def make_hashseed(): max_seed = 4294967295 - if sys.platform == 'win32': + if tox.INFO.IS_WIN: max_seed = 1024 return str(random.randint(1, max_seed)) @@ -821,7 +796,7 @@ def __init__(self, config, inipath): # factors used in config or predefined known_factors = self._list_section_factors("testenv") - known_factors.update(default_factors) + known_factors.update(tox.PYTHON.DEFAULT_FACTORS) known_factors.add("python") # factors stated in config envlist @@ -976,9 +951,11 @@ def __init__(self, name, url=None): self.url = url -#: Check value matches substitution form -#: of referencing value from other section. E.g. {[base]commands} is_section_substitution = re.compile(r"{\[[^{}\s]+\]\S+?}").match +"""Check value matches substitution form of referencing value from other section. + +E.g. {[base]commands} +""" class SectionReader: diff --git a/tox/constants.py b/tox/constants.py new file mode 100644 index 000000000..1eaf64304 --- /dev/null +++ b/tox/constants.py @@ -0,0 +1,60 @@ +"""All non private names (no leading underscore) here are part of the tox API. + +They live in the tox namespace and can be accessed as tox.[NAMESPACE.]NAME +""" +import sys as _sys + + +def _contruct_default_factors(version_tuples, other_interpreters): + default_factors = {'py': _sys.executable, 'py2': 'python2', 'py3': 'python3'} + default_factors.update({'py%s%s' % (major, minor): 'python%s.%s' % (major, minor) + for major, minor in version_tuples}) + default_factors.update({interpreter: interpreter for interpreter in other_interpreters}) + return default_factors + + +class PYTHON: + CPYTHON_VERSION_TUPLES = [(2, 7), (3, 4), (3, 5), (3, 6), (3, 7)] + OTHER_PYTHON_INTERPRETERS = ['jython', 'pypy', 'pypy3'] + DEFAULT_FACTORS = _contruct_default_factors(CPYTHON_VERSION_TUPLES, OTHER_PYTHON_INTERPRETERS) + CURRENT_RELEASE_ENV = 'py36' + """Should hold currently released py -> for easy updating""" + QUICKSTART_PY_ENVS = ['py27', 'py34', 'py35', CURRENT_RELEASE_ENV, 'pypy', 'jython'] + """For choices in tox-quickstart""" + + +class INFO: + DEFAULT_CONFIG_NAME = 'tox.ini' + IS_WIN = _sys.platform == "win32" + + +class PIP: + SHORT_OPTIONS = ['c', 'e', 'r', 'b', 't', 'd'] + LONG_OPTIONS = [ + 'build', + 'cache-dir', + 'client-cert', + 'constraint', + 'download', + 'editable', + 'exists-action', + 'extra-index-url', + 'global-option', + 'find-links', + 'index-url', + 'install-options', + 'prefix', + 'proxy', + 'no-binary', + 'only-binary', + 'requirement', + 'retries', + 'root', + 'src', + 'target', + 'timeout', + 'trusted-host', + 'upgrade-strategy', + ] + INSTALL_SHORT_OPTIONS_ARGUMENT = ['-%s' % option for option in SHORT_OPTIONS] + INSTALL_LONG_OPTIONS_ARGUMENT = ['--%s' % option for option in LONG_OPTIONS] diff --git a/tox/exception.py b/tox/exception.py new file mode 100644 index 000000000..aefad5bfc --- /dev/null +++ b/tox/exception.py @@ -0,0 +1,85 @@ +import os +import signal + + +def exit_code_str(exception_name, command, exit_code): + """String representation for an InvocationError, with exit code + + NOTE: this might also be used by plugin tests (tox-venv at the time of writing), + so some coordination is needed if this is ever moved or a different solution for this hack + is found. + + NOTE: this is a separate function because pytest-mock `spy` does not work on Exceptions + We can use neither a class method nor a static because of https://bugs.python.org/issue23078. + Even a normal method failed with "TypeError: descriptor '__getattribute__' requires a + 'BaseException' object but received a 'type'". + """ + str_ = "%s for command %s" % (exception_name, command) + if exit_code is not None: + str_ += " (exited with code %d)" % (exit_code) + if (os.name == 'posix') and (exit_code > 128): + signals = {number: name + for name, number in vars(signal).items() + if name.startswith("SIG")} + number = exit_code - 128 + name = signals.get(number) + if name: + str_ += ("\nNote: this might indicate a fatal error signal " + "(%d - 128 = %d: %s)" % (number+128, number, name)) + return str_ + + +class Error(Exception): + def __str__(self): + return "%s: %s" % (self.__class__.__name__, self.args[0]) + + +class MissingSubstitution(Error): + FLAG = 'TOX_MISSING_SUBSTITUTION' + """placeholder for debugging configurations""" + + def __init__(self, name): + self.name = name + + +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.""" + def __init__(self, command, exit_code=None): + super(Error, self).__init__(command, exit_code) + self.command = command + self.exit_code = exit_code + + def __str__(self): + return exit_code_str(self.__class__.__name__, self.command, self.exit_code) + + +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(MinVersionError, self).__init__(message) diff --git a/tox/hookspecs.py b/tox/hookspecs.py index ec177b7a8..fca24f4a7 100644 --- a/tox/hookspecs.py +++ b/tox/hookspecs.py @@ -1,11 +1,7 @@ -""" Hook specifications for tox. +"""Hook specifications for tox - see https://pluggy.readthedocs.io/""" +import pluggy -""" -from pluggy import HookimplMarker -from pluggy import HookspecMarker - -hookspec = HookspecMarker("tox") -hookimpl = HookimplMarker("tox") +hookspec = pluggy.HookspecMarker("tox") @hookspec @@ -15,15 +11,16 @@ def tox_addoption(parser): @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). """ + """Called after command line options are parsed and ini-file has been read. + + Please be aware that the config object layout may change between major tox versions. + """ @hookspec(firstresult=True) def tox_get_python_executable(envconfig): - """ return a python executable for the given python base name. + """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 @@ -34,7 +31,7 @@ def tox_get_python_executable(envconfig): @hookspec(firstresult=True) def tox_testenv_create(venv, action): - """ [experimental] perform creation action for this venv. + """Perform creation action for this venv. Some example usage: @@ -47,16 +44,16 @@ def tox_testenv_create(venv, action): .. note:: This api is experimental due to the unstable api of :class:`tox.venv.VirtualEnv`. - .. note:: This hook uses ``firstresult=True`` (see pluggy_) -- hooks + .. note:: This hook uses ``firstresult=True`` (see `pluggy first result only`_) -- hooks implementing this will be run until one returns non-``None``. - .. _pluggy: http://pluggy.readthedocs.io/en/latest/#first-result-only + .. _`pluggy first result only`: http://pluggy.readthedocs.io/en/latest/#first-result-only """ @hookspec(firstresult=True) def tox_testenv_install_deps(venv, action): - """ [experimental] perform install dependencies action for this venv. + """Perform install dependencies action for this venv. Some example usage: @@ -71,18 +68,17 @@ def tox_testenv_install_deps(venv, action): .. note:: This api is experimental due to the unstable api of :class:`tox.venv.VirtualEnv`. - .. note:: This hook uses ``firstresult=True`` (see pluggy_) -- hooks + .. note:: This hook uses ``firstresult=True`` (see `pluggy first result only`_) -- hooks implementing this will be run until one returns non-``None``. .. _pip-accel: https://github.com/paylogic/pip-accel .. _pip-faster: https://github.com/Yelp/venv-update - .. _pluggy: http://pluggy.readthedocs.io/en/latest/#first-result-only """ @hookspec def tox_runtest_pre(venv): - """ [experimental] perform arbitrary action before running tests for this venv. + """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. """ @@ -90,16 +86,16 @@ def tox_runtest_pre(venv): @hookspec(firstresult=True) def tox_runtest(venv, redirect): - """ [experimental] run the tests for this venv. + """Run the tests for this venv. - .. note:: This hook uses ``firstresult=True`` (see pluggy_) -- hooks + .. note:: This hook uses ``firstresult=True`` (see `pluggy first result only`_) -- hooks implementing this will be run until one returns non-``None``. """ @hookspec def tox_runtest_post(venv): - """ [experimental] perform arbitrary action after running tests for this venv. + """Perform arbitrary action after running tests for this venv. This could be used to have per-venv test reporting of pass/fail status. """ @@ -107,7 +103,7 @@ def tox_runtest_post(venv): @hookspec(firstresult=True) def tox_runenvreport(venv, action): - """ [experimental] Get the installed packages and versions in this venv + """Get the installed packages and versions in this venv. This could be used for alternative (ie non-pip) package managers, this plugin should return a ``list`` of type ``str`` diff --git a/tox/interpreters.py b/tox/interpreters.py index c35b9bfa7..219678082 100644 --- a/tox/interpreters.py +++ b/tox/interpreters.py @@ -6,7 +6,7 @@ import py -from tox import hookimpl +import tox class Interpreters: @@ -126,13 +126,13 @@ def __str__(self): return "" % self.name -if sys.platform != "win32": - @hookimpl +if not tox.INFO.IS_WIN: + @tox.hookimpl def tox_get_python_executable(envconfig): return py.path.local.sysfind(envconfig.basepython) else: - @hookimpl + @tox.hookimpl def tox_get_python_executable(envconfig): name = envconfig.basepython p = py.path.local.sysfind(name) diff --git a/tox/result.py b/tox/result.py index 7aba9dc79..e84e85dc2 100644 --- a/tox/result.py +++ b/tox/result.py @@ -4,16 +4,18 @@ import py -from tox import __version__ as toxver +import tox class ResultLog: - - def __init__(self, dict=None): - if dict is None: - dict = {} - self.dict = dict - self.dict.update({"reportversion": "1", "toxversion": toxver}) + def __init__(self, data=None): + if not data: + self.dict = {} + elif isinstance(data, dict): + self.dict = data + else: + self.dict = json.loads(data) + self.dict.update({"reportversion": "1", "toxversion": tox.__version__}) self.dict["platform"] = sys.platform self.dict["host"] = socket.getfqdn() @@ -35,10 +37,6 @@ def get_envlog(self, name): 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): diff --git a/tox/session.py b/tox/session.py index 7f013e451..f4d762341 100644 --- a/tox/session.py +++ b/tox/session.py @@ -16,8 +16,7 @@ import py import tox -from tox._verlib import IrrationalVersionError -from tox._verlib import NormalizedVersion +from tox._verlib import NormalizedVersion, IrrationalVersionError from tox.config import parseconfig from tox.result import ResultLog from tox.venv import VirtualEnv @@ -34,7 +33,7 @@ def prepare(args): return config -def run_main(args=None): +def cmdline(args=None): if args is None: args = sys.argv[1:] main(args) @@ -143,7 +142,7 @@ def popen(self, args, cwd=None, env=None, redirect=True, returnout=False, ignore elif returnout: stdout = subprocess.PIPE if cwd is None: - # XXX cwd = self.session.config.cwd + # FIXME XXX cwd = self.session.config.cwd cwd = py.path.local() try: popen = self._popen(args, cwd, env=env, @@ -221,26 +220,21 @@ def popen(self, args, cwd=None, env=None, redirect=True, returnout=False, ignore def _rewriteargs(self, cwd, args): newargs = [] for arg in args: - if sys.platform != "win32" and isinstance(arg, py.path.local): + if not tox.INFO.IS_WIN 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": + # subprocess does not always take kindly to .py scripts so adding the interpreter here + if tox.INFO.IS_WIN: 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) + return self.session.popen(self._rewriteargs(cwd, args), shell=False, cwd=str(cwd), + universal_newlines=True, stdout=stdout, stderr=stderr, env=env) class Verbosity(object): @@ -348,8 +342,7 @@ def verbosity2(self, msg, **opts): class Session: - """ (unstable API). the session object that ties - together configuration, reporting, venv creation, testing. """ + """The session object that ties together configuration, reporting, venv creation, testing.""" def __init__(self, config, popen=subprocess.Popen, Report=Reporter): self.config = config diff --git a/tox/venv.py b/tox/venv.py index c8e205251..1d52e5216 100755 --- a/tox/venv.py +++ b/tox/venv.py @@ -8,7 +8,6 @@ import tox from .config import DepConfig -from .config import hookimpl class CreationConfig: @@ -137,7 +136,7 @@ def _check_external_allowed_and_warn(self, path): def is_allowed_external(self, p): tryadd = [""] - if sys.platform == "win32": + if tox.INFO.IS_WIN: tryadd += [ os.path.normcase(x) for x in os.environ['PATHEXT'].split(os.pathsep) @@ -416,52 +415,41 @@ def getdigest(path): return path.computehash() -@hookimpl +@tox.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') if venv.envconfig.alwayscopy: args.append('--always-copy') - # add interpreter explicitly, to prevent using - # default (virtualenv.ini) + # add interpreter explicitly, to prevent using default (virtualenv.ini) args.extend(['--python', str(config_interpreter)]) - # if sys.platform == "win32": - # f, path, _ = 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) - # Return non-None to indicate the plugin has completed - return True + return True # Return non-None to indicate plugin has completed -@hookimpl +@tox.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) - # Return non-None to indicate the plugin has completed - return True + return True # Return non-None to indicate plugin has completed -@hookimpl +@tox.hookimpl def tox_runtest(venv, redirect): venv.test(redirect=redirect) - # Return non-None to indicate the plugin has completed - return True + return True # Return non-None to indicate plugin has completed -@hookimpl +@tox.hookimpl def tox_runenvreport(venv, action): # write out version dependency information args = venv.envconfig.list_dependencies_command @@ -471,5 +459,4 @@ def tox_runenvreport(venv, action): # the output contains a mime-header, skip it output = output.split("\n\n")[-1] packages = output.strip().split("\n") - # Return non-None to indicate the plugin has completed - return packages + return packages # Return non-None to indicate plugin has completed