From ff7143e7c991b1a80e7ec1ea6836ef3a21b5a812 Mon Sep 17 00:00:00 2001 From: Oliver Bestwalter Date: Mon, 30 Apr 2018 14:44:41 +0200 Subject: [PATCH] Spring cleaning (#788) closes #797 closes #798 closes #799 closes #800 closes #801 closes #754 Also (no extra issues for that): removed all experimental markers from docstrings (most of them have been there for a few years already). If they turn out wrong/bad, we just have to deprecate them and remove them in a major release hookimpl is instantiated twice - mark one of them as deprecated and get rid of it later moved hookimpl to __init__.py - having a hookimpl object in the hookspecs module is unnecessary and confusing. Access to hookimpl should happen as module attribute of tox (clearer where it comes from) - also kept a deprecated reference there for compatibilty. removed some redundant rst references in hookspecs. If they are initialized once in any namespace they are defined globally (I don't like it either but that is how it works in restructuredText and ignoring that and defining the same things several times is confusing) move _dummy object to only class where it is needed and name it like the constant that it is use six for py2/3 compatibility where noticed it and where it is cheap to do remove dead code move exception related code into its own module rather than having it hang around in a class looking like a module in __init__.py there is no need to have an intermediate run_main - that is used everywhere only under the name tox.cmdline rename function and use it directly, just like client code is using it add a fixture for working in a clean tmpdir --- .pre-commit-config.yaml | 5 - CHANGELOG.rst | 4 +- HOWTORELEASE.rst | 1 - MANIFEST.in | 3 + changelog/754.misc.rst | 1 + changelog/797.doc.rst | 1 + changelog/798.feature.rst | 1 + changelog/799.doc.rst | 1 + changelog/800.misc.rst | 1 + changelog/801.misc.rst | 1 + changelog/examples.rst | 12 +- doc/example/basic.rst | 6 +- doc/example/pytest.rst | 2 - doc/example/unittest.rst | 8 +- doc/index.rst | 16 +- doc/links.rst | 30 +- doc/plugins.rst | 190 ++++++-- setup.py | 6 +- tests/conftest.py | 3 + tests/test_config.py | 221 +++++----- tests/test_interpreters.py | 47 +- tests/test_pytest_plugins.py | 3 +- tests/test_quickstart.py | 808 ++++++++++------------------------- tests/test_result.py | 47 +- tests/test_venv.py | 81 ++-- tests/test_z_cmdline.py | 53 +-- tox.ini | 9 +- tox/__init__.py | 105 ++--- tox/__main__.py | 4 +- tox/_pytestplugin.py | 89 ++-- tox/_quickstart.py | 316 ++++++-------- tox/_verlib.py | 5 +- tox/config.py | 131 +++--- tox/constants.py | 60 +++ tox/exception.py | 85 ++++ tox/hookspecs.py | 42 +- tox/interpreters.py | 8 +- tox/result.py | 20 +- tox/session.py | 25 +- tox/venv.py | 33 +- 40 files changed, 1087 insertions(+), 1397 deletions(-) create mode 100644 changelog/754.misc.rst create mode 100644 changelog/797.doc.rst create mode 100644 changelog/798.feature.rst create mode 100644 changelog/799.doc.rst create mode 100644 changelog/800.misc.rst create mode 100644 changelog/801.misc.rst create mode 100644 tox/constants.py create mode 100644 tox/exception.py 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