././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9115124 tox-3.21.4/0000755000175100001710000000000000000000000012701 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.899511 tox-3.21.4/.github/0000755000175100001710000000000000000000000014241 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/.github/CODEOWNERS0000644000175100001710000000005400000000000015633 0ustar00vstsdocker00000000000000* @gaborbernat @asottile @obestwalter ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.899511 tox-3.21.4/.github/ISSUE_TEMPLATE/0000755000175100001710000000000000000000000016424 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/.github/ISSUE_TEMPLATE/bug_report.md0000644000175100001710000000050100000000000021112 0ustar00vstsdocker00000000000000--- name: Bug report about: Something that does not works as expected title: "" labels: bug:normal assignees: '' --- When submitting a bug make sure you can reproduce it via ``tox -rvv`` and attach the output of that to the bug. Ideally, you should also submit a project that allows easily reproducing the bug. Thanks! ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/.github/ISSUE_TEMPLATE/feature_request.md0000644000175100001710000000030100000000000022143 0ustar00vstsdocker00000000000000--- name: Feature request about: Suggest an improvement for the project title: "" labels: feature:new assignees: '' --- Describe what improvement you want and how would this be used. Thanks! ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/.github/PULL_REQUEST_TEMPLATE.md0000644000175100001710000000230000000000000020035 0ustar00vstsdocker00000000000000## Thanks for contributing a pull request! If you are contributing for the first time or provide a trivial fix don't worry too much about the checklist - we will help you get started. ## Contribution checklist: (also see [CONTRIBUTING.rst](../tree/master/CONTRIBUTING.rst) for details) - [ ] wrote descriptive pull request text - [ ] added/updated test(s) - [ ] updated/extended the documentation - [ ] added relevant [issue keyword](https://help.github.com/articles/closing-issues-using-keywords/) in message body - [ ] added news fragment in [changelog folder](../tree/master/docs/changelog) * fragment name: `..rst` for example (588.bugfix.rst) * `` is must be one of `bugfix`, `feature`, `deprecation`,`breaking`, `doc`, `misc` * if PR has no issue: consider creating one first or change it to the PR number after creating the PR * "sign" fragment with "by :user:``" * please use full sentences with correct case and punctuation, for example: "Fix issue with non-ascii contents in doctest text files - by :user:`superuser`." * also see [examples](../tree/master/docs/changelog) - [ ] added yourself to `CONTRIBUTORS` (preserving alphabetical order) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/.github/config.yml0000644000175100001710000000010400000000000016224 0ustar00vstsdocker00000000000000chronographer: enforce_name: suffix: .rst rtd: project: tox ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/.gitignore0000644000175100001710000000041300000000000014667 0ustar00vstsdocker00000000000000# python *.pyc *.pyo *.swp __pycache__ # packaging folders /src/tox/version.py /build/ /dist/ /src/tox.egg-info # tox working folder /.tox # IDEs /.idea /.vscode # tools /.*_cache # documentation /docs/_draft.rst # release credentials.json pip-wheel-metadata ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/.pre-commit-config.yaml0000644000175100001710000000340300000000000017162 0ustar00vstsdocker00000000000000default_language_version: python: python3 repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v3.4.0 hooks: - id: check-ast - id: check-builtin-literals - id: check-docstring-first - id: check-merge-conflict - id: check-yaml - id: check-toml - id: debug-statements - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/asottile/pyupgrade rev: v2.7.4 hooks: - id: pyupgrade - repo: https://github.com/PyCQA/isort rev: 5.7.0 hooks: - id: isort - repo: https://github.com/psf/black rev: 20.8b1 hooks: - id: black args: - --safe language_version: python3.8 - repo: https://github.com/asottile/blacken-docs rev: v1.9.1 hooks: - id: blacken-docs additional_dependencies: - black==20.8b1 language_version: python3.8 - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.7.0 hooks: - id: rst-backticks - repo: https://github.com/asottile/setup-cfg-fmt rev: v1.16.0 hooks: - id: setup-cfg-fmt args: - --min-py3-version - "3.4" - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.4 hooks: - id: flake8 additional_dependencies: - flake8-bugbear == 20.11.1 language_version: python3.8 - repo: local hooks: - id: changelogs-rst name: changelog filenames language: fail entry: >- changelog files must be named ####.(bugfix|feature|deprecation|breaking|doc|misc).rst exclude: >- ^docs/changelog/(\d+\.(bugfix|feature|deprecation|breaking|doc|misc).rst|README.rst|template.jinja2) files: ^docs/changelog/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/CODE_OF_CONDUCT.md0000644000175100001710000000626400000000000015510 0ustar00vstsdocker00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at tox-dev@python.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html][version] [homepage]: https://www.contributor-covenant.org/ [version]: https://www.contributor-covenant.org/version/1/4/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/CONTRIBUTING.rst0000644000175100001710000001753600000000000015356 0ustar00vstsdocker00000000000000Contribution getting started ============================ Contributions are highly welcomed and appreciated. Every little help counts, so do not hesitate! If you like tox, also share some love on Twitter or in your blog posts. .. contents:: Contribution links :depth: 2 .. _submitfeedback: Feature requests and feedback ----------------------------- We'd also like to hear about your propositions and suggestions. Feel free to `submit them as issues `_ and: * Explain in detail how they should work. * Keep the scope as narrow as possible. This will make it easier to implement. .. _reportbugs: Report bugs ----------- Report bugs for tox in the `issue tracker `_. If you are reporting a bug, please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting, specifically the Python interpreter version, installed libraries, and tox version. * Detailed steps to reproduce the bug, or - even better, an xfailing test reproduces the bug If you can write a demonstration test that currently fails but should pass (xfail), that is a very useful commit to make as well, even if you cannot fix the bug itself (e.g. something like this in `test_config `_). .. _fixbugs: Fix bugs -------- Look through the GitHub issues for bugs. Here is a filter you can use: https://github.com/tox-dev/tox/labels/bug:normal Don't forget to check the issue trackers of your favourite plugins, too! .. _writeplugins: Implement features ------------------ Look through the GitHub issues for enhancements. Here is a filter you can use: https://github.com/tox-dev/tox/labels/feature:new Write documentation ------------------- tox could always use more documentation. What exactly is needed? * More complementary documentation. Have you perhaps found something unclear? * Docstrings. There can never be too many of them. * Blog posts, articles and such -- they're all very appreciated. You can also edit documentation files directly in the GitHub web interface, without using a local copy. This can be convenient for small fixes. .. note:: Build the documentation locally with the following command: .. code:: bash $ tox -e docs The built documentation should be available in the ``.tox/docs_out/``. .. _submitplugin: .. _`pull requests`: .. _pull-requests: Preparing Pull Requests ----------------------- Short version ^^^^^^^^^^^^^ #. `Fork the repository `_. #. Make your changes. #. open a `pull request `_ targeting the ``master`` branch. #. Follow **PEP-8**. There's a ``tox`` command to help fixing it: ``tox -e fix_lint``. You can also add a pre commit hook to your local clone to run the style checks and fixes (see hint after running ``tox -e fix_lint``) #. Tests for tox are (obviously) run using ``tox``:: tox -e fix_lint,py27,py36 The test environments above are usually enough to cover most cases locally. #. Consider the `checklist `_ in the pull request form Long version ^^^^^^^^^^^^ What is a "pull request"? It informs the project's core developers about the changes you want to review and merge. Pull requests are stored on `GitHub servers `_. Once you send a pull request, we can discuss its potential modifications and even add more commits to it later on. There's an excellent tutorial on how Pull Requests work in the `GitHub Help Center `_. Here is a simple overview, with tox-specific bits: #. Fork the `tox GitHub repository `__. It's fine to use ``tox`` as your fork repository name because it will live under your user. #. Clone your fork locally using `git `_ and create a branch:: $ git clone git@github.com:YOUR_GITHUB_USERNAME/tox.git $ cd tox # now, to fix a bug create your own branch off "master": $ git checkout -b your-bugfix-branch-name master # or to instead add a feature create your own branch off "features": $ git checkout -b your-feature-branch-name features If you need some help with Git, follow this quick start guide: https://git.wiki.kernel.org/index.php/QuickStart #. Install tox Of course tox is used to run all the tests of itself:: $ cd $ pip install [-e] . #. Run all the tests You need to have Python 2.7 and 3.6 available in your system. Now running tests is as simple as issuing this command:: $ tox -e fix_lint,py27,py36 This command will run tests via the "tox" tool against Python 2.7 and 3.6 and also perform style checks with some automatic fixes. #. You can now edit your local working copy. Please follow PEP-8. You can now make the changes you want and run the tests again as necessary. $ tox -e py27 -- --pdb Or to only run tests in a particular test module on Python 3.6:: $ tox -e py36 -- testing/test_config.py You can also use the dev environment: $ tox -e dev To get information about all environments, type: $ tox -av #. Commit and push once your tests pass and you are happy with your change(s):: $ git commit -a -m "" $ git push -u #. submit a pull request through the GitHub website and and consider the `checklist `_ in the pull request form:: head-fork: YOUR_GITHUB_USERNAME/tox compare: your-branch-name base-fork: tox-dev/tox base: master Submitting plugins to tox-dev ----------------------------- tox development of the core, some plugins and support code happens in repositories living under the ``tox-dev`` organisation: - `tox-dev on GitHub `_ All tox-dev team members have write access to all contained repositories. tox core and plugins are generally developed using `pull requests`_ to respective repositories. The objectives of the ``tox-dev`` organisation are: * Having a central location for popular tox plugins * Sharing some of the maintenance responsibility (in case a maintainer no longer wishes to maintain a plugin) You can submit your plugin by opening an `issue `_ requesting to add you as a member of tox-dev to be able to integrate the plugin. As a member of the or you can then transfer the plugin yourself. The plugin must have the following: - PyPI presence with a ``setup.py`` that contains a license, ``tox-`` prefixed name, version number, authors, short and long description. - a ``tox.ini`` for running tests using `tox `_. - a ``README`` describing how to use the plugin and on which platforms it runs. - a ``LICENSE`` file or equivalent containing the licensing information, with matching info in ``setup.py``. - an issue tracker for bug reports and enhancement requests. - a `changelog `_ If no contributor strongly objects, the repository can then be transferred to the ``tox-dev`` organisation. For details see `about repository transfers `_ Members of the tox organization have write access to all projects. We recommend that each plugin has at least three people who have the right to release to PyPI. Repository owners can rest assured that no ``tox-dev`` administrator will ever make releases of your repository or take ownership in any way, except in rare cases where someone becomes unresponsive after months of contact attempts. As stated, the objective is to share maintenance and avoid "plugin-abandon". ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/CONTRIBUTORS0000644000175100001710000000325200000000000014563 0ustar00vstsdocker00000000000000Albin Vass Alex Grönholm Alexander Loechel Alexander Schepanovski Alexandre Conrad Allan Feldman Andrii Soldatenko Andrzej Klajnert Anthon van der Neuth Anthony Sottile Anudit Nagar Ashley Whetter Asmund Grammeltwedt Barney Gale Barry Warsaw Bartolome Sanchez Salado Bastien Vallet Benoit Pierre Bernat Gabor Brett Langdon Bruno Oliveira Carl Meyer Charles Brunet Chris Jerdonek Chris Rose Clark Boylan Cyril Roelandt Dane Hillard David Staheli David Diaz Ederag Eli Collins Eugene Yunak Fernando L. Pereira Florian Bruhin Florian Preinstorfer Florian Schulze George Alton Gleb Nikonorov Gonéri Le Bouder Hazal Ozturk Henk-Jaap Wagenaar Ian Stapleton Cordasco Igor Duarte Cardoso Ilya Kulakov Ionel Maries Cristian Itxaka Serrano Jake Windle Jannis Leidel Joachim Brandon LeBlanc Johannes Christ John Mark Vandenberg Jon Dufresne Josh Smeaton Josh Snyder Joshua Hesketh Julian Krause Jürgen Gmach Jurko Gospodnetić Krisztian Fekete Laszlo Vasko Lukasz Balcerzak Lukasz Rogalski Manuel Jacob Marc Abramowitz Marc Schlaich Marius Gedminas Mariusz Rusiniak Mark Hirota Matt Good Matt Jeffery Matthew Kenigsberg Mattieu Agopian Mauricio Villegas Mehdi Abaakouk Michael Manganiello Mickaël Schoentgen Mikhail Kyshtymov Miro Hrončok Monty Taylor Morgan Fainberg Naveen S R Nick Douma Nick Prendergast Nicolas Vivet Oliver Bestwalter Pablo Galindo Paul Moore Paweł Adamczak Peter Kolbus Philip Thiem Pierre-Jean Campigotto Pierre-Luc Tessier Gagné Prakhar Gurunani Ronald Evers Ronny Pfannschmidt Selim Belhaouane Sorin Sbarnea Sridhar Ratnakumar Stephen Finucane Sviatoslav Sydorenko Thomas Grainger Tim Laurence Tyagraj Desigar Usama Sadiq Ville Skyttä Vlastimil Zíma Xander Johnson anatoly techtonik ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/HOWTORELEASE.rst0000644000175100001710000000463100000000000015340 0ustar00vstsdocker00000000000000================== How to release tox ================== This matches the current model that can be summarized as this: * tox has no long lived branches. * Pull requests get integrated into master by members of the project when they feel confident that this could be part of the next release. Small fix ups might be done right after merge instead of discussing back and forth to get minor problems fixed, to keep the workflow simple. **Normal releases**: done from master when enough changes have accumulated (whatever that means at any given point in time). **"Special" releases**: (in seldom cases when master has moved on and is not in a state where a quick release should be done from that state): the current release tag is checked out, the necessary fixes are cherry picked and a package with a patch release increase is built from that state. This is not very clean but seems good enough at the moment as it does not happen very often. If it does happen more often, this needs some rethinking (and rather into the direction of making less buggy releases than complicating release development/release process). HOWTO ===== Prerequisites ------------- * Push and merge rights for https://github.com/tox-dev/tox, also referred to as the *upstream*. * A UNIX system that has: - ``tox`` - ``git`` able to push to upstream * Accountability: if you cut a release that breaks the CI builds of projects using tox, you are expected to fix this within a reasonable time frame (hours/days - not weeks/months) - if you don't feel quite capable of doing this yet, partner up with a more experienced member of the team and make sure they got your back if things break. Release ------- Run the release command and make sure you pass in the desired release number: .. code-block:: bash tox -e release -- Create a pull request and wait until it the CI passes. Now make sure you merge the PR and delete the release branch. The CI will automatically pick the tag up and release it, wait to appear in PyPI. Only merge if the later happens. Post release activities ----------------------- Make sure to let the world know that a new version is out by whatever means you see fit. As a minimum, send out a mail notification by triggering the notify tox environment: .. code-block:: bash TOX_DEV_GOOGLE_SECRET=our_secret tox -e notify Note you'll need the ``TOX_DEV_GOOGLE_SECRET`` key, what you can acquire from other maintainers. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/LICENSE0000644000175100001710000000200000000000000013676 0ustar00vstsdocker00000000000000 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/MANIFEST.in0000644000175100001710000000027300000000000014441 0ustar00vstsdocker00000000000000include CHANGELOG.rst include README.rst include CONTRIBUTORS include LICENSE include setup.py include tox.ini graft docs graft tests global-exclude __pycache__ global-exclude *.py[cod] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9115124 tox-3.21.4/PKG-INFO0000644000175100001710000001574300000000000014010 0ustar00vstsdocker00000000000000Metadata-Version: 2.1 Name: tox Version: 3.21.4 Summary: tox is a generic virtualenv management and test command line tool Home-page: http://tox.readthedocs.org Author: Holger Krekel, Oliver Bestwalter, Bernát Gábor and others Maintainer: Bernat Gabor, Oliver Bestwalter, Anthony Asottile Maintainer-email: tox-dev@python.org License: MIT Project-URL: Source, https://github.com/tox-dev/tox Project-URL: Tracker, https://github.com/tox-dev/tox/issues Description: ![PyPI](https://img.shields.io/pypi/v/tox?style=flat-square) [![Supported Python versions](https://img.shields.io/pypi/pyversions/tox.svg)](https://pypi.org/project/tox/) [![Azure Pipelines build status](https://dev.azure.com/toxdev/tox/_apis/build/status/tox%20ci?branchName=master)](https://dev.azure.com/toxdev/tox/_build/latest?definitionId=9&branchName=master) [![Documentation status](https://readthedocs.org/projects/tox/badge/?version=latest&style=flat-square)](https://tox.readthedocs.io/en/latest/?badge=latest) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Downloads](https://pepy.tech/badge/tox/month)](https://pepy.tech/project/tox/month) tox logo # tox automation project **Command line driven CI frontend and development task automation tool** At its core tox provides a convenient way to run arbitrary commands in isolated environments to serve as a single entry point for build, test and release activities. tox is highly [configurable](https://tox.readthedocs.io/en/latest/config.html) and [pluggable](https://tox.readthedocs.io/en/latest/plugins.html). ## Example: run tests with Python 3.7 and Python 3.8 tox is mainly used as a command line tool and needs a `tox.ini` or a `tool.tox` section in `pyproject.toml` containing the configuration. To test a simple project that has some tests, here is an example with a `tox.ini` in the root of the project: ```{.sourceCode .ini} [tox] envlist = py37,py38 [testenv] deps = pytest commands = pytest ``` ```{.sourceCode .console} $ tox [lots of output from what tox does] [lots of output from commands that were run] __________________ summary _________________ py37: commands succeeded py38: commands succeeded congratulations :) ``` tox created two `testenvs` - one based on Python3.7 and one based on Python3.8, it installed pytest in them and ran the tests. The report at the end summarizes which `testenvs` have failed and which have succeeded. **Note:** To learn more about what you can do with tox, have a look at [the collection of examples in the documentation](https://tox.readthedocs.io/en/latest/examples.html) or [existing projects using tox](https://github.com/search?l=INI&q=tox.ini+in%3Apath&type=Code). ### How it works tox creates virtual environments for all configured so called `testenvs`, it then installs the project and other necessary dependencies and runs the configured set of commands. See [system overview](https://tox.readthedocs.io/en/latest/#system-overview) for more details. tox flow ### tox can be used for ... - creating development environments - running static code analysis and test tools - automating package builds - running tests against the package build by tox - checking that packages install correctly with different Python versions/interpreters - unifying Continuous Integration and command line based testing - building and deploying project documentation - releasing a package to PyPI or any other platform - limit: your imagination ### Documentation Documentation for tox can be found at [Read The Docs](https://tox.readthedocs.org). ### Communication and questions For the fastest and interactive feedback please join our [![Discord](https://img.shields.io/discord/802911963368783933?style=flat-square)](https://discord.gg/edtj86wzBX) server. If you have questions or suggestions you can first check if they have already been answered or discussed on our [issue tracker](https://github.com/tox-dev/tox/issues?utf8=%E2%9C%93&q=is%3Aissue+sort%3Aupdated-desc+label%3A%22type%3Aquestion+%3Agrey_question%3A%22+). On [Stack Overflow (tagged with `tox`)](https://stackoverflow.com/questions/tagged/tox). ### Contributing Contributions are welcome. See [contributing](https://github.com/tox-dev/tox/blob/master/CONTRIBUTING.rst) and our [Contributor Covenant Code of Conduct](https://github.com/tox-dev/tox/blob/master/CODE_OF_CONDUCT.md). Currently the [code](https://github.com/tox-dev/tox) and the [issues](https://github.com/tox-dev/tox/issues) are hosted on Github. The project is licensed under [MIT](https://github.com/tox-dev/tox/blob/master/LICENSE). Keywords: virtual,environments,isolated,testing Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: tox Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Utilities Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7 Description-Content-Type: text/markdown Provides-Extra: docs Provides-Extra: testing ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/README.md0000644000175100001710000001076200000000000014166 0ustar00vstsdocker00000000000000![PyPI](https://img.shields.io/pypi/v/tox?style=flat-square) [![Supported Python versions](https://img.shields.io/pypi/pyversions/tox.svg)](https://pypi.org/project/tox/) [![Azure Pipelines build status](https://dev.azure.com/toxdev/tox/_apis/build/status/tox%20ci?branchName=master)](https://dev.azure.com/toxdev/tox/_build/latest?definitionId=9&branchName=master) [![Documentation status](https://readthedocs.org/projects/tox/badge/?version=latest&style=flat-square)](https://tox.readthedocs.io/en/latest/?badge=latest) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Downloads](https://pepy.tech/badge/tox/month)](https://pepy.tech/project/tox/month) tox logo # tox automation project **Command line driven CI frontend and development task automation tool** At its core tox provides a convenient way to run arbitrary commands in isolated environments to serve as a single entry point for build, test and release activities. tox is highly [configurable](https://tox.readthedocs.io/en/latest/config.html) and [pluggable](https://tox.readthedocs.io/en/latest/plugins.html). ## Example: run tests with Python 3.7 and Python 3.8 tox is mainly used as a command line tool and needs a `tox.ini` or a `tool.tox` section in `pyproject.toml` containing the configuration. To test a simple project that has some tests, here is an example with a `tox.ini` in the root of the project: ```{.sourceCode .ini} [tox] envlist = py37,py38 [testenv] deps = pytest commands = pytest ``` ```{.sourceCode .console} $ tox [lots of output from what tox does] [lots of output from commands that were run] __________________ summary _________________ py37: commands succeeded py38: commands succeeded congratulations :) ``` tox created two `testenvs` - one based on Python3.7 and one based on Python3.8, it installed pytest in them and ran the tests. The report at the end summarizes which `testenvs` have failed and which have succeeded. **Note:** To learn more about what you can do with tox, have a look at [the collection of examples in the documentation](https://tox.readthedocs.io/en/latest/examples.html) or [existing projects using tox](https://github.com/search?l=INI&q=tox.ini+in%3Apath&type=Code). ### How it works tox creates virtual environments for all configured so called `testenvs`, it then installs the project and other necessary dependencies and runs the configured set of commands. See [system overview](https://tox.readthedocs.io/en/latest/#system-overview) for more details. tox flow ### tox can be used for ... - creating development environments - running static code analysis and test tools - automating package builds - running tests against the package build by tox - checking that packages install correctly with different Python versions/interpreters - unifying Continuous Integration and command line based testing - building and deploying project documentation - releasing a package to PyPI or any other platform - limit: your imagination ### Documentation Documentation for tox can be found at [Read The Docs](https://tox.readthedocs.org). ### Communication and questions For the fastest and interactive feedback please join our [![Discord](https://img.shields.io/discord/802911963368783933?style=flat-square)](https://discord.gg/edtj86wzBX) server. If you have questions or suggestions you can first check if they have already been answered or discussed on our [issue tracker](https://github.com/tox-dev/tox/issues?utf8=%E2%9C%93&q=is%3Aissue+sort%3Aupdated-desc+label%3A%22type%3Aquestion+%3Agrey_question%3A%22+). On [Stack Overflow (tagged with `tox`)](https://stackoverflow.com/questions/tagged/tox). ### Contributing Contributions are welcome. See [contributing](https://github.com/tox-dev/tox/blob/master/CONTRIBUTING.rst) and our [Contributor Covenant Code of Conduct](https://github.com/tox-dev/tox/blob/master/CODE_OF_CONDUCT.md). Currently the [code](https://github.com/tox-dev/tox) and the [issues](https://github.com/tox-dev/tox/issues) are hosted on Github. The project is licensed under [MIT](https://github.com/tox-dev/tox/blob/master/LICENSE). ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/azure-pipelines.yml0000644000175100001710000000341200000000000016540 0ustar00vstsdocker00000000000000name: $(BuildDefinitionName)_$(Date:yyyyMMdd)$(Rev:.rr) resources: repositories: - repository: tox type: github endpoint: toxdevorg name: tox-dev/azure-pipelines-template ref: master trigger: batch: true branches: include: - master - rewrite - refs/tags/* pr: branches: include: - master - rewrite variables: PYTEST_ADDOPTS: "-v -v -ra --showlocals" PYTEST_XDIST_PROC_NR: '0' PRE_COMMIT_HOME: $(Pipeline.Workspace)/pre-commit-cache jobs: - template: run-tox-env.yml@tox parameters: tox_version: '' jobs: fix_lint: before: - task: CacheBeta@0 displayName: cache pre-commit inputs: key: pre-commit | .pre-commit-config.yaml path: $(PRE_COMMIT_HOME) docs: null py38: image: [linux, windows, macOs] py27: image: [linux, windows, macOs] pypy: image: [linux] py37: image: [linux, windows, macOs] py36: image: [linux, windows, macOs] py35: image: [linux, windows, macOs] pypy3: image: [linux] dev: null package_description: null coverage: with_toxenv: 'coverage' # generate .tox/.coverage, .tox/coverage.xml after test run for_envs: [py38, py37, py36, py35, py27, pypy3, pypy] before: - task: UsePythonVersion@0 condition: and(succeeded(), in(variables['TOXENV'], 'pypy')) displayName: provision pypy 3 inputs: versionSpec: 'pypy3' - ${{ if startsWith(variables['Build.SourceBranch'], 'refs/tags/') }}: - template: publish-pypi.yml@tox parameters: external_feed: 'toxdev' pypi_remote: 'pypi-toxdev' dependsOn: [fix_lint, docs, package_description, dev, report_coverage] ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.899511 tox-3.21.4/docs/0000755000175100001710000000000000000000000013631 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.899511 tox-3.21.4/docs/_static/0000755000175100001710000000000000000000000015257 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/_static/custom.css0000644000175100001710000000142000000000000017300 0ustar00vstsdocker00000000000000div.document { width: 100%; max-width: 1520px; } div.body { max-width: 1280px; } div.body p, ol > li, div.body td { text-align: justify; } img, div.figure { margin: 0 !important } ul > li { text-align: justify; } ul > li > p { margin-bottom: 0; } ol > li > p { margin-bottom: 0; } div.body code.descclassname { display: none } .wy-table-responsive table td { white-space: normal !important; } .wy-table-responsive { overflow: visible !important; } div.toctree-wrapper.compound > ul > li { margin: 0; padding: 0 } code.docutils.literal { background-color: #ECF0F3; padding: 0 1px; } div#changelog-history h3{ margin-top: 10px; } div#changelog-history h2{ font-style: italic; font-weight: bold; } ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.899511 tox-3.21.4/docs/_static/img/0000755000175100001710000000000000000000000016033 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/_static/img/tox.png0000644000175100001710000015014400000000000017360 0ustar00vstsdocker00000000000000PNG  IHDR@]sBIT|d pHYs$tEXtSoftwarewww.inkscape.org< IDATxy]u}眻Κ,`պT m*b]~*"*ֶƶ֥jQZDD\R-Ha K2d2d{8 e{s_>̽J24IӒ9 M M$ pEj@-tH.ݾ*'&\eIHա :=$tVͼ\ҒV$ݨe3oIzlktIk՘lIGw eپIǶ0I4e3owfIWk}fWQGw ǖIzwJzq s(̀{s$.9DaOgn/k' <񒮔9D ~"ءnMt6ێ|ZҐ`s _ %Tk>% )TIUre;YCaI:S[$z(f]YҟJ:]I2q 4ISvt`ȟU#EVn  WFҨXnv 3c%^ҩ9//`(̐'Lsud$$%%˕zf" 3dQ(IjII/ 3 YH:N)oL0`(̐f3KUV9$9n=a_uwt+)Q!q} (̐Vd' 3P0OfH+ 0`(̐Vf Caz$4BQSRJX<RR5 #2')\|HHo?c.nYXKb'jucb'i%Uc{Ɗ%m7熝Lñ@4l 8`ml|FfpHIӣW=URؤ?4Ѥ1 3dفQX˯{֢`z d˝''8'GWmkpg5"n3;v݁kYam)ǣ%U=d44(e}b#p4N ( m\^sgqdzef9w6:iI /ֈrPZ%+K Ca,0;0 3ru'v=\#$;¤##$[ިLj6}-U9YQy0q=64TnJ63MKmܭ٭qxE`3H^IO˱gKjW f 0uY1fv%]i~:45du$oLACV3\S/~ͫW=dFO ^%J3@Gav`f@tVwǚ8qfL'zۊܹe15i[FvVWqH'9\?}ňT E%{o+J5oZjIӾ9SEI-{,c(ihாS.cK"յT29I?HTh,*Wb'rp\cMvLMFgyM&wml;uo|#m”`J#4%/n%}C*Q^;D8M 3d+f@]*,w8v/rLzn+WLb>خ ǟ#J†5k?;ray.IuAx DIwƢ0CPW]uiE':g/Ӌd͝A6;{䵬rnks f- bw=?aa}3ƎP74oxAR,(̐fFad؆ צN22KZͦoubhm:Li;Mzx!߾>?-$TIJz@aC<# ( Ș,d3zcuhV̙(ܾmfҰ6P^AOۛZbII- ǡQ(̀ XȾiW|NO\r]flZ߭3m֣jF<ˍcOà೒;!OPw0! 3dفQ)zg} fV^lhZl -#s4' ֬p0/gӧ\;8t BaC@V\y3Skte}%b+Ljot˹T`+Lv 3:rPo|Ӿ()ŎcPwK-C)AQ0C&Xaڵ 嗛I/q\%ں4uk15[1iHqhl2IFdԢx͚5;LY&iw4H@J#J `YQ! ( hݺ:CNYVԦ5y΂Kjtid۶EsxcI꾔Ug-dGt'Af9ڋ`j>*wҟKMwtq9B-*]?_g;P9v(_$テ)o,Ba#dtޟw^IJNRӼ"W+8+O2)S5KV̙zzmW%[&i3:kAϞ}ʡ_Hzh8t]9 'K:w M( )Wy=84uO펃7;%=Nd3Ԧ494XZ;laX"+s3`v*K}H-[G){Jې?+A9Jޜn 0o&%J?%UU`[gnC,9MC ퟨM)e#űZ= 8Ρm}sI8c2Y2擒-qHs*G,YQJz& I[8:]\R:/m6T\;B Wvצn9~\ Ud$}w풎WȫAcPZW )9}Xj!lKŎd]IT0۪Eet+LZapx{)s3q(r h'IzԆ9xUfhKׯ|9}\'Βq,ݾkT'|bZX>AaR*4dU^);@$,)+S,(l+)NVr 8! yʧ>.ً}gIl*𝣑&&[vsj39Sc0;O T|@]$mC b%徃Y IPrayU,f&7CɆ _~'Cs7P=D0uS%"@·s+[~Ioo>M$}wA!2`ָB#uK:Uһ%=s 8!A.`eqa M/gn#rypg?0~ߚ~羓$];ys}nT$XaCfhP2/b19:i l+LnۆsD,joƳg+T*I},g;H $m􀤭T2Rvq>?~r q:t5 8!s֮][J ȁ&~|y &iX0qȂw_p|"iġmlLJw`>)!%Ec}i%]69 r LҙJ.ȳ.IӾds+zr?9y`bnugErmsj]{XeD?: Bi! *9 Jcd˥N6HMMBayI*@qB,}[yBdYΓw,ҖavwLn2]8|ۚ5>שG)j0 $Iҗ}IJJnAI*ٝ4$$J9 Ю8! 󝕽%1WUY7K V r '6٨uy|w&{y@+$-{;LesII/SA~p<zI¬J:EIQ;~m2+_hɖ|gg[Qdm3 Ru߽3оkSݙTGEU-pնTdfvY\yœ>%߯V<Ǿ?J*i4A*<$$; Y{zw*9 'd>pBw鮿1ٻ۟eҖ&8(H.sn)S;G:ܤ,,dqոl{-5?$zgy1z%/&)W;) W~eӝ^|}2>Ɏ%&vhNAm¥fV{㻃[tZ2MNA Pq1 N \Ek'/?!'Hze0kn!жz%&$P|A{.%Jұ?NȜ:$uΒU.v[vףo2 ,(v+*3qYmŲ&fiŅPQeqw[}ɨI/UueJf"^/?}@[7@ٵ,%]{8!SW?<~IgΒu.mi ,ʙ{6Z9Io0 ˊ-ן5;H-U4FKZIz[}A[y`q!(g3(;'d^y3SD=w#[1D\(8\6rx{qhdgaisCVÜ LlAr$(ٶ5}F1 l?WOqdUr0h2X /;2Y.=w&AiY8 9ӓ*;JLJ IDATDOEarr2֎μk|'ɉ#%}H>M)iȽP% =I'4IezsWihb:9m J}fn^*]lw]-)vNʴ8zDOE&۝ůswy A𐿐w_K +II(̲d$,-Oxa({ƴ;:;]̛aJXB=S'Tj*-٨):m͹׮%g^(_%=whR<}A. J;Y,tKR'mb⍿be!:YWQoR޳*], lbfX\-T9L|Wj'Ҧv)+w ȴÔx/H:w@(k8h>!sUaO:}gɣ{.?s$[1M.XfZں*lZ9M#˴w(~jBͅ>n !Ӓ*I:AR9$mbANTrJfEa_O\bGKNRYhlwkӦJ&b 'Nဂ^X{2bɰi әnCWzrO zpגw`(ҋ ȿܞyG-.藷V'\l*~( b.Isfa=zʾdnڙ/_ T?LKz|A>fY7U%v*o1{eI(e٤C (ː fFҫzlhᮑBvfpY8:jRgA*^ vԹ߼G1r*.IT_@ݡ䤬Lʣȹ[NJՖ#{wG]fS8Bi <W|g%㕯Z*3r2%sعKzH3$ p!SAa~d~~٥WPftNY{4nŌ5ʐ ~wCZXEUN/Ծy)'P|6Ǘ%m$],)}˓%}w!} K>yxdv#cۊi lbXwp&΁p8:3cJfl~W'BJJg@R7 Aa~dveWL sq\wSfeAU X! 2}]|g8KAMs&Ζt%b@/t 2l+ȘY wn:|)긅3}iJ)6Rٟ=֬^ƫIz!A@d; $wm@nIw4;欮 .Qg>E])Y,`Ϟ`dϞpe`Ob MxAha1j/P3ްޛB o%*JAS%-'+BA9-دV1vEھ#D{$}wW|"}ǙV\ys״,V͊rr\9\ܹ8;ŝ{WaF](mC$ h*Y͙eKq"&rfWrm8;&bw(ZdӦNeпbT(, f;r$;Mr\|jIrݒN{s]A%=[Ҵ,h7)e]^-2 Yv-kp7ߒ鱾]#ȘV(uƲYqU(l1'{O}%_~,93&I?TݑJe~w4$}wtuƙv%H%idwKĖCaK&fK:N]DvstdjVQĸqE8P/]|ٷ|'ɡIzm.9nMH?d%(:Af4IQҝJzKY#djy eYkݽeMP[]2I8VyG ^KJ/*Wrm-03[Ҭ-fՒ)\WdfX5Wrb\X@nQ$t[dsfJ)ݭpȟgK a%bG{9{_X@Ood@)_\3TW`ȬNش#Ɋ;U(vAyM} L=N1p`qNTOD\W_’Ϳ'r)m/,|he%[39h!?>')C,$YɖL (tܳėT,5.Z2P+l-%Ӽ O6w+ +Ĉ&>NuuX=&f*22=5Ʈ:@K9=WڷqbIoQr y qf.%Ô @+pox(/~9W)lyohaIʳ}6g+Ճj%`i"W*" 'k N=CEq8\a/*IC`A#B!`XK%  nXY 4esZ>;sg5x+] X!3gb2[ITYϖhѮzS]u@.+]UwwI}hJ;%2O5I$;J\D@tuΎkqu_ytEa;wcACba c\z|g;gV\uhumu ֪~fNO=^8UҠm;归^;<9II @Q@ Ǿ%AMaN-ϖ(oѰxv fPFs"+EխxQRⲦ";mzz<tm%=wQw%〶Cam_˳}e2P=(T,(זOΐNcw\D?YwqPY.YT:~[1rdTIMɩH%wy[ɒxND[06S<_M: %vQ;|gH8'j28 OŪű A =%GY!"IZ* a%Fam?{KlIgA5_;w&s*MAiPAG)WAzΑN*ɸf=XMqn!YO?;K_ҭC?w^/@,9O~;DxW%}E7$@P@.\NhQG-_TW"YCH:EB*uu'Tr-󝥕T)Y(tt##Qkף Az͂M1wrf+]x:Ε: eV-\9N;O>7f߱rbݒ^;HuKZ,JAJZ͉[ҋ$N`Xa9V;?3V-+ڶjُ<045,@A*]PX!3S;.0NRd5׽u*Zk:^TձT. eH(֌ShyKr\I*%C>I콶ޥmiY{sz~[*lmͥn4a \Jv"qqP\uhum;Va;تFw7mjzVLAe E[kCeUj{pO8,7EfY>;zvY@o{9OoeΔC'JOJV"8 3ș[6{sz 48tE%kyV[$L8C.Vs3Ru>7É NES/4VaX`G’C\fժU8ot9Hw%h9  . GnxpeGdXDdތl̹g~l n2eaq1)纃bw䤸Κ&kSU?Z &sߞ=ٙȈS2FUY/~uSzA4s@DD)??ϱs}rg$5[kΥOkS bT1ɸ%Pq˻vߵWa7iPcՂ5&Ed rP`wF-Rv3U3 Zêh jP/.Va/VC)PKC wđ?a>wx4Gtz6!N'bѹƔj0;;cG8{.59LB!KKCaNfݑ.94< 'P^Ñ.FՂ}&xqk-tJ" [A57uL#1?: 2r 1΅fDD3#<7*@ Kb`՞d@iv;\3iq[vm?bgtH\/u9CX3_RX UªP*&V<6¸(b b<# GyO[**fC[܉XXioG<>u~u{= 3{1[Ų|XFtn#"QØ71/eoZU.>`=7Ѕt*X ("(O"R3%#X}llxlz9 rd#3빜Åpb~U ?l%5foiznǝgoCd\]3Yqg:A ,ͨ #Nz\0N@PUEy,. oπ;6<1 Dq&bkޅJ D~;^~: Ed3 ]?b‚ 't^+捨꒟Ǭ.ݴYa5PBzKAy94.\,%u|Q\JLsw1~C\oP!&b߻B%,C܁ IDAT͠ Xn ria(7{fuK3gUcN`AZ.);\sn@O&+,fK\8\ ̈fGTE '¤2͛ÃpljAuuICk.0_^KrK Ykup&ADǂ =y2;n lT+"%b2~auW*0%pd&~b?4s΁a*l[oZ v;y3f~sgw'!2uxsbBx7u"J fDD3C of2qG*N|땲ؔ =h,pr[!pV62彚zX vĭq1z3C8nЍ¸ Ӯe fDD3@"F]vaFqd7V$]vK({5ۋ[֚v҅Gmt|cr!q"j:cop|sh`FD4>vc-<+dǙ?Fq"y@~W&G_q˃6ndN~g u~߻q C8pbB\ |C'"J蝞?<ꆫN@[R-CyvZ.q,:^1}q5iy\z6ʋsE9CdCA2g1e+~M~uy‚Q={u<Z؏x*(etq /'瘪Hq(bq"_H߹ywܝLw#M 1eo:#׺A4oX0#"J蝞@c@*O-ii: 8Kk-Yk*IP);&`wa[t~JAMcdO3!2|S4vDSǂQv'ϋ9߮x8ciAn_#YӥEݎ4Vwaˆj1fWZǩ ~jB;ݎe15˝Ҵ9 ӮCL:#̈RMB )WK&hU;vfV"xAPސx-`}qM.M2kŌ?Y\E 5Cd.uǤ+:Ѽb(><AKb&u,Va) `6*4sJ6{L}sd7W6-UgbkކJuWNa]ȸx q D3",/*t:"ܽ4)^(ܲKouYE&ݯTgU?w#~]ȸWC|'80[DDsࣟzEyW f׉Qne'5Z\НsN\+n1fƒ5t]gI]85@wq Cdx? $Co<eD,U{ΗcE<Ұ#U;YT g7D \#EryiΒVVNgytG?sd!oyJ>q g]8g| M",_~#Ԫ_,spUjvy7H ,K3fc!۬Prfȯn|`[ˮ՗q|J]u30~ 7 BDb(E _ԕ.(LSYQE1.ȾgIeL[ ik , ^ޯ+<ŷq12>:Dw͜ǯ6Q:7 D@,/rq^VpFji}][Xe9fYXh$Y\+ uv% w;j:G+:Gu<;tP PK' \ `FD_:}WNv?Q:ܿ͢&g٠h+PI|ةH5{u(gyy s~u{ ƅ+x0g?: fDD)oUϗ 5[\X QYUc&DllD[F^Q\t#v'>)|1+!2ov9&ʢ@!"JTM5iQviC,b|uSS`y %Ϙ(X5R$jfؙJE6fhsS3̙?x %ivN)sR<QYƂ#a+5@z\{@v#)^iGz^Xې̰:U\HRI6/t40\pIA:o.s"ÞΤ S`FD䈊r9Qļnvi9].ϰ:FeCof^$`}bH7 v]^q=yOS̙K3S{2); fs|./ux)FvETV1(a!X7ԆUʲmN8 e_)dWgTe:3-?9~&,qysł4r̫LvY/1 bP^e9ΣZز֏c ]X0ƶؤϵl: ^[scν) fsy;7ADchfZqlZҎ6:Ǵ (u ð:GP$KQAoc̹:D]S̿xD4,M? tb: fDD QU,P#-,Αzʆ^i"c…kvN?%IQǺA^`uJkD.,%,F:GY6]VeiG Q,uzQO|+!3 ҊJۈ\2U-3^:~u"J.Y!"JP^BhZ5thvef*¥OQDHGF_Yt ԢE#$"#0(XVX5Q!JDrn]i)AD8""0 AA Ugܻ®Z:,Aam]/}-9a1H?Z1 (W?eB+F#P(<#5rEZtG4mncp/AʶU+^ :ˇ1?ui` l!W$yfVpY~?;}34{~ 4ukeÆ $eH}nuaq%v;p|Qnk.A7tx4g.Ɲ;ED7ě "dez~Q2^͹#Y9-z "qږ`Ն!fpS,lA e(G Y)ɉ|Z#@tAWKja#:Kwlc\,GA(X0#"J*Gg1vI7gVD4ؼ^$gYm_,ÍbR6k_)%k|^ZFfoF}Y  Q tSd:ML1 A(xBDT+sLP؟aw#kVy,|]ZZ}=wkhnW;h7CPtCHբTr5"={֪~Dxv=+3Љ:^xnЄwf/N+`N #NQir8TkfB73?&j6#QS<D֋Tvd]V Wojbge#/eb`X(k1~gj,'c`l67~ADkED4Α "Jb JU3wƌ70T9Bb ѽҞfn:+j(`$;п;͞j rapFxоT d:R&b %]/|m9lT*d|8ywq77̘ wk!+8oNfDDIQ<2ō")Q趧svuO9|0jK˲u~^[݅-S̊)(j}D35kpzD H+F\Q$~(4 '!K{ID#/hgS":̈/zZ ͭ/춃H )QZpIfڬ?(&VrJ)鄡 :Ơ7Z/Q? hW$z$X0z?:]cW8pf fDD pT*hyP؅@==fޏU7D7(faʢuE1gX@4b*(xjddG8t FXRїH|%;砱>lb(jhBCV>V1#kmUD[B-c&vA! IDAT.|ɫl:~,Ԫz0uEqLQ,ɥY0 ӳIB"4tQ_#diM, T,a8gKSG|?wfW.(ê:g^Nt˪XOtbŢzx4ڵȊ UDt5E1h +(v^á3G j jZL4`ʬV 8Ǽrfی_D:2Z-$P8l<ںQ ^(,$Z,ZS^QhbE7dE$Ev>#ŲE,6j뙺ZwIJ :@gNu̩_CBDDRe4R q퇑p˔Xhq}^Qkwz86FV{#o5=:ub/#ȬʢY\g8;nJ^_rبTp61mOyUu :vb^:e;̈%cŪxFg)&A땪[:7o,PT`+b(<)&(N1)ƢXj(d:ZŲBD‚QS+Buu$FZ=׍gy[2-1m\_=O" :h ~vɮ0 2MB"4tA_vܴʙqٟib %u"ʞ|jFD-Ho8*Qv?6 5! įn#Xh\ճ^~n7%YZ s wa{hXF5r[{>J!2uDM0#"YNr5u{gN"aS/H[aQB* f~ю0p*R֋e3ZPso nb٢PX5F}L]- Abǧi~_doq!(X0#"p9%?(;]Uy#jBN.䰙߷ O$/+kAnN3\O"?Ch WܷfIkT[P:Y!JPc?&0 2/ D .$"YdY0;Njg95b݀;cNeoj]†iW [h8+F-qF2ᅥĆ\f~B|ŨT_|ی4!oŮCQv`FD3cxss '+N=,XMx x,$2GQQ<7zh/0f4q3|E]X^렊#IuxA($(~,Clwۘ|zvc1´HQkg%dUm߿$̳]6+aŎ, aC:ìQA PGP##;cd0’}h]  ]3?Lc  pb_6+dp/,E#<E2g-%NԼ%j[P:Y}%> poIDtn0#"ܔzauB5#+ls>sLklpBglFݖGA5F}1*Wo~aw"C 7AD3"}cG*?ޠwXm":LGsLҖ(`a*5u܆WW?{ ~#Rs8cYK*\mdҘAQ~@fTTi_?;*~IDĂQśUr쌲NI`$1M఑8zcm3ww`S!UQ%.sOr7AD3"iX|cW$^.;8f*8vȴCpD/<Zf)0,ָk0^.#+l3V m6E/a:p?|' D `(Fo *OB^M\iJ; ٜ䱢^ґ?7`J Fa`(9 2GF#Ŧk);xi8Ӧ8O~u"J?.$"K5;\PK?֕W eW2RpyJf~ .!2w/3b 3ňrP]ǙGN^͒ `%ocB编@avեUv%:]0#cp"c~] tb(N0X]l]S0"@s#-xvXyrlu#mv.RB$wt"C| `oh~hx*Q-(ϛL.@ĵZY^ *kKjmv57=a ]xi&wN-~ gFDי^"Ya=vM/Z =>S gI.\N{J6cjpL&!2o@Dtٹ%" ft 㾞InYQ;R;TZUv04}L 3z;!2Mu"J̈bt3N0VR;{h g$j84QdVuLm.Z9 zpspDM,DW1ń)j4 }O1i7|cZDg߸&!,ݤA?:D<\ t`(F"w[QΑZƄ#{ҞVyGazȬvm{A1l$!4Vw]g\/vc(n@߃f 3Ft~͌0!%߆pGȣ2=(Cdu"r3" ]GH _<1/id7Q`h]VX_{P0 ͮ,wMe(u.x/g&J;,L㌆^C՜8HxR1Fid'VqBbYb7K2!@hw1; ;+,Q ?:ÂQ,3h+-Rr0N v{v<2ZP:m6!7Dpe~ui?hTPVmdBW/]/~ 7YFMDQK@Sșk)ŝg=M )Ab,FByf:,@薶?_o]nTa9y%9\ `FD3(r# ™ 6" 4Si:h;1,WrYJ?3 C.qMviwQtU<@x43^[\z-ADb(mP=s _083͋D)櫋fnG6 -C$MؾM vޓq)͞n! pu"J fDD 0f|ryoGys*%$8Q#W AwlwM_WE^˨wOTYex7HY,%,0V8hΘG*on¢13m*!U [[A:kh4EYgxΎ'~!(,%aΗd6}cgh0ֳIJ$Ɨ \)nK/nH x I}7g͇*XwNnOvw{X*àǡ_a$}`8W.gIWʹˣE \wvv7hްXWEyu :}HBD73"Ds$v-Ll׊XYwpX<{)_VW6sv͆ }P>ZN1*!\.b\4'JD ^"<\i{x9(0y+8NO2̌\繑pd7HQdz֥AFfi >:%%!(>,%Draڬ=y.ǜD]ډtJDxpFƗյͼ,b2&ۯ_>{CC!9סnHA(QO CP~\ x(!3L[W8PXHE<; NZD?2(AKfuu3ctuHgƒVi5:$|k}d< o3&u1Dts%"JU#A.!٪"LQ[`|Y]k⥦,^f `v4@\rIJAN| mC񂗈(!\gN?%4(;fii$ize!&*6W^WW6suQ]8(h5+۟YMv={)=:q#w]q_eC(!ҎMNN[:v.bԌrTw3yoLyvC],6K{ˍJ? Uf{ ;vwƕ<|'}qfޛ|Dtv%"JZ Fgrf!c2U͗~bC+5o}y32p3U0MviwIUT]g̲n Ri:x^yk ^"NP5GKwje' D,޴ͬJEbqni{ko} >-;dc\J?}A,y8όhf񂗈(IƈrITIc4[y7KUg2Ll84~cYگv7)_,#SPlq)j /ɼL7N< ] c(I%5N~7a?ԜܘeT"M_,T [[^ZweUDwuŭПS|#|3^=8s:N/8\Q]κT|_J8Ϭ8h U)R~歯mLlhh} Awlw.,~qBsM\ U:M/;޲yMo"Jh4,QKԈHQ ѨhTgL0 hlF%̝{{=eg LޯW2=;3z: !~χO K3{RD fJ)5D~f|0,ήŵä \8k%]gad~ юQ3EٰyK%6Bɲ7 v盕ݻ.&օ$n|gP x!fɃ;Rj`RCd ̾7Q)hlƘsƐnlæ\P4{ޝd>R\g~eƽx]]08sp!R321,;Y10kHD :&u0ܾIssy f"Dx*Pk99@sg8wiL)Fe舍a|]X,4kc3I1cC*m͒͢Vz9 k]{gjSwt%s5M}~!;! 8?u|Z| U*`R#P/UFQ/EF;_ŀ1_Eĸf_rfIg,k7l./۬[~!/6'wϭ.oqԊêHs}8k7w=iL)F;kꖫ>o0kCcQI6AD8fkAfni7#w[S[i7٠k.O%`HC~gYc|PJݕ̔RjDV j"w-hv`͜ }>d.oC Wmd&UW5~vG > lD)u'-)ԈzV֙`ժTlnf㵫oHfmfKeqGRkm{74&ZXS ?s ; 7M=\Gp \~U*1`R#G_lB þ;@l={ 6Z0K)[0k6n)W6c+/O/3\nq^IC&.n$FᎣxr ;R*3O|h0cISϠ`wDqZވd}j읳l#7߬5vq6. &qf9-%_쁾?0kf?B)3)# X\,'DldXf^IDrtKf6,NuvUh:}wuH;!>f} ͚x>^"3-)Rz0+4tyML `dz_1k}8}WԻ+Ml;aV;;Sn1k*^w%x)%Je̔RjNؼscjw+l4k{T |7nE욞 MJ7n-hc$9x+vJWR7^}wk 8yqִoLyL!UJ)O=ob˳I3V R]-*(PA̒D\N{`r>.e-ZYw*M[0ML@|; |xTͮRJye`.ƼlC0l`tYBQq"vͲ?vSl k9.^!=SqCP$ A`x*04:1NyJ̔R#$Xss&eII\%aU:䬩CxftzcZu"pQ%uSWfG:]Ol_Btzvwl@r`k}R:nȔ}F.u֣~&1 s{ng)(k?W1?UJ)ώ^&Us44y `wXhStn,͞BLNLt9Ad P:Re|* ̅brnp58 /s2_!R01TjhL) }G#Y;NJ[ n$H_j0h7kCW2 #;.'oi||բ}?#Ǫsd!.e|ȐWOB,тRJ% ]%>34[c j6TR5֨x &+sd]*3p$0MQ s88fm07N?'ÑedlbZ ;"A -)TR5n_Zqc6aжH6αZgEt]ne eF_? a. k׭fieO'}NKk 5?Vs|Ȑu%@*g*4Z0SJ%`+lw*3H3!#1W+70cu`0g;d :ESܰacaϰ6F䒯|Ǣ94! ;;D:!-)TBf"vYY<ϬaʈI|'rA/nc06aT_L"vͰ;lwaӦmvzIwCC,|:\h xJ̔R*AڑKb+vE=e&:lę~?9׋JFYI5b*6n;kK"wΐCkg:47pf +_!Kj iL)&?kv㨮7lMMaYuDg\&-R zo2uŘPčt(v}_`~ߕ|ș"qA(".nj1'ψSJ̔R*anZZNuV&B`&|g8qDBfZ0[N2O#_XF˂5vۑ+jy'۷hP@|$QG|Ș7;Ri3J ŵZHdS .͈a$Fjيa;9Ӌ*[e]M[:<ЙZ;w9 xpp s +!2\lDɽRJKǸy~TlDboeݬd`]s~7:ZT'rYvQ>_gQǕds`s'QO!]C <}pwz4s.2tHQтRJ%-0QW|}_*?fiFd F-CߕfZɲ8z+|qf`Z}~wy(Q $'|Ș'/B4Iʝ- t&X]bɌ|@]^ gtwN8k;ID130 Pd}l]}}Y/poUVC(Z0SJ:f C}Ө"/1LI,vJ3"zdRJ:lћ6Y%W,ʺc4m"w  }Hk]ɡ0hL)8NuՅ5Hc+%3Ǭ,|gf4ЙĽ1\=rP0}Yv~?dqY7dnڡDW;}ȘP*+`Tpw?v{AMN 5\ }gXybɠ}÷_A7խ7&"wX|[,9&s}ȉObӈ;ҽ;DƜv)uHf[tn犺 ބ7u&2T1Шfdu=3zr.#Z8JX*4kwx !4૾C,:~E3d,)*q j?.b>2cðo7ⵒP43z];0,뽩H\wqAQƍآT*$StY #xt5n!R |Ș ~ϤTRiLtӂYN=V,ϊDl&TmXW1 `vN2'D۟0XCǖmu6}GY#|۟~w,p%Lj"cOi-)nZ0ˉ7.Z,.'' *%vgg돊I2bVwrU &Ȥe[Co_a  xX;2t5A2`xo}P*`TZ%ky~qaS0/]kIGdU "g[d7rA?q#W _lႛ>y|Ȱq~,өCdGOA#TJ#EwLJyajqTs4.LKYelBڏB_MV3;r-ΰLsͤ\ f6n;sLi;w!%.ɏ3"~xJ]r@ f997_B7le0Uk7iwJ#+%ٝz7 2}2-Wu [sTqqӧkg\\ 4|!.RJ]rbmέv _{j9^_]n53"&WC$N2',Evcmbv}G=,F{x$!V}ȩgAx}|QʗL*3~PSF`~G:b7Vfs,w2Cfa;5Ӌ*[l.4p;M[{LazT8ory|eC(,iTZ%ĝJN&o}cGĂwD;,:l. 9 Z\,F0\0mպM S/_\p `Qǖ :ˠsȩoK/B! |Pʇ*cG1zbQ,I@כ !xzČ2l3Wc+ x^? ,S8χdxu6h!5-)~G:χ6p/_sqv"GX1Cj5x46ݫ# vwbV7#&ſ8)1l">4oR43Fj #^ wu;DYVA%-)~Z0SE=wʼdtJTPl\ G8W;S2*"b `fC%rVU%. dqB6?Z.dW*`0#ni8%=%3jcYIRwQ Ls\[(U n?\8\ bsrUNw_"$< ܰS |t 7!ԒG!2ŤX%J\> _@%cf;GT*cE6 ⵲쮥N2 ǧN\`A#[ \ǘ {~q0s̑5+ t!Բ o!2 PTiL%Y߄%]<7?|-s$IdʃxF??&̈ ݩ^Tقn ”|gOGS,MCVc`^t6&޴6SW#Vt$[[԰ JA:-{N^*/|HB)z }0mպվDwt1!ore\\m7}Q&|Ȩ 4nUj`N fK@>Cdzo\^;wƌwVqcuN~XktNolW_ވ\C黒/<ysܼ|(Hk!|PC/@g%ĝfJeTily;JG?w@t3ZU=y֎~KHku{Qi=] ϾW}U+#|Y7x. !yRIv9gdNA̭sTeuۅmsVwR" @B0g_3tCwgAx]!2}C(ZZ0SIOzq6!Tr^x[d fYbfUVe^e٫q|aٜ,>_+=M b݅RG G3cxQoT2پCd{>x3seJ,jNX;LifX媮!6lfzQe b 16_VTൾCKFwk&OC(Z0SI7;@ V_t9p>8g ,{^+-{A@X;(Zw"iɲ8=yO 0O뾃d#!Cwx==!2e_Jjt{}H?B%ߚ~;=d׳~d7"O{܏*ӭp]$o;3$k^pw 8 w{6|t\H}BтJ:-|P}_&6YFR=tѨ4,1:&%pbg^eB)hnιͯw X wO0߾w [K<Ϭ;R˥7*H<!T=1=YFP&G!n7#\U),ֻS u,[؇gyP >x p+^ w {8&!Z.- F!T:<}]t9Fc b9E_g݉8f/{w~ ڹd-"c|X fJtУjIC(Z0SI R1#}P\t\;ǨbNtR9PX+خ^e:~pu4Ycs+|ɀW/bBH!}!2D҂J:-Λ!Ljf82V}hTgF KdYWN,6߮|gHK=dぷJonB%YO}ȰM>KTM!RwF͆y.M-{lt\^,feq+r9WZhu'ň]7DIZ`K8$N.'ovoFbkoMA2쏀}Pj)`[a|,Yx_ذEs;˰~P[Ê]qU'佘3T"Gt9HA=|ȸBCтJ̿q)Z+/'o 5рx‡w5YJl_w niHi$Vw/ႰџE-sC<]qu#UOpB|0 \J\W*r|R'd$ջG:|_p ߺX{2Bwa*Wl,8Lg42}WlԻ}t¨|bXn2QLt-a\DFU6'JB癭pM~^;PM`^dfRk="l&;D=8wD f* ~;@N nx-Oח:, ^}G>s1 bd rwԒ[f [FI)ZƑٌ1'|+;H>p7JDxQDf\AFflтJ[W<(Aj-[7Wgf£|";ǠTo M7%zay՝(:gFI)rvf^{CY2~ i߰Db a!23G%~B64l8Qa#2 e|g[y,ƄBg$g8(BXF!rHb,Ɖu&0.$pRX\8Ap"'`@pQXgcED0Vp)L`ZpL?Wvw6,7C%ςZ`ӏsSW|LV^ ;Hƽ xJAdUZxu-YAQ}XpNpYC߁ Qh#N!"Ep q)E$szhD^;2bbs}n3m٥eI2\G6fq8h2 '}ȸ.<3}A f*-^|w xYXą'0 ,;N [OYAkLЙ+$ǂQ$FsF5D!G!Xqb"'Ƃ pR3VZq*r-CZtUpgVe㾃 ȫ)5LVCfAT~iLɛS?#\$n<xX@P?~8Aiǎ.0qLv|k|k>]VuR~`"qGM\4d|(n.%54u'_v%#_;Ȁ|xQwyP=C+5rZ0SiXSC(>KOf4 W}Qq-5\ wOZ0Si2 oڿ7 V;~ S$.=toK}G>sfJFc|20 _E0O"rME 3xygfC#K;KGGQ 9 2zM f*mBmP6`o DG7xP@Px^*71hL/"S%&BrAhDJiL8Lsk(v嬟N!o%ފĝ󾃨H|z` $>U#AThLkq g1*AsFdq9IcPc^nLW þ u.sH}İg~""[͏mp[$yCz:p9p3՞`QjɲFFCo2]J|3Ss_,wc5]ު6{aa,Iw7ڕslkJv,<} TQ˝2;K!i K7ϹUʧ;DOG҂JS* s ć?uʉb&GpNuoW;KRN7<`E QPMαB#\ ^\&CF|,+sMm+L̍L0wKѵ(D_N&2g=YcND,d,vblqx;;KR"?x -JiSe:>vmXX(W Ҍ--HZٮ KI #͈u=gZ/b@'R 1Z,SIx9`9ʉl>VUY|P~B~ Ipg^W{ Z7*+یFe4R>a䢻uqBZUrDv.BEF LӮxY1{Jw!qwYwI CW=PZW JG@V=bk?r,E&'X+rAFN°"Q83ZhVwz fFą-E,&:N1My;̃ g#*y|xw-囓TN DAU;#_ķp}$kDԙv+E'v! 3X0C5TALJ\R*Vy%Gvޙ_eޜ88w! JG;Ho+7uJÀo,˾;H}?lmjCeϵTa*o -^/" ÊI;g&k }"E`@8̮;QdOOR<{ qa)kEEx9:(7I f*n;R iqPq9HƶsDѳ 5Ƴ6T|UbZpDfE=]^ИP41n ά0,\uK v|z C(uz4st~NL1;-4{!pJ~!A&^P,pbDFۙΑt-ᄈ̨ #g ]AHam`&@ cq ^Oׁ|[˴ȪE.1 eCJ -4~ Dks}H;N k;H}-.ͷzA:k}'@F,q-r~)h Z.dq>Y]G16cpS"f /,r1o{1,vr0?$' }Qّ7&*^;ʵ_'჆]ڝtS;˨4cs(X;GV+"nI\4 k&g:JE⯹/̧qϽ-gZ9eIY6YQC(LG&}ɉw)5Z0Siwc`O+ Yq c1'2Hݨ0nw"s)qwv{#ZU _ed*m߾]_ہW1$!\ x9fl!T6hLeGBǣ_vdbbm '2HQToT7KJXkVsQYD$c*T)#C%xގμW\wB;Hx*2}Ӯr8-4Y[f YOq/ Ώ.ֹA wO .OΑGq{fFA_xo=6Y2@Vg,*M| O\hfbZ`PYtAeCǼߍw%Y%0n5f`2,7a9N/n5ŖX&1ʋOb=Ave/bJ.9lTCRr4};ʴO|ɓ۷ۣ>,o&#_jx3yQ3wm0;>s^{$9p0A"*,E{I5:O꾃тݘ1-9rן[؋9wӫ^?N:Z,!sbn=6z}vModUAO|Q3%눏˭DeNx"iAnǎWT;k%8zƚQ`,F]FU7eQkQgPGwB?2GwLw!xJ i?CL]ЂʚoRRjPT㾃;]t)jŜ^ =rf:\db"cdj6$̢Ԋ5;HN< T}?s|QjK}ɩ{}Qɧ35/|Qr*a.ckTtoKr6(5]{yَAr/ GtGeqc>g{0|B f*D<$6WlBܥ7>>eaa֬n |O>< :{#b}MY^2"?~MX۝ ;D76ͯ)7J?BoX"AR}FvV_ k4za2WI\޶;J,ƾc?|g}<)!Rq G`|=Vj/w<~ {7HYV#S,+v5rԓŎvMqvo6sŸΝ;Ofɍ~w4!7'c{ 9J-,7lUE7*Aʈ`.7b;J|sFM(*L/4IJqFfkQyi͑ i>Bo6awhٽ{mW]:35޾)-ZD*ZQA%Dy,DHBSB"-}{rg>3$;9sΝ~3{߽ou{rMy6Λ"`]vi榹i(|4e]B'ސꮋ`/_oq%Iu=?IMedC˵fWut9x9|? Wk-wNNN6C{m;<*\ׅ$u lD N؍fyOƶ܇w{W%$#=C|lnj|$D/32~nutii7`|-MPvfiI>u0Jzׅ0%0cH޴o>\tͯ8 R%^\emva]WrO+;o=&S3͡ٻrnjⰷ%o>7ݢf$u <0# F JzmNr a+wZʚ:4s.-8Bn;<3;1R5 ZSV:_x5iJG{]%`M'yDC]$yGEf#J`?$8[7.=u!lnpSJYvdu`vnMݷ:i~L=Z'j3>xsJ^aF`6Xd]ʮ`C F Jr]'u]iwv][˻?yi)yZMDW{-`k_l4²L[7OnXۙlp>B(<5f'mI~B0%0{%yqڝnL_Lr{ׅu]{+/[]yN6t kk#3 6jf~Zja[Z¡õ߬f#~M$o;k񃟘\h c9w]lR$|&0Q3.O$Nb'' ax-/{tkI躞ڔcGg''Ng-.>t|ya#ԱlLc|W;wWZZ>\:HNM3zf#J`$ynM2 D?.[:x%)N)e~zvxaVc&.jOdԔzSm[b[7f^|C5I`f#J`'$MG^EKMwz{ׅ@묖禩OOyQƖ\]k;YQk-bؑCZA&ML-ip̠)&;u]l|:ɣ.(:핝nK$upO^1Z3jdSSf_Y?k&)5Gg_J:Z/(MoPᾚW~{@d0fEE3itomF`6fpj.K$OOI"'g-y;Ϛ]SǗv}KMBau-Z:~5{^5;<2|ׅkI> وyh&2ɾnK֚$䯻.N[?;R)yFIyJR79~Mr|iǡKڄK+,Iٹg՚S)ׯRz(e{˿;(S$^2lD `0N7yIq-7qOt\ u7!FzyJ?;7bj ʐz/?¡ 6@[HR?Uki,M_c &0;5'yNE6`A`6f0xOL=;I' Iޘ-IV:6u7boԚ[I} Ͽ:vxZ7|iyȱjť2sP#M&N[֒ޔZ?k-7=ɛNri]$H F 6Dg&?s,u\ յ7:+)"%W$9[krlnn ,LݵoƎ1$Ǧsz~i~|ʚ IlJͮO89_"0;yW&ygE62䖴ٜf#J`ñ;ɯ ΞvMN7.>;Fƻ> XHO5IcSIتM];/֔$nPV/hKmO-ƿdf'ciY u^OEkJ`6fЍKgON/rj(Og-6w3񟫥yB)qI,'< ml=jl,3k37kn>GL5gHkZ}㫾f#0;qI.O{ '5h6وh$<"96$_J}<2˚`n3GS.\ZkSzZv5k v,.>t|qPw<M򽃇YZ;&_(i2+y/w]a=:89%0Ѵ3ա^<Ò\r0Ǘ`rrxG<<<5R/JE/[Zx }RT-$wZXtƫ&6Dݞd"<8mOK.$\]wcnۋi{Ϯ7sv${Ҷٟdv- 6=iCOsrjlD `9+mOs֏|HDki3cO;뫦}[Jqn_Ol1&:9缜f^&禗sJ-5u?IjR?:Rz%;j͞Rld^fҤIjMJ5elNך4e4ccS90?=99tQtFڝI^~&u_385{0s9vk(N[IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/_static/img/tox.svg0000644000175100001710000006322200000000000017373 0ustar00vstsdocker00000000000000 image/svg+xml t x ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/_static/img/toxfavi.ico0000644000175100001710000003535600000000000020223 0ustar00vstsdocker0000000000000000 %6  % h6(0` $m{ yy yoooLo~w|ĉyʼnyxi~yXLwVzYoo7ooou{|yyyvh{ZӈxWyX7yXo%ooopv|||yyyyyo^yXyXyX%ooQooopw|||}}yyyyxwp}]yXyXψyXPyXlomoooqx|||{}yvusrp~o}oayYyYyYyYmyY_bϻpeʵfɳgȲgůmponnmnxv}p}o}o}o}o}o}ocyYyYyYyYyYpyYaѽXaѽaѽaѽaҾ_̹ahhhhhim}tw|p}o}o}o}o}oeyYyYyYyYyYyYX^-aѽaѽaѽaѽaѽ_Ǵ]fhhhhim}m}ryzq}o}o}o}ogyYyYyYyYvW^AփH.-JSП_aѽaѽaѽaѽ]°[ahhhhjm}m}m}o{xs}o}o}oizZyYyYvV\?I/I/I/OGOZaѽaѽaѽaм\[]fhhhjm}m}m}m}n|uv|p}okz[yYuVZ>I/I/I/I/GOO֣OT`Ҿaѽaѽ`ͺ\[[bhhhjm}m}m}m}m}m}rx{ql|\tUY=I/I/I/I/I/I/O+OOP]aѽaѽ_ʶ[[[]ghhjm}m}m}m}m}m}m}pzzrwXW;I/I/I/I/I/I/I/+OdOOOWaѽaѽ^Ų[[[[chhjm}m}m}m}m}m}m}m}mvxeNL4I0I/I/I/I/I/I/dPO֙OOOQ_aѼ][[[[^ghjm}m}m}m}m}m}m|jsgzhmgTt[GwWA}P8K2I/I/I/I/I/O OֽOOOOZaϻ\[[[[[chjm}m}m}m}m}kxhmfwffwfljXr]Jr]Jr]JuZEzS=M5J0I/H. OOOOOOT_͹[[[[[[_gkm}m}m}m|jrgzhfwffwffwfklZr]Jr]Jr]Jr]Jr]Js\HwWB|Q:~O7NNNNNNOYͻ[[[[[[[dkm}m}kxhlfwffwffwffwffwfjo]r]Jr]Jr]Jr]Jr]Jr]Jr]Jr]Jr\I=======D˿QTWYZ[[_km{jrgzhfwffwffwffwffwffwfiq`q^Kr]Jr]Jr]Jr]Jr]Jr]Jr\Ir[H88888889GKKMOQTWdhmfwffwffwffwffwffwffwffwfhsbq_Mr]Jr]Jr]Jr[IsXEtRAtM}A~EHHHJ}vQc^Qb]Qb]Qb]Qb]Qb]Qb]Qb]Rb\Wb[xiaS33w3333335=>~?~B~FHMunQa\Qb]Qb]Qb]Pc^Pc^Od_Ne`Oc_vPa]33Z3333338>~>~>~?~C~LnhNd`MfbLgcKhdJieIifHjfHjgZGkh3,333334;>}>~>~@}yHlhHjgHjgHjgHjgHjgHjgHjg,33@33335=>~>~DuqHjgHjgHjgHjgHjgHjg@Hjg33"3W339>~?|FnkHjgHjgHjgWHjg#Hjg26>@~zHkhHjg??( @ nnw4DzDx4ciHooVosz遤yy鉑pԈ_xWWyXpoEoot{|yyyrayXyXEyXiĭnmoot{{{vutrkzZyYyYmyY_bлpd̷e˵fmmmls}|q}o}o}ol{[yYyYzZpf`ӿLaѽaѽaҾ^ðchhhlpzyr}o}o~m|]yYzYqRW;LLV`Ҿaѽaм\_hhhlm}o|wu}o~n~_yYqRT8I/ƒI/OdP]aѽ`κ\\dhhlm}m}m}tw|paoPR7H.I/I/dO OֳOXaѽ_˷[[`hhlm}m}m}m}qzy]Q6H.I/I/I/I/ O&OOR_^Ƴ[[\eilm}m}m}m|jtnmZxU@~O8K1I/I/߃I/&ODOOO[^[[[`hlm}m}lxhmfxgjn\r]Kr]IuYE{S~@~EKxrPc^Pc^Od_NeaMfaNd`tOa]33K3334<>~?~GrmJheIifHjgHjgHjgKEmk33^337=@~yGliHjgHjgHjg^Hjg334;<ؙ;BOR^f}lexhgtcq^KsVDsRA8r89FJL[xbqcsenTFuH8uH8r8,88?IIN]uiacXe]QqM>vG7,?7n55?FI|QgaVa[ZbYd\Qn33l35>F{uLgcLfcMfblSa\33)3m:CuqHjgmGkh)Bom././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.899511 tox-3.21.4/docs/_templates/0000755000175100001710000000000000000000000015766 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/_templates/localtoc.html0000644000175100001710000000225200000000000020455 0ustar00vstsdocker00000000000000 {%- if pagename != "search" %} {%- endif %}

quicklinks

{% extends "basic/localtoc.html" %} ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.899511 tox-3.21.4/docs/announce/0000755000175100001710000000000000000000000015437 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/announce/changelog-only.rst0000644000175100001710000000257500000000000021110 0ustar00vstsdocker00000000000000Less announcing, more change-logging ------------------------------------ With version 2.5.0 we dropped creating special announcement documents and rely on communicating all relevant changes through the `CHANGELOG `_. See at `PyPI `_ for a rendered version of the last changes containing links to the important issues and pull requests that were integrated into the release. The historic release announcements are still online here for various versions: * `0.5 `_, * `1.0 `_, * `1.1 `_, * `1.2 `_, * `1.3 `_, * `1.4 `_, * `1.4.3 `_, * `1.8 `_, * `1.9 `_, * `2.0 `_, * `2.4.0 `_. Happy testing, The tox maintainers ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.899511 tox-3.21.4/docs/changelog/0000755000175100001710000000000000000000000015560 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/changelog/README.rst0000644000175100001710000000116200000000000017247 0ustar00vstsdocker00000000000000.. examples for changelog entries adding to your Pull Requests file ``544.doc.rst``:: explain everything much better - by :user:`passionate_technicalwriter` file ``544.feature.rst``:: ``tox --version`` now shows information about all registered plugins - by :user:`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 :user:`ferdonline` .. see pyproject.toml for all available categories (tool.towncrier.type) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/changelog/template.jinja20000644000175100001710000000142100000000000020470 0ustar00vstsdocker00000000000000{% for section, _ in sections.items() %} {% set underline = underlines[0] %} {% if section %} {{section}} {{ underline * section|length }} {% set underline = underlines[1] %} {% endif %} {% if sections[section] %} {% for category, val in definitions.items() if category in sections[section] %} {{ definitions[category]['name'] }} {{ underline * definitions[category]['name']|length }} {% if definitions[category]['showcontent'] %} {% for text, values in sections[section][category].items() %} - {{ text }} {{ values|join(',\n ') }} {% endfor %} {% else %} - {{ sections[section][category]['']|join(', ') }} {% endif %} {% if sections[section][category]|length == 0 %} No significant changes. {% endif %} {% endfor %} {% else %} No significant changes. {% endif %} {% endfor %} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/changelog.rst0000644000175100001710000027163600000000000016331 0ustar00vstsdocker00000000000000.. _changelog: Changelog history ================= Versions follow `Semantic Versioning `_ (``..``). Backward incompatible (breaking) changes will only be introduced in major versions with advance notice in the **Deprecations** section of releases. .. include:: _draft.rst .. towncrier release notes start v3.21.4 (2021-02-02) -------------------- Bugfixes ^^^^^^^^ - Adapt tests not to assume the ``easy_install`` command exists, as it was removed from ``setuptools`` 52.0.0+ - by :user:`hroncok` `#1893 `_ v3.21.3 (2021-01-28) -------------------- Bugfixes ^^^^^^^^ - Fix a killed tox (via SIGTERM) leaving the commands subprocesses running by handling it as if it were a KeyboardInterrupt - by :user:`dajose` `#1772 `_ v3.21.2 (2021-01-19) -------------------- Bugfixes ^^^^^^^^ - Newer coverage tools update the ``COV_CORE_CONTEXT`` environment variable, add it to the list of environment variables that can change in our pytest plugin - by :user:`gaborbernat`. `#1854 `_ v3.21.1 (2021-01-13) -------------------- Bugfixes ^^^^^^^^ - Fix regression that broke using install_command in config replacements - by :user:`jayvdb` `#1777 `_ - Fix regression parsing posargs default containing colon. - by :user:`jayvdb` `#1785 `_ Features ^^^^^^^^ - Prevent .tox in envlist - by :user:`jayvdb` `#1684 `_ Miscellaneous ^^^^^^^^^^^^^ - Enable building tox with ``setuptools_scm`` 4 and 5 by :user:`hroncok` `#1799 `_ v3.21.0 (2021-01-08) -------------------- Bugfixes ^^^^^^^^ - Fix the false ``congratulations`` message that appears when a ``KeyboardInterrupt`` occurs during package installation. - by :user:`gnikonorov` `#1453 `_ - Fix ``platform`` support for ``install_command``. - by :user:`jayvdb` `#1464 `_ - Fixed regression in v3.20.0 that caused escaped curly braces in setenv to break usage of the variable elsewhere in tox.ini. - by :user:`jayvdb` `#1690 `_ - Prevent ``{}`` and require ``{:`` is only followed by ``}``. - by :user:`jayvdb` `#1711 `_ - Raise ``MissingSubstitution`` on access of broken ini setting. - by :user:`jayvdb` `#1716 `_ Features ^^^^^^^^ - Allow \{ and \} in default of {env:key:default}. - by :user:`jayvdb` `#1502 `_ - Allow {posargs} in setenv. - by :user:`jayvdb` `#1695 `_ - Allow {/} to refer to os.sep. - by :user:`jayvdb` `#1700 `_ - Make parsing [testenv] sections in setup.cfg official. - by :user:`mauvilsa` `#1727 `_ - Relax importlib requirement to allow 3.0.0 or any newer version - by :user:`pkolbus` `#1763 `_ Documentation ^^^^^^^^^^^^^ - Document more info about using ``platform`` setting. - by :user:`prakhargurunani` `#1144 `_ - Replace ``indexserver`` in documentation with environment variables - by :user:`ziima`. `#1357 `_ - Document that the ``passenv`` environment setting is case insensitive. - by :user:`gnikonorov` `#1534 `_ v3.20.1 (2020-10-09) -------------------- Bugfixes ^^^^^^^^ - Relax importlib requirement to allow version<3 - by :user:`usamasadiq` `#1682 `_ v3.20.0 (2020-09-01) -------------------- Bugfixes ^^^^^^^^ - Allow hyphens and empty factors in generative section name. - by :user:`tyagdit` `#1636 `_ - Support for PEP517 in-tree build backend-path key in ``get-build-requires``. - by :user:`nizox` `#1654 `_ - Allow escaping curly braces in setenv. - by :user:`mkenigs` `#1656 `_ Features ^^^^^^^^ - Support for comments within ``setenv`` and environment files via the ``files|`` prefix. - by :user:`gaborbernat` `#1667 `_ v3.19.0 (2020-08-06) -------------------- Bugfixes ^^^^^^^^ - skip ``setup.cfg`` if it has no ``tox:tox`` namespace - by :user:`hroncok` `#1045 `_ Features ^^^^^^^^ - Implement support for building projects having :pep:`517#in-tree-build-backends` ``backend-path`` setting - by :user:`webknjaz` `#1575 `_ - Don't require a tox config file for ``tox --devenv`` - by :user:`hroncok` `#1643 `_ Documentation ^^^^^^^^^^^^^ - Fixed grammar in top-level documentation - by :user:`tfurf` `#1631 `_ v3.18.1 (2020-07-28) -------------------- Bugfixes ^^^^^^^^ - Fix ``TypeError`` when using isolated_build with backends that are not submodules (e.g. ``maturin``) `#1629 `_ v3.18.0 (2020-07-23) -------------------- Deprecations (removal in next major release) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Add allowlist_externals alias to whitelist_externals (whitelist_externals is now deprecated). - by :user:`dajose` `#1491 `_ v3.17.1 (2020-07-15) -------------------- Bugfixes ^^^^^^^^ - Fix tests when the ``HOSTNAME`` environment variable is set, but empty string - by :user:`hroncok` `#1616 `_ v3.17.0 (2020-07-14) -------------------- Features ^^^^^^^^ - The long arguments ``--verbose`` and ``--quiet`` (rather than only their short forms, ``-v`` and ``-q``) are now accepted. `#1612 `_ - The ``ResultLog`` now prefers ``HOSTNAME`` environment variable value (if set) over the full qualified domain name of localhost. This makes it possible to disable an undesired DNS lookup, which happened on all ``tox`` invocations, including trivial ones - by :user:`hroncok` `#1615 `_ Documentation ^^^^^^^^^^^^^ - Update packaging information for Flit. `#1613 `_ v3.16.1 (2020-06-29) -------------------- Bugfixes ^^^^^^^^ - Fixed the support for using ``{temp_dir}`` in ``tox.ini`` - by :user:`webknjaz` `#1609 `_ v3.16.0 (2020-06-26) -------------------- Features ^^^^^^^^ - Allow skipping the package and installation step when passing the ``--skip-pkg-install``. This should be used in pair with the ``--notest``, so you can separate environment setup and test run: .. code-block:: console tox -e py --notest tox -e py --skip-pkg-install by :user:`gaborbernat`. `#1605 `_ Miscellaneous ^^^^^^^^^^^^^ - Improve config parsing performance by precompiling commonly used regular expressions - by :user:`brettlangdon` `#1603 `_ v3.15.2 (2020-06-06) -------------------- Bugfixes ^^^^^^^^ - Add an option to allow a process to suicide before sending the SIGTERM. - by :user:`jhesketh` `#1497 `_ - PyPy 7.3.1 on Windows uses the ``Script`` folder instead of ``bin``. - by :user:`gaborbernat` `#1597 `_ Miscellaneous ^^^^^^^^^^^^^ - Allow to run the tests with pip 19.3.1 once again while preserving the ability to use pip 20.1 - by :user:`hroncok` `#1594 `_ v3.15.1 (2020-05-20) -------------------- Bugfixes ^^^^^^^^ - ``tox --showconfig`` no longer tries to interpolate '%' signs. `#1585 `_ v3.15.0 (2020-05-02) -------------------- Bugfixes ^^^^^^^^ - Respect attempts to change ``PATH`` via ``setenv`` - by :user:`aklajnert`. `#1423 `_ - Fix parsing of architecture in python interpreter name. - by :user:`bruchar1` `#1542 `_ - Prevent exception when command is empty. - by :user:`bruchar1` `#1544 `_ - Fix irrelevant Error message for invalid argument when running outside a directory with tox support files by :user:`nkpro2000sr`. `#1547 `_ Features ^^^^^^^^ - Allow parallel mode without arguments. - by :user:`ssbarnea` `#1418 `_ - Allow generative section name expansion. - by :user:`bruchar1` `#1545 `_ - default to passing the env var PIP_EXTRA_INDEX_URL by :user:`georgealton`. `#1561 `_ Documentation ^^^^^^^^^^^^^ - Improve documentation about config by adding tox environment description at start - by :user:`stephenfin`. `#1573 `_ v3.14.6 (2020-03-25) -------------------- Bugfixes ^^^^^^^^ - Exclude virtualenv dependency versions with known regressions (20.0.[0-7]) - by :user:`webknjaz`. `#1537 `_ - Fix ``tox -h`` and ``tox --hi`` shows an error when run outside a directory with tox support files by :user:`nkpro2000sr`. `#1539 `_ - Fix ValueError on ``tox -l`` for a ``tox.ini`` file that does not contain an ``envlist`` definition. - by :user:`jquast`. `#1343 `_ v3.14.5 (2020-02-16) -------------------- Features ^^^^^^^^ - Add ``--discover`` (fallback to ``TOX_DISCOVER`` environment variable via path separator) to inject python executables to try as first step of a discovery - note the executable still needs to match the environment by :user:`gaborbernat`. `#1526 `_ v3.14.4 (2020-02-13) -------------------- Bugfixes ^^^^^^^^ - Bump minimal six version needed to avoid using one incompatible with newer virtualenv. - by :user:`ssbarnea` `#1519 `_ - Avoid pypy test failure due to undefined printout var. - by :user:`ssbarnea` `#1521 `_ Features ^^^^^^^^ - Add ``interrupt_timeout`` and ``terminate_timeout`` that configure delay between SIGINT, SIGTERM and SIGKILL when tox is interrupted. - by :user:`sileht` `#1493 `_ - Add ``HTTP_PROXY``, ``HTTPS_PROXY`` and ``NO_PROXY`` to default passenv. - by :user:`pfmoore` `#1498 `_ v3.14.3 (2019-12-27) -------------------- Bugfixes ^^^^^^^^ - Relax importlib requirement to allow either version 0 or 1 - by :user:`chyzzqo2` `#1476 `_ Miscellaneous ^^^^^^^^^^^^^ - Clarify legacy setup.py error message: python projects should commit to a strong consistency of message regarding packaging. We no-longer tell people to add a setup.py to their already configured pep-517 project, otherwise it could imply that pyproject.toml isn't as well supported and recommended as it truly is - by :user:`graingert` `#1478 `_ v3.14.2 (2019-12-02) -------------------- Bugfixes ^^^^^^^^ - Fix fallback to global configuration when running in Jenkins. - by :user:`daneah` `#1428 `_ - Fix colouring on windows: colorama is a dep. - by :user:`1138-4EB` `#1471 `_ Miscellaneous ^^^^^^^^^^^^^ - improve performance with internal lookup of Python version information - by :user:`blueyed` `#1462 `_ - Use latest version of importlib_metadata package - by :user:`kammala` `#1472 `_ - Mark poetry related tests as xfail since its dependency pyrsistent won't install in ci due to missing wheels/build deps. - by :user:`RonnyPfannschmidt` `#1474 `_ v3.14.1 (2019-11-13) -------------------- Bugfixes ^^^^^^^^ - fix reporting of exiting due to (real) signals - by :user:`blueyed` `#1401 `_ - Bump minimal virtualenv to 16.0.0 to improve own transitive deps handling in some ancient envs. — by :user:`webknjaz` `#1429 `_ - Adds ``CURL_CA_BUNDLE``, ``REQUESTS_CA_BUNDLE``, ``SSL_CERT_FILE`` to the default passenv values. - by :user:`ssbarnea` `#1437 `_ - Fix nested tox execution in the parallel mode by separating the environment variable that let's tox know it is invoked in the parallel mode (``_TOX_PARALLEL_ENV``) from the variable that informs the tests that tox is running in parallel mode (``TOX_PARALLEL_ENV``). — by :user:`hroncok` `#1444 `_ - Fix provisioning from a pyvenv interpreter. — by :user:`kentzo` `#1452 `_ Deprecations (removal in next major release) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Python ``3.4`` is no longer supported. — by :user:`gaborbernat` `#1456 `_ v3.14.0 (2019-09-03) -------------------- Bugfixes ^^^^^^^^ - Fix ``PythonSpec`` detection of ``python3.10`` - by :user:`asottile` `#1374 `_ - Fix regression failing to detect future and past ``py##`` factors - by :user:`asottile` `#1377 `_ - Fix ``current_tox_py`` for ``pypy`` / ``pypy3`` - by :user:`asottile` `#1378 `_ - Honor environment markers in ``requires`` list - by :user:`asottile` `#1380 `_ - improve recreate check by allowing directories containing ``.tox-config1`` (the marker file created by tox) - by :user:`asottile` `#1383 `_ - Recognize correctly interpreters that have suffixes (like python3.7-dbg). `#1415 `_ Features ^^^^^^^^ - Add support for minor versions with multiple digits ``tox -e py310`` works for ``python3.10`` - by :user:`asottile` `#1374 `_ - Remove dependence on ``md5`` hashing algorithm - by :user:`asottile` `#1384 `_ Documentation ^^^^^^^^^^^^^ - clarify behaviour if recreate is set to false - by :user:`PJCampi` `#1399 `_ Miscellaneous ^^^^^^^^^^^^^ - Fix relative URLs to files in the repo in ``.github/PULL_REQUEST_TEMPLATE.md`` — by :user:`webknjaz` `#1363 `_ - Replace ``importlib_metadata`` backport with ``importlib.metadata`` from the standard library on Python ``3.8+`` - by :user:`hroncok` `#1367 `_ - Render the change fragment help on the ``docs/changelog/`` directory view on GitHub — by :user:`webknjaz` `#1370 `_ v3.13.2 (2019-07-01) -------------------- Bugfixes ^^^^^^^^ - on venv cleanup: add explicit check for pypy venv to make it possible to recreate it - by :user:`obestwalter` `#1355 `_ - non canonical names within :conf:`requires` cause infinite provisioning loop - by :user:`gaborbernat` `#1359 `_ v3.13.1 (2019-06-25) -------------------- Bugfixes ^^^^^^^^ - Fix isolated build double-requirement - by :user:`asottile`. `#1349 `_ v3.13.0 (2019-06-24) -------------------- Bugfixes ^^^^^^^^ - tox used Windows shell rules on non-Windows platforms when transforming positional arguments to a string - by :user:`barneygale`. `#1336 `_ Features ^^^^^^^^ - Replace ``pkg_resources`` with ``importlib_metadata`` for speed - by :user:`asottile`. `#1324 `_ - Add the ``--devenv ENVDIR`` option for creating development environments from ``[testenv]`` configurations - by :user:`asottile`. `#1326 `_ - Refuse to delete ``envdir`` if it doesn't look like a virtualenv - by :user:`asottile`. `#1340 `_ v3.12.1 (2019-05-23) -------------------- Bugfixes ^^^^^^^^ - Ensure ``TOX_WORK_DIR`` is a native string in ``os.environ`` - by :user:`asottile`. `#1313 `_ - Fix import and usage of ``winreg`` for python2.7 on windows - by :user:`asottile`. `#1315 `_ - Fix Windows selects incorrect spec on first discovery - by :user:`gaborbernat` `#1317 `_ v3.12.0 (2019-05-23) -------------------- Bugfixes ^^^^^^^^ - When using ``--parallel`` with ``--result-json`` the test results are now included the same way as with serial runs - by :user:`fschulze` `#1295 `_ - Turns out the output of the ``py -0p`` is not stable yet and varies depending on various edge cases. Instead now we read the interpreter values directly from registry via `PEP-514 `_ - by :user:`gaborbernat`. `#1306 `_ Features ^^^^^^^^ - Adding ``TOX_PARALLEL_NO_SPINNER`` environment variable to disable the spinner in parallel mode for the purposes of clean output when using CI tools - by :user:`zeroshift` `#1184 `_ v3.11.1 (2019-05-16) -------------------- Bugfixes ^^^^^^^^ - When creating virtual environments we no longer ask the python to tell its path, but rather use the discovered path. `#1301 `_ v3.11.0 (2019-05-15) -------------------- Features ^^^^^^^^ - ``--showconfig`` overhaul: - now fully generated via the config parser, so anyone can load it by using the built-in python config parser - the ``tox`` section contains all configuration data from config - the ``tox`` section contains a ``host_python`` key detailing the path of the host python - the ``tox:version`` section contains the versions of all packages tox depends on with their version - passing ``-l`` now allows only listing default target envs - allows showing config for a given set of tox environments only via the ``-e`` cli flag or the ``TOXENV`` environment variable, in this case the ``tox`` and ``tox:version`` section is only shown if at least one verbosity flag is passed this should help inspecting the options. `#1298 `_ v3.10.0 (2019-05-13) -------------------- Bugfixes ^^^^^^^^ - fix for ``tox -l`` command: do not allow setting the ``TOXENV`` or the ``-e`` flag to override the listed default environment variables, they still show up under extra if non defined target - by :user:`gaborbernat` `#720 `_ - tox ignores unknown CLI arguments when provisioning is on and outside of the provisioned environment (allowing provisioning arguments to be forwarded freely) - by :user:`gaborbernat` `#1270 `_ Features ^^^^^^^^ - Virtual environments created now no longer upgrade pip/wheel/setuptools to the latest version. Instead the start packages after virtualenv creation now is whatever virtualenv has bundled in. This allows faster virtualenv creation and builds that are easier to reproduce. `#448 `_ - Improve python discovery and add architecture support: - UNIX: - First, check if the tox host Python matches. - Second, check if the the canonical name (e.g. ``python3.7``, ``python3``) matches or the base python is an absolute path, use that. - Third, check if the the canonical name without version matches (e.g. ``python``, ``pypy``) matches. - Windows: - First, check if the tox host Python matches. - Second, use the ``py.exe`` to list registered interpreters and any of those match. - Third, check if the the canonical name (e.g. ``python3.7``, ``python3``) matches or the base python is an absolute path, use that. - Fourth, check if the the canonical name without version matches (e.g. ``python``, ``pypy``) matches. - Finally, check for known locations (``c:\python{major}{minor}\python.exe``). tox environment configuration generation is now done in parallel (to alleviate the slowdown due to extra checks). `#1290 `_ v3.9.0 (2019-04-17) ------------------- Bugfixes ^^^^^^^^ - Fix ``congratulations`` when using ``^C`` during virtualenv creation - by :user:`asottile` `#1257 `_ Features ^^^^^^^^ - Allow having inline comments in :conf:`deps` — by :user:`webknjaz` `#1262 `_ v3.8.6 (2019-04-03) ------------------- Bugfixes ^^^^^^^^ - :conf:`parallel_show_output` does not work with tox 3.8 `#1245 `_ v3.8.5 (2019-04-03) ------------------- Bugfixes ^^^^^^^^ - the isolated build env now ignores :conf:`sitepackages`, :conf:`deps` and :conf:`description` as these do not make sense - by :user:`gaborbernat` `#1239 `_ - Do not print timings with more than 3 decimal digits on Python 3 - by :user:`mgedmin`. `#1241 `_ v3.8.4 (2019-04-01) ------------------- Bugfixes ^^^^^^^^ - Fix sdist creation on python2.x when there is non-ascii output. `#1234 `_ - fix typos in isolated.py that made it impossible to install package with requirements in pyproject.toml - by :user:`unmade` `#1236 `_ v3.8.3 (2019-03-29) ------------------- Bugfixes ^^^^^^^^ - don't crash when version information is not available for a proposed base python - by :user:`gaborbernat` `#1227 `_ - Do not print exception traceback when the provisioned tox fails - by :user:`gaborbernat` `#1228 `_ v3.8.2 (2019-03-29) ------------------- Bugfixes ^^^^^^^^ - using -v and -e connected (as -ve) fails - by :user:`gaborbernat` `#1218 `_ - Changes to the plugin tester module (cmd no longer sets ``PYTHONPATH``), and ``action.popen`` no longer returns the command identifier information from within the logs. No public facing changes. `#1222 `_ - Spinner fails in CI on ``UnicodeEncodeError`` - by :user:`gaborbernat` `#1223 `_ v3.8.1 (2019-03-28) ------------------- Bugfixes ^^^^^^^^ - The ``-eALL`` command line argument now expands the ``envlist`` key and includes all its environment. `#1155 `_ - Isolated build environment dependency overrides were not taken in consideration (and such it inherited the deps from the testenv section) - by :user:`gaborbernat` `#1207 `_ - ``--result-json`` puts the command into setup section instead of test (pre and post commands are now also correctly put into the commands section) - by :user:`gaborbernat` `#1210 `_ - Set ``setup.cfg`` encoding to UTF-8 as it contains Unicode characters. `#1212 `_ - Fix tox CI, better error reporting when locating via the py fails - by :user:`gaborbernat` `#1215 `_ v3.8.0 (2019-03-27) ------------------- Bugfixes ^^^^^^^^ - In a posix shell, setting the PATH environment variable to an empty value is equivalent to not setting it at all; therefore we no longer if the user sets PYTHONPATH an empty string on python 3.4 or later - by :user:`gaborbernat`. `#1092 `_ - Fixed bug of children process calls logs clashing (log already exists) - by :user:`gaborbernat` `#1137 `_ - Interpreter discovery and virtualenv creation process calls that failed will now print out on the screen their output (via the logfile we automatically save) - by :user:`gaborbernat` `#1150 `_ - Using ``py2`` and ``py3`` with a specific ``basepython`` will no longer raise a warning unless the major version conflicts - by :user:`demosdemon`. `#1153 `_ - Fix missing error for ``tox -e unknown`` when tox.ini declares ``envlist``. - by :user:`medmunds` `#1160 `_ - Resolve symlinks with ``toxworkdir`` - by :user:`blueyed`. `#1169 `_ - Interrupting a tox call (e.g. via CTRL+C) now will ensure that spawn child processes (test calls, interpreter discovery, parallel sub-instances, provisioned hosts) are correctly stopped before exiting (via the pattern of INTERRUPT - 300 ms, TERMINATE - 200 ms, KILL signals) - by :user:`gaborbernat` `#1172 `_ - Fix a ``ResourceWarning: unclosed file`` in ``Action`` - by :user:`BoboTiG`. `#1179 `_ - Fix deadlock when using ``--parallel`` and having environments with lots of output - by :user:`asottile`. `#1183 `_ - Removed code that sometimes caused a difference in results between ``--parallel`` and ``-p`` when using ``posargs`` - by :user:`timdaman` `#1192 `_ Features ^^^^^^^^ - tox now auto-provisions itself if needed (see :ref:`auto-provision`). Plugins or minimum version of tox no longer need to be manually satisfied by the user, increasing their ease of use. - by :user:`gaborbernat` `#998 `_ - tox will inject the ``TOX_PARALLEL_ENV`` environment variable, set to the current running tox environment name, only when running in parallel mode. - by :user:`gaborbernat` `#1139 `_ - Parallel children now save their output to a disk logfile - by :user:`gaborbernat` `#1143 `_ - Parallel children now are added to ``--result-json`` - by :user:`gaborbernat` `#1159 `_ - Display pattern and ``sys.platform`` with platform mismatch - by :user:`blueyed`. `#1176 `_ - Setting the environment variable ``TOX_REPORTER_TIMESTAMP`` to ``1`` will enable showing for each output line its delta since the tox startup. This can be especially handy when debugging parallel runs.- by :user:`gaborbernat` `#1203 `_ Documentation ^^^^^^^^^^^^^ - Add a ``poetry`` examples to packaging - by :user:`gaborbernat` `#1163 `_ v3.7.0 (2019-01-11) ------------------- Features ^^^^^^^^ - Parallel mode added (alternative to ``detox`` which is being deprecated), for more details see :ref:`parallel_mode` - by :user:`gaborbernat`. `#439 `_ - Added command line shortcut ``-s`` for ``--skip-missing-interpreters`` - by :user:`evandrocoan` `#1119 `_ Deprecations (removal in next major release) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - Whitelisting of externals will be mandatory in tox 4: issue a deprecation warning as part of the already existing warning - by :user:`obestwalter` `#1129 `_ Documentation ^^^^^^^^^^^^^ - Clarify explanations in examples and avoid unsupported end line comments - by :user:`obestwalter` `#1110 `_ - Set to PULL_REQUEST_TEMPLATE.md use relative instead of absolute URLs - by :user:`evandrocoan` Fixed PULL_REQUEST_TEMPLATE.md path for changelog/examples.rst to docs/changelog/examples.rst - by :user:`evandrocoan` `#1120 `_ v3.6.1 (2018-12-24) ------------------- Features ^^^^^^^^ - if the packaging phase successfully builds a package set it as environment variable under ``TOX_PACKAGE`` (useful to make assertions on the built package itself, instead of just how it ends up after installation) - by :user:`gaborbernat` (`#1081 `_) v3.6.0 (2018-12-13) ------------------- Bugfixes ^^^^^^^^ - On windows, check ``sys.executable`` before others for interpreter version lookup. This matches what happens on non-windows. (`#1087 `_) - Don't rewrite ``{posargs}`` substitution for absolute paths. (`#1095 `_) - Correctly fail ``tox --notest`` when setup fails. (`#1097 `_) Documentation ^^^^^^^^^^^^^ - Update Contributor Covenant URL to use https:// - by :user:`jdufresne`. (`#1082 `_) - Correct the capitalization of PyPI throughout the documentation - by :user:`jdufresne`. (`#1084 `_) - Link to related projects (Invoke and Nox) from the documentation - by :user:`theacodes`. (`#1088 `_) Miscellaneous ^^^^^^^^^^^^^ - Include the license file in the wheel distribution - by :user:`jdufresne`. (`#1083 `_) v3.5.3 (2018-10-28) ------------------- Bugfixes ^^^^^^^^ - Fix bug with incorrectly defactorized dependencies - by :user:`bartsanchez` (`#706 `_) - do the same transformation to ``egg_info`` folders that ``pkg_resources`` does; this makes it possible for hyphenated names to use the ``develop-inst-noop`` optimization (cf. 910), which previously only worked with non-hyphenated egg names - by :user:`hashbrowncipher` (`#1051 `_) - previously, if a project's ``setup.py --name`` emitted extra information to stderr, tox would capture it and consider it part of the project's name; now, emissions to stderr are printed to the console - by :user:`hashbrowncipher` (`#1052 `_) - change the way we acquire interpreter information to make it compatible with ``jython`` interpreter, note to create jython envs one needs ``virtualenv > 16.0`` which will be released later :user:`gaborbernat` (`#1073 `_) Documentation ^^^^^^^^^^^^^ - document substitutions with additional content starting with a space cannot be alone on a line inside the ini file - by :user:`gaborbernat` (`#437 `_) - change the spelling of a single word from contrains to the proper word, constraints - by :user:`metasyn` (`#1061 `_) - Mention the minimum version required for ``commands_pre``/``commands_post`` support. (`#1071 `_) v3.5.2 (2018-10-09) ------------------- Bugfixes ^^^^^^^^ - session packages are now put inside a numbered directory (instead of prefix numbering it, because pip fails when wheels are not named according to `PEP-491 `_, and prefix numbering messes with this) - by :user:`gaborbernat` (`#1042 `_) Features ^^^^^^^^ - level three verbosity (``-vvv``) show the packaging output - by :user:`gaborbernat` (`#1047 `_) v3.5.1 (2018-10-08) ------------------- Bugfixes ^^^^^^^^ - fix regression with ``3.5.0``: specifying ``--installpkg`` raises ``AttributeError: 'str' object has no attribute 'basename'`` (`#1042 `_) v3.5.0 (2018-10-08) ------------------- Bugfixes ^^^^^^^^ - intermittent failures with ``--parallel--safe-build``, instead of mangling with the file paths now uses a lock to make the package build operation thread safe and is now on by default (``--parallel--safe-build`` is now deprecated) - by :user:`gaborbernat` (`#1026 `_) Features ^^^^^^^^ - Added ``temp_dir`` folder configuration (defaults to ``{toxworkdir}/.tmp``) that contains tox temporary files. Package builds now create a hard link (if possible, otherwise copy - notably in case of Windows Python 2.7) to the built file, and feed that file downstream (e.g. for pip to install it). The hard link is removed at the end of the run (what it points though is kept inside ``distdir``). This ensures that a tox session operates on the same package it built, even if a parallel tox run builds another version. Note ``distdir`` will contain only the last built package in such cases. - by :user:`gaborbernat` (`#1026 `_) Documentation ^^^^^^^^^^^^^ - document tox environment recreate rules (:ref:`recreate`) - by :user:`gaborbernat` (`#93 `_) - document inside the ``--help`` how to disable colorized output via the ``PY_COLORS`` operating system environment variable - by :user:`gaborbernat` (`#163 `_) - document all global tox flags and a more concise format to express default and type - by :user:`gaborbernat` (`#683 `_) - document command line interface under the config section `cli `_ - by :user:`gaborbernat` (`#829 `_) v3.4.0 (2018-09-20) ------------------- Bugfixes ^^^^^^^^ - add ``--exists-action w`` to default pip flags to handle better VCS dependencies (`pip documentation on this `_) - by :user:`gaborbernat` (`#503 `_) - instead of assuming the Python version from the base python name ask the interpreter to reveal the version for the ``ignore_basepython_conflict`` flag - by :user:`gaborbernat` (`#908 `_) - PEP-517 packaging fails with sdist already exists, fixed via ensuring the dist folder is empty before invoking the backend and `pypa/setuptools 1481 `_ - by :user:`gaborbernat` (`#1003 `_) Features ^^^^^^^^ - add ``commands_pre`` and ``commands_post`` that run before and after running the ``commands`` (setup runs always, commands only if setup succeeds, teardown always - all run until the first failing command) - by :user:`gaborbernat` (`#167 `_) - ``pyproject.toml`` config support initially by just inline the tox.ini under ``tool.tox.legacy_tox_ini`` key; config source priority order is ``pyproject.toml``, ``tox.ini`` and then ``setup.cfg`` - by :user:`gaborbernat` (`#814 `_) - use the os environment variable ``TOX_SKIP_ENV`` to filter out tox environment names from the run list (set by ``envlist``) - by :user:`gaborbernat` (`#824 `_) - always set ``PIP_USER=0`` (do not install into the user site package, but inside the virtual environment created) and ``PIP_NO_DEPS=0`` (installing without dependencies can cause broken package installations) inside tox - by :user:`gaborbernat` (`#838 `_) - tox will inject some environment variables that to indicate a command is running within tox: ``TOX_WORK_DIR`` env var is set to the tox work directory, ``TOX_ENV_NAME`` is set to the current running tox environment name, ``TOX_ENV_DIR`` is set to the current tox environments working dir - by :user:`gaborbernat` (`#847 `_) - While running tox invokes various commands (such as building the package, pip installing dependencies and so on), these were printed in case they failed as Python arrays. Changed the representation to a shell command, allowing the users to quickly replicate/debug the failure on their own - by :user:`gaborbernat` (`#851 `_) - skip missing interpreters value from the config file can now be overridden via the ``--skip-missing-interpreters`` cli flag - by :user:`gaborbernat` (`#903 `_) - keep additional environments config order when listing them - by :user:`gaborbernat` (`#921 `_) - allow injecting config value inside the ini file dependent of the fact that we're connected to an interactive shell or not - by :user:`gaborbernat` (`#947 `_) - do not build sdist if skip install is specified for the envs to be run - by :user:`gaborbernat` (`#974 `_) - when verbosity level increases above two start passing through verbosity flags to pip - by :user:`gaborbernat` (`#982 `_) - when discovering the interpreter to use check if the tox host Python matches and use that if so - by :user:`gaborbernat` (`#994 `_) - ``-vv`` will print out why a virtual environment is re-created whenever this operation is triggered - by :user:`gaborbernat` (`#1004 `_) Documentation ^^^^^^^^^^^^^ - clarify that ``python`` and ``pip`` refer to the virtual environments executable - by :user:`gaborbernat` (`#305 `_) - add Sphinx and mkdocs example of generating documentation via tox - by :user:`gaborbernat` (`#374 `_) - specify that ``setup.cfg`` tox configuration needs to be inside the ``tox:tox`` namespace - by :user:`gaborbernat` (`#545 `_) v3.3.0 (2018-09-11) ------------------- Bugfixes ^^^^^^^^ - fix ``TOX_LIMITED_SHEBANG`` when running under python3 - by :user:`asottile` (`#931 `_) Features ^^^^^^^^ - `PEP-517 `_ source distribution support (create a ``.package`` virtual environment to perform build operations inside) by :user:`gaborbernat` (`#573 `_) - `flit `_ support via implementing ``PEP-517`` by :user:`gaborbernat` (`#820 `_) - packaging now is exposed as a hook via ``tox_package(session, venv)`` - by :user:`gaborbernat` (`#951 `_) Miscellaneous ^^^^^^^^^^^^^ - Updated the VSTS build YAML to use the latest jobs and pools syntax - by :user:`davidstaheli` (`#955 `_) v3.2.1 (2018-08-10) ------------------- Bugfixes ^^^^^^^^ - ``--parallel--safe-build`` no longer cleans up its folders (``distdir``, ``distshare``, ``log``). - by :user:`gaborbernat` (`#849 `_) v3.2.0 (2018-08-10) ------------------- Features ^^^^^^^^ - Switch pip invocations to use the module ``-m pip`` instead of direct invocation. This could help avoid some of the shebang limitations. - by :user:`gaborbernat` (`#935 `_) - Ability to specify package requirements for the tox run via the ``tox.ini`` (``tox`` section under key ``requires`` - PEP-508 style): can be used to specify both plugin requirements or build dependencies. - by :user:`gaborbernat` (`#783 `_) - Allow one to run multiple tox instances in parallel by providing the ``--parallel--safe-build`` flag. - by :user:`gaborbernat` (`#849 `_) v3.1.3 (2018-08-03) ------------------- Bugfixes ^^^^^^^^ - A caching issue that caused the ``develop-inst-nodeps`` action, which reinstalls the package under test, to always run has been resolved. The ``develop-inst-noop`` action, which, as the name suggests, is a no-op, will now run unless there are changes to ``setup.py`` or ``setup.cfg`` files that have not been reflected - by @stephenfin (`#909 `_) Features ^^^^^^^^ - Python version testenvs are now automatically detected instead of comparing against a hard-coded list of supported versions. This enables ``py38`` and eventually ``py39`` / ``py40`` / etc. to work without requiring an upgrade to ``tox``. As such, the following public constants are now deprecated (and scheduled for removal in ``tox`` 4.0: ``CPYTHON_VERSION_TUPLES``, ``PYPY_VERSION_TUPLES``, ``OTHER_PYTHON_INTERPRETERS``, and ``DEFAULT_FACTORS`` - by :user:`asottile` (`#914 `_) Documentation ^^^^^^^^^^^^^ - Add a system overview section on the index page that explains briefly how tox works - by :user:`gaborbernat`. (`#867 `_) v3.1.2 (2018-07-12) ------------------- Bugfixes ^^^^^^^^ - Revert "Fix bug with incorrectly defactorized dependencies (`#772 `_)" due to a regression (`(#799) `_) - by :user:`obestwalter` v3.1.1 (2018-07-09) ------------------- Bugfixes ^^^^^^^^ - PyPI documentation for ``3.1.0`` is broken. Added test to check for this, and fix it by :user:`gaborbernat`. (`#879 `_) v3.1.0 (2018-07-08) ------------------- Bugfixes ^^^^^^^^ - Add ``ignore_basepython_conflict``, which determines whether conflicting ``basepython`` settings for environments containing default factors, such as ``py27`` or ``django18-py35``, should be ignored or result in warnings. This was a common source of misconfiguration and is rarely, if ever, desirable from a user perspective - by :user:`stephenfin` (`#477 `_) - Fix bug with incorrectly defactorized dependencies (deps passed to pip were not de-factorized) - by :user:`bartsanchez` (`#706 `_) Features ^^^^^^^^ - Add support for multiple PyPy versions using default factors. This allows you to use, for example, ``pypy27`` knowing that the correct interpreter will be used by default - by :user:`stephenfin` (`#19 `_) - Add support to explicitly invoke interpreter directives for environments with long path lengths. In the event that ``tox`` cannot invoke scripts with a system-limited shebang (e.x. a Linux host running a Jenkins Pipeline), a user can set the environment variable ``TOX_LIMITED_SHEBANG`` to workaround the system's limitation (e.x. ``export TOX_LIMITED_SHEBANG=1``) - by :user:`jdknight` (`#794 `_) - introduce a constants module to be used internally and as experimental API - by :user:`obestwalter` (`#798 `_) - Make ``py2`` and ``py3`` aliases also resolve via ``py`` on windows by :user:`asottile`. This enables the following things: ``tox -e py2`` and ``tox -e py3`` work on windows (they already work on posix); and setting ``basepython=python2`` or ``basepython=python3`` now works on windows. (`#856 `_) - Replace the internal version parsing logic from the not well tested `PEP-386 `_ parser for the more general `PEP-440 `_. `packaging >= 17.1 `_ is now an install dependency by :user:`gaborbernat`. (`#860 `_) Documentation ^^^^^^^^^^^^^ - extend the plugin documentation and make lot of small fixes and improvements - by :user:`obestwalter` (`#797 `_) - tidy up tests - remove unused fixtures, update old cinstructs, etc. - by :user:`obestwalter` (`#799 `_) - Various improvements to documentation: open browser once documentation generation is done, show Github/Travis info on documentation page, remove duplicate header for changelog, generate unreleased news as DRAFT on top of changelog, make the changelog page more compact and readable (width up to 1280px) by :user:`gaborbernat` (`#859 `_) Miscellaneous ^^^^^^^^^^^^^ - filter out unwanted files in package - by :user:`obestwalter` (`#754 `_) - make the already existing implicit API explicit - by :user:`obestwalter` (`#800 `_) - improve tox quickstart and corresponding tests - by :user:`obestwalter` (`#801 `_) - tweak codecov settings via .codecov.yml - by :user:`obestwalter` (`#802 `_) v3.0.0 (2018-04-02) ------------------- Bugfixes ^^^^^^^^ - Write directly to stdout buffer if possible to prevent str vs bytes issues - by @asottile (`#426 `_) - fix #672 reporting to json file when skip-missing-interpreters option is used - by @r2dan (`#672 `_) - avoid ``Requested Python version (X.Y) not installed`` stderr output when a Python environment is looked up using the ``py`` Python launcher on Windows and the environment is not found installed on the system - by @jurko-gospodnetic (`#692 `_) - Fixed an issue where invocation of tox from the Python package, where invocation errors (failed actions) occur results in a change in the sys.stdout stream encoding in Python 3.x. New behaviour is that sys.stdout is reset back to its original encoding after invocation errors - by @tonybaloney (`#723 `_) - The reading of command output sometimes failed with ``IOError: [Errno 0] Error`` on Windows, this was fixed by using a simpler method to update the read buffers. - by @fschulze (`#727 `_) - (only affected rc releases) fix up tox.cmdline to be callable without args - by @gaborbernat. (`#773 `_) - (only affected rc releases) Revert breaking change of tox.cmdline not callable with no args - by @gaborbernat. (`#773 `_) - (only affected rc releases) fix #755 by reverting the ``cmdline`` import to the old location and changing the entry point instead - by @fschulze (`#755 `_) Features ^^^^^^^^ - ``tox`` displays exit code together with ``InvocationError`` - by @blueyed and @ederag. (`#290 `_) - Hint for possible signal upon ``InvocationError``, on posix systems - by @ederag and @asottile. (`#766 `_) - Add a ``-q`` option to progressively silence tox's output. For each time you specify ``-q`` to tox, the output provided by tox reduces. This option allows you to see only your command output without the default verbosity of what tox is doing. This also counter-acts usage of ``-v``. For example, running ``tox -v -q ...`` will provide you with the default verbosity. ``tox -vv -q`` is equivalent to ``tox -v``. By @sigmavirus24 (`#256 `_) - add support for negated factor conditions, e.g. ``!dev: production_log`` - by @jurko-gospodnetic (`#292 `_) - Headings like ``installed: `` will not be printed if there is no output to display after the :, unless verbosity is set. By @cryvate (`#601 `_) - Allow spaces in command line options to pip in deps. Where previously only ``deps=-rreq.txt`` and ``deps=--requirement=req.txt`` worked, now also ``deps=-r req.txt`` and ``deps=--requirement req.txt`` work - by @cryvate (`#668 `_) - drop Python ``2.6`` and ``3.3`` support: ``setuptools`` dropped supporting these, and as we depend on it we'll follow up with doing the same (use ``tox <= 2.9.1`` if you still need this support) - by @gaborbernat (`#679 `_) - Add tox_runenvreport as a possible plugin, allowing the overriding of the default behaviour to execute a command to get the installed packages within a virtual environment - by @tonybaloney (`#725 `_) - Forward ``PROCESSOR_ARCHITECTURE`` by default on Windows to fix ``platform.machine()``. (`#740 `_) Documentation ^^^^^^^^^^^^^ - Change favicon to the vector beach ball - by @hazalozturk (`#748 `_) - Change sphinx theme to alabaster and add logo/favicon - by @hazalozturk (`#639 `_) Miscellaneous ^^^^^^^^^^^^^ - Running ``tox`` without a ``setup.py`` now has a more friendly error message and gives troubleshooting suggestions - by @Volcyy. (`#331 `_) - Fix pycodestyle (formerly pep8) errors E741 (ambiguous variable names, in this case, 'l's) and remove ignore of this error in tox.ini - by @cryvate (`#663 `_) - touched up ``interpreters.py`` code and added some missing tests for it - by @jurko-gospodnetic (`#708 `_) - The ``PYTHONDONTWRITEBYTECODE`` environment variable is no longer unset - by @stephenfin. (`#744 `_) v2.9.1 (2017-09-29) ------------------- Miscellaneous ^^^^^^^^^^^^^ - integrated new release process and fixed changelog rendering for pypi.org - by `@obestwalter `_. v2.9.0 (2017-09-29) ------------------- Features ^^^^^^^^ - ``tox --version`` now shows information about all registered plugins - by `@obestwalter `_ (`#544 `_) Bugfixes ^^^^^^^^ - ``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 `_ (`#571 `_) Miscellaneous ^^^^^^^^^^^^^ - `#635 `_ inherit from correct exception - by `@obestwalter `_ (`#635 `_). - spelling and escape sequence fixes - by `@scoop `_ (`#637 `_ and `#638 `_). - add a badge to show build status of documentation on readthedocs.io - by `@obestwalter `_. Documentation ^^^^^^^^^^^^^ - add `towncrier `_ to allow adding changelog entries with the pull requests without generating merge conflicts; with this release notes are now grouped into four distinct collections: ``Features``, ``Bugfixes``, ``Improved Documentation`` and ``Deprecations and Removals``. (`#614 `_) v2.8.2 (2017-10-09) ------------------- - `#466 `_: stop env var leakage if popen failed with resultjson or redirect v2.8.1 (2017-09-04) ------------------- - `pull request 599 `_: fix problems with implementation of `#515 `_. Substitutions from other sections were not made anymore if they were not in ``envlist``. Thanks to Clark Boylan (`@cboylan `_) for helping to get this fixed (`pull request 597 `_). v2.8.0 (2017-09-01) -------------------- - `#276 `_: Remove easy_install from docs (TL;DR: use pip). Thanks Martin Andrysík (`@sifuraz `_). - `#301 `_: Expand nested substitutions in ``tox.ini``. Thanks `@vlaci `_. Thanks to Eli Collins (`@eli-collins `_) for creating a reproducer. - `#315 `_: add ``--help`` and ``--version`` to helptox-quickstart. Thanks `@vlaci `_. - `#326 `_: Fix ``OSError`` 'Not a directory' when creating env on Jython 2.7.0. Thanks Nick Douma (`@LordGaav `_). - `#429 `_: Forward ``MSYSTEM`` by default on Windows. Thanks Marius Gedminas (`@mgedmin `_) for reporting this. - `#449 `_: add multi platform example to the docs. Thanks Aleks Bunin (`@sashkab `_) and `@rndr `_. - `#474 `_: Start using setuptools_scm for tag based versioning. - `#484 `_: Renamed ``py.test`` to ``pytest`` throughout the project. Thanks Slam (`@3lnc `_). - `#504 `_: With ``-a``: do not show additional environments header if there are none. Thanks `@rndr `_. - `#515 `_: Don't require environment variables in test environments where they are not used. Thanks André Caron (`@AndreLouisCaron `_). - `#517 `_: Forward ``NUMBER_OF_PROCESSORS`` by default on Windows to fix ``multiprocessor.cpu_count()``. Thanks André Caron (`@AndreLouisCaron `_). - `#518 `_: Forward ``USERPROFILE`` by default on Windows. Thanks André Caron (`@AndreLouisCaron `_). - `pull request 528 `_: Fix some of the warnings displayed by pytest 3.1.0. Thanks Bruno Oliveira (`@nicoddemus `_). - `pull request 547 `_: Add regression test for `#137 `_. Thanks Martin Andrysík (`@sifuraz `_). - `pull request 553 `_: Add an XFAIL test to reproduce upstream bug `#203 `_. Thanks Bartolomé Sánchez Salado (`@bartsanchez `_). - `pull request 556 `_: Report more meaningful errors on why virtualenv creation failed. Thanks `@vlaci `_. Also thanks to Igor Sadchenko (`@igor-sadchenko `_) for pointing out a problem with that PR before it hit the masses ☺ - `pull request 575 `_: Add announcement doc to end all announcement docs (using only ``CHANGELOG`` and Github issues since 2.5 already). - `pull request 580 `_: Do not ignore Sphinx warnings anymore. Thanks Bernát Gábor (`@gaborbernat `_). - `pull request 585 `_: Expand documentation to explain pass through of flags from deps to pip (e.g. ``-rrequirements.txt``, ``-cconstraints.txt``). Thanks Alexander Loechel (`@loechel `_). - `pull request 588 `_: Run pytest wit xfail_strict and adapt affected tests. v2.7.0 (2017-04-02) ------------------- - `pull request 450 `_: Stop after the first installdeps and first testenv create hooks succeed. This changes the default behaviour of ``tox_testenv_create`` and ``tox_testenv_install_deps`` to not execute other registered hooks when the first hook returns a result that is not ``None``. Thanks Anthony Sottile (`@asottile `_). - `#271 `_ and `#464 `_: Improve environment information for users. New command line parameter: ``-a`` show **all** defined environments - not just the ones defined in (or generated from) envlist. New verbosity settings for ``-l`` and ``-a``: show user defined descriptions of the environments. This also works for generated environments from factors by concatenating factor descriptions into a complete description. Note that for backwards compatibility with scripts using the output of ``-l`` it's output remains unchanged. Thanks Bernát Gábor (`@gaborbernat `_). - `#464 `_: Fix incorrect egg-info location for modified package_dir in setup.py. Thanks Selim Belhaouane (`@selimb `_). - `#431 `_: Add 'LANGUAGE' to default passed environment variables. Thanks Paweł Adamczak (`@pawelad `_). - `#455 `_: Add a Vagrantfile with a customized Arch Linux box for local testing. Thanks Oliver Bestwalter (`@obestwalter `_). - `#454 `_: Revert `pull request 407 `_, empty commands is not treated as an error. Thanks Anthony Sottile (`@asottile `_). - `#446 `_: (infrastructure) Travis CI tests for tox now also run on OS X now. Thanks Jason R. Coombs (`@jaraco `_). v2.6.0 (2017-02-04) ------------------- - add "alwayscopy" config option to instruct virtualenv to always copy files instead of symlinking. Thanks Igor Duarte Cardoso (`@igordcard `_). - pass setenv variables to setup.py during a usedevelop install. Thanks Eli Collins (`@eli-collins `_). - replace all references to testrun.org with readthedocs ones. Thanks Oliver Bestwalter (`@obestwalter `_). - fix `#323 `_ by avoiding virtualenv14 is not used on py32 (although we don't officially support py32). Thanks Jason R. Coombs (`@jaraco `_). - add Python 3.6 to envlist and CI. Thanks Andrii Soldatenko (`@andriisoldatenko `_). - fix glob resolution from TOX_TESTENV_PASSENV env variable Thanks Allan Feldman (`@a-feld `_). v2.5.0 (2016-11-16) ------------------- - slightly backward incompatible: fix `#310 `_: the {posargs} substitution now properly preserves the tox command line positional arguments. Positional arguments with spaces are now properly handled. NOTE: if your tox invocation previously used extra quoting for positional arguments to work around `#310 `_, you need to remove the quoting. Example: tox -- "'some string'" # has to now be written simply as tox -- "some string" thanks holger krekel. You can set ``minversion = 2.5.0`` in the ``[tox]`` section of ``tox.ini`` to make sure people using your tox.ini use the correct version. - fix `#359 `_: add COMSPEC to default passenv on windows. Thanks `@anthrotype `_. - add support for py36 and py37 and add py36-dev and py37(nightly) to travis builds of tox. Thanks John Vandenberg. - fix `#348 `_: add py2 and py3 as default environments pointing to "python2" and "python3" basepython executables. Also fix `#347 `_ by updating the list of default envs in the tox basic example. Thanks Tobias McNulty. - make "-h" and "--help-ini" options work even if there is no tox.ini, thanks holger krekel. - add {:} substitution, which is replaced with os-specific path separator, thanks Lukasz Rogalski. - fix `#305 `_: ``downloadcache`` test env config is now ignored as pip-8 does caching by default. Thanks holger krekel. - output from install command in verbose (-vv) mode is now printed to console instead of being redirected to file, thanks Lukasz Rogalski - fix `#399 `_. Make sure {envtmpdir} is created if it doesn't exist at the start of a testenvironment run. Thanks Manuel Jacob. - fix `#316 `_: Lack of commands key in ini file is now treated as an error. Reported virtualenv status is 'nothing to do' instead of 'commands succeeded', with relevant error message displayed. Thanks Lukasz Rogalski. v2.4.1 (2016-10-12) ------------------- - fix `#380 `_: properly perform substitution again. Thanks Ian Cordasco. v2.4.0 (2016-10-12) ------------------- - remove PYTHONPATH from environment during the install phase because a tox-run should not have hidden dependencies and the test commands will also not see a PYTHONPATH. If this causes unforeseen problems it may be reverted in a bugfix release. Thanks Jason R. Coombs. - fix `#352 `_: prevent a configuration where envdir==toxinidir and refine docs to warn people about changing "envdir". Thanks Oliver Bestwalter and holger krekel. - fix `#375 `_, fix `#330 `_: warn against tox-setup.py integration as "setup.py test" should really just test with the current interpreter. Thanks Ronny Pfannschmidt. - fix `#302 `_: allow cross-testenv substitution where we substitute with ``{x,y}`` generative syntax. Thanks Andrew Pashkin. - fix `#212 `_: allow escaping curly brace chars "\{" and "\}" if you need the chars "{" and "}" to appear in your commands or other ini values. Thanks John Vandenberg. - addresses `#66 `_: add --workdir option to override where tox stores its ".tox" directory and all of the virtualenv environment. Thanks Danring. - introduce per-venv list_dependencies_command which defaults to "pip freeze" to obtain the list of installed packages. Thanks Ted Shaw, Holger Krekel. - close `#66 `_: add documentation to jenkins page on how to avoid "too long shebang" lines when calling pip from tox. Note that we can not use "python -m pip install X" by default because the latter adds the CWD and pip will think X is installed if it is there. "pip install X" does not do that. - new list_dependencies_command to influence how tox determines which dependencies are installed in a testenv. - (experimental) New feature: When a search for a config file fails, tox tries loading setup.cfg with a section prefix of "tox". - fix `#275 `_: Introduce hooks ``tox_runtest_pre``` and ``tox_runtest_post`` which run before and after the tests of a venv, respectively. Thanks to Matthew Schinckel and itxaka serrano. - fix `#317 `_: evaluate minversion before tox config is parsed completely. Thanks Sachi King for the PR. - added the "extras" environment option to specify the extras to use when doing the sdist or develop install. Contributed by Alex Grönholm. - use pytest-catchlog instead of pytest-capturelog (latter is not maintained, uses deprecated pytest API) v2.3.2 (2016-02-11) ------------------- - fix `#314 `_: fix command invocation with .py scripts on windows. - fix `#279 `_: allow cross-section substitution when the value contains posargs. Thanks Sachi King for the PR. v2.3.1 (2015-12-14) ------------------- - fix `#294 `_: re-allow cross-section substitution for setenv. v2.3.0 (2015-12-09) ------------------- - DEPRECATE use of "indexservers" in tox.ini. It complicates the internal code and it is recommended to rather use the devpi system for managing indexes for pip. - fix `#285 `_: make setenv processing fully lazy to fix regressions of tox-2.2.X and so that we can now have testenv attributes like "basepython" depend on environment variables that are set in a setenv section. Thanks Nelfin for some tests and initial work on a PR. - allow "#" in commands. This is slightly incompatible with commands sections that used a comment after a "\" line continuation. Thanks David Stanek for the PR. - fix `#289 `_: fix build_sphinx target, thanks Barry Warsaw. - fix `#252 `_: allow environment names with special characters. Thanks Julien Castets for initial PR and patience. - introduce experimental tox_testenv_create(venv, action) and tox_testenv_install_deps(venv, action) hooks to allow plugins to do additional work on creation or installing deps. These hooks are experimental mainly because of the involved "venv" and session objects whose current public API is not fully guaranteed. - internal: push some optional object creation into tests because tox core doesn't need it. v2.2.1 (2015-12-09) ------------------- - fix bug where {envdir} substitution could not be used in setenv if that env value is then used in {basepython}. Thanks Florian Bruhin. v2.2.0 (2015-11-11) ------------------- - fix `#265 `_ and add LD_LIBRARY_PATH to passenv on linux by default because otherwise the python interpreter might not start up in certain configurations (redhat software collections). Thanks David Riddle. - fix `#246 `_: fix regression in config parsing by reordering such that {envbindir} can be used again in tox.ini. Thanks Olli Walsh. - fix `#99 `_: the {env:...} substitution now properly uses environment settings from the ``setenv`` section. Thanks Itxaka Serrano. - fix `#281 `_: make --force-dep work when urls are present in dependency configs. Thanks Glyph Lefkowitz for reporting. - fix `#174 `_: add new ``ignore_outcome`` testenv attribute which can be set to True in which case it will produce a warning instead of an error on a failed testenv command outcome. Thanks Rebecka Gulliksson for the PR. - fix `#280 `_: properly skip missing interpreter if {envsitepackagesdir} is present in commands. Thanks BB:ceridwenv v2.1.1 (2015-06-23) ------------------- - fix platform skipping for detox - report skipped platforms as skips in the summary v2.1.0 (2015-06-19) ------------------- - fix `#258 `_, fix `#248 `_, fix `#253 `_: for non-test commands (installation, venv creation) we pass in the full invocation environment. - remove experimental --set-home option which was hardly used and hackily implemented (if people want home-directory isolation we should figure out a better way to do it, possibly through a plugin) - fix `#259 `_: passenv is now a line-list which allows interspersing comments. Thanks stefano-m. - allow envlist to be a multi-line list, to intersperse comments and have long envlist settings split more naturally. Thanks Andre Caron. - introduce a TOX_TESTENV_PASSENV setting which is honored when constructing the set of environment variables for test environments. Thanks Marc Abramowitz for pushing in this direction. v2.0.2 (2015-06-03) ------------------- - fix `#247 `_: tox now passes the LANG variable from the tox invocation environment to the test environment by default. - add SYSTEMDRIVE into default passenv on windows to allow pip6 to work. Thanks Michael Krause. v2.0.1 (2015-05-13) ------------------- - fix wheel packaging to properly require argparse on py26. v2.0.0 (2015-05-12) ------------------- - (new) introduce environment variable isolation: tox now only passes the PATH and PIP_INDEX_URL variable from the tox invocation environment to the test environment and on Windows also ``SYSTEMROOT``, ``PATHEXT``, ``TEMP`` and ``TMP`` whereas on unix additionally ``TMPDIR`` is passed. If you need to pass through further environment variables you can use the new ``passenv`` setting, a space-separated list of environment variable names. Each name can make use of fnmatch-style glob patterns. All environment variables which exist in the tox-invocation environment will be copied to the test environment. - a new ``--help-ini`` option shows all possible testenv settings and their defaults. - (new) introduce a way to specify on which platform a testenvironment is to execute: the new per-venv "platform" setting allows one to specify a regular expression which is matched against sys.platform. If platform is set and doesn't match the platform spec in the test environment the test environment is ignored, no setup or tests are attempted. - (new) add per-venv "ignore_errors" setting, which defaults to False. If ``True``, a non-zero exit code from one command will be ignored and further commands will be executed (which was the default behavior in tox < 2.0). If ``False`` (the default), then a non-zero exit code from one command will abort execution of commands for that environment. - show and store in json the version dependency information for each venv - remove the long-deprecated "distribute" option as it has no effect these days. - fix `#233 `_: avoid hanging with tox-setuptools integration example. Thanks simonb. - fix `#120 `_: allow substitution for the commands section. Thanks Volodymyr Vitvitski. - fix `#235 `_: fix AttributeError with --installpkg. Thanks Volodymyr Vitvitski. - tox has now somewhat pep8 clean code, thanks to Volodymyr Vitvitski. - fix `#240 `_: allow one to specify empty argument list without it being rewritten to ".". Thanks Daniel Hahler. - introduce experimental (not much documented yet) plugin system based on pytest's externalized "pluggy" system. See tox/hookspecs.py for the current hooks. - introduce parser.add_testenv_attribute() to register an ini-variable for testenv sections. Can be used from plugins through the tox_add_option hook. - rename internal files -- tox offers no external API except for the experimental plugin hooks, use tox internals at your own risk. - DEPRECATE distshare in documentation v1.9.2 (2015-03-23) ------------------- - backout ability that --force-dep substitutes name/versions in requirement files due to various issues. This fixes `#228 `_, fixes `#230 `_, fixes `#231 `_ which popped up with 1.9.1. v1.9.1 (2015-03-23) ------------------- - use a file instead of a pipe for command output in "--result-json". Fixes some termination issues with python2.6. - allow --force-dep to override dependencies in "-r" requirements files. Thanks Sontek for the PR. - fix `#227 `_: use "-m virtualenv" instead of "-mvirtualenv" to make it work with pyrun. Thanks Marc-Andre Lemburg. v1.9.0 (2015-02-24) ------------------- - fix `#193 `_: Remove ``--pre`` from the default ``install_command``; by default tox will now only install final releases from PyPI for unpinned dependencies. Use ``pip_pre = true`` in a testenv or the ``--pre`` command-line option to restore the previous behavior. - fix `#199 `_: fill resultlog structure ahead of virtualenv creation - refine determination if we run from Jenkins, thanks Borge Lanes. - echo output to stdout when ``--report-json`` is used - fix `#11 `_: add a ``skip_install`` per-testenv setting which prevents the installation of a package. Thanks Julian Krause. - fix `#124 `_: ignore command exit codes; when a command has a "-" prefix, tox will ignore the exit code of that command - fix `#198 `_: fix broken envlist settings, e.g. {py26,py27}{-lint,} - fix `#191 `_: lessen factor-use checks v1.8.1 (2014-10-24) ------------------- - fix `#190 `_: allow setenv to be empty. - allow escaping curly braces with "\". Thanks Marc Abramowitz for the PR. - allow "." names in environment names such that "py27-django1.7" is a valid environment name. Thanks Alex Gaynor and Alex Schepanovski. - report subprocess exit code when execution fails. Thanks Marius Gedminas. v1.8.0 (2014-09-24) ------------------- - new multi-dimensional configuration support. Many thanks to Alexander Schepanovski for the complete PR with docs. And to Mike Bayer and others for testing and feedback. - fix `#148 `_: remove "__PYVENV_LAUNCHER__" from os.environ when starting subprocesses. Thanks Steven Myint. - fix `#152 `_: set VIRTUAL_ENV when running test commands, thanks Florian Ludwig. - better report if we can't get version_info from an interpreter executable. Thanks Floris Bruynooghe. v1.7.2 (2014-07-15) ------------------- - fix `#150 `_: parse {posargs} more like we used to do it pre 1.7.0. The 1.7.0 behaviour broke a lot of OpenStack projects. See PR85 and the issue discussions for (far) more details, hopefully resulting in a more refined behaviour in the 1.8 series. And thanks to Clark Boylan for the PR. - fix `#59 `_: add a config variable ``skip-missing-interpreters`` as well as command line option ``--skip-missing-interpreters`` which won't fail the build if Python interpreters listed in tox.ini are missing. Thanks Alexandre Conrad for PR104. - fix `#164 `_: better traceback info in case of failing test commands. Thanks Marc Abramowitz for PR92. - support optional env variable substitution, thanks Morgan Fainberg for PR86. - limit python hashseed to 1024 on Windows to prevent possible memory errors. Thanks March Schlaich for the PR90. v1.7.1 (2014-03-28) ------------------- - fix `#162 `_: don't list python 2.5 as compatible/supported - fix `#158 `_ and fix `#155 `_: windows/virtualenv properly works now: call virtualenv through "python -m virtualenv" with the same interpreter which invoked tox. Thanks Chris Withers, Ionel Maries Cristian. v1.7.0 (2014-01-29) ------------------- - don't lookup "pip-script" anymore but rather just "pip" on windows as this is a pip implementation detail and changed with pip-1.5. It might mean that tox-1.7 is not able to install a different pip version into a virtualenv anymore. - drop Python2.5 compatibility because it became too hard due to the setuptools-2.0 dropping support. tox now has no support for creating python2.5 based environments anymore and all internal special-handling has been removed. - merged PR81: new option --force-dep which allows one to override tox.ini specified dependencies in setuptools-style. For example "--force-dep 'django<1.6'" will make sure that any environment using "django" as a dependency will get the latest 1.5 release. Thanks Bruno Oliveria for the complete PR. - merged PR125: tox now sets "PYTHONHASHSEED" to a random value and offers a "--hashseed" option to repeat a test run with a specific seed. You can also use --hashsheed=noset to instruct tox to leave the value alone. Thanks Chris Jerdonek for all the work behind this. - fix `#132 `_: removing zip_safe setting (so it defaults to false) to allow installation of tox via easy_install/eggs. Thanks Jenisys. - fix `#126 `_: depend on virtualenv>=1.11.2 so that we can rely (hopefully) on a pip version which supports --pre. (tox by default uses to --pre). also merged in PR84 so that we now call "virtualenv" directly instead of looking up interpreters. Thanks Ionel Maries Cristian. This also fixes `#140 `_. - fix `#130 `_: you can now set install_command=easy_install {opts} {packages} and expect it to work for repeated tox runs (previously it only worked when always recreating). Thanks jenisys for precise reporting. - fix `#129 `_: tox now uses Popen(..., universal_newlines=True) to force creation of unicode stdout/stderr streams. fixes a problem on specific platform configs when creating virtualenvs with Python3.3. Thanks Jorgen Schäfer or investigation and solution sketch. - fix `#128 `_: enable full substitution in install_command, thanks for the PR to Ronald Evers - rework and simplify "commands" parsing and in particular posargs substitutions to avoid various win32/posix related quoting issues. - make sure that the --installpkg option trumps any usedevelop settings in tox.ini or - introduce --no-network to tox's own test suite to skip tests requiring networks - introduce --sitepackages to force sitepackages=True in all environments. - fix `#105 `_ -- don't depend on an existing HOME directory from tox tests. v1.6.1 (2013-09-04) ------------------- - fix `#119 `_: {envsitepackagesdir} is now correctly computed and has a better test to prevent regression. - fix `#116 `_: make 1.6 introduced behaviour of changing to a per-env HOME directory during install activities dependent on "--set-home" for now. Should re-establish the old behaviour when no option is given. - fix `#118 `_: correctly have two tests use realpath(). Thanks Barry Warsaw. - fix test runs on environments without a home directory (in this case we use toxinidir as the homedir) - fix `#117 `_: python2.5 fix: don't use ``--insecure`` option because its very existence depends on presence of "ssl". If you want to support python2.5/pip1.3.1 based test environments you need to install ssl and/or use PIP_INSECURE=1 through ``setenv``. section. - fix `#102 `_: change to {toxinidir} when installing dependencies. This allows one to use relative path like in "-rrequirements.txt". v1.6.0 (2013-08-15) ------------------- - fix `#35 `_: add new EXPERIMENTAL "install_command" testenv-option to configure the installation command with options for dep/pkg install. Thanks Carl Meyer for the PR and docs. - fix `#91 `_: python2.5 support by vendoring the virtualenv-1.9.1 script and forcing pip<1.4. Also the default [py25] environment modifies the default installer_command (new config option) to use pip without the "--pre" option which was introduced with pip-1.4 and is now required if you want to install non-stable releases. (tox defaults to install with "--pre" everywhere). - during installation of dependencies HOME is now set to a pseudo 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 PyPI. - fix `#1 `_: empty setup files are properly detected, thanks Anthon van der Neuth - remove toxbootstrap.py for now because it is broken. - fix `#109 `_ and fix `#111 `_: multiple "-e" options are now combined (previously the last one would win). Thanks Anthon van der Neut. - add --result-json option to write out detailed per-venv information into a json report file to be used by upstream tools. - add new config options ``usedevelop`` and ``skipsdist`` as well as a command line option ``--develop`` to install the package-under-test in develop mode. thanks Monty Tailor for the PR. - always unset PYTHONDONTWRITEBYTE because newer setuptools doesn't like it - if a HOMEDIR cannot be determined, use the toxinidir. - refactor interpreter information detection to live in new tox/interpreters.py file, tests in tests/test_interpreters.py. v1.5.0 (2013-06-22) ------------------- - fix `#104 `_: use setuptools by default, instead of distribute, now that setuptools has distribute merged. - make sure test commands are searched first in the virtualenv - re-fix `#2 `_ - add whitelist_externals to be used in ``[testenv*]`` sections, allowing to avoid warnings for commands such as ``make``, used from the commands value. - fix `#97 `_ - allow substitutions to reference from other sections (thanks Krisztian Fekete) - fix `#92 `_ - fix {envsitepackagesdir} to actually work again - show (test) command that is being executed, thanks Lukasz Balcerzak - re-license tox to MIT license - depend on virtualenv-1.9.1 - rename README.txt to README.rst to make bitbucket happier v1.4.3 (2013-02-28) ------------------- - use pip-script.py instead of pip.exe on win32 to avoid the lock exe file on execution issue (thanks Philip Thiem) - introduce -l|--listenv option to list configured environments (thanks Lukasz Balcerzak) - fix downloadcache determination to work according to docs: Only make pip use a download cache if PIP_DOWNLOAD_CACHE or a downloadcache=PATH testenv setting is present. (The ENV setting takes precedence) - fix `#84 `_ - pypy on windows creates a bin not a scripts venv directory (thanks Lukasz Balcerzak) - experimentally introduce --installpkg=PATH option to install a package rather than create/install an sdist package. This will still require and use tox.ini and tests from the current working dir (and not from the remote package). - substitute {envsitepackagesdir} with the package installation directory (closes `#72 `_) (thanks g2p) - issue `#70 `_ remove PYTHONDONTWRITEBYTECODE workaround now that virtualenv behaves properly (thanks g2p) - merged tox-quickstart command, contributed by Marc Abramowitz, which generates a default tox.ini after asking a few questions - fix `#48 `_ - win32 detection of pypy and other interpreters that are on PATH (thanks Gustavo Picon) - fix grouping of index servers, it is now done by name instead of indexserver url, allowing to use it to separate dependencies into groups even if using the same default indexserver. - look for "tox.ini" files in parent dirs of current dir (closes `#34 `_) - the "py" environment now by default uses the current interpreter (sys.executable) make tox' own setup.py test execute tests with it (closes `#46 `_) - change tests to not rely on os.path.expanduser (closes `#60 `_), also make mock session return args[1:] for more precise checking (closes `#61 `_) thanks to Barry Warsaw for both. v1.4.2 (2012-07-20) ------------------- - fix some tests which fail if /tmp is a symlink to some other place - "python setup.py test" now runs tox tests via tox :) also added an example on how to do it for your project. v1.4.1 (2012-07-03) ------------------- - fix `#41 `_ better quoting on windows - you can now use "<" and ">" in deps specifications, thanks Chris Withers for reporting v1.4 (2012-06-13) ----------------- - fix `#26 `_ - no warnings on absolute or relative specified paths for commands - fix `#33 `_ - commentchars are ignored in key-value settings allowing for specifying commands like: python -c "import sys ; print sys" which would formerly raise irritating errors because the ";" was considered a comment - tweak and improve reporting - refactor reporting and virtualenv manipulation to be more accessible from 3rd party tools - support value substitution from other sections with the {[section]key} syntax - fix `#29 `_ - correctly point to pytest explanation for importing modules fully qualified - fix `#32 `_ - use --system-site-packages and don't pass --no-site-packages - add python3.3 to the default env list, so early adopters can test - drop python2.4 support (you can still have your tests run on - fix the links/checkout howtos in the docs python-2.4, just tox itself requires 2.5 or higher. v1.3 2011-12-21 --------------- - fix: allow one to specify wildcard filesystem paths when specifying dependencies such that tox searches for the highest version - fix issue `#21 `_: clear PIP_REQUIRES_VIRTUALENV which avoids pip installing to the wrong environment, thanks to bb's streeter - make the install step honour a testenv's setenv setting (thanks Ralf Schmitt) v1.2 2011-11-10 --------------- - remove the virtualenv.py that was distributed with tox and depend on >=virtualenv-1.6.4 (possible now since the latter fixes a few bugs that the inlining tried to work around) - fix `#10 `_: work around UnicodeDecodeError when invoking pip (thanks Marc Abramowitz) - fix a problem with parsing {posargs} in tox commands (spotted by goodwill) - fix the warning check for commands to be installed in testenvironment (thanks Michael Foord for reporting) v1.1 (2011-07-08) ----------------- - fix `#5 `_ - don't require argparse for python versions that have it - fix `#6 `_ - recreate virtualenv if installing dependencies failed - fix `#3 `_ - fix example on frontpage - fix `#2 `_ - warn if a test command does not come from the test environment - fixed/enhanced: except for initial install always call "-U --no-deps" for installing the sdist package to ensure that a package gets upgraded even if its version number did not change. (reported on TIP mailing list and IRC) - inline virtualenv.py (1.6.1) script to avoid a number of issues, particularly failing to install python3 environments from a python2 virtualenv installation. - rework and enhance docs for display on readthedocs.org v1.0 ---- - move repository and toxbootstrap links to https://bitbucket.org/hpk42/tox - fix `#7 `_: introduce a "minversion" directive such that tox bails out if it does not have the correct version. - fix `#24 `_: introduce a way to set environment variables for for test commands (thanks Chris Rose) - fix `#22 `_: require virtualenv-1.6.1, obsoleting virtualenv5 (thanks Jannis Leidel) and making things work with pypy-1.5 and python3 more seamlessly - toxbootstrap.py (used by jenkins build agents) now follows the latest release of virtualenv - fix `#20 `_: document format of URLs for specifying dependencies - fix `#19 `_: substitute Hudson for Jenkins everywhere following the renaming of the project. NOTE: if you used the special [tox:hudson] section it will now need to be named [tox:jenkins]. - fix issue 23 / apply some ReST fixes - change the positional argument specifier to use {posargs:} syntax and fix issues `#15 `_ and `#10 `_ by refining the argument parsing method (Chris Rose) - remove use of inipkg lazy importing logic - the namespace/imports are anyway very small with tox. - fix a fspath related assertion to work with debian installs which uses symlinks - show path of the underlying virtualenv invocation and bootstrap virtualenv.py into a working subdir - added a CONTRIBUTORS file v0.9 ---- - fix pip-installation mixups by always unsetting PIP_RESPECT_VIRTUALENV (thanks Armin Ronacher) - `#1 `_: Add a toxbootstrap.py script for tox, thanks to Sridhar Ratnakumar - added support for working with different and multiple PyPI indexservers. - new option: -r|--recreate to force recreation of virtualenv - depend on py>=1.4.0 which does not contain or install the py.test anymore which is now a separate distribution "pytest". - show logfile content if there is an error (makes CI output more readable) v0.8 ---- - work around a virtualenv limitation which crashes if PYTHONDONTWRITEBYTECODE is set. - run pip/easy installs from the environment log directory, avoids naming clashes between env names and dependencies (thanks ronny) - require a more recent version of py lib - refactor and refine config detection to work from a single file and to detect the case where a python installation overwrote an old one and resulted in a new executable. This invalidates the existing virtualenvironment now. - change all internal source to strip trailing whitespaces v0.7 ---- - use virtualenv5 (my own fork of virtualenv3) for now to create python3 environments, fixes a couple of issues and makes tox more likely to work with Python3 (on non-windows environments) - add ``sitepackages`` option for testenv sections so that environments can be created with access to globals (default is not to have access, i.e. create environments with ``--no-site-packages``. - addressing `#4 `_: always prepend venv-path to PATH variable when calling subprocesses - fix `#2 `_: exit with proper non-zero return code if there were errors or test failures. - added unittest2 examples contributed by Michael Foord - only allow 'True' or 'False' for boolean config values (lowercase / uppercase is irrelevant) - recreate virtualenv on changed configurations v0.6 ---- - fix OSX related bugs that could cause the caller's environment to get screwed (sorry). tox was using the same file as virtualenv for tracking the Python executable dependency and there also was confusion wrt links. this should be fixed now. - fix long description, thanks Michael Foord v0.5 ---- - initial release ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/conf.py0000644000175100001710000000727000000000000015136 0ustar00vstsdocker00000000000000import os import re import subprocess import sys from datetime import date from pathlib import Path from docutils import nodes from sphinx import addnodes import tox extensions = [ "sphinx.ext.autodoc", "sphinx.ext.extlinks", "sphinx.ext.intersphinx", "sphinx.ext.viewcode", "sphinxcontrib.autoprogram", ] ROOT_SRC_TREE_DIR = Path(__file__).parents[1] def generate_draft_news(): home = "https://github.com" issue = "{}/issue".format(home) fragments_path = ROOT_SRC_TREE_DIR / "docs" / "changelog" for pattern, replacement in ( (r"[^`]@([^,\s]+)", r"`@\1 <{}/\1>`_".format(home)), (r"[^`]#([\d]+)", r"`#pr\1 <{}/\1>`_".format(issue)), ): for path in fragments_path.glob("*.rst"): path.write_text(re.sub(pattern, replacement, path.read_text())) env = os.environ.copy() env["PATH"] += os.pathsep.join( [os.path.dirname(sys.executable)] + env["PATH"].split(os.pathsep), ) changelog = subprocess.check_output( ["towncrier", "--draft", "--version", "DRAFT"], cwd=str(ROOT_SRC_TREE_DIR), env=env, ).decode("utf-8") if "No significant changes" in changelog: content = "" else: note = "*Changes in master, but not released yet are under the draft section*." content = "{}\n\n{}".format(note, changelog) (ROOT_SRC_TREE_DIR / "docs" / "_draft.rst").write_text(content) generate_draft_news() project = u"tox" _full_version = tox.__version__ release = _full_version.split("+", 1)[0] version = ".".join(release.split(".")[:2]) author = "holger krekel and others" year = date.today().year copyright = u"2010-{}, {}".format(year, author) master_doc = "index" source_suffix = ".rst" exclude_patterns = ["changelog/*"] templates_path = ["_templates"] pygments_style = "sphinx" html_theme = "alabaster" html_theme_options = { "logo": "img/tox.png", "github_user": "tox-dev", "github_repo": "tox", "description": "standardise testing in Python", "github_banner": "true", "github_type": "star", "travis_button": "false", "badge_branch": "master", "fixed_sidebar": "false", } html_sidebars = { "**": ["about.html", "localtoc.html", "relations.html", "searchbox.html", "donate.html"], } html_show_sourcelink = False html_static_path = ["_static"] htmlhelp_basename = "{}doc".format(project) latex_documents = [("index", "tox.tex", u"{} Documentation".format(project), author, "manual")] man_pages = [("index", project, u"{} Documentation".format(project), [author], 1)] epub_title = project epub_author = author epub_publisher = author epub_copyright = copyright intersphinx_mapping = {"https://docs.python.org/": None} def setup(app): def parse_node(env, text, node): args = text.split("^") name = args[0].strip() node += addnodes.literal_strong(name, name) if len(args) > 2: default = "={}".format(args[2].strip()) node += nodes.literal(text=default) if len(args) > 1: content = "({})".format(args[1].strip()) node += addnodes.compact_paragraph(text=content) return name # this will be the link app.add_object_type( directivename="conf", rolename="conf", objname="configuration value", indextemplate="pair: %s; configuration value", parse_node=parse_node, ) tls_cacerts = os.getenv("SSL_CERT_FILE") # we don't care here about the validity of certificates linkcheck_timeout = 30 linkcheck_ignore = [r"https://holgerkrekel.net"] extlinks = { "issue": ("https://github.com/tox-dev/tox/issues/%s", "#"), "pull": ("https://github.com/tox-dev/tox/pull/%s", "p"), "user": ("https://github.com/%s", "@"), } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/config.rst0000644000175100001710000011526600000000000015643 0ustar00vstsdocker00000000000000.. be in -*- rst -*- mode! tox configuration specification =============================== Configuration discovery ----------------------- At the moment tox supports three configuration locations prioritized in the following order: 1. ``pyproject.toml``, 2. ``tox.ini``, 3. ``setup.cfg``. As far as the configuration format at the moment we only support standard ConfigParser_ "ini-style" format (there is a plan to add a pure TOML one soon). ``tox.ini`` and ``setup.cfg`` are such files. Note that ``setup.cfg`` requires the content to be under the ``tox:tox`` and ``testenv`` sections and is otherwise ignored. ``pyproject.toml`` on the other hand is in TOML format. However, one can inline the *ini-style* format under the ``tool.tox.legacy_tox_ini`` key as a multi-line string. Below you find the specification for the *ini-style* format, but you might want to skim some :doc:`examples` first and use this page as a reference. tox global settings ------------------- Global settings are defined under the ``tox`` section as: .. code-block:: ini [tox] minversion = 3.4.0 .. conf:: minversion Define the minimal tox version required to run; if the host's tox version is less than this the tool will create an environment and provision it with a version of tox that satisfies this under :conf:`provision_tox_env`. .. conf:: requires ^ LIST of PEP-508 .. versionadded:: 3.2.0 Specify python packages that need to exist alongside the tox installation for the tox build to be able to start (must be PEP-508_ compliant). Use this to specify plugin requirements (or the version of ``virtualenv`` - determines the default ``pip``, ``setuptools``, and ``wheel`` versions the tox environments start with). If these dependencies are not specified tox will create :conf:`provision_tox_env` environment so that they are satisfied and delegate all calls to that. .. code-block:: ini [tox] requires = tox-pipenv setuptools >= 30.0.0 .. conf:: provision_tox_env ^ string ^ .tox .. versionadded:: 3.8.0 Name of the virtual environment used to provision a tox having all dependencies specified inside :conf:`requires` and :conf:`minversion`. .. conf:: toxworkdir ^ PATH ^ {toxinidir}/.tox Directory for tox to generate its environments into, will be created if it does not exist. .. conf:: temp_dir ^ PATH ^ {toxworkdir}/.tmp Directory where to put tox temporary files. For example: we create a hard link (if possible, otherwise new copy) in this directory for the project package. This ensures tox works correctly when having parallel runs (as each session will have its own copy of the project package - e.g. the source distribution). .. conf:: skipsdist ^ true|false ^ false Flag indicating to perform the packaging operation or not. Set it to ``true`` when using tox for an application, instead of a library. .. conf:: setupdir ^ PATH ^ {toxinidir} Indicates where the packaging root file exists (historically the ``setup.py`` for ``setuptools``). This will be the working directory when performing the packaging. .. conf:: distdir ^ PATH ^ {toxworkdir}/dist Directory where the packaged source distribution should be put. Note this is cleaned at the start of every packaging invocation. .. conf:: sdistsrc ^ PATH ^ {toxworkdir}/dist Do not build the package, but instead use the latest package available under this path. You can override it via the command line flag ``--installpkg``. .. conf:: distshare ^ PATH ^ {homedir}/.tox/distshare Folder where the packaged source distribution will be moved, this is not cleaned between packaging invocations. On Jenkins (exists ``JENKINS_URL`` or ``HUDSON_URL`` environment variable) the default path is ``{toxworkdir}/distshare``. .. conf:: envlist ^ comma separated values Determining the environment list that ``tox`` is to operate on happens in this order (if any is found, no further lookups are made): * command line option ``-eENVLIST`` * environment variable ``TOXENV`` * ``tox.ini`` file's ``envlist`` .. versionadded:: 3.4.0 Which tox environments are run during the tox invocation can be further filtered via the operating system environment variable ``TOX_SKIP_ENV`` regular expression (e.g. ``py27.*`` means **don't** evaluate environments that start with the key ``py27``). Skipped environments will be logged at level two verbosity level. .. conf:: skip_missing_interpreters ^ config|true|false ^ config .. versionadded:: 1.7.2 Setting this to ``true`` will force ``tox`` to return success even if some of the specified environments were missing. This is useful for some CI systems or when running on a developer box, where you might only have a subset of all your supported interpreters installed but don't want to mark the build as failed because of it. As expected, the command line switch always overrides this setting if passed on the invocation. Setting it to ``config`` means that the value is read from the config file. .. conf:: ignore_basepython_conflict ^ true|false ^ false .. versionadded:: 3.1.0 tox allows setting the python version for an environment via the :conf:`basepython` setting. If that's not set tox can set a default value from the environment name ( e.g. ``py37`` implies Python 3.7). Matching up the python version with the environment name has became expected at this point, leading to surprises when some configs don't do so. To help with sanity of users a warning will be emitted whenever the environment name version does not matches up with this expectation. In a future version of tox, this warning will become an error. Furthermore, we allow hard enforcing this rule (and bypassing the warning) by setting this flag to ``true``. In such cases we ignore the :conf:`basepython` and instead always use the base python implied from the Python name. This allows you to configure :conf:`basepython` in the global testenv without affecting environments that have implied base python versions. .. conf:: isolated_build ^ true|false ^ false .. versionadded:: 3.3.0 Activate isolated build environment. tox will use a virtual environment to build a source distribution from the source tree. For build tools and arguments use the ``pyproject.toml`` file as specified in `PEP-517`_ and `PEP-518`_. To specify the virtual environment Python version define use the :conf:`isolated_build_env` config section. .. conf:: isolated_build_env ^ string ^ .package .. versionadded:: 3.3.0 Name of the virtual environment used to create a source distribution from the source tree. Jenkins override ++++++++++++++++ It is possible to override global settings inside a Jenkins_ instance (detection is done by checking for existence of the ``JENKINS_URL`` environment variable) by using the ``tox:jenkins`` section: .. code-block:: ini [tox:jenkins] commands = ... # override settings for the jenkins context tox environments ---------------- Test environments are defined under the ``testenv`` section and individual ``testenv:NAME`` sections, where ``NAME`` is the name of a specific environment. .. code-block:: ini [testenv] commands = ... [testenv:NAME] commands = ... Settings defined in the top-level ``testenv`` section are automatically inherited by individual environments unless overridden. Test environment names can consist of alphanumeric characters and dashes; for example: ``py38-django30``. The name will be split on dashes into multiple factors, meaning ``py38-django30`` will be split into two factors: ``py38`` and ``django30``. *tox* defines a number of default factors, which correspond to various versions and implementations of Python and provide default values for :conf:`basepython`: - ``pyNM``: configures ``basepython = pythonN.M`` - ``pyN``: configures ``basepython = pythonN`` - ``py``: configures ``basepython = python`` - ``pypyN``: configures ``basepython = pypyN`` - ``pypy``: configures ``basepython = pypy`` - ``jythonN``: configures ``basepython = jythonN`` - ``jython``: configures ``basepython = jython`` It is also possible to define what's know as *generative names*, where an individual section maps to multiple environments. For example, ``py{37,38}-django{30,31}`` would generate four environments, each consisting of two factors: ``py37-django30`` (``py37``, ``django30``), ``py37-django31`` (``py37``, ``django31``), ``py38-django30`` (``py38``, ``django30``), and ``py38-django31`` (``py38``, ``django31``). Combined, these features provide the ability to write very concise ``tox.ini`` files. This is discussed further in :ref:`below `. tox environment settings ------------------------ Complete list of settings that you can put into ``testenv*`` sections: .. conf:: basepython ^ NAME-OR-PATH Name or path to a Python interpreter which will be used for creating the virtual environment, this determines in practice the python for what we'll create a virtual isolated environment. Use this to specify the python version for a tox environment. If not specified, the virtual environments factors (e.g. name part) will be used to automatically set one. For example, ``py37`` means ``python3.7``, ``py3`` means ``python3`` and ``py`` means ``python``. :conf:`provision_tox_env` environment does not inherit this setting from the ``toxenv`` section. .. versionchanged:: 3.1 After resolving this value if the interpreter reports back a different version number than implied from the name a warning will be printed by default. However, if :conf:`ignore_basepython_conflict` is set, the value is ignored and we force the ``basepython`` implied from the factor name. .. conf:: commands ^ ARGVLIST The commands to be called for testing. Only execute if :conf:`commands_pre` succeed. Each line is interpreted as one command; however a command can be split over multiple lines by ending the line with the ``\`` character. Commands will execute one by one in sequential fashion until one of them fails (their exit code is non-zero) or all of them succeed. The exit code of a command may be ignored (meaning they are always considered successful) by prefixing the command with a dash (``-``) - this is similar to how ``make`` recipe lines work. The outcome of the environment is considered successful only if all commands (these + setup + teardown) succeeded (exit code ignored via the ``-`` or success exit code value of zero). :note: the virtual environment binary path (the ``bin`` folder within) is prepended to the os ``PATH``, meaning commands will first try to resolve to an executable from within the virtual environment, and only after that outside of it. Therefore ``python`` translates as the virtual environments ``python`` (having the same runtime version as the :conf:`basepython`), and ``pip`` translates as the virtual environments ``pip``. .. conf:: commands_pre ^ ARGVLIST .. versionadded:: 3.4 Commands to run before running the :conf:`commands`. All evaluation and configuration logic applies from :conf:`commands`. .. conf:: commands_post ^ ARGVLIST .. versionadded:: 3.4 Commands to run after running the :conf:`commands`. Execute regardless of the outcome of both :conf:`commands` and :conf:`commands_pre`. All evaluation and configuration logic applies from :conf:`commands`. .. conf:: install_command ^ ARGV ^ python -m pip install {opts} {packages} .. versionadded:: 1.6 Determines the command used for installing packages into the virtual environment; both the package under test and its dependencies (defined with :conf:`deps`). Must contain the substitution key ``{packages}`` which will be replaced by the package(s) to install. You should also accept ``{opts}`` if you are using pip -- it will contain index server options such as ``--pre`` (configured as ``pip_pre``) and potentially index-options from the deprecated :conf:`indexserver` option. .. conf:: list_dependencies_command ^ ARGV ^ python -m pip freeze .. versionadded:: 2.4 The ``list_dependencies_command`` setting is used for listing the packages installed into the virtual environment. .. conf:: ignore_errors ^ true|false ^ false .. versionadded:: 2.0 If ``false``, a non-zero exit code from one command will abort execution of commands for that environment. If ``true``, a non-zero exit code from one command will be ignored and further commands will be executed. The overall status will be "commands failed", i.e. tox will exit non-zero in case any command failed. It may be helpful to note that this setting is analogous to the ``-k`` or ``--keep-going`` option of GNU Make. Note that in tox 2.0, the default behavior of tox with respect to treating errors from commands changed. tox < 2.0 would ignore errors by default. tox >= 2.0 will abort on an error by default, which is safer and more typical of CI and command execution tools, as it doesn't make sense to run tests if installing some prerequisite failed and it doesn't make sense to try to deploy if tests failed. .. conf:: pip_pre ^ true|false ^ false .. versionadded:: 1.9 If ``true``, adds ``--pre`` to the ``opts`` passed to :conf:`install_command`. If :conf:`install_command` uses pip, this will cause it to install the latest available pre-release of any dependencies without a specified version. If ``false``, pip will only install final releases of unpinned dependencies. Passing the ``--pre`` command-line option to tox will force this to ``true`` for all testenvs. Don't set this option if your :conf:`install_command` does not use pip. .. conf:: allowlist_externals ^ MULTI-LINE-LIST .. versionadded:: 3.18 Each line specifies a command name (in glob-style pattern format) which can be used in the ``commands`` section without triggering a "not installed in virtualenv" warning. Example: if you use the unix ``make`` for running tests you can list ``allowlist_externals=make`` or ``allowlist_externals=/usr/bin/make`` if you want more precision. If you don't want tox to issue a warning in any case, just use ``allowlist_externals=*`` which will match all commands (not recommended). .. note:: ``whitelist_externals`` has the same meaning and usage as ``allowlist_externals`` but it is now deprecated. .. conf:: changedir ^ PATH ^ {toxinidir} Change to this working directory when executing the test command. .. note:: If the directory does not exist yet, it will be created. .. conf:: deps ^ MULTI-LINE-LIST Environment dependencies - installed into the environment ((see :conf:`install_command`) prior to project after environment creation. One dependency (a file, a URL or a package name) per line. Must be PEP-508_ compliant. All installer commands are executed using the toxinidir_ as the current working directory. .. code-block:: ini [testenv] deps = pytest pytest-cov >= 3.5 pywin32 >=1.0 ; sys_platform == 'win32' octomachinery==0.0.13 # pyup: < 0.1.0 # disable feature updates .. versionchanged:: 2.3 Support for index servers is now deprecated, and its usage discouraged. .. versionchanged:: 3.9 Comment support on the same line as the dependency. When feeding the content to the install tool we'll strip off content (including) from the first comment marker (``#``) preceded by one or more space. For example, if a dependency is ``octomachinery==0.0.13 # pyup: < 0.1.0 # disable feature updates`` it will be turned into just ``octomachinery==0.0.13``. .. conf:: platform ^ REGEX .. versionadded:: 2.0 A testenv can define a new ``platform`` setting as a regular expression. If a non-empty expression is defined and does not match against the ``sys.platform`` string the entire test environment will be skipped and none of the commands will be executed. Running ``tox -e `` will run commands for a particular platform and skip the rest. .. conf:: setenv ^ MULTI-LINE-LIST .. versionadded:: 0.9 Each line contains a NAME=VALUE environment variable setting which will be used for all test command invocations as well as for installing the sdist package into a virtual environment. Notice that when updating a path variable, you can consider the use of variable substitution for the current value and to handle path separator. .. code-block:: ini [testenv] setenv = PYTHONPATH = {env:PYTHONPATH}{:}{toxinidir} .. versionadded:: 3.20 Support for comments. Lines starting with ``#`` are ignored. Support for environment files. Lines starting with the ``file|`` contain path to a environment file to load. Rules within the environment file are the same as within the ``setenv`` (same replacement and comment support). .. conf:: passenv ^ SPACE-SEPARATED-GLOBNAMES .. versionadded:: 2.0 A list of wildcard environment variable names which shall be copied from the tox invocation environment to the test environment when executing test commands. If a specified environment variable doesn't exist in the tox invocation environment it is ignored. You can use ``*`` and ``?`` to match multiple environment variables with one name. The list of environment variable names is not case sensitive, and all variables that match when upper cased will be passed. For example, passing ``A`` will pass both ``A`` and ``a``. Some variables are always passed through to ensure the basic functionality of standard library functions or tooling like pip: * passed through on all platforms: ``CURL_CA_BUNDLE``, ``PATH``, ``LANG``, ``LANGUAGE``, ``LD_LIBRARY_PATH``, ``PIP_INDEX_URL``, ``PIP_EXTRA_INDEX_URL``, ``REQUESTS_CA_BUNDLE``, ``SSL_CERT_FILE``, ``HTTP_PROXY``, ``HTTPS_PROXY``, ``NO_PROXY`` * Windows: ``SYSTEMDRIVE``, ``SYSTEMROOT``, ``PATHEXT``, ``TEMP``, ``TMP`` ``NUMBER_OF_PROCESSORS``, ``USERPROFILE``, ``MSYSTEM`` * Others (e.g. UNIX, macOS): ``TMPDIR`` You can override these variables with the ``setenv`` option. If defined the ``TOX_TESTENV_PASSENV`` environment variable (in the tox invocation environment) can define additional space-separated variable names that are to be passed down to the test command environment. .. versionchanged:: 2.7 ``PYTHONPATH`` will be passed down if explicitly defined. If ``PYTHONPATH`` exists in the host environment but is **not** declared in ``passenv`` a warning will be emitted. .. conf:: recreate ^ true|false ^ false Always recreate virtual environment if this option is true. If this option is false, ``tox``'s resolution mechanism will be used to determine whether to recreate the environment. .. conf:: downloadcache ^ PATH **IGNORED** -- Since pip-8 has caching by default this option is now ignored. Please remove it from your configs as a future tox version might bark on it. .. conf:: sitepackages ^ true|false ^ false Set to ``true`` if you want to create virtual environments that also have access to globally installed packages. .. warning:: In cases where a command line tool is also installed globally you have to make sure that you use the tool installed in the virtualenv by using ``python -m `` (if supported by the tool) or ``{envbindir}/``. If you forget to do that you will get a warning like this:: WARNING: test command found but not installed in testenv cmd: /path/to/parent/interpreter/bin/ env: /foo/bar/.tox/python Maybe you forgot to specify a dependency? See also the allowlist_externals envconfig setting. .. conf:: alwayscopy ^ true|false ^ false Set to ``true`` if you want virtualenv to always copy files rather than symlinking. This is useful for situations where hardlinks don't work (e.g. running in VMS with Windows guests). .. conf:: download ^ true|false ^ false .. versionadded:: 3.10 Set to ``true`` if you want virtualenv to upgrade pip/wheel/setuptools to the latest version. If (and only if) you want to choose a specific version (not necessarily the latest) then you can add e.g. ``VIRTUALENV_PIP=20.3.3`` to your setenv. .. conf:: args_are_paths ^ true|false ^ true Treat positional arguments passed to ``tox`` as file system paths and - if they exist on the filesystem - rewrite them according to the ``changedir``. Default is true due to the exists-on-filesystem check it's usually safe to try rewriting. .. conf:: envtmpdir ^ PATH ^ {envdir}/tmp Defines a temporary directory for the virtualenv which will be cleared each time before the group of test commands is invoked. .. conf:: envlogdir ^ PATH ^ {envdir}/log Defines a directory for logging where tox will put logs of tool invocation. .. conf:: indexserver ^ URL .. versionadded:: 0.9 (DEPRECATED, will be removed in a future version) Multi-line ``name = URL`` definitions of python package servers. Dependencies can specify using a specified index server through the ``:indexservername:depname`` pattern. The ``default`` indexserver definition determines where unscoped dependencies and the sdist install installs from. Example: .. code-block:: ini [tox] indexserver = default = https://mypypi.org will make tox install all dependencies from this PyPI index server (including when installing the project sdist package). .. conf:: envdir ^ PATH ^ {toxworkdir}/{envname} .. versionadded:: 1.5 User can set specific path for environment. If path would not be absolute it would be treated as relative to ``{toxinidir}``. .. conf:: usedevelop ^ true|false ^ false .. versionadded:: 1.6 Install the current package in development mode with "setup.py develop" instead of installing from the ``sdist`` package. (This uses pip's ``-e`` option, so should be avoided if you've specified a custom :conf:`install_command` that does not support ``-e``). .. conf:: skip_install ^ true|false ^ false .. versionadded:: 1.9 Do not install the current package. This can be used when you need the virtualenv management but do not want to install the current package into that environment. .. conf:: ignore_outcome ^ true|false ^ false .. versionadded:: 2.2 If set to true a failing result of this testenv will not make tox fail, only a warning will be produced. .. conf:: extras ^ MULTI-LINE-LIST .. versionadded:: 2.4 A list of "extras" to be installed with the sdist or develop install. For example, ``extras = testing`` is equivalent to ``[testing]`` in a ``pip install`` command. These are not installed if ``skip_install`` is ``true``. .. conf:: description ^ SINGLE-LINE-TEXT ^ no description A short description of the environment, this will be used to explain the environment to the user upon listing environments for the command line with any level of verbosity higher than zero. .. conf:: parallel_show_output ^ bool ^ false .. versionadded:: 3.7.0 If set to True the content of the output will always be shown when running in parallel mode. .. conf:: depends ^ comma separated values .. versionadded:: 3.7.0 tox environments this depends on. tox will try to run all dependent environments before running this environment. Format is same as :conf:`envlist` (allows factor usage). .. warning:: ``depends`` does not pull in dependencies into the run target, for example if you select ``py27,py36,coverage`` via the ``-e`` tox will only run those three (even if ``coverage`` may specify as ``depends`` other targets too - such as ``py27, py35, py36, py37``). .. conf:: suicide_timeout ^ float ^ 0.0 .. versionadded:: 3.15.2 When an interrupt is sent via Ctrl+C or the tox process is killed with a SIGTERM, a SIGINT is sent to all foreground processes. The :conf:``suicide_timeout`` gives the running process time to cleanup and exit before receiving (in some cases, a duplicate) SIGINT from tox. .. conf:: interrupt_timeout ^ float ^ 0.3 .. versionadded:: 3.15.0 When tox is interrupted, it propagates the signal to the child process after :conf:``suicide_timeout`` seconds. If the process still hasn't exited after :conf:``interrupt_timeout`` seconds, its sends a SIGTERM. .. conf:: terminate_timeout ^ float ^ 0.2 .. versionadded:: 3.15.0 When tox is interrupted, after waiting :conf:``interrupt_timeout`` seconds, it propagates the signal to the child process, waits :conf:``interrupt_timeout`` seconds, sends it a SIGTERM, waits :conf:``terminate_timeout`` seconds, and sends it a SIGKILL if it hasn't exited. Substitutions ------------- Any ``key=value`` setting in an ini-file can make use of value substitution through the ``{...}`` string-substitution pattern. You can escape curly braces with the ``\`` character if you need them, for example:: commands = echo "\{posargs\}" = {posargs} Note some substitutions (e.g. ``posargs``, ``env``) may have addition values attached to it, via the ``:`` character (e.g. ``posargs`` - default value, ``env`` - key). Such substitutions cannot have a space after the ``:`` character (e.g. ``{posargs: magic}`` while being at the start of a line inside the ini configuration (this would be parsed as factorial ``{posargs``, having value magic). Globally available substitutions ++++++++++++++++++++++++++++++++ .. _`toxinidir`: ``{toxinidir}`` the directory where ``tox.ini`` is located .. _`toxworkdir`: ``{toxworkdir}`` the directory where virtual environments are created and sub directories for packaging reside. ``{homedir}`` the user-home directory path. ``{distdir}`` the directory where sdist-packages will be created in ``{distshare}`` (DEPRECATED) the directory where sdist-packages will be copied to so that they may be accessed by other processes or tox runs. ``{:}`` OS-specific path separator (``:`` on \*nix family, ``;`` on Windows). May be used in ``setenv``, when target variable is path variable (e.g. PATH or PYTHONPATH). ``{/}`` OS-specific directory separator (``/`` on \*nix family, ``\\`` on Windows). Useful for deriving filenames from preset paths, as arguments for commands that requires ``\\`` on Windows. e.g. ``{distdir}{/}file.txt``. It is not usually needed when using commands written in Python. Substitutions for virtualenv-related sections +++++++++++++++++++++++++++++++++++++++++++++ ``{envname}`` the name of the virtual environment ``{envpython}`` path to the virtual Python interpreter ``{envdir}`` directory of the virtualenv hierarchy ``{envbindir}`` directory where executables are located ``{envsitepackagesdir}`` directory where packages are installed. Note that architecture-specific files may appear in a different directory. ``{envtmpdir}`` the environment temporary directory ``{envlogdir}`` the environment log directory Environment variable substitutions ++++++++++++++++++++++++++++++++++ If you specify a substitution string like this:: {env:KEY} then the value will be retrieved as ``os.environ['KEY']`` and raise an Error if the environment variable does not exist. Environment variable substitutions with default values ++++++++++++++++++++++++++++++++++++++++++++++++++++++ If you specify a substitution string like this:: {env:KEY:DEFAULTVALUE} then the value will be retrieved as ``os.environ['KEY']`` and replace with DEFAULTVALUE if the environment variable does not exist. If you specify a substitution string like this:: {env:KEY:} then the value will be retrieved as ``os.environ['KEY']`` and replace with an empty string if the environment variable does not exist. Substitutions can also be nested. In that case they are expanded starting from the innermost expression:: {env:KEY:{env:DEFAULT_OF_KEY}} the above example is roughly equivalent to ``os.environ.get('KEY', os.environ['DEFAULT_OF_KEY'])`` .. _`command positional substitution`: .. _`positional substitution`: Interactive shell substitution ++++++++++++++++++++++++++++++ It's possible to inject a config value only when tox is running in interactive shell (standard input):: {tty:ON_VALUE:OFF_VALUE} The first value is the value to inject when the interactive terminal is available, the second value is the value to use when it's not. The later on is optional. A good use case for this is e.g. passing in the ``--pdb`` flag for pytest. Substitutions for positional arguments in commands ++++++++++++++++++++++++++++++++++++++++++++++++++ .. versionadded:: 1.0 If you specify a substitution string like this:: {posargs:DEFAULTS} then the value will be replaced with positional arguments as provided to the tox command:: tox arg1 arg2 In this instance, the positional argument portion will be replaced with ``arg1 arg2``. If no positional arguments were specified, the value of DEFAULTS will be used instead. If DEFAULTS contains other substitution strings, such as ``{env:*}``, they will be interpreted., Use a double ``--`` if you also want to pass options to an underlying test command, for example:: tox -- --opt1 ARG1 will make the ``--opt1 ARG1`` appear in all test commands where ``[]`` or ``{posargs}`` was specified. By default (see ``args_are_paths`` setting), ``tox`` rewrites each positional argument if it is a relative path and exists on the filesystem to become a path relative to the ``changedir`` setting. Previous versions of tox supported the ``[.*]`` pattern to denote positional arguments with defaults. This format has been deprecated. Use ``{posargs:DEFAULTS}`` to specify those. Substitution for values from other sections +++++++++++++++++++++++++++++++++++++++++++ .. versionadded:: 1.4 Values from other sections can be referred to via:: {[sectionname]valuename} which you can use to avoid repetition of config values. You can put default values in one section and reference them in others to avoid repeating the same values: .. code-block:: ini [base] deps = pytest mock pytest-xdist [testenv:dulwich] deps = dulwich {[base]deps} [testenv:mercurial] deps = mercurial {[base]deps} .. _generating-environments: Generating environments, conditional settings --------------------------------------------- .. versionadded:: 1.8 Suppose you want to test your package against python2.7, python3.6 and against several versions of a dependency, say Django 1.5 and Django 1.6. You can accomplish that by writing down 2*2 = 4 ``[testenv:*]`` sections and then listing all of them in ``envlist``. However, a better approach looks like this: .. code-block:: ini [tox] envlist = {py27,py36}-django{15,16} [testenv] deps = pytest django15: Django>=1.5,<1.6 django16: Django>=1.6,<1.7 py36: unittest2 commands = pytest This uses two new facilities of tox-1.8: - generative envlist declarations where each envname consists of environment parts or "factors" - "factor" specific settings Let's go through this step by step. .. _generative-envlist: Generative envlist ++++++++++++++++++ :: envlist = {py36,py27}-django{15,16} This is bash-style syntax and will create ``2*2=4`` environment names like this:: py27-django15 py27-django16 py36-django15 py36-django16 You can still list environments explicitly along with generated ones:: envlist = {py27,py36}-django{15,16}, docs, flake Keep in mind that whitespace characters (except newline) within ``{}`` are stripped, so the following line defines the same environment names:: envlist = {py27,py36}-django{ 15, 16 }, docs, flake .. note:: To help with understanding how the variants will produce section values, you can ask tox to show their expansion with a new option:: $ tox -l py27-django15 py27-django16 py36-django15 py36-django16 docs flake .. _generative-sections: Generative section names ++++++++++++++++++++++++ .. versionadded:: 3.15 Using similar syntax, it is possible to generate sections:: [testenv:py{27,36}-flake] This is equivalent to defining distinct sections:: $ tox -a py27-flake py36-flake It is useful when you need an environment different from the default one, but still want to take advantage of factor-conditional settings. .. _factors: Factors and factor-conditional settings ++++++++++++++++++++++++++++++++++++++++ As discussed previously, parts of an environment name delimited by hyphens are called factors and can be used to set values conditionally. In list settings such as ``deps`` or ``commands`` you can freely intermix optional lines with unconditional ones: .. code-block:: ini [testenv] deps = pytest django15: Django>=1.5,<1.6 django16: Django>=1.6,<1.7 py36: unittest2 Reading it line by line: - ``pytest`` will be included unconditionally, - ``Django>=1.5,<1.6`` will be included for environments containing ``django15`` factor, - ``Django>=1.6,<1.7`` similarly depends on ``django16`` factor, - ``unittest2`` will be loaded for Python 3.6 environments. tox provides a number of default factors corresponding to Python interpreter versions. The conditional setting above will lead to either ``python3.6`` or ``python2.7`` used as base python, e.g. ``python3.6`` is selected if current environment contains ``py36`` factor. .. note:: Configuring :conf:`basepython` for environments using default factors will result in a warning. Configure :conf:`ignore_basepython_conflict` if you wish to explicitly ignore these conflicts, allowing you to define a global :conf:`basepython` for all environments *except* those with default factors. Complex factor conditions +++++++++++++++++++++++++ Sometimes you need to specify the same line for several factors or create a special case for a combination of factors. Here is how you do it: .. code-block:: ini [tox] envlist = py{27,34,36}-django{15,16}-{sqlite,mysql} [testenv] deps = py34-mysql: PyMySQL # use if both py34 and mysql are in the env name py27,py36: urllib3 # use if either py36 or py27 are in the env name py{27,36}-sqlite: mock # mocking sqlite in python 2.x & 3.6 !py34-sqlite: mock # mocking sqlite, except in python 3.4 sqlite-!py34: mock # (same as the line above) !py34-!py36: enum34 # use if neither py34 nor py36 are in the env name Take a look at the first ``deps`` line. It shows how you can special case something for a combination of factors, by just hyphenating the combining factors together. This particular line states that ``PyMySQL`` will be loaded for python 3.4, mysql environments, e.g. ``py34-django15-mysql`` and ``py34-django16-mysql``. The second line shows how you use the same setting for several factors - by listing them delimited by commas. It's possible to list not only simple factors, but also their combinations like ``py27-sqlite,py36-sqlite``. The remaining lines all have the same effect and use conditions equivalent to ``py27-sqlite,py36-sqlite``. They have all been added only to help demonstrate the following: - how factor expressions get expanded the same way as in envlist - how to use negated factor conditions by prefixing negated factors with ``!`` - that the order in which factors are hyphenated together does not matter .. note:: Factors don't do substring matching against env name, instead every hyphenated expression is split by ``-`` and if ALL of its non-negated factors and NONE of its negated ones are also factors of an env then that condition is considered to hold for that env. For example, environment ``py36-mysql-!dev``: - would be matched by expressions ``py36``, ``py36-mysql`` or ``mysql-py36``, - but not ``py2``, ``py36-sql`` or ``py36-mysql-dev``. Factors and values substitution are compatible ++++++++++++++++++++++++++++++++++++++++++++++ It is possible to mix both values substitution and factor expressions. For example:: [tox] envlist = py27,py36,coverage [testenv] deps = flake8 coverage: coverage [testenv:py27] deps = {[testenv]deps} pytest With the previous configuration, it will install: - ``flake8`` and ``pytest`` packages for ``py27`` environment. - ``flake8`` package for ``py36`` environment. - ``flake8`` and ``coverage`` packages for ``coverage`` environment. Advanced settings ----------------- .. _`long interpreter directives`: Handle interpreter directives with long lengths +++++++++++++++++++++++++++++++++++++++++++++++ For systems supporting executable text files (scripts with a shebang), the system will attempt to parse the interpreter directive to determine the program to execute on the target text file. When ``tox`` prepares a virtual environment in a file container which has a large length (e.x. using Jenkins Pipelines), the system might not be able to invoke shebang scripts which define interpreters beyond system limits (e.x. Linux as a limit of 128; ``BINPRM_BUF_SIZE``). To workaround an environment which suffers from an interpreter directive limit, a user can bypass the system's interpreter parser by defining the ``TOX_LIMITED_SHEBANG`` environment variable before invoking ``tox``:: export TOX_LIMITED_SHEBANG=1 When the workaround is enabled, all tox-invoked text file executables will have their interpreter directive parsed by and explicitly executed by ``tox``. Injected environment variables ------------------------------ tox will inject the following environment variables that you can use to test that your command is running within tox: .. versionadded:: 3.4 - ``TOX_WORK_DIR`` env var is set to the tox work directory - ``TOX_ENV_NAME`` is set to the current running tox environment name - ``TOX_ENV_DIR`` is set to the current tox environments working dir. - ``TOX_PACKAGE`` the packaging phases outcome path (useful to inspect and make assertion of the built package itself). - ``TOX_PARALLEL_ENV`` is set to the current running tox environment name, only when running in parallel mode. :note: this applies for all tox envs (isolated packaging too) and all external commands called (e.g. install command - pip). Other Rules and notes --------------------- * ``path`` specifications: if a specified ``path`` is a relative path it will be considered as relative to the ``toxinidir``, the directory where the configuration file resides. CLI === .. autoprogram:: tox.cli:cli :prog: tox .. include:: links.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/developers.rst0000644000175100001710000000413400000000000016535 0ustar00vstsdocker00000000000000.. _developers: Developers FAQ ============== This section contains information for users who want to extend the tox source code. .. contents:: :local: PyCharm ------- 1. To generate the **project interpreter** you can use ``tox -rvvve dev``. 2. For tests we use **pytest**, therefore change the `Default test runner `_ to ``pytest``. 3. In order to be able to **debug** tests which create a virtual environment (the ones in ``test_z_cmdline.py``) one needs to disable the PyCharm feature `Attach to subprocess automatically while debugging `_ (because virtualenv creation calls via subprocess to the ``pip`` executable, and PyCharm rewrites all calls to Python interpreters to attach to its debugger - however, this rewrite for pip makes it to have bad arguments: ``no such option --port``). Multiple Python versions on Windows ----------------------------------- In order to run the unit tests locally all Python versions enlisted in ``tox.ini`` need to be installed. .. note:: For a nice Windows terminal take a look at `cmder`_. .. _cmder: http://cmder.net/ One solution for this is to install the latest conda, and then install all Python versions via conda envs. This will create separate folders for each Python version. .. code-block:: bat conda create -n python2.7 python=2.7 anaconda For tox to find them you'll need to: - add the main installation version to the systems ``PATH`` variable (e.g. ``D:\Anaconda`` - you can use `WindowsPathEditor`_) - for other versions create a BAT scripts into the main installation folder to delegate the call to the correct Python interpreter: .. code-block:: bat @echo off REM python2.7.bat @D:\Anaconda\pkgs\python-2.7.13-1\python.exe %* .. _WindowsPathEditor: https://rix0rrr.github.io/WindowsPathEditor/ This way you can also directly call from cli the matching Python version if you need to(similarly to UNIX systems), for example: .. code-block:: bat python2.7 main.py python3.6 main.py ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.899511 tox-3.21.4/docs/drafts/0000755000175100001710000000000000000000000015114 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/drafts/extend-envs-and-packagebuilds.md0000644000175100001710000001374000000000000023237 0ustar00vstsdocker00000000000000# Extension of environment handling and building packages Issue reference: #338 *Notes from a discussion at the pytest sprint 2016* Goal: drive building of packages and the environments needed to test them, exercising the tests and report the results for more than just virtualenvs and python virtualenvs ### Problems * No concept of mapping environments to specific packages (versioned packages) * no control over when it happens for specific environment * no control over how it happens (e.g. which python interpreter is used to create the package) * No way of triggering build only if there is an environment that needs a specific build trigger it only if an environment actually needs it * package definition that might match on everything might be a problem for which environments test? Not clear? ### Solution It should be possible to build other kinds of packages than just the standard sdist and it should also be possible to create different kinds of builds that can be used from different environments. To make this possible there has to be some concept of factorized package definitions and a way to match these factorized builds to environments with a similar way of matching like what is in place already to generate environments. sdist would for example would match to a "sdist" factor to only be matched against virtualenvs as the default. This could then be used to have virtualenv, conda, nixos, docker, pyenv, rpm, deb, etc. builds and tie them to concrete test environments. To summarize - we would need a: * packagedef (how to build a package) * envdef (how to build an environment) * way of matching envs to concrete packages (at package definition level) (e.g `{py27,py34}-{win32,linux}-{venv,conda,pyenv}-[...]`) ## Beginnings of configuration examples (not thought out yet) [tox] envlist={py,27,py34}-{win32, linux}-{conda,virtualenv} [packagedef:sdist] # how to build (e.g. {py27,py34}-{sdist}) # how to match (e.g. {py27,py34}-{sdist}) [packagedef:conda] # how to build (e.g. {py27,py34}-{conda}) # how to match (e.g. {py27,py34}-{conda}) [packagedef:wheel] # how to build # how to match #### integrate detox * reporting in detox is minimal (would need to improve) * restricting processes would be necessary depending on power of the machine (creating 16 processe on a dual core machine might be overkill) * port it from eventlets to threads? ### Concrete use case conda integration (started by Bruno) * Asynchronicity / detox not taken into account yet * Conda activation might do anything (change filesys, start DBs) * Can I activate environments in parallel * Packages would need to be created (from conda.yml) * Activation is a problem ### Unsorted discussion notes * Simplify for the common case: most packages are universal, so it should be simple one to one relationship from environment to directory * Floris: metadata driven. Package has metadata to the env with what env it is compatible * Holger: configuration driven. explicitly configuring which packages should be used (default sdist to be used, overridable by concrete env) * Ronny: "package definitions" (this package, this setup command) + matching definitions (matching packages (with wildcards) for environments) ## Proposal This feature shall allow one to specify how plugins can specify new types of package formats and environments to run test commands in. Such plugins would take care of setting up the environment, create packages and run test commands using hooks provided by tox. The actual knowledge how to create a certain package format is implement in the plugin. Plugin decides which is the required python interpreter to use in order to create the relevant package format. ```ini [tox] plugins=conda # virtualenv plugin is builtin; intention here is to bail out early in case the specified plugins # are not installed envlist=py27,py35 [testenv] package_formats= # new option to specify wanted package formats for test environment using tox factors feature # defaults to "sdist" if not set py35: sdist wheel conda # names here are provided by plugins (reserved keywords) py27: sdist conda commands = py.test ``` Lising tox environments (`tox --list`) would display the following output: ``` (sdist) py27 (conda) py27 (sdist) py35 (wheel) py35 (conda) py35 ``` To remain backward-compatible, the package format will not be displayed if only a single package format is specified. How to skip building a package for a specific factor? Illustrate how to exclude a certain package format for a factor: ```ini [tox] plugins=conda envlist=py27,py35,py27-xdist [testenv] commands = py.test package_formats=sdist wheel conda exclude_package_formats= # new option which filters out packages py27-xdist: wheel ``` or possibly using the negated factor condition support: ```ini [tox] plugins=conda envlist=py27,py35,py27-xdist [testenv] commands = py.test package_formats= sdist !py27,!xdist: wheel conda ``` Output of `tox --list`: ``` (sdist) py27 (wheel) py27 (conda) py27 (sdist) py35 (wheel) py35 (conda) py35 (sdist) py27-xdist (conda) py27-xdist ``` ### Implemenation Details ``` tox_package_formats() -> ['conda'] # ['sdist', 'wheel'] tox_testenv_create(env_meta, package_type) -> # creates an environment for given package, using # information from env_meta (like .envdir) # returns: an "env" object which is forwaded to the next hooks tox_testenv_install(env_meta, package_type, env) -> # installs deps and package into environment tox_testenv_runtest(env_meta, package_type, env) -> # activates environment and runs test commands tox_testenv_updated(env_meta, package_type) -> # returns True if hte environment is already up to date # otherwise, tox will remove the environment completely and # create a new one ``` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/drafts/tox_conda_notes_niccodemus.md0000644000175100001710000000432200000000000023036 0ustar00vstsdocker00000000000000``` [tox] envlist=py27,py35 [testenv] commands= py.test --timeout=180 {posargs:tests} deps=pytest>=2.3.5 pytest-timeout # USE CASE 1: plain conda, with deps on tox.ini create_env_command = conda create --prefix {envdir} python={python_version} install_command = conda install --prefix {envdir} {opts} {packages} list_dependencies_command = conda list --prefix {envdir} # deprecated: see tox_create_popen hook linux:env_activate_command=source activate {envdir} win:env_activate_command=activate.bat {envdir} # USE CASE 2: plain conda, using requirements.txt install_command = conda install --prefix {envdir} {opts} --file requirements.txt # USE CASE 3: conda env create_env_command = conda env create --prefix {envdir} python={python_version} --file environment.yml install_command = [testenv] type=virtualenv type=venv type=conda type=conda-reqs type=conda-env ``` 1. Create a new ``create_env_command`` option. ;2. Create a new ``env_activate_command`` option (also consider how to make that platform dependent). 2. New substitution variable: {python_version} ('3.5', '2.7', etc') 3. env type concept: different types change the default options. 1. tox_addoption can now add new "testenv" sections to tox.ini: ``` [virtualenv] [conda] [venv] ``` 2. extend hooks: ``` * tox_addoption * tox_configure for each requested env in config: tox_testenv_up_to_date(envmeta) tox_testenv_create(envmeta) tox_testenv_install_deps(envmeta, env) tox_runtest_pre(envmeta, env) tox_runtest(envmeta, env, popen) tox_runtest_post(envmeta, env) ``` 3. separate virtualenv details from "VirtualEnv" class into a plugin. ``` [tox] envlist={py27,py35}-{sdist,wheel,conda} [package-sdist] command = python setup.py sdist [package-wheel] command = python setup.py bdist_wheel [package-conda] command = conda build ./conda-recipe [testenv:{sdist,wheel}] commands = py.test [testenv:conda] packages = sdist,wheel commands = py.test --conda-only ``` * tox_addoption * tox_get_python_executable * tox_configure for each requested env in config: tox_testenv_create(envmeta) tox_testenv_install_deps(envmeta, env) tox_runtest_pre(envmeta, env) tox_runtest(envmeta, env, popen) tox_runtest_post(envmeta, env) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9035115 tox-3.21.4/docs/example/0000755000175100001710000000000000000000000015264 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/example/basic.rst0000644000175100001710000003564500000000000017114 0ustar00vstsdocker00000000000000Basic usage ============================================= A simple tox.ini / default environments ----------------------------------------------- Put basic information about your project and the test environments you want your project to run in into a ``tox.ini`` file that should reside next to your ``setup.py`` file: .. code-block:: ini # content of: tox.ini , put in same dir as setup.py [tox] envlist = py27,py36 [testenv] # install testing framework # ... or install anything else you might need here deps = pytest # run the tests # ... or run any other command line tool you need to run here commands = pytest To sdist-package, install and test your project, you can now type at the command prompt: .. code-block:: shell tox This will sdist-package your current project, create two virtualenv_ Environments, install the sdist-package into the environments and run the specified command in each of them. With: .. code-block:: shell tox -e py36 you can restrict the test run to the python3.6 environment. Tox currently understands the following patterns: .. code-block:: shell py: The current Python version tox is using pypy: Whatever available PyPy there is jython: Whatever available Jython there is pyN: Python of version N. for example py2 or py3 ... etc pyNM: Python of version N.M. for example py27 or py38 ... etc pypyN: PyPy of version N. for example pypy2 or pypy3 ... etc pypyNM: PyPy version N.M. for example pypy27 or pypy35 ... etc However, you can also create your own test environment names, see some of the examples in :doc:`examples <../examples>`. pyproject.toml tox legacy ini ----------------------------- The tox configuration can also be in ``pyproject.toml`` (if you want to avoid an extra file). Currently only the old format is supported via ``legacy_tox_ini``, a native implementation is planned though. .. code-block:: toml [build-system] requires = [ "setuptools >= 35.0.2", "wheel >= 0.29.0"] build-backend = "setuptools.build_meta" [tool.tox] legacy_tox_ini = """ [tox] envlist = py27,py36 [testenv] deps = pytest >= 3.0.0, <4 commands = pytest """ Note that when you define a ``pyproject.toml`` you must define the ``build-system`` section per PEP-518. Specifying a platform ----------------------------------------------- .. versionadded:: 2.0 If you want to specify which platform(s) your test environment runs on you can set a platform regular expression like this: .. code-block:: ini [testenv] platform = linux2|darwin If the expression does not match against ``sys.platform`` the test environment will be skipped. Allowing non-virtualenv commands ----------------------------------------------- .. versionadded:: 1.5 Sometimes you may want to use tools not contained in your virtualenv such as ``make``, ``bash`` or others. To avoid warnings you can use the ``allowlist_externals`` testenv configuration: .. code-block:: ini # content of tox.ini [testenv] allowlist_externals = make /bin/bash .. _virtualenv: https://pypi.org/project/virtualenv .. _multiindex: Depending on requirements.txt or defining constraints ----------------------------------------------------- .. versionadded:: 1.6.1 (experimental) If you have a ``requirements.txt`` file or a ``constraints.txt`` file you can add it to your ``deps`` variable like this: .. code-block:: ini [testenv] deps = -rrequirements.txt or .. code-block:: ini [testenv] deps = -cconstraints.txt or .. code-block:: ini [testenv] deps = -rrequirements.txt -cconstraints.txt All installation commands are executed using ``{toxinidir}`` (the directory where ``tox.ini`` resides) as the current working directory. Therefore, the underlying ``pip`` installation will assume ``requirements.txt`` or ``constraints.txt`` to exist at ``{toxinidir}/requirements.txt`` or ``{toxinidir}/constraints.txt``. This is actually a side effect that all elements of the dependency list is directly passed to ``pip``. For more details on ``requirements.txt`` files or ``constraints.txt`` files please see: * https://pip.pypa.io/en/stable/user_guide/#requirements-files * https://pip.pypa.io/en/stable/user_guide/#constraints-files Using a different default PyPI URL ---------------------------------- To install dependencies and packages from a different default PyPI server you can type interactively: .. code-block:: shell tox -i https://pypi.my-alternative-index.org This causes tox to install dependencies and the sdist install step to use the specified URL as the index server. You can cause the same effect by using a ``PIP_INDEX_URL`` environment variable. This variable can be also set in ``tox.ini``: .. code-block:: ini [tox] setenv = PIP_INDEX_URL = https://pypi.my-alternative-index.org Alternatively, a configuration where ``PIP_INDEX_URL`` could be overriden from environment: .. code-block:: ini [tox] setenv = PIP_INDEX_URL = {env:PIP_INDEX_URL:https://pypi.my-alternative-index.org} Installing dependencies from multiple PyPI servers -------------------------------------------------- You can instrument tox to install dependencies from multiple PyPI servers, using ``PIP_EXTRA_INDEX_URL`` environment variable: .. code-block:: ini [tox] setenv = PIP_EXTRA_INDEX_URL = https://mypypiserver.org [testenv] deps = # docutils will be installed directly from PyPI docutils # mypackage missing at PyPI will be installed from custom PyPI URL mypackage This configuration will install ``docutils`` from the default Python PYPI server and will install the ``mypackage`` from our index server at ``https://mypypiserver.org`` URL. Further customizing installation --------------------------------- .. versionadded:: 1.6 By default tox uses `pip`_ to install packages, both the package-under-test and any dependencies you specify in ``tox.ini``. You can fully customize tox's install-command through the testenv-specific :conf:`install_command=ARGV` setting. For instance, to use pip's ``--find-links`` and ``--no-index`` options to specify an alternative source for your dependencies: .. code-block:: ini [testenv] install_command = pip install --pre --find-links https://packages.example.com --no-index {opts} {packages} .. _pip: https://pip.pypa.io/en/stable/ Forcing re-creation of virtual environments ----------------------------------------------- .. versionadded:: 0.9 To force tox to recreate a (particular) virtual environment: .. code-block:: shell tox --recreate -e py27 would trigger a complete reinstallation of the existing py27 environment (or create it afresh if it doesn't exist). Passing down environment variables ------------------------------------------- .. versionadded:: 2.0 By default tox will only pass the ``PATH`` environment variable (and on windows ``SYSTEMROOT`` and ``PATHEXT``) from the tox invocation to the test environments. If you want to pass down additional environment variables you can use the ``passenv`` option: .. code-block:: ini [testenv] passenv = LANG When your test commands execute they will execute with the same LANG setting as the one with which tox was invoked. Setting environment variables ------------------------------------------- .. versionadded:: 1.0 If you need to set an environment variable like ``PYTHONPATH`` you can use the ``setenv`` directive: .. code-block:: ini [testenv] setenv = PYTHONPATH = {toxinidir}/subdir When your test commands execute they will execute with a PYTHONPATH setting that will lead Python to also import from the ``subdir`` below the directory where your ``tox.ini`` file resides. Special handling of PYTHONHASHSEED ------------------------------------------- .. versionadded:: 1.6.2 By default, tox sets PYTHONHASHSEED_ for test commands to a random integer generated when ``tox`` is invoked. This mimics Python's hash randomization enabled by default starting `in Python 3.3`_. To aid in reproducing test failures, tox displays the value of ``PYTHONHASHSEED`` in the test output. You can tell tox to use an explicit hash seed value via the ``--hashseed`` command-line option to ``tox``. You can also override the hash seed value per test environment in ``tox.ini`` as follows: .. code-block:: ini [testenv] setenv = PYTHONHASHSEED = 100 If you wish to disable this feature, you can pass the command line option ``--hashseed=noset`` when ``tox`` is invoked. You can also disable it from the ``tox.ini`` by setting ``PYTHONHASHSEED = 0`` as described above. .. _`in Python 3.3`: https://docs.python.org/3/whatsnew/3.3.html#builtin-functions-and-types .. _PYTHONHASHSEED: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHASHSEED Integration with "setup.py test" command ---------------------------------------------------- .. warning:: ``setup.py test`` is `deprecated `_ and will be removed in a future version. .. _`ignoring exit code`: Ignoring a command exit code ---------------------------- In some cases, you may want to ignore a command exit code. For example: .. code-block:: ini [testenv:py27] commands = coverage erase {envbindir}/python setup.py develop coverage run -p setup.py test coverage combine - coverage html {envbindir}/flake8 loads By using the ``-`` prefix, similar to a ``make`` recipe line, you can ignore the exit code for that command. Compressing dependency matrix ----------------------------- If you have a large matrix of dependencies, python versions and/or environments you can use :ref:`generative-envlist` and :ref:`conditional settings ` to express that in a concise form: .. code-block:: ini [tox] envlist = py{36,37,38}-django{22,30}-{sqlite,mysql} [testenv] deps = django22: Django>=2.2,<2.3 django30: Django>=3.0,<3.1 # use PyMySQL if factors "py37" and "mysql" are present in env name py38-mysql: PyMySQL # use urllib3 if any of "py36" or "py37" are present in env name py36,py37: urllib3 # mocking sqlite on 3.6 and 3.7 if factor "sqlite" is present py{36,37}-sqlite: mock Using generative section names ------------------------------ Suppose you have some binary packages, and need to run tests both in 32 and 64 bits. You also want an environment to create your virtual env for the developers. .. code-block:: ini [testenv] basepython = py38-x86: python3.8-32 py38-x64: python3.8-64 commands = pytest [testenv:py38-{x86,x64}-venv] usedevelop = true envdir = x86: .venv-x86 x64: .venv-x64 commands = Prevent symbolic links in virtualenv ------------------------------------ By default virtualenv will use symlinks to point to the system's python files, modules, etc. If you want the files to be copied instead, possibly because your filesystem is not capable of handling symbolic links, you can instruct virtualenv to use the "--always-copy" argument meant exactly for that purpose, by setting the ``alwayscopy`` directive in your environment: .. code-block:: ini [testenv] alwayscopy = True .. _`parallel_mode`: Parallel mode ------------- ``tox`` allows running environments in parallel: - Invoke by using the ``--parallel`` or ``-p`` flag. After the packaging phase completes tox will run in parallel processes tox environments (spins a new instance of the tox interpreter, but passes through all host flags and environment variables). - ``-p`` takes an argument specifying the degree of parallelization, defaulting to ``auto``: - ``all`` to run all invoked environments in parallel, - ``auto`` to limit it to CPU count, - or pass an integer to set that limit. - Parallel mode displays a progress spinner while running tox environments in parallel, and reports outcome of these as soon as completed with a human readable duration timing attached. This spinner can be disabled by setting the environment variable ``TOX_PARALLEL_NO_SPINNER`` to the value ``1``. - Parallel mode by default shows output only of failed environments and ones marked as :conf:`parallel_show_output` ``=True``. - There's now a concept of dependency between environments (specified via :conf:`depends`), tox will re-order the environment list to be run to satisfy these dependencies (in sequential run too). Furthermore, in parallel mode, will only schedule a tox environment to run once all of its dependencies finished (independent of their outcome). .. warning:: ``depends`` does not pull in dependencies into the run target, for example if you select ``py27,py36,coverage`` via the ``-e`` tox will only run those three (even if ``coverage`` may specify as ``depends`` other targets too - such as ``py27, py35, py36, py37``). - ``--parallel-live``/``-o`` allows showing the live output of the standard output and error, also turns off reporting described above. - Note: parallel evaluation disables standard input. Use non parallel invocation if you need standard input. Example final output: .. code-block:: bash $ tox -e py27,py36,coverage -p all ✔ OK py36 in 9.533 seconds ✔ OK py27 in 9.96 seconds ✔ OK coverage in 2.0 seconds ___________________________ summary ______________________________________________________ py27: commands succeeded py36: commands succeeded coverage: commands succeeded congratulations :) Example progress bar, showing a rotating spinner, the number of environments running and their list (limited up to \ 120 characters): .. code-block:: bash ⠹ [2] py27 | py36 .. _`auto-provision`: tox auto-provisioning --------------------- In case the host tox does not satisfy either the :conf:`minversion` or the :conf:`requires`, tox will now automatically create a virtual environment under :conf:`provision_tox_env` that satisfies those constraints and delegate all calls to this meta environment. This should allow automatically satisfying constraints on your tox environment, given you have at least version ``3.8.0`` of tox. For example given: .. code-block:: ini [tox] minversion = 3.10.0 requires = tox_venv >= 1.0.0 if the user runs it with tox ``3.8.0`` or later installed tox will automatically ensured that both the minimum version and requires constraints are satisfied, by creating a virtual environment under ``.tox`` folder, and then installing into it ``tox >= 3.10.0`` and ``tox_venv >= 1.0.0``. Afterwards all tox invocations are forwarded to the tox installed inside ``.tox\.tox`` folder (referred to as meta-tox or auto-provisioned tox). This allows tox to automatically setup itself with all its plugins for the current project. If the host tox satisfies the constraints expressed with the :conf:`requires` and :conf:`minversion` no such provisioning is done (to avoid setup cost when it's not explicitly needed). ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/example/devenv.rst0000644000175100001710000000713500000000000017313 0ustar00vstsdocker00000000000000======================= Development environment ======================= tox can be used for just preparing different virtual environments required by a project. This feature can be used by deployment tools when preparing deployed project environments. It can also be used for setting up normalized project development environments and thus help reduce the risk of different team members using mismatched development environments. Creating development environments using the ``--devenv`` option =============================================================== The easiest way to set up a development environment is to use the ``--devenv`` option along with your existing configured ``testenv``\ s. The ``--devenv`` option accepts a single argument, the location you want to create a development environment at. For example, if I wanted to replicate the ``py36`` environment, I could run:: $ tox --devenv venv-py36 -e py36 ... $ source venv-py36/bin/activate (venv-py36) $ python --version Python 3.6.7 The ``--devenv`` option skips the ``commands=`` section of that configured test environment and always sets ``usedevelop=true`` for the environment that is created. If you don't specify an environment with ``-e``, the devenv feature will default to ``-e py`` -- usually taking the interpreter you're running ``tox`` with and the default ``[testenv]`` configuration. It is possible to use the ``--devenv`` option without a tox configuration file, however the configuration file is respected if present. Creating development environments using configuration ===================================================== Here are some examples illustrating how to set up a project's development environment using tox. For illustration purposes, let us call the development environment ``dev``. Example 1: Basic scenario ------------------------- Step 1 - Configure the development environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ First, we prepare the tox configuration for our development environment by defining a ``[testenv:dev]`` section in the project's ``tox.ini`` configuration file: .. code-block:: ini [testenv:dev] basepython = python2.7 usedevelop = True In it we state: - what Python executable to use in the environment, - that our project should be installed into the environment using ``setup.py develop``, as opposed to building and installing its source distribution using ``setup.py install``. The development environment will reside in ``toxworkdir`` (default is ``.tox``) just like the other tox environments. We can configure a lot more, if we want to. For example, we can add the following to our configuration, telling tox not to reuse ``commands`` or ``deps`` settings from the base ``[testenv]`` configuration: .. code-block:: ini [testenv:dev] commands = deps = Step 2 - Create the development environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Once the ``[testenv:dev]`` configuration section has been defined, we create the actual development environment by running the following: .. code-block:: shell tox -e dev This creates the environment at the path specified by the environment's ``envdir`` configuration value. Example 2: A more complex scenario ---------------------------------- Let us say we want our project development environment to: - use Python executable ``python2.7``, - pull packages from ``requirements.txt``, located in the same directory as ``tox.ini``. Here is an example configuration for the described scenario: .. code-block:: ini [testenv:dev] basepython = python2.7 usedevelop = True deps = -rrequirements.txt ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/example/documentation.rst0000644000175100001710000000420700000000000020672 0ustar00vstsdocker00000000000000Generate documentation ====================== It's possible to generate the projects documentation with tox itself. The advantage of this path is that now generating the documentation can be part of the CI, and whenever any validations/checks/operations fail while generating the documentation you'll catch it within tox. Sphinx ------ No need to use the cryptic make file to generate a sphinx documentation. One can use tox to ensure all right dependencies are available within a virtual environment, and even specify the python version needed to perform the build. For example if the sphinx file structure is under the ``doc`` folder the following configuration will generate the documentation under ``{toxworkdir}/docs_out`` and print out a link to the generated documentation: .. code-block:: ini [testenv:docs] description = invoke sphinx-build to build the HTML docs basepython = python3.7 deps = sphinx >= 1.7.5, < 2 commands = sphinx-build -d "{toxworkdir}/docs_doctree" doc "{toxworkdir}/docs_out" --color -W -bhtml {posargs} python -c 'import pathlib; print("documentation available under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "docs_out" / "index.html"))' Note here we say we also require python 3.7, allowing us to use f-strings within the sphinx ``conf.py``. Now one can specify a separate test environment that will validate that the links are correct. mkdocs ------ Define one environment to write/generate the documentation, and another to deploy it. Use the config substitution logic to avoid defining dependencies multiple time: .. code-block:: ini [testenv:docs] description = Run a development server for working on documentation basepython = python3.7 deps = mkdocs >= 1.7.5, < 2 mkdocs-material commands = mkdocs build --clean python -c 'print("###### Starting local server. Press Control+C to stop server ######")' mkdocs serve -a localhost:8080 [testenv:docs-deploy] description = built fresh docs and deploy them deps = {[testenv:docs]deps} basepython = {[testenv:docs]basepython} commands = mkdocs gh-deploy --clean ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/example/general.rst0000644000175100001710000002213700000000000017440 0ustar00vstsdocker00000000000000.. be in -*- rst -*- mode! General tips and tricks ================================ Interactively passing positional arguments ----------------------------------------------- If you invoke ``tox`` like this: .. code-block:: shell tox -- -x tests/test_something.py the arguments after the ``--`` will be substituted everywhere where you specify ``{posargs}`` in your test commands, for example using ``pytest``: .. code-block:: ini [testenv] # Could also be in a specific ``[testenv:]`` section commands = pytest {posargs} or using ``nosetests``: .. code-block:: ini [testenv] commands = nosetests {posargs} the above ``tox`` invocation will trigger the test runners to stop after the first failure and to only run a particular test file. You can specify defaults for the positional arguments using this syntax: .. code-block:: ini [testenv] commands = nosetests {posargs:--with-coverage} .. _recreate: Dependency changes and tracking ------------------------------- Creating virtual environments and installing dependencies is an expensive operation. Therefore tox tries to avoid it whenever possible, meaning it will never perform this unless it detects with absolute certainty that it needs to perform an update. A tox environment creation is made up of: - create the virtual environment - install dependencies specified inside deps - if it's a library project (has build package phase), install library dependencies (with potential extras) These three steps are only performed once (given they all succeeded). Subsequent calls that don't detect changes to the traits of that step will not alter the virtual environment in any way. When a change is detected for any of the steps, the entire virtual environment is removed and the operation starts from scratch (this is because it's very hard to determine what would the delta changes would be needed - e.g. a dependency could migrate from one dependency to another, and in this case we would need to install the new while removing the old one). Here's what traits we track at the moment for each steps: - virtual environment trait is tied to the python path the :conf:`basepython` resolves too (if this config changes, the virtual environment will be recreated), - :conf:`deps` sections changes (meaning any string-level change for the entries, note requirement file content changes are not tracked), - library dependencies are tracked at :conf:`extras` level (because there's no Python API to enquire about the actual dependencies in a non-tool specific way, e.g. setuptools has one way, flit something else, and poetry another). Whenever you change traits that are not tracked we recommend you to manually trigger a rebuild of the tox environment by passing the ``-r`` flag for the tox invocation. For instance, for a setuptools project whenever you modify the ``install_requires`` keyword at the next run force the recreation of the tox environment by passing the recreate cli tox flag. .. _`TOXENV`: Selecting one or more environments to run tests against -------------------------------------------------------- Using the ``-e ENV[,ENV36,...]`` option you explicitly list the environments where you want to run tests against. For example, given the previous sphinx example you may call: .. code-block:: shell tox -e docs which will make ``tox`` only manage the ``docs`` environment and call its test commands. You may specify more than one environment like this: .. code-block:: shell tox -e py27,py36 which would run the commands of the ``py27`` and ``py36`` testenvironments respectively. The special value ``ALL`` selects all environments. You can also specify an environment list in your ``tox.ini``: .. code-block:: ini [tox] envlist = py27,py36 or override it from the command line or from the environment variable ``TOXENV``: .. code-block:: shell export TOXENV=py27,py36 # in bash style shells .. _artifacts: Access package artifacts between multiple tox-runs -------------------------------------------------------- If you have multiple projects using tox you can make use of a ``distshare`` directory where ``tox`` will copy in sdist-packages so that another tox run can find the "latest" dependency. This feature allows you to test a package against an unreleased development version or even an uncommitted version on your own machine. By default, ``{homedir}/.tox/distshare`` will be used for copying in and copying out artifacts (i.e. Python packages). For project ``two`` to depend on the ``one`` package you use the following entry: .. code-block:: ini # example two/tox.ini [testenv] # install latest package from "one" project deps = {distshare}/one-*.zip That's all. tox running on project ``one`` will copy the sdist-package into the ``distshare`` directory after which a ``tox`` run on project ``two`` will grab it because ``deps`` contain an entry with the ``one-*.zip`` pattern. If there is more than one matching package the highest version will be taken. ``tox`` uses verlib_ to compare version strings which must be compliant with :pep:`386`. If you want to use this with Jenkins_, also checkout the :ref:`jenkins artifact example`. .. _verlib: https://bitbucket.org/tarek/distutilsversion/ basepython defaults, overriding +++++++++++++++++++++++++++++++ For any ``pyXY`` test environment name the underlying ``pythonX.Y`` executable will be searched in your system ``PATH``. Similarly, for ``jython`` and ``pypy`` the respective ``jython`` and ``pypy-c`` names will be looked for. The executable must exist in order to successfully create *virtualenv* environments. On Windows a ``pythonX.Y`` named executable will be searched in typical default locations using the ``C:\PythonXY\python.exe`` pattern. All other targets will use the system ``python`` instead. You can override any of the default settings by defining the :conf:`basepython` variable in a specific test environment section, for example: .. code-block:: ini [testenv:docs] basepython = python2.7 Avoiding expensive sdist ------------------------ Some projects are large enough that running an sdist, followed by an install every time can be prohibitively costly. To solve this, there are two different options you can add to the ``tox`` section. First, you can simply ask tox to please not make an sdist: .. code-block:: ini [tox] skipsdist=True If you do this, your local software package will not be installed into the virtualenv. You should probably be okay with that, or take steps to deal with it in your commands section: .. code-block:: ini [testenv] commands = python setup.py develop pytest Running ``setup.py develop`` is a common enough model that it has its own option: .. code-block:: ini [testenv] usedevelop=True And a corresponding command line option ``--develop``, which will set ``skipsdist`` to True and then perform the ``setup.py develop`` step at the place where ``tox`` normally performs the installation of the sdist. Specifically, it actually runs ``pip install -e .`` behind the scenes, which itself calls ``setup.py develop``. There is an optimization coded in to not bother re-running the command if ``$projectname.egg-info`` is newer than ``setup.py`` or ``setup.cfg``. .. include:: ../links.rst Understanding ``InvocationError`` exit codes -------------------------------------------- When a command (defined by ``commands =`` in ``tox.ini``) fails, it has a non-zero exit code, and an ``InvocationError`` exception is raised by ``tox``: .. code-block:: shell ERROR: InvocationError for command '' (exited with code 1) If the command starts with ``pytest`` or ``python setup.py test`` for instance, then the `pytest exit codes`_ are relevant. On unix systems, there are some rather `common exit codes`_. This is why for exit codes larger than 128, if a signal with number equal to `` - 128`` is found in the :py:mod:`signal` module, an additional hint is given: .. code-block:: shell ERROR: InvocationError for command '' (exited with code 139) Note: this might indicate a fatal error signal (139 - 128 = 11: SIGSEGV) where ```` is the command defined in ``tox.ini``, with quotes removed. The signal numbers (e.g. 11 for a segmentation fault) can be found in the "Standard signals" section of the `signal man page`_. Their meaning is described in `POSIX signals`_. Beware that programs may issue custom exit codes with any value, so their documentation should be consulted. Sometimes, no exit code is given at all. An example may be found in `pytest-qt issue #170`_, where Qt was calling ``abort()`` instead of ``exit()``. .. seealso:: :ref:`ignoring exit code`. .. _`pytest exit codes`: https://docs.pytest.org/en/latest/usage.html#possible-exit-codes .. _`common exit codes`: http://www.faqs.org/docs/abs/HTML/exitcodes.html .. _`abort()``: http://www.unix.org/version2/sample/abort.html .. _`pytest-qt issue #170`: https://github.com/pytest-dev/pytest-qt/issues/170 .. _`signal man page`: http://man7.org/linux/man-pages/man7/signal.7.html .. _`POSIX signals`: https://en.wikipedia.org/wiki/Signal_(IPC)#POSIX_signals ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/example/jenkins.rst0000644000175100001710000001747600000000000017476 0ustar00vstsdocker00000000000000Using tox with the Jenkins Integration Server ================================================= Using Jenkins multi-configuration jobs ------------------------------------------- The Jenkins_ continuous integration server allows you to define "jobs" with "build steps" which can be test invocations. If you :doc:`install <../install>` ``tox`` on your default Python installation on each Jenkins agent, you can easily create a Jenkins multi-configuration job that will drive your tox runs from the CI-server side, using these steps: * install the Python plugin for Jenkins under "manage jenkins" * create a "multi-configuration" job, give it a name of your choice * configure your repository so that Jenkins can pull it * (optional) configure multiple nodes so that tox-runs are performed on multiple hosts * configure ``axes`` by using :ref:`TOXENV ` as an axis name and as values provide space-separated test environment names you want Jenkins/tox to execute. * add a **Python-build step** with this content (see also next example): .. code-block:: python import tox os.chdir(os.getenv("WORKSPACE")) tox.cmdline() # environment is selected by ``TOXENV`` env variable * check ``Publish JUnit test result report`` and enter ``**/junit-*.xml`` as the pattern so that Jenkins collects test results in the JUnit XML format. The last point requires that your test command creates JunitXML files, for example with ``pytest`` it is done like this: .. code-block:: ini [testenv] commands = pytest --junitxml=junit-{envname}.xml **zero-installation** for agents ------------------------------------------------------------- .. note:: This feature is broken currently because "toxbootstrap.py" has been removed. Please file an issue if you'd like to see it back. If you manage many Jenkins agents and want to use the latest officially released tox (or latest development version) and want to skip manually installing ``tox`` then substitute the above **Python build step** code with this: .. code-block:: python import urllib, os url = "https://bitbucket.org/hpk42/tox/raw/default/toxbootstrap.py" # os.environ['USETOXDEV']="1" # use tox dev version d = dict(__file__="toxbootstrap.py") exec urllib.urlopen(url).read() in d d["cmdline"](["--recreate"]) The downloaded ``toxbootstrap.py`` file downloads all necessary files to install ``tox`` in a virtual sub environment. Notes: * uncomment the line containing ``USETOXDEV`` to use the latest development-release version of tox instead of the latest released version. * adapt the options in the last line as needed (the example code will cause tox to reinstall all virtual environments all the time which is often what one wants in CI server contexts) Integrating "sphinx" documentation checks in a Jenkins job ---------------------------------------------------------------- If you are using a multi-configuration Jenkins job which collects JUnit Test results you will run into problems using the previous method of running the sphinx-build command because it will not generate JUnit results. To accommodate this issue one solution is to have ``pytest`` wrap the sphinx-checks and create a JUnit result file which wraps the result of calling sphinx-build. Here is an example: 1. create a ``docs`` environment in your ``tox.ini`` file like this: .. code-block:: ini [testenv:docs] basepython = python # change to ``doc`` dir if that is where your sphinx-docs live changedir = doc deps = sphinx pytest commands = pytest --tb=line -v --junitxml=junit-{envname}.xml check_sphinx.py 2. create a ``doc/check_sphinx.py`` file like this: .. code-block:: python import subprocess def test_linkcheck(tmpdir): doctrees = tmpdir.join("doctrees") htmldir = tmpdir.join("html") subprocess.check_call( ["sphinx-build", "-W", "-blinkcheck", "-d", str(doctrees), ".", str(htmldir)] ) def test_build_docs(tmpdir): doctrees = tmpdir.join("doctrees") htmldir = tmpdir.join("html") subprocess.check_call( ["sphinx-build", "-W", "-bhtml", "-d", str(doctrees), ".", str(htmldir)] ) 3. run ``tox -e docs`` and then you may integrate this environment along with your other environments into Jenkins. Note that ``pytest`` is only installed into the docs environment and does not need to be in use or installed with any other environment. .. _`jenkins artifact example`: Access package artifacts between Jenkins jobs -------------------------------------------------------- .. _`Jenkins Copy Artifact plugin`: https://wiki.jenkins.io/display/JENKINS/Copy+Artifact+Plugin In an extension to :ref:`artifacts` you can also configure Jenkins jobs to access each others artifacts. ``tox`` uses the ``distshare`` directory to access artifacts and in a Jenkins context (detected via existence of the environment variable ``HUDSON_URL``); it defaults to to ``{toxworkdir}/distshare``. This means that each workspace will have its own ``distshare`` directory and we need to configure Jenkins to perform artifact copying. The recommend way to do this is to install the `Jenkins Copy Artifact plugin`_ and for each job which "receives" artifacts you add a **Copy artifacts from another project** build step using roughly this configuration: .. code-block:: shell Project-name: name of the other (tox-managed) job you want the artifact from Artifacts to copy: .tox/dist/*.zip # where tox jobs create artifacts Target directory: .tox/distshare # where we want it to appear for us Flatten Directories: CHECK # create no subdir-structure You also need to configure the "other" job to archive artifacts; This is done by checking ``Archive the artifacts`` and entering: .. code-block:: shell Files to archive: .tox/dist/*.zip So our "other" job will create an sdist-package artifact and the "copy-artifacts" plugin will copy it to our ``distshare`` area. Now everything proceeds as :ref:`artifacts` shows it. So if you are using defaults you can re-use and debug exactly the same ``tox.ini`` file and make use of automatic sharing of your artifacts between runs or Jenkins jobs. Avoiding the "path too long" error with long shebang lines --------------------------------------------------------------- When using ``tox`` on a Jenkins instance, there may be a scenario where ``tox`` can not invoke ``pip`` because the shebang (Unix) line is too long. Some systems only support a limited amount of characters for an interpreter directive (e.x. Linux as a limit of 128). There are two methods to workaround this issue: 1. Invoke ``tox`` with the ``--workdir`` option which tells ``tox`` to use a specific directory for its virtual environments. Using a unique and short path can prevent this issue. 2. Use the environment variable ``TOX_LIMITED_SHEBANG`` to deal with environments with interpreter directive limitations (consult :ref:`long interpreter directives` for more information). Running tox environments in parallel ------------------------------------ Jenkins has parallel stages allowing you to run commands in parallel, however tox package building it is not parallel safe. Use the ``--parallel--safe-build`` flag to enable parallel safe builds (this will generate unique folder names for ``distdir``, ``distshare`` and ``log``. Here's a generic stage definition demonstrating how to use this inside Jenkins: .. code-block:: groovy stage('run tox envs') { steps { script { def envs = sh(returnStdout: true, script: "tox -l").trim().split('\n') def cmds = envs.collectEntries({ tox_env -> [tox_env, { sh "tox --parallel--safe-build -vve $tox_env" }] }) parallel(cmds) } } } .. include:: ../links.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/example/nose.rst0000644000175100001710000000202600000000000016762 0ustar00vstsdocker00000000000000nose and tox ================================= It is easy to integrate `nosetests`_ runs with tox. For starters here is a simple ``tox.ini`` config to configure your project for running with nose: Basic nosetests example -------------------------- Assuming the following layout: .. code-block:: shell tox.ini # see below for content setup.py # a classic distutils/setuptools setup.py file and the following ``tox.ini`` content: .. code-block:: ini [testenv] deps = nose # ``{posargs}`` will be substituted with positional arguments from command line commands = nosetests {posargs} you can invoke ``tox`` in the directory where your ``tox.ini`` resides. ``tox`` will sdist-package your project create two virtualenv environments with the ``python2.7`` and ``python3.6`` interpreters, respectively, and will then run the specified test command. More examples? ------------------------------------------ Also you might want to checkout :doc:`general` and :doc:`documentation`. .. include:: ../links.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/example/package.rst0000644000175100001710000000526100000000000017415 0ustar00vstsdocker00000000000000Packaging ========= Although one can use tox to develop and test applications one of its most popular usage is to help library creators. Libraries need first to be packaged, so then they can be installed inside a virtual environment for testing. To help with this tox implements PEP-517_ and PEP-518_. This means that by default tox will build source distribution out of source trees. Before running test commands ``pip`` is used to install the source distribution inside the build environment. To create a source distribution there are multiple tools out there and with PEP-517_ and PEP-518_ you can easily use your favorite one with tox. Historically tox only supported ``setuptools``, and always used the tox host environment to build a source distribution from the source tree. This is still the default behavior. To opt out of this behaviour you need to set isolated builds to true. setuptools ---------- Using the ``pyproject.toml`` file at the root folder (alongside ``setup.py``) one can specify build requirements. .. code-block:: toml [build-system] requires = [ "setuptools >= 35.0.2", "setuptools_scm >= 2.0.0, <3" ] build-backend = "setuptools.build_meta" .. code-block:: ini # tox.ini [tox] isolated_build = True flit ---- flit_ requires ``Python 3``, however the generated source distribution can be installed under ``python 2``. Furthermore it does not require a ``setup.py`` file as that information is also added to the ``pyproject.toml`` file. .. code-block:: toml [build-system] requires = ["flit_core >=2,<4"] build-backend = "flit_core.buildapi" [tool.flit.metadata] module = "package_toml_flit" author = "Happy Harry" author-email = "happy@harry.com" home-page = "https://github.com/happy-harry/is" .. code-block:: ini # tox.ini [tox] isolated_build = True poetry ------ poetry_ requires ``Python 3``, however the generated source distribution can be installed under ``python 2``. Furthermore it does not require a ``setup.py`` file as that information is also added to the ``pyproject.toml`` file. .. code-block:: toml [build-system] requires = ["poetry_core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry] name = "package_toml_poetry" version = "0.1.0" description = "" authors = ["Name "] .. code-block:: ini # tox.ini [tox] isolated_build = True [tox:.package] # note tox will use the same python version as under what tox is installed to package # so unless this is python 3 you can require a given python version for the packaging # environment via the basepython key basepython = python3 .. include:: ../links.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/example/platform.rst0000644000175100001710000000254000000000000017643 0ustar00vstsdocker00000000000000Platform specification ============================ Basic multi-platform example ---------------------------- Assuming the following layout: .. code-block:: shell tox.ini # see below for content setup.py # a classic distutils/setuptools setup.py file and the following ``tox.ini`` content: .. code-block:: ini [tox] # platform specification support is available since version 2.0 minversion = 2.0 envlist = py{27,36}-{mylinux,mymacos,mywindows} [testenv] # environment will be skipped if regular expression does not match against the sys.platform string platform = mylinux: linux mymacos: darwin mywindows: win32 # you can specify dependencies and their versions based on platform filtered environments deps = mylinux,mymacos: py==1.4.32 mywindows: py==1.4.30 # upon tox invocation you will be greeted according to your platform commands= mylinux: python -c 'print("Hello, Linus!")' mymacos: python -c 'print("Hello, Steve!")' mywindows: python -c 'print("Hello, Bill!")' you can invoke ``tox`` in the directory where your ``tox.ini`` resides. ``tox`` creates two virtualenv environments with the ``python2.7`` and ``python3.6`` interpreters, respectively, and will then run the specified command according to platform you invoke ``tox`` at. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/example/pytest.rst0000644000175100001710000001046000000000000017347 0ustar00vstsdocker00000000000000pytest and tox ================================= 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>`. Basic example -------------------------- Assuming the following layout: .. code-block:: shell tox.ini # see below for content setup.py # a classic distutils/setuptools setup.py file and the following ``tox.ini`` content: .. code-block:: ini [tox] envlist = py35,py36 [testenv] deps = pytest # PYPI package providing pytest commands = pytest {posargs} # substitute with tox' positional arguments you can now invoke ``tox`` in the directory where your ``tox.ini`` resides. ``tox`` will sdist-package your project, create two virtualenv environments with the ``python3.5`` and ``python3.6`` interpreters, respectively, and will then run the specified test command in each of them. Extended example: change dir before test and use per-virtualenv tempdir -------------------------------------------------------------------------- Assuming the following layout: .. code-block:: shell tox.ini # see below for content setup.py # a classic distutils/setuptools setup.py file tests # the directory containing tests and the following ``tox.ini`` content: .. code-block:: ini [tox] envlist = py35,py36 [testenv] changedir = tests deps = pytest # change pytest tempdir and add posargs from command line commands = pytest --basetemp="{envtmpdir}" {posargs} you can invoke ``tox`` in the directory where your ``tox.ini`` resides. Differently than in the previous example the ``pytest`` command will be executed with a current working directory set to ``tests`` and the test run will use the per-virtualenv temporary directory. .. _`passing positional arguments`: Using multiple CPUs for test runs ----------------------------------- ``pytest`` supports distributing tests to multiple processes and hosts through the `pytest-xdist`_ plugin. Here is an example configuration to make ``tox`` use this feature: .. code-block:: ini [testenv] deps = pytest-xdist changedir = tests # use three sub processes commands = pytest --basetemp="{envtmpdir}" \ --confcutdir=.. \ -n 3 \ {posargs} .. _`listed as a known issue`: Known issues and limitations ----------------------------- **Too long filenames**. you may encounter "too long filenames" for temporarily created files in your pytest run. Try to not use the "--basetemp" parameter. **installed-versus-checkout version**. ``pytest`` collects test modules on the filesystem and then tries to import them under their `fully qualified name`_. This means that if your test files are importable from somewhere then your ``pytest`` invocation may end up importing the package from the checkout directory rather than the installed package. This issue may be characterised by pytest test-collection error messages, in python 3.x environments, that look like: .. code-block:: shell import file mismatch: imported module 'myproj.foo.tests.test_foo' has this __file__ attribute: /home/myuser/repos/myproj/build/lib/myproj/foo/tests/test_foo.py which is not the same as the test file we want to collect: /home/myuser/repos/myproj/myproj/foo/tests/test_foo.py HINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules There are a few ways to prevent this. With installed tests (the tests packages are known to ``setup.py``), a safe and explicit option is to give the explicit path ``{envsitepackagesdir}/mypkg`` to pytest. Alternatively, it is possible to use ``changedir`` so that checked-out files are outside the import path, then pass ``--pyargs mypkg`` to pytest. With tests that won't be installed, the simplest way to run them against your installed package is to avoid ``__init__.py`` files in test directories; pytest will still find and import them by adding their parent directory to ``sys.path`` but they won't be copied to other places or be found by Python's import system outside of pytest. .. _`fully qualified name`: https://docs.pytest.org/en/latest/goodpractices.html#test-package-name .. include:: ../links.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/example/result.rst0000644000175100001710000000232600000000000017337 0ustar00vstsdocker00000000000000Writing a JSON result file -------------------------------------------------------- .. versionadded: 1.6 You can instruct tox to write a json-report file via: .. code-block:: shell tox --result-json=PATH This will create a json-formatted result file using this schema: .. code-block:: json { "testenvs": { "py27": { "python": { "executable": "/home/hpk/p/tox/.tox/py27/bin/python", "version": "2.7.3 (default, Aug 1 2012, 05:14:39) \n[GCC 4.6.3]", "version_info": [ 2, 7, 3, "final", 0 ] }, "test": [ { "output": "...", "command": [ "/home/hpk/p/tox/.tox/py27/bin/pytest", "--instafail", "--junitxml=/home/hpk/p/tox/.tox/py27/log/junit-py27.xml", "tests/test_config.py" ], "retcode": "0" } ], "setup": [] } }, "platform": "linux2", "installpkg": { "basename": "tox-1.6.0.dev1.zip", "sha256": "b6982dde5789a167c4c35af0d34ef72176d0575955f5331ad04aee9f23af4326" }, "toxversion": "1.6.0.dev1", "reportversion": "1" } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/example/unittest.rst0000644000175100001710000000470700000000000017705 0ustar00vstsdocker00000000000000unittest2, discover and tox =============================== Running unittests with 'discover' ------------------------------------------ The discover_ project allows you to discover and run unittests that you can easily integrate it in a ``tox`` run. As an example, perform a checkout of `Pygments `_: .. code-block:: shell hg clone https://bitbucket.org/birkenfeld/pygments-main and add the following ``tox.ini`` to it: .. code-block:: ini [tox] envlist = py27,py35,py36 [testenv] changedir = tests commands = discover deps = discover If you now invoke ``tox`` you will see the creation of three virtual environments and a unittest-run performed in each of them. Running unittest2 and sphinx tests in one go ----------------------------------------------------- .. _`Michael Foord`: http://www.voidspace.org.uk/ `Michael Foord`_ has contributed a ``tox.ini`` file that allows you to run all tests for his mock_ project, including some sphinx-based doctests. If you checkout its repository with: .. code-block:: shell git clone https://github.com/testing-cabal/mock.git The checkout has a `tox.ini file `_ that looks like this: .. code-block:: ini [tox] envlist = py27,py35,py36,py37 [testenv] deps = unittest2 commands = unit2 discover [] [testenv:py36] commands = unit2 discover [] sphinx-build -b doctest docs html sphinx-build docs html deps = unittest2 sphinx [testenv:py27] commands = unit2 discover [] sphinx-build -b doctest docs html sphinx-build docs html deps = unittest2 sphinx mock uses unittest2_ to run the tests. Invoking ``tox`` starts test discovery by executing the ``unit2 discover`` commands on Python 2.7, 3.5, 3.6 and 3.7 respectively. Against Python3.6 and Python2.7 it will additionally run sphinx-mediated doctests. If building the docs fails, due to a reST error, or any of the doctests fails, it will be reported by the tox run. The ``[]`` parentheses in the commands provide :ref:`positional substitution` which means you can e.g. type: .. code-block:: shell tox -- -f -s SOMEPATH which will ultimately invoke: .. code-block:: shell unit2 discover -f -s SOMEPATH in each of the environments. This allows you to customize test discovery in your ``tox`` runs. .. include:: ../links.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/examples.rst0000644000175100001710000000056600000000000016210 0ustar00vstsdocker00000000000000tox configuration and usage examples ============================================================================== .. toctree:: :maxdepth: 2 example/basic.rst example/package.rst example/pytest.rst example/unittest example/nose.rst example/documentation.rst example/general.rst example/jenkins.rst example/devenv.rst example/platform.rst ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9035115 tox-3.21.4/docs/img/0000755000175100001710000000000000000000000014405 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/img/tox_flow.png0000644000175100001710000011602500000000000016761 0ustar00vstsdocker00000000000000PNG  IHDRyGy&gAMA a cHRMz&u0`:pQ<PLTEbbbOOOsss===)))}w{tYR:amwΓ.*wUĵꥥYYYDDDnnn...neHD>-‹؛maPs`VH(!4+ K? rjX @5{fjߪer~K.#YD(D4RpBĕYnnT2Ҡ_ b:Kvn{fff"z|*w9s4t&x|DzoXnj^jhQrl/v?~qԵپ`ϊkdqpcelbgIYM'ҷ]AD;.(n`0PoīVp9 ջ^ǮX뾾Ɏttteee~ZI[>2? wos]u/&0M>N  gShf88wbKGDH pHYs.#.#x?vtIME7ڗIDATx3Uo6S7_m/`')_wwtu _rsd zt|ѡDnOy @pB*fd?NTD.Ů OS{i);&=X%qa2cuQn{/}s'oA0wݐ'ɾ͛_m/~ܹ/k'cps a/f;;=ɯFy'wvfA5NĘ%=(Z(ɎxڮZ )ْ/T:_#_{87 yxaί=̎,_b7Fxs6Z'$?'ihTTw7'ȑξI~5 8!Ff; I{W.D'{RmA5bAP<{K JEic<'|RGAx_v*vX{Y,-Ֆjr@[7=lǿۉ~_1cjbk1ILy'nElem|ݻ'7jc},ʬA$o8Ф gܔ_;="mS(Y|)^׬^m!$)\Խtීo߸Ay8w;YP]\;#M{T I]y;]g}dۙͲ[bpwqAyn^ 7Ԓy[C[{7{_hmv_`?W̊paݸAy87bVFF2ޅ_q[Ny`7%.5<en/ng /SޛKGnS=+Jk[I>I?&o E It qʎ7Ly7NJ3n,Q"W~o x|k~3uW8*oS^(XtIyp4͓=7ծ' iL:~WWw3UŁR^(ZʔLboIyN!J>NՖV|`v+oF\O=^h^;I*o; 8Zރn.2gRqiztA<V,'~q^@\Na@$yQd5 t'o1Z^^[sPzWWWG[^Iz/6WnE9WP˫Xե̵ G"yiѮ:i W.קFA.EfP\(;fDv/oZCsyby/-yo쩫ɝo_)u*N\xwK%nyyӋmT;J9bU^2 ;ݍ ;3'W]^^Od^_\79ۚQ\W"~ 8wo{;Y>896{olo/'hco{'~^!Vnomƛ[;;{ڎ_Ga_fdGdɐ_NNG_\hs[xv'q/yzlQT̓N`Q^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^+:kVY_3ݬz (o(=[eq̌wأ᣼(o13.ͪ`ڣUV̸|7H=>kVY_3ݬz (o(=[eq̌wأ᣼(o13.ͪ`ڣUV̸|7H=>kVY_3ݬz (o(=[eq̌wأ᣼(o13.ͪ`ڣUV̸|7H=>kVY_3ݬz (o(=[]wR{7|.:`ڣEyCGQQ^{(oH=>kV  GyGyQꢼ#(o(=[]7t$ Gyأ᣼<{/qnV={7|gҨ8fY@QQ^{f GyGyQ*+cf\U$ Gy/qnV={7|孲8fY@QQ^{f GyGyQ*+cf\U$ Gy/qnV={7|孲8fY@QQ^{f GyGyQ*+cf\U$ Gy/qnV={7|孲8fY@QQ^{f GyGyq}#(o(=[]7t$ Gyأ᣼(ouQБ{7|.:`ڣEyCGQQ^{(oH=>kV  GyGyq&*+cf\U$ Gy/qnV={7|孲8fY@QQ^{f GyGyQ*+cf\U$ Gy/qݤ͛[;훛7`ڣUV̸ hÒjX&`ڣUV̸ V^iE, GyGyQ*+cf\Ŕhh7F], GyGyQ*+cf\Eth(]w{{+^%(o(=[eq̌}M]?tȜ>|̣5KQQ^{wG~[$ Gy/q9NСޯC<G/neأ᣼(o13.j V˽'mtأtBO\ڣug]ʻe?Uፗ{?[55J@(N{0vNg0Wߨ]P>)jE}WwWnUҭ:$,7?;=Ѱ=KQ^{jR^u,ѿX,QyK'LW괧¸˾孮zW^ %!'l7;],GNܵp{|P^{jQ9rתӕpP>;fv?z=zŠw=s|kVW+wc7UݬMM/ow_3DA6R^{jP޶<UsY~pQ M{jxޏV孮{x|U͙ǁR#xݽ_+o8f3S̫: Q4qεwW렼%sW.¿}.$VSO-ڞmy{WGiP^{W^[_3rC_䘣|Dj7䝬.IC1+R_LU3!>(rG/q9ȎX<3 |v;yؿdh˫4߽UΞ8S^{WEkלO_]CʛWks~Gީ߈ڣUV̸WG62Z0>57Օ+޹:hsc/hyŊGnxz?dw|ا]iy?)W)ncFcO~:}?dVfm#Ɲ~'K?O|GxQq̌+uI4;;scU_`E@mNh?9ig0E\FE3ƃC%kgFWaO8FM{ͧ}:` #u8 ӑFh<=C_;eFnᏯQ59may1^b>u睏EwZ;{K?1;7Ro5J9[xT#O?N-cf\CɵWx@ %sHJ9eHp'(~|M}vi2⧟eF[VV,᪗ pFy:zUh4'osj{tP=m:I;K'ޅ?&W7?M|wz)xUVMŔ>DzJe3'z4Њ>t:S7|4Dl<%XK"HdU-{xPR-^3=͢~rxz;37o&gR2}fG; qWoCyt I(]py.˙i@|D%Q8C=Z}2Ǔh'=z3ēm7~SubYIsg+΢q6o4nڔ`kd6xΒMMT-`3܁\OO8+ԟ]xjegU%4]O'CTrsg$|d.ǽb&Grd?85jZG3Ӿ|^}[w8>b(Z*>48w5Oͼ|2o#O.&W 'o3HFU~ WQ]jV gOPB.ں+ ?}P [Нꡇ>i^Lx3de?S+I߾W)983^5X.Mib6NIN}5D,Jq澳pr9Xrz|j+N}d9}(uۼbdzf&;KޤʰN4+oEyV7>"ɮQztpmdyc'i|.b izP^WlV{9-/of(}nFCN}](oKBë3ry?5URoA?1}'T|2<-Cuz'/U%O+:yS5+*o&WY오Nus{GeP+wһ,/iP2|v޴`ZƅfɏU.ygB.<,NӼǒ?42=5s?y,9Hx2-H\y1˛=/)v=!G),EsrtxI X5ƳM܋ԉ/!@^5aGv۪CKjgiUhl#3Ʋf e]_]啓ڠKܗ%-SOLi1S|R/>z-}H>CcOg)9'ifXWTtۜ^yUR Ѭk@y'T94xoJ,g՟欼鵊Zݙ^}kOA'g._ EyKFڷ[-yQ[җKޥV|͟M#HՃ?P)zLZQޖ9?͵3mOWgR1s}^ޠk. ٜHfm_z}H^ʫl?H; j;UTK=쨢+NpJyV$W's$o.7ujGD $ڛ:*jarҪS,oOI/ `hnyGW/?ʿgy>-yzIu)ꑏ?# wשO%_xrwO~JˣI[U=e 8jNm`%쨃c{h:0AzcAAVW43ϭϺsO56COnCRwA_b4u2oˌ߳A. B+Wcs)DVeuO; ~p<{ gW/|3zmS|_ͼ_d6?/<=u' Lꀡx$[~cnk7O-O IW7x/]Yϙ=}KmVQd1{>뚅3xb!نI߭++]nR-W> W^n?}]/eϪ~A.tZ? 5KIy_0mNyIy?3s5ϖQ$FWI.;?n~O>fU{y|w?eW*,e٭pyЛ@.zˊbS2bggT^HA{$sEMyU5֗7~?#6˛˾M^|na2z3/<_y$e *dd sݾ)UM@56 NcD˷+U^T}_H|VW,//}g}ڌd ~13j_2jrY\Ly/ZqwnEN֬Sl^&P^1^WF.wZ,Z}&mb.thUyL{H=c2~qq-+|3fR2j/DyWwB[Y_grY1lKPTQ+e+#zGG;3Ww~yA=coYyfy\y"^V/$KUKf!q*.{(+ǮeEU4ԌsH徢-nW\=ϙ%){a~AKʛW79SӟB;/xOX㸢{qfḳ.eyo8Ց~*API77oyex&0+ʛ<|uyzvYU޹Q{nըq:;_ d'_{7ݟD{6ۖ ,*"uXl/$]X˼keC>3jheާB(o_k3h}*N+O|wf5Oa]*&_^q8KgڹƌvŵkQ^vާK /ūFmF}-eNVZ3rЛkK"oUPI."뭼1qAR ^d⹢ejK瓇;Njoq}k]V5N\Kžr'l9-\+QC^j*_]Dywed?%]/}q澹wZU^9/%2$}Ƽ>()Sա꘥//tqz2WxW8v%N- r[^q<':YE_p+6 \E.ov4\=*uϛ|73I'sXٔWK|QQ>N;wZ _N!sX}I\HS]^wZoiS޾}/1~}ctr紧s\bH^,5{-g^Mon7pryٷyjpM2h_ާqJy3C>%#%ҳ.RT3j_VQfyo/~Mp޻yɏu[n'Rbձ9ct@zEܱ*+JNrKܾɵ \:>u2J=8j, _5}$z .ogX8ω򪝬>zM~T6ݫnT6F?--A a'fy?Br9(>KQ՗^˪x>5OaIyW]:Md&J,*_Ϸf̸gUU4{n3_^|³}8Ow}6ygęg[*ϋ/>P->?3yY~g/~*F|2j(oUƗwŤܗW]+%_Ph男T\,_̔+kya_fb+|=/ԋ|o& +X@ye?(wk>{#0v8;3-t݅ zk_u|}ˋmP\wP3|grm:W-_1]&3QpvgUu2/ezIfoaw^ q̌Y|6v_V5nx++&s6VA>Q,uf퉞M&kܗUn睝oYwZ*wp?ۏk;[6kvh^RPޕTJwLXoESǯ;~ɫn)7g'[](13.wi~h=?!ޠUK@.?uN ^fGg_6wWδtp7ݫ]^ujEr]+*_X"}֋/_u ̯E__|[iy"7~{#Eqկ3^VA_voTn&*;N}jWV}e41'H0VEh@yXچ޻#;Xw]eƙ}p7:Zl*4Uqܯ:֯Vw++ߕ뫿ony۱XR)z[ot6OPT:*sTk8\Q\{.^"Ogz6~ߕy>ۊv&zuT9shByD`I}MC5{g?b7Zd-;0[;8]VޯǷn}S/~%w_wݯAU^/q9 6eq{OݪEx KXVFsWt9û*tI<>{ϦЫʛzt_+?>?rgk^ly-^Vv9[ޯg}ڿΠ8JfwJ: (T]؆,OV0gXU޹&壝y2} ՠ<^lyc[ofʴWy;v]ꮷ;kaHe]?WBQWQuԫkuʵ߻P^1o{;umD+ԫo睿+[%es|y>eg_yfV`S^gQ[o/P"kQQ\> YbͿ/--7ңJZ-}R|yce=ޯ~UX- |y!Dso&k} #9SbN]Wi72G"sv:P\ond{}.*j &MghϗWS5i kնGAS^ħZ1^y4$m3i|W3Y ^z3i,6^4'7c5\M5b*,]+Z r0=6ϸjW73m,OXӂQ^ {"*GWjV93d/~%[o%O眔XS`))/V*l 2ڹ%hڕ4CNsgyB*=^ꤑiR LyO_,GU墼jV^{{v߽ݗ^K- $7)0YXwK׿#Nyq¦lrqzD?՛QZcՂu>ZG(W~-=ſ%<CJ4UByo%~]=+:vm)nR\%cHuTxǃY(&v} ʛ\=ܒ:q-[/~ŋ/+N=˻kW#{nAo&lPy;+cf+or)EHq̌mDv=Ҡr 5@yQ*+cf\s{-m#qTΞ~N|s$;;? &W^}kx)Qoq̌Y|KEmx(=:FM\:TTGʫw!wYjq$By5/q̌U{DHww隷2I̻/s}l+ܖbIU^}?kAyQ*+cf\ڳEHv7^>mωn[BIjY^^e !UѰ/]孲8f(=ݝ 'ryXVӴr[]2S᥼kyCV̸g#=2o{lݏ-ZӸݬ{ܬihu*ڣUV̸\d1K*2moLyy5'"c{\ᒼs(=[eq̌U{nT7;ʻ'9{]G(o4>=9ާx'>EyQ*+cf\c:ҫ3=H8JkM&P~P}F>7J孲8f>@(: I]7D';;;,or鋻fkkVY_3rMuQEbֆOyZgM;sy孲8fAzWi3,mN&ݻׯxDkVY_3r>qaz8ʋ斷ꚫ'G/>xLw޷*孮;/o{oP7TR67*ݽ8H<){yssZd؎Nng^P6ʛ蟝_VѰ]孮BO@(N{0;kP^{(oꐀPe:]'(Foh(cf\U5J@(o(=兵8fY@QQ^{f GyVW*tH/Gy/qnV="%&v;F4EyQ*+cf\UH@I(w)o(o13.ͪ:$,wNϮVY_3ݬzCvΦhؖ?8 w&EY?;Fy(=[eq̌w!xv9#Emu490<{[ɈZ0(o13.ͪ +s1˓FN8RmKw|:1UU[8ʼnFUYLyQ*+cf\U&f7.o412y"6ZT|THjv@Q*+cf\U&F7D]sŶޤbikno^HyCEy/qnV=]39#]̹2zuX'/VDyCEy/qnV=]~ђ}47TfCx dK/;WYw {9S`Q*+cf\U&fw*fʛ;kV  GyGyy8E G_3ݬz (o(=ΤQeq̌wأ᣼(o13.ͪ`ڣUV̸|7H=>kVY_3ݬz (o(=[eq̌wأ᣼(o13.ͪ`ڣUV̸|7H=>kVY_3ݬz (o(=[eq̌wأ᣼(o13.ͪ`ڣUV̸|7H=>kVY_3ݬz (o(=[eq̌wأ᣼(o13.ͪ`yCGQQ^{(oH=0m$F3ݷ.cf\Uڡqy/?0m(-wS^,K@fuw+H!cf\Uڡqy/?0mVL\wS"vh|c/:Q4=Q&@PEyӗmN_ 8;})/PF@P}cP;})/dBz)oCwR^ $Qt{*6B}/B2DyӗAa;@yӗmN_ 8;})/PF@P8o@yӗ!ZEmN_ J;})/6 ;})/PFCyӗmN_ =U@yӗ!E饼 QKyh{*6B}/B;ǡ(o#wR^ (ۢ QKy6B}/ k {XI;})/*(o#wR^ $ICPKypH6B}/@p(o#wR^PKyWPKyoDykc{\@H8W5v-u{\@H8DylkYxw|S m(o,+r孳e=VNQ^v|1Ǿ- 孵%뵲Aa+ڒZLypTDy+)/Τ!Qz;f G 6j孹zlCykn{5 8m(/T孻lxO|s4H/孿Zl@P(Dy]͔ k[{'u^Ly~W6S^᡼׮f ^Q^|OҐ)/SV6'(s'iHڔD I6aes:{$MyP٠P^Azl6(c$: f:FQ^Q#cuأ@(c$ef;=&Q^H= 4Xw{<{h3V6+1`2ceBykWe-MyY,Q^|OҐ)/3V6K9ߓ4$?m df:{$MyY,P^JrAlp=1{6B2q\ISn F]{6Bҏq\ISJ1\ISDȔ@ lnQ4fZ# `}Ct_9ߓt)p DF:qj.П[(,:5BmFz)s%M(֋oՍNou38gH=?}iF#ӓNF&Ŕ 饼Ε4BvōV4U'JDy8t|)/DR?^\ISnaRIkPYQj `mR/u)7_^QLے!>ƹ8G|x:z:LyP^yߐΔױ0_޸VWq[y}sCm֝a`YuWGy•^ڧ:櫼C39U]'I?;~3qjAǼTGyP~y^XM-vK+^p=e_`;kH# *>[R^<|t_贝>/Ayq `AQ^H= Fy#(/uأ@(k1`AQ^H= Fyk }Fg~6 Gy]έQ4.nJ]^Ք*5Ϻ[h-QS^P^(s+?*u:򳦼u:򳦼u:w3ӳ|ymt]qg|_pLgd8ݮxAwx:M\y;ݳ4Ҭ틗gŻv燽]=`᠝];m1'ţP^ P5Gy&XiB& 3O MyT!ҷ-]b]3Yh3Z, i(N)/$un*Ś fYW-LʂveL.^N gjRq xΒḐq$u=doG&]٬K=bH&szu"Vw㢞w~z/^4Ec&b!]Q@x(k׹xjj0Mku.ufrGZYÚ};weUsD>KrS= IjຼK\/tfzG[zQ31 r)/un$:̢䮖^EMV83]Ŗ/tz~?I̠T/KrOeb| ]x׶[NmI7NVu:7fh8ijY[ ktK@=)~U/?){ Hζ^zޅNfU\{ar6; zW f\43> Fy|k2;S3FvA-w| .)vn͓vs`mvfmDy]-$ ۸EW.i.wuwٻ~bK`d ʹբ.(/P8u򊽘'Z+96wt;UU$~@jDyQ^|;ezo]\{˷ =3j,;ov6=NeY BDy]-)YP&;$/F2|qoM"~5Z2̯9ց<2bA9Ol:_Z^#YfS})/ul!R$pt,ʳG^\>w"7y7T퐼3kjgu,u:a=X}u:3XRK&^tYL9ҏ],Y׼L=Ҝ"rIyA['$':H'z#߱?z!Q@x(kױGiyh$5 t0UY,ҵ*hb6vԕe Ns&zw*L{.Jcc(Fy*ٲ9S^ Dy_!]Dy[j遰j!Re΁a6ӓbͫ˛^p%KW@{@yQkv{@yS}8O^?ѫ',Sɟ+byG $~ѫ?AG ?xz/ Bz1(Q^p?, W=o̕ooh?2y 6~+ɳ~ cO|ۻkLq?2P^pIy_{ee_7b?c++,?U˷/׼я+񫯊c2 aߜBz}OҐ(\R^Οf(2oJ)Ţ2?}U?_wE?QzN!?!(/Pj?5L'e)M(Zf$ zϒ!l/DgQ^8ɞLq!'iHO .)O#1ߘߑ'7^}GIy2Yzh+*?6{_AR^x5}Z=ICZP^pIyQfi5[r\yU#ϾR"~6^d> =ICbP^pIycFؗʫ=V%W~.^fG 1J{;;q6'i8:ARޗ3_{3J~roy.+Ѽ7)oө HK@VW5W_+^R(= )_}WեDy/56"l??>~W޹y(sxُ76+y͸;_eMأ@VWE~v./2ۈ%7_y#j9[ޟ$qy-SxrTCz?בwwecc;ۻx~T~M[AAHkz5k>T?K;ǙKѤ<gOϮӇGyŌK-ݸqE'\l#Hx͏ŢnqOo_K~UK-=}_*AXo}_m4]/loC6ub 1N+=j3^nGy͏GGdr@N.­.y}%ٷ9cqv,}+s_~Mjl~Ϋ~Gܒ;X@y]^Wk~9snʋ%[hy癮HGTRoV)s yi:~=*p7d@.8ǛeqOޠdKh6lM.4iqk0Ly'o*]n^Ɠ~S+|;ϓ>*e-&b)u:PeyŎXKx9H3kr>UeB/pus@y갢7"YQ l{6 $˥ׅۃS桁= lO NWT7^/aʋӆ1GYVfEQ^(kױ[R(/u,NPB Fy+jRX^s`,{%2)/P8uIUf`0J]mQ^ 5\QӋ )9+MyQ^(sEMBCE(r6 Gy]5꘠t2.0'MyQ^(s'iHڔ(u:{$MyQ^(s'iHڔ(u:{$MyQ^(s'iHڔ(u:{$MyQ^(s'iHڔ(u:{$MyQ^(s'鼷n߾v׷oߞ7v?m Fy=I7q;E[!Q^Ǫw{o*p޹}ƑAQ^Ǫ_8NƑAQ^*_߾b8nQ#Iy P^(cI;op6nMa#Iy P^(c$ޯ2?b/~[oڍ{4[7en-Ozb{`Ey0P^(c'oon;y/d߻ɵꠠޛo]6䠦I{7nN(/ultoԾO+}ׯ2;os~9M^wKms^E$Co aQ^Z"'Xۍw[]ũ\U Ȼζ2#j\Konr%Eqa%_n;X8u%;uS#wdзWF݈]Y_Lt`;vjQ^ 5\+Ǔ^KNV ⊕ɉ2[ؠߘ]~V{SKʢ,]ڔ(u:WUf/2˥RNN\wEBRq݋v6-t@y]9ͮ}}'U]ߙVZN};8%[LհE`EyP^(sN/<{B2bWq{mzfyT;4;X{N\ wbzdsuq_7ێ(/u \{W_"Pl}crpn}{s"%WWH]Y޵nJ:'e{_a;X1KyR;})/PFCyӗmN_ 8;})/PFCyӗmN_ 8;})/PFCyӗmN_ 8;})/PFCyӗmN_ 8;})/PFCyӗmN_ 8;})/PFCyӗmN_ 8;})/PFCyӗmN_ 8;})/PFCyӗmN_ 8;})/PFCyӗmN_ 8;% 8}K@p(om- r孳e==VNQ^u) 8֎/8EyڒZLyڒZLyqW6S^᡼vP͔@x(om|e3[s^Ly|y}k@p(o͵=>Q^uw q孻ZlCy]͔@x(oye3[{u^Ly^+)/P;f +d){W9cy@-} VwBS13|s@W cU TR[ޛ>VML W6f Yw:ɛx`G;x<t#^yz6GxPb p4TӶͣF`` ^8<M{fbg۝Σh8k]š-Wpo{`LTݞzb|sDHOn>q񵝡~(,# ALJZdPe5t7NZ^3hٿ.|޶;(^=t(ZuŢ8QeOlYe=6a-t6N?jY3S-]hkԌou͝jMxLɌ]4鷮@yP^{f]w`K=vy'Ѩ*D.DQɧwUFL+e[RFBGt5d!W'u> ϗamByZ"ʻ}m6ҵadiyyY>QNY-qEݮ`/h'ѼQ-ѩ_q&*lWˤm(jy({d~yeՏN^m&~|Pn/VKzR'8rGOGy_S^V2*u?tWm饼@q(PԑJ}.yzsGV٪fGoO ,i պu_(ylܧ6 ϒ'IešB-oMǨ<a4XQ;XVaVʤ.p+8]Z޹?fgwUu>oۏhk^(_ś.eLyT; ,JwxNʻdm4_^$12ho,98 T$+M,U¦4BΗwǜm5坫KR^8^,KlbyV +yY^y`p]Yqkir҄=(=[W};rG̚+<G?ypKKSk]N<_ E E3wՇ>_^gqmpjqđ]JхfmV_,H[ϡ@1ėwBQ $]/CD*Z+{'=?ܗLyeeG̱t-;yŜ_ E E3mv^<͡MyegEz>V\H09K+;3bfʛUtG9twn3t/'s4 hy۷6%ݸPPߪ6s(ͱzh_U'<=ݥckN1> \5gpy~@P^{y>>L[UDK;ތV7s̏ ]{7$Ѹ/-o/yix/;*W.R^@P wGQ]qGqk0及:5}qX%%VM=NSvx?R^H^ZEK(jy>o$CyZޛEw8ء5AyP^{ÊW  7Q+_vq|)o(/Pk//^$Cy[ޛ{3BQ^8^qr^wC]`š-al&k@q(pv]޶^$Cy[^'9:{(pyo-oWuP^8^uUWuP^8^sY=Q`šB.X_H=  [SCڕX@q(@%롼@q({&`š.k&렼@q({cEx3w H%  .d٫`š/aCKByP^{F;^V^) (/Pk/Lo%k@q(7Wz/ XCyա9~xI(/PknV\%U,t?Y̡@q(zu{TV*‡:1 o5Y, DyըE;7q[w̝-!u:f ګOyՁwUz+ߤX5:'@q(ڔwC qk{{_u_#ʻ#,qhwӶ|^B>5(Wnk-rxL,%O9r[1 ګKy77fY^ՁX0GyK=wzO詧@( 3(:6{M[7/Ȼ)!~R^8^]ʻEmy3YەXL_@yQyIGdb uP^{u)8Hk&RIľyͮstZ;DyQU49{t-^]ʻۜrVeC*ڣk8m!ʋϣ·XW,uyyYۜZK 饼((_k.ݾУvʛGz[lg>7R^TK [[jTK?{8(%hZTEtVݕ[|߮p^]ʻū'Lyd[\3ʋ + 3WA5K+η!dPV 巼K\L.Vѝ(T~ګMyBwIyŒ1WLKwܷ)/OXjS⻤7SNڔ7U_ץh{4/3gk>{NWƔZ5o\{)7>48nn4߈o7nno껊MbI|% ^~&Ԥ饼U&u k(/.Gx^kWoE oKyQJ7sM:(/ [_ڣ:\uk.+@y[;WUM/GyC+j{:ޢEy4wxsrP^{7tQ3uOѿf 9ik)(=:o P;7?)/ʹwQѽ(=:o Pz׽ޟ굟(/<ީ.E(o%`+_tNP^xu(z9k_NS^T%-(o%@,Υv"D(/|r_Q)=: P?JW@mP^x4s^o[l.CyQyLj5h{w'\襼(o|&@oY^]jo9+TpK/GyC3zdfU^s?g(/8 \&P[QyM^6XAyϹ[wJQǒފk 5tH›ʭ<|yDyaosKh.9uW *99sg_4R^xv*e+-*x蛿K6K?f K0dgz+V7wV޿wz//㟳CM^翫27 G򺜆1 Fs/$} _ܝ9͝ީs\޿J-Hycy+4yM@w~NimD _N (ٍ|ToXH_4Ὗ^ЕFqK/oȕ&O_zê?(fR^)7%ۄM `zX.uҺ4TEMCX;cd'GgM(_҆EKcm?}?'#[C-vR^Ǽ&ӝ-ԦU9'Oڬ3.e7T4<_U6N=1򶋞ױ&9'w?vTSq]??dz,g|==> )cUM@4,q𣈃֮e7[Q-멼%MC Gy(*YPs)YmXNozͮQ^=%_]Dyb[AWUm?W̌̚wA|g+pWQ^V^yT_/>}7ߟ6Xuy uX\XyUFlVO^.i6Aϥ5(Sh_үQE"(/o!(9r.ch(}Q^|,4>o!(O< ] wBQ $n.Yh|c=CP:ntcb|-@"ʋQ^\}*]AyqM3u.(/ʋr[E^ʻ nV^ʻ ܝ,ȸGyQ^`7r`ڣ:(/f}r(= "|Q^{XE.o~GyuP^︊5 AyKC*y@DyQ^`˪{W AySV8w XE^9[R5P^`ո᥼k:(/\dV\%Q^{X kw{@yQ^`Nt;P^{XZV{M@yQ^`twW~EyAyuP^;yE/ڣ:(/\j_{$-Q^{X[e C}}렼p=\w\OpܡByuP^8bTU]4˻fZ(oP^`.DҬ\,*ʫsF$ 9[+X"g]mtvN؄ Ay@w"tyGqTQP^`,Wlե ByuP^8 ;ĞUA(/ fOwᾥG[+X O{\3X -}y=[/X =}X29^,,9W^IAyD;靝v/W\Ce6uByuP^r6':="5DypEݢP^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^ʋQ^̌ "p@X"ʋQ^a(/Gy%b*ʋ:AyQ@8(/cW|%=\]zI7P%tEXtdate:create2018-07-19T15:19:55+00:00t%tEXtdate:modify2018-07-19T15:19:55+00:00 IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/index.rst0000644000175100001710000001666300000000000015506 0ustar00vstsdocker00000000000000Welcome to the tox automation project =============================================== Vision: standardize testing in Python --------------------------------------------- ``tox`` aims to automate and standardize testing in Python. It is part of a larger vision of easing the packaging, testing and release process of Python software. What is tox? -------------------- tox is a generic virtualenv_ management and test command line tool you can use for: * checking that your package installs correctly with different Python versions and interpreters * running your tests in each of the environments, configuring your test tool of choice * acting as a frontend to Continuous Integration servers, greatly reducing boilerplate and merging CI and shell-based testing. Basic example ----------------- First, install ``tox`` with ``pip install tox``. Then put basic information about your project and the test environments you want your project to run in into a ``tox.ini`` file residing right next to your ``setup.py`` file: .. code-block:: ini # content of: tox.ini , put in same dir as setup.py [tox] envlist = py27,py36 [testenv] # install pytest in the virtualenv where commands will be executed deps = pytest commands = # NOTE: you can run any command line tool here - not just tests pytest You can also try generating a ``tox.ini`` file automatically, by running ``tox-quickstart`` and then answering a few simple questions. To sdist-package, install and test your project against Python2.7 and Python3.6, just type:: tox and watch things happen (you must have python2.7 and python3.6 installed in your environment otherwise you will see errors). When you run ``tox`` a second time you'll note that it runs much faster because it keeps track of virtualenv details and will not recreate or re-install dependencies. You also might want to checkout :doc:`examples` to get some more ideas. System overview --------------- .. figure:: img/tox_flow.png :align: center :width: 800px tox workflow diagram .. The above image raw can be found and edited by using the toxdevorg Google role account under https://www.lucidchart.com/documents/edit/5d921f32-f2e1-4618-a265-7f9e30503dc6/0 tox roughly follows the following phases: 1. **configuration:** load ``tox.ini`` and merge it with options from the command line and the operating system environment variables. 2. **packaging** (optional): create a source distribution of the current project by invoking .. code-block:: bash python setup.py sdist Note that for this operation the same Python environment will be used as the one tox is installed into (therefore you need to make sure that it contains your build dependencies). Skip this step for application projects that don't have a ``setup.py``. 3. **environment** - for each tox environment (e.g. ``py27``, ``py36``) do: 1. **environment creation**: create a fresh environment, by default virtualenv_ is used. tox will automatically try to discover a valid Python interpreter version by using the environment name (e.g. ``py27`` means Python 2.7 and the ``basepython`` configuration value) and the current operating system ``PATH`` value. This is created at first run only to be re-used at subsequent runs. If certain aspects of the project change, a re-creation of the environment is automatically triggered. To force the recreation tox can be invoked with ``-r``/``--recreate``. 2. **install** (optional): install the environment dependencies specified inside the :conf:`deps` configuration section, and then the earlier packaged source distribution. By default ``pip`` is used to install packages, however one can customise this via :conf:`install_command`. Note ``pip`` will not update project dependencies (specified either in the ``install_requires`` or the ``extras`` section of the ``setup.py``) if any version already exists in the virtual environment; therefore we recommend to recreate your environments whenever your project dependencies change. 3. **commands**: run the specified commands in the specified order. Whenever the exit code of any of them is not zero stop, and mark the environment failed. Note, starting a command with a single dash character means ignore exit code. 4. **report** print out a report of outcomes for each tox environment: .. code:: bash ____________________ summary ____________________ py27: commands succeeded ERROR: py36: commands failed Only if all environments ran successfully tox will return exit code ``0`` (success). In this case you'll also see the message ``congratulations :)``. tox will take care of environment isolation for you: it will strip away all operating system environment variables not specified via :conf:`passenv`. Furthermore, it will also alter the ``PATH`` variable so that your commands resolve first and foremost within the current active tox environment. In general all executables in the path are available in ``commands``, but tox will emit a warning if it was not explicitly allowed via :conf:`allowlist_externals`. Current features ------------------- * **automation of tedious Python related test activities** * **test your Python package against many interpreter and dependency configs** - automatic customizable (re)creation of virtualenv_ test environments - installs your ``setup.py`` based project into each virtual environment - test-tool agnostic: runs pytest, nose or unittests in a uniform manner * :doc:`plugin system ` to modify tox execution with simple hooks. * uses pip_ and setuptools_ by default. Support for configuring the installer command through :conf:`install_command=ARGV`. * **cross-Python compatible**: CPython-2.7, 3.5 and higher, Jython and pypy_. * **cross-platform**: Windows and Unix style environments * **integrates with continuous integration servers** like Jenkins_ (formerly known as Hudson) and helps you to avoid boilerplatish and platform-specific build-step hacks. * **full interoperability with devpi**: is integrated with and is used for testing in the devpi_ system, a versatile PyPI index server and release managing tool. * **driven by a simple ini-style config file** * **documented** :doc:`examples ` and :doc:`configuration ` * **concise reporting** about tool invocations and configuration errors * **professionally** :doc:`supported ` * supports :ref:`using different / multiple PyPI index servers ` Related projects ---------------- tox has influenced several other projects in the Python test automation space. If tox doesn't quite fit your needs or you want to do more research, we recommend taking a look at these projects: - `Invoke `__ is a general-purpose task execution library, similar to Make. Invoke is far more general-purpose than tox but it does not contain the Python testing-specific features that tox specializes in. - `Nox `__ is a project similar in spirit to tox but different in approach. Nox's key difference is that it uses Python scripts instead of a configuration file. Nox might be useful if you find tox's configuration too limiting but aren't looking to move to something as general-purpose as Invoke or Make. .. toctree:: :hidden: install examples config support changelog plugins developers example/result announce/changelog-only .. include:: links.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/install.rst0000644000175100001710000000306000000000000016030 0ustar00vstsdocker00000000000000tox installation ================================== Install info in a nutshell ---------------------------------- **Pythons**: CPython 2.7 and 3.5 or later, Jython-2.5.1, pypy-1.9ff **Operating systems**: Linux, Windows, OSX, Unix **Installer Requirements**: setuptools_ **License**: MIT license **git repository**: https://github.com/tox-dev/tox Installation with pip -------------------------------------- Use the following command: .. code-block:: shell pip install tox It is fine to install ``tox`` itself into a virtualenv_ environment. Install from clone ------------------------- Consult the GitHub page how to clone the git repository: https://github.com/tox-dev/tox and then install in your environment with something like: .. code-block:: shell $ cd $ pip install . or install it `editable `_ if you want code changes to propagate automatically: .. code-block:: shell $ cd $ pip install --editable . so that you can do changes and submit patches. [Linux/macOS] Install via your package manager ---------------------------------------------- You can also find tox packaged for many Linux distributions and Homebrew for macOs - usually under the name of **python-tox** or simply **tox**. Be aware though that there also other projects under the same name (most prominently a `secure chat client `_ with no affiliation to this project), so make sure you install the correct package. .. include:: links.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/links.rst0000644000175100001710000000252600000000000015510 0ustar00vstsdocker00000000000000.. _`Cookiecutter`: https://cookiecutter.readthedocs.io .. _`pluggy`: https://pluggy.readthedocs.io .. _`cookiecutter-tox-plugin`: https://github.com/tox-dev/cookiecutter-tox-plugin .. _devpi: https://doc.devpi.net .. _Python: https://www.python.org .. _virtualenv: https://pypi.org/project/virtualenv .. _`pytest`: https://pytest.org .. _nosetests: .. _`nose`: https://pypi.org/project/nose .. _`Holger Krekel`: https://twitter.com/hpk42 .. _`pytest-xdist`: https://pypi.org/project/pytest-xdist .. _ConfigParser: https://docs.python.org/3/library/configparser.html .. _`easy_install`: http://peak.telecommunity.com/DevCenter/EasyInstall .. _pip: https://pypi.org/project/pip .. _setuptools: https://pypi.org/project/setuptools .. _`jenkins`: https://jenkins.io/index.html .. _sphinx: https://pypi.org/project/Sphinx .. _discover: https://pypi.org/project/discover .. _unittest2: https://pypi.org/project/unittest2 .. _mock: https://pypi.org/project/mock/ .. _flit: https://flit.readthedocs.io/en/latest/ .. _poetry: https://poetry.eustace.io/ .. _pypy: https://pypy.org .. _`Python Packaging Guide`: https://packaging.python.org/tutorials/packaging-projects/ .. _`tox.ini`: :doc:configfile .. _`PEP-508`: https://www.python.org/dev/peps/pep-0508/ .. _`PEP-517`: https://www.python.org/dev/peps/pep-0517/ .. _`PEP-518`: https://www.python.org/dev/peps/pep-0518/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/plugins.rst0000644000175100001710000001571100000000000016051 0ustar00vstsdocker00000000000000.. be in -*- rst -*- mode! tox plugins =========== .. versionadded:: 2.0 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. Using plugins ------------- To start using a plugin you need to install it in the same environment where the tox host is installed. e.g.: .. code-block:: shell $ 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 word "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-tags (0.2.0) - Allows running subsets of environments based on tags 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 from source hosters like Github or Bitbucket (or from a local clone). See the associated `pip documentation `_. 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 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. .. code-block:: shell $ pip install -U cookiecutter $ cookiecutter gh:tox-dev/cookiecutter-tox-plugin Tutorial: a minimal tox plugin ------------------------------ .. note:: 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). 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: .. code-block:: shell $ mkdir tox-fireworks $ cd tox-fireworks $ touch tox_fireworks.py $ touch setup.py contents of ``tox_fireworks.py``: .. code-block:: python import pluggy hookimpl = pluggy.HookimplMarker("tox") @hookimpl def tox_addoption(parser): """Add command line option to display fireworks on request.""" @hookimpl def tox_configure(config): """Post process config after parsing.""" @hookimpl def tox_runenvreport(config): """Display fireworks if all was fine and requested.""" .. note:: See :ref:`toxHookSpecsApi` for details 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"], ) 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: .. autoclass:: tox.config.Parser() :members: .. autoclass:: tox.config.Config() :members: .. autoclass:: tox.config.TestenvConfig() :members: .. autoclass:: tox.venv.VirtualEnv() :members: .. autoclass:: tox.session.Session() :members: .. include:: links.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/docs/support.rst0000644000175100001710000000176700000000000016112 0ustar00vstsdocker00000000000000.. _support: Support and contact channels ===================================== Getting in contact: * join the `tox-dev mailing list`_ for tox related questions and development discussions * file a `report on the issue tracker`_ * hang out on the irc.freenode.net #pylib channel * `fork the github repository`_ and submit merge/pull requests (see the developers help page -- :ref:`developers`) Paid professional support ---------------------------- Contact holger at `merlinux.eu`_, an association of experienced well-known Python developers. .. _`Testing In Python (TIP) mailing list`: http://lists.idyll.org/listinfo/testing-in-python .. _`holger's twitter presence`: https://twitter.com/hpk42 .. _`merlinux.eu`: https://merlinux.eu .. _`report on the issue tracker`: https://github.com/tox-dev/tox/issues .. _`tetamap blog`: https://holgerkrekel.net .. _`tox-dev mailing list`: https://mail.python.org/mm3/mailman3/lists/tox-dev.python.org/ .. _`fork the github repository`: https://github.com/tox-dev/tox ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/pyproject.toml0000644000175100001710000000240600000000000015617 0ustar00vstsdocker00000000000000[build-system] requires = [ "setuptools >= 40.0.4", "setuptools_scm >= 2.0.0, <6", "wheel >= 0.29.0", ] build-backend = 'setuptools.build_meta' [tool.towncrier] package = "tox" filename = "docs/changelog.rst" directory = "docs/changelog" template = "docs/changelog/template.jinja2" title_format = "v{version} ({project_date})" issue_format = "`#{issue} `_" underlines = ["-", "^"] [[tool.towncrier.section]] path = "" [[tool.towncrier.type]] directory = "bugfix" name = "Bugfixes" showcontent = true [[tool.towncrier.type]] directory = "feature" name = "Features" showcontent = true [[tool.towncrier.type]] directory = "deprecation" name = "Deprecations (removal in next major release)" showcontent = true [[tool.towncrier.type]] directory = "breaking" name = "Backward incompatible changes" showcontent = true [[tool.towncrier.type]] directory = "doc" name = "Documentation" showcontent = true [[tool.towncrier.type]] directory = "misc" name = "Miscellaneous" showcontent = true [tool.black] line-length = 99 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/readthedocs.yml0000644000175100001710000000015500000000000015712 0ustar00vstsdocker00000000000000build: image: latest python: version: 3.6 pip_install: true extra_requirements: - docs formats: [] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9115124 tox-3.21.4/setup.cfg0000644000175100001710000000443700000000000014532 0ustar00vstsdocker00000000000000[metadata] name = tox description = tox is a generic virtualenv management and test command line tool long_description = file: README.md long_description_content_type = text/markdown url = http://tox.readthedocs.org author = Holger Krekel, Oliver Bestwalter, Bernát Gábor and others maintainer = Bernat Gabor, Oliver Bestwalter, Anthony Asottile maintainer_email = tox-dev@python.org license = MIT license_file = LICENSE platforms = any classifiers = Development Status :: 5 - Production/Stable Framework :: tox Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: MacOS :: MacOS X Operating System :: Microsoft :: Windows Operating System :: POSIX Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Libraries Topic :: Software Development :: Testing Topic :: Utilities keywords = virtual, environments, isolated, testing project_urls = Source=https://github.com/tox-dev/tox Tracker=https://github.com/tox-dev/tox/issues [options] packages = find: install_requires = filelock>=3.0.0 packaging>=14 pluggy>=0.12.0 py>=1.4.17 six>=1.14.0 # required when virtualenv>=20 toml>=0.9.4 virtualenv!=20.0.0,!=20.0.1,!=20.0.2,!=20.0.3,!=20.0.4,!=20.0.5,!=20.0.6,!=20.0.7,>=16.0.0 colorama>=0.4.1 ;platform_system=="Windows" importlib-metadata>=0.12;python_version<"3.8" python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.* [options.entry_points] console_scripts = tox=tox:cmdline tox-quickstart=tox._quickstart:main [options.extras_require] docs = pygments-github-lexers>=0.0.5 sphinx>=2.0.0 sphinxcontrib-autoprogram>=0.1.5 towncrier>=18.5.0 testing = flaky>=3.4.0 freezegun>=0.3.11 psutil>=5.6.1 pytest>=4.0.0 pytest-cov>=2.5.1 pytest-mock>=1.10.0 pytest-randomly>=1.0.0 pytest-xdist>=1.22.2 pathlib2>=2.3.3;python_version<"3.4" [options.packages.find] where = src [bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/setup.py0000644000175100001710000000062200000000000014413 0ustar00vstsdocker00000000000000# -*- coding: utf-8 -*- import textwrap from setuptools import setup setup( use_scm_version={ "write_to": "src/tox/version.py", "write_to_template": textwrap.dedent( """ # coding: utf-8 from __future__ import unicode_literals __version__ = {version!r} """, ).lstrip(), }, package_dir={"": "src"}, ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.8955107 tox-3.21.4/src/0000755000175100001710000000000000000000000013470 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9035115 tox-3.21.4/src/tox/0000755000175100001710000000000000000000000014302 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/__init__.py0000644000175100001710000000202000000000000016405 0ustar00vstsdocker00000000000000"""Everything made explicitly available via `__all__` can be considered as part of the tox API. We will emit deprecation warnings for one minor release before making changes to these objects. If objects are marked experimental they might change between minor versions. To override/modify tox behaviour via plugins see `tox.hookspec` and its use with pluggy. """ import pluggy from . import exception from .constants import INFO, PIP, PYTHON from .hookspecs import hookspec from .version import __version__ __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 # EXPERIMENTAL CONSTANTS API "PYTHON", "INFO", "PIP", # DEPRECATED - will be removed from API in tox 4 "hookspec", ) hookimpl = pluggy.HookimplMarker("tox") # NOTE: must come last due to circular import from .session import cmdline # isort:skip ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/__main__.py0000644000175100001710000000007100000000000016372 0ustar00vstsdocker00000000000000import tox if __name__ == "__main__": tox.cmdline() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/_pytestplugin.py0000644000175100001710000004523000000000000017566 0ustar00vstsdocker00000000000000from __future__ import print_function, unicode_literals import os import subprocess import sys import textwrap import time import traceback from collections import OrderedDict from fnmatch import fnmatch import py import pytest import six import tox import tox.session from tox import venv from tox.config import parseconfig from tox.config.parallel import ENV_VAR_KEY_PRIVATE as PARALLEL_ENV_VAR_KEY_PRIVATE from tox.config.parallel import ENV_VAR_KEY_PUBLIC as PARALLEL_ENV_VAR_KEY_PUBLIC from tox.reporter import update_default_reporter from tox.venv import CreationConfig, VirtualEnv, getdigest 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(): if "TOXENV" in os.environ: del os.environ["TOXENV"] if "HUDSON_URL" in os.environ: del os.environ["HUDSON_URL"] def pytest_addoption(parser): parser.addoption( "--no-network", action="store_true", dest="no_network", help="don't run tests requiring network", ) def pytest_report_header(): return "tox comes from: {!r}".format(tox.__file__) @pytest.fixture def work_in_clean_dir(tmpdir): with tmpdir.as_cwd(): yield @pytest.fixture(autouse=True) def check_cwd_not_changed_by_test(): old = os.getcwd() yield new = os.getcwd() if old != new: pytest.fail("test changed cwd: {!r} => {!r}".format(old, new)) @pytest.fixture(autouse=True) def check_os_environ_stable(): old = os.environ.copy() to_clean = { k: os.environ.pop(k, None) for k in { PARALLEL_ENV_VAR_KEY_PRIVATE, PARALLEL_ENV_VAR_KEY_PUBLIC, str("TOX_WORK_DIR"), str("PYTHONPATH"), } } yield for key, value in to_clean.items(): if value is not None: os.environ[key] = value new = os.environ extra = {k: new[k] for k in set(new) - set(old)} miss = {k: old[k] for k in set(old) - set(new)} diff = { "{} = {} vs {}".format(k, old[k], new[k]) for k in set(old) & set(new) if old[k] != new[k] and not (k.startswith("PYTEST_") or k.startswith("COV_")) } if extra or miss or diff: msg = "test changed environ" if extra: msg += " extra {}".format(extra) if miss: msg += " miss {}".format(miss) if diff: msg += " diff {}".format(diff) pytest.fail(msg) @pytest.fixture(name="newconfig") def create_new_config_file(tmpdir): def create_new_config_file_(args, source=None, plugins=(), filename="tox.ini"): if source is None: source = args args = [] s = textwrap.dedent(source) p = tmpdir.join(filename) p.write(s) tox.session.setup_reporter(args) with tmpdir.as_cwd(): return parseconfig(args, plugins=plugins) return create_new_config_file_ @pytest.fixture def cmd(request, monkeypatch, capfd): if request.config.option.no_network: pytest.skip("--no-network was specified, test cannot run") request.addfinalizer(py.path.local().chdir) def run(*argv): reset_report() with RunResult(argv, capfd) as result: _collect_session(result) # noinspection PyBroadException try: tox.session.main([str(x) for x in argv]) assert False # this should always exist with SystemExit except SystemExit as exception: result.ret = exception.code except OSError as e: traceback.print_exc() result.ret = e.errno except Exception: traceback.print_exc() result.ret = 1 return result def _collect_session(result): prev_build = tox.session.build_session def build_session(config): result.session = prev_build(config) return result.session monkeypatch.setattr(tox.session, "build_session", build_session) yield run class RunResult: def __init__(self, args, capfd): self.args = args self.ret = None self.duration = None self.out = None self.err = None self.session = None self.capfd = capfd def __enter__(self): self._start = time.time() return self def __exit__(self, exc_type, exc_val, exc_tb): self.duration = time.time() - self._start self.out, self.err = self.capfd.readouterr() def _read(self, out, pos): out.buffer.seek(pos) return out.buffer.read().decode(out.encoding, errors=out.errors) @property def outlines(self): out = [] if self.out is None else self.out.splitlines() err = [] if self.err is None else self.err.splitlines() return err + out def __repr__(self): res = "RunResult(ret={}, args={!r}, out=\n{}\n, err=\n{})".format( self.ret, self.args, self.out, self.err, ) if six.PY2: return res.encode("UTF-8") else: return res def output(self): return "{}\n{}\n{}".format(self.ret, self.err, self.out) def assert_success(self, is_run_test_env=True): msg = self.output() assert self.ret == 0, msg if is_run_test_env: assert any(" congratulations :)" == line for line in reversed(self.outlines)), msg def assert_fail(self, is_run_test_env=True): msg = self.output() assert self.ret, msg if is_run_test_env: assert not any(" congratulations :)" == line for line in reversed(self.outlines)), msg class ReportExpectMock: def __init__(self): from tox import reporter self.instance = reporter._INSTANCE self.clear() self._index = -1 def clear(self): self._index = -1 if not six.PY2: self.instance.reported_lines.clear() else: del self.instance.reported_lines[:] def getnext(self, cat): __tracebackhide__ = True newindex = self._index + 1 while newindex < len(self.instance.reported_lines): call = self.instance.reported_lines[newindex] lcat = call[0] if fnmatch(lcat, cat): self._index = newindex return call newindex += 1 raise LookupError( "looking for {!r}, no reports found at >={:d} in {!r}".format( cat, self._index + 1, self.instance.reported_lines, ), ) def expect(self, cat, messagepattern="*", invert=False): __tracebackhide__ = True if not messagepattern.startswith("*"): messagepattern = "*{}".format(messagepattern) while self._index < len(self.instance.reported_lines): try: call = self.getnext(cat) except LookupError: break for lmsg in call[1:]: lmsg = str(lmsg).replace("\n", " ") if fnmatch(lmsg, messagepattern): if invert: raise AssertionError( "found {}({!r}), didn't expect it".format(cat, messagepattern), ) return if not invert: raise AssertionError( "looking for {}({!r}), no reports found at >={:d} in {!r}".format( cat, messagepattern, self._index + 1, self.instance.reported_lines, ), ) def not_expect(self, cat, messagepattern="*"): return self.expect(cat, messagepattern, invert=True) class pcallMock: def __init__(self, args, cwd, env, stdout, stderr, shell): self.arg0 = args[0] self.args = args self.cwd = cwd self.env = env self.stdout = stdout self.stderr = stderr self.shell = shell self.pid = os.getpid() self.returncode = 0 @staticmethod def communicate(): return "", "" def wait(self): pass @pytest.fixture(name="mocksession") def create_mocksession(request): config = request.getfixturevalue("newconfig")([], "") class MockSession(tox.session.Session): def __init__(self, config): self.logging_levels(config.option.quiet_level, config.option.verbose_level) super(MockSession, self).__init__(config, popen=self.popen) self._pcalls = [] self.report = ReportExpectMock() def _clearmocks(self): if not six.PY2: self._pcalls.clear() else: del self._pcalls[:] self.report.clear() def popen(self, args, cwd, shell=None, stdout=None, stderr=None, env=None, **_): process_call_mock = pcallMock(args, cwd, env, stdout, stderr, shell) self._pcalls.append(process_call_mock) return process_call_mock def new_config(self, config): self.logging_levels(config.option.quiet_level, config.option.verbose_level) self.config = config self.venv_dict.clear() self.existing_venvs.clear() def logging_levels(self, quiet, verbose): update_default_reporter(quiet, verbose) if hasattr(self, "config"): self.config.option.quiet_level = quiet self.config.option.verbose_level = verbose return MockSession(config) @pytest.fixture def newmocksession(mocksession, newconfig): def newmocksession_(args, source, plugins=()): config = newconfig(args, source, plugins=plugins) mocksession._reset(config, mocksession.popen) return mocksession return newmocksession_ def getdecoded(out): try: return out.decode("utf-8") except UnicodeDecodeError: return "INTERNAL not-utf8-decodeable, truncated string:\n{}".format(py.io.saferepr(out)) @pytest.fixture def initproj(tmpdir): """Create a factory function for creating example projects. Constructed folder/file hierarchy examples: with `src_root` other than `.`: tmpdir/ name/ # base src_root/ # src_root name/ # package_dir __init__.py name.egg-info/ # created later on package build setup.py with `src_root` given as `.`: tmpdir/ name/ # base, src_root name/ # package_dir __init__.py name.egg-info/ # created later on package build setup.py """ def initproj_(nameversion, filedefs=None, src_root=".", add_missing_setup_py=True): if filedefs is None: filedefs = {} if not src_root: src_root = "." if isinstance(nameversion, six.string_types): parts = nameversion.rsplit(str("-"), 1) if len(parts) == 1: parts.append("0.1") 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") and add_missing_setup_py: create_files( base, { "setup.py": """ from setuptools import setup, find_packages setup( name='{name}', description='{name} project', version='{version}', license='MIT', platforms=['unix', 'win32'], packages=find_packages('{src_root}'), package_dir={{'':'{src_root}'}}, ) """.format( **locals() ), }, ) if not _filedefs_contains(base, filedefs, src_root_path.join(name)): create_files( src_root_path, { name: { "__init__.py": textwrap.dedent( ''' """ module {} """ __version__ = {!r}''', ) .strip() .format(name, version), }, }, ) manifestlines = [ "include {}".format(p.relto(base)) for p in base.visit(lambda x: x.check(file=1)) ] create_files(base, {"MANIFEST.in": "\n".join(manifestlines)}) base.chdir() return base with py.path.local().as_cwd(): yield initproj_ def _path_parts(path): path = path and str(path) # py.path.local support parts = [] while path: folder, name = os.path.split(path) if folder == path: # root folder folder, name = name, folder if name: parts.append(name) path = folder parts.reverse() return parts def _path_join(base, *args): # workaround for a py.path.local bug on Windows (`path.join('/x', abs=1)` # should be py.path.local('X:\\x') where `X` is the current drive, when in # fact it comes out as py.path.local('\\x')) return py.path.local(base.join(*args, abs=1)) def _filedefs_contains(base, filedefs, path): """ whether `filedefs` defines a file/folder with the given `path` `path`, if relative, will be interpreted relative to the `base` folder, and whether relative or not, must refer to either the `base` folder or one of its direct or indirect children. The base folder itself is considered created if the filedefs structure is not empty. """ unknown = object() base = py.path.local(base) path = _path_join(base, path) path_rel_parts = _path_parts(path.relto(base)) for part in path_rel_parts: if not isinstance(filedefs, dict): return False filedefs = filedefs.get(part, unknown) if filedefs is unknown: return False return path_rel_parts or path == base and filedefs def create_files(base, filedefs): for key, value in filedefs.items(): if isinstance(value, dict): create_files(base.ensure(key, dir=1), value) elif isinstance(value, six.string_types): s = textwrap.dedent(value) base.join(key).write(s) @pytest.fixture() def mock_venv(monkeypatch): """This creates a mock virtual environment (e.g. will inherit the current interpreter). Note: because we inherit, to keep things sane you must call the py environment and only that; and cannot install any packages.""" # first ensure we have a clean python path monkeypatch.delenv(str("PYTHONPATH"), raising=False) # object to collect some data during the execution class Result(object): def __init__(self, session): self.popens = popen_list self.session = session res = OrderedDict() # convince tox that the current running virtual environment is already the env we would create class ProxyCurrentPython: @classmethod def readconfig(cls, path): if path.dirname.endswith("{}py".format(os.sep)): return CreationConfig( base_resolved_python_sha256=getdigest(sys.executable), base_resolved_python_path=sys.executable, tox_version=tox.__version__, sitepackages=False, usedevelop=False, deps=[], alwayscopy=False, ) elif path.dirname.endswith("{}.package".format(os.sep)): return CreationConfig( base_resolved_python_sha256=getdigest(sys.executable), base_resolved_python_path=sys.executable, tox_version=tox.__version__, sitepackages=False, usedevelop=False, deps=[(getdigest(""), "setuptools >= 35.0.2"), (getdigest(""), "wheel")], alwayscopy=False, ) assert False # pragma: no cover monkeypatch.setattr(CreationConfig, "readconfig", ProxyCurrentPython.readconfig) # provide as Python the current python executable def venv_lookup(venv, name): assert name == "python" venv.envconfig.envdir = py.path.local(sys.executable).join("..", "..") return sys.executable monkeypatch.setattr(VirtualEnv, "_venv_lookup", venv_lookup) # don't allow overriding the tox config data for the host Python def finish_venv(self): return monkeypatch.setattr(VirtualEnv, "finish", finish_venv) # we lie that it's an environment with no packages in it @tox.hookimpl def tox_runenvreport(venv, action): return [] monkeypatch.setattr(venv, "tox_runenvreport", tox_runenvreport) # intercept the build session to save it and we intercept the popen invocations # collect all popen calls popen_list = [] def popen(cmd, **kwargs): # we don't want to perform installation of new packages, # just replace with an always ok cmd if "pip" in cmd and "install" in cmd: cmd = ["python", "-c", "print({!r})".format(cmd)] ret = None try: ret = subprocess.Popen(cmd, **kwargs) except tox.exception.InvocationError as exception: # pragma: no cover ret = exception # pragma: no cover finally: popen_list.append((kwargs.get("env"), ret, cmd)) return ret def build_session(config): session = tox.session.Session(config, popen=popen) res[id(session)] = Result(session) return session monkeypatch.setattr(tox.session, "build_session", build_session) return res @pytest.fixture(scope="session") def current_tox_py(): """generate the current (test runners) python versions key e.g. py37 when running under Python 3.7""" return "{}{}{}".format("pypy" if tox.INFO.IS_PYPY else "py", *sys.version_info) def pytest_runtest_setup(item): reset_report() def pytest_runtest_teardown(item): reset_report() def pytest_pyfunc_call(pyfuncitem): reset_report() def reset_report(quiet=0, verbose=0): from tox.reporter import _INSTANCE _INSTANCE._reset(quiet_level=quiet, verbose_level=verbose) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/_quickstart.py0000644000175100001710000002225300000000000017211 0ustar00vstsdocker00000000000000# -*- coding: utf-8 -*- """ tox._quickstart ~~~~~~~~~~~~~~~~~ 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`_. .. Sphinx project_: http://sphinx.pocoo.org/ License for Sphinx ================== Copyright (c) 2007-2011 by the Sphinx team (see AUTHORS file). All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ import argparse import codecs import os import sys import textwrap import six 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 # test suite on all supported python versions. To use it, "pip install tox" # and then run "tox" from this directory. [tox] envlist = {envlist} [testenv] deps = {deps} commands = {commands} """ class ValidationError(Exception): """Raised for validation errors.""" def nonempty(x): if not x: raise ValidationError("Please enter some text.") return x def choice(*line): def val(x): if x not in line: raise ValidationError("Please enter one of {}.".format(", ".join(line))) return x return val def boolean(x): if x.upper() not in ("Y", "YES", "N", "NO"): raise ValidationError("Please enter either 'y' or 'n'.") return x.upper() in ("Y", "YES") def 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: prompt = "> {} [{}]: ".format(text, default) if default else "> {}: ".format(text) answer = six.moves.input(prompt) if default and not answer: answer = default # FIXME use six instead of self baked solution # noinspection PyUnresolvedReferences 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 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 but terminal encoding unknown" " -> assuming UTF-8 or Latin-1.", ) try: answer = answer.decode("utf-8") except UnicodeDecodeError: answer = answer.decode("latin1") if validator: try: answer = validator(answer) except ValidationError as exception: print("* {}".format(exception)) continue break 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 {} quickstart utility.".format(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( textwrap.dedent( """What Python versions do you want to test against? [1] {} [2] py27, {} [3] (All versions) {} [4] Choose each one-by-one""", ).format( 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 {} (Y/n)".format(pyenv), "Y", validator=boolean, ) print( textwrap.dedent( """What command should be used to test your project? Examples:\ - pytest\n" - python -m unittest discover - python setup.py test - trial package.module""", ), ) 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: {}".format(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", "{} exists - choose an alternative".format(targetpath), altpath) with codecs.open(targetpath, "w", encoding="utf-8") as f: f.write(prepare_content(QUICKSTART_CONF.format(**map_))) print( "Finished: {} has been created. For information on this file, " "see https://tox.readthedocs.io/en/latest/config.html\n" "Execute `tox` to test your project.".format(targetpath), ) def prepare_content(content): return "\n".join([line.rstrip() for line in content.split("\n")]) def parse_args(): parser = argparse.ArgumentParser( 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 config to. Defaults to current directory.", ) parser.add_argument( "--version", action="version", version="%(prog)s {}".format(tox.__version__), ) return parser.parse_args() def main(): args = parse_args() map_ = {"path": args.root} try: ask_user(map_) except (KeyboardInterrupt, EOFError): print("\n[Interrupted.]") return 1 post_process_input(map_) generate(map_) if __name__ == "__main__": sys.exit(main()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/action.py0000644000175100001710000002662700000000000016146 0ustar00vstsdocker00000000000000from __future__ import absolute_import, unicode_literals import os import pipes import signal import subprocess import sys import time from contextlib import contextmanager from threading import Thread import py from tox import reporter from tox.constants import INFO from tox.exception import InvocationError from tox.reporter import Verbosity from tox.util.lock import get_unique_file from tox.util.stdlib import is_main_thread class Action(object): """Action is an effort to group operations with the same goal (within reporting)""" def __init__( self, name, msg, args, log_dir, generate_tox_log, command_log, popen, python, suicide_timeout, interrupt_timeout, terminate_timeout, ): self.name = name self.args = args self.msg = msg self.activity = self.msg.split(" ", 1)[0] self.log_dir = log_dir self.generate_tox_log = generate_tox_log self.via_popen = popen self.command_log = command_log self._timed_report = None self.python = python self.suicide_timeout = suicide_timeout self.interrupt_timeout = interrupt_timeout self.terminate_timeout = terminate_timeout if is_main_thread(): # python allows only main thread to install signal handlers # see https://docs.python.org/3/library/signal.html#signals-and-threads self._install_sigterm_handler() def __enter__(self): msg = "{} {}".format(self.msg, " ".join(map(str, self.args))) self._timed_report = reporter.timed_operation(self.name, msg) self._timed_report.__enter__() return self def __exit__(self, type, value, traceback): self._timed_report.__exit__(type, value, traceback) def setactivity(self, name, msg): self.activity = name if msg: reporter.verbosity0("{} {}: {}".format(self.name, name, msg), bold=True) else: reporter.verbosity1("{} {}: {}".format(self.name, name, msg), bold=True) def info(self, name, msg): reporter.verbosity1("{} {}: {}".format(self.name, name, msg), bold=True) def popen( self, args, cwd=None, env=None, redirect=True, returnout=False, ignore_ret=False, capture_err=True, callback=None, report_fail=True, ): """this drives an interaction with a subprocess""" cwd = py.path.local() if cwd is None else cwd cmd_args = [str(x) for x in self._rewrite_args(cwd, args)] cmd_args_shell = " ".join(pipes.quote(i) for i in cmd_args) stream_getter = self._get_standard_streams( capture_err, cmd_args_shell, redirect, returnout, cwd, ) exit_code, output = None, None with stream_getter as (fin, out_path, stderr, stdout): try: process = self.via_popen( cmd_args, stdout=stdout, stderr=stderr, cwd=str(cwd), env=os.environ.copy() if env is None else env, universal_newlines=True, shell=False, creationflags=( subprocess.CREATE_NEW_PROCESS_GROUP if sys.platform == "win32" else 0 # needed for Windows signal send ability (CTRL+C) ), ) except OSError as exception: exit_code = exception.errno else: if callback is not None: callback(process) reporter.log_popen(cwd, out_path, cmd_args_shell, process.pid) output = self.evaluate_cmd(fin, process, redirect) exit_code = process.returncode finally: if out_path is not None and out_path.exists(): lines = out_path.read_text("UTF-8").split("\n") # first three lines are the action, cwd, and cmd - remove it output = "\n".join(lines[3:]) try: if exit_code and not ignore_ret: if report_fail: msg = "invocation failed (exit code {:d})".format(exit_code) if out_path is not None: msg += ", logfile: {}".format(out_path) if not out_path.exists(): msg += " warning log file missing" reporter.error(msg) if out_path is not None and out_path.exists(): reporter.separator("=", "log start", Verbosity.QUIET) reporter.quiet(output) reporter.separator("=", "log end", Verbosity.QUIET) raise InvocationError(cmd_args_shell, exit_code, output) finally: self.command_log.add_command(cmd_args, output, exit_code) return output def evaluate_cmd(self, input_file_handler, process, redirect): try: if self.generate_tox_log and not redirect: if process.stderr is not None: # prevent deadlock raise ValueError("stderr must not be piped here") # we read binary from the process and must write using a binary stream buf = getattr(sys.stdout, "buffer", sys.stdout) last_time = time.time() while True: # we have to read one byte at a time, otherwise there # might be no output for a long time with slow tests data = input_file_handler.read(1) if data: buf.write(data) if b"\n" in data or (time.time() - last_time) > 1: # we flush on newlines or after 1 second to # provide quick enough feedback to the user # when printing a dot per test buf.flush() last_time = time.time() elif process.poll() is not None: if process.stdout is not None: process.stdout.close() break else: time.sleep(0.1) # the seek updates internal read buffers input_file_handler.seek(0, 1) input_file_handler.close() out, _ = process.communicate() # wait to finish except KeyboardInterrupt as exception: reporter.error("got KeyboardInterrupt signal") main_thread = is_main_thread() while True: try: if main_thread: # spin up a new thread to disable further interrupt on main thread stopper = Thread(target=self.handle_interrupt, args=(process,)) stopper.start() stopper.join() else: self.handle_interrupt(process) except KeyboardInterrupt: continue break raise exception return out def handle_interrupt(self, process): """A three level stop mechanism for children - INT -> TERM -> KILL""" msg = "from {} {{}} pid {}".format(os.getpid(), process.pid) if self._wait(process, self.suicide_timeout) is None: self.info("KeyboardInterrupt", msg.format("SIGINT")) process.send_signal(signal.CTRL_C_EVENT if sys.platform == "win32" else signal.SIGINT) if self._wait(process, self.interrupt_timeout) is None: self.info("KeyboardInterrupt", msg.format("SIGTERM")) process.terminate() if self._wait(process, self.terminate_timeout) is None: self.info("KeyboardInterrupt", msg.format("SIGKILL")) process.kill() process.communicate() @staticmethod def _wait(process, timeout): if sys.version_info >= (3, 3): # python 3 has timeout feature built-in try: process.communicate(timeout=timeout) except subprocess.TimeoutExpired: pass else: # on Python 2 we need to simulate it delay = 0.01 while process.poll() is None and timeout > 0: time.sleep(delay) timeout -= delay return process.poll() @contextmanager def _get_standard_streams(self, capture_err, cmd_args_shell, redirect, returnout, cwd): stdout = out_path = input_file_handler = None stderr = subprocess.STDOUT if capture_err else None if self.generate_tox_log or redirect: out_path = self.get_log_path(self.name) with out_path.open("wt") as stdout, out_path.open("rb") as input_file_handler: msg = "action: {}, msg: {}\ncwd: {}\ncmd: {}\n".format( self.name.replace("\n", " "), self.msg.replace("\n", " "), str(cwd).replace("\n", " "), cmd_args_shell.replace("\n", " "), ) stdout.write(msg) stdout.flush() input_file_handler.read() # read the header, so it won't be written to stdout yield input_file_handler, out_path, stderr, stdout return if returnout: stdout = subprocess.PIPE yield input_file_handler, out_path, stderr, stdout def get_log_path(self, actionid): log_file = get_unique_file(self.log_dir, prefix=actionid, suffix=".log") return log_file def _rewrite_args(self, cwd, args): executable = None if INFO.IS_WIN: # shebang lines are not adhered on Windows so if it's a python script # pre-pend the interpreter ext = os.path.splitext(str(args[0]))[1].lower() if ext == ".py": executable = str(self.python) if executable is None: executable = args[0] args = args[1:] new_args = [executable] # to make the command shorter try to use relative paths for all subsequent arguments # note the executable cannot be relative as the Windows applies cwd after invocation for arg in args: if arg and os.path.isabs(str(arg)): arg_path = py.path.local(arg) if arg_path.exists() and arg_path.common(cwd) is not None: potential_arg = cwd.bestrelpath(arg_path) if len(potential_arg.split("..")) < 2: # just one parent directory accepted as relative path arg = potential_arg new_args.append(str(arg)) return new_args def _install_sigterm_handler(self): """Handle sigterm as if it were a keyboardinterrupt""" def sigterm_handler(signum, frame): reporter.error("Got SIGTERM, handling it as a KeyboardInterrupt") raise KeyboardInterrupt() signal.signal(signal.SIGTERM, sigterm_handler) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/cli.py0000644000175100001710000000033300000000000015422 0ustar00vstsdocker00000000000000from tox.config import Parser, get_plugin_manager def cli_parser(): parser = Parser() pm = get_plugin_manager(tuple()) pm.hook.tox_addoption(parser=parser) return parser.argparser cli = cli_parser() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9035115 tox-3.21.4/src/tox/config/0000755000175100001710000000000000000000000015547 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/config/__init__.py0000644000175100001710000021751400000000000017672 0ustar00vstsdocker00000000000000from __future__ import print_function import argparse import itertools import os import random import re import shlex import string import sys import traceback import warnings from collections import OrderedDict from fnmatch import fnmatchcase from subprocess import list2cmdline from threading import Thread import pluggy import py import six import toml from packaging import requirements from packaging.utils import canonicalize_name from packaging.version import Version import tox from tox.constants import INFO from tox.exception import MissingDependency from tox.interpreters import Interpreters, NoInterpreterInfo from tox.reporter import ( REPORTER_TIMESTAMP_ON_ENV, error, update_default_reporter, using, verbosity1, ) from tox.util.path import ensure_empty_dir from tox.util.stdlib import importlib_metadata from .parallel import ENV_VAR_KEY_PRIVATE as PARALLEL_ENV_VAR_KEY_PRIVATE from .parallel import ENV_VAR_KEY_PUBLIC as PARALLEL_ENV_VAR_KEY_PUBLIC from .parallel import add_parallel_config, add_parallel_flags from .reporter import add_verbosity_commands try: from shlex import quote as shlex_quote except ImportError: from pipes import quote as shlex_quote hookimpl = tox.hookimpl # DEPRECATED - REMOVE - left for compatibility with plugins importing from here. # Import hookimpl directly from tox instead. WITHIN_PROVISION = os.environ.get(str("TOX_PROVISION")) == "1" SUICIDE_TIMEOUT = 0.0 INTERRUPT_TIMEOUT = 0.3 TERMINATE_TIMEOUT = 0.2 _FACTOR_LINE_PATTERN = re.compile(r"^([\w{}\.!,-]+)\:\s+(.+)") _ENVSTR_SPLIT_PATTERN = re.compile(r"((?:\{[^}]+\})+)|,") _ENVSTR_EXPAND_PATTERN = re.compile(r"\{([^}]+)\}") _WHITESPACE_PATTERN = re.compile(r"\s+") def get_plugin_manager(plugins=()): # initialize plugin manager import tox.venv pm = pluggy.PluginManager("tox") pm.add_hookspecs(tox.hookspecs) pm.register(tox.config) pm.register(tox.interpreters) pm.register(tox.venv) pm.register(tox.session) from tox import package pm.register(package) pm.load_setuptools_entrypoints("tox") for plugin in plugins: pm.register(plugin) pm.check_pending() return pm class Parser: """Command line and ini-parser control object.""" def __init__(self): class HelpFormatter(argparse.ArgumentDefaultsHelpFormatter): def __init__(self, prog): super(HelpFormatter, self).__init__(prog, max_help_position=35, width=190) self.argparser = argparse.ArgumentParser( description="tox options", add_help=False, prog="tox", formatter_class=HelpFormatter, ) self._testenv_attr = [] def add_argument(self, *args, **kwargs): """add argument to command line parser. This takes the same arguments that ``argparse.ArgumentParser.add_argument``. """ return self.argparser.add_argument(*args, **kwargs) def add_testenv_attribute(self, name, type, help, default=None, postprocess=None): """add an ini-file variable for "testenv" section. Types are specified as strings like "bool", "line-list", "string", "argv", "path", "argvlist". The ``postprocess`` function will be called for each testenv like ``postprocess(testenv_config=testenv_config, value=value)`` where ``value`` is the value as read from the ini (or the default value) and ``testenv_config`` is a :py:class:`tox.config.TestenvConfig` instance which will receive all ini-variables as object attributes. Any postprocess function must return a value which will then be set as the final value in the testenv section. """ self._testenv_attr.append(VenvAttribute(name, type, default, help, postprocess)) def add_testenv_attribute_obj(self, obj): """add an ini-file variable as an object. This works as the ``add_testenv_attribute`` function but expects "name", "type", "help", and "postprocess" attributes on the object. """ assert hasattr(obj, "name") assert hasattr(obj, "type") assert hasattr(obj, "help") assert hasattr(obj, "postprocess") self._testenv_attr.append(obj) def parse_cli(self, args, strict=False): args, argv = self.argparser.parse_known_args(args) if argv and (strict or WITHIN_PROVISION): self.argparser.error("unrecognized arguments: {}".format(" ".join(argv))) return args def _format_help(self): return self.argparser.format_help() class VenvAttribute: def __init__(self, name, type, default, help, postprocess): self.name = name self.type = type self.default = default self.help = help self.postprocess = postprocess class DepOption: name = "deps" type = "line-list" help = "each line specifies a dependency in pip/setuptools format." default = () def postprocess(self, testenv_config, value): deps = [] config = testenv_config.config for depline in value: m = re.match(r":(\w+):\s*(\S+)", depline) if m: iname, name = m.groups() ixserver = config.indexserver[iname] else: name = depline.strip() ixserver = None # 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 tox.PIP.INSTALL_SHORT_OPTIONS_ARGUMENT: if name.startswith(option): name = "{}{}".format(option, name[len(option) :].strip()) # in case of a long option, we add an equal sign for option in tox.PIP.INSTALL_LONG_OPTIONS_ARGUMENT: name_start = "{} ".format(option) if name.startswith(name_start): name = "{}={}".format(option, name[len(option) :].strip()) name = self._cut_off_dep_comment(name) name = self._replace_forced_dep(name, config) deps.append(DepConfig(name, ixserver)) return deps def _replace_forced_dep(self, name, config): """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 :return: the new dependency that should be used for virtual environments """ if not config.option.force_dep: return name for forced_dep in config.option.force_dep: if self._is_same_dep(forced_dep, name): return forced_dep return name @staticmethod def _cut_off_dep_comment(name): return re.sub(r"\s+#.*", "", name).strip() @classmethod def _is_same_dep(cls, dep1, dep2): """Definitions are the same if they refer to the same package, even if versions differ.""" dep1_name = canonicalize_name(requirements.Requirement(dep1).name) try: dep2_name = canonicalize_name(requirements.Requirement(dep2).name) except requirements.InvalidRequirement: # we couldn't parse a version, probably a URL return False return dep1_name == dep2_name class PosargsOption: name = "args_are_paths" type = "bool" default = True help = "treat positional args in commands as paths" def postprocess(self, testenv_config, value): config = testenv_config.config args = config.option.args if args: if value: args = [] for arg in config.option.args: if arg and not os.path.isabs(arg): origpath = os.path.join(config.invocationcwd.strpath, arg) if os.path.exists(origpath): arg = os.path.relpath(origpath, testenv_config.changedir.strpath) args.append(arg) testenv_config._reader.addsubstitutions(args) return value class InstallcmdOption: name = "install_command" type = "argv_install_command" default = r"python -m pip install \{opts\} \{packages\}" help = "install command for dependencies and package under test." def postprocess(self, testenv_config, value): if "{packages}" not in value: raise tox.exception.ConfigError( "'install_command' must contain '{packages}' substitution", ) return value def parseconfig(args, plugins=()): """Parse the configuration file and create a Config object. :param plugins: :param list[str] args: list of arguments. :rtype: :class:`Config` :raise SystemExit: toxinit file is not found """ pm = get_plugin_manager(plugins) config, option = parse_cli(args, pm) update_default_reporter(config.option.quiet_level, config.option.verbose_level) for config_file in propose_configs(option.configfile): config_type = config_file.basename content = None if config_type == "pyproject.toml": toml_content = get_py_project_toml(config_file) try: content = toml_content["tool"]["tox"]["legacy_tox_ini"] except KeyError: continue try: ParseIni(config, config_file, content) except SkipThisIni: continue pm.hook.tox_configure(config=config) # post process config object break else: parser = Parser() pm.hook.tox_addoption(parser=parser) # if no tox config file, now we need do a strict argument evaluation # raise on unknown args parser.parse_cli(args, strict=True) if option.help or option.helpini: return config if option.devenv: # To load defaults, we parse an empty config ParseIni(config, py.path.local(), "") pm.hook.tox_configure(config=config) return config msg = "tox config file (either {}) not found" candidates = ", ".join(INFO.CONFIG_CANDIDATES) feedback(msg.format(candidates), sysexit=not (option.help or option.helpini)) return config def get_py_project_toml(path): with open(str(path)) as file_handler: config_data = toml.load(file_handler) return config_data def propose_configs(cli_config_file): from_folder = py.path.local() if cli_config_file is not None: if os.path.isfile(cli_config_file): yield py.path.local(cli_config_file) return if os.path.isdir(cli_config_file): from_folder = py.path.local(cli_config_file) else: print( "ERROR: {} is neither file or directory".format(cli_config_file), file=sys.stderr, ) return for basename in INFO.CONFIG_CANDIDATES: if from_folder.join(basename).isfile(): yield from_folder.join(basename) for path in from_folder.parts(reverse=True): ini_path = path.join(basename) if ini_path.check(): yield ini_path def parse_cli(args, pm): parser = Parser() pm.hook.tox_addoption(parser=parser) option = parser.parse_cli(args) if option.version: print(get_version_info(pm)) raise SystemExit(0) interpreters = Interpreters(hook=pm.hook) config = Config( pluginmanager=pm, option=option, interpreters=interpreters, parser=parser, args=args, ) return config, option def feedback(msg, sysexit=False): print("ERROR: {}".format(msg), file=sys.stderr) if sysexit: raise SystemExit(1) def get_version_info(pm): out = ["{} imported from {}".format(tox.__version__, tox.__file__)] plugin_dist_info = pm.list_plugin_distinfo() if plugin_dist_info: out.append("registered plugins:") for mod, egg_info in plugin_dist_info: source = getattr(mod, "__file__", repr(mod)) out.append(" {}-{} at {}".format(egg_info.project_name, egg_info.version, source)) return "\n".join(out) class SetenvDict(object): _DUMMY = object() def __init__(self, definitions, reader): self.definitions = definitions self.reader = reader self.resolved = {} self._lookupstack = [] def __repr__(self): return "{}: {}".format(self.__class__.__name__, self.definitions) def __contains__(self, name): return name in self.definitions def get(self, name, default=None): try: return self.resolved[name] except KeyError: try: if name in self._lookupstack: raise KeyError(name) val = self.definitions[name] except KeyError: return os.environ.get(name, default) self._lookupstack.append(name) try: self.resolved[name] = res = self.reader._replace(val, name="setenv") finally: self._lookupstack.pop() return res def __getitem__(self, name): x = self.get(name, self._DUMMY) if x is self._DUMMY: raise KeyError(name) return x def keys(self): return self.definitions.keys() def __setitem__(self, name, value): self.definitions[name] = value self.resolved[name] = value def items(self): return ((name, self[name]) for name in self.definitions) def export(self): # post-process items to avoid internal syntax/semantics # such as {} being escaped using \{\}, suitable for use with # os.environ . return { name: Replacer._unescape(value) for name, value in self.items() if value is not self._DUMMY } @tox.hookimpl def tox_addoption(parser): parser.add_argument( "--version", action="store_true", help="report version information to stdout.", ) parser.add_argument("-h", "--help", action="store_true", help="show help about options") parser.add_argument( "--help-ini", "--hi", action="store_true", dest="helpini", help="show help about ini-names", ) add_verbosity_commands(parser) parser.add_argument( "--showconfig", action="store_true", help="show live configuration (by default all env, with -l only default targets," " specific via TOXENV/-e)", ) parser.add_argument( "-l", "--listenvs", action="store_true", help="show list of test environments (with description if verbose)", ) parser.add_argument( "-a", "--listenvs-all", action="store_true", help="show list of all defined environments (with description if verbose)", ) parser.add_argument( "-c", dest="configfile", help="config file name or directory with 'tox.ini' file.", ) parser.add_argument( "-e", action="append", dest="env", metavar="envlist", help="work against specified environments (ALL selects all).", ) parser.add_argument( "--devenv", metavar="ENVDIR", help=( "sets up a development environment at ENVDIR based on the env's tox " "configuration specified by `-e` (-e defaults to py)." ), ) parser.add_argument("--notest", action="store_true", help="skip invoking test commands.") parser.add_argument( "--sdistonly", action="store_true", help="only perform the sdist packaging activity.", ) parser.add_argument( "--skip-pkg-install", action="store_true", help="skip package installation for this run", ) add_parallel_flags(parser) parser.add_argument( "--parallel--safe-build", action="store_true", dest="parallel_safe_build", help="(deprecated) ensure two tox builds can run in parallel " "(uses a lock file in the tox workdir with .lock extension)", ) parser.add_argument( "--installpkg", metavar="PATH", help="use specified package for installation into venv, instead of creating an sdist.", ) parser.add_argument( "--develop", action="store_true", help="install package in the venv using 'setup.py develop' via 'pip -e .'", ) parser.add_argument( "-i", "--index-url", action="append", dest="indexurl", metavar="URL", help="set indexserver url (if URL is of form name=url set the " "url for the 'name' indexserver, specifically)", ) parser.add_argument( "--pre", action="store_true", help="install pre-releases and development versions of dependencies. " "This will pass the --pre option to install_command " "(pip by default).", ) parser.add_argument( "-r", "--recreate", action="store_true", help="force recreation of virtual environments", ) parser.add_argument( "--result-json", dest="resultjson", metavar="PATH", help="write a json file with detailed information " "about all commands and results involved.", ) parser.add_argument( "--discover", dest="discover", nargs="+", metavar="PATH", help="for python discovery first try the python executables under these paths", default=[], ) # We choose 1 to 4294967295 because it is the range of PYTHONHASHSEED. parser.add_argument( "--hashseed", metavar="SEED", help="set PYTHONHASHSEED to SEED before running commands. " "Defaults to a random integer in the range [1, 4294967295] " "([1, 1024] on Windows). " "Passing 'noset' suppresses this behavior.", ) parser.add_argument( "--force-dep", action="append", metavar="REQ", help="Forces a certain version of one of the dependencies " "when configuring the virtual environment. REQ Examples " "'pytest<2.7' or 'django>=1.6'.", ) parser.add_argument( "--sitepackages", action="store_true", help="override sitepackages setting to True in all envs", ) parser.add_argument( "--alwayscopy", action="store_true", help="override alwayscopy setting to True in all envs", ) cli_skip_missing_interpreter(parser) parser.add_argument("--workdir", metavar="PATH", help="tox working directory") parser.add_argument( "args", nargs="*", help="additional arguments available to command positional substitution", ) def _set_envdir_from_devenv(testenv_config, value): if testenv_config.config.option.devenv is not None: return py.path.local(testenv_config.config.option.devenv) else: return value parser.add_testenv_attribute( name="envdir", type="path", default="{toxworkdir}/{envname}", help="set venv directory -- be very careful when changing this as tox " "will remove this directory when recreating an environment", postprocess=_set_envdir_from_devenv, ) # add various core venv interpreter attributes def setenv(testenv_config, value): setenv = value config = testenv_config.config if "PYTHONHASHSEED" not in setenv and config.hashseed is not None: setenv["PYTHONHASHSEED"] = config.hashseed setenv["TOX_ENV_NAME"] = str(testenv_config.envname) setenv["TOX_ENV_DIR"] = str(testenv_config.envdir) return setenv parser.add_testenv_attribute( name="setenv", type="dict_setenv", postprocess=setenv, help="list of X=Y lines with environment variable settings", ) def basepython_default(testenv_config, value): """either user set or proposed from the factor name in both cases we check that the factor name implied python version and the resolved python interpreter version match up; if they don't we warn, unless ignore base python conflict is set in which case the factor name implied version if forced """ for factor in testenv_config.factors: match = tox.PYTHON.PY_FACTORS_RE.match(factor) if match: base_exe = {"py": "python"}.get(match.group(1), match.group(1)) version_s = match.group(2) if not version_s: version_info = () elif len(version_s) == 1: version_info = (version_s,) else: version_info = (version_s[0], version_s[1:]) implied_version = ".".join(version_info) implied_python = "{}{}".format(base_exe, implied_version) break else: implied_python, version_info, implied_version = None, (), "" if testenv_config.config.ignore_basepython_conflict and implied_python is not None: return implied_python proposed_python = (implied_python or sys.executable) if value is None else str(value) if implied_python is not None and implied_python != proposed_python: testenv_config.basepython = proposed_python python_info_for_proposed = testenv_config.python_info if not isinstance(python_info_for_proposed, NoInterpreterInfo): proposed_version = ".".join( str(x) for x in python_info_for_proposed.version_info[: len(version_info)] ) if proposed_version != implied_version: # TODO(stephenfin): Raise an exception here in tox 4.0 warnings.warn( "conflicting basepython version (set {}, should be {}) for env '{}';" "resolve conflict or set ignore_basepython_conflict".format( proposed_version, implied_version, testenv_config.envname, ), ) return proposed_python parser.add_testenv_attribute( name="basepython", type="basepython", default=None, postprocess=basepython_default, help="executable name or path of interpreter used to create a virtual test environment.", ) def merge_description(testenv_config, value): """the reader by default joins generated description with new line, replace new line with space""" return value.replace("\n", " ") parser.add_testenv_attribute( name="description", type="string", default="", postprocess=merge_description, help="short description of this environment", ) parser.add_testenv_attribute( name="envtmpdir", type="path", default="{envdir}/tmp", help="venv temporary directory", ) parser.add_testenv_attribute( name="envlogdir", type="path", default="{envdir}/log", help="venv log directory", ) parser.add_testenv_attribute( name="downloadcache", type="string", default=None, help="(ignored) has no effect anymore, pip-8 uses local caching by default", ) parser.add_testenv_attribute( name="changedir", type="path", default="{toxinidir}", help="directory to change to when running commands", ) parser.add_testenv_attribute_obj(PosargsOption()) def skip_install_default(testenv_config, value): return value is True or testenv_config.config.option.skip_pkg_install is True parser.add_testenv_attribute( name="skip_install", type="bool", default=False, postprocess=skip_install_default, help="Do not install the current package. This can be used when you need the virtualenv " "management but do not want to install the current package", ) parser.add_testenv_attribute( name="ignore_errors", type="bool", default=False, help="if set to True all commands will be executed irrespective of their result error " "status.", ) def recreate(testenv_config, value): if testenv_config.config.option.recreate: return True return value parser.add_testenv_attribute( name="recreate", type="bool", default=False, postprocess=recreate, help="always recreate this test environment.", ) def passenv(testenv_config, value): # Flatten the list to deal with space-separated values. value = list(itertools.chain.from_iterable([x.split(" ") for x in value])) passenv = { "CURL_CA_BUNDLE", "LANG", "LANGUAGE", "LD_LIBRARY_PATH", "PATH", "PIP_INDEX_URL", "PIP_EXTRA_INDEX_URL", "REQUESTS_CA_BUNDLE", "SSL_CERT_FILE", "TOX_WORK_DIR", "HTTP_PROXY", "HTTPS_PROXY", "NO_PROXY", str(REPORTER_TIMESTAMP_ON_ENV), str(PARALLEL_ENV_VAR_KEY_PUBLIC), } # read in global passenv settings p = os.environ.get("TOX_TESTENV_PASSENV", None) if p is not None: env_values = [x for x in p.split() if x] value.extend(env_values) # we ensure that tmp directory settings are passed on # we could also set it to the per-venv "envtmpdir" # but this leads to very long paths when run with jenkins # so we just pass it on by default for now. if 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). passenv.add("NUMBER_OF_PROCESSORS") passenv.add("PROCESSOR_ARCHITECTURE") # platform.machine() passenv.add("USERPROFILE") # needed for `os.path.expanduser()` passenv.add("MSYSTEM") # fixes #429 else: passenv.add("TMPDIR") for spec in value: for name in os.environ: if fnmatchcase(name.upper(), spec.upper()): passenv.add(name) return passenv parser.add_testenv_attribute( name="passenv", type="line-list", postprocess=passenv, help="environment variables needed during executing test commands (taken from invocation " "environment). Note that tox always passes through some basic environment variables " "which are needed for basic functioning of the Python system. See --showconfig for the " "eventual passenv setting.", ) parser.add_testenv_attribute( name="whitelist_externals", type="line-list", help="DEPRECATED: use allowlist_externals", ) parser.add_testenv_attribute( name="allowlist_externals", type="line-list", help="each lines specifies a path or basename for which tox will not warn " "about it coming from outside the test environment.", ) parser.add_testenv_attribute( name="platform", type="string", default=".*", help="regular expression which must match against ``sys.platform``. " "otherwise testenv will be skipped.", ) def sitepackages(testenv_config, value): return testenv_config.config.option.sitepackages or value def alwayscopy(testenv_config, value): return testenv_config.config.option.alwayscopy or value parser.add_testenv_attribute( name="sitepackages", type="bool", default=False, postprocess=sitepackages, help="Set to ``True`` if you want to create virtual environments that also " "have access to globally installed packages.", ) parser.add_testenv_attribute( "download", type="bool", default=False, help="download the latest pip, setuptools and wheel when creating the virtual" "environment (default is to use the one bundled in virtualenv)", ) parser.add_testenv_attribute( name="alwayscopy", type="bool", default=False, postprocess=alwayscopy, help="Set to ``True`` if you want virtualenv to always copy files rather " "than symlinking.", ) def pip_pre(testenv_config, value): return testenv_config.config.option.pre or value parser.add_testenv_attribute( name="pip_pre", type="bool", default=False, postprocess=pip_pre, help="If ``True``, adds ``--pre`` to the ``opts`` passed to the install command. ", ) def develop(testenv_config, value): option = testenv_config.config.option return not option.installpkg and (value or option.develop or option.devenv is not None) parser.add_testenv_attribute( name="usedevelop", type="bool", postprocess=develop, default=False, help="install package in develop/editable mode", ) parser.add_testenv_attribute_obj(InstallcmdOption()) parser.add_testenv_attribute( name="list_dependencies_command", type="argv", default="python -m pip freeze", help="list dependencies for a virtual environment", ) parser.add_testenv_attribute_obj(DepOption()) parser.add_testenv_attribute( name="suicide_timeout", type="float", default=SUICIDE_TIMEOUT, help="timeout to allow process to exit before sending SIGINT", ) parser.add_testenv_attribute( name="interrupt_timeout", type="float", default=INTERRUPT_TIMEOUT, help="timeout before sending SIGTERM after SIGINT", ) parser.add_testenv_attribute( name="terminate_timeout", type="float", default=TERMINATE_TIMEOUT, help="timeout before sending SIGKILL after SIGTERM", ) parser.add_testenv_attribute( name="commands", type="argvlist", default="", help="each line specifies a test command and can use substitution.", ) parser.add_testenv_attribute( name="commands_pre", type="argvlist", default="", help="each line specifies a setup command action and can use substitution.", ) parser.add_testenv_attribute( name="commands_post", type="argvlist", default="", help="each line specifies a teardown command and can use substitution.", ) parser.add_testenv_attribute( "ignore_outcome", type="bool", default=False, help="if set to True a failing result of this testenv will not make " "tox fail, only a warning will be produced", ) parser.add_testenv_attribute( "extras", type="line-list", help="list of extras to install with the source distribution or develop install", ) add_parallel_config(parser) def cli_skip_missing_interpreter(parser): class SkipMissingInterpreterAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): value = "true" if values is None else values if value not in ("config", "true", "false"): raise argparse.ArgumentTypeError("value must be config, true or false") setattr(namespace, self.dest, value) parser.add_argument( "-s", "--skip-missing-interpreters", default="config", metavar="val", nargs="?", action=SkipMissingInterpreterAction, help="don't fail tests for missing interpreters: {config,true,false} choice", ) class Config(object): """Global Tox config object.""" def __init__(self, pluginmanager, option, interpreters, parser, args): self.envconfigs = OrderedDict() """Mapping envname -> envconfig""" self.invocationcwd = py.path.local() self.interpreters = interpreters self.pluginmanager = pluginmanager self.option = option self._parser = parser self._testenv_attr = parser._testenv_attr self.args = args """option namespace containing all parsed command line options""" @property def homedir(self): homedir = get_homedir() if homedir is None: homedir = self.toxinidir # FIXME XXX good idea? return homedir class TestenvConfig: """Testenv Configuration object. In addition to some core attributes/properties this config object holds all per-testenv ini attributes as attributes, see "tox --help-ini" for an overview. """ def __init__(self, envname, config, factors, reader): #: test environment name self.envname = envname #: global tox config object self.config = config #: set of factors self.factors = factors self._reader = reader self._missing_subs = {} """Holds substitutions that could not be resolved. Pre 2.8.1 missing substitutions crashed with a ConfigError although this would not be a problem if the env is not part of the current testrun. So we need to remember this and check later when the testenv is actually run and crash only then. """ # Python 3 only, as __getattribute__ is ignored for old-style types on Python 2 def __getattribute__(self, name): rv = object.__getattribute__(self, name) if isinstance(rv, Exception): raise rv return rv if six.PY2: def __getattr__(self, name): if name in self._missing_subs: raise self._missing_subs[name] raise AttributeError(name) def get_envbindir(self): """Path to directory where scripts/binaries reside.""" is_bin = ( isinstance(self.python_info, NoInterpreterInfo) or tox.INFO.IS_WIN is False or self.python_info.implementation == "Jython" or ( tox.INFO.IS_WIN and self.python_info.implementation == "PyPy" and self.python_info.extra_version_info < (7, 3, 1) ) ) return self.envdir.join("bin" if is_bin else "Scripts") @property def envbindir(self): return self.get_envbindir() @property def envpython(self): """Path to python executable.""" return self.get_envpython() def get_envpython(self): """ path to python/jython executable. """ if "jython" in str(self.basepython): name = "jython" else: name = "python" return self.envbindir.join(name) def get_envsitepackagesdir(self): """Return sitepackagesdir of the virtualenv environment. NOTE: Only available during execution, not during parsing. """ x = self.config.interpreters.get_sitepackagesdir(info=self.python_info, envdir=self.envdir) return x @property def python_info(self): """Return sitepackagesdir of the virtualenv environment.""" return self.config.interpreters.get_info(envconfig=self) def getsupportedinterpreter(self): if 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) if not info.executable: raise tox.exception.InterpreterNotFound(self.basepython) if not info.version_info: raise tox.exception.InvocationError( "Failed to get version_info for {}: {}".format(info.name, info.err), ) return info.executable testenvprefix = "testenv:" def get_homedir(): try: return py.path.local._gethomedir() except Exception: return None def make_hashseed(): max_seed = 4294967295 if tox.INFO.IS_WIN: max_seed = 1024 return str(random.randint(1, max_seed)) class SkipThisIni(Exception): """Internal exception to indicate the parsed ini file should be skipped""" class ParseIni(object): def __init__(self, config, ini_path, ini_data): # noqa config.toxinipath = ini_path using("tox.ini: {} (pid {})".format(config.toxinipath, os.getpid())) config.toxinidir = config.toxinipath.dirpath() if ini_path.check(file=True) else ini_path self._cfg = py.iniconfig.IniConfig(config.toxinipath, ini_data) if ini_path.basename == "setup.cfg" and "tox:tox" not in self._cfg: verbosity1("Found no [tox:tox] section in setup.cfg, skipping.") raise SkipThisIni() previous_line_of = self._cfg.lineof self.expand_section_names(self._cfg) def line_of_default_to_zero(section, name=None): at = previous_line_of(section, name=name) if at is None: at = 0 return at self._cfg.lineof = line_of_default_to_zero config._cfg = self._cfg self.config = config prefix = "tox" if ini_path.basename == "setup.cfg" else None fallbacksection = "tox:tox" if ini_path.basename == "setup.cfg" else "tox" context_name = getcontextname() if context_name == "jenkins": reader = SectionReader( "tox:jenkins", self._cfg, prefix=prefix, fallbacksections=[fallbacksection], ) dist_share_default = "{toxworkdir}/distshare" elif not context_name: reader = SectionReader("tox", self._cfg, prefix=prefix) dist_share_default = "{homedir}/.tox/distshare" else: raise ValueError("invalid context") if config.option.hashseed is None: hash_seed = make_hashseed() elif config.option.hashseed == "noset": hash_seed = None else: hash_seed = config.option.hashseed config.hashseed = hash_seed reader.addsubstitutions(toxinidir=config.toxinidir, homedir=config.homedir) if config.option.workdir is None: config.toxworkdir = reader.getpath("toxworkdir", "{toxinidir}/.tox") else: config.toxworkdir = config.toxinidir.join(config.option.workdir, abs=True) if os.path.exists(str(config.toxworkdir)): config.toxworkdir = config.toxworkdir.realpath() reader.addsubstitutions(toxworkdir=config.toxworkdir) config.ignore_basepython_conflict = reader.getbool("ignore_basepython_conflict", False) config.distdir = reader.getpath("distdir", "{toxworkdir}/dist") reader.addsubstitutions(distdir=config.distdir) config.distshare = reader.getpath("distshare", dist_share_default) reader.addsubstitutions(distshare=config.distshare) config.temp_dir = reader.getpath("temp_dir", "{toxworkdir}/.tmp") reader.addsubstitutions(temp_dir=config.temp_dir) config.sdistsrc = reader.getpath("sdistsrc", None) config.setupdir = reader.getpath("setupdir", "{toxinidir}") config.logdir = config.toxworkdir.join("log") within_parallel = PARALLEL_ENV_VAR_KEY_PRIVATE in os.environ if not within_parallel and not WITHIN_PROVISION: ensure_empty_dir(config.logdir) # determine indexserver dictionary config.indexserver = {"default": IndexServerConfig("default")} prefix = "indexserver" for line in reader.getlist(prefix): name, url = map(lambda x: x.strip(), line.split("=", 1)) config.indexserver[name] = IndexServerConfig(name, url) if config.option.skip_missing_interpreters == "config": val = reader.getbool("skip_missing_interpreters", False) config.option.skip_missing_interpreters = "true" if val else "false" override = False if config.option.indexurl: for url_def in config.option.indexurl: m = re.match(r"\W*(\w+)=(\S+)", url_def) if m is None: url = url_def name = "default" else: name, url = m.groups() if not url: url = None if name != "ALL": config.indexserver[name].url = url else: override = url # let ALL override all existing entries if override: for name in config.indexserver: config.indexserver[name] = IndexServerConfig(name, override) self.handle_provision(config, reader) self.parse_build_isolation(config, reader) res = self._getenvdata(reader, config) config.envlist, all_envs, config.envlist_default, config.envlist_explicit = res # factors used in config or predefined known_factors = self._list_section_factors("testenv") known_factors.update({"py", "python"}) # factors stated in config envlist stated_envlist = reader.getstring("envlist", replace=False) if stated_envlist: for env in _split_env(stated_envlist): known_factors.update(env.split("-")) # configure testenvs to_do = [] failures = OrderedDict() results = {} cur_self = self def run(name, section, subs, config): try: results[name] = cur_self.make_envconfig(name, section, subs, config) except Exception as exception: failures[name] = (exception, traceback.format_exc()) order = [] for name in all_envs: section = "{}{}".format(testenvprefix, name) factors = set(name.split("-")) if ( section in self._cfg or factors <= known_factors or all( tox.PYTHON.PY_FACTORS_RE.match(factor) for factor in factors - known_factors ) ): order.append(name) thread = Thread(target=run, args=(name, section, reader._subs, config)) thread.daemon = True thread.start() to_do.append(thread) for thread in to_do: while thread.is_alive(): thread.join(timeout=20) if failures: raise tox.exception.ConfigError( "\n".join( "{} failed with {} at {}".format(key, exc, trace) for key, (exc, trace) in failures.items() ), ) for name in order: config.envconfigs[name] = results[name] all_develop = all( name in config.envconfigs and config.envconfigs[name].usedevelop for name in config.envlist ) config.skipsdist = reader.getbool("skipsdist", all_develop) if config.option.devenv is not None: config.option.notest = True if config.option.devenv is not None and len(config.envlist) != 1: feedback("--devenv requires only a single -e", sysexit=True) def handle_provision(self, config, reader): requires_list = reader.getlist("requires") config.minversion = reader.getstring("minversion", None) config.provision_tox_env = name = reader.getstring("provision_tox_env", ".tox") min_version = "tox >= {}".format(config.minversion or Version(tox.__version__).public) deps = self.ensure_requires_satisfied(config, requires_list, min_version) if config.run_provision: section_name = "testenv:{}".format(name) if section_name not in self._cfg.sections: self._cfg.sections[section_name] = {} self._cfg.sections[section_name]["description"] = "meta tox" env_config = self.make_envconfig( name, "{}{}".format(testenvprefix, name), reader._subs, config, ) env_config.deps = deps config.envconfigs[config.provision_tox_env] = env_config raise tox.exception.MissingRequirement(config) # if provisioning is not on, now we need do a strict argument evaluation # raise on unknown args self.config._parser.parse_cli(args=self.config.args, strict=True) @staticmethod def ensure_requires_satisfied(config, requires, min_version): missing_requirements = [] failed_to_parse = False deps = [] exists = set() for require in requires + [min_version]: # noinspection PyBroadException try: package = requirements.Requirement(require) # check if the package even applies if package.marker and not package.marker.evaluate({"extra": ""}): continue package_name = canonicalize_name(package.name) if package_name not in exists: deps.append(DepConfig(require, None)) exists.add(package_name) dist = importlib_metadata.distribution(package.name) if not package.specifier.contains(dist.version, prereleases=True): raise MissingDependency(package) except requirements.InvalidRequirement as exception: failed_to_parse = True error("failed to parse {!r}".format(exception)) except Exception as exception: verbosity1("could not satisfy requires {!r}".format(exception)) missing_requirements.append(str(requirements.Requirement(require))) if failed_to_parse: raise tox.exception.BadRequirement() if WITHIN_PROVISION and missing_requirements: msg = "break infinite loop provisioning within {} missing {}" raise tox.exception.Error(msg.format(sys.executable, missing_requirements)) config.run_provision = bool(len(missing_requirements)) return deps def parse_build_isolation(self, config, reader): config.isolated_build = reader.getbool("isolated_build", False) config.isolated_build_env = reader.getstring("isolated_build_env", ".package") if config.isolated_build is True: name = config.isolated_build_env section_name = "testenv:{}".format(name) if section_name not in self._cfg.sections: self._cfg.sections[section_name] = {} self._cfg.sections[section_name]["deps"] = "" self._cfg.sections[section_name]["sitepackages"] = "False" self._cfg.sections[section_name]["description"] = "isolated packaging environment" config.envconfigs[name] = self.make_envconfig( name, "{}{}".format(testenvprefix, name), reader._subs, config, ) def _list_section_factors(self, section): factors = set() if section in self._cfg: for _, value in self._cfg[section].items(): exprs = re.findall(r"^([\w{}\.!,-]+)\:\s+", value, re.M) factors.update(*mapcat(_split_factor_expr_all, exprs)) return factors def make_envconfig(self, name, section, subs, config, replace=True): factors = set(name.split("-")) reader = SectionReader(section, self._cfg, fallbacksections=["testenv"], factors=factors) tc = TestenvConfig(name, config, factors, reader) reader.addsubstitutions( envname=name, envbindir=tc.get_envbindir, envsitepackagesdir=tc.get_envsitepackagesdir, envpython=tc.get_envpython, **subs ) for env_attr in config._testenv_attr: atype = env_attr.type try: if atype in ( "bool", "float", "path", "string", "dict", "dict_setenv", "argv", "argvlist", "argv_install_command", ): meth = getattr(reader, "get{}".format(atype)) res = meth(env_attr.name, env_attr.default, replace=replace) elif atype == "basepython": no_fallback = name in (config.provision_tox_env,) res = reader.getstring( env_attr.name, env_attr.default, replace=replace, no_fallback=no_fallback, ) elif atype == "space-separated-list": res = reader.getlist(env_attr.name, sep=" ") elif atype == "line-list": res = reader.getlist(env_attr.name, sep="\n") elif atype == "env-list": res = reader.getstring(env_attr.name, replace=False) res = tuple(_split_env(res)) else: raise ValueError("unknown type {!r}".format(atype)) if env_attr.postprocess: res = env_attr.postprocess(testenv_config=tc, value=res) except tox.exception.MissingSubstitution as e: tc._missing_subs[env_attr.name] = res = e # On Python 2, exceptions are handled in __getattr__ if not six.PY2 or not isinstance(res, Exception): setattr(tc, env_attr.name, res) if atype in ("path", "string", "basepython"): reader.addsubstitutions(**{env_attr.name: res}) return tc def _getallenvs(self, reader, extra_env_list=None): extra_env_list = extra_env_list or [] env_str = reader.getstring("envlist", replace=False) env_list = _split_env(env_str) for env in extra_env_list: if env not in env_list: env_list.append(env) all_envs = OrderedDict((i, None) for i in env_list) for section in self._cfg: if section.name.startswith(testenvprefix): all_envs[section.name[len(testenvprefix) :]] = None if not all_envs: all_envs["python"] = None return list(all_envs.keys()) def _getenvdata(self, reader, config): from_option = self.config.option.env from_environ = os.environ.get("TOXENV") from_config = reader.getstring("envlist", replace=False) env_list = [] envlist_explicit = False if (from_option and "ALL" in from_option) or ( not from_option and from_environ and "ALL" in from_environ.split(",") ): all_envs = self._getallenvs(reader) else: candidates = ( (os.environ.get(PARALLEL_ENV_VAR_KEY_PRIVATE), True), (from_option, True), (from_environ, True), ("py" if self.config.option.devenv is not None else None, False), (from_config, False), ) env_str, envlist_explicit = next(((i, e) for i, e in candidates if i), ([], False)) env_list = _split_env(env_str) all_envs = self._getallenvs(reader, env_list) if not env_list: env_list = all_envs provision_tox_env = config.provision_tox_env if config.provision_tox_env in env_list: msg = "provision_tox_env {} cannot be part of envlist".format(provision_tox_env) raise tox.exception.ConfigError(msg) package_env = config.isolated_build_env if config.isolated_build is True and package_env in all_envs: all_envs.remove(package_env) if config.isolated_build is True and package_env in env_list: msg = "isolated_build_env {} cannot be part of envlist".format(package_env) raise tox.exception.ConfigError(msg) return env_list, all_envs, _split_env(from_config), envlist_explicit @staticmethod def expand_section_names(config): """Generative section names. Allow writing section as [testenv:py{36,37}-cov] The parser will see it as two different sections: [testenv:py36-cov], [testenv:py37-cov] """ factor_re = re.compile(r"\{\s*([\w\s,-]+)\s*\}") split_re = re.compile(r"\s*,\s*") to_remove = set() for section in list(config.sections): split_section = factor_re.split(section) for parts in itertools.product(*map(split_re.split, split_section)): section_name = "".join(parts) if section_name not in config.sections: config.sections[section_name] = config.sections[section] to_remove.add(section) for section in to_remove: del config.sections[section] def _split_env(env): """if handed a list, action="append" was used for -e """ if env is None: return [] if not isinstance(env, list): env = [e.split("#", 1)[0].strip() for e in env.split("\n")] env = ",".join([e for e in env if e]) env = [env] return mapcat(_expand_envstr, env) def _is_negated_factor(factor): return factor.startswith("!") def _base_factor_name(factor): return factor[1:] if _is_negated_factor(factor) else factor def _split_factor_expr(expr): def split_single(e): raw = e.split("-") included = {_base_factor_name(factor) for factor in raw if not _is_negated_factor(factor)} excluded = {_base_factor_name(factor) for factor in raw if _is_negated_factor(factor)} return included, excluded partial_envs = _expand_envstr(expr) return [split_single(e) for e in partial_envs] def _split_factor_expr_all(expr): partial_envs = _expand_envstr(expr) return [{_base_factor_name(factor) for factor in e.split("-")} for e in partial_envs] def _expand_envstr(envstr): # split by commas not in groups tokens = _ENVSTR_SPLIT_PATTERN.split(envstr) envlist = ["".join(g).strip() for k, g in itertools.groupby(tokens, key=bool) if k] def expand(env): tokens = _ENVSTR_EXPAND_PATTERN.split(env) parts = [_WHITESPACE_PATTERN.sub("", token).split(",") for token in tokens] return ["".join(variant) for variant in itertools.product(*parts)] return mapcat(expand, envlist) def mapcat(f, seq): return list(itertools.chain.from_iterable(map(f, seq))) class DepConfig: def __init__(self, name, indexserver=None): self.name = name self.indexserver = indexserver def __repr__(self): if self.indexserver: if self.indexserver.name == "default": return self.name return ":{}:{}".format(self.indexserver.name, self.name) return str(self.name) class IndexServerConfig: def __init__(self, name, url=None): self.name = name self.url = url def __repr__(self): return "IndexServerConfig(name={}, url={})".format(self.name, self.url) 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: def __init__( self, section_name, cfgparser, fallbacksections=None, factors=(), prefix=None, posargs="", ): if prefix is None: self.section_name = section_name else: self.section_name = "{}:{}".format(prefix, section_name) self._cfg = cfgparser self.fallbacksections = fallbacksections or [] self.factors = factors self._subs = {} self._subststack = [] self._setenv = None self.posargs = posargs def get_environ_value(self, name): if self._setenv is None: return os.environ.get(name) return self._setenv.get(name) def addsubstitutions(self, _posargs=None, **kw): self._subs.update(kw) if _posargs: self.posargs = _posargs def getpath(self, name, defaultpath, replace=True): path = self.getstring(name, defaultpath, replace=replace) if path is not None: toxinidir = self._subs["toxinidir"] return toxinidir.join(path, abs=True) def getlist(self, name, sep="\n"): s = self.getstring(name, None) if s is None: return [] return [x.strip() for x in s.split(sep) if x.strip()] def getdict(self, name, default=None, sep="\n", replace=True): value = self.getstring(name, None, replace=replace) return self._getdict(value, default=default, sep=sep, replace=replace) def getdict_setenv(self, name, default=None, sep="\n", replace=True): value = self.getstring(name, None, replace=replace, crossonly=True) definitions = self._getdict(value, default=default, sep=sep, replace=replace) self._setenv = SetenvDict(definitions, reader=self) return self._setenv def _getdict(self, value, default, sep, replace=True): if value is None or not replace: return default or {} env_values = {} for line in value.split(sep): if line.strip(): if line.startswith("#"): # comment lines are ignored pass elif line.startswith("file|"): # file markers contain paths to env files file_path = line[5:].strip() if os.path.exists(file_path): with open(file_path, "rt") as file_handler: content = file_handler.read() env_values.update(self._getdict(content, "", sep, replace)) else: name, value = line.split("=", 1) env_values[name.strip()] = value.strip() return env_values def getfloat(self, name, default=None, replace=True): s = self.getstring(name, default, replace=replace) if not s or not replace: s = default if s is None: raise KeyError("no config value [{}] {} found".format(self.section_name, name)) if not isinstance(s, float): try: s = float(s) except ValueError: raise tox.exception.ConfigError("{}: invalid float {!r}".format(name, s)) return s def getbool(self, name, default=None, replace=True): s = self.getstring(name, default, replace=replace) if not s or not replace: s = default if s is None: raise KeyError("no config value [{}] {} found".format(self.section_name, name)) if not isinstance(s, bool): if s.lower() == "true": s = True elif s.lower() == "false": s = False else: raise tox.exception.ConfigError( "{}: boolean value {!r} needs to be 'True' or 'False'".format(name, s), ) return s def getargvlist(self, name, default="", replace=True): s = self.getstring(name, default, replace=False) return _ArgvlistReader.getargvlist(self, s, replace=replace, name=name) def getargv(self, name, default="", replace=True): return self.getargvlist(name, default, replace=replace)[0] def getargv_install_command(self, name, default="", replace=True): s = self.getstring(name, default, replace=False) if not s: # This occurs when factors are used, and a testenv doesnt have # a factorised value for install_command, most commonly occurring # if setting platform is also used. # An empty value causes error install_command must contain '{packages}'. s = default if "{packages}" in s: s = s.replace("{packages}", r"\{packages\}") if "{opts}" in s: s = s.replace("{opts}", r"\{opts\}") return _ArgvlistReader.getargvlist(self, s, replace=replace, name=name)[0] def getstring(self, name, default=None, replace=True, crossonly=False, no_fallback=False): x = None sections = [self.section_name] + ([] if no_fallback else self.fallbacksections) for s in sections: try: x = self._cfg[s][name] break except KeyError: continue if x is None: x = default else: # It is needed to apply factors before unwrapping # dependencies, otherwise it can break the substitution # process. Once they are unwrapped, we call apply factors # again for those new dependencies. x = self._apply_factors(x) x = self._replace_if_needed(x, name, replace, crossonly) x = self._apply_factors(x) x = self._replace_if_needed(x, name, replace, crossonly) return x def getposargs(self, default=None): if self.posargs: posargs = self.posargs if sys.platform.startswith("win"): posargs_string = list2cmdline([x for x in posargs if x]) else: posargs_string = " ".join([shlex_quote(x) for x in posargs if x]) return posargs_string else: return default or "" def _replace_if_needed(self, x, name, replace, crossonly): if replace and x and hasattr(x, "replace"): x = self._replace(x, name=name, crossonly=crossonly) return x def _apply_factors(self, s): def factor_line(line): m = _FACTOR_LINE_PATTERN.search(line) if not m: return line expr, line = m.groups() if any( included <= self.factors and not any(x in self.factors for x in excluded) for included, excluded in _split_factor_expr(expr) ): return line lines = s.strip().splitlines() return "\n".join(filter(None, map(factor_line, lines))) def _replace(self, value, name=None, section_name=None, crossonly=False): if "{" not in value: return value section_name = section_name if section_name else self.section_name assert name self._subststack.append((section_name, name)) try: replaced = Replacer(self, crossonly=crossonly).do_replace(value) assert self._subststack.pop() == (section_name, name) except tox.exception.MissingSubstitution: if not section_name.startswith(testenvprefix): raise tox.exception.ConfigError( "substitution env:{!r}: unknown or recursive definition in" " section {!r}.".format(value, section_name), ) raise return replaced class Replacer: RE_ITEM_REF = re.compile( r""" (?[^[:{}]+):)? # optional sub_type for special rules (?P(?:\[[^,{}]*\])?[^:,{}]*) # substitution key (?::(?P([^{}]|\\{|\\})*))? # default value [}] """, re.VERBOSE, ) def __init__(self, reader, crossonly=False): self.reader = reader self.crossonly = crossonly def do_replace(self, value): """ Recursively expand substitutions starting from the innermost expression """ def substitute_once(x): return self.RE_ITEM_REF.sub(self._replace_match, x) expanded = substitute_once(value) while expanded != value: # substitution found value = expanded expanded = substitute_once(value) return expanded @staticmethod def _unescape(s): return s.replace("\\{", "{").replace("\\}", "}") def _replace_match(self, match): g = match.groupdict() sub_value = g["substitution_value"] if self.crossonly: if sub_value.startswith("["): return self._substitute_from_other_section(sub_value) # in crossonly we return all other hits verbatim start, end = match.span() return match.string[start:end] full_match = match.group(0) # ":" is swallowed by the regex, so the raw matched string is checked if full_match.startswith("{:"): if full_match != "{:}": raise tox.exception.ConfigError( "Malformed substitution with prefix ':': {}".format(full_match), ) return os.pathsep default_value = g["default_value"] # special case: opts and packages. Leave {opts} and # {packages} intact, they are replaced manually in # _venv.VirtualEnv.run_install_command. if sub_value in ("opts", "packages"): return "{{{}}}".format(sub_value) if sub_value == "posargs": return self.reader.getposargs(default_value) sub_type = g["sub_type"] if sub_type == "posargs": if default_value: value = "{}:{}".format(sub_value, default_value) else: value = sub_value return self.reader.getposargs(value) if not sub_type and not sub_value: raise tox.exception.ConfigError( "Malformed substitution; no substitution type provided. " "If you were using `{}` for `os.pathsep`, please use `{:}`.", ) if not sub_type and not default_value and sub_value == "/": return os.sep if sub_type == "env": return self._replace_env(sub_value, default_value) if sub_type == "tty": if is_interactive(): return match.group("substitution_value") return match.group("default_value") if sub_type == "posargs": return self.reader.getposargs(sub_value) if sub_type is not None: raise tox.exception.ConfigError( "No support for the {} substitution type".format(sub_type), ) return self._replace_substitution(sub_value) def _replace_env(self, key, default): if not key: raise tox.exception.ConfigError("env: requires an environment variable name") value = self.reader.get_environ_value(key) if value is not None: return value if default is not None: return default raise tox.exception.MissingSubstitution(key) def _substitute_from_other_section(self, key): if key.startswith("[") and "]" in key: i = key.find("]") section, item = key[1:i], key[i + 1 :] cfg = self.reader._cfg if section in cfg and item in cfg[section]: if (section, item) in self.reader._subststack: raise tox.exception.SubstitutionStackError( "{} already in {}".format((section, item), self.reader._subststack), ) x = str(cfg[section][item]) return self.reader._replace( x, name=item, section_name=section, crossonly=self.crossonly, ) raise tox.exception.ConfigError("substitution key {!r} not found".format(key)) def _replace_substitution(self, sub_key): val = self.reader._subs.get(sub_key, None) if val is None: val = self._substitute_from_other_section(sub_key) if callable(val): val = val() return str(val) def is_interactive(): return sys.stdin.isatty() class _ArgvlistReader: @classmethod def getargvlist(cls, reader, value, replace=True, name=None): """Parse ``commands`` argvlist multiline string. :param SectionReader reader: reader to be used. :param str value: Content stored by key. :rtype: list[list[str]] :raise :class:`tox.exception.ConfigError`: line-continuation ends nowhere while resolving for specified section """ commands = [] current_command = "" for line in value.splitlines(): line = line.rstrip() if not line: continue if line.endswith("\\"): current_command += " {}".format(line[:-1]) continue current_command += line if is_section_substitution(current_command): replaced = reader._replace(current_command, crossonly=True, name=name) commands.extend(cls.getargvlist(reader, replaced, name=name)) else: commands.append(cls.processcommand(reader, current_command, replace, name=name)) current_command = "" else: if current_command: raise tox.exception.ConfigError( "line-continuation ends nowhere while resolving for [{}] {}".format( reader.section_name, "commands", ), ) return commands @classmethod def processcommand(cls, reader, command, replace=True, name=None): # Iterate through each word of the command substituting as # appropriate to construct the new command string. This # string is then broken up into exec argv components using # shlex. if replace: newcommand = "" for word in CommandParser(command).words(): if word == "[]": newcommand += reader.getposargs() continue new_arg = "" new_word = reader._replace(word, name=name) new_word = reader._replace(new_word, name=name) new_word = Replacer._unescape(new_word) new_arg += new_word newcommand += new_arg else: newcommand = command # Construct shlex object that will not escape any values, # use all values as is in argv. shlexer = shlex.shlex(newcommand, posix=True) shlexer.whitespace_split = True shlexer.escape = "" return list(shlexer) class CommandParser(object): class State(object): def __init__(self): self.word = "" self.depth = 0 self.yield_words = [] def __init__(self, command): self.command = command def words(self): ps = CommandParser.State() def word_has_ended(): return ( ( cur_char in string.whitespace and ps.word and ps.word[-1] not in string.whitespace ) or (cur_char == "{" and ps.depth == 0 and not ps.word.endswith("\\")) or (ps.depth == 0 and ps.word and ps.word[-1] == "}") or (cur_char not in string.whitespace and ps.word and ps.word.strip() == "") ) def yield_this_word(): yieldword = ps.word ps.word = "" if yieldword: ps.yield_words.append(yieldword) def yield_if_word_ended(): if word_has_ended(): yield_this_word() def accumulate(): ps.word += cur_char def push_substitution(): ps.depth += 1 def pop_substitution(): ps.depth -= 1 for cur_char in self.command: if cur_char in string.whitespace: if ps.depth == 0: yield_if_word_ended() accumulate() elif cur_char == "{": yield_if_word_ended() accumulate() push_substitution() elif cur_char == "}": accumulate() pop_substitution() else: yield_if_word_ended() accumulate() if ps.word.strip(): yield_this_word() return ps.yield_words def getcontextname(): if any(env in os.environ for env in ["JENKINS_URL", "HUDSON_URL"]): return "jenkins" return None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/config/parallel.py0000644000175100001710000000401600000000000017716 0ustar00vstsdocker00000000000000from __future__ import absolute_import, unicode_literals from argparse import ArgumentTypeError ENV_VAR_KEY_PUBLIC = "TOX_PARALLEL_ENV" ENV_VAR_KEY_PRIVATE = "_TOX_PARALLEL_ENV" OFF_VALUE = 0 DEFAULT_PARALLEL = OFF_VALUE def auto_detect_cpus(): try: from os import sched_getaffinity # python 3 only def cpu_count(): return len(sched_getaffinity(0)) except ImportError: # python 2 options try: from os import cpu_count except ImportError: from multiprocessing import cpu_count try: n = cpu_count() except NotImplementedError: # pragma: no cov n = None # pragma: no cov return n if n else 1 def parse_num_processes(s): if s == "all": return None if s == "auto": return auto_detect_cpus() else: value = int(s) if value < 0: raise ArgumentTypeError("value must be positive") return value def add_parallel_flags(parser): parser.add_argument( "-p", "--parallel", nargs="?", const="auto", dest="parallel", help="run tox environments in parallel, the argument controls limit: all," " auto or missing argument - cpu count, some positive number, 0 to turn off", action="store", type=parse_num_processes, default=DEFAULT_PARALLEL, metavar="VAL", ) parser.add_argument( "-o", "--parallel-live", action="store_true", dest="parallel_live", help="connect to stdout while running environments", ) def add_parallel_config(parser): parser.add_testenv_attribute( "depends", type="env-list", help="tox environments that this environment depends on (must be run after those)", ) parser.add_testenv_attribute( "parallel_show_output", type="bool", default=False, help="if set to True the content of the output will always be shown " "when running in parallel mode", ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/config/reporter.py0000644000175100001710000000120600000000000017762 0ustar00vstsdocker00000000000000from __future__ import absolute_import, unicode_literals def add_verbosity_commands(parser): parser.add_argument( "-v", "--verbose", action="count", dest="verbose_level", default=0, help="increase verbosity of reporting output." "-vv mode turns off output redirection for package installation, " "above level two verbosity flags are passed through to pip (with two less level)", ) parser.add_argument( "-q", "--quiet", action="count", dest="quiet_level", default=0, help="progressively silence reporting output.", ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/constants.py0000644000175100001710000000366400000000000016701 0ustar00vstsdocker00000000000000"""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 os import re import sys _THIS_FILE = os.path.realpath(os.path.abspath(__file__)) class PYTHON: PY_FACTORS_RE = re.compile("^(?!py$)(py|pypy|jython)([2-9][0-9]?[0-9]?)?$") CURRENT_RELEASE_ENV = "py37" """Should hold currently released py -> for easy updating""" QUICKSTART_PY_ENVS = ["py27", "py35", "py36", CURRENT_RELEASE_ENV, "pypy", "jython"] """For choices in tox-quickstart""" class INFO: DEFAULT_CONFIG_NAME = "tox.ini" CONFIG_CANDIDATES = ("pyproject.toml", "tox.ini", "setup.cfg") IS_WIN = sys.platform == "win32" IS_PYPY = hasattr(sys, "pypy_version_info") 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 = ["-{}".format(option) for option in SHORT_OPTIONS] INSTALL_LONG_OPTIONS_ARGUMENT = ["--{}".format(option) for option in LONG_OPTIONS] _HELP_DIR = os.path.join(os.path.dirname(_THIS_FILE), "helper") VERSION_QUERY_SCRIPT = os.path.join(_HELP_DIR, "get_version.py") SITE_PACKAGE_QUERY_SCRIPT = os.path.join(_HELP_DIR, "get_site_package_dir.py") BUILD_REQUIRE_SCRIPT = os.path.join(_HELP_DIR, "build_requires.py") BUILD_ISOLATED = os.path.join(_HELP_DIR, "build_isolated.py") PARALLEL_RESULT_JSON_PREFIX = ".tox-result" PARALLEL_RESULT_JSON_SUFFIX = ".json" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/exception.py0000644000175100001710000000636000000000000016657 0ustar00vstsdocker00000000000000import os import pipes 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_ = "{} for command {}".format(exception_name, command) if exit_code is not None: if exit_code < 0 or (os.name == "posix" and exit_code > 128): signals = { number: name for name, number in vars(signal).items() if name.startswith("SIG") } if exit_code < 0: # Signal reported via subprocess.Popen. sig_name = signals.get(-exit_code) str_ += " (exited with code {:d} ({}))".format(exit_code, sig_name) else: str_ += " (exited with code {:d})".format(exit_code) number = exit_code - 128 name = signals.get(number) if name: str_ += ( ")\nNote: this might indicate a fatal error signal " "({:d} - 128 = {:d}: {})".format(exit_code, number, name) ) str_ += " (exited with code {:d})".format(exit_code) return str_ class Error(Exception): def __str__(self): return "{}: {}".format(self.__class__.__name__, self.args[0]) class MissingSubstitution(Error): FLAG = "TOX_MISSING_SUBSTITUTION" """placeholder for debugging configurations""" def __init__(self, name): self.name = name super(Error, self).__init__(name) class ConfigError(Error): """Error in tox configuration.""" class SubstitutionStackError(ConfigError, ValueError): """Error in tox configuration recursive substitution.""" 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, out=None): super(Error, self).__init__(command, exit_code) self.command = command self.exit_code = exit_code self.out = out def __str__(self): return exit_code_str(self.__class__.__name__, self.command, self.exit_code) class MissingDirectory(Error): """A directory did not exist.""" class MissingDependency(Error): """A dependency could not be found or determined.""" class MissingRequirement(Error): """A requirement defined in :config:`require` is not met.""" def __init__(self, config): self.config = config def __str__(self): return " ".join(pipes.quote(i) for i in self.config.requires) class BadRequirement(Error): """A requirement defined in :config:`require` cannot be parsed.""" ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9035115 tox-3.21.4/src/tox/helper/0000755000175100001710000000000000000000000015561 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/helper/__init__.py0000644000175100001710000000000000000000000017660 0ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/helper/build_isolated.py0000644000175100001710000000236000000000000021117 0ustar00vstsdocker00000000000000"""PEP 517 build backend invocation script. It accepts externally parsed build configuration from `[build-system]` in `pyproject.toml` and invokes an API endpoint for building an sdist tarball. """ import os import sys def _ensure_module_in_paths(module, paths): """Verify that the imported backend belongs in-tree.""" if not paths: return module_path = os.path.normcase(os.path.abspath(module.__file__)) normalized_paths = (os.path.normcase(os.path.abspath(path)) for path in paths) if any(os.path.commonprefix((module_path, path)) == path for path in normalized_paths): return raise SystemExit( "build-backend ({!r}) must exist in one of the paths " "specified by backend-path ({!r})".format(module, paths), ) dist_folder = sys.argv[1] backend_spec = sys.argv[2] backend_obj = sys.argv[3] if len(sys.argv) >= 4 else None backend_paths = sys.argv[4].split(os.path.pathsep) if sys.argv[4] else [] sys.path[:0] = backend_paths backend = __import__(backend_spec, fromlist=["_trash"]) _ensure_module_in_paths(backend, backend_paths) if backend_obj: backend = getattr(backend, backend_obj) basename = backend.build_sdist(dist_folder, {"--global-option": ["--formats=gztar"]}) print(basename) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/helper/build_requires.py0000644000175100001710000000072200000000000021152 0ustar00vstsdocker00000000000000import json import os import sys backend_spec = sys.argv[1] backend_obj = sys.argv[2] if len(sys.argv) >= 3 else None backend_paths = sys.argv[3].split(os.path.pathsep) if len(sys.argv) >= 4 else [] sys.path[:0] = backend_paths backend = __import__(backend_spec, fromlist=["_trash"]) if backend_obj: backend = getattr(backend, backend_obj) for_build_requires = backend.get_requires_for_build_sdist(None) output = json.dumps(for_build_requires) print(output) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/helper/get_site_package_dir.py0000644000175100001710000000027300000000000022251 0ustar00vstsdocker00000000000000from __future__ import unicode_literals import distutils.sysconfig import json import sys data = json.dumps({"dir": distutils.sysconfig.get_python_lib(prefix=sys.argv[1])}) print(data) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/helper/get_version.py0000644000175100001710000000067200000000000020464 0ustar00vstsdocker00000000000000from __future__ import unicode_literals import json import platform import sys info = { "executable": sys.executable, "implementation": platform.python_implementation(), "version_info": list(sys.version_info), "version": sys.version, "is_64": sys.maxsize > 2 ** 32, "sysplatform": sys.platform, "extra_version_info": getattr(sys, "pypy_version_info", None), } info_as_dump = json.dumps(info) print(info_as_dump) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/hookspecs.py0000644000175100001710000000750000000000000016654 0ustar00vstsdocker00000000000000"""Hook specifications for tox - see https://pluggy.readthedocs.io/""" import pluggy hookspec = pluggy.HookspecMarker("tox") @hookspec def tox_addoption(parser): """ add command line options to the argparse-style parser object.""" @hookspec def tox_configure(config): """Called after command line options 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_package(session, venv): """Return the package to be installed for the given venv. Called once for every environment.""" @hookspec(firstresult=True) def tox_get_python_executable(envconfig): """Return a python executable for the given python base name. The first plugin/hook which returns an executable path will determine it. ``envconfig`` is the testenv configuration which contains per-testenv configuration, notably the ``.envname`` and ``.basepython`` setting. """ @hookspec(firstresult=True) def tox_testenv_create(venv, action): """Perform creation action for this venv. Some example usage: - To *add* behavior but still use tox's implementation to set up a virtualenv, implement this hook but do not return a value (or explicitly return ``None``). - To *override* tox's virtualenv creation, implement this hook and return a non-``None`` value. .. note:: This api is experimental due to the unstable api of :class:`tox.venv.VirtualEnv`. .. note:: This hook uses ``firstresult=True`` (see `pluggy first result only`_) -- hooks implementing this will be run until one returns non-``None``. .. _`pluggy first result only`: https://pluggy.readthedocs.io/en/latest/#first-result-only """ @hookspec(firstresult=True) def tox_testenv_install_deps(venv, action): """Perform install dependencies action for this venv. Some example usage: - To *add* behavior but still use tox's implementation to install dependencies, implement this hook but do not return a value (or explicitly return ``None``). One use-case may be to install (or ensure) non-python dependencies such as debian packages. - To *override* tox's installation of dependencies, implement this hook and return a non-``None`` value. One use-case may be to install via a different installation tool such as `pip-accel`_ or `pip-faster`_. .. note:: This api is experimental due to the unstable api of :class:`tox.venv.VirtualEnv`. .. 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 """ @hookspec def tox_runtest_pre(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. """ @hookspec(firstresult=True) def tox_runtest(venv, redirect): """Run the tests for this venv. .. 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): """Perform arbitrary action after running tests for this venv. This could be used to have per-venv test reporting of pass/fail status. """ @hookspec(firstresult=True) def tox_runenvreport(venv, action): """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`` """ @hookspec def tox_cleanup(session): """Called just before the session is destroyed, allowing any final cleanup operation""" ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9035115 tox-3.21.4/src/tox/interpreters/0000755000175100001710000000000000000000000017030 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/interpreters/__init__.py0000644000175100001710000001035600000000000021146 0ustar00vstsdocker00000000000000from __future__ import unicode_literals import json import sys import tox from tox import reporter from tox.constants import SITE_PACKAGE_QUERY_SCRIPT from tox.interpreters.via_path import get_python_info class Interpreters: def __init__(self, hook): self.name2executable = {} self.executable2info = {} self.hook = hook def get_executable(self, envconfig): """return path object to the executable for the given name (e.g. python2.7, python3.6, python etc.) if name is already an existing path, return name. If an interpreter cannot be found, return None. """ try: return self.name2executable[envconfig.envname] except KeyError: exe = self.hook.tox_get_python_executable(envconfig=envconfig) reporter.verbosity2("{} uses {}".format(envconfig.envname, exe)) self.name2executable[envconfig.envname] = exe return exe def get_info(self, envconfig): executable = self.get_executable(envconfig) name = envconfig.basepython if not executable: return NoInterpreterInfo(name=name) try: return self.executable2info[executable] except KeyError: info = run_and_get_interpreter_info(name, executable) self.executable2info[executable] = info return info def get_sitepackagesdir(self, info, envdir): if not info.executable: return "" envdir = str(envdir) try: res = exec_on_interpreter(str(info.executable), SITE_PACKAGE_QUERY_SCRIPT, str(envdir)) except ExecFailed as e: reporter.verbosity1("execution failed: {} -- {}".format(e.out, e.err)) return "" else: return res["dir"] def run_and_get_interpreter_info(name, executable): assert executable try: result = get_python_info(str(executable)) result["version_info"] = tuple(result["version_info"]) # fix json dump transformation if result["extra_version_info"] is not None: result["extra_version_info"] = tuple( result["extra_version_info"], ) # fix json dump transformation del result["version"] result["executable"] = str(executable) except ExecFailed as e: return NoInterpreterInfo(name, executable=e.executable, out=e.out, err=e.err) else: return InterpreterInfo(**result) def exec_on_interpreter(*args): from subprocess import PIPE, Popen popen = Popen(args, stdout=PIPE, stderr=PIPE, universal_newlines=True) out, err = popen.communicate() if popen.returncode: raise ExecFailed(args[0], args[1:], out, err) if err: sys.stderr.write(err) try: result = json.loads(out) except Exception: raise ExecFailed(args[0], args[1:], out, "could not decode {!r}".format(out)) return result class ExecFailed(Exception): def __init__(self, executable, source, out, err): self.executable = executable self.source = source self.out = out self.err = err class InterpreterInfo: def __init__( self, implementation, executable, version_info, sysplatform, is_64, extra_version_info, ): self.implementation = implementation self.executable = executable self.version_info = version_info self.sysplatform = sysplatform self.is_64 = is_64 self.extra_version_info = extra_version_info def __str__(self): return "".format(self.executable, self.version_info) class NoInterpreterInfo: def __init__(self, name, executable=None, out=None, err="not found"): self.name = name self.executable = executable self.version_info = None self.out = out self.err = err def __str__(self): if self.executable: return "".format(self.executable) else: return "".format(self.name) if tox.INFO.IS_WIN: from .windows import tox_get_python_executable else: from .unix import tox_get_python_executable assert tox_get_python_executable ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/interpreters/common.py0000644000175100001710000000144400000000000020675 0ustar00vstsdocker00000000000000import os from tox.interpreters.py_spec import CURRENT, PythonSpec from tox.interpreters.via_path import exe_spec def base_discover(envconfig): base_python = envconfig.basepython spec = PythonSpec.from_name(base_python) # 1. check passed in discover elements discovers = envconfig.config.option.discover if not discovers: discovers = os.environ.get(str("TOX_DISCOVER"), "").split(os.pathsep) for discover in discovers: if os.path.exists(discover): cur_spec = exe_spec(discover, envconfig.basepython) if cur_spec is not None and cur_spec.satisfies(spec): return spec, cur_spec.path # 2. check current if spec.name is not None and CURRENT.satisfies(spec): return spec, CURRENT.path return spec, None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/interpreters/py_spec.py0000644000175100001710000000462200000000000021050 0ustar00vstsdocker00000000000000from __future__ import unicode_literals import os import re import sys import six import tox class PythonSpec(object): def __init__(self, name, major, minor, architecture, path, args=None): self.name = name self.major = major self.minor = minor self.architecture = architecture self.path = path self.args = args def __repr__(self): return ( "{0.__class__.__name__}(name={0.name!r}, major={0.major!r}, minor={0.minor!r}, " "architecture={0.architecture!r}, path={0.path!r}, args={0.args!r})" ).format(self) def __str__(self): msg = repr(self) return msg.encode("utf-8") if six.PY2 else msg def satisfies(self, req): if req.is_abs and self.is_abs and self.path != req.path: return False if req.name is not None and req.name != self.name: return False if req.architecture is not None and req.architecture != self.architecture: return False if req.major is not None and req.major != self.major: return False if req.minor is not None and req.minor != self.minor: return False if req.major is None and req.minor is not None: return False return True @property def is_abs(self): return self.path is not None and os.path.isabs(self.path) @classmethod def from_name(cls, base_python): name, major, minor, architecture, path = None, None, None, None, None if os.path.isabs(base_python): path = base_python else: match = re.match(r"(python|pypy|jython)(\d)?(?:\.(\d+))?(?:-(32|64))?$", base_python) if match: groups = match.groups() name = groups[0] major = int(groups[1]) if len(groups) >= 2 and groups[1] is not None else None minor = int(groups[2]) if len(groups) >= 3 and groups[2] is not None else None architecture = ( int(groups[3]) if len(groups) >= 4 and groups[3] is not None else None ) else: path = base_python return cls(name, major, minor, architecture, path) CURRENT = PythonSpec( "pypy" if tox.constants.INFO.IS_PYPY else "python", sys.version_info[0], sys.version_info[1], 64 if sys.maxsize > 2 ** 32 else 32, sys.executable, ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/interpreters/unix.py0000644000175100001710000000104600000000000020366 0ustar00vstsdocker00000000000000from __future__ import unicode_literals import tox from .common import base_discover from .via_path import check_with_path @tox.hookimpl def tox_get_python_executable(envconfig): spec, path = base_discover(envconfig) if path is not None: return path # 3. check if the literal base python candidates = [envconfig.basepython] # 4. check if the un-versioned name is good if spec.name is not None and spec.name != envconfig.basepython: candidates.append(spec.name) return check_with_path(candidates, spec) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/interpreters/via_path.py0000644000175100001710000000426700000000000021206 0ustar00vstsdocker00000000000000from __future__ import unicode_literals import json import os import subprocess from collections import defaultdict from threading import Lock import py from tox import reporter from tox.constants import VERSION_QUERY_SCRIPT from .py_spec import PythonSpec def check_with_path(candidates, spec): for path in candidates: base = path if not os.path.isabs(path): path = py.path.local.sysfind(path) if path is not None: if os.path.exists(str(path)): cur_spec = exe_spec(path, base) if cur_spec is not None and cur_spec.satisfies(spec): return cur_spec.path _SPECS = {} _SPECK_LOCK = defaultdict(Lock) def exe_spec(python_exe, base): if not isinstance(python_exe, str): python_exe = str(python_exe) with _SPECK_LOCK[python_exe]: if python_exe not in _SPECS: info = get_python_info(python_exe) if info is not None: found = PythonSpec( "pypy" if info["implementation"] == "PyPy" else "python", info["version_info"][0], info["version_info"][1], 64 if info["is_64"] else 32, info["executable"], ) reporter.verbosity2("{} ({}) is {}".format(base, python_exe, info)) else: found = None _SPECS[python_exe] = found return _SPECS[python_exe] _python_info_cache = {} def get_python_info(cmd): try: return _python_info_cache[cmd].copy() except KeyError: pass proc = subprocess.Popen( [cmd] + [VERSION_QUERY_SCRIPT], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, ) out, err = proc.communicate() if not proc.returncode: try: result = json.loads(out) except ValueError as exception: failure = exception else: _python_info_cache[cmd] = result return result.copy() else: failure = "exit code {}".format(proc.returncode) reporter.verbosity1("{!r} cmd {!r} out {!r} err {!r} ".format(failure, cmd, out, err)) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.907512 tox-3.21.4/src/tox/interpreters/windows/0000755000175100001710000000000000000000000020522 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/interpreters/windows/__init__.py0000644000175100001710000000264200000000000022637 0ustar00vstsdocker00000000000000from __future__ import unicode_literals from threading import Lock import tox from ..common import base_discover from ..py_spec import CURRENT from ..via_path import check_with_path @tox.hookimpl def tox_get_python_executable(envconfig): spec, path = base_discover(envconfig) if path is not None: return path # second check if the py.exe has it (only for non path specs) if spec.path is None: py_exe = locate_via_pep514(spec) if py_exe is not None: return py_exe # third check if the literal base python is on PATH candidates = [envconfig.basepython] # fourth check if the name is on PATH if spec.name is not None and spec.name != envconfig.basepython: candidates.append(spec.name) # or check known locations if spec.major is not None and spec.minor is not None: if spec.name == "python": # The standard names are in predictable places. candidates.append(r"c:\python{}{}\python.exe".format(spec.major, spec.minor)) return check_with_path(candidates, spec) _PY_AVAILABLE = [] _PY_LOCK = Lock() def locate_via_pep514(spec): with _PY_LOCK: if not _PY_AVAILABLE: from . import pep514 _PY_AVAILABLE.extend(pep514.discover_pythons()) _PY_AVAILABLE.append(CURRENT) for cur_spec in _PY_AVAILABLE: if cur_spec.satisfies(spec): return cur_spec.path ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/interpreters/windows/pep514.py0000644000175100001710000001232700000000000022117 0ustar00vstsdocker00000000000000"""Implement https://www.python.org/dev/peps/pep-0514/ to discover interpreters - Windows only""" from __future__ import unicode_literals import os import re import six from six.moves import winreg from tox import reporter from tox.interpreters.py_spec import PythonSpec def enum_keys(key): at = 0 while True: try: yield winreg.EnumKey(key, at) except OSError: break at += 1 def get_value(key, value_name): try: return winreg.QueryValueEx(key, value_name)[0] except OSError: return None def discover_pythons(): for hive, hive_name, key, flags, default_arch in [ (winreg.HKEY_CURRENT_USER, "HKEY_CURRENT_USER", r"Software\Python", 0, 64), ( winreg.HKEY_LOCAL_MACHINE, "HKEY_LOCAL_MACHINE", r"Software\Python", winreg.KEY_WOW64_64KEY, 64, ), ( winreg.HKEY_LOCAL_MACHINE, "HKEY_LOCAL_MACHINE", r"Software\Python", winreg.KEY_WOW64_32KEY, 32, ), ]: for spec in process_set(hive, hive_name, key, flags, default_arch): yield spec def process_set(hive, hive_name, key, flags, default_arch): try: with winreg.OpenKeyEx(hive, key, 0, winreg.KEY_READ | flags) as root_key: for company in enum_keys(root_key): if company == "PyLauncher": # reserved continue for spec in process_company(hive_name, company, root_key, default_arch): yield spec except OSError: pass def process_company(hive_name, company, root_key, default_arch): with winreg.OpenKeyEx(root_key, company) as company_key: for tag in enum_keys(company_key): for spec in process_tag(hive_name, company, company_key, tag, default_arch): yield spec def process_tag(hive_name, company, company_key, tag, default_arch): with winreg.OpenKeyEx(company_key, tag) as tag_key: major, minor = load_version_data(hive_name, company, tag, tag_key) if major is None: return arch = load_arch_data(hive_name, company, tag, tag_key, default_arch) exe, args = load_exe(hive_name, company, company_key, tag) if exe is not None: name = "python" if company == "PythonCore" else company yield PythonSpec(name, major, minor, arch, exe, args) def load_exe(hive_name, company, company_key, tag): key_path = "{}/{}/{}".format(hive_name, company, tag) try: with winreg.OpenKeyEx(company_key, r"{}\InstallPath".format(tag)) as ip_key: with ip_key: exe = get_value(ip_key, "ExecutablePath") if exe is None: ip = get_value(ip_key, None) if ip is None: msg(key_path, "no ExecutablePath or default for it") else: exe = os.path.join(ip, "python.exe") if os.path.exists(exe): args = get_value(ip_key, "ExecutableArguments") return exe, args else: msg(key_path, "exe does not exists {}".format(exe)) except OSError: msg("{}/{}".format(key_path, "InstallPath"), "missing") return None, None def load_arch_data(hive_name, company, tag, tag_key, default_arch): arch_str = get_value(tag_key, "SysArchitecture") if arch_str is not None: key_path = "{}/{}/{}/SysArchitecture".format(hive_name, company, tag) try: return parse_arch(arch_str) except ValueError as sys_arch: msg(key_path, sys_arch) return default_arch def parse_arch(arch_str): if not isinstance(arch_str, six.string_types): raise ValueError("arch is not string") match = re.match(r"(\d+)bit", arch_str) if match: return int(next(iter(match.groups()))) raise ValueError("invalid format {}".format(arch_str)) def load_version_data(hive_name, company, tag, tag_key): version_str = get_value(tag_key, "SysVersion") major, minor = None, None if version_str is not None: key_path = "{}/{}/{}/SysVersion".format(hive_name, company, tag) try: major, minor = parse_version(get_value(tag_key, "SysVersion")) except ValueError as sys_version: msg(key_path, sys_version) if major is None: key_path = "{}/{}/{}".format(hive_name, company, tag) try: major, minor = parse_version(tag) except ValueError as tag_version: msg(key_path, tag_version) return major, minor def parse_version(version_str): if not isinstance(version_str, six.string_types): raise ValueError("key is not string") match = re.match(r"(\d+)\.(\d+).*", version_str) if match: return tuple(int(i) for i in match.groups()) raise ValueError("invalid format {}".format(version_str)) def msg(path, what): reporter.verbosity1("PEP-514 violation in Windows Registry at {} error: {}".format(path, what)) def _run(): reporter.update_default_reporter(0, reporter.Verbosity.DEBUG) for spec in discover_pythons(): print(repr(spec)) if __name__ == "__main__": _run() ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.907512 tox-3.21.4/src/tox/logs/0000755000175100001710000000000000000000000015246 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/logs/__init__.py0000644000175100001710000000021100000000000017351 0ustar00vstsdocker00000000000000"""This module handles collecting and persisting in json format a tox session""" from .result import ResultLog __all__ = ("ResultLog",) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/logs/command.py0000644000175100001710000000063600000000000017243 0ustar00vstsdocker00000000000000from __future__ import absolute_import, unicode_literals class CommandLog(object): """Report commands interacting with third party tools""" def __init__(self, env_log, list): self.envlog = env_log self.list = list def add_command(self, argv, output, retcode): data = {"command": argv, "output": output, "retcode": retcode} self.list.append(data) return data ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/logs/env.py0000644000175100001710000000210000000000000016401 0ustar00vstsdocker00000000000000from __future__ import absolute_import, unicode_literals from tox.interpreters.via_path import get_python_info from .command import CommandLog class EnvLog(object): """Report the status of a tox environment""" def __init__(self, result_log, name, dict): self.reportlog = result_log self.name = name self.dict = dict def set_python_info(self, python_executable): answer = get_python_info(str(python_executable)) answer["executable"] = python_executable self.dict["python"] = answer def get_commandlog(self, name): """get the command log for a given group name""" data = self.dict.setdefault(name, []) return CommandLog(self, data) def set_installed(self, packages): self.dict["installed_packages"] = packages def set_header(self, installpkg): """ :param py.path.local installpkg: Path ot the package. """ self.dict["installpkg"] = { "sha256": installpkg.computehash("sha256"), "basename": installpkg.basename, } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/logs/result.py0000644000175100001710000000235000000000000017136 0ustar00vstsdocker00000000000000"""Generate json report of a run""" from __future__ import absolute_import, unicode_literals import json import os import socket import sys from tox.version import __version__ from .command import CommandLog from .env import EnvLog class ResultLog(object): """The result of a tox session""" def __init__(self): command_log = [] self.command_log = CommandLog(None, command_log) self.dict = { "reportversion": "1", "toxversion": __version__, "platform": sys.platform, "host": os.getenv(str("HOSTNAME")) or socket.getfqdn(), "commands": command_log, } @classmethod def from_json(cls, data): result = cls() result.dict = json.loads(data) result.command_log = CommandLog(None, result.dict["commands"]) return result def get_envlog(self, name): """Return the env log of an environment (create on first call)""" test_envs = self.dict.setdefault("testenvs", {}) env_data = test_envs.setdefault(name, {}) return EnvLog(self, name, env_data) def dumps_json(self): """Return the json dump of the current state, indented""" return json.dumps(self.dict, indent=2) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.907512 tox-3.21.4/src/tox/package/0000755000175100001710000000000000000000000015675 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/package/__init__.py0000644000175100001710000000500400000000000020005 0ustar00vstsdocker00000000000000import py import tox from tox.reporter import error, info, verbosity0, verbosity2, warning from tox.util.lock import hold_lock from .builder import build_package from .local import resolve_package from .view import create_session_view @tox.hookimpl def tox_package(session, venv): """Build an sdist at first call return that for all calls""" if not hasattr(session, "package"): session.package, session.dist = get_package(session) return session.package def get_package(session): """"Perform the package operation""" config = session.config if config.skipsdist: info("skipping sdist step") return None lock_file = session.config.toxworkdir.join("{}.lock".format(session.config.isolated_build_env)) with hold_lock(lock_file, verbosity0): package = acquire_package(config, session) session_package = create_session_view(package, config.temp_dir) return session_package, package def acquire_package(config, session): """acquire a source distribution (either by loading a local file or triggering a build)""" if not config.option.sdistonly and (config.sdistsrc or config.option.installpkg): path = get_local_package(config) else: try: path = build_package(config, session) except tox.exception.InvocationError as exception: error("FAIL could not package project - v = {!r}".format(exception)) return None sdist_file = config.distshare.join(path.basename) if sdist_file != path: info("copying new sdistfile to {!r}".format(str(sdist_file))) try: sdist_file.dirpath().ensure(dir=1) except py.error.Error: warning("could not copy distfile to {}".format(sdist_file.dirpath())) else: path.copy(sdist_file) return path def get_local_package(config): path = config.option.installpkg if not path: path = config.sdistsrc py_path = py.path.local(resolve_package(path)) info("using package {!r}, skipping 'sdist' activity ".format(str(py_path))) return py_path @tox.hookimpl def tox_cleanup(session): for tox_env in session.venv_dict.values(): if hasattr(tox_env, "package") and isinstance(tox_env.package, py.path.local): package = tox_env.package if package.exists(): verbosity2("cleanup {}".format(package)) package.remove() py.path.local(package.dirname).remove(ignore_errors=True) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.907512 tox-3.21.4/src/tox/package/builder/0000755000175100001710000000000000000000000017323 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/package/builder/__init__.py0000644000175100001710000000033600000000000021436 0ustar00vstsdocker00000000000000from .isolated import build from .legacy import make_sdist def build_package(config, session): if not config.isolated_build: return make_sdist(config, session) else: return build(config, session) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/package/builder/isolated.py0000644000175100001710000001243300000000000021504 0ustar00vstsdocker00000000000000from __future__ import unicode_literals import json import os from collections import namedtuple import six from packaging.requirements import Requirement from packaging.utils import canonicalize_name from tox import reporter from tox.config import DepConfig, get_py_project_toml from tox.constants import BUILD_ISOLATED, BUILD_REQUIRE_SCRIPT BuildInfo = namedtuple( "BuildInfo", ["requires", "backend_module", "backend_object", "backend_paths"], ) def build(config, session): build_info = get_build_info(config.setupdir) package_venv = session.getvenv(config.isolated_build_env) package_venv.envconfig.deps_matches_subset = True # we allow user specified dependencies so the users can write extensions to # install additional type of dependencies (e.g. binary) user_specified_deps = package_venv.envconfig.deps package_venv.envconfig.deps = [DepConfig(r, None) for r in build_info.requires] package_venv.envconfig.deps.extend(user_specified_deps) if package_venv.setupenv(): package_venv.finishvenv() if isinstance(package_venv.status, Exception): raise package_venv.status build_requires = get_build_requires(build_info, package_venv, config.setupdir) # we need to filter out requirements already specified in pyproject.toml or user deps base_build_deps = { canonicalize_name(Requirement(r.name).name) for r in package_venv.envconfig.deps } build_requires_dep = [ DepConfig(r, None) for r in build_requires if canonicalize_name(Requirement(r).name) not in base_build_deps ] if build_requires_dep: with package_venv.new_action("build_requires", package_venv.envconfig.envdir) as action: package_venv.run_install_command(packages=build_requires_dep, action=action) package_venv.finishvenv() return perform_isolated_build(build_info, package_venv, config.distdir, config.setupdir) def get_build_info(folder): toml_file = folder.join("pyproject.toml") # as per https://www.python.org/dev/peps/pep-0517/ def abort(message): reporter.error("{} inside {}".format(message, toml_file)) raise SystemExit(1) if not toml_file.exists(): reporter.error("missing {}".format(toml_file)) raise SystemExit(1) config_data = get_py_project_toml(toml_file) if "build-system" not in config_data: abort("build-system section missing") build_system = config_data["build-system"] if "requires" not in build_system: abort("missing requires key at build-system section") if "build-backend" not in build_system: abort("missing build-backend key at build-system section") requires = build_system["requires"] if not isinstance(requires, list) or not all(isinstance(i, six.text_type) for i in requires): abort("requires key at build-system section must be a list of string") backend = build_system["build-backend"] if not isinstance(backend, six.text_type): abort("build-backend key at build-system section must be a string") args = backend.split(":") module = args[0] obj = args[1] if len(args) > 1 else "" backend_paths = build_system.get("backend-path", []) if not isinstance(backend_paths, list): abort("backend-path key at build-system section must be a list, if specified") backend_paths = [folder.join(p) for p in backend_paths] normalized_folder = os.path.normcase(str(folder.realpath())) normalized_paths = (os.path.normcase(str(path.realpath())) for path in backend_paths) if not all( os.path.commonprefix((normalized_folder, path)) == normalized_folder for path in normalized_paths ): abort("backend-path must exist in the project root") return BuildInfo(requires, module, obj, backend_paths) def perform_isolated_build(build_info, package_venv, dist_dir, setup_dir): with package_venv.new_action( "perform-isolated-build", package_venv.envconfig.envdir, ) as action: # need to start with an empty (but existing) source distribution folder if dist_dir.exists(): dist_dir.remove(rec=1, ignore_errors=True) dist_dir.ensure_dir() result = package_venv._pcall( [ package_venv.envconfig.envpython, BUILD_ISOLATED, str(dist_dir), build_info.backend_module, build_info.backend_object, os.path.pathsep.join(str(p) for p in build_info.backend_paths), ], returnout=True, action=action, cwd=setup_dir, ) reporter.verbosity2(result) return dist_dir.join(result.split("\n")[-2]) def get_build_requires(build_info, package_venv, setup_dir): with package_venv.new_action("get-build-requires", package_venv.envconfig.envdir) as action: result = package_venv._pcall( [ package_venv.envconfig.envpython, BUILD_REQUIRE_SCRIPT, build_info.backend_module, build_info.backend_object, os.path.pathsep.join(str(p) for p in build_info.backend_paths), ], returnout=True, action=action, cwd=setup_dir, ) return json.loads(result.split("\n")[-2]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/package/builder/legacy.py0000644000175100001710000000432500000000000021145 0ustar00vstsdocker00000000000000import sys import py from tox import reporter from tox.util.path import ensure_empty_dir def make_sdist(config, session): setup = config.setupdir.join("setup.py") pyproject = config.setupdir.join("pyproject.toml") setup_check = setup.check() if not setup_check and not pyproject.check(): reporter.error( "No pyproject.toml or setup.py file found. The expected locations are:\n" " {pyproject} or {setup}\n" "You can\n" " 1. Create one:\n" " https://tox.readthedocs.io/en/latest/example/package.html\n" " 2. Configure tox to avoid running sdist:\n" " https://tox.readthedocs.io/en/latest/example/general.html\n" " 3. Configure tox to use an isolated_build".format(pyproject=pyproject, setup=setup), ) raise SystemExit(1) if not setup_check: reporter.error( "pyproject.toml file found.\n" "To use a PEP 517 build-backend you are required to " "configure tox to use an isolated_build:\n" "https://tox.readthedocs.io/en/latest/example/package.html\n", ) raise SystemExit(1) with session.newaction("GLOB", "packaging") as action: action.setactivity("sdist-make", setup) ensure_empty_dir(config.distdir) build_log = action.popen( [sys.executable, setup, "sdist", "--formats=zip", "--dist-dir", config.distdir], cwd=config.setupdir, returnout=True, ) reporter.verbosity2(build_log) try: return config.distdir.listdir()[0] except py.error.ENOENT: # check if empty or comment only data = [] with open(str(setup)) as fp: for line in fp: if line and line[0] == "#": continue data.append(line) if not "".join(data).strip(): reporter.error("setup.py is empty") raise SystemExit(1) reporter.error( "No dist directory found. Please check setup.py, e.g with:\n" " python setup.py sdist", ) raise SystemExit(1) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/package/local.py0000644000175100001710000000351400000000000017344 0ustar00vstsdocker00000000000000import os import re import packaging.version import py import tox from tox import reporter from tox.exception import MissingDependency _SPEC_2_PACKAGE = {} def resolve_package(package_spec): global _SPEC_2_PACKAGE try: return _SPEC_2_PACKAGE[package_spec] except KeyError: _SPEC_2_PACKAGE[package_spec] = x = get_latest_version_of_package(package_spec) return x def get_latest_version_of_package(package_spec): if not os.path.isabs(str(package_spec)): return package_spec p = py.path.local(package_spec) if p.check(): return p if not p.dirpath().check(dir=1): raise tox.exception.MissingDirectory(p.dirpath()) reporter.info("determining {}".format(p)) candidates = p.dirpath().listdir(p.basename) if len(candidates) == 0: raise MissingDependency(package_spec) if len(candidates) > 1: version_package = [] for filename in candidates: version = get_version_from_filename(filename.basename) if version is not None: version_package.append((version, filename)) else: reporter.warning("could not determine version of: {}".format(str(filename))) if not version_package: raise tox.exception.MissingDependency(package_spec) version_package.sort() _, package_with_largest_version = version_package[-1] return package_with_largest_version else: return candidates[0] _REGEX_FILE_NAME_WITH_VERSION = re.compile(r"[\w_\-\+\.]+-(.*)\.(zip|tar\.gz)") def get_version_from_filename(basename): m = _REGEX_FILE_NAME_WITH_VERSION.match(basename) if m is None: return None version = m.group(1) try: return packaging.version.Version(version) except packaging.version.InvalidVersion: return None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/package/view.py0000644000175100001710000000322700000000000017225 0ustar00vstsdocker00000000000000import os from itertools import chain import six from tox.reporter import verbosity1 def create_session_view(package, temp_dir): """once we build a package we cannot return that directly, as a subsequent call might delete that package (in order to do its own build); therefore we need to return a view of the file that it's not prone to deletion and can be removed when the session ends """ if not package: return package package_dir = temp_dir.join("package") package_dir.ensure(dir=True) # we'll number the active instances, and use the max value as session folder for a new build # note we cannot change package names as PEP-491 (wheel binary format) # is strict about file name structure exists = [i.basename for i in package_dir.listdir()] file_id = max(chain((0,), (int(i) for i in exists if six.text_type(i).isnumeric()))) session_dir = package_dir.join(str(file_id + 1)) session_dir.ensure(dir=True) session_package = session_dir.join(package.basename) # if we can do hard links do that, otherwise just copy links = False if hasattr(os, "link"): try: os.link(str(package), str(session_package)) links = True except (OSError, NotImplementedError): pass if not links: package.copy(session_package) operation = "links" if links else "copied" common = session_package.common(package) verbosity1( "package {} {} to {} ({})".format( common.bestrelpath(session_package), operation, common.bestrelpath(package), common, ), ) return session_package ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/reporter.py0000644000175100001710000001101300000000000016512 0ustar00vstsdocker00000000000000"""A progress reporter inspired from the logging modules""" from __future__ import absolute_import, unicode_literals import os import time from contextlib import contextmanager from datetime import datetime import py class Verbosity(object): DEBUG = 2 INFO = 1 DEFAULT = 0 QUIET = -1 EXTRA_QUIET = -2 REPORTER_TIMESTAMP_ON_ENV = str("TOX_REPORTER_TIMESTAMP") REPORTER_TIMESTAMP_ON = os.environ.get(REPORTER_TIMESTAMP_ON_ENV, False) == "1" START = datetime.now() class Reporter(object): def __init__(self, verbose_level=None, quiet_level=None): kwargs = {} if verbose_level is not None: kwargs["verbose_level"] = verbose_level if quiet_level is not None: kwargs["quiet_level"] = quiet_level self._reset(**kwargs) def _reset(self, verbose_level=0, quiet_level=0): self.verbose_level = verbose_level self.quiet_level = quiet_level self.reported_lines = [] self.tw = py.io.TerminalWriter() @property def verbosity(self): return self.verbose_level - self.quiet_level def log_popen(self, cwd, outpath, cmd_args_shell, pid): """ log information about the action.popen() created process. """ msg = "[{}] {}$ {}".format(pid, cwd, cmd_args_shell) if outpath: if outpath.common(cwd) is not None: outpath = cwd.bestrelpath(outpath) msg = "{} >{}".format(msg, outpath) self.verbosity1(msg, of="logpopen") @property def messages(self): return [i for _, i in self.reported_lines] @contextmanager def timed_operation(self, name, msg): self.verbosity2("{} start: {}".format(name, msg), bold=True) start = time.time() yield duration = time.time() - start self.verbosity2( "{} finish: {} after {:.2f} seconds".format(name, msg, duration), bold=True, ) def separator(self, of, msg, level): if self.verbosity >= level: self.reported_lines.append(("separator", "- summary -")) self.tw.sep(of, msg) def logline_if(self, level, of, msg, key=None, **kwargs): if self.verbosity >= level: message = str(msg) if key is None else "{}{}".format(key, msg) self.logline(of, message, **kwargs) def logline(self, of, msg, **opts): self.reported_lines.append((of, msg)) timestamp = "" if REPORTER_TIMESTAMP_ON: timestamp = "{} ".format(datetime.now() - START) line_msg = "{}{}\n".format(timestamp, msg) self.tw.write(line_msg, **opts) def keyvalue(self, name, value): if name.endswith(":"): name += " " self.tw.write(name, bold=True) self.tw.write(value) self.tw.line() def line(self, msg, **opts): self.logline("line", msg, **opts) def info(self, msg): self.logline_if(Verbosity.DEBUG, "info", msg) def using(self, msg): self.logline_if(Verbosity.INFO, "using", msg, "using ", bold=True) def good(self, msg): self.logline_if(Verbosity.QUIET, "good", msg, green=True) def warning(self, msg): self.logline_if(Verbosity.QUIET, "warning", msg, "WARNING: ", red=True) def error(self, msg): self.logline_if(Verbosity.QUIET, "error", msg, "ERROR: ", red=True) def skip(self, msg): self.logline_if(Verbosity.QUIET, "skip", msg, "SKIPPED: ", yellow=True) def verbosity0(self, msg, **opts): self.logline_if(Verbosity.DEFAULT, "verbosity0", msg, **opts) def verbosity1(self, msg, of="verbosity1", **opts): self.logline_if(Verbosity.INFO, of, msg, **opts) def verbosity2(self, msg, **opts): self.logline_if(Verbosity.DEBUG, "verbosity2", msg, **opts) def quiet(self, msg): self.logline_if(Verbosity.QUIET, "quiet", msg) _INSTANCE = Reporter() def update_default_reporter(quiet_level, verbose_level): _INSTANCE.quiet_level = quiet_level _INSTANCE.verbose_level = verbose_level def has_level(of): return _INSTANCE.verbosity > of def verbosity(): return _INSTANCE.verbosity verbosity0 = _INSTANCE.verbosity0 verbosity1 = _INSTANCE.verbosity1 verbosity2 = _INSTANCE.verbosity2 error = _INSTANCE.error warning = _INSTANCE.warning good = _INSTANCE.good using = _INSTANCE.using skip = _INSTANCE.skip info = _INSTANCE.info line = _INSTANCE.line separator = _INSTANCE.separator keyvalue = _INSTANCE.keyvalue quiet = _INSTANCE.quiet timed_operation = _INSTANCE.timed_operation log_popen = _INSTANCE.log_popen ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.907512 tox-3.21.4/src/tox/session/0000755000175100001710000000000000000000000015765 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/session/__init__.py0000644000175100001710000002535100000000000020104 0ustar00vstsdocker00000000000000""" Automatically package and test a Python project against configurable Python2 and Python3 based virtual environments. Environments are setup by using virtualenv. Configuration is generally done through an INI-style "tox.ini" file. """ from __future__ import absolute_import, unicode_literals import json import os import re import subprocess import sys from collections import OrderedDict from contextlib import contextmanager import py import tox from tox import reporter from tox.action import Action from tox.config import INTERRUPT_TIMEOUT, SUICIDE_TIMEOUT, TERMINATE_TIMEOUT, parseconfig from tox.config.parallel import ENV_VAR_KEY_PRIVATE as PARALLEL_ENV_VAR_KEY_PRIVATE from tox.config.parallel import OFF_VALUE as PARALLEL_OFF from tox.logs.result import ResultLog from tox.reporter import update_default_reporter from tox.util import set_os_env_var from tox.util.graph import stable_topological_sort from tox.util.stdlib import suppress_output from tox.venv import VirtualEnv from .commands.help import show_help from .commands.help_ini import show_help_ini from .commands.provision import provision_tox from .commands.run.parallel import run_parallel from .commands.run.sequential import run_sequential from .commands.show_config import show_config from .commands.show_env import show_envs def cmdline(args=None): if args is None: args = sys.argv[1:] main(args) def setup_reporter(args): from argparse import ArgumentParser from tox.config.reporter import add_verbosity_commands parser = ArgumentParser(add_help=False) add_verbosity_commands(parser) with suppress_output(): try: options, _ = parser.parse_known_args(args) update_default_reporter(options.quiet_level, options.verbose_level) except SystemExit: pass def main(args): setup_reporter(args) try: config = load_config(args) config.logdir.ensure(dir=1) with set_os_env_var(str("TOX_WORK_DIR"), config.toxworkdir): session = build_session(config) exit_code = session.runcommand() if exit_code is None: exit_code = 0 raise SystemExit(exit_code) except tox.exception.BadRequirement: raise SystemExit(1) except KeyboardInterrupt: raise SystemExit(2) def load_config(args): try: config = parseconfig(args) if config.option.help: show_help(config) raise SystemExit(0) elif config.option.helpini: show_help_ini(config) raise SystemExit(0) except tox.exception.MissingRequirement as exception: config = exception.config return config def build_session(config): return Session(config) class Session(object): """The session object that ties together configuration, reporting, venv creation, testing.""" def __init__(self, config, popen=subprocess.Popen): self._reset(config, popen) def _reset(self, config, popen=subprocess.Popen): self.config = config self.popen = popen self.resultlog = ResultLog() self.existing_venvs = OrderedDict() self.venv_dict = {} if self.config.run_provision else self._build_venvs() def _build_venvs(self): try: need_to_run = OrderedDict((v, self.getvenv(v)) for v in self._evaluated_env_list) try: venv_order = stable_topological_sort( OrderedDict((name, v.envconfig.depends) for name, v in need_to_run.items()), ) venvs = OrderedDict((v, need_to_run[v]) for v in venv_order) return venvs except ValueError as exception: reporter.error("circular dependency detected: {}".format(exception)) except LookupError: pass except tox.exception.ConfigError as exception: reporter.error(str(exception)) raise SystemExit(1) def getvenv(self, name): if name in self.existing_venvs: return self.existing_venvs[name] env_config = self.config.envconfigs.get(name, None) if env_config is None: reporter.error("unknown environment {!r}".format(name)) raise LookupError(name) elif env_config.envdir == self.config.toxinidir: reporter.error("venv {!r} in {} would delete project".format(name, env_config.envdir)) raise tox.exception.ConfigError("envdir must not equal toxinidir") env_log = self.resultlog.get_envlog(name) venv = VirtualEnv(envconfig=env_config, popen=self.popen, env_log=env_log) self.existing_venvs[name] = venv return venv @property def _evaluated_env_list(self): tox_env_filter = os.environ.get("TOX_SKIP_ENV") tox_env_filter_re = re.compile(tox_env_filter) if tox_env_filter is not None else None visited = set() for name in self.config.envlist: if name in visited: continue visited.add(name) if tox_env_filter_re is not None and tox_env_filter_re.match(name): msg = "skip environment {}, matches filter {!r}".format( name, tox_env_filter_re.pattern, ) reporter.verbosity1(msg) continue yield name @property def hook(self): return self.config.pluginmanager.hook def newaction(self, name, msg, *args): return Action( name, msg, args, self.config.logdir, self.config.option.resultjson, self.resultlog.command_log, self.popen, sys.executable, SUICIDE_TIMEOUT, INTERRUPT_TIMEOUT, TERMINATE_TIMEOUT, ) def runcommand(self): reporter.using( "tox-{} from {} (pid {})".format(tox.__version__, tox.__file__, os.getpid()), ) show_description = reporter.has_level(reporter.Verbosity.DEFAULT) if self.config.run_provision: provision_tox_venv = self.getvenv(self.config.provision_tox_env) return provision_tox(provision_tox_venv, self.config.args) else: if self.config.option.showconfig: self.showconfig() elif self.config.option.listenvs: self.showenvs(all_envs=False, description=show_description) elif self.config.option.listenvs_all: self.showenvs(all_envs=True, description=show_description) else: with self.cleanup(): return self.subcommand_test() @contextmanager def cleanup(self): self.config.temp_dir.ensure(dir=True) try: yield finally: self.hook.tox_cleanup(session=self) def subcommand_test(self): if self.config.skipsdist: reporter.info("skipping sdist step") else: for venv in self.venv_dict.values(): if not venv.envconfig.skip_install: venv.package = self.hook.tox_package(session=self, venv=venv) if not venv.package: return 2 venv.envconfig.setenv[str("TOX_PACKAGE")] = str(venv.package) if self.config.option.sdistonly: return within_parallel = PARALLEL_ENV_VAR_KEY_PRIVATE in os.environ try: if not within_parallel and self.config.option.parallel != PARALLEL_OFF: run_parallel(self.config, self.venv_dict) else: run_sequential(self.config, self.venv_dict) finally: retcode = self._summary() return retcode def _add_parallel_summaries(self): if self.config.option.parallel != PARALLEL_OFF and "testenvs" in self.resultlog.dict: result_log = self.resultlog.dict["testenvs"] for tox_env in self.venv_dict.values(): data = self._load_parallel_env_report(tox_env) if data and "testenvs" in data and tox_env.name in data["testenvs"]: result_log[tox_env.name] = data["testenvs"][tox_env.name] @staticmethod def _load_parallel_env_report(tox_env): """Load report data into memory, remove disk file""" result_json_path = tox_env.get_result_json_path() if result_json_path and result_json_path.exists(): with result_json_path.open("r") as file_handler: data = json.load(file_handler) result_json_path.remove() return data def _summary(self): is_parallel_child = PARALLEL_ENV_VAR_KEY_PRIVATE in os.environ if not is_parallel_child: reporter.separator("_", "summary", reporter.Verbosity.QUIET) exit_code = 0 for venv in self.venv_dict.values(): report = reporter.good status = getattr(venv, "status", "undefined") if isinstance(status, tox.exception.InterpreterNotFound): msg = " {}: {}".format(venv.envconfig.envname, str(status)) if self.config.option.skip_missing_interpreters == "true": report = reporter.skip else: exit_code = 1 report = reporter.error elif status == "platform mismatch": msg = " {}: {} ({!r} does not match {!r})".format( venv.envconfig.envname, str(status), sys.platform, venv.envconfig.platform, ) report = reporter.skip elif status and status == "ignored failed command": msg = " {}: {}".format(venv.envconfig.envname, str(status)) elif status and status != "skipped tests": msg = " {}: {}".format(venv.envconfig.envname, str(status)) report = reporter.error exit_code = 1 else: if not status: status = "commands succeeded" msg = " {}: {}".format(venv.envconfig.envname, status) if not is_parallel_child: report(msg) if not exit_code and not is_parallel_child: reporter.good(" congratulations :)") path = self.config.option.resultjson if path: if not is_parallel_child: self._add_parallel_summaries() path = py.path.local(path) data = self.resultlog.dumps_json() reporter.line("write json report at: {}".format(path)) path.write(data) return exit_code def showconfig(self): show_config(self.config) def showenvs(self, all_envs=False, description=False): show_envs(self.config, all_envs=all_envs, description=description) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.907512 tox-3.21.4/src/tox/session/commands/0000755000175100001710000000000000000000000017566 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/session/commands/__init__.py0000644000175100001710000000000000000000000021665 0ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/session/commands/help.py0000644000175100001710000000124200000000000021067 0ustar00vstsdocker00000000000000from tox import reporter def show_help(config): reporter.line(config._parser._format_help()) reporter.line("Environment variables", bold=True) reporter.line("TOXENV: comma separated list of environments (overridable by '-e')") reporter.line("TOX_SKIP_ENV: regular expression to filter down from running tox environments") reporter.line( "TOX_TESTENV_PASSENV: space-separated list of extra environment variables to be " "passed into test command environments", ) reporter.line("PY_COLORS: 0 disable colorized output, 1 enable (default)") reporter.line("TOX_PARALLEL_NO_SPINNER: 1 disable spinner for CI, 0 enable (default)") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/session/commands/help_ini.py0000644000175100001710000000072300000000000021731 0ustar00vstsdocker00000000000000from tox import reporter def show_help_ini(config): reporter.separator("-", "per-testenv attributes", reporter.Verbosity.INFO) for env_attr in config._testenv_attr: reporter.line( "{:<15} {:<8} default: {}".format( env_attr.name, "<{}>".format(env_attr.type), env_attr.default, ), bold=True, ) reporter.line(env_attr.help) reporter.line("") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/session/commands/provision.py0000644000175100001710000000160100000000000022166 0ustar00vstsdocker00000000000000"""In case the tox environment is not correctly setup provision it and delegate execution""" from __future__ import absolute_import, unicode_literals import os from tox.exception import InvocationError def provision_tox(provision_venv, args): ensure_meta_env_up_to_date(provision_venv) with provision_venv.new_action("provision") as action: provision_args = [str(provision_venv.envconfig.envpython), "-m", "tox"] + args try: env = os.environ.copy() env[str("TOX_PROVISION")] = str("1") env.pop("__PYVENV_LAUNCHER__", None) action.popen(provision_args, redirect=False, report_fail=False, env=env) return 0 except InvocationError as exception: return exception.exit_code def ensure_meta_env_up_to_date(provision_venv): if provision_venv.setupenv(): provision_venv.finishvenv() ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.907512 tox-3.21.4/src/tox/session/commands/run/0000755000175100001710000000000000000000000020372 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/session/commands/run/__init__.py0000644000175100001710000000000000000000000022471 0ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/session/commands/run/parallel.py0000644000175100001710000001275600000000000022553 0ustar00vstsdocker00000000000000import os import sys from collections import OrderedDict, deque from threading import Event, Semaphore, Thread from tox import reporter from tox.config.parallel import ENV_VAR_KEY_PRIVATE as PARALLEL_ENV_VAR_KEY_PRIVATE from tox.config.parallel import ENV_VAR_KEY_PUBLIC as PARALLEL_ENV_VAR_KEY_PUBLIC from tox.exception import InvocationError from tox.util.main import MAIN_FILE from tox.util.spinner import Spinner def run_parallel(config, venv_dict): """here we'll just start parallel sub-processes""" live_out = config.option.parallel_live disable_spinner = bool(os.environ.get("TOX_PARALLEL_NO_SPINNER") == "1") args = [sys.executable, MAIN_FILE] + config.args try: position = args.index("--") except ValueError: position = len(args) max_parallel = config.option.parallel if max_parallel is None: max_parallel = len(venv_dict) semaphore = Semaphore(max_parallel) finished = Event() show_progress = ( not disable_spinner and not live_out and reporter.verbosity() > reporter.Verbosity.QUIET ) with Spinner(enabled=show_progress) as spinner: def run_in_thread(tox_env, os_env, processes): output = None print_out = None env_name = tox_env.envconfig.envname status = "skipped tests" if config.option.notest else None try: os_env[str(PARALLEL_ENV_VAR_KEY_PRIVATE)] = str(env_name) os_env[str(PARALLEL_ENV_VAR_KEY_PUBLIC)] = str(env_name) args_sub = list(args) if hasattr(tox_env, "package"): args_sub.insert(position, str(tox_env.package)) args_sub.insert(position, "--installpkg") if tox_env.get_result_json_path(): result_json_index = args_sub.index("--result-json") args_sub[result_json_index + 1] = "{}".format(tox_env.get_result_json_path()) with tox_env.new_action("parallel {}".format(tox_env.name)) as action: def collect_process(process): processes[tox_env] = (action, process) print_out = not live_out and tox_env.envconfig.parallel_show_output output = action.popen( args=args_sub, env=os_env, redirect=not live_out, capture_err=print_out, callback=collect_process, returnout=print_out, ) except InvocationError as err: status = "parallel child exit code {}".format(err.exit_code) finally: semaphore.release() finished.set() tox_env.status = status done.add(env_name) outcome = spinner.succeed if config.option.notest: outcome = spinner.skip elif status is not None: outcome = spinner.fail outcome(env_name) if print_out and output is not None: reporter.verbosity0(output) threads = deque() processes = {} todo_keys = set(venv_dict.keys()) todo = OrderedDict((n, todo_keys & set(v.envconfig.depends)) for n, v in venv_dict.items()) done = set() try: while todo: for name, depends in list(todo.items()): if depends - done: # skip if has unfinished dependencies continue del todo[name] venv = venv_dict[name] semaphore.acquire(blocking=True) spinner.add(name) thread = Thread( target=run_in_thread, args=(venv, os.environ.copy(), processes), ) thread.daemon = True thread.start() threads.append(thread) if todo: # wait until someone finishes and retry queuing jobs finished.wait() finished.clear() while threads: threads = [ thread for thread in threads if not thread.join(0.1) and thread.is_alive() ] except KeyboardInterrupt: reporter.verbosity0( "[{}] KeyboardInterrupt parallel - stopping children".format(os.getpid()), ) while True: # do not allow to interrupt until children interrupt try: # putting it inside a thread so it's not interrupted stopper = Thread(target=_stop_child_processes, args=(processes, threads)) stopper.start() stopper.join() except KeyboardInterrupt: continue raise KeyboardInterrupt def _stop_child_processes(processes, main_threads): """A three level stop mechanism for children - INT (250ms) -> TERM (100ms) -> KILL""" # first stop children def shutdown(tox_env, action, process): action.handle_interrupt(process) threads = [Thread(target=shutdown, args=(n, a, p)) for n, (a, p) in processes.items()] for thread in threads: thread.start() for thread in threads: thread.join() # then its threads for thread in main_threads: thread.join() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/session/commands/run/sequential.py0000644000175100001710000000457700000000000023133 0ustar00vstsdocker00000000000000import py import tox from tox.exception import InvocationError def run_sequential(config, venv_dict): for venv in venv_dict.values(): if venv.setupenv(): if venv.envconfig.skip_install: venv.finishvenv() else: if venv.envconfig.usedevelop: develop_pkg(venv, config.setupdir) elif config.skipsdist: venv.finishvenv() else: installpkg(venv, venv.package) if venv.status == 0: runenvreport(venv, config) if venv.status == 0: runtestenv(venv, config) def develop_pkg(venv, setupdir): with venv.new_action("developpkg", setupdir) as action: try: venv.developpkg(setupdir, action) return True except InvocationError as exception: venv.status = exception return False def installpkg(venv, path): """Install package in the specified virtual environment. :param VenvConfig venv: Destination environment :param str path: Path to the distribution package. :return: True if package installed otherwise False. :rtype: bool """ venv.env_log.set_header(installpkg=py.path.local(path)) with venv.new_action("installpkg", path) as action: try: venv.installpkg(path, action) return True except tox.exception.InvocationError as exception: venv.status = exception return False def runenvreport(venv, config): """ Run an environment report to show which package versions are installed in the venv """ try: with venv.new_action("envreport") as action: packages = config.pluginmanager.hook.tox_runenvreport(venv=venv, action=action) action.setactivity("installed", ",".join(packages)) venv.env_log.set_installed(packages) except InvocationError as exception: venv.status = exception def runtestenv(venv, config, redirect=False): if venv.status == 0 and config.option.notest: venv.status = "skipped tests" else: if venv.status: return config.pluginmanager.hook.tox_runtest_pre(venv=venv) if venv.status == 0: config.pluginmanager.hook.tox_runtest(venv=venv, redirect=redirect) config.pluginmanager.hook.tox_runtest_post(venv=venv) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/session/commands/show_config.py0000644000175100001710000000475300000000000022456 0ustar00vstsdocker00000000000000import sys from collections import OrderedDict from packaging.requirements import Requirement from packaging.utils import canonicalize_name from six import StringIO from six.moves import configparser from tox import reporter from tox.util.stdlib import importlib_metadata DO_NOT_SHOW_CONFIG_ATTRIBUTES = ( "interpreters", "envconfigs", "envlist", "pluginmanager", "envlist_explicit", ) def show_config(config): parser = configparser.RawConfigParser() if not config.envlist_explicit or reporter.verbosity() >= reporter.Verbosity.INFO: tox_info(config, parser) version_info(parser) tox_envs_info(config, parser) content = StringIO() parser.write(content) value = content.getvalue().rstrip() reporter.verbosity0(value) def tox_envs_info(config, parser): if config.envlist_explicit: env_list = config.envlist elif config.option.listenvs: env_list = config.envlist_default else: env_list = list(config.envconfigs.keys()) for name in env_list: env_config = config.envconfigs[name] values = OrderedDict( (attr.name, str(getattr(env_config, attr.name))) for attr in config._parser._testenv_attr ) section = "testenv:{}".format(name) set_section(parser, section, values) def tox_info(config, parser): info = OrderedDict( (i, str(getattr(config, i))) for i in sorted(dir(config)) if not i.startswith("_") and i not in DO_NOT_SHOW_CONFIG_ATTRIBUTES ) info["host_python"] = sys.executable set_section(parser, "tox", info) def version_info(parser): versions = OrderedDict() to_visit = {"tox"} while to_visit: current = to_visit.pop() current_dist = importlib_metadata.distribution(current) current_name = canonicalize_name(current_dist.metadata["name"]) versions[current_name] = current_dist.version if current_dist.requires is not None: for require in current_dist.requires: pkg = Requirement(require) pkg_name = canonicalize_name(pkg.name) if ( pkg.marker is None or pkg.marker.evaluate({"extra": ""}) ) and pkg_name not in versions: to_visit.add(pkg_name) set_section(parser, "tox:versions", versions) def set_section(parser, section, values): parser.add_section(section) for key, value in values.items(): parser.set(section, key, value) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/session/commands/show_env.py0000644000175100001710000000207100000000000021770 0ustar00vstsdocker00000000000000from __future__ import absolute_import, unicode_literals from tox import reporter as report def show_envs(config, all_envs=False, description=False): env_conf = config.envconfigs # this contains all environments default = config.envlist_default # this only the defaults ignore = {config.isolated_build_env, config.provision_tox_env}.union(default) extra = [e for e in env_conf if e not in ignore] if all_envs else [] if description and default: report.line("default environments:") max_length = max(len(env) for env in (default + extra) or [""]) def report_env(e): if description: text = env_conf[e].description or "[no description]" msg = "{} -> {}".format(e.ljust(max_length), text).strip() else: msg = e report.line(msg) for e in default: report_env(e) if all_envs and extra: if description: if default: report.line("") report.line("additional environments:") for e in extra: report_env(e) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.907512 tox-3.21.4/src/tox/util/0000755000175100001710000000000000000000000015257 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/util/__init__.py0000644000175100001710000000077200000000000017376 0ustar00vstsdocker00000000000000from __future__ import absolute_import, unicode_literals import os from contextlib import contextmanager @contextmanager def set_os_env_var(env_var_name, value): """Set an environment variable with unrolling once the context exists""" prev_value = os.environ.get(env_var_name) try: os.environ[env_var_name] = str(value) yield finally: if prev_value is None: del os.environ[env_var_name] else: os.environ[env_var_name] = prev_value ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/util/graph.py0000644000175100001710000000422700000000000016737 0ustar00vstsdocker00000000000000from __future__ import absolute_import, unicode_literals from collections import OrderedDict, defaultdict def stable_topological_sort(graph): to_order = set(graph.keys()) # keep a log of what we need to order # normalize graph - fill missing nodes (assume no dependency) for values in list(graph.values()): for value in values: if value not in graph: graph[value] = () inverse_graph = defaultdict(set) for key, depends in graph.items(): for depend in depends: inverse_graph[depend].add(key) topology = [] degree = {k: len(v) for k, v in graph.items()} ready_to_visit = {n for n, d in degree.items() if not d} need_to_visit = OrderedDict((i, None) for i in graph.keys()) while need_to_visit: # to keep stable, pick the first node ready to visit in the original order for node in need_to_visit: if node in ready_to_visit: break else: break del need_to_visit[node] topology.append(node) # decrease degree for nodes we're going too for to_node in inverse_graph[node]: degree[to_node] -= 1 if not degree[to_node]: # if a node has no more incoming node it's ready to visit ready_to_visit.add(to_node) result = [n for n in topology if n in to_order] # filter out missing nodes we extended if len(result) < len(to_order): identify_cycle(graph) msg = "could not order tox environments and failed to detect circle" # pragma: no cover raise ValueError(msg) # pragma: no cover return result def identify_cycle(graph): path = OrderedDict() visited = set() def visit(vertex): if vertex in visited: return None visited.add(vertex) path[vertex] = None for neighbour in graph.get(vertex, ()): if neighbour in path or visit(neighbour): return path del path[vertex] return None for node in graph: result = visit(node) if result is not None: raise ValueError("{}".format(" | ".join(result.keys()))) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/util/lock.py0000644000175100001710000000241700000000000016565 0ustar00vstsdocker00000000000000"""holds locking functionality that works across processes""" from __future__ import absolute_import, unicode_literals from contextlib import contextmanager import py from filelock import FileLock, Timeout from tox.reporter import verbosity1 @contextmanager def hold_lock(lock_file, reporter=verbosity1): py.path.local(lock_file.dirname).ensure(dir=1) lock = FileLock(str(lock_file)) try: try: lock.acquire(0.0001) except Timeout: reporter("lock file {} present, will block until released".format(lock_file)) lock.acquire() yield finally: lock.release(force=True) def get_unique_file(path, prefix, suffix): """get a unique file in a folder having a given prefix and suffix, with unique number in between""" lock_file = path.join(".lock") prefix = "{}-".format(prefix) with hold_lock(lock_file): max_value = -1 for candidate in path.listdir("{}*{}".format(prefix, suffix)): try: max_value = max(max_value, int(candidate.basename[len(prefix) : -len(suffix)])) except ValueError: continue winner = path.join("{}{}{}".format(prefix, max_value + 1, suffix)) winner.ensure(dir=0) return winner ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/util/main.py0000644000175100001710000000016500000000000016557 0ustar00vstsdocker00000000000000import inspect import os import tox MAIN_FILE = os.path.join(os.path.dirname(inspect.getfile(tox)), "__main__.py") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/util/path.py0000644000175100001710000000033700000000000016570 0ustar00vstsdocker00000000000000import shutil from tox import reporter def ensure_empty_dir(path): if path.check(): reporter.info(" removing {}".format(path)) shutil.rmtree(str(path), ignore_errors=True) path.ensure(dir=1) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/util/spinner.py0000644000175100001710000001273200000000000017314 0ustar00vstsdocker00000000000000# -*- coding: utf-8 -*- """A minimal non-colored version of https://pypi.org/project/halo, to track list progress""" from __future__ import absolute_import, unicode_literals import os import sys import threading from collections import OrderedDict from datetime import datetime import py threads = [] if os.name == "nt": import ctypes class _CursorInfo(ctypes.Structure): _fields_ = [("size", ctypes.c_int), ("visible", ctypes.c_byte)] def _file_support_encoding(chars, file): encoding = getattr(file, "encoding", None) if encoding is not None: for char in chars: try: char.encode(encoding) except UnicodeEncodeError: break else: return True return False class Spinner(object): CLEAR_LINE = "\033[K" max_width = 120 UNICODE_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] ASCII_FRAMES = ["|", "-", "+", "x", "*"] def __init__(self, enabled=True, refresh_rate=0.1): self.refresh_rate = refresh_rate self.enabled = enabled self._file = sys.stdout self.frames = ( self.UNICODE_FRAMES if _file_support_encoding(self.UNICODE_FRAMES, sys.stdout) else self.ASCII_FRAMES ) self.stream = py.io.TerminalWriter(file=self._file) self._envs = OrderedDict() self._frame_index = 0 def clear(self): if self.enabled: self.stream.write("\r") self.stream.write(self.CLEAR_LINE) def render(self): while True: self._stop_spinner.wait(self.refresh_rate) if self._stop_spinner.is_set(): break self.render_frame() return self def render_frame(self): if self.enabled: self.clear() self.stream.write("\r{}".format(self.frame())) def frame(self): frame = self.frames[self._frame_index] self._frame_index += 1 self._frame_index = self._frame_index % len(self.frames) text_frame = "[{}] {}".format(len(self._envs), " | ".join(self._envs)) if len(text_frame) > self.max_width - 1: text_frame = "{}...".format(text_frame[: self.max_width - 1 - 3]) return "{} {}".format(*[(frame, text_frame)][0]) def __enter__(self): if self.enabled: self.disable_cursor() self.render_frame() self._stop_spinner = threading.Event() self._spinner_thread = threading.Thread(target=self.render) self._spinner_thread.setDaemon(True) self._spinner_thread.start() return self def __exit__(self, exc_type, exc_val, exc_tb): if not self._stop_spinner.is_set(): if self._spinner_thread: self._stop_spinner.set() self._spinner_thread.join() self._frame_index = 0 if self.enabled: self.clear() self.enable_cursor() return self def add(self, name): self._envs[name] = datetime.now() def succeed(self, key): self.finalize(key, "✔ OK", green=True) def fail(self, key): self.finalize(key, "✖ FAIL", red=True) def skip(self, key): self.finalize(key, "⚠ SKIP", white=True) def finalize(self, key, status, **kwargs): start_at = self._envs[key] del self._envs[key] if self.enabled: self.clear() self.stream.write( "{} {} in {}{}".format( status, key, td_human_readable(datetime.now() - start_at), os.linesep, ), **kwargs ) if not self._envs: self.__exit__(None, None, None) def disable_cursor(self): if self._file.isatty(): if os.name == "nt": ci = _CursorInfo() handle = ctypes.windll.kernel32.GetStdHandle(-11) ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci)) ci.visible = False ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci)) elif os.name == "posix": self.stream.write("\033[?25l") def enable_cursor(self): if self._file.isatty(): if os.name == "nt": ci = _CursorInfo() handle = ctypes.windll.kernel32.GetStdHandle(-11) ctypes.windll.kernel32.GetConsoleCursorInfo(handle, ctypes.byref(ci)) ci.visible = True ctypes.windll.kernel32.SetConsoleCursorInfo(handle, ctypes.byref(ci)) elif os.name == "posix": self.stream.write("\033[?25h") def td_human_readable(delta): seconds = int(delta.total_seconds()) periods = [ ("year", 60 * 60 * 24 * 365), ("month", 60 * 60 * 24 * 30), ("day", 60 * 60 * 24), ("hour", 60 * 60), ("minute", 60), ("second", 1), ] texts = [] for period_name, period_seconds in periods: if seconds > period_seconds or period_seconds == 1: period_value, seconds = divmod(seconds, period_seconds) if period_name == "second": ms = delta.total_seconds() - int(delta.total_seconds()) period_value = round(period_value + ms, 3) has_s = "s" if period_value != 1 else "" texts.append("{} {}{}".format(period_value, period_name, has_s)) return ", ".join(texts) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/util/stdlib.py0000644000175100001710000000313000000000000017107 0ustar00vstsdocker00000000000000import sys import threading from contextlib import contextmanager from tempfile import TemporaryFile if sys.version_info >= (3, 8): from importlib import metadata as importlib_metadata # noqa else: import importlib_metadata # noqa def is_main_thread(): """returns true if we are within the main thread""" cur_thread = threading.current_thread() if sys.version_info >= (3, 4): return cur_thread is threading.main_thread() else: # noinspection PyUnresolvedReferences return isinstance(cur_thread, threading._MainThread) # noinspection PyPep8Naming @contextmanager def suppress_output(): """suppress both stdout and stderr outputs""" if sys.version_info >= (3, 5): from contextlib import redirect_stderr, redirect_stdout else: class _RedirectStream(object): _stream = None def __init__(self, new_target): self._new_target = new_target self._old_targets = [] def __enter__(self): self._old_targets.append(getattr(sys, self._stream)) setattr(sys, self._stream, self._new_target) return self._new_target def __exit__(self, exctype, excinst, exctb): setattr(sys, self._stream, self._old_targets.pop()) class redirect_stdout(_RedirectStream): _stream = "stdout" class redirect_stderr(_RedirectStream): _stream = "stderr" with TemporaryFile("wt") as file: with redirect_stdout(file): with redirect_stderr(file): yield ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/src/tox/venv.py0000644000175100001710000007252100000000000015641 0ustar00vstsdocker00000000000000import codecs import json import os import pipes import re import sys from itertools import chain import py import tox from tox import reporter from tox.action import Action from tox.config.parallel import ENV_VAR_KEY_PRIVATE as PARALLEL_ENV_VAR_KEY_PRIVATE from tox.constants import INFO, PARALLEL_RESULT_JSON_PREFIX, PARALLEL_RESULT_JSON_SUFFIX from tox.package.local import resolve_package from tox.util.lock import get_unique_file from tox.util.path import ensure_empty_dir from .config import DepConfig class CreationConfig: def __init__( self, base_resolved_python_sha256, base_resolved_python_path, tox_version, sitepackages, usedevelop, deps, alwayscopy, ): self.base_resolved_python_sha256 = base_resolved_python_sha256 self.base_resolved_python_path = base_resolved_python_path self.tox_version = tox_version self.sitepackages = sitepackages self.usedevelop = usedevelop self.alwayscopy = alwayscopy self.deps = deps def writeconfig(self, path): lines = [ "{} {}".format(self.base_resolved_python_sha256, self.base_resolved_python_path), "{} {:d} {:d} {:d}".format( self.tox_version, self.sitepackages, self.usedevelop, self.alwayscopy, ), ] for dep in self.deps: lines.append("{} {}".format(*dep)) content = "\n".join(lines) path.ensure() path.write(content) return content @classmethod def readconfig(cls, path): try: lines = path.readlines(cr=0) base_resolved_python_info = lines.pop(0).split(None, 1) tox_version, sitepackages, usedevelop, alwayscopy = lines.pop(0).split(None, 4) sitepackages = bool(int(sitepackages)) usedevelop = bool(int(usedevelop)) alwayscopy = bool(int(alwayscopy)) deps = [] for line in lines: base_resolved_python_sha256, depstring = line.split(None, 1) deps.append((base_resolved_python_sha256, depstring)) base_resolved_python_sha256, base_resolved_python_path = base_resolved_python_info return CreationConfig( base_resolved_python_sha256, base_resolved_python_path, tox_version, sitepackages, usedevelop, deps, alwayscopy, ) except Exception: return None def matches_with_reason(self, other, deps_matches_subset=False): for attr in ( "base_resolved_python_sha256", "base_resolved_python_path", "tox_version", "sitepackages", "usedevelop", "alwayscopy", ): left = getattr(self, attr) right = getattr(other, attr) if left != right: return False, "attr {} {!r}!={!r}".format(attr, left, right) self_deps = set(self.deps) other_deps = set(other.deps) if self_deps != other_deps: if deps_matches_subset: diff = other_deps - self_deps if diff: return False, "missing in previous {!r}".format(diff) else: return False, "{!r}!={!r}".format(self_deps, other_deps) return True, None def matches(self, other, deps_matches_subset=False): outcome, _ = self.matches_with_reason(other, deps_matches_subset) return outcome class VirtualEnv(object): def __init__(self, envconfig=None, popen=None, env_log=None): self.envconfig = envconfig self.popen = popen self._actions = [] self.env_log = env_log self._result_json_path = None def new_action(self, msg, *args): config = self.envconfig.config command_log = self.env_log.get_commandlog( "test" if msg in ("run-test", "run-test-pre", "run-test-post") else "setup", ) return Action( self.name, msg, args, self.envconfig.envlogdir, config.option.resultjson, command_log, self.popen, self.envconfig.envpython, self.envconfig.suicide_timeout, self.envconfig.interrupt_timeout, self.envconfig.terminate_timeout, ) def get_result_json_path(self): if self._result_json_path is None: if self.envconfig.config.option.resultjson: self._result_json_path = get_unique_file( self.path, PARALLEL_RESULT_JSON_PREFIX, PARALLEL_RESULT_JSON_SUFFIX, ) return self._result_json_path @property def hook(self): return self.envconfig.config.pluginmanager.hook @property def path(self): """ Path to environment base dir. """ return self.envconfig.envdir @property def path_config(self): return self.path.join(".tox-config1") @property def name(self): """ test environment name. """ return self.envconfig.envname def __repr__(self): return "".format(self.path) def getcommandpath(self, name, venv=True, cwd=None): """Return absolute path (str or localpath) for specified command name. - If it's a local path we will rewrite it as as a relative path. - If venv is True we will check if the command is coming from the venv or is allowed to come from external. """ name = str(name) if os.path.isabs(name): return name if os.path.split(name)[0] == ".": path = cwd.join(name) if path.check(): return str(path) if venv: path = self._venv_lookup_and_check_external_allowlist(name) else: path = self._normal_lookup(name) if path is None: raise tox.exception.InvocationError( "could not find executable {}".format(pipes.quote(name)), ) return str(path) # will not be rewritten for reporting def _venv_lookup_and_check_external_allowlist(self, name): path = self._venv_lookup(name) if path is None: path = self._normal_lookup(name) if path is not None: self._check_external_allowed_and_warn(path) return path def _venv_lookup(self, name): return py.path.local.sysfind(name, paths=[self.envconfig.envbindir]) def _normal_lookup(self, name): return py.path.local.sysfind(name) def _check_external_allowed_and_warn(self, path): if not self.is_allowed_external(path): reporter.warning( "test command found but not installed in testenv\n" " cmd: {}\n" " env: {}\n" "Maybe you forgot to specify a dependency? " "See also the allowlist_externals envconfig setting.\n\n" "DEPRECATION WARNING: this will be an error in tox 4 and above!".format( path, self.envconfig.envdir, ), ) def is_allowed_external(self, p): tryadd = [""] if tox.INFO.IS_WIN: tryadd += [os.path.normcase(x) for x in os.environ["PATHEXT"].split(os.pathsep)] p = py.path.local(os.path.normcase(str(p))) if self.envconfig.allowlist_externals and self.envconfig.whitelist_externals: raise tox.exception.ConfigError( "Either whitelist_externals or allowlist_externals might be specified, not both", ) allowed_externals = ( self.envconfig.whitelist_externals or self.envconfig.allowlist_externals ) for x in allowed_externals: for add in tryadd: if p.fnmatch(x + add): return True return False def update(self, action): """return status string for updating actual venv to match configuration. if status string is empty, all is ok. """ rconfig = CreationConfig.readconfig(self.path_config) if self.envconfig.recreate: reason = "-r flag" else: if rconfig is None: reason = "no previous config {}".format(self.path_config) else: live_config = self._getliveconfig() deps_subset_match = getattr(self.envconfig, "deps_matches_subset", False) outcome, reason = rconfig.matches_with_reason(live_config, deps_subset_match) if reason is None: action.info("reusing", self.envconfig.envdir) return action.info("cannot reuse", reason) if rconfig is None: action.setactivity("create", self.envconfig.envdir) else: action.setactivity("recreate", self.envconfig.envdir) try: self.hook.tox_testenv_create(action=action, venv=self) self.just_created = True except tox.exception.UnsupportedInterpreter as exception: return exception try: self.hook.tox_testenv_install_deps(action=action, venv=self) except tox.exception.InvocationError as exception: return "could not install deps {}; v = {!r}".format(self.envconfig.deps, exception) def _getliveconfig(self): base_resolved_python_path = self.envconfig.python_info.executable version = tox.__version__ sitepackages = self.envconfig.sitepackages develop = self.envconfig.usedevelop alwayscopy = self.envconfig.alwayscopy deps = [] for dep in self.get_resolved_dependencies(): dep_name_sha256 = getdigest(dep.name) deps.append((dep_name_sha256, dep.name)) base_resolved_python_sha256 = getdigest(base_resolved_python_path) return CreationConfig( base_resolved_python_sha256, base_resolved_python_path, version, sitepackages, develop, deps, alwayscopy, ) def get_resolved_dependencies(self): dependencies = [] for dependency in self.envconfig.deps: if dependency.indexserver is None: package = resolve_package(package_spec=dependency.name) if package != dependency.name: dependency = dependency.__class__(package) dependencies.append(dependency) return dependencies def getsupportedinterpreter(self): return self.envconfig.getsupportedinterpreter() def matching_platform(self): return re.match(self.envconfig.platform, sys.platform) def finish(self): previous_config = CreationConfig.readconfig(self.path_config) live_config = self._getliveconfig() if previous_config is None or not previous_config.matches(live_config): content = live_config.writeconfig(self.path_config) reporter.verbosity1("write config to {} as {!r}".format(self.path_config, content)) def _needs_reinstall(self, setupdir, action): setup_py = setupdir.join("setup.py") setup_cfg = setupdir.join("setup.cfg") args = [self.envconfig.envpython, str(setup_py), "--name"] env = self._get_os_environ() output = action.popen( args, cwd=setupdir, redirect=False, returnout=True, env=env, capture_err=False, ) name = next( (i for i in output.split("\n") if i and not i.startswith("pydev debugger:")), "", ) args = [ self.envconfig.envpython, "-c", "import sys; import json; print(json.dumps(sys.path))", ] out = action.popen(args, redirect=False, returnout=True, env=env) try: sys_path = json.loads(out) except ValueError: sys_path = [] egg_info_fname = ".".join((name.replace("-", "_"), "egg-info")) for d in reversed(sys_path): egg_info = py.path.local(d).join(egg_info_fname) if egg_info.check(): break else: return True needs_reinstall = any( conf_file.check() and conf_file.mtime() > egg_info.mtime() for conf_file in (setup_py, setup_cfg) ) # Ensure the modification time of the egg-info folder is updated so we # won't need to do this again. # TODO(stephenfin): Remove once the minimum version of setuptools is # high enough to include https://github.com/pypa/setuptools/pull/1427/ if needs_reinstall: egg_info.setmtime() return needs_reinstall def install_pkg(self, dir, action, name, is_develop=False): assert action is not None if getattr(self, "just_created", False): action.setactivity(name, dir) self.finish() pip_flags = ["--exists-action", "w"] else: if is_develop and not self._needs_reinstall(dir, action): action.setactivity("{}-noop".format(name), dir) return action.setactivity("{}-nodeps".format(name), dir) pip_flags = ["--no-deps"] + ([] if is_develop else ["-U"]) pip_flags.extend(["-v"] * min(3, reporter.verbosity() - 2)) if self.envconfig.extras: dir += "[{}]".format(",".join(self.envconfig.extras)) target = [dir] if is_develop: target.insert(0, "-e") self._install(target, extraopts=pip_flags, action=action) def developpkg(self, setupdir, action): self.install_pkg(setupdir, action, "develop-inst", is_develop=True) def installpkg(self, sdistpath, action): self.install_pkg(sdistpath, action, "inst") def _installopts(self, indexserver): options = [] if indexserver: options += ["-i", indexserver] if self.envconfig.pip_pre: options.append("--pre") return options def run_install_command(self, packages, action, options=()): def expand(val): # expand an install command if val == "{packages}": for package in packages: yield package elif val == "{opts}": for opt in options: yield opt else: yield val cmd = list(chain.from_iterable(expand(val) for val in self.envconfig.install_command)) env = self._get_os_environ() self.ensure_pip_os_environ_ok(env) old_stdout = sys.stdout sys.stdout = codecs.getwriter("utf8")(sys.stdout) try: self._pcall( cmd, cwd=self.envconfig.config.toxinidir, action=action, redirect=reporter.verbosity() < reporter.Verbosity.DEBUG, env=env, ) except KeyboardInterrupt: self.status = "keyboardinterrupt" raise finally: sys.stdout = old_stdout def ensure_pip_os_environ_ok(self, env): for key in ("PIP_RESPECT_VIRTUALENV", "PIP_REQUIRE_VIRTUALENV", "__PYVENV_LAUNCHER__"): env.pop(key, None) if all("PYTHONPATH" not in i for i in (self.envconfig.passenv, self.envconfig.setenv)): # If PYTHONPATH not explicitly asked for, remove it. if "PYTHONPATH" in env: if sys.version_info < (3, 4) or bool(env["PYTHONPATH"]): # https://docs.python.org/3/whatsnew/3.4.html#changes-in-python-command-behavior # In a posix shell, setting the PATH environment variable to an empty value is # equivalent to not setting it at all. reporter.warning( "Discarding $PYTHONPATH from environment, to override " "specify PYTHONPATH in 'passenv' in your configuration.", ) env.pop("PYTHONPATH") # installing packages at user level may mean we're not installing inside the venv env["PIP_USER"] = "0" # installing without dependencies may lead to broken packages env["PIP_NO_DEPS"] = "0" def _install(self, deps, extraopts=None, action=None): if not deps: return d = {} ixservers = [] for dep in deps: if isinstance(dep, (str, py.path.local)): dep = DepConfig(str(dep), None) assert isinstance(dep, DepConfig), dep if dep.indexserver is None: ixserver = self.envconfig.config.indexserver["default"] else: ixserver = dep.indexserver d.setdefault(ixserver, []).append(dep.name) if ixserver not in ixservers: ixservers.append(ixserver) assert ixserver.url is None or isinstance(ixserver.url, str) for ixserver in ixservers: packages = d[ixserver] options = self._installopts(ixserver.url) if extraopts: options.extend(extraopts) self.run_install_command(packages=packages, options=options, action=action) def _get_os_environ(self, is_test_command=False): if is_test_command: # for executing tests we construct a clean environment env = {} for env_key in self.envconfig.passenv: if env_key in os.environ: env[env_key] = os.environ[env_key] else: # for executing non-test commands we use the full # invocation environment env = os.environ.copy() # in any case we honor per-testenv setenv configuration env.update(self.envconfig.setenv.export()) env["VIRTUAL_ENV"] = str(self.path) return env def test( self, redirect=False, name="run-test", commands=None, ignore_outcome=None, ignore_errors=None, display_hash_seed=False, ): if commands is None: commands = self.envconfig.commands if ignore_outcome is None: ignore_outcome = self.envconfig.ignore_outcome if ignore_errors is None: ignore_errors = self.envconfig.ignore_errors with self.new_action(name) as action: cwd = self.envconfig.changedir if display_hash_seed: env = self._get_os_environ(is_test_command=True) # Display PYTHONHASHSEED to assist with reproducibility. action.setactivity(name, "PYTHONHASHSEED={!r}".format(env.get("PYTHONHASHSEED"))) for i, argv in enumerate(filter(bool, commands)): # have to make strings as _pcall changes argv[0] to a local() # happens if the same environment is invoked twice message = "commands[{}] | {}".format( i, " ".join([pipes.quote(str(x)) for x in argv]), ) action.setactivity(name, message) # check to see if we need to ignore the return code # if so, we need to alter the command line arguments if argv[0].startswith("-"): ignore_ret = True if argv[0] == "-": del argv[0] else: argv[0] = argv[0].lstrip("-") else: ignore_ret = False try: self._pcall( argv, cwd=cwd, action=action, redirect=redirect, ignore_ret=ignore_ret, is_test_command=True, ) except tox.exception.InvocationError as err: if ignore_outcome: msg = "command failed but result from testenv is ignored\ncmd:" reporter.warning("{} {}".format(msg, err)) self.status = "ignored failed command" continue # keep processing commands reporter.error(str(err)) self.status = "commands failed" if not ignore_errors: break # Don't process remaining commands except KeyboardInterrupt: self.status = "keyboardinterrupt" raise def _pcall( self, args, cwd, venv=True, is_test_command=False, action=None, redirect=True, ignore_ret=False, returnout=False, env=None, ): if env is None: env = self._get_os_environ(is_test_command=is_test_command) # construct environment variables env.pop("VIRTUALENV_PYTHON", None) bin_dir = str(self.envconfig.envbindir) path = self.envconfig.setenv.get("PATH") or os.environ["PATH"] env["PATH"] = os.pathsep.join([bin_dir, path]) reporter.verbosity2("setting PATH={}".format(env["PATH"])) # get command args[0] = self.getcommandpath(args[0], venv, cwd) if sys.platform != "win32" and "TOX_LIMITED_SHEBANG" in os.environ: args = prepend_shebang_interpreter(args) cwd.ensure(dir=1) # ensure the cwd exists return action.popen( args, cwd=cwd, env=env, redirect=redirect, ignore_ret=ignore_ret, returnout=returnout, report_fail=not is_test_command, ) def setupenv(self): if self.envconfig._missing_subs: self.status = ( "unresolvable substitution(s):\n {}\n" "Environment variables are missing or defined recursively.".format( "\n ".join( [ "{}: '{}'".format(section_key, exc.name) for section_key, exc in sorted(self.envconfig._missing_subs.items()) ], ), ) ) return if not self.matching_platform(): self.status = "platform mismatch" return # we simply omit non-matching platforms with self.new_action("getenv", self.envconfig.envdir) as action: self.status = 0 default_ret_code = 1 envlog = self.env_log try: status = self.update(action=action) except IOError as e: if e.args[0] != 2: raise status = ( "Error creating virtualenv. Note that spaces in paths are " "not supported by virtualenv. Error details: {!r}".format(e) ) except tox.exception.InvocationError as e: status = e except tox.exception.InterpreterNotFound as e: status = e if self.envconfig.config.option.skip_missing_interpreters == "true": default_ret_code = 0 except KeyboardInterrupt: self.status = "keyboardinterrupt" raise if status: str_status = str(status) command_log = envlog.get_commandlog("setup") command_log.add_command(["setup virtualenv"], str_status, default_ret_code) self.status = status if default_ret_code == 0: reporter.skip(str_status) else: reporter.error(str_status) return False command_path = self.getcommandpath("python") envlog.set_python_info(command_path) return True def finishvenv(self): with self.new_action("finishvenv"): self.finish() return True def getdigest(path): path = py.path.local(path) if not path.check(file=1): return "0" * 32 return path.computehash("sha256") def prepend_shebang_interpreter(args): # prepend interpreter directive (if any) to argument list # # When preparing virtual environments in a file container which has large # length, the system might not be able to invoke shebang scripts which # define interpreters beyond system limits (e.x. Linux as a limit of 128; # BINPRM_BUF_SIZE). This method can be used to check if the executable is # a script containing a shebang line. If so, extract the interpreter (and # possible optional argument) and prepend the values to the provided # argument list. tox will only attempt to read an interpreter directive of # a maximum size of 2048 bytes to limit excessive reading and support UNIX # systems which may support a longer interpret length. try: with open(args[0], "rb") as f: if f.read(1) == b"#" and f.read(1) == b"!": MAXINTERP = 2048 interp = f.readline(MAXINTERP).rstrip().decode("UTF-8") interp_args = interp.split(None, 1)[:2] return interp_args + args except (UnicodeDecodeError, IOError): pass return args _SKIP_VENV_CREATION = os.environ.get("_TOX_SKIP_ENV_CREATION_TEST", False) == "1" @tox.hookimpl def tox_testenv_create(venv, action): 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") if not venv.envconfig.download: args.append("--no-download") else: args.append("--download") # add interpreter explicitly, to prevent using default (virtualenv.ini) args.extend(["--python", str(config_interpreter)]) cleanup_for_venv(venv) base_path = venv.path.dirpath() base_path.ensure(dir=1) args.append(venv.path.basename) if not _SKIP_VENV_CREATION: try: venv._pcall( args, venv=False, action=action, cwd=base_path, redirect=reporter.verbosity() < reporter.Verbosity.DEBUG, ) except KeyboardInterrupt: venv.status = "keyboardinterrupt" raise return True # Return non-None to indicate plugin has completed def cleanup_for_venv(venv): within_parallel = PARALLEL_ENV_VAR_KEY_PRIVATE in os.environ # if the directory exists and it doesn't look like a virtualenv, produce # an error if venv.path.exists(): dir_items = set(os.listdir(str(venv.path))) - {".lock", "log"} dir_items = {p for p in dir_items if not p.startswith(".tox-") or p == ".tox-config1"} else: dir_items = set() if not ( # doesn't exist => OK not venv.path.exists() # does exist, but it's empty => OK or not dir_items # tox has marked this as an environment it has created in the past or ".tox-config1" in dir_items # it exists and we're on windows with Lib and Scripts => OK or (INFO.IS_WIN and dir_items > {"Scripts", "Lib"}) # non-windows, with lib and bin => OK or dir_items > {"bin", "lib"} # pypy has a different lib folder => OK or dir_items > {"bin", "lib_pypy"} ): venv.status = "error" reporter.error( "cowardly refusing to delete `envdir` (it does not look like a virtualenv): " "{}".format(venv.path), ) raise SystemExit(2) if within_parallel: if venv.path.exists(): # do not delete the log folder as that's used by parent for content in venv.path.listdir(): if not content.basename == "log": content.remove(rec=1, ignore_errors=True) else: ensure_empty_dir(venv.path) @tox.hookimpl def tox_testenv_install_deps(venv, action): deps = venv.get_resolved_dependencies() if deps: depinfo = ", ".join(map(str, deps)) action.setactivity("installdeps", depinfo) venv._install(deps, action=action) return True # Return non-None to indicate plugin has completed @tox.hookimpl def tox_runtest(venv, redirect): venv.test(redirect=redirect) return True # Return non-None to indicate plugin has completed @tox.hookimpl def tox_runtest_pre(venv): venv.status = 0 ensure_empty_dir(venv.envconfig.envtmpdir) venv.envconfig.envtmpdir.ensure(dir=1) venv.test( name="run-test-pre", commands=venv.envconfig.commands_pre, redirect=False, ignore_outcome=False, ignore_errors=False, display_hash_seed=True, ) @tox.hookimpl def tox_runtest_post(venv): venv.test( name="run-test-post", commands=venv.envconfig.commands_post, redirect=False, ignore_outcome=False, ignore_errors=False, ) @tox.hookimpl def tox_runenvreport(venv, action): # write out version dependency information args = venv.envconfig.list_dependencies_command output = venv._pcall(args, cwd=venv.envconfig.config.toxinidir, action=action, returnout=True) # the output contains a mime-header, skip it output = output.split("\n\n")[-1] packages = output.strip().split("\n") return packages # Return non-None to indicate plugin has completed ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297739.0 tox-3.21.4/src/tox/version.py0000644000175100001710000000012000000000000016332 0ustar00vstsdocker00000000000000# coding: utf-8 from __future__ import unicode_literals __version__ = '3.21.4' ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9035115 tox-3.21.4/src/tox.egg-info/0000755000175100001710000000000000000000000015774 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297739.0 tox-3.21.4/src/tox.egg-info/PKG-INFO0000644000175100001710000001574300000000000017103 0ustar00vstsdocker00000000000000Metadata-Version: 2.1 Name: tox Version: 3.21.4 Summary: tox is a generic virtualenv management and test command line tool Home-page: http://tox.readthedocs.org Author: Holger Krekel, Oliver Bestwalter, Bernát Gábor and others Maintainer: Bernat Gabor, Oliver Bestwalter, Anthony Asottile Maintainer-email: tox-dev@python.org License: MIT Project-URL: Source, https://github.com/tox-dev/tox Project-URL: Tracker, https://github.com/tox-dev/tox/issues Description: ![PyPI](https://img.shields.io/pypi/v/tox?style=flat-square) [![Supported Python versions](https://img.shields.io/pypi/pyversions/tox.svg)](https://pypi.org/project/tox/) [![Azure Pipelines build status](https://dev.azure.com/toxdev/tox/_apis/build/status/tox%20ci?branchName=master)](https://dev.azure.com/toxdev/tox/_build/latest?definitionId=9&branchName=master) [![Documentation status](https://readthedocs.org/projects/tox/badge/?version=latest&style=flat-square)](https://tox.readthedocs.io/en/latest/?badge=latest) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Downloads](https://pepy.tech/badge/tox/month)](https://pepy.tech/project/tox/month) tox logo # tox automation project **Command line driven CI frontend and development task automation tool** At its core tox provides a convenient way to run arbitrary commands in isolated environments to serve as a single entry point for build, test and release activities. tox is highly [configurable](https://tox.readthedocs.io/en/latest/config.html) and [pluggable](https://tox.readthedocs.io/en/latest/plugins.html). ## Example: run tests with Python 3.7 and Python 3.8 tox is mainly used as a command line tool and needs a `tox.ini` or a `tool.tox` section in `pyproject.toml` containing the configuration. To test a simple project that has some tests, here is an example with a `tox.ini` in the root of the project: ```{.sourceCode .ini} [tox] envlist = py37,py38 [testenv] deps = pytest commands = pytest ``` ```{.sourceCode .console} $ tox [lots of output from what tox does] [lots of output from commands that were run] __________________ summary _________________ py37: commands succeeded py38: commands succeeded congratulations :) ``` tox created two `testenvs` - one based on Python3.7 and one based on Python3.8, it installed pytest in them and ran the tests. The report at the end summarizes which `testenvs` have failed and which have succeeded. **Note:** To learn more about what you can do with tox, have a look at [the collection of examples in the documentation](https://tox.readthedocs.io/en/latest/examples.html) or [existing projects using tox](https://github.com/search?l=INI&q=tox.ini+in%3Apath&type=Code). ### How it works tox creates virtual environments for all configured so called `testenvs`, it then installs the project and other necessary dependencies and runs the configured set of commands. See [system overview](https://tox.readthedocs.io/en/latest/#system-overview) for more details. tox flow ### tox can be used for ... - creating development environments - running static code analysis and test tools - automating package builds - running tests against the package build by tox - checking that packages install correctly with different Python versions/interpreters - unifying Continuous Integration and command line based testing - building and deploying project documentation - releasing a package to PyPI or any other platform - limit: your imagination ### Documentation Documentation for tox can be found at [Read The Docs](https://tox.readthedocs.org). ### Communication and questions For the fastest and interactive feedback please join our [![Discord](https://img.shields.io/discord/802911963368783933?style=flat-square)](https://discord.gg/edtj86wzBX) server. If you have questions or suggestions you can first check if they have already been answered or discussed on our [issue tracker](https://github.com/tox-dev/tox/issues?utf8=%E2%9C%93&q=is%3Aissue+sort%3Aupdated-desc+label%3A%22type%3Aquestion+%3Agrey_question%3A%22+). On [Stack Overflow (tagged with `tox`)](https://stackoverflow.com/questions/tagged/tox). ### Contributing Contributions are welcome. See [contributing](https://github.com/tox-dev/tox/blob/master/CONTRIBUTING.rst) and our [Contributor Covenant Code of Conduct](https://github.com/tox-dev/tox/blob/master/CODE_OF_CONDUCT.md). Currently the [code](https://github.com/tox-dev/tox) and the [issues](https://github.com/tox-dev/tox/issues) are hosted on Github. The project is licensed under [MIT](https://github.com/tox-dev/tox/blob/master/LICENSE). Keywords: virtual,environments,isolated,testing Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: tox Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: MacOS :: MacOS X Classifier: Operating System :: Microsoft :: Windows Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries Classifier: Topic :: Software Development :: Testing Classifier: Topic :: Utilities Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7 Description-Content-Type: text/markdown Provides-Extra: docs Provides-Extra: testing ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297739.0 tox-3.21.4/src/tox.egg-info/SOURCES.txt0000644000175100001710000001026100000000000017660 0ustar00vstsdocker00000000000000.gitignore .pre-commit-config.yaml CODE_OF_CONDUCT.md CONTRIBUTING.rst CONTRIBUTORS HOWTORELEASE.rst LICENSE MANIFEST.in README.md azure-pipelines.yml pyproject.toml readthedocs.yml setup.cfg setup.py tox.ini .github/CODEOWNERS .github/PULL_REQUEST_TEMPLATE.md .github/config.yml .github/ISSUE_TEMPLATE/bug_report.md .github/ISSUE_TEMPLATE/feature_request.md docs/changelog.rst docs/conf.py docs/config.rst docs/developers.rst docs/examples.rst docs/index.rst docs/install.rst docs/links.rst docs/plugins.rst docs/support.rst docs/_static/custom.css docs/_static/img/tox.png docs/_static/img/tox.svg docs/_static/img/toxfavi.ico docs/_templates/localtoc.html docs/announce/changelog-only.rst docs/changelog/README.rst docs/changelog/template.jinja2 docs/drafts/extend-envs-and-packagebuilds.md docs/drafts/tox_conda_notes_niccodemus.md docs/example/basic.rst docs/example/devenv.rst docs/example/documentation.rst docs/example/general.rst docs/example/jenkins.rst docs/example/nose.rst docs/example/package.rst docs/example/platform.rst docs/example/pytest.rst docs/example/result.rst docs/example/unittest.rst docs/img/tox_flow.png src/tox/__init__.py src/tox/__main__.py src/tox/_pytestplugin.py src/tox/_quickstart.py src/tox/action.py src/tox/cli.py src/tox/constants.py src/tox/exception.py src/tox/hookspecs.py src/tox/reporter.py src/tox/venv.py src/tox/version.py src/tox.egg-info/PKG-INFO src/tox.egg-info/SOURCES.txt src/tox.egg-info/dependency_links.txt src/tox.egg-info/entry_points.txt src/tox.egg-info/requires.txt src/tox.egg-info/top_level.txt src/tox/config/__init__.py src/tox/config/parallel.py src/tox/config/reporter.py src/tox/helper/__init__.py src/tox/helper/build_isolated.py src/tox/helper/build_requires.py src/tox/helper/get_site_package_dir.py src/tox/helper/get_version.py src/tox/interpreters/__init__.py src/tox/interpreters/common.py src/tox/interpreters/py_spec.py src/tox/interpreters/unix.py src/tox/interpreters/via_path.py src/tox/interpreters/windows/__init__.py src/tox/interpreters/windows/pep514.py src/tox/logs/__init__.py src/tox/logs/command.py src/tox/logs/env.py src/tox/logs/result.py src/tox/package/__init__.py src/tox/package/local.py src/tox/package/view.py src/tox/package/builder/__init__.py src/tox/package/builder/isolated.py src/tox/package/builder/legacy.py src/tox/session/__init__.py src/tox/session/commands/__init__.py src/tox/session/commands/help.py src/tox/session/commands/help_ini.py src/tox/session/commands/provision.py src/tox/session/commands/show_config.py src/tox/session/commands/show_env.py src/tox/session/commands/run/__init__.py src/tox/session/commands/run/parallel.py src/tox/session/commands/run/sequential.py src/tox/util/__init__.py src/tox/util/graph.py src/tox/util/lock.py src/tox/util/main.py src/tox/util/path.py src/tox/util/spinner.py src/tox/util/stdlib.py tasks/client_secret.json tasks/notify.py tasks/release.py tests/__init__.py tests/conftest.py tests/integration/test_jython_env_create.py tests/integration/test_package_int.py tests/integration/test_parallel_inception.py tests/integration/test_parallel_interrupt.py tests/integration/test_provision_int.py tests/lib/__init__.py tests/unit/__init__.py tests/unit/test_docs.py tests/unit/test_pytest_plugins.py tests/unit/test_quickstart.py tests/unit/test_result.py tests/unit/test_venv.py tests/unit/test_z_cmdline.py tests/unit/config/test_config.py tests/unit/config/test_config_parallel.py tests/unit/interpreters/test_interpreters.py tests/unit/interpreters/test_py_spec.py tests/unit/interpreters/windows/test_pep514.py tests/unit/interpreters/windows/test_windows.py tests/unit/package/test_package.py tests/unit/package/test_package_parallel.py tests/unit/package/test_package_view.py tests/unit/package/builder/test_package_builder_isolated.py tests/unit/package/builder/test_package_builder_legacy.py tests/unit/session/__init__.py tests/unit/session/test_list_env.py tests/unit/session/test_parallel.py tests/unit/session/test_provision.py tests/unit/session/test_session.py tests/unit/session/test_show_config.py tests/unit/session/plugin/setup.cfg tests/unit/session/plugin/setup.py tests/unit/session/plugin/a/__init__.py tests/unit/util/test_graph.py tests/unit/util/test_spinner.py tests/unit/util/test_util.py././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297739.0 tox-3.21.4/src/tox.egg-info/dependency_links.txt0000644000175100001710000000000100000000000022042 0ustar00vstsdocker00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297739.0 tox-3.21.4/src/tox.egg-info/entry_points.txt0000644000175100001710000000011300000000000021265 0ustar00vstsdocker00000000000000[console_scripts] tox = tox:cmdline tox-quickstart = tox._quickstart:main ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297739.0 tox-3.21.4/src/tox.egg-info/requires.txt0000644000175100001710000000110100000000000020365 0ustar00vstsdocker00000000000000filelock>=3.0.0 packaging>=14 pluggy>=0.12.0 py>=1.4.17 six>=1.14.0 toml>=0.9.4 virtualenv!=20.0.0,!=20.0.1,!=20.0.2,!=20.0.3,!=20.0.4,!=20.0.5,!=20.0.6,!=20.0.7,>=16.0.0 [:platform_system == "Windows"] colorama>=0.4.1 [:python_version < "3.8"] importlib-metadata>=0.12 [docs] pygments-github-lexers>=0.0.5 sphinx>=2.0.0 sphinxcontrib-autoprogram>=0.1.5 towncrier>=18.5.0 [testing] flaky>=3.4.0 freezegun>=0.3.11 psutil>=5.6.1 pytest>=4.0.0 pytest-cov>=2.5.1 pytest-mock>=1.10.0 pytest-randomly>=1.0.0 pytest-xdist>=1.22.2 [testing:python_version < "3.4"] pathlib2>=2.3.3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297739.0 tox-3.21.4/src/tox.egg-info/top_level.txt0000644000175100001710000000000400000000000020520 0ustar00vstsdocker00000000000000tox ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.907512 tox-3.21.4/tasks/0000755000175100001710000000000000000000000014026 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tasks/client_secret.json0000644000175100001710000000075000000000000017546 0ustar00vstsdocker00000000000000{ "installed": { "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "client_id": "84180756337-bd32ogk9j5nbu71srlsko923r8s78ler.apps.googleusercontent.com", "client_secret": "", "project_id": "send-notificatio-1530696447231", "redirect_uris": [ "urn:ietf:wg:oauth:2.0:oob", "http://localhost" ], "token_uri": "https://accounts.google.com/o/oauth2/token" } } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tasks/notify.py0000644000175100001710000001300200000000000015704 0ustar00vstsdocker00000000000000# -*- coding: utf-8 -*- """Handles creating a release PR""" import base64 import json import os import tempfile import textwrap from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from pathlib import Path from typing import Tuple import httplib2 from apiclient import discovery from git import Remote, Repo from oauth2client import client, file, tools from packaging.version import Version ROOT_SRC_DIR = Path(__file__).parents[1] def main() -> None: repo = Repo(str(ROOT_SRC_DIR)) update_upstream(repo) prev_version, release_version = get_last_release_versions(repo) send_mail_message( subject=f"tox release {release_version}", content=get_message_body(release_version, prev_version), ) print("All done! ✨ 🍰 ✨") def get_message_body(release_version: Version, prev_version: Version) -> str: is_major_release = release_version.release[0:2] != prev_version.release[0:2] if is_major_release: return textwrap.dedent( f""" The tox team is proud to announce the {release_version} feature release at https://pypi.org/project/tox/! tox aims to automate and standardize testing in Python. It is part of a larger vision of easing the packaging, testing and release process of Python software. Details about the changes can be found at https://tox.readthedocs.io/en/{release_version}/changelog.html For complete documentation, please visit: https://tox.readthedocs.io/en/{release_version}/ As usual, you can upgrade from PyPI via: pip install --upgrade tox or - if you also want to get pre release versions: pip install -upgrade --pre tox We thank all present and past contributors to tox. Have a look at https://github.com/tox-dev/tox/blob/master/CONTRIBUTORS to see who contributed. Happy toxing, the tox-dev team """, # noqa ) else: return textwrap.dedent( f""" The tox team is proud to announce the {release_version} bug fix release at https://pypi.org/project/tox/! tox aims to automate and standardize testing in Python. It is part of a larger vision of easing the packaging, testing and release process of Python software. For details about the fix(es),please check the CHANGELOG: https://tox.readthedocs.io/en/{release_version}/changelog.html We thank all present and past contributors to tox. Have a look at https://github.com/tox-dev/tox/blob/master/CONTRIBUTORS to see who contributed. Happy toxing, the tox-dev team """, # noqa ) def get_upstream(repo: Repo) -> Remote: for remote in repo.remotes: for url in remote.urls: if url.endswith("tox-dev/tox.git"): return remote raise RuntimeError("could not find tox-dev/tox.git remote") def get_last_release_versions(repo: Repo) -> Tuple[Version, Version]: print("get latest release version") commit_to_tag = {tag.commit.hexsha: tag for tag in repo.tags} _, release_tag = sorted( [(tag.commit.committed_datetime, tag) for tag in repo.tags], reverse=True, )[0] for commit in release_tag.commit.iter_parents(): if commit.hexsha in commit_to_tag: prev_release_tag = commit_to_tag[commit.hexsha] prev_version = Version(prev_release_tag.name) if not any( ( prev_version.is_devrelease, prev_version.is_prerelease, prev_version.is_postrelease, ), ): break else: raise RuntimeError("no previous release") release_version = Version(release_tag.name) print(f"\trelease {release_version} with previous {prev_version}") return prev_version, release_version def update_upstream(repo: Repo) -> None: print("fetch latest remote") upstream = get_upstream(repo) upstream.fetch() def send_mail_message(subject, content): this_dir = Path(__file__).parent store = file.Storage("credentials.json") credentials = store.get() if not credentials or credentials.invalid: client_secret_json = json.loads((this_dir / "client_secret.json").read_text()) client_secret_json["installed"]["client_secret"] = os.environ["TOX_DEV_GOOGLE_SECRET"] with tempfile.NamedTemporaryFile(mode="w+t") as temp_filename: json.dump(client_secret_json, temp_filename) temp_filename.flush() flow = client.flow_from_clientsecrets( filename=temp_filename.name, scope="https://www.googleapis.com/auth/gmail.send", ) credentials = tools.run_flow(flow, store) service = discovery.build("gmail", "v1", http=credentials.authorize(httplib2.Http())) message = MIMEMultipart("alternative") message["Subject"] = subject message["From"] = "toxdevorg@gmail.com" recipients = ["testing-in-python@lists.idyll.org", "tox-dev@python.org"] message["To"] = ", ".join(recipients) message.attach(MIMEText(content, "plain")) raw_message_no_attachment = base64.urlsafe_b64encode(message.as_bytes()) raw_message_no_attachment = raw_message_no_attachment.decode() body = {"raw": raw_message_no_attachment} message_sent = service.users().messages().send(userId="me", body=body).execute() message_id = message_sent["id"] print(f"\tMessage sent with id: {message_id}") if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tasks/release.py0000644000175100001710000000500300000000000016016 0ustar00vstsdocker00000000000000# -*- coding: utf-8 -*- """Handles creating a release PR""" from pathlib import Path from subprocess import check_call from typing import Tuple from git import Commit, Head, Remote, Repo, TagReference from packaging.version import Version ROOT_SRC_DIR = Path(__file__).parents[1] def main(version_str: str) -> None: version = Version(version_str) repo = Repo(str(ROOT_SRC_DIR)) if repo.is_dirty(): raise RuntimeError("Current repository is dirty. Please commit any changes and try again.") upstream, release_branch = create_release_branch(repo, version) release_commit = release_changelog(repo, version) tag = tag_release_commit(release_commit, repo, version) print("push release commit") repo.git.push(upstream.name, release_branch) print("push release tag") repo.git.push(upstream.name, tag) print("All done! ✨ 🍰 ✨") def create_release_branch(repo: Repo, version: Version) -> Tuple[Remote, Head]: print("create release branch from upstream master") upstream = get_upstream(repo) upstream.fetch() branch_name = f"release-{version}" release_branch = repo.create_head(branch_name, upstream.refs.master, force=True) upstream.push(refspec=f"{branch_name}:{branch_name}", force=True) release_branch.set_tracking_branch(repo.refs[f"{upstream.name}/{branch_name}"]) release_branch.checkout() return upstream, release_branch def get_upstream(repo: Repo) -> Remote: for remote in repo.remotes: for url in remote.urls: if url.endswith("tox-dev/tox.git"): return remote raise RuntimeError("could not find tox-dev/tox.git remote") def release_changelog(repo: Repo, version: Version) -> Commit: print("generate release commit") check_call(["towncrier", "--yes", "--version", version.public], cwd=str(ROOT_SRC_DIR)) release_commit = repo.index.commit(f"release {version}") return release_commit def tag_release_commit(release_commit, repo, version) -> TagReference: print("tag release commit") existing_tags = [x.name for x in repo.tags] if version in existing_tags: print("delete existing tag {}".format(version)) repo.delete_tag(version) print("create tag {}".format(version)) tag = repo.create_tag(version, ref=release_commit, force=True) return tag if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(prog="release") parser.add_argument("--version", required=True) options = parser.parse_args() main(options.version) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.907512 tox-3.21.4/tests/0000755000175100001710000000000000000000000014043 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/__init__.py0000644000175100001710000000000000000000000016142 0ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/conftest.py0000644000175100001710000000040300000000000016237 0ustar00vstsdocker00000000000000# 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? # noinspection PyUnresolvedReferences from tox._pytestplugin import * # noqa ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.907512 tox-3.21.4/tests/integration/0000755000175100001710000000000000000000000016366 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/integration/test_jython_env_create.py0000644000175100001710000000100200000000000023476 0ustar00vstsdocker00000000000000import pytest # TODO @pytest.mark.skip(reason="needs jython and dev cut of virtualenv") def test_jython_create(initproj, cmd): initproj( "py_jython-0.1", filedefs={ "tox.ini": """ [tox] skipsdist = true envlist = jython commands = python -c 'import sys; print(sys.executable)' """, }, ) result = cmd("--notest", "-vvv") result.assert_success() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/integration/test_package_int.py0000644000175100001710000000765100000000000022255 0ustar00vstsdocker00000000000000"""Tests that require external access (e.g. pip install, virtualenv creation)""" import os import subprocess import sys import pytest if sys.version_info[:2] >= (3, 4): from pathlib import Path else: from pathlib2 import Path from tests.lib import need_git @pytest.mark.network def test_package_setuptools(initproj, cmd): initproj( "magic-0.1", filedefs={ "tox.ini": """\ [tox] isolated_build = true [testenv:.package] basepython = {} """.format( sys.executable, ), "pyproject.toml": """\ [build-system] requires = ["setuptools >= 35.0.2", "setuptools_scm >= 2.0.0, <3"] build-backend = "setuptools.build_meta" """, }, ) run(cmd, "magic-0.1.tar.gz") @pytest.mark.network @need_git @pytest.mark.skipif(sys.version_info < (3, 0), reason="flit is Python 3 only") def test_package_flit(initproj, cmd): initproj( "magic-0.1", filedefs={ "tox.ini": """\ [tox] isolated_build = true [testenv:.package] basepython = {} """.format( sys.executable, ), "pyproject.toml": """\ [build-system] requires = ["flit"] build-backend = "flit.buildapi" [tool.flit.metadata] module = "magic" author = "Happy Harry" author-email = "happy@harry.com" home-page = "https://github.com/happy-harry/is" requires = [ "tox", ] """, ".gitignore": ".tox", }, add_missing_setup_py=False, ) env = os.environ.copy() env["GIT_COMMITTER_NAME"] = "committer joe" env["GIT_AUTHOR_NAME"] = "author joe" env["EMAIL"] = "joe@example.com" subprocess.check_call(["git", "init"], env=env) subprocess.check_call(["git", "add", "-A", "."], env=env) subprocess.check_call(["git", "commit", "-m", "first commit", "--no-gpg-sign"], env=env) run(cmd, "magic-0.1.tar.gz") @pytest.mark.network @pytest.mark.skipif(sys.version_info < (3, 0), reason="poetry is Python 3 only") def test_package_poetry(initproj, cmd): initproj( "magic-0.1", filedefs={ "tox.ini": """\ [tox] isolated_build = true [testenv:.package] basepython = {} """.format( sys.executable, ), "pyproject.toml": """\ [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" [tool.poetry] name = "magic" version = "0.1.0" description = "" authors = ["Name "] """, ".gitignore": ".tox", }, add_missing_setup_py=False, ) run(cmd, "magic-0.1.0.tar.gz") def run(cmd, package): result = cmd("--sdistonly", "-e", "py", "-v", "-v") result.assert_success(is_run_test_env=False) package_venv = (Path() / ".tox" / ".package").resolve() assert ".package create: {}".format(package_venv) in result.outlines, result.out assert "write config to {}".format(package_venv / ".tox-config1") in result.out, result.out package_path = (Path() / ".tox" / "dist" / package).resolve() assert package_path.exists() package_path.unlink() # second call re-uses result2 = cmd("--sdistonly", "-e", "py", "-v", "-v") result2.assert_success(is_run_test_env=False) assert ( ".package reusing: {}".format(package_venv) in result2.outlines ), "Second call output:\n{}First call output:\n{}".format(result2.out, result.out) assert package_path.exists() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/integration/test_parallel_inception.py0000644000175100001710000000310700000000000023644 0ustar00vstsdocker00000000000000def test_parallel_inception(initproj, cmd): initproj( "inception-1.2.3", filedefs={ # the outer config just has one env: graham "tox.ini": """ [tox] envlist = graham skipsdist = True [testenv] commands = python runner.py """, # the inner config has 3 different envs, 1 of them is graham "inner": { "tox.ini": """ [tox] envlist = graham,john,terry skipsdist = True [testenv] commands = python -c 'pass' """, }, # the outer test runs the inner tox and asserts all 3 envs were run "runner.py": """ import os import subprocess import sys os.chdir("inner") p = subprocess.Popen(("tox"), stdout=subprocess.PIPE, universal_newlines=True) stdout, _ = p.communicate() sys.stdout.write(stdout) assert "graham" in stdout assert "john" in stdout assert "terry" in stdout """, }, add_missing_setup_py=False, ) result = cmd("-p", "all", "-o") result.assert_success() # 1 from the outer, 1 from the inner assert result.out.count("graham: commands succeeded") == 2 # those gentlemen are only inside assert result.out.count("john: commands succeeded") == 1 assert result.out.count("terry: commands succeeded") == 1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/integration/test_parallel_interrupt.py0000644000175100001710000000615400000000000023715 0ustar00vstsdocker00000000000000from __future__ import absolute_import, unicode_literals import signal import subprocess import sys from datetime import datetime import pytest from flaky import flaky if sys.version_info[:2] >= (3, 4): from pathlib import Path else: from pathlib2 import Path from tox.constants import INFO from tox.util.main import MAIN_FILE @flaky(max_runs=3) @pytest.mark.skipif(INFO.IS_PYPY, reason="TODO: process numbers work differently on pypy") @pytest.mark.skipif( "sys.platform == 'win32'", reason="triggering SIGINT reliably on Windows is hard", ) def test_parallel_interrupt(initproj, monkeypatch, capfd): monkeypatch.setenv(str("_TOX_SKIP_ENV_CREATION_TEST"), str("1")) monkeypatch.setenv(str("TOX_REPORTER_TIMESTAMP"), str("1")) start = datetime.now() initproj( "pkg123-0.7", filedefs={ "tox.ini": """ [tox] envlist = a, b [testenv] skip_install = True commands = python -c "open('{{envname}}', 'w').write('done'); \ import time; time.sleep(100)" allowlist_externals = {} """.format( sys.executable, ), }, ) process = subprocess.Popen( [sys.executable, MAIN_FILE, "-p", "all"], creationflags=( subprocess.CREATE_NEW_PROCESS_GROUP if sys.platform == "win32" else 0 # needed for Windows signal send ability (CTRL+C) ), ) try: import psutil current_process = psutil.Process(process.pid) except ImportError: current_process = None wait_for_env_startup(process) all_children = [] if current_process is not None: all_children.append(current_process) all_children.extend(current_process.children(recursive=True)) assert len(all_children) >= 1 + 2 + 2, all_children end = datetime.now() - start assert end process.send_signal(signal.CTRL_C_EVENT if sys.platform == "win32" else signal.SIGINT) process.wait() out, err = capfd.readouterr() output = "{}\n{}".format(out, err) assert "KeyboardInterrupt parallel - stopping children" in output, output assert "ERROR: a: parallel child exit code " in output, output assert "ERROR: b: parallel child exit code " in output, output for process in all_children: msg = "{}{}".format(output, "\n".join(repr(i) for i in all_children)) assert not process.is_running(), msg def wait_for_env_startup(process): """the environments will write files once they are up""" signal_files = [Path() / "a", Path() / "b"] found = False while True: if process.poll() is not None: break for signal_file in signal_files: if not signal_file.exists(): break else: found = True break if not found or process.poll() is not None: missing = [f for f in signal_files if not f.exists()] out, _ = process.communicate() assert len(missing), out assert False, out ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/integration/test_provision_int.py0000644000175100001710000001032200000000000022677 0ustar00vstsdocker00000000000000import signal import subprocess import sys import time import pytest if sys.version_info[:2] >= (3, 4): from pathlib import Path else: from pathlib2 import Path from tox.constants import INFO from tox.util.main import MAIN_FILE @pytest.mark.skipif( "sys.platform == 'win32' and sys.version_info < (3,)", reason="does not run on windows with py2", ) def test_provision_missing(initproj, cmd): initproj( "pkg123-0.7", filedefs={ "tox.ini": """\ [tox] skipsdist=True minversion = 3.7.0 requires = setuptools == 40.6.3 [testenv] commands=python -c "import sys; print(sys.executable); raise SystemExit(1)" """, }, ) result = cmd("-e", "py") result.assert_fail() assert "tox.exception.InvocationError" not in result.output() assert not result.err assert ".tox create: " in result.out assert ".tox installdeps: " in result.out assert "py create: " in result.out at = next(at for at, l in enumerate(result.outlines) if l.startswith("py run-test: ")) + 1 meta_python = Path(result.outlines[at]) assert meta_python.exists() @pytest.mark.skipif("sys.platform == 'win32'", reason="pyenv does not exists on Windows") def test_provision_from_pyvenv(initproj, cmd, monkeypatch): initproj( "pkg123-0.7", filedefs={ "tox.ini": """\ [tox] skipsdist=True minversion = 3.7.0 requires = setuptools == 40.6.3 [testenv] commands=python -c "import sys; print(sys.executable); raise SystemExit(1)" """, }, ) monkeypatch.setenv(str("__PYVENV_LAUNCHER__"), sys.executable) result = cmd("-e", "py", "-vv") result.assert_fail() assert ".tox/.tox/bin/python -m virtualenv" in result.out @pytest.mark.skipif(INFO.IS_PYPY, reason="TODO: process numbers work differently on pypy") @pytest.mark.skipif( "sys.platform == 'win32'", reason="triggering SIGINT reliably on Windows is hard", ) @pytest.mark.parametrize("signal_type", [signal.SIGINT, signal.SIGTERM]) def test_provision_interrupt_child(initproj, monkeypatch, capfd, signal_type): monkeypatch.delenv(str("PYTHONPATH"), raising=False) monkeypatch.setenv(str("TOX_REPORTER_TIMESTAMP"), str("1")) initproj( "pkg123-0.7", filedefs={ "tox.ini": """ [tox] skipsdist=True minversion = 3.7.0 requires = setuptools == 40.6.3 tox == 3.7.0 [testenv:b] commands=python -c "import time; open('a', 'w').write('content'); \ time.sleep(10)" basepython = python """, }, ) cmd = [sys.executable, MAIN_FILE, "-v", "-v", "-e", "b"] process = subprocess.Popen( cmd, creationflags=( subprocess.CREATE_NEW_PROCESS_GROUP if sys.platform == "win32" else 0 # needed for Windows signal send ability (CTRL+C) ), ) try: import psutil current_process = psutil.Process(process.pid) except ImportError: current_process = None signal_file = Path() / "a" while not signal_file.exists() and process.poll() is None: time.sleep(0.1) if process.poll() is not None: out, err = process.communicate() assert False, out all_process = [] if current_process is not None: all_process.append(current_process) all_process.extend(current_process.children(recursive=False)) # 1 process for the host tox, 1 for the provisioned assert len(all_process) >= 2, all_process process.send_signal(signal.CTRL_C_EVENT if sys.platform == "win32" else signal_type) process.communicate() out, err = capfd.readouterr() assert ".tox KeyboardInterrupt: from" in out, out for process in all_process: assert not process.is_running(), "{}{}".format( out, "\n".join(repr(i) for i in all_process), ) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1612297739.907512 tox-3.21.4/tests/lib/0000755000175100001710000000000000000000000014611 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/lib/__init__.py0000644000175100001710000000061400000000000016723 0ustar00vstsdocker00000000000000import subprocess import pytest def need_executable(name, check_cmd): def wrapper(fn): try: subprocess.check_output(check_cmd) except OSError: return pytest.mark.skip(reason="{} is not available".format(name))(fn) return fn return wrapper def need_git(fn): return pytest.mark.git(need_executable("git", ("git", "--version"))(fn)) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9115124 tox-3.21.4/tests/unit/0000755000175100001710000000000000000000000015022 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/__init__.py0000644000175100001710000000000000000000000017121 0ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9115124 tox-3.21.4/tests/unit/config/0000755000175100001710000000000000000000000016267 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/config/test_config.py0000644000175100001710000035675100000000000021166 0ustar00vstsdocker00000000000000import os import re import sys from textwrap import dedent import py import pytest from pluggy import PluginManager from six import PY2 import tox from tox.config import ( CommandParser, DepOption, PosargsOption, SectionReader, get_homedir, get_version_info, getcontextname, is_section_substitution, parseconfig, ) from tox.config.parallel import ENV_VAR_KEY_PRIVATE as PARALLEL_ENV_VAR_KEY_PRIVATE from tox.config.parallel import ENV_VAR_KEY_PUBLIC as PARALLEL_ENV_VAR_KEY_PUBLIC class TestVenvConfig: def test_config_parsing_minimal(self, tmpdir, newconfig): config = newconfig( [], """ [testenv:py1] """, ) assert len(config.envconfigs) == 1 assert config.toxworkdir.realpath() == tmpdir.join(".tox").realpath() assert config.envconfigs["py1"].basepython == sys.executable assert config.envconfigs["py1"].deps == [] assert config.envconfigs["py1"].platform == ".*" def test_config_parsing_multienv(self, tmpdir, newconfig): config = newconfig( [], """ [tox] toxworkdir = {} indexserver = xyz = xyz_repo [testenv:py1] deps=hello [testenv:py2] deps= world1 :xyz:http://hello/world """.format( tmpdir, ), ) assert config.toxworkdir == tmpdir assert len(config.envconfigs) == 2 assert config.envconfigs["py1"].envdir == tmpdir.join("py1") dep = config.envconfigs["py1"].deps[0] assert dep.name == "hello" assert dep.indexserver is None assert config.envconfigs["py2"].envdir == tmpdir.join("py2") dep1, dep2 = config.envconfigs["py2"].deps assert dep1.name == "world1" assert dep2.name == "http://hello/world" assert dep2.indexserver.name == "xyz" assert dep2.indexserver.url == "xyz_repo" def test_envdir_set_manually(self, tmpdir, newconfig): config = newconfig( [], """ [testenv:dev] envdir = dev """, ) envconfig = config.envconfigs["dev"] assert envconfig.envdir == tmpdir.join("dev") def test_envdir_set_manually_with_substitutions(self, newconfig): config = newconfig( [], """ [testenv:dev] envdir = {toxworkdir}/foobar """, ) envconfig = config.envconfigs["dev"] assert envconfig.envdir == config.toxworkdir.join("foobar") def test_envdir_set_manually_setup_cfg(self, tmpdir, newconfig): config = newconfig( [], """ [tox:tox] envlist = py36,py37 [testenv] envdir = dev [testenv:py36] envdir = dev36 """, filename="setup.cfg", ) envconfig = config.envconfigs["py36"] assert envconfig.envdir == tmpdir.join("dev36") envconfig = config.envconfigs["py37"] assert envconfig.envdir == tmpdir.join("dev") def test_force_dep_version(self, initproj): """ Make sure we can override dependencies configured in tox.ini when using the command line option --force-dep. """ initproj( "example123-0.5", filedefs={ "tox.ini": """ [tox] [testenv] deps= dep1==1.0 dep2>=2.0 dep3 dep4==4.0 """, }, ) config = parseconfig( ["--force-dep=dep1==1.5", "--force-dep=dep2==2.1", "--force-dep=dep3==3.0"], ) assert config.option.force_dep == ["dep1==1.5", "dep2==2.1", "dep3==3.0"] expected_deps = ["dep1==1.5", "dep2==2.1", "dep3==3.0", "dep4==4.0"] assert expected_deps == [str(x) for x in config.envconfigs["python"].deps] def test_force_dep_with_url(self, initproj): initproj( "example123-0.5", filedefs={ "tox.ini": """ [tox] [testenv] deps= dep1==1.0 https://pypi.org/xyz/pkg1.tar.gz """, }, ) config = parseconfig(["--force-dep=dep1==1.5"]) assert config.option.force_dep == ["dep1==1.5"] expected_deps = ["dep1==1.5", "https://pypi.org/xyz/pkg1.tar.gz"] assert [str(x) for x in config.envconfigs["python"].deps] == expected_deps def test_process_deps(self, newconfig): config = newconfig( [], """ [testenv] deps = -r requirements.txt yapf>=0.25.0,<0.27 # pyup: < 0.27 # disable updates --index-url https://pypi.org/simple pywin32 >=1.0 ; sys_platform == '#my-magic-platform' # so what now -fhttps://pypi.org/packages --global-option=foo -v dep1 --help dep2 """, ) # note that those last two are invalid expected_deps = [ "-rrequirements.txt", "yapf>=0.25.0,<0.27", "--index-url=https://pypi.org/simple", "pywin32 >=1.0 ; sys_platform == '#my-magic-platform'", "-fhttps://pypi.org/packages", "--global-option=foo", "-v dep1", "--help dep2", ] assert [str(x) for x in config.envconfigs["python"].deps] == expected_deps def test_is_same_dep(self): """ Ensure correct parseini._is_same_dep is working with a few samples. """ assert DepOption._is_same_dep("pkg_hello-world3==1.0", "pkg_hello-world3") assert DepOption._is_same_dep("pkg_hello-world3==1.0", "pkg_hello-world3>=2.0") assert DepOption._is_same_dep("pkg_hello-world3==1.0", "pkg_hello-world3>2.0") assert DepOption._is_same_dep("pkg_hello-world3==1.0", "pkg_hello-world3<2.0") assert DepOption._is_same_dep("pkg_hello-world3==1.0", "pkg_hello-world3<=2.0") assert not DepOption._is_same_dep("pkg_hello-world3==1.0", "otherpkg>=2.0") def test_suicide_interrupt_terminate_timeout_set_manually(self, newconfig): config = newconfig( [], """ [testenv:dev] suicide_timeout = 30.0 interrupt_timeout = 5.0 terminate_timeout = 10.0 [testenv:other] """, ) envconfig = config.envconfigs["other"] assert 0.0 == envconfig.suicide_timeout assert 0.3 == envconfig.interrupt_timeout assert 0.2 == envconfig.terminate_timeout envconfig = config.envconfigs["dev"] assert 30.0 == envconfig.suicide_timeout assert 5.0 == envconfig.interrupt_timeout assert 10.0 == envconfig.terminate_timeout class TestConfigPlatform: def test_config_parse_platform(self, newconfig): config = newconfig( [], """ [testenv:py1] platform = linux2 """, ) assert len(config.envconfigs) == 1 assert config.envconfigs["py1"].platform == "linux2" def test_config_parse_platform_rex(self, newconfig, mocksession, monkeypatch): config = newconfig( [], """ [testenv:py1] platform = a123|b123 """, ) mocksession.config = config assert len(config.envconfigs) == 1 venv = mocksession.getvenv("py1") assert not venv.matching_platform() monkeypatch.setattr(sys, "platform", "a123") assert venv.matching_platform() monkeypatch.setattr(sys, "platform", "b123") assert venv.matching_platform() monkeypatch.undo() assert not venv.matching_platform() @pytest.mark.parametrize("plat", ["win", "lin", "osx"]) def test_config_parse_platform_with_factors(self, newconfig, plat): config = newconfig( [], """ [tox] envlist = py27-{win, lin,osx } [testenv] platform = win: win32 lin: linux2 """, ) assert len(config.envconfigs) == 3 platform = config.envconfigs["py27-" + plat].platform expected = {"win": "win32", "lin": "linux2", "osx": ""}.get(plat) assert platform == expected def test_platform_install_command(self, newconfig, mocksession, monkeypatch): # Expanded from docs/example/platform.html config = newconfig( [], """ [tox] envlist = py{27,36}-{mylinux,mymacos,mywindows} [testenv] platform = mylinux: linux mymacos: darwin mywindows: win32 deps = mylinux,mymacos: py==1.4.32 mywindows: py==1.4.30 install_command = mylinux: python -m pip install {packages} distro mywindows: python -m pip install {packages} pywin32 commands= mylinux: echo Linus mymacos: echo Steve mywindows: echo Bill """, ) mocksession.config = config assert len(config.envconfigs) == 6 monkeypatch.setattr(sys, "platform", "linux") venv = mocksession.getvenv("py27-mylinux") assert venv.envconfig._reader.factors == {"py27", "mylinux"} assert venv.matching_platform() assert str(venv.envconfig.deps[0]) == "py==1.4.32" assert venv.envconfig.install_command == [ "python", "-m", "pip", "install", "{packages}", "distro", ] assert venv.envconfig.commands[0] == ["echo", "Linus"] venv = mocksession.getvenv("py27-mymacos") assert venv.envconfig._reader.factors == {"py27", "mymacos"} assert not venv.matching_platform() assert str(venv.envconfig.deps[0]) == "py==1.4.32" assert venv.envconfig.install_command == [ "python", "-m", "pip", "install", "{opts}", "{packages}", ] assert venv.envconfig.commands[0] == ["echo", "Steve"] venv = mocksession.getvenv("py27-mywindows") assert venv.envconfig._reader.factors == {"py27", "mywindows"} assert not venv.matching_platform() assert str(venv.envconfig.deps[0]) == "py==1.4.30" assert venv.envconfig.install_command == [ "python", "-m", "pip", "install", "{packages}", "pywin32", ] assert venv.envconfig.commands[0] == ["echo", "Bill"] monkeypatch.undo() monkeypatch.setattr(sys, "platform", "darwin") venv = mocksession.getvenv("py27-mymacos") assert venv.envconfig.install_command == [ "python", "-m", "pip", "install", "{opts}", "{packages}", ] monkeypatch.undo() class TestConfigPackage: def test_defaults(self, tmpdir, newconfig): config = newconfig([], "") assert config.setupdir.realpath() == tmpdir.realpath() assert config.toxworkdir.realpath() == tmpdir.join(".tox").realpath() envconfig = config.envconfigs["python"] assert envconfig.args_are_paths assert not envconfig.recreate assert not envconfig.pip_pre def test_defaults_distshare(self, newconfig): config = newconfig([], "") assert config.distshare == config.homedir.join(".tox", "distshare") def test_defaults_changed_dir(self, tmpdir, newconfig): with tmpdir.mkdir("abc").as_cwd(): config = newconfig([], "") assert config.setupdir.realpath() == tmpdir.realpath() assert config.toxworkdir.realpath() == tmpdir.join(".tox").realpath() def test_project_paths(self, tmpdir, newconfig): config = newconfig( """ [tox] toxworkdir={} """.format( tmpdir, ), ) assert config.toxworkdir == tmpdir class TestParseconfig: def test_search_parents(self, tmpdir): b = tmpdir.mkdir("a").mkdir("b") toxinipath = tmpdir.ensure("tox.ini") with b.as_cwd(): config = parseconfig([]) assert config.toxinipath == toxinipath def test_explicit_config_path(self, tmpdir): """ Test explicitly setting config path, both with and without the filename """ path = tmpdir.mkdir("tox_tmp_directory") config_file_path = path.ensure("tox.ini") config = parseconfig(["-c", str(config_file_path)]) assert config.toxinipath == config_file_path # Passing directory of the config file should also be possible # ('tox.ini' filename is assumed) config = parseconfig(["-c", str(path)]) assert config.toxinipath == config_file_path @pytest.mark.skipif(sys.platform == "win32", reason="no symlinks on Windows") def test_workdir_gets_resolved(self, tmp_path, monkeypatch): """ Test explicitly setting config path, both with and without the filename """ real = tmp_path / "real" real.mkdir() symlink = tmp_path / "link" symlink.symlink_to(real) (tmp_path / "tox.ini").touch() monkeypatch.chdir(tmp_path) config = parseconfig(["--workdir", str(symlink)]) assert config.toxworkdir == real def test_get_homedir(monkeypatch): monkeypatch.setattr(py.path.local, "_gethomedir", classmethod(lambda x: {}[1])) assert not get_homedir() monkeypatch.setattr(py.path.local, "_gethomedir", classmethod(lambda x: 0 / 0)) assert not get_homedir() monkeypatch.setattr(py.path.local, "_gethomedir", classmethod(lambda x: "123")) assert get_homedir() == "123" class TestGetcontextname: def test_blank(self, monkeypatch): monkeypatch.setattr(os, "environ", {}) assert getcontextname() is None def test_jenkins(self, monkeypatch): monkeypatch.setattr(os, "environ", {"JENKINS_URL": "xyz"}) assert getcontextname() == "jenkins" def test_hudson_legacy(self, monkeypatch): monkeypatch.setattr(os, "environ", {"HUDSON_URL": "xyz"}) assert getcontextname() == "jenkins" class TestIniParserAgainstCommandsKey: """Test parsing commands with substitutions""" def test_command_substitution_recursion_error_same_section(self, newconfig): expected = r"\('testenv:a', 'commands'\) already in \[\('testenv:a', 'commands'\)\]" with pytest.raises(tox.exception.ConfigError, match=expected): newconfig( """ [testenv:a] commands = {[testenv:a]commands} """, ) def test_command_substitution_recursion_error_other_section(self, newconfig): expected = ( r"\('testenv:py27', 'commands'\) already in " r"\[\('testenv:py27', 'commands'\), " r"\('testenv:base', 'foo'\)\]" ) with pytest.raises(tox.exception.ConfigError, match=expected): newconfig( """ [testenv:base] foo = {[testenv:py27]commands} [testenv:py27] commands = {[testenv:base]foo} """, ) def test_command_substitution_recursion_error_unnecessary(self, newconfig): # TODO: There is no reason for this recursion error to occur, so it # could be optimised away, or emit a warning, or give a custom error expected = ( r"\('testenv:base', 'foo'\) already in " r"\[\('testenv:py27', 'commands'\), \('testenv:base', 'foo'\)\]" ) with pytest.raises(tox.exception.ConfigError, match=expected): newconfig( """ [testenv:base] foo = {[testenv:base]foo} [testenv:py27] bar = {[testenv:base]foo} setenv = FOO = foo commands = {env:FOO:{[testenv:base]foo}} """, ) def test_command_missing_substitution_simple(self, newconfig): config = newconfig( """ [testenv:py27] commands = {env:{env:FOO}} """, ) envconfig = config.envconfigs["py27"] expected = "MissingSubstitution: FOO" with pytest.raises(tox.exception.MissingSubstitution, match=expected): envconfig.commands def test_command_missing_substitution_setenv(self, newconfig): config = newconfig( """ [testenv:py27] setenv = FOO = {env:{env:FOO}} commands = {env:FOO} """, ) envconfig = config.envconfigs["py27"] expected = "MissingSubstitution: FOO" with pytest.raises(tox.exception.MissingSubstitution, match=expected): envconfig.setenv["FOO"] with pytest.raises(tox.exception.MissingSubstitution, match=expected): envconfig.commands def test_command_missing_substitution_inherit(self, newconfig): config = newconfig( """ [testenv] setenv = FOO = {[testenv:py27]commands} [testenv:py27] commands = {env:FOO} """, ) envconfig = config.envconfigs["py27"] expected = "MissingSubstitution: FOO" with pytest.raises(tox.exception.MissingSubstitution, match=expected): envconfig.commands with pytest.raises(tox.exception.MissingSubstitution, match=expected): envconfig.setenv["FOO"] def test_command_missing_substitution_other_section(self, newconfig): config = newconfig( """ [testenv:base] bar = {[testenv:py27]foo} [testenv:py27] foo = {env:FOO} setenv = FOO = {[testenv:base]bar} commands = {env:FOO} """, ) envconfig = config.envconfigs["py27"] expected = "MissingSubstitution: FOO" with pytest.raises(tox.exception.MissingSubstitution, match=expected): envconfig.commands with pytest.raises(tox.exception.MissingSubstitution, match=expected): envconfig.setenv["FOO"] def test_command_missing_substitution_multi_env(self, newconfig): config = newconfig( """ [testenv:py27] setenv = FOO = {env:BAR} BAR = {env:FOO} commands = {env:BAR} """, ) envconfig = config.envconfigs["py27"] expected = "MissingSubstitution: BAR" with pytest.raises(tox.exception.MissingSubstitution, match=expected): envconfig.commands expected = "MissingSubstitution: FOO" with pytest.raises(tox.exception.MissingSubstitution, match=expected): envconfig.setenv["FOO"] def test_command_missing_substitution_complex(self, newconfig): config = newconfig( """ [testenv:base] bar = {env:BAR} setenv = BAR = {[testenv:py27]foo} [testenv:py27] foo = {env:FOO} setenv = FOO = {[testenv:base]bar} commands = {env:FOO} """, ) envconfig = config.envconfigs["py27"] expected = "MissingSubstitution: BAR" with pytest.raises(tox.exception.MissingSubstitution, match=expected): envconfig.setenv["FOO"] with pytest.raises(tox.exception.MissingSubstitution, match=expected): envconfig.commands def test_command_substitution_from_other_section(self, newconfig): config = newconfig( """ [section] key = whatever [testenv] commands = echo {[section]key} """, ) reader = SectionReader("testenv", config._cfg) x = reader.getargvlist("commands") assert x == [["echo", "whatever"]] def test_command_substitution_from_other_section_multiline(self, newconfig): """Ensure referenced multiline commands form from other section injected as multiple commands.""" config = newconfig( """ [section] commands = cmd1 param11 param12 # comment is omitted cmd2 param21 \ param22 [base] commands = cmd 1 \ 2 3 4 cmd 2 [testenv] commands = {[section]commands} {[section]commands} # comment is omitted echo {[base]commands} """, ) reader = SectionReader("testenv", config._cfg) x = reader.getargvlist("commands") expected_deps = [ "cmd1 param11 param12".split(), "cmd2 param21 param22".split(), "cmd1 param11 param12".split(), "cmd2 param21 param22".split(), ["echo", "cmd", "1", "2", "3", "4", "cmd", "2"], ] assert x == expected_deps def test_command_substitution_from_other_section_posargs(self, newconfig): """Ensure subsitition from other section with posargs succeeds""" config = newconfig( """ [section] key = thing {posargs} arg2 [testenv] commands = {[section]key} """, ) reader = SectionReader("testenv", config._cfg) reader.addsubstitutions(["argpos"]) x = reader.getargvlist("commands") assert x == [["thing", "argpos", "arg2"]] def test_command_section_and_posargs_substitution(self, newconfig): """Ensure subsitition from other section with posargs succeeds""" config = newconfig( """ [section] key = thing arg1 [testenv] commands = {[section]key} {posargs} endarg """, ) reader = SectionReader("testenv", config._cfg) reader.addsubstitutions(["argpos"]) x = reader.getargvlist("commands") assert x == [["thing", "arg1", "argpos", "endarg"]] def test_command_posargs_with_colon(self, newconfig): """Ensure posargs with default containing : succeeds""" config = newconfig( r""" [testenv] commands = pytest {posargs:default with : colon after} """, ) reader = SectionReader("testenv", config._cfg) x = reader.getargvlist("commands") assert x[0] == ["pytest", "default", "with", ":", "colon", "after"] def test_command_missing_substitution(self, newconfig): config = newconfig( """ [testenv:a] setenv = FOO = foo commands = {env:FOO} """, ) reader = SectionReader("testenv:a", config._cfg) expected = "MissingSubstitution: FOO" with pytest.raises(tox.exception.MissingSubstitution, match=expected): reader.getargvlist("commands") def test_command_env_substitution(self, newconfig): """Ensure referenced {env:key:default} values are substituted correctly.""" config = newconfig( """ [testenv:py27] setenv = TEST=testvalue commands = ls {env:TEST} """, ) envconfig = config.envconfigs["py27"] assert envconfig.commands == [["ls", "testvalue"]] assert envconfig.setenv["TEST"] == "testvalue" def test_command_env_substitution_posargs(self, newconfig): """Ensure {posargs} values are substituted correctly.""" config = newconfig( """ [testenv:py27] setenv = TEST={posargs:default} commands = ls {env:TEST} """, ) envconfig = config.envconfigs["py27"] assert envconfig.setenv["TEST"] == "default" assert envconfig.commands == [["ls", "default"]] def test_command_env_substitution_posargs_with_colon(self, newconfig): """Ensure {posargs} values are substituted correctly.""" config = newconfig( """ [testenv:py27] setenv = TEST=pytest {posargs:default with:colon after} commands = ls {env:TEST} """, ) envconfig = config.envconfigs["py27"] assert envconfig.setenv["TEST"] == "pytest default with:colon after" assert envconfig.commands == [["ls", "pytest", "default", "with:colon", "after"]] def test_command_env_substitution_posargs_with_spaced_colon(self, newconfig): """Ensure {posargs} values are substituted correctly.""" config = newconfig( """ [testenv:py27] setenv = TEST=pytest {posargs:default with : colon after} commands = ls {env:TEST} """, ) envconfig = config.envconfigs["py27"] assert envconfig.setenv["TEST"] == "pytest default with : colon after" assert envconfig.commands == [["ls", "pytest", "default", "with", ":", "colon", "after"]] def test_command_env_substitution_global(self, newconfig): """Ensure referenced {env:key:default} values are substituted correctly.""" config = newconfig( """ [testenv] setenv = FOO = bar commands = echo {env:FOO} """, ) envconfig = config.envconfigs["python"] assert envconfig.commands == [["echo", "bar"]] def test_command_env_substitution_default_escape(self, newconfig): """Ensure literal { and } in default of {env:key:default} values.""" config = newconfig( r""" [testenv] commands = echo {env:FOO:\{bar\}} """, ) envconfig = config.envconfigs["python"] assert envconfig.commands == [["echo", "{bar}"]] def test_regression_issue595(self, newconfig): config = newconfig( """ [tox] envlist = foo [testenv] setenv = VAR = x [testenv:bar] setenv = {[testenv]setenv} [testenv:baz] setenv = """, ) assert config.envconfigs["foo"].setenv["VAR"] == "x" assert config.envconfigs["bar"].setenv["VAR"] == "x" assert "VAR" not in config.envconfigs["baz"].setenv class TestIniParser: def test_getstring_single(self, newconfig): config = newconfig( """ [section] key=value """, ) reader = SectionReader("section", config._cfg) x = reader.getstring("key") assert x == "value" assert not reader.getstring("hello") x = reader.getstring("hello", "world") assert x == "world" def test_substitution_empty(self, newconfig): config = newconfig( """ [mydefault] key2={} """, ) reader = SectionReader("mydefault", config._cfg, fallbacksections=["mydefault"]) assert reader is not None with pytest.raises(tox.exception.ConfigError, match="no substitution type provided"): reader.getstring("key2") def test_substitution_colon_prefix(self, newconfig): config = newconfig( """ [mydefault] key2={:abc} """, ) reader = SectionReader("mydefault", config._cfg, fallbacksections=["mydefault"]) assert reader is not None with pytest.raises( tox.exception.ConfigError, match="Malformed substitution with prefix ':'", ): reader.getstring("key2") def test_missing_substitution(self, newconfig): config = newconfig( """ [mydefault] key2={xyz} """, ) reader = SectionReader("mydefault", config._cfg, fallbacksections=["mydefault"]) assert reader is not None with pytest.raises(tox.exception.ConfigError, match="substitution key '.*' not found"): reader.getstring("key2") def test_getstring_fallback_sections(self, newconfig): config = newconfig( """ [mydefault] key2=value2 [section] key=value """, ) reader = SectionReader("section", config._cfg, fallbacksections=["mydefault"]) x = reader.getstring("key2") assert x == "value2" x = reader.getstring("key3") assert not x x = reader.getstring("key3", "world") assert x == "world" def test_getstring_substitution(self, newconfig): config = newconfig( """ [mydefault] key2={value2} [section] key={value} """, ) reader = SectionReader("section", config._cfg, fallbacksections=["mydefault"]) reader.addsubstitutions(value="newvalue", value2="newvalue2") x = reader.getstring("key2") assert x == "newvalue2" x = reader.getstring("key3") assert not x x = reader.getstring("key3", "{value2}") assert x == "newvalue2" def test_getlist(self, newconfig): config = newconfig( """ [section] key2= item1 {item2} """, ) reader = SectionReader("section", config._cfg) reader.addsubstitutions(item1="not", item2="grr") x = reader.getlist("key2") assert x == ["item1", "grr"] def test_getdict(self, newconfig): config = newconfig( """ [section] key2= key1=item1 key2={item2} """, ) reader = SectionReader("section", config._cfg) reader.addsubstitutions(item1="not", item2="grr") x = reader.getdict("key2") assert "key1" in x assert "key2" in x assert x["key1"] == "item1" assert x["key2"] == "grr" x = reader.getdict("key3", {1: 2}) assert x == {1: 2} def test_normal_env_sub_works(self, monkeypatch, newconfig): monkeypatch.setenv("VAR", "hello") config = newconfig("[section]\nkey={env:VAR}") assert SectionReader("section", config._cfg).getstring("key") == "hello" def test_missing_env_sub_raises_config_error_in_non_testenv(self, newconfig): config = newconfig("[section]\nkey={env:VAR}") with pytest.raises(tox.exception.ConfigError): SectionReader("section", config._cfg).getstring("key") def test_missing_env_sub_populates_missing_subs(self, newconfig): config = newconfig("[testenv:foo]\ncommands={env:VAR}") print(SectionReader("section", config._cfg).getstring("commands")) assert "commands" in config.envconfigs["foo"]._missing_subs missing_exception = config.envconfigs["foo"]._missing_subs["commands"] assert missing_exception.name == "VAR" def test_getstring_environment_substitution_with_default(self, monkeypatch, newconfig): monkeypatch.setenv("KEY1", "hello") config = newconfig( """ [section] key1={env:KEY1:DEFAULT_VALUE} key2={env:KEY2:DEFAULT_VALUE} key3={env:KEY3:} """, ) reader = SectionReader("section", config._cfg) x = reader.getstring("key1") assert x == "hello" x = reader.getstring("key2") assert x == "DEFAULT_VALUE" x = reader.getstring("key3") assert x == "" def test_value_matches_section_substitution(self): assert is_section_substitution("{[setup]commands}") def test_value_doesn_match_section_substitution(self): assert is_section_substitution("{[ ]commands}") is None assert is_section_substitution("{[setup]}") is None assert is_section_substitution("{[setup] commands}") is None def test_getstring_other_section_substitution(self, newconfig): config = newconfig( """ [section] key = rue [testenv] key = t{[section]key} """, ) reader = SectionReader("testenv", config._cfg) x = reader.getstring("key") assert x == "true" def test_argvlist(self, newconfig): config = newconfig( """ [section] key2= cmd1 {item1} {item2} cmd2 {item2} """, ) reader = SectionReader("section", config._cfg) reader.addsubstitutions(item1="with space", item2="grr") assert reader.getargvlist("key1") == [] x = reader.getargvlist("key2") assert x == [["cmd1", "with", "space", "grr"], ["cmd2", "grr"]] def test_argvlist_windows_escaping(self, newconfig): config = newconfig( """ [section] comm = pytest {posargs} """, ) reader = SectionReader("section", config._cfg) reader.addsubstitutions([r"hello\this"]) argv = reader.getargv("comm") assert argv == ["pytest", "hello\\this"] def test_argvlist_multiline(self, newconfig): config = newconfig( """ [section] key2= cmd1 {item1} \ {item2} """, ) reader = SectionReader("section", config._cfg) reader.addsubstitutions(item1="with space", item2="grr") assert reader.getargvlist("key1") == [] x = reader.getargvlist("key2") assert x == [["cmd1", "with", "space", "grr"]] def test_argvlist_quoting_in_command(self, newconfig): config = newconfig( """ [section] key1= cmd1 'part one' \ 'part two' """, ) reader = SectionReader("section", config._cfg) x = reader.getargvlist("key1") assert x == [["cmd1", "part one", "part two"]] def test_argvlist_comment_after_command(self, newconfig): config = newconfig( """ [section] key1= cmd1 --flag # run the flag on the command """, ) reader = SectionReader("section", config._cfg) x = reader.getargvlist("key1") assert x == [["cmd1", "--flag"]] def test_argvlist_command_contains_hash(self, newconfig): config = newconfig( """ [section] key1= cmd1 --re "use the # symbol for an arg" """, ) reader = SectionReader("section", config._cfg) x = reader.getargvlist("key1") assert x == [["cmd1", "--re", "use the # symbol for an arg"]] def test_argvlist_positional_substitution(self, newconfig): config = newconfig( """ [section] key2= cmd1 [] cmd2 {posargs:{item2} \ other} """, ) reader = SectionReader("section", config._cfg) posargs = ["hello", "world"] reader.addsubstitutions(posargs, item2="value2") assert reader.getargvlist("key1") == [] argvlist = reader.getargvlist("key2") assert argvlist[0] == ["cmd1"] + posargs assert argvlist[1] == ["cmd2"] + posargs reader = SectionReader("section", config._cfg) reader.addsubstitutions([], item2="value2") assert reader.getargvlist("key1") == [] argvlist = reader.getargvlist("key2") assert argvlist[0] == ["cmd1"] assert argvlist[1] == ["cmd2", "value2", "other"] def test_argvlist_quoted_posargs(self, newconfig): config = newconfig( """ [section] key2= cmd1 --foo-args='{posargs}' cmd2 -f '{posargs}' cmd3 -f {posargs} """, ) reader = SectionReader("section", config._cfg) reader.addsubstitutions(["foo", "bar"]) assert reader.getargvlist("key1") == [] x = reader.getargvlist("key2") expected_deps = [ ["cmd1", "--foo-args=foo bar"], ["cmd2", "-f", "foo bar"], ["cmd3", "-f", "foo", "bar"], ] assert x == expected_deps def test_argvlist_posargs_with_quotes(self, newconfig): config = newconfig( """ [section] key2= cmd1 -f {posargs} """, ) # The operating system APIs for launching processes differ between # Windows and other OSs. On Windows, the command line is passed as a # string (and not a list of strings). Python uses the MS C runtime # rules for splitting this string into `sys.argv`, and those rules # differ from POSIX shell rules in their treatment of quoted arguments. if sys.platform.startswith("win"): substitutions = ["foo", "'bar", "baz'"] else: substitutions = ["foo", "bar baz"] reader = SectionReader("section", config._cfg) reader.addsubstitutions(substitutions) assert reader.getargvlist("key1") == [] x = reader.getargvlist("key2") assert x == [["cmd1", "-f", "foo", "bar baz"]] def test_positional_arguments_are_only_replaced_when_standing_alone(self, newconfig): config = newconfig( """ [section] key= cmd0 [] cmd1 -m '[abc]' cmd2 -m '\'something\'' [] cmd3 something[]else """, ) reader = SectionReader("section", config._cfg) posargs = ["hello", "world"] reader.addsubstitutions(posargs) argvlist = reader.getargvlist("key") assert argvlist[0] == ["cmd0"] + posargs assert argvlist[1] == ["cmd1", "-m", "[abc]"] assert argvlist[2] == ["cmd2", "-m", "something"] + posargs assert argvlist[3] == ["cmd3", "something[]else"] def test_posargs_are_added_escaped_issue310(self, newconfig): config = newconfig( """ [section] key= cmd0 {posargs} """, ) reader = SectionReader("section", config._cfg) posargs = ["hello world", "--x==y z", "--format=%(code)s: %(text)s"] reader.addsubstitutions(posargs) argvlist = reader.getargvlist("key") assert argvlist[0] == ["cmd0"] + posargs def test_substitution_with_multiple_words(self, newconfig): inisource = """ [section] key = pytest -n5 --junitxml={envlogdir}/junit-{envname}.xml [] """ config = newconfig(inisource) reader = SectionReader("section", config._cfg) posargs = ["hello", "world"] reader.addsubstitutions(posargs, envlogdir="ENV_LOG_DIR", envname="ENV_NAME") expected = ["pytest", "-n5", "--junitxml=ENV_LOG_DIR/junit-ENV_NAME.xml", "hello", "world"] assert reader.getargvlist("key")[0] == expected def test_getargv(self, newconfig): config = newconfig( """ [section] key=some command "with quoting" """, ) reader = SectionReader("section", config._cfg) expected = ["some", "command", "with quoting"] assert reader.getargv("key") == expected def test_getpath(self, tmpdir, newconfig): config = newconfig( """ [section] path1={HELLO} """, ) reader = SectionReader("section", config._cfg) reader.addsubstitutions(toxinidir=tmpdir, HELLO="mypath") x = reader.getpath("path1", tmpdir) assert x == tmpdir.join("mypath") def test_getbool(self, newconfig): config = newconfig( """ [section] key1=True key2=False key1a=true key2a=falsE key5=yes """, ) reader = SectionReader("section", config._cfg) assert reader.getbool("key1") is True assert reader.getbool("key1a") is True assert reader.getbool("key2") is False assert reader.getbool("key2a") is False with pytest.raises(KeyError): reader.getbool("key3") with pytest.raises(tox.exception.ConfigError) as excinfo: reader.getbool("key5") (msg,) = excinfo.value.args assert msg == "key5: boolean value 'yes' needs to be 'True' or 'False'" def test_expand_section_name(self, newconfig): config = newconfig( """ [testenv:custom{,-one,-two,-three}-{four,five}-six] """, ) assert "testenv:custom-one-five-six" in config._cfg.sections assert "testenv:custom-four-six" in config._cfg.sections assert "testenv:custom-{one,two,three}-{four,five}-six" not in config._cfg.sections class TestIniParserPrefix: def test_basic_section_access(self, newconfig): config = newconfig( """ [p:section] key=value """, ) reader = SectionReader("section", config._cfg, prefix="p") x = reader.getstring("key") assert x == "value" assert not reader.getstring("hello") x = reader.getstring("hello", "world") assert x == "world" def test_fallback_sections(self, newconfig): config = newconfig( """ [p:mydefault] key2=value2 [p:section] key=value """, ) reader = SectionReader( "section", config._cfg, prefix="p", fallbacksections=["p:mydefault"], ) x = reader.getstring("key2") assert x == "value2" x = reader.getstring("key3") assert not x x = reader.getstring("key3", "world") assert x == "world" def test_value_matches_prefixed_section_substitution(self): assert is_section_substitution("{[p:setup]commands}") def test_value_doesn_match_prefixed_section_substitution(self): assert is_section_substitution("{[p: ]commands}") is None assert is_section_substitution("{[p:setup]}") is None assert is_section_substitution("{[p:setup] commands}") is None def test_other_section_substitution(self, newconfig): config = newconfig( """ [p:section] key = rue [p:testenv] key = t{[p:section]key} """, ) reader = SectionReader("testenv", config._cfg, prefix="p") x = reader.getstring("key") assert x == "true" class TestConfigTestEnv: def test_commentchars_issue33(self, newconfig): config = newconfig( """ [testenv] # hello deps = http://abc#123 commands= python -c "x ; y" """, ) envconfig = config.envconfigs["python"] assert envconfig.deps[0].name == "http://abc#123" assert envconfig.commands[0] == ["python", "-c", "x ; y"] def test_defaults(self, newconfig): config = newconfig( """ [testenv] commands= xyz --abc """, ) assert len(config.envconfigs) == 1 envconfig = config.envconfigs["python"] assert envconfig.commands == [["xyz", "--abc"]] assert envconfig.changedir == config.setupdir assert envconfig.sitepackages is False assert envconfig.usedevelop is False assert envconfig.ignore_errors is False assert envconfig.envlogdir == envconfig.envdir.join("log") assert set(envconfig.setenv.definitions.keys()) == { "PYTHONHASHSEED", "TOX_ENV_NAME", "TOX_ENV_DIR", } hashseed = envconfig.setenv["PYTHONHASHSEED"] assert isinstance(hashseed, str) # The following line checks that hashseed parses to an integer. int_hashseed = int(hashseed) # hashseed is random by default, so we can't assert a specific value. assert int_hashseed > 0 assert envconfig.ignore_outcome is False def test_sitepackages_switch(self, newconfig): config = newconfig(["--sitepackages"], "") envconfig = config.envconfigs["python"] assert envconfig.sitepackages is True def test_installpkg_tops_develop(self, newconfig): config = newconfig( ["--installpkg=abc"], """ [testenv] usedevelop = True """, ) assert not config.envconfigs["python"].usedevelop def test_specific_command_overrides(self, newconfig): config = newconfig( """ [testenv] commands=xyz [testenv:py] commands=abc """, ) assert len(config.envconfigs) == 1 envconfig = config.envconfigs["py"] assert envconfig.commands == [["abc"]] def test_allowlist_externals(self, newconfig): config = newconfig( """ [testenv] allowlist_externals = xyz commands=xyz [testenv:x] [testenv:py] allowlist_externals = xyz2 commands=abc """, ) assert len(config.envconfigs) == 2 envconfig = config.envconfigs["py"] assert envconfig.commands == [["abc"]] assert envconfig.allowlist_externals == ["xyz2"] envconfig = config.envconfigs["x"] assert envconfig.allowlist_externals == ["xyz"] def test_changedir(self, newconfig): config = newconfig( """ [testenv] changedir=xyz """, ) assert len(config.envconfigs) == 1 envconfig = config.envconfigs["python"] assert envconfig.changedir.basename == "xyz" assert envconfig.changedir == config.toxinidir.join("xyz") def test_ignore_errors(self, newconfig): config = newconfig( """ [testenv] ignore_errors=True """, ) assert len(config.envconfigs) == 1 envconfig = config.envconfigs["python"] assert envconfig.ignore_errors is True def test_envbindir(self, newconfig): config = newconfig( """ [testenv] basepython=python """, ) assert len(config.envconfigs) == 1 envconfig = config.envconfigs["python"] assert envconfig.envpython == envconfig.envbindir.join("python") @pytest.mark.parametrize("bp", ["jython", "pypy", "pypy3"]) def test_envbindir_jython(self, newconfig, bp): config = newconfig( """ [testenv] basepython={} """.format( bp, ), ) assert len(config.envconfigs) == 1 envconfig = config.envconfigs["python"] # on win32 and linux virtualenv uses "bin" for pypy/jython assert envconfig.envbindir.basename == "bin" if bp == "jython": assert envconfig.envpython == envconfig.envbindir.join(bp) @pytest.mark.parametrize("plat", ["win32", "linux2"]) 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") config = newconfig( """ [testenv] passenv = A123* # isolated comment B?23 """, ) assert len(config.envconfigs) == 1 envconfig = config.envconfigs["python"] if plat == "win32": assert "PATHEXT" in envconfig.passenv assert "SYSTEMDRIVE" in envconfig.passenv assert "SYSTEMROOT" in envconfig.passenv assert "COMSPEC" in envconfig.passenv assert "TEMP" in envconfig.passenv assert "TMP" in envconfig.passenv assert "NUMBER_OF_PROCESSORS" in envconfig.passenv assert "PROCESSOR_ARCHITECTURE" in envconfig.passenv assert "USERPROFILE" in envconfig.passenv assert "MSYSTEM" in envconfig.passenv else: assert "TMPDIR" in envconfig.passenv assert "CURL_CA_BUNDLE" in envconfig.passenv assert "PATH" in envconfig.passenv assert "PIP_INDEX_URL" in envconfig.passenv assert "PIP_EXTRA_INDEX_URL" in envconfig.passenv assert "REQUESTS_CA_BUNDLE" in envconfig.passenv assert "SSL_CERT_FILE" in envconfig.passenv assert "LANG" in envconfig.passenv assert "LANGUAGE" in envconfig.passenv assert "LD_LIBRARY_PATH" in envconfig.passenv assert "HTTP_PROXY" in envconfig.passenv assert "HTTPS_PROXY" in envconfig.passenv assert "NO_PROXY" in envconfig.passenv assert PARALLEL_ENV_VAR_KEY_PUBLIC in envconfig.passenv assert PARALLEL_ENV_VAR_KEY_PRIVATE not in envconfig.passenv assert "A123A" in envconfig.passenv assert "A123B" in envconfig.passenv @pytest.mark.parametrize("plat", ["win32", "linux2"]) 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") config = newconfig( """ [testenv] passenv = # comment A123* B?23 """, ) assert len(config.envconfigs) == 1 envconfig = config.envconfigs["python"] if plat == "win32": assert "PATHEXT" in envconfig.passenv assert "SYSTEMDRIVE" in envconfig.passenv assert "SYSTEMROOT" in envconfig.passenv assert "TEMP" in envconfig.passenv assert "TMP" in envconfig.passenv else: assert "TMPDIR" in envconfig.passenv assert "PATH" in envconfig.passenv assert "PIP_INDEX_URL" in envconfig.passenv assert "LANG" in envconfig.passenv assert "LANGUAGE" in envconfig.passenv assert "A123A" in envconfig.passenv assert "A123B" in envconfig.passenv def test_passenv_with_factor(self, newconfig, monkeypatch): monkeypatch.setenv("A123A", "a") monkeypatch.setenv("A123B", "b") monkeypatch.setenv("A123C", "c") monkeypatch.setenv("A123D", "d") monkeypatch.setenv("BX23", "0") monkeypatch.setenv("CCA43", "3") monkeypatch.setenv("CB21", "4") config = newconfig( """ [tox] envlist = {x1,x2} [testenv] passenv = x1: A123A CC* x1: CB21 # passed to both environments A123C x2: A123B A123D """, ) assert len(config.envconfigs) == 2 assert "A123A" in config.envconfigs["x1"].passenv assert "A123C" in config.envconfigs["x1"].passenv assert "CCA43" in config.envconfigs["x1"].passenv assert "CB21" in config.envconfigs["x1"].passenv assert "A123B" not in config.envconfigs["x1"].passenv assert "A123D" not in config.envconfigs["x1"].passenv assert "BX23" not in config.envconfigs["x1"].passenv assert "A123B" in config.envconfigs["x2"].passenv assert "A123D" in config.envconfigs["x2"].passenv assert "A123A" not in config.envconfigs["x2"].passenv assert "A123C" in config.envconfigs["x2"].passenv assert "CCA43" not in config.envconfigs["x2"].passenv assert "CB21" not in config.envconfigs["x2"].passenv assert "BX23" not in config.envconfigs["x2"].passenv def test_passenv_from_global_env(self, newconfig, monkeypatch): monkeypatch.setenv("A1", "a1") monkeypatch.setenv("A2", "a2") monkeypatch.setenv("TOX_TESTENV_PASSENV", "A1") config = newconfig( """ [testenv] passenv = A2 """, ) env = config.envconfigs["python"] assert "A1" in env.passenv assert "A2" in env.passenv def test_passenv_glob_from_global_env(self, newconfig, monkeypatch): monkeypatch.setenv("A1", "a1") monkeypatch.setenv("A2", "a2") monkeypatch.setenv("TOX_TESTENV_PASSENV", "A*") config = newconfig( """ [testenv] """, ) env = config.envconfigs["python"] assert "A1" in env.passenv assert "A2" in env.passenv def test_no_spinner(self, newconfig, monkeypatch): monkeypatch.setenv("TOX_PARALLEL_NO_SPINNER", "1") config = newconfig( """ [testenv] passenv = TOX_PARALLEL_NO_SPINNER """, ) env = config.envconfigs["python"] assert "TOX_PARALLEL_NO_SPINNER" in env.passenv def test_changedir_override(self, newconfig): config = newconfig( """ [testenv] changedir=xyz [testenv:python] changedir=abc basepython=python3.6 """, ) assert len(config.envconfigs) == 1 envconfig = config.envconfigs["python"] assert envconfig.changedir.basename == "abc" assert envconfig.changedir == config.setupdir.join("abc") def test_install_command_setting(self, newconfig): config = newconfig( """ [testenv] install_command=some_install {packages} """, ) envconfig = config.envconfigs["python"] assert envconfig.install_command == ["some_install", "{packages}"] def test_install_command_must_contain_packages(self, newconfig): with pytest.raises(tox.exception.ConfigError): newconfig("[testenv]\ninstall_command=pip install") def test_install_command_substitutions(self, newconfig): config = newconfig( """ [testenv] install_command=some_install --arg={toxinidir}/foo \ {envname} {opts} {packages} """, ) envconfig = config.envconfigs["python"] expected_deps = [ "some_install", "--arg={}/foo".format(config.toxinidir), "python", "{opts}", "{packages}", ] assert envconfig.install_command == expected_deps def test_install_command_substitutions_other_section(self, newconfig): config = newconfig( """ [base] install_command=some_install --arg={toxinidir}/foo \ {envname} {opts} {packages} [testenv] install_command={[base]install_command} """, ) envconfig = config.envconfigs["python"] expected_deps = [ "some_install", "--arg={}/foo".format(config.toxinidir), "python", "{opts}", "{packages}", ] assert envconfig.install_command == expected_deps def test_pip_pre(self, newconfig): config = newconfig( """ [testenv] pip_pre=true """, ) envconfig = config.envconfigs["python"] assert envconfig.pip_pre def test_pip_pre_cmdline_override(self, newconfig): config = newconfig( ["--pre"], """ [testenv] pip_pre=false """, ) envconfig = config.envconfigs["python"] assert envconfig.pip_pre def test_simple(self, newconfig): config = newconfig( """ [testenv:py36] basepython=python3.6 [testenv:py27] basepython=python2.7 """, ) assert len(config.envconfigs) == 2 assert "py36" in config.envconfigs assert "py27" in config.envconfigs def test_substitution_error(self, newconfig): with pytest.raises(tox.exception.ConfigError): newconfig("[testenv:py27]\nbasepython={xyz}") def test_substitution_defaults(self, newconfig): config = newconfig( """ [testenv:py27] commands = {toxinidir} {toxworkdir} {envdir} {envbindir} {envtmpdir} {envpython} {homedir} {distshare} {envlogdir} """, ) conf = config.envconfigs["py27"] argv = conf.commands assert argv[0][0] == config.toxinidir assert argv[1][0] == config.toxworkdir assert argv[2][0] == conf.envdir assert argv[3][0] == conf.envbindir assert argv[4][0] == conf.envtmpdir assert argv[5][0] == conf.envpython assert argv[6][0] == str(config.homedir) assert argv[7][0] == config.homedir.join(".tox", "distshare") assert argv[8][0] == conf.envlogdir def test_substitution_notfound_issue246(self, newconfig): config = newconfig( """ [testenv:py27] setenv = FOO={envbindir} BAR={envsitepackagesdir} """, ) conf = config.envconfigs["py27"] env = conf.setenv assert "FOO" in env assert "BAR" in env def test_substitution_notfound_issue515(self, newconfig): config = newconfig( """ [tox] envlist = standard-greeting [testenv:standard-greeting] commands = python -c 'print("Hello, world!")' [testenv:custom-greeting] passenv = NAME commands = python -c 'print("Hello, {env:NAME}!")' """, ) conf = config.envconfigs["standard-greeting"] assert conf.commands == [["python", "-c", 'print("Hello, world!")']] def test_substitution_nested_env_defaults(self, newconfig, monkeypatch): monkeypatch.setenv("IGNORE_STATIC_DEFAULT", "env") monkeypatch.setenv("IGNORE_DYNAMIC_DEFAULT", "env") config = newconfig( """ [testenv:py27] passenv = IGNORE_STATIC_DEFAULT USE_STATIC_DEFAULT IGNORE_DYNAMIC_DEFAULT USE_DYNAMIC_DEFAULT setenv = OTHER_VAR=other IGNORE_STATIC_DEFAULT={env:IGNORE_STATIC_DEFAULT:default} USE_STATIC_DEFAULT={env:USE_STATIC_DEFAULT:default} IGNORE_DYNAMIC_DEFAULT={env:IGNORE_DYNAMIC_DEFAULT:{env:OTHER_VAR}+default} USE_DYNAMIC_DEFAULT={env:USE_DYNAMIC_DEFAULT:{env:OTHER_VAR}+default} IGNORE_OTHER_DEFAULT={env:OTHER_VAR:{env:OTHER_VAR}+default} USE_OTHER_DEFAULT={env:NON_EXISTENT_VAR:{env:OTHER_VAR}+default} """, ) conf = config.envconfigs["py27"] env = conf.setenv assert env["IGNORE_STATIC_DEFAULT"] == "env" assert env["USE_STATIC_DEFAULT"] == "default" assert env["IGNORE_OTHER_DEFAULT"] == "other" assert env["USE_OTHER_DEFAULT"] == "other+default" assert env["IGNORE_DYNAMIC_DEFAULT"] == "env" assert env["USE_DYNAMIC_DEFAULT"] == "other+default" def test_substitution_positional(self, newconfig): inisource = """ [testenv:py27] commands = cmd1 [hello] \ world cmd1 {posargs:hello} \ world """ conf = newconfig([], inisource).envconfigs["py27"] argv = conf.commands assert argv[0] == ["cmd1", "[hello]", "world"] assert argv[1] == ["cmd1", "hello", "world"] conf = newconfig(["brave", "new"], inisource).envconfigs["py27"] argv = conf.commands assert argv[0] == ["cmd1", "[hello]", "world"] assert argv[1] == ["cmd1", "brave", "new", "world"] def test_substitution_noargs_issue240(self, newconfig): inisource = """ [testenv] commands = echo {posargs:foo} """ conf = newconfig([""], inisource).envconfigs["python"] argv = conf.commands assert argv[0] == ["echo"] def test_substitution_double(self, newconfig): inisource = """ [params] foo = bah foo2 = [params]foo [testenv:py27] commands = echo {{[params]foo2}} """ conf = newconfig([], inisource).envconfigs["py27"] argv = conf.commands assert argv[0] == ["echo", "bah"] def test_posargs_backslashed_or_quoted(self, newconfig): inisource = r""" [testenv:py27] commands = echo "\{posargs\}" = {posargs} echo "posargs = " "{posargs}" """ conf = newconfig([], inisource).envconfigs["py27"] argv = conf.commands assert argv[0] == ["echo", "{posargs}", "="] assert argv[1] == ["echo", "posargs = ", ""] conf = newconfig(["dog", "cat"], inisource).envconfigs["py27"] argv = conf.commands assert argv[0] == ["echo", "{posargs}", "=", "dog", "cat"] assert argv[1] == ["echo", "posargs = ", "dog cat"] def test_rewrite_posargs(self, tmpdir, newconfig): inisource = """ [testenv:py27] args_are_paths = True changedir = tests commands = cmd1 {posargs:hello} """ conf = newconfig([], inisource).envconfigs["py27"] argv = conf.commands assert argv[0] == ["cmd1", "hello"] conf = newconfig(["tests/hello"], inisource).envconfigs["py27"] argv = conf.commands assert argv[0] == ["cmd1", "tests/hello"] tmpdir.ensure("tests", "hello") conf = newconfig(["tests/hello"], inisource).envconfigs["py27"] argv = conf.commands assert argv[0] == ["cmd1", "hello"] def test_rewrite_simple_posargs(self, tmpdir, newconfig): inisource = """ [testenv:py27] args_are_paths = True changedir = tests commands = cmd1 {posargs} """ conf = newconfig([], inisource).envconfigs["py27"] argv = conf.commands assert argv[0] == ["cmd1"] conf = newconfig(["tests/hello"], inisource).envconfigs["py27"] argv = conf.commands assert argv[0] == ["cmd1", "tests/hello"] tmpdir.ensure("tests", "hello") conf = newconfig(["tests/hello"], inisource).envconfigs["py27"] argv = conf.commands assert argv[0] == ["cmd1", "hello"] @pytest.mark.parametrize( "envlist, deps", [ (["py27"], ("pytest", "pytest-cov")), (["py27", "py34"], ("pytest", "py{27,34}: pytest-cov")), ], ) def test_take_dependencies_from_other_testenv(self, newconfig, envlist, deps): inisource = """ [tox] envlist = {envlist} [testenv] deps={deps} [testenv:py27] deps= {{[testenv]deps}} fun frob{{env:ENV_VAR:>1.0,<2.0}} """.format( envlist=",".join(envlist), deps="\n" + "\n".join([" " * 17 + d for d in deps]), ) conf = newconfig([], inisource).envconfigs["py27"] packages = [dep.name for dep in conf.deps] assert packages == ["pytest", "pytest-cov", "fun", "frob>1.0,<2.0"] # https://github.com/tox-dev/tox/issues/706 @pytest.mark.parametrize("envlist", [["py27", "coverage", "other"]]) def test_regression_test_issue_706(self, newconfig, envlist): inisource = """ [tox] envlist = {envlist} [testenv] deps= flake8 coverage: coverage [testenv:py27] deps= {{[testenv]deps}} fun """.format( envlist=",".join(envlist), ) conf = newconfig([], inisource).envconfigs["coverage"] packages = [dep.name for dep in conf.deps] assert packages == ["flake8", "coverage"] conf = newconfig([], inisource).envconfigs["other"] packages = [dep.name for dep in conf.deps] assert packages == ["flake8"] conf = newconfig([], inisource).envconfigs["py27"] packages = [dep.name for dep in conf.deps] assert packages == ["flake8", "fun"] def test_factor_expansion(self, newconfig): inisource = """ [tox] envlist = {py27, py37}-cover [testenv] deps= {py27}: foo {py37}: bar """ conf = newconfig([], inisource).envconfigs["py27-cover"] packages = [dep.name for dep in conf.deps] assert packages == ["foo"] conf = newconfig([], inisource).envconfigs["py37-cover"] packages = [dep.name for dep in conf.deps] assert packages == ["bar"] # Regression test https://github.com/tox-dev/tox/issues/899 def test_factors_support_curly_braces(self, newconfig): inisource = """ [tox] envlist = style sdist bdist_wheel {py27,py34,py35,py36,pypy,pypy3}-cover {py27,py34,py35,py36,pypy,pypy3}-nocov [testenv] deps = cover: coverage cover: codecov {py27}: unittest2 {py27}: mysql-python {py27,py36}: mmtf-python {py27,py35}: reportlab {py27,py34,py35,py36}: psycopg2-binary {py27,py34,py35,py35}: mysql-connector-python-rf {py27,py35,pypy}: rdflib {pypy,pypy3}: numpy==1.12.1 {py27,py34,py36}: numpy {py36}: scipy {py27}: networkx {py36}: matplotlib """ conf = newconfig([], inisource).envconfigs["style"] packages = [dep.name for dep in conf.deps] assert packages == [] conf = newconfig([], inisource).envconfigs["py27-cover"] packages = [dep.name for dep in conf.deps] assert packages == [ "coverage", "codecov", "unittest2", "mysql-python", "mmtf-python", "reportlab", "psycopg2-binary", "mysql-connector-python-rf", "rdflib", "numpy", "networkx", ] conf = newconfig([], inisource).envconfigs["py34-cover"] packages = [dep.name for dep in conf.deps] assert packages == [ "coverage", "codecov", "psycopg2-binary", "mysql-connector-python-rf", "numpy", ] conf = newconfig([], inisource).envconfigs["py35-cover"] packages = [dep.name for dep in conf.deps] assert packages == [ "coverage", "codecov", "reportlab", "psycopg2-binary", "mysql-connector-python-rf", "rdflib", ] conf = newconfig([], inisource).envconfigs["py36-cover"] packages = [dep.name for dep in conf.deps] assert packages == [ "coverage", "codecov", "mmtf-python", "psycopg2-binary", "numpy", "scipy", "matplotlib", ] conf = newconfig([], inisource).envconfigs["pypy-cover"] packages = [dep.name for dep in conf.deps] assert packages == ["coverage", "codecov", "rdflib", "numpy==1.12.1"] conf = newconfig([], inisource).envconfigs["pypy3-cover"] packages = [dep.name for dep in conf.deps] assert packages == ["coverage", "codecov", "numpy==1.12.1"] conf = newconfig([], inisource).envconfigs["py27-nocov"] packages = [dep.name for dep in conf.deps] assert packages == [ "unittest2", "mysql-python", "mmtf-python", "reportlab", "psycopg2-binary", "mysql-connector-python-rf", "rdflib", "numpy", "networkx", ] conf = newconfig([], inisource).envconfigs["py34-nocov"] packages = [dep.name for dep in conf.deps] assert packages == ["psycopg2-binary", "mysql-connector-python-rf", "numpy"] conf = newconfig([], inisource).envconfigs["py35-nocov"] packages = [dep.name for dep in conf.deps] assert packages == ["reportlab", "psycopg2-binary", "mysql-connector-python-rf", "rdflib"] conf = newconfig([], inisource).envconfigs["py36-nocov"] packages = [dep.name for dep in conf.deps] assert packages == ["mmtf-python", "psycopg2-binary", "numpy", "scipy", "matplotlib"] conf = newconfig([], inisource).envconfigs["pypy-nocov"] packages = [dep.name for dep in conf.deps] assert packages == ["rdflib", "numpy==1.12.1"] conf = newconfig([], inisource).envconfigs["pypy3-cover"] packages = [dep.name for dep in conf.deps] assert packages == ["coverage", "codecov", "numpy==1.12.1"] # Regression test https://github.com/tox-dev/tox/issues/906 def test_do_not_substitute_more_than_needed(self, newconfig): inisource = """ [tox] envlist = django_master-py{36,35} django20-py{36,35,34,py3} django111-py{36,35,34,27,py} django18-py{35,34,27,py} lint docs [testenv] deps = .[test] django18: {[django]1.8.x} django111: {[django]1.11.x} django20: {[django]2.0.x} django_master: {[django]master} [django] 1.8.x = Django>=1.8.0,<1.9.0 django-reversion==1.10.0 djangorestframework>=3.3.3,<3.7.0 1.11.x = Django>=1.11.0,<2.0.0 django-reversion>=2.0.8 djangorestframework>=3.6.2 2.0.x = Django>=2.0,<2.1 django-reversion>=2.0.8 djangorestframework>=3.7.3 master = https://github.com/django/django/tarball/master django-reversion>=2.0.8 djangorestframework>=3.6.2 """ conf = newconfig([], inisource).envconfigs["django_master-py36"] packages = [dep.name for dep in conf.deps] assert packages == [ ".[test]", "https://github.com/django/django/tarball/master", "django-reversion>=2.0.8", "djangorestframework>=3.6.2", ] conf = newconfig([], inisource).envconfigs["django20-pypy3"] packages = [dep.name for dep in conf.deps] assert packages == [ ".[test]", "Django>=2.0,<2.1", "django-reversion>=2.0.8", "djangorestframework>=3.7.3", ] conf = newconfig([], inisource).envconfigs["django111-py34"] packages = [dep.name for dep in conf.deps] assert packages == [ ".[test]", "Django>=1.11.0,<2.0.0", "django-reversion>=2.0.8", "djangorestframework>=3.6.2", ] conf = newconfig([], inisource).envconfigs["django18-py27"] packages = [dep.name for dep in conf.deps] assert packages == [ ".[test]", "Django>=1.8.0,<1.9.0", "django-reversion==1.10.0", "djangorestframework>=3.3.3,<3.7.0", ] conf = newconfig([], inisource).envconfigs["lint"] packages = [dep.name for dep in conf.deps] assert packages == [".[test]"] conf = newconfig([], inisource).envconfigs["docs"] packages = [dep.name for dep in conf.deps] assert packages == [".[test]"] def test_take_dependencies_from_other_section(self, newconfig): inisource = """ [testing:pytest] deps= pytest pytest-cov [testing:mock] deps= mock [testenv] deps= {[testing:pytest]deps} {[testing:mock]deps} fun """ conf = newconfig([], inisource) env = conf.envconfigs["python"] packages = [dep.name for dep in env.deps] assert packages == ["pytest", "pytest-cov", "mock", "fun"] def test_multilevel_substitution(self, newconfig): inisource = """ [testing:pytest] deps= pytest pytest-cov [testing:mock] deps= mock [testing] deps= {[testing:pytest]deps} {[testing:mock]deps} [testenv] deps= {[testing]deps} fun """ conf = newconfig([], inisource) env = conf.envconfigs["python"] packages = [dep.name for dep in env.deps] assert packages == ["pytest", "pytest-cov", "mock", "fun"] def test_recursive_substitution_cycle_fails(self, newconfig): inisource = """ [testing:pytest] deps= {[testing:mock]deps} [testing:mock] deps= {[testing:pytest]deps} [testenv] deps= {[testing:pytest]deps} """ with pytest.raises(tox.exception.ConfigError): newconfig([], inisource) def test_single_value_from_other_secton(self, newconfig, tmpdir): inisource = """ [common] changedir = testing [testenv] changedir = {[common]changedir} """ conf = newconfig([], inisource).envconfigs["python"] assert conf.changedir.basename == "testing" assert conf.changedir.dirpath().realpath() == tmpdir.realpath() def test_factors(self, newconfig): inisource = """ [tox] envlist = a-x,b [testenv] deps= dep-all a: dep-a b: dep-b x: dep-x !a: dep-!a !b: dep-!b !x: dep-!x """ conf = newconfig([], inisource) configs = conf.envconfigs expected = ["dep-all", "dep-a", "dep-x", "dep-!b"] assert [dep.name for dep in configs["a-x"].deps] == expected expected = ["dep-all", "dep-b", "dep-!a", "dep-!x"] assert [dep.name for dep in configs["b"].deps] == expected expected = ["dep-all", "dep-a", "dep-x", "dep-!b"] assert [dep.name for dep in configs["a-x"].deps] == expected expected = ["dep-all", "dep-b", "dep-!a", "dep-!x"] assert [dep.name for dep in configs["b"].deps] == expected def test_factor_ops(self, newconfig): inisource = """ [tox] envlist = {a,b}-{x,y} [testenv] deps= a,b: dep-a-or-b a-x: dep-a-and-x {a,b}-y: dep-ab-and-y a-!x: dep-a-and-!x a,!x: dep-a-or-!x !a-!x: dep-!a-and-!x !a,!x: dep-!a-or-!x !a-!b: dep-!a-and-!b !a-!b-!x-!y: dep-!a-and-!b-and-!x-and-!y """ configs = newconfig([], inisource).envconfigs def get_deps(env): return [dep.name for dep in configs[env].deps] assert get_deps("a-x") == ["dep-a-or-b", "dep-a-and-x", "dep-a-or-!x"] expected = ["dep-a-or-b", "dep-ab-and-y", "dep-a-and-!x", "dep-a-or-!x", "dep-!a-or-!x"] assert get_deps("a-y") == expected assert get_deps("b-x") == ["dep-a-or-b", "dep-!a-or-!x"] expected = ["dep-a-or-b", "dep-ab-and-y", "dep-a-or-!x", "dep-!a-and-!x", "dep-!a-or-!x"] assert get_deps("b-y") == expected def test_envconfigs_based_on_factors(self, newconfig): inisource = """ [testenv] some-setting= a: something b,c: something d-e: something !f: something !g,!h: something !i-!j: something [unknown-section] some-setting= eggs: something """ config = newconfig(["-e spam"], inisource) assert not config.envconfigs assert config.envlist == ["spam"] config = newconfig(["-e eggs"], inisource) assert not config.envconfigs assert config.envlist == ["eggs"] config = newconfig(["-e py3-spam"], inisource) assert not config.envconfigs assert config.envlist == ["py3-spam"] for x in "abcdefghij": env = "py3-{}".format(x) config = newconfig(["-e {}".format(env)], inisource) assert sorted(config.envconfigs) == [env] assert config.envlist == [env] def test_default_factors(self, newconfig): inisource = """ [tox] envlist = py{27,34,36}-dep [testenv] deps= dep: dep """ conf = newconfig([], inisource) configs = conf.envconfigs for name, config in configs.items(): assert config.basepython == "python{}.{}".format(name[2], name[3]) def test_default_factors_conflict(self, newconfig, capsys): with pytest.warns(UserWarning, match=r"conflicting basepython .*"): exe = "pypy3" if tox.INFO.IS_PYPY else "python3" env = "pypy27" if tox.INFO.IS_PYPY else "py27" config = newconfig( """\ [testenv] basepython={} [testenv:{}] commands = python --version """.format( exe, env, ), ) assert len(config.envconfigs) == 1 envconfig = config.envconfigs[env] assert envconfig.basepython == exe def test_default_factors_conflict_lying_name( self, newconfig, capsys, tmpdir, recwarn, monkeypatch, ): # we first need to create a lying Python here, let's mock out here from tox.interpreters import Interpreters def get_executable(self, envconfig): return sys.executable monkeypatch.setattr(Interpreters, "get_executable", get_executable) major, minor = sys.version_info[0:2] config = newconfig( """ [testenv:py{0}{1}] basepython=python{0}.{2} commands = python --version """.format( major, minor, minor - 1, ), ) env_config = config.envconfigs["py{}{}".format(major, minor)] assert env_config.basepython == "python{}.{}".format(major, minor - 1) assert not recwarn.list, "\n".join(repr(i.message) for i in recwarn.list) def test_default_single_digit_factors(self, newconfig, monkeypatch): from tox.interpreters import Interpreters def get_executable(self, envconfig): return sys.executable monkeypatch.setattr(Interpreters, "get_executable", get_executable) major, minor = sys.version_info[0:2] with pytest.warns(None) as lying: config = newconfig( """ [testenv:py{0}] basepython=python{0}.{1} commands = python --version """.format( major, minor - 1, ), ) env_config = config.envconfigs["py{}".format(major)] assert env_config.basepython == "python{}.{}".format(major, minor - 1) assert len(lying) == 0, "\n".join(repr(r.message) for r in lying) with pytest.warns(None) as truthful: config = newconfig( """ [testenv:py{0}] basepython=python{0}.{1} commands = python --version """.format( major, minor, ), ) env_config = config.envconfigs["py{}".format(major)] assert env_config.basepython == "python{}.{}".format(major, minor) assert len(truthful) == 0, "\n".join(repr(r.message) for r in truthful) def test_default_factors_conflict_ignore(self, newconfig, capsys): with pytest.warns(None) as record: config = newconfig( """ [tox] ignore_basepython_conflict=True [testenv] basepython=python3 [testenv:py27] commands = python --version """, ) assert len(config.envconfigs) == 1 envconfig = config.envconfigs["py27"] assert envconfig.basepython == "python2.7" assert len(record) == 0, "\n".join(repr(r.message) for r in record) def test_factors_in_boolean(self, newconfig): inisource = """ [tox] envlist = py{27,36} [testenv] recreate = py27: True """ configs = newconfig([], inisource).envconfigs assert configs["py27"].recreate assert not configs["py36"].recreate def test_factors_in_setenv(self, newconfig): inisource = """ [tox] envlist = py27,py36 [testenv] setenv = py27: X = 1 """ configs = newconfig([], inisource).envconfigs assert configs["py27"].setenv["X"] == "1" assert "X" not in configs["py36"].setenv def test_curly_braces_in_setenv(self, newconfig): inisource = r""" [testenv] setenv = VAR = \{val\} commands = {env:VAR} """ configs = newconfig([], inisource).envconfigs assert configs["python"].setenv["VAR"] == r"\{val\}" assert configs["python"].commands[0] == ["{val}"] def test_factor_use_not_checked(self, newconfig): inisource = """ [tox] envlist = py27-{a,b} [testenv] deps = b: test """ configs = newconfig([], inisource).envconfigs assert set(configs.keys()) == {"py27-a", "py27-b"} def test_factors_groups_touch(self, newconfig): inisource = """ [tox] envlist = {a,b}{-x,} [testenv] deps= a,b,x,y: dep """ configs = newconfig([], inisource).envconfigs assert set(configs.keys()) == {"a", "a-x", "b", "b-x"} def test_period_in_factor(self, newconfig): inisource = """ [tox] envlist = py27-{django1.6,django1.7} [testenv] deps = django1.6: Django==1.6 django1.7: Django==1.7 """ configs = newconfig([], inisource).envconfigs assert sorted(configs) == ["py27-django1.6", "py27-django1.7"] assert [d.name for d in configs["py27-django1.6"].deps] == ["Django==1.6"] def test_ignore_outcome(self, newconfig): inisource = """ [testenv] ignore_outcome=True """ config = newconfig([], inisource).envconfigs assert config["python"].ignore_outcome is True class TestGlobalOptions: def test_notest(self, newconfig): config = newconfig([], "") assert not config.option.notest config = newconfig(["--notest"], "") assert config.option.notest @pytest.mark.parametrize( "args, expected", [([], 0), (["-v"], 1), (["-vv"], 2), (["--verbose", "--verbose"], 2), (["-vvv"], 3)], ) def test_verbosity(self, args, expected, newconfig): config = newconfig(args, "") assert config.option.verbose_level == expected @pytest.mark.parametrize( "args, expected", [([], 0), (["-q"], 1), (["-qq"], 2), (["--quiet", "--quiet"], 2), (["-qqq"], 3)], ) def test_quiet(self, args, expected, newconfig): config = newconfig(args, "") assert config.option.quiet_level == expected def test_substitution_jenkins_global(self, monkeypatch, newconfig): monkeypatch.setenv("HUDSON_URL", "xyz") config = newconfig( """ [tox:tox] envlist = py37 """, filename="setup.cfg", ) assert "py37" in config.envconfigs def test_substitution_jenkins_default(self, monkeypatch, newconfig): monkeypatch.setenv("HUDSON_URL", "xyz") config = newconfig( """ [testenv:py27] commands = {distshare} """, ) conf = config.envconfigs["py27"] argv = conf.commands expect_path = config.toxworkdir.join("distshare") assert argv[0][0] == expect_path def test_substitution_jenkins_context(self, tmpdir, monkeypatch, newconfig): monkeypatch.setenv("HUDSON_URL", "xyz") monkeypatch.setenv("WORKSPACE", str(tmpdir)) config = newconfig( """ [tox:jenkins] distshare = {env:WORKSPACE}/hello [testenv:py27] commands = {distshare} """, ) conf = config.envconfigs["py27"] argv = conf.commands assert argv[0][0] == config.distshare assert config.distshare == tmpdir.join("hello") def test_sdist_specification(self, newconfig): config = newconfig( """ [tox] sdistsrc = {distshare}/xyz.zip """, ) assert config.sdistsrc == config.distshare.join("xyz.zip") config = newconfig([], "") assert not config.sdistsrc def test_env_selection_with_section_name(self, newconfig, monkeypatch): inisource = """ [tox] envlist = py36 [testenv:py36] basepython=python3.6 [testenv:py35] basepython=python3.5 [testenv:py27] basepython=python2.7 """ config = newconfig([], inisource) assert config.envlist == ["py36"] config = newconfig(["-epy35"], inisource) assert config.envlist == ["py35"] monkeypatch.setenv("TOXENV", "py35,py36") config = newconfig([], inisource) assert config.envlist == ["py35", "py36"] monkeypatch.setenv("TOXENV", "ALL") config = newconfig([], inisource) assert config.envlist == ["py36", "py35", "py27"] config = newconfig(["-eALL"], inisource) assert config.envlist == ["py36", "py35", "py27"] config = newconfig(["-espam"], inisource) assert config.envlist == ["spam"] def test_env_selection_expanded_envlist(self, newconfig, monkeypatch): inisource = """ [tox] envlist = py{36,35,27} [testenv:py36] basepython=python3.6 """ config = newconfig([], inisource) assert config.envlist == ["py36", "py35", "py27"] config = newconfig(["-eALL"], inisource) assert config.envlist == ["py36", "py35", "py27"] def test_py_venv(self, newconfig): config = newconfig(["-epy"], "") env = config.envconfigs["py"] assert str(env.basepython) == sys.executable def test_no_implicit_venv_from_cli_with_envlist(self, newconfig): # See issue 1160. inisource = """ [tox] envlist = stated-factors """ config = newconfig(["-etypo-factor"], inisource) assert "typo-factor" not in config.envconfigs def test_correct_basepython_chosen_from_default_factors(self, newconfig): envs = { "py": sys.executable, "py2": "python2", "py3": "python3", "py27": "python2.7", "py36": "python3.6", "py310": "python3.10", "pypy": "pypy", "pypy2": "pypy2", "pypy3": "pypy3", "pypy36": "pypy3.6", "jython": "jython", } config = newconfig([], "[tox]\nenvlist={}".format(", ".join(envs))) assert set(config.envlist) == set(envs) for name in config.envlist: basepython = config.envconfigs[name].basepython assert basepython == envs[name] def test_envlist_expansion(self, newconfig): inisource = """ [tox] envlist = py{36,27},docs """ config = newconfig([], inisource) assert config.envlist == ["py36", "py27", "docs"] def test_envlist_cross_product(self, newconfig): inisource = """ [tox] envlist = py{36,27}-dep{1,2} """ config = newconfig([], inisource) envs = ["py36-dep1", "py36-dep2", "py27-dep1", "py27-dep2"] assert config.envlist == envs def test_envlist_multiline(self, newconfig): inisource = """ [tox] envlist = py27 py34 """ config = newconfig([], inisource) assert config.envlist == ["py27", "py34"] def test_skip_missing_interpreters_true(self, newconfig): ini_source = """ [tox] skip_missing_interpreters = True """ config = newconfig([], ini_source) assert config.option.skip_missing_interpreters == "true" def test_skip_missing_interpreters_false(self, newconfig): ini_source = """ [tox] skip_missing_interpreters = False """ config = newconfig([], ini_source) assert config.option.skip_missing_interpreters == "false" def test_skip_missing_interpreters_cli_no_arg(self, newconfig): ini_source = """ [tox] skip_missing_interpreters = False """ config = newconfig(["--skip-missing-interpreters"], ini_source) assert config.option.skip_missing_interpreters == "true" def test_skip_missing_interpreters_cli_not_specified(self, newconfig): config = newconfig([], "") assert config.option.skip_missing_interpreters == "false" def test_skip_missing_interpreters_cli_overrides_true(self, newconfig): ini_source = """ [tox] skip_missing_interpreters = False """ config = newconfig(["--skip-missing-interpreters", "true"], ini_source) assert config.option.skip_missing_interpreters == "true" def test_skip_missing_interpreters_cli_overrides_false(self, newconfig): ini_source = """ [tox] skip_missing_interpreters = True """ config = newconfig(["--skip-missing-interpreters", "false"], ini_source) assert config.option.skip_missing_interpreters == "false" 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, newconfig): inisource = """ [tox] envlist = py27 [testenv:py27] commands= xyz """ config = newconfig([], inisource) env = config.envconfigs["py27"] assert env.basepython == "python2.7" assert env.commands == [["xyz"]] class TestHashseedOption: def _get_envconfigs(self, newconfig, args=None, tox_ini=None, make_hashseed=None): if args is None: args = [] if tox_ini is None: tox_ini = """ [testenv] """ if make_hashseed is None: def make_hashseed(): return "123456789" original_make_hashseed = tox.config.make_hashseed tox.config.make_hashseed = make_hashseed try: config = newconfig(args, tox_ini) finally: tox.config.make_hashseed = original_make_hashseed return config.envconfigs def _get_envconfig(self, newconfig, args=None, tox_ini=None): envconfigs = self._get_envconfigs(newconfig, args=args, tox_ini=tox_ini) return envconfigs["python"] def _check_hashseed(self, envconfig, expected): assert envconfig.setenv["PYTHONHASHSEED"] == expected 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, newconfig): self._check_testenv(newconfig, "123456789") def test_passing_integer(self, newconfig): args = ["--hashseed", "1"] self._check_testenv(newconfig, "1", args=args) def test_passing_string(self, newconfig): args = ["--hashseed", "random"] self._check_testenv(newconfig, "random", args=args) def test_passing_empty_string(self, newconfig): args = ["--hashseed", ""] self._check_testenv(newconfig, "", args=args) def test_passing_no_argument(self, newconfig): """Test that passing no arguments to --hashseed is not allowed.""" args = ["--hashseed"] try: self._check_testenv(newconfig, "", args=args) except SystemExit as exception: assert exception.code == 2 return assert False # getting here means we failed the test. def test_setenv(self, newconfig): """Check that setenv takes precedence.""" tox_ini = """ [testenv] setenv = PYTHONHASHSEED = 2 """ self._check_testenv(newconfig, "2", tox_ini=tox_ini) args = ["--hashseed", "1"] self._check_testenv(newconfig, "2", args=args, tox_ini=tox_ini) def test_noset(self, newconfig): args = ["--hashseed", "noset"] envconfig = self._get_envconfig(newconfig, args=args) assert set(envconfig.setenv.definitions.keys()) == {"TOX_ENV_DIR", "TOX_ENV_NAME"} def test_noset_with_setenv(self, newconfig): tox_ini = """ [testenv] setenv = PYTHONHASHSEED = 2 """ args = ["--hashseed", "noset"] self._check_testenv(newconfig, "2", args=args, tox_ini=tox_ini) def test_one_random_hashseed(self, newconfig): """Check that different testenvs use the same random seed.""" tox_ini = """ [testenv:hash1] [testenv:hash2] """ next_seed = [1000] # This function is guaranteed to generate a different value each time. def make_hashseed(): next_seed[0] += 1 return str(next_seed[0]) # Check that make_hashseed() works. assert make_hashseed() == "1001" envconfigs = self._get_envconfigs(newconfig, tox_ini=tox_ini, make_hashseed=make_hashseed) self._check_hashseed(envconfigs["hash1"], "1002") # Check that hash2's value is not '1003', for example. self._check_hashseed(envconfigs["hash2"], "1002") def test_setenv_in_one_testenv(self, newconfig): """Check using setenv in one of multiple testenvs.""" tox_ini = """ [testenv:hash1] setenv = PYTHONHASHSEED = 2 [testenv:hash2] """ envconfigs = self._get_envconfigs(newconfig, tox_ini=tox_ini) self._check_hashseed(envconfigs["hash1"], "2") self._check_hashseed(envconfigs["hash2"], "123456789") class TestSetenv: def test_getdict_lazy(self, newconfig, monkeypatch): monkeypatch.setenv("X", "2") config = newconfig( """ [testenv:X] key0 = key1 = {env:X} key2 = {env:Y:1} """, ) envconfig = config.envconfigs["X"] val = envconfig._reader.getdict_setenv("key0") assert val["key1"] == "2" assert val["key2"] == "1" def test_getdict_lazy_update(self, newconfig, monkeypatch): monkeypatch.setenv("X", "2") config = newconfig( """ [testenv:X] key0 = key1 = {env:X} key2 = {env:Y:1} """, ) envconfig = config.envconfigs["X"] val = envconfig._reader.getdict_setenv("key0") d = {} d.update(val) assert d == {"key1": "2", "key2": "1"} def test_setenv_uses_os_environ(self, newconfig, monkeypatch): monkeypatch.setenv("X", "1") config = newconfig( """ [testenv:env1] setenv = X = {env:X} """, ) assert config.envconfigs["env1"].setenv["X"] == "1" def test_setenv_default_os_environ(self, newconfig, monkeypatch): monkeypatch.delenv("X", raising=False) config = newconfig( """ [testenv:env1] setenv = X = {env:X:2} """, ) assert config.envconfigs["env1"].setenv["X"] == "2" def test_setenv_uses_other_setenv(self, newconfig): config = newconfig( """ [testenv:env1] setenv = Y = 5 X = {env:Y} """, ) assert config.envconfigs["env1"].setenv["X"] == "5" def test_setenv_recursive_direct_with_default(self, newconfig): config = newconfig( """ [testenv:env1] setenv = X = {env:X:3} """, ) assert config.envconfigs["env1"].setenv["X"] == "3" def test_setenv_recursive_direct_with_default_nested(self, newconfig): config = newconfig( """ [testenv:env1] setenv = X = {env:X:{env:X:3}} """, ) assert config.envconfigs["env1"].setenv["X"] == "3" def test_setenv_recursive_direct_without_default(self, newconfig): config = newconfig( """ [testenv:env1] setenv = X = {env:X} """, ) with pytest.raises(tox.exception.MissingSubstitution): config.envconfigs["env1"].setenv["X"] def test_setenv_overrides(self, newconfig): config = newconfig( """ [testenv] setenv = PYTHONPATH = something ANOTHER_VAL=else """, ) assert len(config.envconfigs) == 1 envconfig = config.envconfigs["python"] assert "PYTHONPATH" in envconfig.setenv assert "ANOTHER_VAL" in envconfig.setenv assert envconfig.setenv["PYTHONPATH"] == "something" assert envconfig.setenv["ANOTHER_VAL"] == "else" def test_setenv_with_envdir_and_basepython(self, newconfig): config = newconfig( """ [testenv] setenv = VAL = {envdir} basepython = {env:VAL} """, ) assert len(config.envconfigs) == 1 envconfig = config.envconfigs["python"] assert "VAL" in envconfig.setenv assert envconfig.setenv["VAL"] == envconfig.envdir assert envconfig.basepython == envconfig.envdir def test_setenv_ordering_1(self, newconfig): config = newconfig( """ [testenv] setenv= VAL={envdir} commands=echo {env:VAL} """, ) assert len(config.envconfigs) == 1 envconfig = config.envconfigs["python"] assert "VAL" in envconfig.setenv assert envconfig.setenv["VAL"] == envconfig.envdir assert str(envconfig.envdir) in envconfig.commands[0] def test_setenv_cross_section_subst_issue294(self, monkeypatch, newconfig): """test that we can do cross-section substitution with setenv""" monkeypatch.delenv("TEST", raising=False) config = newconfig( """ [section] x = NOT_TEST={env:TEST:defaultvalue} [testenv] setenv = {[section]x} """, ) envconfig = config.envconfigs["python"] assert envconfig.setenv["NOT_TEST"] == "defaultvalue" def test_setenv_cross_section_subst_twice(self, monkeypatch, newconfig): """test that we can do cross-section substitution with setenv""" monkeypatch.delenv("TEST", raising=False) config = newconfig( """ [section] x = NOT_TEST={env:TEST:defaultvalue} [section1] y = {[section]x} [testenv] setenv = {[section1]y} """, ) envconfig = config.envconfigs["python"] assert envconfig.setenv["NOT_TEST"] == "defaultvalue" def test_setenv_cross_section_mixed(self, monkeypatch, newconfig): """test that we can do cross-section substitution with setenv""" monkeypatch.delenv("TEST", raising=False) config = newconfig( """ [section] x = NOT_TEST={env:TEST:defaultvalue} [testenv] setenv = {[section]x} y = 7 """, ) envconfig = config.envconfigs["python"] assert envconfig.setenv["NOT_TEST"] == "defaultvalue" assert envconfig.setenv["y"] == "7" def test_setenv_comment(self, newconfig): """Check that setenv ignores comments.""" envconfig = newconfig( """ [testenv] setenv = # MAGIC = yes """, ).envconfigs["python"] assert "MAGIC" not in envconfig.setenv @pytest.mark.parametrize( "content, has_magic", [ (None, False), ("\n", False), ("#MAGIC = yes", False), ("MAGIC=yes", True), ("\nMAGIC = yes", True), ], ) def test_setenv_env_file(self, newconfig, content, has_magic, tmp_path): """Check that setenv handles env files.""" env_path = tmp_path / ".env" if content else None if content: env_path.write_text(content.decode() if PY2 else content) env_config = newconfig( """ [testenv] setenv = ALPHA = 1 file| {} """.format( env_path, ), ).envconfigs["python"] envs = env_config.setenv.definitions assert envs["ALPHA"] == "1" if has_magic: assert envs["MAGIC"] == "yes" else: assert "MAGIC" not in envs expected_vars = ["ALPHA", "PYTHONHASHSEED", "TOX_ENV_DIR", "TOX_ENV_NAME"] if has_magic: expected_vars = sorted(expected_vars + ["MAGIC"]) exported = env_config.setenv.export() assert sorted(exported) == expected_vars class TestIndexServer: def test_indexserver(self, newconfig): config = newconfig( """ [tox] indexserver = name1 = XYZ name2 = ABC """, ) assert config.indexserver["default"].url is None assert config.indexserver["name1"].url == "XYZ" assert config.indexserver["name2"].url == "ABC" def test_parse_indexserver(self, newconfig): inisource = """ [tox] indexserver = default = https://pypi.somewhere.org name1 = whatever """ config = newconfig([], inisource) assert config.indexserver["default"].url == "https://pypi.somewhere.org" assert config.indexserver["name1"].url == "whatever" config = newconfig(["-i", "qwe"], inisource) assert config.indexserver["default"].url == "qwe" assert config.indexserver["name1"].url == "whatever" config = newconfig(["-i", "name1=abc", "-i", "qwe2"], inisource) assert config.indexserver["default"].url == "qwe2" assert config.indexserver["name1"].url == "abc" config = newconfig(["-i", "ALL=xzy"], inisource) assert len(config.indexserver) == 2 assert config.indexserver["default"].url == "xzy" assert config.indexserver["name1"].url == "xzy" def test_multiple_homedir_relative_local_indexservers(self, newconfig): inisource = """ [tox] indexserver = default = file://{homedir}/.pip/downloads/simple local1 = file://{homedir}/.pip/downloads/simple local2 = file://{toxinidir}/downloads/simple pypi = https://pypi.org/simple """ config = newconfig([], inisource) expected = "file://{}/.pip/downloads/simple".format(config.homedir) assert config.indexserver["default"].url == expected assert config.indexserver["local1"].url == config.indexserver["default"].url class TestConfigConstSubstitutions: @pytest.mark.parametrize("pathsep", [":", ";"]) def test_replace_pathsep(self, monkeypatch, newconfig, pathsep): """Replace {:} with OS path separator.""" monkeypatch.setattr("os.pathsep", pathsep) config = newconfig( """ [testenv] setenv = PATH = dira{:}dirb{:}dirc """, ) envconfig = config.envconfigs["python"] assert envconfig.setenv["PATH"] == pathsep.join(["dira", "dirb", "dirc"]) def test_pathsep_regex(self): """Sanity check for regex behavior for empty colon.""" regex = tox.config.Replacer.RE_ITEM_REF match = next(regex.finditer("{:}")) mdict = match.groupdict() assert mdict["sub_type"] is None assert mdict["substitution_value"] == "" assert mdict["default_value"] == "" @pytest.mark.parametrize("dirsep", ["\\", "\\\\"]) def test_dirsep_replace(self, monkeypatch, newconfig, dirsep): """Replace {/} with OS directory separator.""" monkeypatch.setattr("os.sep", dirsep) config = newconfig( """ [testenv] setenv = VAR = dira{/}subdirb{/}subdirc """, ) envconfig = config.envconfigs["python"] assert envconfig.setenv["VAR"] == dirsep.join(["dira", "subdirb", "subdirc"]) def test_dirsep_regex(self): """Sanity check for regex behavior for directory separator.""" regex = tox.config.Replacer.RE_ITEM_REF match = next(regex.finditer("{/}")) mdict = match.groupdict() assert mdict["sub_type"] is None assert mdict["substitution_value"] == "/" assert mdict["default_value"] is None class TestParseEnv: def test_parse_recreate(self, newconfig): inisource = "" config = newconfig([], inisource) assert not config.envconfigs["python"].recreate config = newconfig(["--recreate"], inisource) assert config.envconfigs["python"].recreate config = newconfig(["-r"], inisource) assert config.envconfigs["python"].recreate inisource = """ [testenv:hello] recreate = True """ config = newconfig([], inisource) assert config.envconfigs["hello"].recreate class TestCmdInvocation: def test_help(self, cmd, initproj): initproj("help", filedefs={"tox.ini": ""}) result = cmd("-h") assert not result.ret assert re.match(r"usage:.*help.*", result.out, re.DOTALL) def test_version_simple(self, cmd, initproj): initproj("help", filedefs={"tox.ini": ""}) result = cmd("--version") assert not result.ret assert "{} imported from".format(tox.__version__) in result.out def test_version_no_plugins(self): pm = PluginManager("fakeprject") version_info = get_version_info(pm) assert "imported from" in version_info assert "registered plugins:" not in version_info def test_version_with_normal_plugin(self, monkeypatch): def fake_normal_plugin_distinfo(): class MockModule: __file__ = "some-file" class MockEggInfo: project_name = "some-project" version = "1.0" return [(MockModule, MockEggInfo)] pm = PluginManager("fakeproject") monkeypatch.setattr(pm, "list_plugin_distinfo", fake_normal_plugin_distinfo) version_info = get_version_info(pm) assert "registered plugins:" in version_info assert "some-file" in version_info assert "some-project" in version_info assert "1.0" in version_info def test_version_with_fileless_module(self, monkeypatch): def fake_no_file_plugin_distinfo(): class MockModule: def __repr__(self): return "some-repr" class MockEggInfo: project_name = "some-project" version = "1.0" return [(MockModule(), MockEggInfo)] pm = PluginManager("fakeproject") monkeypatch.setattr(pm, "list_plugin_distinfo", fake_no_file_plugin_distinfo) version_info = get_version_info(pm) assert "registered plugins:" in version_info assert "some-project" in version_info assert "some-repr" in version_info assert "1.0" in version_info def test_no_tox_ini(self, cmd, initproj): initproj("noini-0.5") result = cmd() result.assert_fail() msg = "ERROR: tox config file (either pyproject.toml, tox.ini, setup.cfg) not found\n" assert result.err == msg assert not result.out @pytest.mark.parametrize( "cli_args,run_envlist", [ ("-e py36", ["py36"]), ("-e py36,py34", ["py36", "py34"]), ("-e py36,py36", ["py36", "py36"]), ("-e py36,py34 -e py34,py27", ["py36", "py34", "py34", "py27"]), ], ) def test_env_spec(initproj, cli_args, run_envlist): initproj( "env_spec", filedefs={ "tox.ini": """ [tox] envlist = [testenv] commands = python -c "" """, }, ) args = cli_args.split() config = parseconfig(args) assert config.envlist == run_envlist class TestCommandParser: def test_command_parser_for_word(self): p = CommandParser("word") assert list(p.words()) == ["word"] def test_command_parser_for_posargs(self): p = CommandParser("[]") assert list(p.words()) == ["[]"] def test_command_parser_for_multiple_words(self): p = CommandParser("w1 w2 w3 ") assert list(p.words()) == ["w1", " ", "w2", " ", "w3"] def test_command_parser_for_substitution_with_spaces(self): p = CommandParser("{sub:something with spaces}") assert list(p.words()) == ["{sub:something with spaces}"] def test_command_parser_with_complex_word_set(self): complex_case = ( "word [] [literal] {something} {some:other thing} w{ord} w{or}d w{ord} " "w{o:rd} w{o:r}d {w:or}d w[]ord {posargs:{a key}}" ) p = CommandParser(complex_case) parsed = list(p.words()) expected = [ "word", " ", "[]", " ", "[literal]", " ", "{something}", " ", "{some:other thing}", " ", "w", "{ord}", " ", "w", "{or}", "d", " ", "w", "{ord}", " ", "w", "{o:rd}", " ", "w", "{o:r}", "d", " ", "{w:or}", "d", " ", "w[]ord", " ", "{posargs:{a key}}", ] assert parsed == expected def test_command_with_runs_of_whitespace(self): cmd = "cmd1 {item1}\n {item2}" p = CommandParser(cmd) parsed = list(p.words()) assert parsed == ["cmd1", " ", "{item1}", "\n ", "{item2}"] def test_command_with_split_line_in_subst_arguments(self): cmd = dedent( """ cmd2 {posargs:{item2} other}""", ) p = CommandParser(cmd) parsed = list(p.words()) expected = ["cmd2", " ", "{posargs:{item2}\n other}"] assert parsed == expected def test_command_parsing_for_issue_10(self): cmd = "nosetests -v -a !deferred --with-doctest []" p = CommandParser(cmd) parsed = list(p.words()) expected = [ "nosetests", " ", "-v", " ", "-a", " ", "!deferred", " ", "--with-doctest", " ", "[]", ] assert parsed == expected # @mark_dont_run_on_windows def test_commands_with_backslash(self, newconfig): config = newconfig( [r"hello\world"], """ [testenv:py36] commands = some {posargs} """, ) envconfig = config.envconfigs["py36"] assert envconfig.commands[0] == ["some", r"hello\world"] def test_provision_tox_env_cannot_be_in_envlist(newconfig, capsys): inisource = """ [tox] envlist = py36,.tox """ with pytest.raises( tox.exception.ConfigError, match="provision_tox_env .tox cannot be part of envlist", ): newconfig([], inisource) out, err = capsys.readouterr() assert not err assert not out def test_isolated_build_env_cannot_be_in_envlist(newconfig, capsys): inisource = """ [tox] envlist = py36,package isolated_build = True isolated_build_env = package """ with pytest.raises( tox.exception.ConfigError, match="isolated_build_env package cannot be part of envlist", ): newconfig([], inisource) out, err = capsys.readouterr() assert not err assert not out def test_isolated_build_overrides(newconfig, capsys): inisource = """ [tox] isolated_build = True [testenv] deps = something crazy here [testenv:.package] deps = """ config = newconfig([], inisource) deps = config.envconfigs.get(".package").deps assert deps == [] @pytest.mark.parametrize( "key, set_value, default", [("deps", "crazy", []), ("sitepackages", "True", False)], ) def test_isolated_build_ignores(newconfig, capsys, key, set_value, default): config = newconfig( [], """ [tox] isolated_build = True [testenv] {} = {} """.format( key, set_value, ), ) package_env = config.envconfigs.get(".package") value = getattr(package_env, key) assert value == default def test_config_via_pyproject_legacy(initproj): initproj( "config_via_pyproject_legacy-0.5", filedefs={ "pyproject.toml": ''' [tool.tox] legacy_tox_ini = """ [tox] envlist = py27 """ ''', }, ) config = parseconfig([]) assert config.envlist == ["py27"] def test_config_bad_pyproject_specified(initproj, capsys): base = initproj("config_via_pyproject_legacy-0.5", filedefs={"pyproject.toml": ""}) with pytest.raises(SystemExit): parseconfig(["-c", str(base.join("pyproject.toml"))]) out, err = capsys.readouterr() msg = "ERROR: tox config file (either pyproject.toml, tox.ini, setup.cfg) not found\n" assert err == msg assert "ERROR:" not in out def test_config_setup_cfg_no_tox_section(initproj, capsys): setup_cfg = """ [nope:nope] envlist = py37 """ initproj("setup_cfg_no_tox-0.1", filedefs={"setup.cfg": setup_cfg}) with pytest.raises(SystemExit): parseconfig([]) out, err = capsys.readouterr() msg = "ERROR: tox config file (either pyproject.toml, tox.ini, setup.cfg) not found\n" assert err == msg assert "ERROR:" not in out def test_config_file_not_required_with_devenv(initproj, capsys): initproj("no_tox_config-0.7") config = parseconfig(["--devenv", "myenv"]) out, err = capsys.readouterr() assert "ERROR:" not in err assert "ERROR:" not in out assert config.option.devenv == "myenv" assert config.option.notest is True @pytest.mark.skipif(sys.platform == "win32", reason="no named pipes on Windows") def test_config_bad_config_type_specified(monkeypatch, tmpdir, capsys): monkeypatch.chdir(tmpdir) name = tmpdir.join("named_pipe") os.mkfifo(str(name)) with pytest.raises(SystemExit): parseconfig(["-c", str(name)]) out, err = capsys.readouterr() notes = ( "ERROR: {} is neither file or directory".format(name), "ERROR: tox config file (either pyproject.toml, tox.ini, setup.cfg) not found", ) msg = "\n".join(notes) + "\n" assert err == msg assert "ERROR:" not in out def test_interactive_na(newconfig, monkeypatch): monkeypatch.setattr(tox.config, "is_interactive", lambda: False) config = newconfig( """ [testenv:py] setenv = A = {tty:X:Y} """, ) assert config.envconfigs["py"].setenv["A"] == "Y" def test_interactive_available(newconfig, monkeypatch): monkeypatch.setattr(tox.config, "is_interactive", lambda: True) config = newconfig( """ [testenv:py] setenv = A = {tty:X:Y} """, ) assert config.envconfigs["py"].setenv["A"] == "X" def test_interactive(): tox.config.is_interactive() def test_config_current_py(newconfig, current_tox_py, cmd, tmpdir, monkeypatch): monkeypatch.chdir(tmpdir) config = newconfig( """ [tox] envlist = {0} skipsdist = True [testenv:{0}] commands = python -c "print('all')" """.format( current_tox_py, ), ) assert config.envconfigs[current_tox_py] result = cmd() result.assert_success() def test_posargs_relative_changedir(newconfig, tmpdir): dir1 = tmpdir.join("dir1").ensure() tmpdir.join("dir2").ensure() with tmpdir.as_cwd(): config = newconfig( """\ [tox] [testenv] changedir = dir2 commands = echo {posargs} """, ) config.option.args = ["dir1", dir1.strpath, "dir3"] testenv = config.envconfigs["python"] PosargsOption().postprocess(testenv, config.option.args) assert testenv._reader.posargs == [ # should have relative-ized os.path.join("..", "dir1"), # should have stayed the same, dir1.strpath, "dir3", ] def test_config_no_version_data_in__name(newconfig, capsys): newconfig( """ [tox] envlist = py, pypy, jython [testenv] basepython = python """, ) out, err = capsys.readouterr() assert not out assert not err def test_overwrite_skip_install_override(newconfig): source = """ [tox] envlist = py, skip [testenv:skip] skip_install = True """ config = newconfig(args=[], source=source) assert config.envconfigs["py"].skip_install is False # by default do not skip assert config.envconfigs["skip"].skip_install is True config = newconfig(args=["--skip-pkg-install"], source=source) assert config.envconfigs["py"].skip_install is True # skip if the flag is passed assert config.envconfigs["skip"].skip_install is True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/config/test_config_parallel.py0000644000175100001710000000332300000000000023022 0ustar00vstsdocker00000000000000import pytest def test_parallel_default(newconfig): config = newconfig([], "") assert isinstance(config.option.parallel, int) assert config.option.parallel == 0 assert config.option.parallel_live is False def test_parallel_live_on(newconfig): config = newconfig(["-o"], "") assert config.option.parallel_live is True def test_parallel_auto(newconfig): config = newconfig(["-p", "auto"], "") assert isinstance(config.option.parallel, int) assert config.option.parallel > 0 def test_parallel_all(newconfig): config = newconfig(["-p", "all"], "") assert config.option.parallel is None def test_parallel_number(newconfig): config = newconfig(["-p", "2"], "") assert config.option.parallel == 2 def test_parallel_number_negative(newconfig, capsys): with pytest.raises(SystemExit): newconfig(["-p", "-1"], "") out, err = capsys.readouterr() assert not out assert "value must be positive" in err def test_depends(newconfig, capsys): config = newconfig( """\ [tox] [testenv:py] depends = py37, py36 """, ) assert config.envconfigs["py"].depends == ("py37", "py36") def test_depends_multi_row_facotr(newconfig, capsys): config = newconfig( """\ [tox] [testenv:py] depends = py37, {py36}-{a,b} """, ) assert config.envconfigs["py"].depends == ("py37", "py36-a", "py36-b") def test_depends_factor(newconfig, capsys): config = newconfig( """\ [tox] [testenv:py] depends = {py37, py36}-{cov,no} """, ) assert config.envconfigs["py"].depends == ("py37-cov", "py37-no", "py36-cov", "py36-no") ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9115124 tox-3.21.4/tests/unit/interpreters/0000755000175100001710000000000000000000000017550 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/interpreters/test_interpreters.py0000644000175100001710000001725400000000000023720 0ustar00vstsdocker00000000000000from __future__ import unicode_literals import os import platform import stat import subprocess import sys import py import pytest import tox from tox import reporter from tox.config import get_plugin_manager from tox.interpreters import ( ExecFailed, InterpreterInfo, Interpreters, NoInterpreterInfo, run_and_get_interpreter_info, tox_get_python_executable, ) from tox.reporter import Verbosity @pytest.fixture(name="interpreters") def create_interpreters_instance(): pm = get_plugin_manager() return Interpreters(hook=pm.hook) @pytest.mark.skipif(tox.INFO.IS_PYPY, reason="testing cpython interpreter discovery") def test_tox_get_python_executable(mocker): class envconfig: basepython = sys.executable envname = "pyxx" config = mocker.MagicMock() config.return_value.option.return_value.discover = [] def get_exe(name): envconfig.basepython = name p = tox_get_python_executable(envconfig) assert p return str(p) def assert_version_in_output(exe, version): out = subprocess.check_output((exe, "-V"), stderr=subprocess.STDOUT) assert version in out.decode() p = tox_get_python_executable(envconfig) assert p == py.path.local(sys.executable) for major, minor in [(2, 7), (3, 5), (3, 6), (3, 7), (3, 8)]: name = "python{}.{}".format(major, minor) if tox.INFO.IS_WIN: pydir = "python{}{}".format(major, minor) x = py.path.local(r"c:\{}".format(pydir)) if not x.check(): continue else: if not py.path.local.sysfind(name) or subprocess.call((name, "-c", "")): continue exe = get_exe(name) assert_version_in_output(exe, "{}.{}".format(major, minor)) has_py_exe = py.path.local.sysfind("py") is not None for major in (2, 3): name = "python{}".format(major) if has_py_exe: error_code = subprocess.call(("py", "-{}".format(major), "-c", "")) if error_code: continue elif not py.path.local.sysfind(name): continue exe = get_exe(name) assert_version_in_output(exe, str(major)) @pytest.mark.skipif("sys.platform == 'win32'", reason="symlink execution unreliable on Windows") def test_find_alias_on_path(monkeypatch, tmp_path, mocker): reporter.update_default_reporter(Verbosity.DEFAULT, Verbosity.DEBUG) magic = tmp_path / "magic{}".format(os.path.splitext(sys.executable)[1]) os.symlink(sys.executable, str(magic)) monkeypatch.setenv( str("PATH"), os.pathsep.join([str(tmp_path)] + os.environ.get(str("PATH"), "").split(os.pathsep)), ) class envconfig: basepython = "magic" envname = "pyxx" config = mocker.MagicMock() config.return_value.option.return_value.discover = [] detected = py.path.local.sysfind("magic") assert detected t = tox_get_python_executable(envconfig).lower() assert t == str(magic).lower() def test_run_and_get_interpreter_info(): name = os.path.basename(sys.executable) info = run_and_get_interpreter_info(name, sys.executable) assert info.version_info == tuple(sys.version_info) assert info.implementation == platform.python_implementation() assert info.executable == sys.executable class TestInterpreters: def test_get_executable(self, interpreters, mocker): class envconfig: basepython = sys.executable envname = "pyxx" config = mocker.MagicMock() config.return_value.option.return_value.discover = [] x = interpreters.get_executable(envconfig) assert x == sys.executable info = interpreters.get_info(envconfig) assert info.version_info == tuple(sys.version_info) assert info.executable == sys.executable assert isinstance(info, InterpreterInfo) def test_get_executable_no_exist(self, interpreters, mocker): class envconfig: basepython = "1lkj23" envname = "pyxx" config = mocker.MagicMock() config.return_value.option.return_value.discover = [] assert not interpreters.get_executable(envconfig) info = interpreters.get_info(envconfig) assert not info.version_info assert info.name == "1lkj23" assert not info.executable assert isinstance(info, NoInterpreterInfo) @pytest.mark.skipif("sys.platform == 'win32'", reason="Uses a unix only wrapper") def test_get_info_uses_hook_path(self, tmp_path): magic = tmp_path / "magic{}".format(os.path.splitext(sys.executable)[1]) wrapper = ( "#!{executable}\n" "import subprocess\n" "import sys\n" 'sys.exit(subprocess.call(["{executable}"] + sys.argv[1:]))\n' ).format(executable=sys.executable) magic.write_text(wrapper) magic.chmod(magic.stat().st_mode | stat.S_IEXEC) class MockHook: def tox_get_python_executable(self, envconfig): return str(magic) class envconfig: basepython = sys.executable envname = "magicpy" # Check that the wrapper is working first. # If it isn't, the default is to return the passed path anyway. subprocess.check_call([str(magic), "--help"]) interpreters = Interpreters(hook=MockHook()) info = interpreters.get_info(envconfig) assert info.executable == str(magic) def test_get_sitepackagesdir_error(self, interpreters, mocker): class envconfig: basepython = sys.executable envname = "123" config = mocker.MagicMock() config.return_value.option.return_value.discover = [] info = interpreters.get_info(envconfig) s = interpreters.get_sitepackagesdir(info, "") assert s def test_exec_failed(): x = ExecFailed("my-executable", "my-source", "my-out", "my-err") assert isinstance(x, Exception) assert x.executable == "my-executable" assert x.source == "my-source" assert x.out == "my-out" assert x.err == "my-err" class TestInterpreterInfo: @staticmethod def info( implementation="CPython", executable="my-executable", version_info="my-version-info", sysplatform="my-sys-platform", ): return InterpreterInfo(implementation, executable, version_info, sysplatform, True, None) def test_data(self): x = self.info("larry", "moe", "shemp", "curly") assert x.implementation == "larry" assert x.executable == "moe" assert x.version_info == "shemp" assert x.sysplatform == "curly" def test_str(self): x = self.info(executable="foo", version_info="bar") assert str(x) == "" class TestNoInterpreterInfo: def test_default_data(self): x = NoInterpreterInfo("foo") assert x.name == "foo" assert x.executable is None assert x.version_info is None assert x.out is None assert x.err == "not found" def test_set_data(self): 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 assert x.out == "my-out" assert x.err == "my-err" def test_str_without_executable(self): x = NoInterpreterInfo("coconut") assert str(x) == "" def test_str_with_executable(self): x = NoInterpreterInfo("coconut", executable="bang/em/together") assert str(x) == "" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/interpreters/test_py_spec.py0000644000175100001710000000067400000000000022632 0ustar00vstsdocker00000000000000from tox.interpreters.py_spec import PythonSpec def test_py_3_10(): spec = PythonSpec.from_name("python3.10") assert (spec.major, spec.minor) == (3, 10) def test_debug_python(): spec = PythonSpec.from_name("python3.10-dbg") assert (spec.major, spec.minor) == (None, None) def test_parse_architecture(): spec = PythonSpec.from_name("python3.10-32") assert (spec.major, spec.minor, spec.architecture) == (3, 10, 32) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9115124 tox-3.21.4/tests/unit/interpreters/windows/0000755000175100001710000000000000000000000021242 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/interpreters/windows/test_pep514.py0000644000175100001710000000122500000000000023671 0ustar00vstsdocker00000000000000from __future__ import unicode_literals import inspect import subprocess import sys from tox._pytestplugin import mark_dont_run_on_posix @mark_dont_run_on_posix def test_discover_winreg(): from tox.interpreters.windows.pep514 import discover_pythons list(discover_pythons()) # raises no error @mark_dont_run_on_posix def test_run_pep514_main_no_warnings(): # check we trigger no warnings import tox.interpreters.windows.pep514 as pep514 out = subprocess.check_output( [sys.executable, inspect.getsourcefile(pep514)], universal_newlines=True, ) assert "PEP-514 violation in Windows Registry " not in out, out ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/interpreters/windows/test_windows.py0000644000175100001710000000120300000000000024341 0ustar00vstsdocker00000000000000from tox._pytestplugin import mark_dont_run_on_posix @mark_dont_run_on_posix def test_locate_via_pep514(monkeypatch): import tox.interpreters.windows from tox.interpreters.py_spec import CURRENT del tox.interpreters.windows._PY_AVAILABLE[:] exe = tox.interpreters.windows.locate_via_pep514(CURRENT) assert exe assert len(tox.interpreters.windows._PY_AVAILABLE) import tox.interpreters.windows.pep514 def raise_on_call(): raise RuntimeError() monkeypatch.setattr(tox.interpreters.windows.pep514, "discover_pythons", raise_on_call) assert tox.interpreters.windows.locate_via_pep514(CURRENT) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9115124 tox-3.21.4/tests/unit/package/0000755000175100001710000000000000000000000016415 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9115124 tox-3.21.4/tests/unit/package/builder/0000755000175100001710000000000000000000000020043 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/package/builder/test_package_builder_isolated.py0000644000175100001710000001230600000000000026443 0ustar00vstsdocker00000000000000import os import py import pytest from tox.package.builder.isolated import get_build_info from tox.reporter import _INSTANCE def test_verbose_isolated_build(initproj, mock_venv, cmd): initproj( "example123-0.5", filedefs={ "tox.ini": """ [tox] isolated_build = true """, "pyproject.toml": """ [build-system] requires = ["setuptools >= 35.0.2"] build-backend = 'setuptools.build_meta' """, }, ) result = cmd("--sdistonly", "-v", "-v", "-v", "-e", "py") assert "running sdist" in result.out, result.out assert "running egg_info" in result.out, result.out assert "Writing example123-0.5{}setup.cfg".format(os.sep) in result.out, result.out def test_dist_exists_version_change(mock_venv, initproj, cmd): base = initproj( "package_toml-{}".format("0.1"), filedefs={ "tox.ini": """ [tox] isolated_build = true """, "pyproject.toml": """ [build-system] requires = ["setuptools >= 35.0.2"] build-backend = 'setuptools.build_meta' """, }, ) result = cmd("-e", "py") result.assert_success() new_code = base.join("setup.py").read_text("utf-8").replace("0.1", "0.2") base.join("setup.py").write_text(new_code, "utf-8") result = cmd("-e", "py") result.assert_success() def test_package_isolated_no_pyproject_toml(initproj, cmd): initproj( "package_no_toml-0.1", filedefs={ "tox.ini": """ [tox] isolated_build = true """, }, ) result = cmd("--sdistonly", "-e", "py") result.assert_fail() assert result.outlines == ["ERROR: missing {}".format(py.path.local().join("pyproject.toml"))] def toml_file_check(initproj, version, message, toml): initproj( "package_toml-{}".format(version), filedefs={ "tox.ini": """ [tox] isolated_build = true """, "pyproject.toml": toml, }, ) with pytest.raises(SystemExit, match="1"): get_build_info(py.path.local()) toml_file = py.path.local().join("pyproject.toml") msg = "ERROR: {} inside {}".format(message, toml_file) assert _INSTANCE.messages == [msg] def test_package_isolated_toml_no_build_system(initproj): toml_file_check(initproj, 1, "build-system section missing", "") def test_package_isolated_toml_no_requires(initproj): toml_file_check( initproj, 2, "missing requires key at build-system section", """ [build-system] """, ) def test_package_isolated_toml_no_backend(initproj): toml_file_check( initproj, 3, "missing build-backend key at build-system section", """ [build-system] requires = [] """, ) def test_package_isolated_toml_bad_requires(initproj): toml_file_check( initproj, 4, "requires key at build-system section must be a list of string", """ [build-system] requires = "" build-backend = "" """, ) def test_package_isolated_toml_bad_backend(initproj): toml_file_check( initproj, 5, "build-backend key at build-system section must be a string", """ [build-system] requires = [] build-backend = [] """, ) def test_package_isolated_toml_bad_backend_path(initproj): """Verify that a non-list 'backend-path' is forbidden.""" toml_file_check( initproj, 6, "backend-path key at build-system section must be a list, if specified", """ [build-system] requires = [] build-backend = 'setuptools.build_meta' backend-path = 42 """, ) def test_package_isolated_toml_backend_path_outside_root(initproj): """Verify that a 'backend-path' outside the project root is forbidden.""" toml_file_check( initproj, 6, "backend-path must exist in the project root", """ [build-system] requires = [] build-backend = 'setuptools.build_meta' backend-path = ['..'] """, ) def test_verbose_isolated_build_in_tree(initproj, mock_venv, cmd): initproj( "example123-0.5", filedefs={ "tox.ini": """ [tox] isolated_build = true """, "build.py": """ from setuptools.build_meta import * """, "pyproject.toml": """ [build-system] requires = ["setuptools >= 35.0.2"] build-backend = 'build' backend-path = ['.'] """, }, ) result = cmd("--sdistonly", "-v", "-v", "-v", "-e", "py") assert "running sdist" in result.out, result.out assert "running egg_info" in result.out, result.out assert "Writing example123-0.5{}setup.cfg".format(os.sep) in result.out, result.out ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/package/builder/test_package_builder_legacy.py0000644000175100001710000000100300000000000026073 0ustar00vstsdocker00000000000000import os def test_verbose_legacy_build(initproj, mock_venv, cmd): initproj( "example123-0.5", filedefs={ "tox.ini": """ [tox] isolated_build = false """, }, ) result = cmd("--sdistonly", "-vvv", "-e", "py") assert "running sdist" in result.out, result.out assert "running egg_info" in result.out, result.out assert "Writing example123-0.5{}setup.cfg".format(os.sep) in result.out, result.out ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/package/test_package.py0000644000175100001710000001161200000000000021422 0ustar00vstsdocker00000000000000import re import sys from tox.config import parseconfig from tox.package import get_package from tox.session import Session def test_install_via_installpkg(mock_venv, initproj, cmd): base = initproj( "pkg-0.1", filedefs={ "tox.ini": """ [tox] install_cmd = python -m -c 'print("ok")' -- {opts} {packages}' """, }, ) fake_package = base.ensure(".tox", "dist", "pkg123-0.1.zip") result = cmd("-e", "py", "--notest", "--installpkg", str(fake_package.relto(base))) result.assert_success() def test_installpkg(tmpdir, newconfig): p = tmpdir.ensure("pkg123-1.0.zip") config = newconfig(["--installpkg={}".format(p)], "") session = Session(config) _, sdist_path = get_package(session) assert sdist_path == p def test_sdist_latest(tmpdir, newconfig): distshare = tmpdir.join("distshare") config = newconfig( [], """ [tox] distshare={} sdistsrc={{distshare}}/pkg123-* """.format( distshare, ), ) p = distshare.ensure("pkg123-1.4.5.zip") distshare.ensure("pkg123-1.4.5a1.zip") session = Session(config) _, dist = get_package(session) assert dist == p def test_separate_sdist_no_sdistfile(cmd, initproj, tmpdir): distshare = tmpdir.join("distshare") initproj( ("pkg123-foo", "0.7"), filedefs={ "tox.ini": """ [tox] distshare={} """.format( distshare, ), }, ) result = cmd("--sdistonly", "-e", "py") assert not result.ret distshare_files = distshare.listdir() assert len(distshare_files) == 1 sdistfile = distshare_files[0] assert "pkg123-foo-0.7.zip" in str(sdistfile) def test_sdistonly(initproj, cmd): initproj( "example123", filedefs={ "tox.ini": """ """, }, ) result = cmd("-v", "--sdistonly", "-e", "py") assert not result.ret assert re.match(r".*sdist-make.*setup.py.*", result.out, re.DOTALL) assert "-mvirtualenv" not in result.out def test_make_sdist(initproj): initproj( "example123-0.5", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "tox.ini": """ """, }, ) config = parseconfig([]) session = Session(config) _, sdist = get_package(session) assert sdist.check() assert sdist.ext == ".zip" assert sdist == config.distdir.join(sdist.basename) _, sdist2 = get_package(session) assert sdist2 == sdist sdist.write("hello") assert sdist.stat().size < 10 _, sdist_new = get_package(Session(config)) assert sdist_new == sdist assert sdist_new.stat().size > 10 def test_build_backend_without_submodule(initproj, cmd): # The important part of this test is that the build backend # "inline_backend" is just a base package without a submodule. # (Regression test for #1344) initproj( "magic-0.1", filedefs={ "tox.ini": """\ [tox] isolated_build = true [testenv:.package] basepython = {} [testenv] setenv = PYTHONPATH = {{toxinidir}} """.format( sys.executable, ), "pyproject.toml": """\ [build-system] requires = [] build-backend = "inline_backend" """, # To trigger original bug, must be package with __init__.py "inline_backend": { "__init__.py": """\ import sys def get_requires_for_build_sdist(*args, **kwargs): return ["pathlib2;python_version<'3.4'"] def build_sdist(sdist_directory, config_settings=None): if sys.version_info[:2] >= (3, 4): import pathlib else: import pathlib2 as pathlib (pathlib.Path(sdist_directory) / "magic-0.1.0.tar.gz").touch() return "magic-0.1.0.tar.gz" """, }, ".gitignore": ".tox", }, add_missing_setup_py=False, ) result = cmd("--sdistonly", "-e", "py", "-v", "-v") result.assert_success(is_run_test_env=False) def test_package_inject(initproj, cmd, monkeypatch, tmp_path): monkeypatch.delenv(str("PYTHONPATH"), raising=False) initproj( "example123-0.5", filedefs={ "tox.ini": """ [testenv:py] passenv = PYTHONPATH commands = python -c 'import os; assert os.path.exists(os.environ["TOX_PACKAGE"])' """, }, ) result = cmd("-q") assert result.session.getvenv("py").envconfig.setenv.get("TOX_PACKAGE") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/package/test_package_parallel.py0000644000175100001710000001026000000000000023274 0ustar00vstsdocker00000000000000import os import traceback import py from flaky import flaky from tox.session.commands.run import sequential @flaky(max_runs=3) def test_tox_parallel_build_safe(initproj, cmd, mock_venv, monkeypatch): initproj( "env_var_test", filedefs={ "tox.ini": """ [tox] envlist = py install_cmd = python -m -c 'print("ok")' -- {opts} {packages}' [testenv] commands = python -c 'import sys; print(sys.version)' """, }, ) # we try to recreate the following situation # t1 starts and performs build # t2 starts, but is blocked from t1 build lock to build # t1 gets unblocked, t2 can now enter # t1 is artificially blocked to run test command until t2 finishes build # (parallel build package present) # t2 package build finishes both t1 and t2 can now finish and clean up their build packages import threading import tox.package t1_build_started = threading.Event() t1_build_blocker = threading.Event() t2_build_started = threading.Event() t2_build_finished = threading.Event() invoke_result = {} def invoke_tox_in_thread(thread_name): try: result = cmd("--parallel--safe-build", "-vv") except Exception as exception: result = exception, traceback.format_exc() invoke_result[thread_name] = result prev_build_package = tox.package.build_package with monkeypatch.context() as m: def build_package(config, session): t1_build_started.set() t1_build_blocker.wait() return prev_build_package(config, session) m.setattr(tox.package, "build_package", build_package) prev_run_test_env = sequential.runtestenv def run_test_env(venv, redirect=False): t2_build_finished.wait() return prev_run_test_env(venv, redirect) m.setattr(sequential, "runtestenv", run_test_env) t1 = threading.Thread(target=invoke_tox_in_thread, args=("t1",)) t1.start() t1_build_started.wait() with monkeypatch.context() as m: def build_package(config, session): t2_build_started.set() try: return prev_build_package(config, session) finally: t2_build_finished.set() m.setattr(tox.package, "build_package", build_package) t2 = threading.Thread(target=invoke_tox_in_thread, args=("t2",)) t2.start() # t2 should get blocked by t1 build lock t2_build_started.wait(timeout=0.1) assert not t2_build_started.is_set() t1_build_blocker.set() # release t1 blocker -> t1 can now finish # t1 at this point should block at run test until t2 build finishes t2_build_started.wait() t1.join() # wait for both t1 and t2 to finish t2.join() # all threads finished without error for val in invoke_result.values(): if isinstance(val, tuple): assert False, "{!r}\n{}".format(val[0], val[1]) err = "\n".join( "{}=\n{}".format(k, v.err).strip() for k, v in invoke_result.items() if v.err.strip() ) out = "\n".join( "{}=\n{}".format(k, v.out).strip() for k, v in invoke_result.items() if v.out.strip() ) for val in invoke_result.values(): assert not val.ret, "{}\n{}".format(err, out) assert not err # when the lock is hit we notify lock_file = py.path.local().join(".tox", ".package.lock") msg = "lock file {} present, will block until released".format(lock_file) assert msg in out # intermediate packages are removed at end of build t1_package = invoke_result["t1"].session.getvenv("py").package t2_package = invoke_result["t1"].session.getvenv("py").package assert t1 != t2 assert not t1_package.exists() assert not t2_package.exists() # the final distribution remains dist_after = invoke_result["t1"].session.config.distdir.listdir() assert len(dist_after) == 1 sdist = dist_after[0] assert t1_package != sdist # our set_os_env_var is not thread-safe so clean-up TOX_WORK_DIR os.environ.pop("TOX_WORK_DIR", None) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/package/test_package_view.py0000644000175100001710000000376200000000000022463 0ustar00vstsdocker00000000000000import os from tox.config import parseconfig from tox.package import get_package from tox.session import Session def test_make_sdist_distshare(tmpdir, initproj): distshare = tmpdir.join("distshare") initproj( "example123-0.6", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "tox.ini": """ [tox] distshare={} """.format( distshare, ), }, ) config = parseconfig([]) session = Session(config) package, dist = get_package(session) assert package.check() assert package.ext == ".zip" assert package == config.temp_dir.join("package", "1", package.basename) assert dist == config.distdir.join(package.basename) assert dist.check() assert os.stat(str(dist)).st_ino == os.stat(str(package)).st_ino sdist_share = config.distshare.join(package.basename) assert sdist_share.check() assert sdist_share.read("rb") == dist.read("rb"), (sdist_share, package) def test_separate_sdist(cmd, initproj, tmpdir): distshare = tmpdir.join("distshare") initproj( "pkg123-0.7", filedefs={ "tox.ini": """ [tox] distshare={} sdistsrc={{distshare}}/pkg123-0.7.zip """.format( distshare, ), }, ) result = cmd("--sdistonly", "-e", "py") assert not result.ret dist_share_files = distshare.listdir() assert len(dist_share_files) == 1 assert dist_share_files[0].check() result = cmd("-v", "--notest") result.assert_success() msg = "python inst: {}".format(result.session.package) assert msg in result.out, result.out operation = "copied" if not hasattr(os, "link") else "links" msg = "package {} {} to {}".format( os.sep.join(("pkg123", ".tox", ".tmp", "package", "1", "pkg123-0.7.zip")), operation, os.sep.join(("distshare", "pkg123-0.7.zip")), ) assert msg in result.out, result.out ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9115124 tox-3.21.4/tests/unit/session/0000755000175100001710000000000000000000000016505 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/session/__init__.py0000644000175100001710000000000000000000000020604 0ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9115124 tox-3.21.4/tests/unit/session/plugin/0000755000175100001710000000000000000000000020003 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9115124 tox-3.21.4/tests/unit/session/plugin/a/0000755000175100001710000000000000000000000020223 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/session/plugin/a/__init__.py0000644000175100001710000000026300000000000022335 0ustar00vstsdocker00000000000000import pluggy hookimpl = pluggy.HookimplMarker("tox") @hookimpl def tox_addoption(parser): parser.add_argument("--option", choices=["a", "b"], default="a", required=False) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/session/plugin/setup.cfg0000644000175100001710000000022500000000000021623 0ustar00vstsdocker00000000000000[metadata] name = plugin version = 0.1 description = test stuff [options] packages = find: zip_safe = True [options.entry_points] tox = plugin = a ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/session/plugin/setup.py0000644000175100001710000000004600000000000021515 0ustar00vstsdocker00000000000000from setuptools import setup setup() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/session/test_list_env.py0000644000175100001710000001423400000000000021745 0ustar00vstsdocker00000000000000def test_listenvs(cmd, initproj, monkeypatch): monkeypatch.delenv(str("TOXENV"), raising=False) initproj( "listenvs", filedefs={ "tox.ini": """ [tox] envlist=py36,py27,py37,pypi,docs description= py27: run pytest on Python 2.7 py37: run pytest on Python 3.6 pypi: publish to PyPI docs: document stuff notincluded: random extra [testenv:notincluded] changedir = whatever [testenv:docs] changedir = docs """, }, ) result = cmd("-l") assert result.outlines == ["py36", "py27", "py37", "pypi", "docs"] result = cmd("-l", "-e", "py") assert result.outlines == ["py36", "py27", "py37", "pypi", "docs"] monkeypatch.setenv(str("TOXENV"), str("py")) result = cmd("-l") assert result.outlines == ["py36", "py27", "py37", "pypi", "docs"] monkeypatch.setenv(str("TOXENV"), str("py36")) result = cmd("-l") assert result.outlines == ["py36", "py27", "py37", "pypi", "docs"] def test_listenvs_verbose_description(cmd, initproj): initproj( "listenvs_verbose_description", filedefs={ "tox.ini": """ [tox] envlist=py36,py27,py37,pypi,docs [testenv] description= py36: run pytest on Python 3.6 py27: run pytest on Python 2.7 py37: run pytest on Python 3.7 pypi: publish to PyPI docs: document stuff notincluded: random extra [testenv:notincluded] changedir = whatever [testenv:docs] changedir = docs description = let me overwrite that """, }, ) result = cmd("-lv") expected = [ "default environments:", "py36 -> run pytest on Python 3.6", "py27 -> run pytest on Python 2.7", "py37 -> run pytest on Python 3.7", "pypi -> publish to PyPI", "docs -> let me overwrite that", ] assert result.outlines[2:] == expected def test_listenvs_all(cmd, initproj, monkeypatch): initproj( "listenvs_all", filedefs={ "tox.ini": """ [tox] envlist=py36,py27,py37,pypi,docs [testenv:notincluded] changedir = whatever [testenv:docs] changedir = docs """, }, ) result = cmd("-a") expected = ["py36", "py27", "py37", "pypi", "docs", "notincluded"] assert result.outlines == expected result = cmd("-a", "-e", "py") assert result.outlines == ["py36", "py27", "py37", "pypi", "docs", "py", "notincluded"] monkeypatch.setenv(str("TOXENV"), str("py")) result = cmd("-a") assert result.outlines == ["py36", "py27", "py37", "pypi", "docs", "py", "notincluded"] monkeypatch.setenv(str("TOXENV"), str("py36")) result = cmd("-a") assert result.outlines == ["py36", "py27", "py37", "pypi", "docs", "notincluded"] def test_listenvs_all_verbose_description(cmd, initproj): initproj( "listenvs_all_verbose_description", filedefs={ "tox.ini": """ [tox] envlist={py27,py36}-{windows,linux} # py35 [testenv] description= py27: run pytest on Python 2.7 py36: run pytest on Python 3.6 windows: on Windows platform linux: on Linux platform docs: generate documentation commands=pytest {posargs} [testenv:docs] changedir = docs """, }, ) result = cmd("-av") expected = [ "default environments:", "py27-windows -> run pytest on Python 2.7 on Windows platform", "py27-linux -> run pytest on Python 2.7 on Linux platform", "py36-windows -> run pytest on Python 3.6 on Windows platform", "py36-linux -> run pytest on Python 3.6 on Linux platform", "", "additional environments:", "docs -> generate documentation", ] assert result.outlines[-len(expected) :] == expected def test_listenvs_all_verbose_description_no_additional_environments(cmd, initproj): initproj( "listenvs_all_verbose_description", filedefs={ "tox.ini": """ [tox] envlist=py27,py36 """, }, ) result = cmd("-av") expected = ["default environments:", "py27 -> [no description]", "py36 -> [no description]"] assert result.out.splitlines()[-3:] == expected assert "additional environments" not in result.out def test_listenvs_packaging_excluded(cmd, initproj): initproj( "listenvs", filedefs={ "tox.ini": """ [tox] envlist = py36,py27,py37,pypi,docs isolated_build = True [testenv:notincluded] changedir = whatever [testenv:docs] changedir = docs """, }, ) result = cmd("-a") expected = ["py36", "py27", "py37", "pypi", "docs", "notincluded"] assert result.outlines == expected, result.outlines def test_listenvs_all_extra_definition_order_decreasing(cmd, initproj): initproj( "listenvs_all", filedefs={ "tox.ini": """ [tox] envlist=py36 [testenv:b] changedir = whatever [testenv:a] changedir = docs """, }, ) result = cmd("-a") expected = ["py36", "b", "a"] assert result.outlines == expected def test_listenvs_all_extra_definition_order_increasing(cmd, initproj): initproj( "listenvs_all", filedefs={ "tox.ini": """ [tox] envlist=py36 [testenv:a] changedir = whatever [testenv:b] changedir = docs """, }, ) result = cmd("-a") expected = ["py36", "a", "b"] assert result.outlines == expected def test_listenvs_without_default_envs(cmd, initproj): """When running tox -l without any default envirinments, nothing happens.""" initproj( "logsnada", filedefs={"tox.ini": ""}, ) result = cmd("-l") assert result.ret == 0 assert result.out == "" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/session/test_parallel.py0000644000175100001710000002073300000000000021717 0ustar00vstsdocker00000000000000from __future__ import absolute_import, unicode_literals import json import os import subprocess import sys import threading import pytest from flaky import flaky from tox._pytestplugin import RunResult def test_parallel(cmd, initproj): initproj( "pkg123-0.7", filedefs={ "tox.ini": """ [tox] envlist = a, b isolated_build = true [testenv] commands=python -c "import sys; print(sys.executable)" [testenv:b] depends = a """, "pyproject.toml": """ [build-system] requires = ["setuptools >= 35.0.2"] build-backend = 'setuptools.build_meta' """, }, ) result = cmd("-p", "all") result.assert_success() @flaky(max_runs=3) def test_parallel_live(cmd, initproj): initproj( "pkg123-0.7", filedefs={ "tox.ini": """ [tox] isolated_build = true envlist = a, b [testenv] commands=python -c "import sys; print(sys.executable)" """, "pyproject.toml": """ [build-system] requires = ["setuptools >= 35.0.2"] build-backend = 'setuptools.build_meta' """, }, ) result = cmd("-p", "all", "-o") result.assert_success() def test_parallel_circular(cmd, initproj): initproj( "pkg123-0.7", filedefs={ "tox.ini": """ [tox] isolated_build = true envlist = a, b [testenv:a] depends = b [testenv:b] depends = a """, "pyproject.toml": """ [build-system] requires = ["setuptools >= 35.0.2"] build-backend = 'setuptools.build_meta' """, }, ) result = cmd("-p", "1") result.assert_fail() assert result.out == "ERROR: circular dependency detected: a | b\n" @pytest.mark.parametrize("live", [True, False]) def test_parallel_error_report(cmd, initproj, monkeypatch, live): monkeypatch.setenv(str("_TOX_SKIP_ENV_CREATION_TEST"), str("1")) initproj( "pkg123-0.7", filedefs={ "tox.ini": """ [tox] isolated_build = true envlist = a [testenv] skip_install = true commands=python -c "import sys, os; sys.stderr.write(str(12345) + os.linesep);\ raise SystemExit(17)" allowlist_externals = {} """.format( sys.executable, ), }, ) args = ["-o"] if live else [] result = cmd("-p", "all", *args) result.assert_fail() msg = result.out # for live we print the failure logfile, otherwise just stream through (no logfile present) assert "(exited with code 17)" in result.out, msg if not live: assert "ERROR: invocation failed (exit code 1), logfile:" in result.out, msg assert any(line for line in result.outlines if line == "12345"), result.out # single summary at end summary_lines = [j for j, l in enumerate(result.outlines) if " summary " in l] assert len(summary_lines) == 1, msg assert result.outlines[summary_lines[0] + 1 :] == ["ERROR: a: parallel child exit code 1"] def test_parallel_deadlock(cmd, initproj, monkeypatch): monkeypatch.setenv(str("_TOX_SKIP_ENV_CREATION_TEST"), str("1")) tox_ini = """\ [tox] envlist = e1,e2 skipsdist = true [testenv] allowlist_externals = {} commands = python -c '[print("hello world") for _ in range(5000)]' """.format( sys.executable, ) initproj("pkg123-0.7", filedefs={"tox.ini": tox_ini}) cmd("-p", "2") # used to hang indefinitely def test_parallel_recreate(cmd, initproj, monkeypatch): monkeypatch.setenv(str("_TOX_SKIP_ENV_CREATION_TEST"), str("1")) tox_ini = """\ [tox] envlist = e1,e2 skipsdist = true [testenv] allowlist_externals = {} commands = python -c '[print("hello world") for _ in range(1)]' """.format( sys.executable, ) cwd = initproj("pkg123-0.7", filedefs={"tox.ini": tox_ini}) log_dir = cwd / ".tox" / "e1" / "log" assert not log_dir.exists() cmd("-p", "2") after = log_dir.listdir() assert len(after) >= 2 res = cmd("-p", "2", "-rv") assert res end = log_dir.listdir() assert len(end) >= 3 assert not ({f.basename for f in after} - {f.basename for f in end}) @flaky(max_runs=3) def test_parallel_show_output(cmd, initproj, monkeypatch): monkeypatch.setenv(str("_TOX_SKIP_ENV_CREATION_TEST"), str("1")) tox_ini = """\ [tox] envlist = e1,e2,e3 skipsdist = true [testenv] allowlist_externals = {} commands = python -c 'import sys; sys.stderr.write("stderr env"); sys.stdout.write("stdout env")' [testenv:e3] commands = python -c 'import sys; sys.stderr.write("stderr always "); sys.stdout.write("stdout always ")' parallel_show_output = True """.format( sys.executable, ) initproj("pkg123-0.7", filedefs={"tox.ini": tox_ini}) result = cmd("-p", "all") result.assert_success() assert "stdout env" not in result.out, result.output() assert "stderr env" not in result.out, result.output() assert "stdout always" in result.out, result.output() assert "stderr always" in result.out, result.output() @pytest.fixture() def parallel_project(initproj): return initproj( "pkg123-0.7", filedefs={ "tox.ini": """ [tox] skipsdist = True envlist = a, b [testenv] skip_install = True commands=python -c "import sys; print(sys.executable)" """, }, ) def test_parallel_no_spinner_on(cmd, parallel_project, monkeypatch): monkeypatch.setenv(str("TOX_PARALLEL_NO_SPINNER"), str("1")) result = cmd("-p", "all") result.assert_success() assert "[2] a | b" not in result.out def test_parallel_no_spinner_off(cmd, parallel_project, monkeypatch): monkeypatch.setenv(str("TOX_PARALLEL_NO_SPINNER"), str("0")) result = cmd("-p", "all") result.assert_success() assert "[2] a | b" in result.out def test_parallel_no_spinner_not_set(cmd, parallel_project, monkeypatch): monkeypatch.delenv(str("TOX_PARALLEL_NO_SPINNER"), raising=False) result = cmd("-p", "all") result.assert_success() assert "[2] a | b" in result.out def test_parallel_result_json(cmd, parallel_project, tmp_path): parallel_result_json = tmp_path / "parallel.json" result = cmd("-p", "all", "--result-json", "{}".format(parallel_result_json)) ensure_result_json_ok(result, parallel_result_json) def ensure_result_json_ok(result, json_path): if isinstance(result, RunResult): result.assert_success() else: assert not isinstance(result, subprocess.CalledProcessError) assert json_path.exists() serial_data = json.loads(json_path.read_text()) ensure_key_in_env(serial_data) def ensure_key_in_env(serial_data): for env in ("a", "b"): for key in ("setup", "test"): assert key in serial_data["testenvs"][env], json.dumps( serial_data["testenvs"], indent=2, ) def test_parallel_result_json_concurrent(cmd, parallel_project, tmp_path): # first run to set up the environments (env creation is not thread safe) result = cmd("-p", "all") result.assert_success() invoke_result = {} def invoke_tox_in_thread(thread_name, result_json): try: # needs to be process to have it's own stdout invoke_result[thread_name] = subprocess.check_output( [sys.executable, "-m", "tox", "-p", "all", "--result-json", str(result_json)], universal_newlines=True, ) except subprocess.CalledProcessError as exception: invoke_result[thread_name] = exception # now concurrently parallel1_result_json = tmp_path / "parallel1.json" parallel2_result_json = tmp_path / "parallel2.json" threads = [ threading.Thread(target=invoke_tox_in_thread, args=(k, p)) for k, p in (("t1", parallel1_result_json), ("t2", parallel2_result_json)) ] [t.start() for t in threads] [t.join() for t in threads] ensure_result_json_ok(invoke_result["t1"], parallel1_result_json) ensure_result_json_ok(invoke_result["t2"], parallel2_result_json) # our set_os_env_var is not thread-safe so clean-up TOX_WORK_DIR os.environ.pop("TOX_WORK_DIR", None) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/session/test_provision.py0000644000175100001710000001573000000000000022154 0ustar00vstsdocker00000000000000from __future__ import absolute_import, unicode_literals import os import shutil import subprocess import sys import py import pytest if sys.version_info[:2] >= (3, 4): from pathlib import Path else: from pathlib2 import Path from six.moves.urllib.parse import urljoin from six.moves.urllib.request import pathname2url from tox.exception import BadRequirement, MissingRequirement @pytest.fixture(scope="session") def next_tox_major(): """a tox version we can guarantee to not be available""" return "10.0.0" def test_provision_min_version_is_requires(newconfig, next_tox_major): with pytest.raises(MissingRequirement) as context: newconfig( [], """\ [tox] minversion = {} """.format( next_tox_major, ), ) config = context.value.config deps = [r.name for r in config.envconfigs[config.provision_tox_env].deps] assert deps == ["tox >= {}".format(next_tox_major)] assert config.run_provision is True assert config.toxworkdir assert config.toxinipath assert config.provision_tox_env == ".tox" assert config.ignore_basepython_conflict is False def test_provision_tox_change_name(newconfig): config = newconfig( [], """\ [tox] provision_tox_env = magic """, ) assert config.provision_tox_env == "magic" def test_provision_basepython_global_only(newconfig, next_tox_major): """we don't want to inherit basepython from global""" with pytest.raises(MissingRequirement) as context: newconfig( [], """\ [tox] minversion = {} [testenv] basepython = what """.format( next_tox_major, ), ) config = context.value.config base_python = config.envconfigs[".tox"].basepython assert base_python == sys.executable def test_provision_basepython_local(newconfig, next_tox_major): """however adhere to basepython when explicitly set""" with pytest.raises(MissingRequirement) as context: newconfig( [], """\ [tox] minversion = {} [testenv:.tox] basepython = what """.format( next_tox_major, ), ) config = context.value.config base_python = config.envconfigs[".tox"].basepython assert base_python == "what" def test_provision_bad_requires(newconfig, capsys, monkeypatch): with pytest.raises(BadRequirement): newconfig( [], """\ [tox] requires = sad >sds d ok """, ) out, err = capsys.readouterr() assert "ERROR: failed to parse InvalidRequirement" in out assert not err @pytest.fixture() def plugin(monkeypatch, tmp_path): dest = tmp_path / "a" shutil.copytree(str(py.path.local(__file__).dirpath().join("plugin")), str(dest)) subprocess.check_output([sys.executable, "setup.py", "egg_info"], cwd=str(dest)) monkeypatch.setenv(str("PYTHONPATH"), str(dest)) def test_provision_cli_args_ignore(cmd, initproj, monkeypatch, plugin): import tox.config import tox.session prev_ensure = tox.config.ParseIni.ensure_requires_satisfied @staticmethod def ensure_requires_satisfied(config, requires, min_version): result = prev_ensure(config, requires, min_version) config.run_provision = True return result monkeypatch.setattr( tox.config.ParseIni, "ensure_requires_satisfied", ensure_requires_satisfied, ) prev_get_venv = tox.session.Session.getvenv def getvenv(self, name): venv = prev_get_venv(self, name) venv.envconfig.envdir = py.path.local(sys.executable).dirpath().dirpath() venv.setupenv = lambda: True venv.finishvenv = lambda: True return venv monkeypatch.setattr(tox.session.Session, "getvenv", getvenv) initproj("test-0.1", {"tox.ini": "[tox]"}) result = cmd("-a", "--option", "b") result.assert_success(is_run_test_env=False) def test_provision_cli_args_not_ignored_if_provision_false(cmd, initproj): initproj("test-0.1", {"tox.ini": "[tox]"}) result = cmd("-a", "--option", "b") result.assert_fail(is_run_test_env=False) @pytest.fixture(scope="session") def wheel(tmp_path_factory): """create a wheel for a project""" state = {"at": 0} def _wheel(path): state["at"] += 1 dest_path = tmp_path_factory.mktemp("wheel-{}-".format(state["at"])) env = os.environ.copy() try: subprocess.check_output( [ sys.executable, "-m", "pip", "wheel", "-w", str(dest_path), "--no-deps", str(path), ], universal_newlines=True, stderr=subprocess.STDOUT, env=env, ) except subprocess.CalledProcessError as exception: assert not exception.returncode, exception.output wheels = list(dest_path.glob("*.whl")) assert len(wheels) == 1 wheel = wheels[0] return wheel return _wheel THIS_PROJECT_ROOT = Path(__file__).resolve().parents[3] @pytest.fixture(scope="session") def tox_wheel(wheel): return wheel(THIS_PROJECT_ROOT) @pytest.fixture(scope="session") def magic_non_canonical_wheel(wheel, tmp_path_factory): magic_proj = tmp_path_factory.mktemp("magic") (magic_proj / "setup.py").write_text( "from setuptools import setup\nsetup(name='com.magic.this-is-fun')", ) return wheel(magic_proj) def test_provision_non_canonical_dep( cmd, initproj, monkeypatch, tox_wheel, magic_non_canonical_wheel, ): initproj( "w-0.1", { "tox.ini": """\ [tox] envlist = py requires = com.magic.this-is-fun tox == {} [testenv:.tox] passenv = * """.format( tox_wheel.name.split("-")[1], ), }, ) find_links = " ".join( space_path2url(d) for d in (tox_wheel.parent, magic_non_canonical_wheel.parent) ) monkeypatch.setenv(str("PIP_FIND_LINKS"), str(find_links)) result = cmd("-a", "-v", "-v") result.assert_success(is_run_test_env=False) def test_provision_requirement_with_environment_marker(cmd, initproj): initproj( "proj", { "tox.ini": """\ [tox] requires = package-that-does-not-exist;python_version=="1.0" """, }, ) result = cmd("-e", "py", "-vv") result.assert_success(is_run_test_env=False) def space_path2url(path): at_path = str(path) if " " not in at_path: return at_path return urljoin("file:", pathname2url(os.path.abspath(at_path))) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/session/test_session.py0000644000175100001710000002651300000000000021610 0ustar00vstsdocker00000000000000import os import pipes import sys import textwrap from threading import Thread import pytest import tox from tox.exception import MissingDependency, MissingDirectory from tox.package import resolve_package from tox.reporter import Verbosity def test_resolve_pkg_missing_directory(tmpdir, mocksession): distshare = tmpdir.join("distshare") spec = distshare.join("pkg123-*") with pytest.raises(MissingDirectory): resolve_package(spec) def test_resolve_pkg_missing_directory_in_distshare(tmpdir, mocksession): distshare = tmpdir.join("distshare") spec = distshare.join("pkg123-*") distshare.ensure(dir=1) with pytest.raises(MissingDependency): resolve_package(spec) def test_resolve_pkg_multiple_valid_versions(tmpdir, mocksession): mocksession.logging_levels(quiet=Verbosity.DEFAULT, verbose=Verbosity.DEBUG) distshare = tmpdir.join("distshare") distshare.ensure("pkg123-1.3.5.zip") p = distshare.ensure("pkg123-1.4.5.zip") result = resolve_package(distshare.join("pkg123-*")) assert result == p mocksession.report.expect("info", "determin*pkg123*") def test_resolve_pkg_with_invalid_version(tmpdir, mocksession): distshare = tmpdir.join("distshare") distshare.ensure("pkg123-1.something_bad.zip") distshare.ensure("pkg123-1.3.5.zip") p = distshare.ensure("pkg123-1.4.5.zip") result = resolve_package(distshare.join("pkg123-*")) mocksession.report.expect("warning", "*1.something_bad*") assert result == p def test_resolve_pkg_with_alpha_version(tmpdir, mocksession): distshare = tmpdir.join("distshare") distshare.ensure("pkg123-1.3.5.zip") distshare.ensure("pkg123-1.4.5a1.tar.gz") p = distshare.ensure("pkg123-1.4.5.zip") result = resolve_package(distshare.join("pkg123-*")) assert result == p def test_resolve_pkg_doubledash(tmpdir, mocksession): distshare = tmpdir.join("distshare") p = distshare.ensure("pkg-mine-1.3.0.zip") res = resolve_package(distshare.join("pkg-mine*")) assert res == p distshare.ensure("pkg-mine-1.3.0a1.zip") res = resolve_package(distshare.join("pkg-mine*")) assert res == p def test_skip_sdist(cmd, initproj): initproj( "pkg123-0.7", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "setup.py": """ syntax error """, "tox.ini": """ [tox] skipsdist=True [testenv] commands=python -c "print('done')" """, }, ) result = cmd() result.assert_success() def test_skip_install_skip_package(cmd, initproj, mock_venv): initproj( "pkg123-0.7", filedefs={ "setup.py": """raise RuntimeError""", "tox.ini": """ [tox] envlist = py [testenv] skip_install = true """, }, ) result = cmd("--notest") result.assert_success() @pytest.fixture() def venv_filter_project(initproj, cmd): def func(*args): initproj( "pkg123-0.7", filedefs={ "tox.ini": """ [tox] envlist = {py27,py36}-{nocov,cov,diffcov}{,-extra} skipsdist = true [testenv] skip_install = true commands = python -c 'print("{envname}")' """, }, ) result = cmd(*args) result.assert_success(is_run_test_env=False) active = [i.name for i in result.session.existing_venvs.values()] return active, result yield func def test_venv_filter_empty_all_active(venv_filter_project, monkeypatch): monkeypatch.delenv("TOX_SKIP_ENV", raising=False) active, result = venv_filter_project("-a") assert result.outlines == [ "py27-nocov", "py27-nocov-extra", "py27-cov", "py27-cov-extra", "py27-diffcov", "py27-diffcov-extra", "py36-nocov", "py36-nocov-extra", "py36-cov", "py36-cov-extra", "py36-diffcov", "py36-diffcov-extra", ] assert active == result.outlines def test_venv_filter_match_all_none_active(venv_filter_project, monkeypatch): monkeypatch.setenv("TOX_SKIP_ENV", ".*") active, result = venv_filter_project("-a") assert not active existing_envs = result.outlines _, result = venv_filter_project("-avv") for name in existing_envs: msg = "skip environment {}, matches filter '.*'".format(name) assert msg in result.outlines def test_venv_filter_match_some_some_active(venv_filter_project, monkeypatch): monkeypatch.setenv("TOX_SKIP_ENV", "py27.*") active, result = venv_filter_project("-avvv") assert active == [ "py36-nocov", "py36-nocov-extra", "py36-cov", "py36-cov-extra", "py36-diffcov", "py36-diffcov-extra", ] @pytest.fixture() def popen_env_test(initproj, cmd, monkeypatch): def func(tox_env, isolated_build): files = { "tox.ini": """ [tox] isolated_build = {} [testenv:{}] commands = python -c "print('ok')" """.format( "True" if isolated_build else "False", tox_env, ), } if isolated_build: files[ "pyproject.toml" ] = """ [build-system] requires = ["setuptools >= 35.0.2", "setuptools_scm >= 2.0.0, <3"] build-backend = 'setuptools.build_meta' """ initproj("env_var_test", filedefs=files) class IsolatedResult(object): def __init__(self): self.popens = [] self.cwd = None res = IsolatedResult() class EnvironmentTestRun(Thread): """we wrap this invocation into a thread to avoid modifying in any way the current threads environment variable (e.g. on failure of this test incorrect teardown) """ def run(self): prev_build = tox.session.build_session def build_session(config): res.session = prev_build(config) res._popen = res.session.popen monkeypatch.setattr(res.session, "popen", popen) return res.session monkeypatch.setattr(tox.session, "build_session", build_session) def popen(cmd, **kwargs): activity_id = _actions[-1].name activity_name = _actions[-1].activity ret = "NOTSET" try: ret = res._popen(cmd, **kwargs) except tox.exception.InvocationError as exception: ret = exception finally: res.popens.append( (activity_id, activity_name, kwargs.get("env"), ret, cmd), ) return ret _actions = [] from tox.action import Action _prev_enter = Action.__enter__ def enter(self): _actions.append(self) return _prev_enter(self) monkeypatch.setattr(Action, "__enter__", enter) _prev_exit = Action.__exit__ def exit_func(self, *args, **kwargs): del _actions[_actions.index(self)] _prev_exit(self, *args, **kwargs) monkeypatch.setattr(Action, "__exit__", exit_func) res.result = cmd("-e", tox_env) res.cwd = os.getcwd() thread = EnvironmentTestRun() thread.start() thread.join() return res yield func @pytest.mark.network def test_tox_env_var_flags_inserted_non_isolated(popen_env_test): res = popen_env_test("py", False) assert_popen_env(res) @pytest.mark.network def test_tox_env_var_flags_inserted_isolated(popen_env_test): res = popen_env_test("py", True) assert_popen_env(res) def assert_popen_env(res): res.result.assert_success() for tox_id, _, env, __, ___ in res.popens: assert env["TOX_WORK_DIR"] == os.path.join(res.cwd, ".tox") if tox_id != "GLOB": assert env["TOX_ENV_NAME"] == tox_id assert env["TOX_ENV_DIR"] == os.path.join(res.cwd, ".tox", tox_id) # ensure native strings for environ for windows for k, v in env.items(): assert type(k) is str, (k, v, type(k)) assert type(v) is str, (k, v, type(v)) def test_command_prev_post_ok(cmd, initproj, mock_venv): initproj( "pkg_command_test_123-0.7", filedefs={ "tox.ini": """ [tox] envlist = py [testenv] commands_pre = python -c 'print("pre")' commands = python -c 'print("command")' commands_post = python -c 'print("post")' """, }, ) result = cmd() result.assert_success() expected = textwrap.dedent( """ py run-test-pre: commands[0] | python -c 'print("pre")' pre py run-test: commands[0] | python -c 'print("command")' command py run-test-post: commands[0] | python -c 'print("post")' post ___________________________________ summary ___________________________________{} py: commands succeeded congratulations :) """.format( "_" if sys.platform != "win32" else "", ), ).lstrip() have = result.out.replace(os.linesep, "\n") actual = have[len(have) - len(expected) :] assert actual == expected def test_command_prev_fail_command_skip_post_run(cmd, initproj, mock_venv): initproj( "pkg_command_test_123-0.7", filedefs={ "tox.ini": """ [tox] envlist = py [testenv] commands_pre = python -c 'raise SystemExit(2)' commands = python -c 'print("command")' commands_post = python -c 'print("post")' """, }, ) result = cmd() result.assert_fail() expected = textwrap.dedent( """ py run-test-pre: commands[0] | python -c 'raise SystemExit(2)' ERROR: InvocationError for command {} -c 'raise SystemExit(2)' (exited with code 2) py run-test-post: commands[0] | python -c 'print("post")' post ___________________________________ summary ___________________________________{} ERROR: py: commands failed """.format( pipes.quote(sys.executable), "_" if sys.platform != "win32" else "", ), ) have = result.out.replace(os.linesep, "\n") actual = have[len(have) - len(expected) :] assert actual == expected def test_help_compound_ve_works(cmd, initproj, monkeypatch): initproj("test-0.1", {"tox.ini": ""}) result = cmd("-ve", "py", "-a") result.assert_success(is_run_test_env=False) assert not result.err assert result.outlines[0].startswith("using") assert result.outlines[1].startswith("using") assert result.outlines[2] == "additional environments:" assert result.outlines[3] == "py -> [no description]" assert len(result.outlines) == 4 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/session/test_show_config.py0000644000175100001710000000623600000000000022432 0ustar00vstsdocker00000000000000import py import pytest from six import PY2, StringIO from six.moves import configparser def load_config(args, cmd): result = cmd(*args) result.assert_success(is_run_test_env=False) parser = configparser.ConfigParser() output = StringIO(result.out) (parser.readfp if PY2 else parser.read_file)(output) return parser def test_showconfig_with_force_dep_version(cmd, initproj): initproj( "force_dep_version", filedefs={ "tox.ini": """ [tox] [testenv] deps= dep1==2.3 dep2 """, }, ) parser = load_config(("--showconfig",), cmd) assert parser.get("testenv:python", "deps") == "[dep1==2.3, dep2]" parser = load_config(("--showconfig", "--force-dep=dep1", "--force-dep=dep2==5.0"), cmd) assert parser.get("testenv:python", "deps") == "[dep1, dep2==5.0]" @pytest.fixture() def setup_mixed_conf(initproj): initproj( "force_dep_version", filedefs={ "tox.ini": """ [tox] envlist = py37,py27,pypi,docs [testenv:notincluded] changedir = whatever [testenv:docs] changedir = docs """, }, ) @pytest.mark.parametrize( "args, expected", [ ( ["--showconfig"], [ "tox", "tox:versions", "testenv:py37", "testenv:py27", "testenv:pypi", "testenv:docs", "testenv:notincluded", ], ), ( ["--showconfig", "-l"], [ "tox", "tox:versions", "testenv:py37", "testenv:py27", "testenv:pypi", "testenv:docs", ], ), (["--showconfig", "-e", "py37,py36"], ["testenv:py37", "testenv:py36"]), ], ids=["all", "default_only", "-e"], ) def test_showconfig(cmd, setup_mixed_conf, args, expected): parser = load_config(args, cmd) found_sections = parser.sections() assert found_sections == expected def test_showconfig_interpolation(cmd, initproj): initproj( "no_interpolation", filedefs={ "tox.ini": """ [tox] envlist = %s [testenv:%s] commands = python -c "print('works')" """, }, ) load_config(("--showconfig",), cmd) def test_config_specific_ini(tmpdir, cmd): ini = tmpdir.ensure("hello.ini") output = load_config(("-c", ini, "--showconfig"), cmd) assert output.get("tox", "toxinipath") == ini def test_override_workdir(cmd, initproj): baddir = "badworkdir-123" gooddir = "overridden-234" initproj( "overrideworkdir-0.5", filedefs={ "tox.ini": """ [tox] toxworkdir={} """.format( baddir, ), }, ) result = cmd("--workdir", gooddir, "--showconfig") assert not result.ret assert gooddir in result.out assert baddir not in result.out assert py.path.local(gooddir).check() assert not py.path.local(baddir).check() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/test_docs.py0000644000175100001710000000310300000000000017360 0ustar00vstsdocker00000000000000import os.path import re import textwrap import pytest import tox from tox.config import parseconfig INI_BLOCK_RE = re.compile( r"(?P" r"^(?P *)\.\. (code-block|sourcecode):: ini\n" r"((?P=indent) +:.*\n)*" r"\n*" r")" r"(?P(^((?P=indent) +.*)?\n)+)", re.MULTILINE, ) RST_FILES = [] TOX_ROOT = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) for root, _, filenames in os.walk(os.path.join(TOX_ROOT, "docs")): for filename in filenames: if filename.endswith(".rst"): RST_FILES.append(os.path.join(root, filename)) def test_some_files_exist(): assert RST_FILES @pytest.mark.parametrize("filename", RST_FILES) def test_all_rst_ini_blocks_parse(filename, tmpdir): with open(filename) as f: contents = f.read() for match in INI_BLOCK_RE.finditer(contents): code = textwrap.dedent(match.group("code")) config_path = tmpdir / "tox.ini" config_path.write(code) try: parseconfig(["-c", str(config_path)]) except tox.exception.MissingRequirement: pass except Exception as e: raise AssertionError( "Error parsing ini block\n\n" "{filename}:{lineno}\n\n" "{code}\n\n" "{error}\n\n{error!r}".format( filename=filename, lineno=contents[: match.start()].count("\n") + 1, code="\t" + code.replace("\n", "\n\t").strip(), error=e, ), ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/test_pytest_plugins.py0000644000175100001710000001114400000000000021525 0ustar00vstsdocker00000000000000""" Test utility tests, intended to cover use-cases not used in the current project test suite, e.g. as shown by the code coverage report. """ import os import sys import py.path import pytest from tox._pytestplugin import RunResult, _filedefs_contains, _path_parts class TestInitProj: @pytest.mark.parametrize( "kwargs", ({}, {"src_root": None}, {"src_root": ""}, {"src_root": "."}), ) def test_no_src_root(self, kwargs, tmpdir, initproj): initproj("black_knight-42", **kwargs) init_file = tmpdir.join("black_knight", "black_knight", "__init__.py") expected = b'""" module black_knight """' + linesep_bytes() + b"__version__ = '42'" assert init_file.read_binary() == expected def test_existing_src_root(self, tmpdir, initproj): initproj("spam-666", src_root="ham") assert not tmpdir.join("spam", "spam").check(exists=1) init_file = tmpdir.join("spam", "ham", "spam", "__init__.py") expected = b'""" module spam """' + linesep_bytes() + b"__version__ = '666'" assert init_file.read_binary() == expected def test_prebuilt_src_dir_with_no_src_root(self, tmpdir, initproj): initproj("spam-1.0", filedefs={"spam": {}}) src_dir = tmpdir.join("spam", "spam") assert src_dir.check(dir=1) assert not src_dir.join("__init__.py").check(exists=1) def test_prebuilt_src_dir_with_src_root(self, tmpdir, initproj): initproj( "spam-1.0", filedefs={"incontinentia": {"spam": {"__init__.py": "buttocks"}}}, src_root="incontinentia", ) assert not tmpdir.join("spam", "spam").check(exists=1) init_file = tmpdir.join("spam", "incontinentia", "spam", "__init__.py") assert init_file.read_binary() == b"buttocks" def test_broken_py_path_local_join_workaround_on_Windows(self, tmpdir, initproj, monkeypatch): # construct an absolute folder path for our src_root folder without the # Windows drive indicator src_root = tmpdir.join("spam") src_root = _path_parts(src_root) src_root[0] = "" src_root = "/".join(src_root) # make sure tmpdir drive is the current one so the constructed src_root # folder path gets interpreted correctly on Windows monkeypatch.chdir(tmpdir) # will throw an assertion error if the bug is not worked around initproj("spam-666", src_root=src_root) init_file = tmpdir.join("spam", "spam", "__init__.py") expected = b'""" module spam """' + linesep_bytes() + b"__version__ = '666'" assert init_file.read_binary() == expected def linesep_bytes(): return os.linesep.encode() class TestPathParts: @pytest.mark.parametrize( "input, expected", ( ("", []), ("/", ["/"]), ("//", ["//"]), ("/a", ["/", "a"]), ("/a/", ["/", "a"]), ("/a/b", ["/", "a", "b"]), ("a", ["a"]), ("a/b", ["a", "b"]), ), ) def test_path_parts(self, input, expected): assert _path_parts(input) == expected def test_on_py_path(self): cwd_parts = _path_parts(py.path.local()) folder_parts = _path_parts(py.path.local("a/b/c")) assert folder_parts[len(cwd_parts) :] == ["a", "b", "c"] @pytest.mark.parametrize( "base, filedefs, target, expected", ( ("/base", {}, "", False), ("/base", {}, "/base", False), ("/base", {"a": {"b": "data"}}, "", True), ("/base", {"a": {"b": "data"}}, "a", True), ("/base", {"a": {"b": "data"}}, "a/b", True), ("/base", {"a": {"b": "data"}}, "a/x", False), ("/base", {"a": {"b": "data"}}, "a/b/c", False), ("/base", {"a": {"b": "data"}}, "/base", True), ("/base", {"a": {"b": "data"}}, "/base/a", True), ("/base", {"a": {"b": "data"}}, "/base/a/b", True), ("/base", {"a": {"b": "data"}}, "/base/a/x", False), ("/base", {"a": {"b": "data"}}, "/base/a/b/c", False), ("/base", {"a": {"b": "data"}}, "/a", False), ), ) def test_filedefs_contains(base, filedefs, target, expected): assert bool(_filedefs_contains(base, filedefs, target)) == expected def test_run_result_repr(capfd): with RunResult(["hello", "world"], capfd) as run_result: # simulate tox writing some unicode output stdout_buffer = getattr(sys.stdout, "buffer", sys.stdout) stdout_buffer.write(u"\u2603".encode("UTF-8")) # must not `UnicodeError` on repr(...) ret = repr(run_result) # must be native `str`, (bytes in py2, str in py3) assert isinstance(ret, str) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/test_quickstart.py0000644000175100001710000002116100000000000020626 0ustar00vstsdocker00000000000000import os import pytest import tox from tox._quickstart import ( ALTERNATIVE_CONFIG_NAME, QUICKSTART_CONF, list_modificator, main, post_process_input, prepare_content, ) 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: '{}'".format(prompt)) try: answer = self._inputs.pop(0) print("user answer: '{}'".format(answer)) return answer except IndexError: pytest.fail("missing user answer for '{}'".format(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, {}".format(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 overridden, generated to alternative with default name"), _cnf(exists=True), ), ( _answers([4, "", "", "", "", "", "", "", ""]), _exp("existing not overridden, 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 overridden, 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 {}:\n{}\n".format(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("{} not in\n{}".format(sign, content)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/test_result.py0000644000175100001710000000750400000000000017757 0ustar00vstsdocker00000000000000import functools import os import signal import socket import sys import py import pytest import tox from tox.logs import ResultLog @pytest.fixture(name="pkg") def create_fake_pkg(tmpdir): pkg = tmpdir.join("hello-1.0.tar.gz") pkg.write("whatever") return pkg @pytest.fixture() def clean_hostname_envvar(monkeypatch): monkeypatch.delenv("HOSTNAME", raising=False) return functools.partial(monkeypatch.setenv, "HOSTNAME") def test_pre_set_header(clean_hostname_envvar): replog = ResultLog() d = replog.dict assert replog.dict == d assert replog.dict["reportversion"] == "1" assert replog.dict["toxversion"] == tox.__version__ assert replog.dict["platform"] == sys.platform assert replog.dict["host"] == socket.getfqdn() data = replog.dumps_json() replog2 = ResultLog.from_json(data) assert replog2.dict == replog.dict def test_set_header(pkg, clean_hostname_envvar): replog = ResultLog() d = replog.dict assert replog.dict == d assert replog.dict["reportversion"] == "1" assert replog.dict["toxversion"] == tox.__version__ assert replog.dict["platform"] == sys.platform assert replog.dict["host"] == socket.getfqdn() expected = {"basename": "hello-1.0.tar.gz", "sha256": pkg.computehash("sha256")} env_log = replog.get_envlog("a") env_log.set_header(installpkg=pkg) assert env_log.dict["installpkg"] == expected data = replog.dumps_json() replog2 = ResultLog.from_json(data) assert replog2.dict == replog.dict def test_hosname_via_envvar(clean_hostname_envvar): clean_hostname_envvar("toxicity") replog = ResultLog() assert replog.dict["host"] == "toxicity" def test_addenv_setpython(pkg): replog = ResultLog() envlog = replog.get_envlog("py36") envlog.set_python_info(py.path.local(sys.executable)) envlog.set_header(installpkg=pkg) assert envlog.dict["python"]["version_info"] == list(sys.version_info) assert envlog.dict["python"]["version"] == sys.version assert envlog.dict["python"]["executable"] == sys.executable def test_get_commandlog(pkg): replog = ResultLog() envlog = replog.get_envlog("py36") assert "setup" not in envlog.dict setuplog = envlog.get_commandlog("setup") envlog.set_header(installpkg=pkg) setuplog.add_command(["virtualenv", "..."], "venv created", 0) expected = [{"command": ["virtualenv", "..."], "output": "venv created", "retcode": 0}] assert setuplog.list == expected assert envlog.dict["setup"] setuplog2 = replog.get_envlog("py36").get_commandlog("setup") assert setuplog2.list == setuplog.list @pytest.mark.parametrize("exit_code", [None, 0, 5, 128 + signal.SIGTERM, 1234, -15]) @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.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 call_args = tox.exception.exit_code_str.call_args assert call_args == mocker.call("InvocationError", "", exit_code) if exit_code is None: assert "(exited with code" not in result elif exit_code == -15: assert "(exited with code -15 (SIGTERM))" in result else: assert "(exited with code {})".format(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 assert "({} - 128 = {}: SIGTERM)".format(exit_code, signal.SIGTERM) in result else: assert note not in result ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/test_venv.py0000644000175100001710000011414100000000000017413 0ustar00vstsdocker00000000000000import os import sys import py import pytest from six import PY2 import tox from tox.interpreters import NoInterpreterInfo from tox.session.commands.run.sequential import installpkg, runtestenv from tox.venv import ( CreationConfig, VirtualEnv, getdigest, prepend_shebang_interpreter, tox_testenv_create, tox_testenv_install_deps, ) def test_getdigest(tmpdir): assert getdigest(tmpdir) == "0" * 32 def test_getsupportedinterpreter(monkeypatch, newconfig, mocksession): config = newconfig( [], """\ [testenv:python] basepython={} """.format( sys.executable, ), ) mocksession.new_config(config) venv = mocksession.getvenv("python") interp = venv.getsupportedinterpreter() # realpath needed for debian symlinks assert py.path.local(interp).realpath() == py.path.local(sys.executable).realpath() monkeypatch.setattr(tox.INFO, "IS_WIN", True) monkeypatch.setattr(venv.envconfig, "basepython", "jython") with pytest.raises(tox.exception.UnsupportedInterpreter): venv.getsupportedinterpreter() monkeypatch.undo() monkeypatch.setattr(venv.envconfig, "envname", "py1") monkeypatch.setattr(venv.envconfig, "basepython", "notexisting") 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) with pytest.raises(tox.exception.InvocationError): venv.getsupportedinterpreter() def test_create(mocksession, newconfig): config = newconfig( [], """\ [testenv:py123] """, ) envconfig = config.envconfigs["py123"] mocksession.new_config(config) venv = mocksession.getvenv("py123") assert venv.path == envconfig.envdir assert not venv.path.check() with mocksession.newaction(venv.name, "getenv") as action: tox_testenv_create(action=action, venv=venv) pcalls = mocksession._pcalls assert len(pcalls) >= 1 args = pcalls[0].args assert "virtualenv" == str(args[2]) if not tox.INFO.IS_WIN: # realpath is needed for stuff like the debian symlinks our_sys_path = py.path.local(sys.executable).realpath() assert our_sys_path == py.path.local(args[0]).realpath() # assert Envconfig.toxworkdir in args assert venv.getcommandpath("pip", cwd=py.path.local()) interp = venv._getliveconfig().base_resolved_python_path assert interp == venv.envconfig.python_info.executable assert venv.path_config.check(exists=False) @pytest.mark.parametrize("patched_venv_methodname", ["_pcall", "update"]) def test_create_KeyboardInterrupt(mocksession, newconfig, mocker, patched_venv_methodname): config = newconfig( [], """\ [testenv:py123] deps = pip >= 19.3.1 """, ) mocksession.new_config(config) venv = mocksession.getvenv("py123") mocker.patch.object(venv, patched_venv_methodname, side_effect=KeyboardInterrupt) with pytest.raises(KeyboardInterrupt): venv.setupenv() assert venv.status == "keyboardinterrupt" def test_commandpath_venv_precedence(tmpdir, monkeypatch, mocksession, newconfig): config = newconfig( [], """\ [testenv:py123] """, ) mocksession.new_config(config) venv = mocksession.getvenv("py123") envconfig = venv.envconfig tmpdir.ensure("pip") monkeypatch.setenv("PATH", str(tmpdir), prepend=os.pathsep) envconfig.envbindir.ensure("pip") p = venv.getcommandpath("pip") assert py.path.local(p).relto(envconfig.envbindir), p def test_create_sitepackages(mocksession, newconfig): config = newconfig( [], """\ [testenv:site] sitepackages=True [testenv:nosite] sitepackages=False """, ) mocksession.new_config(config) venv = mocksession.getvenv("site") with mocksession.newaction(venv.name, "getenv") as action: tox_testenv_create(action=action, venv=venv) pcalls = mocksession._pcalls assert len(pcalls) >= 1 args = pcalls[0].args assert "--system-site-packages" in map(str, args) mocksession._clearmocks() venv = mocksession.getvenv("nosite") with mocksession.newaction(venv.name, "getenv") as action: tox_testenv_create(action=action, venv=venv) pcalls = mocksession._pcalls assert len(pcalls) >= 1 args = pcalls[0].args assert "--system-site-packages" not in map(str, args) assert "--no-site-packages" not in map(str, args) def test_install_deps_wildcard(newmocksession): mocksession = newmocksession( [], """\ [tox] distshare = {toxworkdir}/distshare [testenv:py123] deps= {distshare}/dep1-* """, ) venv = mocksession.getvenv("py123") with mocksession.newaction(venv.name, "getenv") as action: tox_testenv_create(action=action, venv=venv) pcalls = mocksession._pcalls assert len(pcalls) == 1 distshare = venv.envconfig.config.distshare distshare.ensure("dep1-1.0.zip") distshare.ensure("dep1-1.1.zip") tox_testenv_install_deps(action=action, venv=venv) assert len(pcalls) == 2 args = pcalls[-1].args assert pcalls[-1].cwd == venv.envconfig.config.toxinidir assert py.path.local.sysfind("python") == args[0] assert ["-m", "pip"] == args[1:3] assert args[3] == "install" args = [arg for arg in args if str(arg).endswith("dep1-1.1.zip")] assert len(args) == 1 def test_install_deps_indexserver(newmocksession): mocksession = newmocksession( [], """\ [tox] indexserver = abc = ABC abc2 = ABC [testenv:py123] deps= dep1 :abc:dep2 :abc2:dep3 """, ) venv = mocksession.getvenv("py123") with mocksession.newaction(venv.name, "getenv") as action: tox_testenv_create(action=action, venv=venv) pcalls = mocksession._pcalls assert len(pcalls) == 1 pcalls[:] = [] tox_testenv_install_deps(action=action, venv=venv) # two different index servers, two calls assert len(pcalls) == 3 args = " ".join(pcalls[0].args) assert "-i " not in args assert "dep1" in args args = " ".join(pcalls[1].args) assert "-i ABC" in args assert "dep2" in args args = " ".join(pcalls[2].args) assert "-i ABC" in args assert "dep3" in args def test_install_deps_pre(newmocksession): mocksession = newmocksession( [], """\ [testenv] pip_pre=true deps= dep1 """, ) venv = mocksession.getvenv("python") with mocksession.newaction(venv.name, "getenv") as action: tox_testenv_create(action=action, venv=venv) pcalls = mocksession._pcalls assert len(pcalls) == 1 pcalls[:] = [] tox_testenv_install_deps(action=action, venv=venv) assert len(pcalls) == 1 args = " ".join(pcalls[0].args) assert "--pre " in args assert "dep1" in args def test_installpkg_indexserver(newmocksession, tmpdir): mocksession = newmocksession( [], """\ [tox] indexserver = default = ABC """, ) venv = mocksession.getvenv("python") pcalls = mocksession._pcalls p = tmpdir.ensure("distfile.tar.gz") installpkg(venv, p) # two different index servers, two calls assert len(pcalls) == 1 args = " ".join(pcalls[0].args) assert "-i ABC" in args def test_install_recreate(newmocksession, tmpdir): pkg = tmpdir.ensure("package.tar.gz") mocksession = newmocksession( ["--recreate"], """\ [testenv] deps=xyz """, ) venv = mocksession.getvenv("python") with mocksession.newaction(venv.name, "update") as action: venv.update(action) installpkg(venv, pkg) mocksession.report.expect("verbosity0", "*create*") venv.update(action) mocksession.report.expect("verbosity0", "*recreate*") def test_install_sdist_extras(newmocksession): mocksession = newmocksession( [], """\ [testenv] extras = testing development """, ) venv = mocksession.getvenv("python") with mocksession.newaction(venv.name, "getenv") as action: tox_testenv_create(action=action, venv=venv) pcalls = mocksession._pcalls assert len(pcalls) == 1 pcalls[:] = [] venv.installpkg("distfile.tar.gz", action=action) assert "distfile.tar.gz[testing,development]" in pcalls[-1].args def test_develop_extras(newmocksession, tmpdir): mocksession = newmocksession( [], """\ [testenv] extras = testing development """, ) venv = mocksession.getvenv("python") with mocksession.newaction(venv.name, "getenv") as action: tox_testenv_create(action=action, venv=venv) pcalls = mocksession._pcalls assert len(pcalls) == 1 pcalls[:] = [] venv.developpkg(tmpdir, action=action) expected = "{}[testing,development]".format(tmpdir.strpath) assert expected in pcalls[-1].args def test_env_variables_added_to_needs_reinstall(tmpdir, mocksession, newconfig, monkeypatch): tmpdir.ensure("setup.py") monkeypatch.setenv("TEMP_PASS_VAR", "123") monkeypatch.setenv("TEMP_NOPASS_VAR", "456") config = newconfig( [], """\ [testenv:python] passenv = temp_pass_var setenv = CUSTOM_VAR = 789 """, ) mocksession.new_config(config) venv = mocksession.getvenv("python") with mocksession.newaction(venv.name, "hello") as action: venv._needs_reinstall(tmpdir, action) pcalls = mocksession._pcalls assert len(pcalls) == 2 env = pcalls[0].env # should have access to setenv vars assert "CUSTOM_VAR" in env assert env["CUSTOM_VAR"] == "789" # should have access to passenv vars assert "TEMP_PASS_VAR" in env assert env["TEMP_PASS_VAR"] == "123" # should also have access to full invocation environment, # for backward compatibility, and to match behavior of venv.run_install_command() assert "TEMP_NOPASS_VAR" in env assert env["TEMP_NOPASS_VAR"] == "456" def test_test_empty_commands(newmocksession): mocksession = newmocksession( [], """\ [testenv] commands = {posargs} echo foo bar """, ) venv = mocksession.getvenv("python") with mocksession.newaction(venv.name, "update") as action: venv.update(action) venv.test() # The first command is empty. It should be skipped. # Therefore, echo foo bar has index 0. mocksession.report.expect("verbosity0", "*run-test:*commands?0? | echo foo bar") def test_test_hashseed_is_in_output(newmocksession, monkeypatch): seed = "123456789" monkeypatch.setattr("tox.config.make_hashseed", lambda: seed) mocksession = newmocksession([], "") venv = mocksession.getvenv("python") with mocksession.newaction(venv.name, "update") as action: venv.update(action) tox.venv.tox_runtest_pre(venv) mocksession.report.expect("verbosity0", "run-test-pre: PYTHONHASHSEED='{}'".format(seed)) def test_test_runtests_action_command_is_in_output(newmocksession): mocksession = newmocksession( [], """\ [testenv] commands = echo foo bar """, ) venv = mocksession.getvenv("python") with mocksession.newaction(venv.name, "update") as action: venv.update(action) venv.test() mocksession.report.expect("verbosity0", "*run-test:*commands?0? | echo foo bar") def test_install_error(newmocksession): mocksession = newmocksession( ["--recreate"], """\ [testenv] deps=xyz commands= qwelkqw """, ) venv = mocksession.getvenv("python") venv.test() mocksession.report.expect("error", "*not find*qwelkqw*") assert venv.status == "commands failed" def test_install_command_not_installed(newmocksession): mocksession = newmocksession( ["--recreate"], """\ [testenv] commands= pytest """, ) venv = mocksession.getvenv("python") venv.status = 0 venv.test() mocksession.report.expect("warning", "*test command found but not*") assert venv.status == 0 def test_install_command_whitelisted(newmocksession): mocksession = newmocksession( ["--recreate"], """\ [testenv] whitelist_externals = pytest xy* commands= pytest xyz """, ) venv = mocksession.getvenv("python") venv.test() mocksession.report.expect("warning", "*test command found but not*", invert=True) assert venv.status == "commands failed" def test_install_command_allowlisted(newmocksession): mocksession = newmocksession( ["--recreate"], """\ [testenv] allowlist_externals = pytest xy* commands= pytest xyz """, ) venv = mocksession.getvenv("python") venv.test() mocksession.report.expect("warning", "*test command found but not*", invert=True) assert venv.status == "commands failed" def test_install_command_allowlisted_exclusive(newmocksession): mocksession = newmocksession( ["--recreate"], """\ [testenv] allowlist_externals = pytest whitelist_externals = xy* commands= pytest xyz """, ) venv = mocksession.getvenv("python") with pytest.raises(tox.exception.ConfigError): venv.test() def test_install_command_not_installed_bash(newmocksession): mocksession = newmocksession( ["--recreate"], """\ [testenv] commands= bash """, ) venv = mocksession.getvenv("python") venv.test() mocksession.report.expect("warning", "*test command found but not*") def test_install_python3(newmocksession): if not py.path.local.sysfind("python3") or tox.INFO.IS_PYPY: pytest.skip("needs cpython3") mocksession = newmocksession( [], """\ [testenv:py123] basepython=python3 deps= dep1 dep2 """, ) venv = mocksession.getvenv("py123") with mocksession.newaction(venv.name, "getenv") as action: tox_testenv_create(action=action, venv=venv) pcalls = mocksession._pcalls assert len(pcalls) == 1 args = pcalls[0].args assert str(args[2]) == "virtualenv" pcalls[:] = [] with mocksession.newaction(venv.name, "hello") as action: venv._install(["hello"], action=action) assert len(pcalls) == 1 args = pcalls[0].args assert py.path.local.sysfind("python") == args[0] assert ["-m", "pip"] == args[1:3] for _ in args: assert "--download-cache" not in args, args class TestCreationConfig: def test_basic(self, newconfig, mocksession, tmpdir): config = newconfig([], "") mocksession.new_config(config) venv = mocksession.getvenv("python") cconfig = venv._getliveconfig() assert cconfig.matches(cconfig) path = tmpdir.join("configdump") cconfig.writeconfig(path) newconfig = CreationConfig.readconfig(path) assert newconfig.matches(cconfig) assert cconfig.matches(newconfig) def test_matchingdependencies(self, newconfig, mocksession): config = newconfig( [], """\ [testenv] deps=abc """, ) mocksession.new_config(config) venv = mocksession.getvenv("python") cconfig = venv._getliveconfig() config = newconfig( [], """\ [testenv] deps=xyz """, ) mocksession.new_config(config) venv = mocksession.getvenv("python") otherconfig = venv._getliveconfig() assert not cconfig.matches(otherconfig) def test_matchingdependencies_file(self, newconfig, mocksession): config = newconfig( [], """\ [tox] distshare={toxworkdir}/distshare [testenv] deps=abc {distshare}/xyz.zip """, ) xyz = config.distshare.join("xyz.zip") xyz.ensure() mocksession.new_config(config) venv = mocksession.getvenv("python") cconfig = venv._getliveconfig() assert cconfig.matches(cconfig) xyz.write("hello") newconfig = venv._getliveconfig() assert not cconfig.matches(newconfig) def test_matchingdependencies_latest(self, newconfig, mocksession): config = newconfig( [], """\ [tox] distshare={toxworkdir}/distshare [testenv] deps={distshare}/xyz-* """, ) config.distshare.ensure("xyz-1.2.0.zip") xyz2 = config.distshare.ensure("xyz-1.2.1.zip") mocksession.new_config(config) venv = mocksession.getvenv("python") cconfig = venv._getliveconfig() sha256, path = cconfig.deps[0] assert path == xyz2 assert sha256 == path.computehash("sha256") def test_python_recreation(self, tmpdir, newconfig, mocksession): pkg = tmpdir.ensure("package.tar.gz") config = newconfig(["-v"], "") mocksession.new_config(config) venv = mocksession.getvenv("python") create_config = venv._getliveconfig() with mocksession.newaction(venv.name, "update") as action: venv.update(action) assert not venv.path_config.check() installpkg(venv, pkg) assert venv.path_config.check() assert mocksession._pcalls args1 = map(str, mocksession._pcalls[0].args) assert "virtualenv" in " ".join(args1) mocksession.report.expect("*", "*create*") # modify config and check that recreation happens mocksession._clearmocks() with mocksession.newaction(venv.name, "update") as action: venv.update(action) mocksession.report.expect("*", "*reusing*") mocksession._clearmocks() with mocksession.newaction(venv.name, "update") as action: create_config.base_resolved_python_path = py.path.local("balla") create_config.writeconfig(venv.path_config) venv.update(action) mocksession.report.expect("verbosity0", "*recreate*") def test_dep_recreation(self, newconfig, mocksession): config = newconfig([], "") mocksession.new_config(config) venv = mocksession.getvenv("python") with mocksession.newaction(venv.name, "update") as action: venv.update(action) cconfig = venv._getliveconfig() cconfig.deps[:] = [("1" * 32, "xyz.zip")] cconfig.writeconfig(venv.path_config) mocksession._clearmocks() with mocksession.newaction(venv.name, "update") as action: venv.update(action) mocksession.report.expect("*", "*recreate*") def test_develop_recreation(self, newconfig, mocksession): config = newconfig([], "") mocksession.new_config(config) venv = mocksession.getvenv("python") with mocksession.newaction(venv.name, "update") as action: venv.update(action) cconfig = venv._getliveconfig() cconfig.usedevelop = True cconfig.writeconfig(venv.path_config) mocksession._clearmocks() with mocksession.newaction(venv.name, "update") as action: venv.update(action) mocksession.report.expect("verbosity0", "*recreate*") class TestVenvTest: def test_envbindir_path(self, newmocksession, monkeypatch): monkeypatch.setenv("PIP_RESPECT_VIRTUALENV", "1") mocksession = newmocksession( [], """\ [testenv:python] commands=abc """, ) venv = mocksession.getvenv("python") with mocksession.newaction(venv.name, "getenv") as action: monkeypatch.setenv("PATH", "xyz") sysfind_calls = [] monkeypatch.setattr( "py.path.local.sysfind", classmethod(lambda *args, **kwargs: sysfind_calls.append(kwargs) or 0 / 0), ) with pytest.raises(ZeroDivisionError): venv._install(list("123"), action=action) assert sysfind_calls.pop()["paths"] == [venv.envconfig.envbindir] with pytest.raises(ZeroDivisionError): venv.test(action) assert sysfind_calls.pop()["paths"] == [venv.envconfig.envbindir] with pytest.raises(ZeroDivisionError): venv.run_install_command(["qwe"], action=action) assert sysfind_calls.pop()["paths"] == [venv.envconfig.envbindir] monkeypatch.setenv("PIP_RESPECT_VIRTUALENV", "1") monkeypatch.setenv("PIP_REQUIRE_VIRTUALENV", "1") monkeypatch.setenv("__PYVENV_LAUNCHER__", "1") prev_pcall = venv._pcall def collect(*args, **kwargs): env = kwargs["env"] assert "PIP_RESPECT_VIRTUALENV" not in env assert "PIP_REQUIRE_VIRTUALENV" not in env assert "__PYVENV_LAUNCHER__" not in env assert env["PIP_USER"] == "0" assert env["PIP_NO_DEPS"] == "0" return prev_pcall(*args, **kwargs) monkeypatch.setattr(venv, "_pcall", collect) with pytest.raises(ZeroDivisionError): venv.run_install_command(["qwe"], action=action) def test_pythonpath_remove(self, newmocksession, monkeypatch, caplog): monkeypatch.setenv("PYTHONPATH", "/my/awesome/library") mocksession = newmocksession( [], """\ [testenv:python] commands=abc """, ) venv = mocksession.getvenv("python") with mocksession.newaction(venv.name, "getenv") as action: venv.run_install_command(["qwe"], action=action) mocksession.report.expect("warning", "*Discarding $PYTHONPATH from environment*") pcalls = mocksession._pcalls assert len(pcalls) == 1 assert "PYTHONPATH" not in pcalls[0].env def test_pythonpath_keep(self, newmocksession, monkeypatch, caplog): # passenv = PYTHONPATH allows PYTHONPATH to stay in environment monkeypatch.setenv("PYTHONPATH", "/my/awesome/library") mocksession = newmocksession( [], """\ [testenv:python] commands=abc passenv = PYTHONPATH """, ) venv = mocksession.getvenv("python") with mocksession.newaction(venv.name, "getenv") as action: venv.run_install_command(["qwe"], action=action) mocksession.report.not_expect("warning", "*Discarding $PYTHONPATH from environment*") assert "PYTHONPATH" in os.environ pcalls = mocksession._pcalls assert len(pcalls) == 1 assert pcalls[0].env["PYTHONPATH"] == "/my/awesome/library" def test_pythonpath_empty(self, newmocksession, monkeypatch, caplog): monkeypatch.setenv("PYTHONPATH", "") mocksession = newmocksession( [], """\ [testenv:python] commands=abc """, ) venv = mocksession.getvenv("python") with mocksession.newaction(venv.name, "getenv") as action: venv.run_install_command(["qwe"], action=action) if sys.version_info < (3, 4): mocksession.report.expect("warning", "*Discarding $PYTHONPATH from environment*") else: with pytest.raises(AssertionError): mocksession.report.expect("warning", "*Discarding $PYTHONPATH from environment*") pcalls = mocksession._pcalls assert len(pcalls) == 1 assert "PYTHONPATH" not in pcalls[0].env def test_env_variables_added_to_pcall(tmpdir, mocksession, newconfig, monkeypatch, tmp_path): monkeypatch.delenv("PYTHONPATH", raising=False) pkg = tmpdir.ensure("package.tar.gz") monkeypatch.setenv("X123", "123") monkeypatch.setenv("YY", "456") env_path = tmp_path / ".env" env_file_content = "ENV_FILE_VAR = file_value" env_path.write_text(env_file_content.decode() if PY2 else env_file_content) config = newconfig( [], r""" [base] base_var = base_value [testenv:python] commands=python -V passenv = x123 setenv = ENV_VAR = value ESCAPED_VAR = \{value\} ESCAPED_VAR2 = \\{value\\} BASE_VAR = {[base]base_var} PYTHONPATH = value TTY_VAR = {tty:ON_VALUE:OFF_VALUE} COLON = {:} REUSED_FILE_VAR = reused {env:ENV_FILE_VAR} file| %s """ % env_path, ) mocksession._clearmocks() mocksession.new_config(config) venv = mocksession.getvenv("python") installpkg(venv, pkg) venv.test() pcalls = mocksession._pcalls assert len(pcalls) == 2 for x in pcalls: env = x.env assert env is not None assert "ENV_VAR" in env assert env["ENV_VAR"] == "value" assert env["ESCAPED_VAR"] == "{value}" assert env["ESCAPED_VAR2"] == r"\{value\}" assert env["COLON"] == ";" if sys.platform == "win32" else ":" assert env["TTY_VAR"] == "OFF_VALUE" assert env["ENV_FILE_VAR"] == "file_value" assert env["REUSED_FILE_VAR"] == "reused file_value" assert env["BASE_VAR"] == "base_value" assert env["VIRTUAL_ENV"] == str(venv.path) assert env["X123"] == "123" assert "PYTHONPATH" in env assert env["PYTHONPATH"] == "value" # all env variables are passed for installation assert pcalls[0].env["YY"] == "456" assert "YY" not in pcalls[1].env assert {"ENV_VAR", "VIRTUAL_ENV", "PYTHONHASHSEED", "X123", "PATH"}.issubset(pcalls[1].env) # setenv does not trigger PYTHONPATH warnings mocksession.report.not_expect("warning", "*Discarding $PYTHONPATH from environment*") # for e in os.environ: # assert e in env def test_installpkg_no_upgrade(tmpdir, newmocksession): pkg = tmpdir.ensure("package.tar.gz") mocksession = newmocksession([], "") venv = mocksession.getvenv("python") venv.just_created = True venv.envconfig.envdir.ensure(dir=1) installpkg(venv, pkg) pcalls = mocksession._pcalls assert len(pcalls) == 1 assert pcalls[0].args[1:-1] == ["-m", "pip", "install", "--exists-action", "w"] @pytest.mark.parametrize("count, level", [(0, 0), (1, 0), (2, 0), (3, 1), (4, 2), (5, 3), (6, 3)]) def test_install_command_verbosity(tmpdir, newmocksession, count, level): pkg = tmpdir.ensure("package.tar.gz") mock_session = newmocksession(["-{}".format("v" * count)], "") env = mock_session.getvenv("python") env.just_created = True env.envconfig.envdir.ensure(dir=1) installpkg(env, pkg) pcalls = mock_session._pcalls assert len(pcalls) == 1 expected = ["-m", "pip", "install", "--exists-action", "w"] + (["-v"] * level) assert pcalls[0].args[1:-1] == expected def test_installpkg_upgrade(newmocksession, tmpdir): pkg = tmpdir.ensure("package.tar.gz") mocksession = newmocksession([], "") venv = mocksession.getvenv("python") assert not hasattr(venv, "just_created") installpkg(venv, pkg) pcalls = mocksession._pcalls assert len(pcalls) == 1 index = pcalls[0].args.index(pkg.basename) assert index >= 0 assert "-U" in pcalls[0].args[:index] assert "--no-deps" in pcalls[0].args[:index] def test_run_install_command(newmocksession): mocksession = newmocksession([], "") venv = mocksession.getvenv("python") venv.just_created = True venv.envconfig.envdir.ensure(dir=1) with mocksession.newaction(venv.name, "hello") as action: venv.run_install_command(packages=["whatever"], action=action) pcalls = mocksession._pcalls assert len(pcalls) == 1 args = pcalls[0].args assert py.path.local.sysfind("python") == args[0] assert ["-m", "pip"] == args[1:3] assert "install" in args env = pcalls[0].env assert env is not None def test_run_custom_install_command(newmocksession): mocksession = newmocksession( [], """\ [testenv] install_command=cool-installer {opts} {packages} """, ) venv = mocksession.getvenv("python") venv.just_created = True venv.envconfig.envdir.ensure(dir=1) venv.envconfig.envbindir.ensure("cool-installer") with mocksession.newaction(venv.name, "hello") as action: venv.run_install_command(packages=["whatever"], action=action) pcalls = mocksession._pcalls assert len(pcalls) == 1 assert "cool-installer" in pcalls[0].args[0] assert pcalls[0].args[1:] == ["whatever"] def test_run_install_command_handles_KeyboardInterrupt(newmocksession, mocker): mocksession = newmocksession([], "") venv = mocksession.getvenv("python") venv.just_created = True venv.envconfig.envdir.ensure(dir=1) mocker.patch.object(venv, "_pcall", side_effect=KeyboardInterrupt) with mocksession.newaction(venv.name, "hello") as action: with pytest.raises(KeyboardInterrupt): venv.run_install_command(packages=["whatever"], action=action) assert venv.status == "keyboardinterrupt" def test_command_relative_issue36(newmocksession, tmpdir, monkeypatch): mocksession = newmocksession( [], """\ [testenv] """, ) x = tmpdir.ensure("x") venv = mocksession.getvenv("python") x2 = venv.getcommandpath("./x", cwd=tmpdir) assert x == x2 mocksession.report.not_expect("warning", "*test command found but not*") x3 = venv.getcommandpath("/bin/bash", cwd=tmpdir) assert x3 == "/bin/bash" mocksession.report.not_expect("warning", "*test command found but not*") monkeypatch.setenv("PATH", str(tmpdir)) x4 = venv.getcommandpath("x", cwd=tmpdir) assert x4.endswith(os.sep + "x") mocksession.report.expect("warning", "*test command found but not*") def test_ignore_outcome_failing_cmd(newmocksession): mocksession = newmocksession( [], """\ [testenv] commands=testenv_fail ignore_outcome=True """, ) venv = mocksession.getvenv("python") venv.test() assert venv.status == "ignored failed command" mocksession.report.expect("warning", "*command failed but result from testenv is ignored*") def test_tox_testenv_create(newmocksession): log = [] class Plugin: @tox.hookimpl def tox_testenv_create(self, action, venv): assert isinstance(action, tox.session.Action) assert isinstance(venv, VirtualEnv) log.append(1) @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( [], """\ [testenv] commands=testenv_fail ignore_outcome=True """, plugins=[Plugin()], ) venv = mocksession.getvenv("python") with mocksession.newaction(venv.name, "getenv") as action: venv.update(action=action) assert log == [1, 2] def test_tox_testenv_pre_post(newmocksession): log = [] class Plugin: @tox.hookimpl def tox_runtest_pre(self): log.append("started") @tox.hookimpl def tox_runtest_post(self): log.append("finished") mocksession = newmocksession( [], """\ [testenv] commands=testenv_fail """, plugins=[Plugin()], ) venv = mocksession.getvenv("python") venv.status = None assert log == [] runtestenv(venv, venv.envconfig.config) assert log == ["started", "finished"] @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") def test_tox_testenv_interpret_shebang_empty_instance(tmpdir): testfile = tmpdir.join("check_shebang_empty_instance.py") base_args = [str(testfile), "arg1", "arg2", "arg3"] # empty instance testfile.write("") args = prepend_shebang_interpreter(base_args) assert args == base_args @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") def test_tox_testenv_interpret_shebang_empty_interpreter(tmpdir): testfile = tmpdir.join("check_shebang_empty_interpreter.py") base_args = [str(testfile), "arg1", "arg2", "arg3"] # empty interpreter testfile.write("#!") args = prepend_shebang_interpreter(base_args) assert args == base_args @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") def test_tox_testenv_interpret_shebang_empty_interpreter_ws(tmpdir): testfile = tmpdir.join("check_shebang_empty_interpreter_ws.py") base_args = [str(testfile), "arg1", "arg2", "arg3"] # empty interpreter (whitespaces) testfile.write("#! \n") args = prepend_shebang_interpreter(base_args) assert args == base_args @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") def test_tox_testenv_interpret_shebang_non_utf8(tmpdir): testfile = tmpdir.join("check_non_utf8.py") base_args = [str(testfile), "arg1", "arg2", "arg3"] testfile.write_binary(b"#!\x9a\xef\x12\xaf\n") args = prepend_shebang_interpreter(base_args) assert args == base_args @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") def test_tox_testenv_interpret_shebang_interpreter_simple(tmpdir): testfile = tmpdir.join("check_shebang_interpreter_simple.py") base_args = [str(testfile), "arg1", "arg2", "arg3"] # interpreter (simple) testfile.write("#!interpreter") args = prepend_shebang_interpreter(base_args) assert args == ["interpreter"] + base_args @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") def test_tox_testenv_interpret_shebang_interpreter_ws(tmpdir): testfile = tmpdir.join("check_shebang_interpreter_ws.py") base_args = [str(testfile), "arg1", "arg2", "arg3"] # interpreter (whitespaces) testfile.write("#! interpreter \n\n") args = prepend_shebang_interpreter(base_args) assert args == ["interpreter"] + base_args @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") def test_tox_testenv_interpret_shebang_interpreter_arg(tmpdir): testfile = tmpdir.join("check_shebang_interpreter_arg.py") base_args = [str(testfile), "arg1", "arg2", "arg3"] # interpreter with argument testfile.write("#!interpreter argx\n") args = prepend_shebang_interpreter(base_args) assert args == ["interpreter", "argx"] + base_args @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") def test_tox_testenv_interpret_shebang_interpreter_args(tmpdir): testfile = tmpdir.join("check_shebang_interpreter_args.py") base_args = [str(testfile), "arg1", "arg2", "arg3"] # interpreter with argument (ensure single argument) testfile.write("#!interpreter argx argx-part2\n") args = prepend_shebang_interpreter(base_args) assert args == ["interpreter", "argx argx-part2"] + base_args @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") def test_tox_testenv_interpret_shebang_real(tmpdir): testfile = tmpdir.join("check_shebang_real.py") base_args = [str(testfile), "arg1", "arg2", "arg3"] # interpreter (real example) testfile.write("#!/usr/bin/env python\n") args = prepend_shebang_interpreter(base_args) assert args == ["/usr/bin/env", "python"] + base_args @pytest.mark.skipif("sys.platform == 'win32'", reason="no shebang on Windows") def test_tox_testenv_interpret_shebang_long_example(tmpdir): testfile = tmpdir.join("check_shebang_long_example.py") base_args = [str(testfile), "arg1", "arg2", "arg3"] # interpreter (long example) testfile.write( "#!this-is-an-example-of-a-very-long-interpret-directive-what-should-" "be-directly-invoked-when-tox-needs-to-invoked-the-provided-script-" "name-in-the-argument-list", ) args = prepend_shebang_interpreter(base_args) expected = [ "this-is-an-example-of-a-very-long-interpret-directive-what-should-be-" "directly-invoked-when-tox-needs-to-invoked-the-provided-script-name-" "in-the-argument-list", ] assert args == expected + base_args @pytest.mark.parametrize("download", [True, False, None]) def test_create_download(mocksession, newconfig, download): config = newconfig( [], """\ [testenv:env] {} """.format( "download={}".format(download) if download else "", ), ) mocksession.new_config(config) venv = mocksession.getvenv("env") with mocksession.newaction(venv.name, "getenv") as action: tox_testenv_create(action=action, venv=venv) pcalls = mocksession._pcalls assert len(pcalls) >= 1 args = pcalls[0].args if download is True: assert "--no-download" not in map(str, args) else: assert "--no-download" in map(str, args) mocksession._clearmocks() def test_path_change(tmpdir, mocksession, newconfig, monkeypatch): config = newconfig( [], """\ [testenv:python] setenv = PATH = {env:PATH}{:}{toxinidir}/bin """, ) pkg = tmpdir.ensure("package.tar.gz") mocksession._clearmocks() mocksession.new_config(config) venv = mocksession.getvenv("python") installpkg(venv, pkg) venv.test() pcalls = mocksession._pcalls for x in pcalls: path = x.env["PATH"] assert os.environ["PATH"] in path assert path.endswith(str(venv.envconfig.config.toxinidir) + "/bin") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/test_z_cmdline.py0000644000175100001710000007705400000000000020414 0ustar00vstsdocker00000000000000import json import os import re import shutil import subprocess import sys import tempfile if sys.version_info[:2] >= (3, 4): import pathlib else: import pathlib2 as pathlib import py import pytest import tox from tox.config import parseconfig from tox.reporter import Verbosity from tox.session import Session pytest_plugins = "pytester" class TestSession: def test_log_pcall(self, mocksession): mocksession.logging_levels(quiet=Verbosity.DEFAULT, verbose=Verbosity.INFO) mocksession.config.logdir.ensure(dir=1) assert not mocksession.config.logdir.listdir() with mocksession.newaction("what", "something") as action: action.popen(["echo"]) match = mocksession.report.getnext("logpopen") log_name = py.path.local(match[1].split(">")[-1].strip()).relto( mocksession.config.logdir, ) assert log_name == "what-0.log" def test_summary_status(self, initproj, capfd): initproj( "logexample123-0.5", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "tox.ini": """ [testenv:hello] [testenv:world] """, }, ) config = parseconfig([]) session = Session(config) envs = list(session.venv_dict.values()) assert len(envs) == 2 env1, env2 = envs env1.status = "FAIL XYZ" assert env1.status env2.status = 0 assert not env2.status session._summary() out, err = capfd.readouterr() exp = "{}: FAIL XYZ".format(env1.envconfig.envname) assert exp in out exp = "{}: commands succeeded".format(env2.envconfig.envname) assert exp in out def test_getvenv(self, initproj): initproj( "logexample123-0.5", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "tox.ini": """ [testenv:hello] [testenv:world] """, }, ) config = parseconfig([]) session = Session(config) venv1 = session.getvenv("hello") venv2 = session.getvenv("hello") assert venv1 is venv2 venv1 = session.getvenv("world") venv2 = session.getvenv("world") assert venv1 is venv2 with pytest.raises(LookupError): session.getvenv("qwe") def test_notoxini_help_still_works(initproj, cmd): initproj("example123-0.5", filedefs={"tests": {"test_hello.py": "def test_hello(): pass"}}) result = cmd("-h") assert result.out.startswith("usage: ") assert any("--help" in line for line in result.outlines), result.outlines result.assert_success(is_run_test_env=False) def test_notoxini_noerror_in_help(initproj, cmd): initproj("examplepro", filedefs={}) result = cmd("-h") msg = "ERROR: tox config file (either pyproject.toml, tox.ini, setup.cfg) not found\n" assert result.err != msg def test_notoxini_help_ini_still_works(initproj, cmd): initproj("example123-0.5", filedefs={"tests": {"test_hello.py": "def test_hello(): pass"}}) result = cmd("--help-ini") assert any("setenv" in line for line in result.outlines), result.outlines result.assert_success(is_run_test_env=False) def test_notoxini_noerror_in_help_ini(initproj, cmd): initproj("examplepro", filedefs={}) result = cmd("--help-ini") msg = "ERROR: tox config file (either pyproject.toml, tox.ini, setup.cfg) not found\n" assert result.err != msg def test_unrecognized_arguments_error(initproj, cmd): initproj( "examplepro1", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "tox.ini": """ [testenv:hello] [testenv:world] """, }, ) result1 = cmd("--invalid-argument") withtoxini = result1.err initproj("examplepro2", filedefs={}) result2 = cmd("--invalid-argument") notoxini = result2.err assert withtoxini == notoxini def test_envdir_equals_toxini_errors_out(cmd, initproj): initproj( "interp123-0.7", filedefs={ "tox.ini": """ [testenv] envdir={toxinidir} """, }, ) result = cmd() assert result.outlines[1] == "ERROR: ConfigError: envdir must not equal toxinidir" assert re.match( r"ERROR: venv \'python\' in .* would delete project", result.outlines[0], ), result.outlines[0] result.assert_fail() def test_envdir_would_delete_some_directory(cmd, initproj): projdir = initproj( "example-123", filedefs={ "tox.ini": """\ [tox] [testenv:venv] envdir=example commands= """, }, ) result = cmd("-e", "venv") assert projdir.join("example/__init__.py").exists() result.assert_fail() assert "cowardly refusing to delete `envdir`" in result.out def test_recreate(cmd, initproj): initproj("example-123", filedefs={"tox.ini": ""}) cmd("-e", "py", "--notest").assert_success() cmd("-r", "-e", "py", "--notest").assert_success() def test_run_custom_install_command_error(cmd, initproj): initproj( "interp123-0.5", filedefs={ "tox.ini": """ [testenv] install_command=./tox.ini {opts} {packages} """, }, ) result = cmd() result.assert_fail() re.match( r"ERROR: python: InvocationError for command .* \(exited with code \d+\)", result.outlines[-1], ), result.out def test_unknown_interpreter_and_env(cmd, initproj): initproj( "interp123-0.5", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "tox.ini": """\ [testenv:python] basepython=xyz_unknown_interpreter [testenv] changedir=tests skip_install = true """, }, ) result = cmd() result.assert_fail() assert "ERROR: InterpreterNotFound: xyz_unknown_interpreter" in result.outlines result = cmd("-exyz") result.assert_fail() assert result.out == "ERROR: unknown environment 'xyz'\n" def test_unknown_interpreter_factor(cmd, initproj): initproj("py21", filedefs={"tox.ini": "[testenv]\nskip_install=true"}) result = cmd("-e", "py21") result.assert_fail() assert "ERROR: InterpreterNotFound: python2.1" in result.outlines def test_unknown_interpreter(cmd, initproj): initproj( "interp123-0.5", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "tox.ini": """ [testenv:python] basepython=xyz_unknown_interpreter [testenv] changedir=tests """, }, ) result = cmd() result.assert_fail() assert any( "ERROR: InterpreterNotFound: xyz_unknown_interpreter" == line for line in result.outlines ), result.outlines def test_skip_platform_mismatch(cmd, initproj): initproj( "interp123-0.5", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "tox.ini": """ [testenv] changedir=tests platform=x123 """, }, ) result = cmd() result.assert_success() assert any( "SKIPPED: python: platform mismatch ({!r} does not match 'x123')".format(sys.platform) == line for line in result.outlines ), result.outlines def test_skip_unknown_interpreter(cmd, initproj): initproj( "interp123-0.5", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "tox.ini": """ [testenv:python] basepython=xyz_unknown_interpreter [testenv] changedir=tests """, }, ) result = cmd("--skip-missing-interpreters") result.assert_success() msg = "SKIPPED: python: InterpreterNotFound: xyz_unknown_interpreter" assert any(msg == line for line in result.outlines), result.outlines def test_skip_unknown_interpreter_result_json(cmd, initproj, tmpdir): report_path = tmpdir.join("toxresult.json") initproj( "interp123-0.5", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "tox.ini": """ [testenv:python] basepython=xyz_unknown_interpreter [testenv] changedir=tests """, }, ) result = cmd("--skip-missing-interpreters", "--result-json", report_path) result.assert_success() msg = "SKIPPED: python: InterpreterNotFound: xyz_unknown_interpreter" assert any(msg == line for line in result.outlines), result.outlines setup_result_from_json = json.load(report_path)["testenvs"]["python"]["setup"] for setup_step in setup_result_from_json: assert "InterpreterNotFound" in setup_step["output"] assert setup_step["retcode"] == 0 def test_unknown_dep(cmd, initproj): initproj( "dep123-0.7", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "tox.ini": """ [testenv] deps=qweqwe123 changedir=tests """, }, ) result = cmd() result.assert_fail() assert result.outlines[-1].startswith("ERROR: python: could not install deps [qweqwe123];") def test_venv_special_chars_issue252(cmd, initproj): initproj( "pkg123-0.7", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "tox.ini": """ [tox] envlist = special&&1 [testenv:special&&1] changedir=tests """, }, ) result = cmd() result.assert_success() pattern = re.compile(r"special&&1 installed: .*pkg123( @ .*-|==)0\.7(\.zip)?.*") assert any(pattern.match(line) for line in result.outlines), "\n".join(result.outlines) def test_unknown_environment(cmd, initproj): initproj("env123-0.7", filedefs={"tox.ini": ""}) result = cmd("-e", "qpwoei") result.assert_fail() assert result.out == "ERROR: unknown environment 'qpwoei'\n" def test_unknown_environment_with_envlist(cmd, initproj): initproj( "pkg123", filedefs={ "tox.ini": """ [tox] envlist = py{36,37}-django{20,21} """, }, ) result = cmd("-e", "py36-djagno21") result.assert_fail() assert result.out == "ERROR: unknown environment 'py36-djagno21'\n" def test_minimal_setup_py_empty(cmd, initproj): initproj( "pkg123-0.7", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "setup.py": """ """, "tox.ini": "", }, ) result = cmd() result.assert_fail() assert result.outlines[-1] == "ERROR: setup.py is empty" def test_minimal_setup_py_comment_only(cmd, initproj): initproj( "pkg123-0.7", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "setup.py": """\n# some comment """, "tox.ini": "", }, ) result = cmd() result.assert_fail() assert result.outlines[-1] == "ERROR: setup.py is empty" def test_minimal_setup_py_non_functional(cmd, initproj): initproj( "pkg123-0.7", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "setup.py": """ import sys """, "tox.ini": "", }, ) result = cmd() result.assert_fail() assert any( re.match(r".*ERROR.*check setup.py.*", line) for line in result.outlines ), result.outlines def test_sdist_fails(cmd, initproj): initproj( "pkg123-0.7", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "setup.py": """ syntax error """, "tox.ini": "", }, ) result = cmd() result.assert_fail() assert any( re.match(r".*FAIL.*could not package project.*", line) for line in result.outlines ), result.outlines def test_no_setup_py_exits(cmd, initproj): initproj( "pkg123-0.7", filedefs={ "tox.ini": """ [testenv] commands=python -c "2 + 2" """, }, ) os.remove("setup.py") result = cmd() result.assert_fail() assert any( re.match(r".*ERROR.*No pyproject.toml or setup.py file found.*", line) for line in result.outlines ), result.outlines def test_no_setup_py_exits_but_pyproject_toml_does(cmd, initproj): initproj( "pkg123-0.7", filedefs={ "tox.ini": """ [testenv] commands=python -c "2 + 2" """, }, ) os.remove("setup.py") pathlib.Path("pyproject.toml").touch() result = cmd() result.assert_fail() assert any( re.match(r".*ERROR.*pyproject.toml file found.*", line) for line in result.outlines ), result.outlines assert any( re.match(r".*To use a PEP 517 build-backend you are required to*", line) for line in result.outlines ), result.outlines def test_package_install_fails(cmd, initproj): initproj( "pkg123-0.7", filedefs={ "tests": {"test_hello.py": "def test_hello(): pass"}, "setup.py": """ from setuptools import setup setup( name='pkg123', description='pkg123 project', version='0.7', license='MIT', platforms=['unix', 'win32'], packages=['pkg123',], install_requires=['qweqwe123'], ) """, "tox.ini": "", }, ) result = cmd() result.assert_fail() assert result.outlines[-1].startswith("ERROR: python: InvocationError for command ") @pytest.fixture def example123(initproj): yield initproj( "example123-0.5", filedefs={ "tests": { "test_hello.py": """ def test_hello(pytestconfig): pass """, }, "tox.ini": """ [testenv] changedir=tests commands= pytest --basetemp={envtmpdir} \ --junitxml=junit-{envname}.xml deps=pytest """, }, ) def test_toxuone_env(cmd, example123): result = cmd() result.assert_success() assert re.match( r".*generated\W+xml\W+file.*junit-python\.xml" r".*\W+1\W+passed.*", result.out, re.DOTALL, ) result = cmd("-epython") result.assert_success() assert re.match( r".*\W+1\W+passed.*" r"summary.*" r"python:\W+commands\W+succeeded.*", result.out, re.DOTALL, ) def test_different_config_cwd(cmd, example123): # see that things work with a different CWD with example123.dirpath().as_cwd(): result = cmd("-c", "example123/tox.ini") result.assert_success() assert re.match( r".*\W+1\W+passed.*" r"summary.*" r"python:\W+commands\W+succeeded.*", result.out, re.DOTALL, ) def test_result_json(cmd, initproj, example123): cwd = initproj( "example123", filedefs={ "tox.ini": """ [testenv] deps = setuptools commands_pre = python -c 'print("START")' commands = python -c 'print("OK")' - python -c 'print("1"); raise SystemExit(1)' python -c 'print("1"); raise SystemExit(2)' python -c 'print("SHOULD NOT HAPPEN")' commands_post = python -c 'print("END")' """, }, ) json_path = cwd / "res.json" result = cmd("--result-json", json_path) result.assert_fail() data = json.loads(json_path.read_text(encoding="utf-8")) assert data["reportversion"] == "1" assert data["toxversion"] == tox.__version__ for env_data in data["testenvs"].values(): for command_type in ("setup", "test"): if command_type not in env_data: assert False, "missing {}".format(command_type) for command in env_data[command_type]: assert isinstance(command["command"], list) assert command["output"] assert "retcode" in command assert isinstance(command["retcode"], int) # virtualenv, deps install, package install, freeze assert len(env_data["setup"]) == 4 # 1 pre + 3 command + 1 post assert len(env_data["test"]) == 5 assert isinstance(env_data["installed_packages"], list) pyinfo = env_data["python"] assert isinstance(pyinfo["version_info"], list) assert pyinfo["version"] assert pyinfo["executable"] assert "write json report at: {}".format(json_path) == result.outlines[-1] def test_developz(initproj, cmd): initproj( "example123", filedefs={ "tox.ini": """ """, }, ) result = cmd("-vv", "--develop") result.assert_success() assert "sdist-make" not in result.out def test_usedevelop(initproj, cmd): initproj( "example123", filedefs={ "tox.ini": """ [testenv] usedevelop=True """, }, ) result = cmd("-vv") result.assert_success() assert "sdist-make" not in result.out def test_usedevelop_mixed(initproj, cmd): initproj( "example123", filedefs={ "tox.ini": """ [testenv:dev] usedevelop=True [testenv:nondev] usedevelop=False """, }, ) # running only 'dev' should not do sdist result = cmd("-vv", "-e", "dev") result.assert_success() assert "sdist-make" not in result.out # running all envs should do sdist result = cmd("-vv") result.assert_success() assert "sdist-make" in result.out @pytest.mark.parametrize("skipsdist", [False, True]) @pytest.mark.parametrize("src_root", [".", "src"]) def test_test_usedevelop(cmd, initproj, src_root, skipsdist): name = "example123-spameggs" base = initproj( (name, "0.5"), src_root=src_root, filedefs={ "tests": { "test_hello.py": """ def test_hello(pytestconfig): pass """, }, "tox.ini": """ [testenv] usedevelop=True changedir=tests commands= pytest --basetemp={envtmpdir} --junitxml=junit-{envname}.xml [] deps=pytest""" + """ skipsdist={} """.format( skipsdist, ), }, ) result = cmd("-v") result.assert_success() assert re.match( r".*generated\W+xml\W+file.*junit-python\.xml" r".*\W+1\W+passed.*", result.out, re.DOTALL, ) assert "sdist-make" not in result.out result = cmd("-epython") result.assert_success() assert "develop-inst-noop" in result.out assert re.match( r".*\W+1\W+passed.*" r"summary.*" r"python:\W+commands\W+succeeded.*", result.out, re.DOTALL, ) # see that things work with a different CWD with base.dirpath().as_cwd(): result = cmd("-c", "{}/tox.ini".format(name)) result.assert_success() assert "develop-inst-noop" in result.out assert re.match( r".*\W+1\W+passed.*" r"summary.*" r"python:\W+commands\W+succeeded.*", result.out, re.DOTALL, ) # see that tests can also fail and retcode is correct testfile = py.path.local("tests").join("test_hello.py") assert testfile.check() testfile.write("def test_fail(): assert 0") result = cmd() result.assert_fail() assert "develop-inst-noop" in result.out assert re.match( r".*\W+1\W+failed.*" r"summary.*" r"python:\W+commands\W+failed.*", result.out, re.DOTALL, ) # test develop is called if setup.py changes setup_py = py.path.local("setup.py") setup_py.write(setup_py.read() + " ") result = cmd() result.assert_fail() assert "develop-inst-nodeps" in result.out def test_warning_emitted(cmd, initproj): initproj( "spam-0.0.1", filedefs={ "tox.ini": """ [testenv] skipsdist=True usedevelop=True """, "setup.py": """ from setuptools import setup from warnings import warn warn("I am a warning") setup(name="spam", version="0.0.1") """, }, ) cmd() result = cmd() assert "develop-inst-noop" in result.out assert "I am a warning" in result.err def _alwayscopy_not_supported(): # This is due to virtualenv bugs with alwayscopy in some platforms # see: https://github.com/pypa/virtualenv/issues/565 supported = True tmpdir = tempfile.mkdtemp() try: with open(os.devnull) as fp: subprocess.check_call( [sys.executable, "-m", "virtualenv", "--always-copy", tmpdir], stdout=fp, stderr=fp, ) except subprocess.CalledProcessError: supported = False finally: shutil.rmtree(tmpdir) return not supported alwayscopy_not_supported = _alwayscopy_not_supported() @pytest.mark.skipif(alwayscopy_not_supported, reason="Platform doesnt support alwayscopy") def test_alwayscopy(initproj, cmd): initproj( "example123", filedefs={ "tox.ini": """ [testenv] commands={envpython} --version alwayscopy=True """, }, ) result = cmd("-vv") result.assert_success() assert "virtualenv --always-copy" in result.out def test_alwayscopy_default(initproj, cmd): initproj( "example123", filedefs={ "tox.ini": """ [testenv] commands={envpython} --version """, }, ) result = cmd("-vv") result.assert_success() assert "virtualenv --always-copy" not in result.out @pytest.mark.skipif("sys.platform == 'win32'", reason="no echo on Windows") def test_empty_activity_ignored(initproj, cmd): initproj( "example123", filedefs={ "tox.ini": """ [testenv] list_dependencies_command=echo commands={envpython} --version """, }, ) result = cmd() result.assert_success() assert "installed:" not in result.out @pytest.mark.skipif("sys.platform == 'win32'", reason="no echo on Windows") def test_empty_activity_shown_verbose(initproj, cmd): initproj( "example123", filedefs={ "tox.ini": """ [testenv] list_dependencies_command=echo commands={envpython} --version allowlist_externals = echo """, }, ) result = cmd("-v") result.assert_success() assert "installed:" in result.out def test_test_piphelp(initproj, cmd): initproj( "example123", filedefs={ "tox.ini": """ # content of: tox.ini [testenv] commands=pip -h """, }, ) result = cmd("-vv") result.assert_success() def test_notest(initproj, cmd): initproj( "example123", filedefs={ "tox.ini": """\ # content of: tox.ini [testenv:py26] basepython={} """.format( sys.executable, ), }, ) result = cmd("-v", "--notest") result.assert_success() assert re.match(r".*summary.*" r"py26\W+skipped\W+tests.*", result.out, re.DOTALL) result = cmd("-v", "--notest", "-epy26") result.assert_success() assert re.match(r".*py26\W+reusing.*", result.out, re.DOTALL) def test_notest_setup_py_error(initproj, cmd): initproj( "example123", filedefs={ "setup.py": """\ from setuptools import setup setup(name='x', install_requires=['fakefakefakefakefakefake']), """, "tox.ini": "", }, ) result = cmd("--notest") result.assert_fail() assert re.search("ERROR:.*InvocationError", result.out) @pytest.mark.parametrize("has_config", [True, False]) def test_devenv(initproj, cmd, has_config): filedefs = { "setup.py": """\ from setuptools import setup setup(name='x') """, } if has_config: filedefs[ "tox.ini" ] = """\ [tox] # envlist is ignored for --devenv envlist = foo,bar,baz [testenv] # --devenv implies --notest commands = python -c "exit(1)" """ initproj( "example123", filedefs=filedefs, ) result = cmd("--devenv", "venv") result.assert_success() # `--devenv` defaults to the `py` environment and a develop install assert "py develop-inst:" in result.out assert re.search("py create:.*venv", result.out) def test_devenv_does_not_allow_multiple_environments(initproj, cmd): initproj( "example123", filedefs={ "setup.py": """\ from setuptools import setup setup(name='x') """, "tox.ini": """\ [tox] envlist=foo,bar,baz """, }, ) result = cmd("--devenv", "venv", "-e", "foo,bar") result.assert_fail() assert result.err == "ERROR: --devenv requires only a single -e\n" def test_devenv_does_not_delete_project(initproj, cmd): initproj( "example123", filedefs={ "setup.py": """\ from setuptools import setup setup(name='x') """, "tox.ini": """\ [tox] envlist=foo,bar,baz """, }, ) result = cmd("--devenv", "") result.assert_fail() assert "would delete project" in result.out assert "ERROR: ConfigError: envdir must not equal toxinidir" in result.out def test_PYC(initproj, cmd, monkeypatch): initproj("example123", filedefs={"tox.ini": ""}) monkeypatch.setenv("PYTHONDOWNWRITEBYTECODE", "1") result = cmd("-v", "--notest") result.assert_success() assert "create" in result.out def test_env_VIRTUALENV_PYTHON(initproj, cmd, monkeypatch): initproj("example123", filedefs={"tox.ini": ""}) monkeypatch.setenv("VIRTUALENV_PYTHON", "/FOO") result = cmd("-v", "--notest") result.assert_success() assert "create" in result.out def test_setup_prints_non_ascii(initproj, cmd): initproj( "example123", filedefs={ "setup.py": """\ import sys getattr(sys.stdout, 'buffer', sys.stdout).write(b'\\xe2\\x98\\x83\\n') import setuptools setuptools.setup(name='example123') """, "tox.ini": "", }, ) result = cmd("--notest") result.assert_success() assert "create" in result.out def test_envsitepackagesdir(cmd, initproj): initproj( "pkg512-0.0.5", filedefs={ "tox.ini": """ [testenv] commands= python -c "print(r'X:{envsitepackagesdir}')" """, }, ) result = cmd() result.assert_success() assert re.match(r".*\nX:.*tox.*site-packages.*", result.out, re.DOTALL) def test_envsitepackagesdir_skip_missing_issue280(cmd, initproj): initproj( "pkg513-0.0.5", filedefs={ "tox.ini": """ [testenv] basepython=/usr/bin/qwelkjqwle commands= {envsitepackagesdir} """, }, ) result = cmd("--skip-missing-interpreters") result.assert_success() assert re.match(r".*SKIPPED:.*qwelkj.*", result.out, re.DOTALL) @pytest.mark.parametrize("verbosity", ["", "-v", "-vv"]) def test_verbosity(cmd, initproj, verbosity): initproj( "pkgX-0.0.5", filedefs={ "tox.ini": """ [testenv] """, }, ) result = cmd(verbosity) result.assert_success() needle = "Successfully installed pkgX-0.0.5" if verbosity == "-vv": assert any(needle in line for line in result.outlines), result.outlines else: assert all(needle not in line for line in result.outlines), result.outlines def test_envtmpdir(initproj, cmd): initproj( "foo", filedefs={ # This file first checks that envtmpdir is existent and empty. Then it # creates an empty file in that directory. The tox command is run # twice below, so this is to test whether the directory is cleared # before the second run. "check_empty_envtmpdir.py": """if True: import os from sys import argv envtmpdir = argv[1] assert os.path.exists(envtmpdir) assert os.listdir(envtmpdir) == [] open(os.path.join(envtmpdir, 'test'), 'w').close() """, "tox.ini": """ [testenv] commands=python check_empty_envtmpdir.py {envtmpdir} """, }, ) result = cmd() result.assert_success() result = cmd() result.assert_success() def test_missing_env_fails(initproj, cmd): ini = """ [testenv:foo] install_command={env:FOO} commands={env:VAR} """ initproj("foo", filedefs={"tox.ini": ini}) result = cmd() result.assert_fail() assert result.out.endswith( "foo: unresolvable substitution(s):\n" " commands: 'VAR'\n" " install_command: 'FOO'\n" "Environment variables are missing or defined recursively.\n", ) def test_tox_console_script(initproj): initproj("help", filedefs={"tox.ini": ""}) result = subprocess.check_call(["tox", "--help"]) assert result == 0 def test_tox_quickstart_script(initproj): initproj("help", filedefs={"tox.ini": ""}) result = subprocess.check_call(["tox-quickstart", "--help"]) assert result == 0 def test_tox_cmdline_no_args(monkeypatch, initproj): initproj("help", filedefs={"tox.ini": ""}) monkeypatch.setattr(sys, "argv", ["caller_script", "--help"]) with pytest.raises(SystemExit): tox.cmdline() def test_tox_cmdline_args(initproj): initproj("help", filedefs={"tox.ini": ""}) with pytest.raises(SystemExit): tox.cmdline(["caller_script", "--help"]) @pytest.mark.parametrize("exit_code", [0, 6]) def test_exit_code(initproj, cmd, exit_code, mocker): """Check for correct InvocationError, with exit code, except for zero exit code""" import tox.exception mocker.spy(tox.exception, "exit_code_str") tox_ini_content = "[testenv:foo]\ncommands=python -c 'import sys; sys.exit({:d})'".format( exit_code, ) initproj("foo", filedefs={"tox.ini": tox_ini_content}) cmd() if exit_code: # need mocker.spy above 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" # quotes are removed in result.out # do not include "python" as it is changed to python.EXE by appveyor expected_command_arg = " -c 'import sys; sys.exit({:d})'".format(exit_code) assert expected_command_arg in call_command assert call_exit_code == exit_code else: # need mocker.spy above assert tox.exception.exit_code_str.call_count == 0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1612297739.9115124 tox-3.21.4/tests/unit/util/0000755000175100001710000000000000000000000015777 5ustar00vstsdocker00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/util/test_graph.py0000644000175100001710000000272600000000000020520 0ustar00vstsdocker00000000000000from collections import OrderedDict import pytest from tox.util.graph import stable_topological_sort def test_topological_order_specified_only(): graph = OrderedDict() graph["A"] = "B", "C" result = stable_topological_sort(graph) assert result == ["A"] def test_topological_order(): graph = OrderedDict() graph["A"] = "B", "C" graph["B"] = () graph["C"] = () result = stable_topological_sort(graph) assert result == ["B", "C", "A"] def test_topological_order_cycle(): graph = OrderedDict() graph["A"] = "B", "C" graph["B"] = ("A",) with pytest.raises(ValueError, match="A | B"): stable_topological_sort(graph) def test_topological_complex(): graph = OrderedDict() graph["A"] = "B", "C" graph["B"] = "C", "D" graph["C"] = ("D",) graph["D"] = () result = stable_topological_sort(graph) assert result == ["D", "C", "B", "A"] def test_two_sub_graph(): graph = OrderedDict() graph["F"] = () graph["E"] = () graph["D"] = "E", "F" graph["A"] = "B", "C" graph["B"] = () graph["C"] = () result = stable_topological_sort(graph) assert result == ["F", "E", "D", "B", "C", "A"] def test_two_sub_graph_circle(): graph = OrderedDict() graph["F"] = () graph["E"] = () graph["D"] = "E", "F" graph["A"] = "B", "C" graph["B"] = ("A",) graph["C"] = () with pytest.raises(ValueError, match="A | B"): stable_topological_sort(graph) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/util/test_spinner.py0000644000175100001710000000767500000000000021105 0ustar00vstsdocker00000000000000# -*- coding: utf-8 -*- from __future__ import absolute_import, unicode_literals import datetime import os import sys import time import pytest from freezegun import freeze_time from tox.util import spinner @freeze_time("2012-01-14") def test_spinner(capfd, monkeypatch): monkeypatch.setattr(sys.stdout, "isatty", lambda: False) with spinner.Spinner(refresh_rate=100) as spin: for _ in range(len(spin.frames)): spin.stream.write("\n") spin.render_frame() spin.stream.write("\n") out, err = capfd.readouterr() lines = out.split("\n") expected = ["\r{}\r{} [0] ".format(spin.CLEAR_LINE, i) for i in spin.frames] + [ "\r{}\r{} [0] ".format(spin.CLEAR_LINE, spin.frames[0]), "\r{}".format(spin.CLEAR_LINE), ] assert lines == expected @freeze_time("2012-01-14") def test_spinner_progress(capfd, monkeypatch): monkeypatch.setattr(sys.stdout, "isatty", lambda: False) with spinner.Spinner() as spin: for _ in range(len(spin.frames)): spin.stream.write("\n") time.sleep(spin.refresh_rate) out, err = capfd.readouterr() assert not err assert len({i.strip() for i in out.split("[0]")}) > len(spin.frames) / 2 @freeze_time("2012-01-14") def test_spinner_atty(capfd, monkeypatch): monkeypatch.setattr(sys.stdout, "isatty", lambda: True) with spinner.Spinner(refresh_rate=100) as spin: spin.stream.write("\n") out, err = capfd.readouterr() lines = out.split("\n") posix = os.name == "posix" expected = [ "{}\r{}\r{} [0] ".format("\x1b[?25l" if posix else "", spin.CLEAR_LINE, spin.frames[0]), "\r\x1b[K{}".format("\x1b[?25h" if posix else ""), ] assert lines == expected @freeze_time("2012-01-14") def test_spinner_report(capfd, monkeypatch): monkeypatch.setattr(sys.stdout, "isatty", lambda: False) with spinner.Spinner(refresh_rate=100) as spin: spin.stream.write(os.linesep) spin.add("ok") spin.add("fail") spin.add("skip") spin.succeed("ok") spin.fail("fail") spin.skip("skip") out, err = capfd.readouterr() lines = out.split(os.linesep) del lines[0] expected = [ "\r{}✔ OK ok in 0.0 seconds".format(spin.CLEAR_LINE), "\r{}✖ FAIL fail in 0.0 seconds".format(spin.CLEAR_LINE), "\r{}⚠ SKIP skip in 0.0 seconds".format(spin.CLEAR_LINE), "\r{}".format(spin.CLEAR_LINE), ] assert lines == expected assert not err def test_spinner_long_text(capfd, monkeypatch): monkeypatch.setattr(sys.stdout, "isatty", lambda: False) with spinner.Spinner(refresh_rate=100) as spin: spin.stream.write("\n") spin.add("a" * 60) spin.add("b" * 60) spin.render_frame() spin.stream.write("\n") out, err = capfd.readouterr() assert not err expected = [ "\r{}\r{} [2] {} | {}...".format(spin.CLEAR_LINE, spin.frames[1], "a" * 60, "b" * 49), "\r{}".format(spin.CLEAR_LINE), ] lines = out.split("\n") del lines[0] assert lines == expected def test_spinner_stdout_not_unicode(mocker, capfd): stdout = mocker.patch("tox.util.spinner.sys.stdout") stdout.encoding = "ascii" with spinner.Spinner(refresh_rate=100) as spin: for _ in range(len(spin.frames)): spin.render_frame() out, err = capfd.readouterr() assert not err assert not out written = "".join({i[0][0] for i in stdout.write.call_args_list}) assert all(f in written for f in spin.frames) @pytest.mark.parametrize( "seconds, expected", [ (0, "0.0 seconds"), (1.0, "1.0 second"), (4.0, "4.0 seconds"), (4.130, "4.13 seconds"), (4.137, "4.137 seconds"), (42.12345, "42.123 seconds"), (61, "1 minute, 1.0 second"), ], ) def test_td_human_readable(seconds, expected): dt = datetime.timedelta(seconds=seconds) assert spinner.td_human_readable(dt) == expected ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tests/unit/util/test_util.py0000644000175100001710000000106400000000000020366 0ustar00vstsdocker00000000000000import os from tox.util import set_os_env_var def test_set_os_env_var_clean_env(monkeypatch): monkeypatch.delenv("ENV", raising=False) with set_os_env_var("ENV", "a"): assert os.environ["ENV"] == "a" assert "ENV" not in os.environ def test_set_os_env_var_exist_env(monkeypatch): monkeypatch.setenv("ENV", "b") with set_os_env_var("ENV", "a"): assert os.environ["ENV"] == "a" assert os.environ["ENV"] == "b" def test_set_os_env_var_non_str(): with set_os_env_var("ENV", 1): assert os.environ["ENV"] == "1" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1612297720.0 tox-3.21.4/tox.ini0000644000175100001710000001224300000000000014216 0ustar00vstsdocker00000000000000[tox] envlist = py27, py35, py36, py37, py38, pypy, pypy3, coverage, fix_lint, docs, package_description minversion = 3.12 isolated_build = true skip_missing_interpreters = true [testenv] description = run the tests with pytest under {basepython} setenv = PIP_DISABLE_PIP_VERSION_CHECK = 1 COVERAGE_FILE = {env:COVERAGE_FILE:{toxworkdir}/.coverage.{envname}} {py27,pypy}: PYTHONWARNINGS=ignore:DEPRECATION::pip._internal.cli.base_command passenv = CURL_CA_BUNDLE http_proxy https_proxy no_proxy REQUESTS_CA_BUNDLE SSL_CERT_FILE PYTEST_* PIP_CACHE_DIR deps = pip >= 19.3.1 extras = testing commands = pytest \ --cov "{envsitepackagesdir}/tox" \ --cov-config "{toxinidir}/tox.ini" \ --junitxml {toxworkdir}/junit.{envname}.xml \ -n={env:PYTEST_XDIST_PROC_NR:auto} \ {posargs:.} [testenv:pypy] deps = pip >= 19.3.1 psutil <= 5.6.7 [testenv:docs] description = invoke sphinx-build to build the HTML docs basepython = python3.8 extras = docs commands = sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" --color -W -bhtml {posargs} python -c 'import pathlib; print("documentation available under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "docs_out" / "index.html"))' [testenv:package_description] description = check that the long description is valid basepython = python3.8 deps = twine >= 1.12.1 # TODO installing readme-renderer[md] should not be necessary readme-renderer[md] >= 24.0 pip >= 18.0.0 skip_install = true extras = commands = pip wheel -w {envtmpdir}/build --no-deps . twine check {envtmpdir}/build/* [testenv:fix_lint] description = format the code base to adhere to our styles, and complain about what we cannot do automatically basepython = python3.8 passenv = {[testenv]passenv} # without PROGRAMDATA cloning using git for Windows will fail with an `error setting certificate verify locations` error PROGRAMDATA PRE_COMMIT_HOME extras = lint deps = pre-commit>=2 skip_install = True commands = pre-commit run --all-files --show-diff-on-failure {posargs} python -c 'import pathlib; print("hint: run \{\} install to add checks as pre-commit hook".format(pathlib.Path(r"{envdir}") / "bin" / "pre-commit"))' [testenv:coverage] description = [run locally after tests]: combine coverage data and create report; generates a diff coverage against origin/master (can be changed by setting DIFF_AGAINST env var) deps = {[testenv]deps} coverage >= 5.0.1 diff_cover skip_install = True passenv = {[testenv]passenv} DIFF_AGAINST setenv = COVERAGE_FILE={toxworkdir}/.coverage commands = coverage combine coverage report -m coverage xml -o {toxworkdir}/coverage.xml coverage html -d {toxworkdir}/htmlcov diff-cover --compare-branch {env:DIFF_AGAINST:origin/master} {toxworkdir}/coverage.xml depends = py27, py34, py35, py36, py37, py38, pypy, pypy3 parallel_show_output = True [testenv:exit_code] # to see how the InvocationError is displayed, use # PYTHONPATH=.:$PYTHONPATH python3 -m tox -e exit_code basepython = python3.8 description = commands with several exit codes skip_install = True commands = python3.8 -c "import sys; sys.exit(139)" [testenv:X] description = print the positional arguments passed in with echo commands = echo {posargs} [flake8] max-complexity = 22 max-line-length = 99 ignore = E203, W503, C901, E402, B011 [pep8] max-line-length = 99 [coverage:run] branch = true parallel = true [coverage:report] skip_covered = True show_missing = True exclude_lines = \#\s*pragma: no cover ^\s*raise AssertionError\b ^\s*raise NotImplementedError\b ^\s*return NotImplemented\b ^\s*raise$ ^if __name__ == ['"]__main__['"]:$ [coverage:paths] source = src/tox */.tox/*/lib/python*/site-packages/tox */.tox/pypy*/site-packages/tox */.tox\*\Lib\site-packages\tox */src/tox *\src\tox [pytest] addopts = -ra --showlocals --no-success-flaky-report rsyncdirs = tests tox looponfailroots = tox tests testpaths = tests xfail_strict = True markers = git network [isort] profile = black line_length = 99 known_first_party = tox,tests [testenv:release] description = do a release, required posarg of the version number basepython = python3.8 passenv = * deps = gitpython >= 2.1.10 towncrier >= 18.5.0 packaging >= 17.1 commands = python {toxinidir}/tasks/release.py --version {posargs} [testenv:notify] description = notify people about the release of the library basepython = python3.8 skip_install = true passenv = * deps = gitpython >= 2.1.10 packaging >= 17.1 google-api-python-client >= 1.7.3 oauth2client >= 4.1.2 commands = python {toxinidir}/tasks/notify.py [testenv:dev] description = dev environment with all deps at {envdir} extras = testing, docs deps = {[testenv]deps} {[testenv:release]deps} {[testenv:notify]deps} usedevelop = True commands = python -m pip list --format=columns python -c "print(r'{envpython}')"