Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
home:ldragon
python-tox-current-env
tox-current-env.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File tox-current-env.obscpio of Package python-tox-current-env
07070100000000000081a4000000000000000000000001664ef79d00000028000000000000000000000000000000000000001500000000tox-current-env/.gitgitdir: ../.git/modules/tox-current-env 07070100000001000081a4000000000000000000000001664ef79d000003c7000000000000000000000000000000000000002c00000000tox-current-env/.github/workflows/main.yamlon: push: pull_request: schedule: - cron: '0 0 * * 1' # every Monday name: Run tox tests jobs: tox_test: name: tox test steps: - uses: actions/checkout@v3 - name: Run tox tests id: test uses: fedora-python/tox-github-action@master with: tox_env: ${{ matrix.tox_env }} strategy: matrix: tox_env: # This information is repeated in tox.ini # (see https://github.com/fedora-python/tox-github-action/issues/8) # Generate it by: tox -l | sed "s/^/- /" - py36-tox324 - py36-tox3 - py37-tox324 - py37-tox3 - py37-tox4 - py38-tox324 - py38-tox3 - py38-tox4 - py39-tox324 - py39-tox3 - py39-tox4 - py310-tox324 - py310-tox3 - py310-tox4 - py311-tox324 - py311-tox3 - py311-tox4 # Use GitHub's Linux Docker host runs-on: ubuntu-latest 07070100000002000041ed000000000000000000000001664ef79d00000000000000000000000000000000000000000000002200000000tox-current-env/.github/workflows07070100000003000041ed000000000000000000000001664ef79d00000000000000000000000000000000000000000000001800000000tox-current-env/.github07070100000004000081a4000000000000000000000001664ef79d00000046000000000000000000000000000000000000001b00000000tox-current-env/.gitignore__pycache__/ build/ dist/ .tox/ *.tar.gz *.zip *.egg-info *.dist-info 07070100000005000081a4000000000000000000000001664ef79d0000042c000000000000000000000000000000000000001800000000tox-current-env/LICENSECopyright 2019 tox-current-env contributors 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. 07070100000006000081a4000000000000000000000001664ef79d00000083000000000000000000000000000000000000001c00000000tox-current-env/MANIFEST.ininclude LICENSE include pyproject.toml include tox.ini recursive-include tests *.py recursive-include tests *.ini prune tests/.tox 07070100000007000081a4000000000000000000000001664ef79d0000329f000000000000000000000000000000000000001b00000000tox-current-env/README.rst=============== tox-current-env =============== --------------------------------------------------------------------------------------- `tox <https://tox.readthedocs.io/>`_ plugin to run tests in current Python environment --------------------------------------------------------------------------------------- The ``tox-current-env`` plugin adds these options: ``tox --current-env`` Runs the tox testenv's ``commands`` in the current Python environment (that is, the environment where ``tox`` is invoked from and installed in). Unlike regular ``tox`` invocation, this installs no dependencies declared in ``deps``. An attempt to run this with a Python version that doesn't match will fail (if ``tox`` is invoked from an Python 3.7 environment, any non 3.7 testenv will fail). ``tox --print-deps-to=FILE`` Instead of running any ``commands``, simply prints the `declared dependencies <https://tox.readthedocs.io/en/latest/config.html#conf-deps>`_ in ``deps`` to the specified ``FILE``. This is useful for preparing the current environment for ``tox --current-env``. Use ``-`` for ``FILE`` to print to standard output. ``tox --print-extras-to=FILE`` Instead of running any ``commands``, simply prints the names of the `declared extras <https://tox.readthedocs.io/en/latest/config.html#conf-extras>`_ in ``extras`` to the specified ``FILE``. This is useful for preparing the current environment for ``tox --current-env``. Use ``-`` for ``FILE`` to print to standard output. It is possible to use the two printing options together, as long as the ``FILE`` is different. Invoking ``tox`` without any of the above options should behave as regular ``tox`` invocation without this plugin. Any deviation from this behavior is considered a bug. The plugin disables *tox's way* of providing a testing environment, but assumes that you supply one in *some other way*. Always run ``tox`` with this plugin in a fresh isolated environment, such as Python virtualenv, Linux container or chroot. \ See other caveats below. Motivation ---------- Obviously, ``tox`` was created to run tests in isolated Python virtual environments. The ``--current-env`` flag totally defeats the purpose of ``tox``. Why would anybody do that, you might ask? Well, it turns out that ``tox`` became too popular and gained another purpose. The Python ecosystem now has formal `specifications <https://packaging.python.org/specifications/>`_ for many pieces of package metadata like versions or dependencies. However, there is no standardization yet for declaring *test dependencies* or *running tests*. The most popular de-facto standard for that today is ``tox``, and we expect a future standard to evolve from ``tox.ini``. This plugin lets us use ``tox``'s dependency lists and testing commands for environments other than Python venvs. We hope this plugin will enable community best practices around ``tox`` configuration to grow to better accomodate non-virtualenv environments in general – for example, Linux distros, Conda, or containers. Specifically, this plugin was created for `Fedora <https://fedoralovespython.org/>`_'s needs. When we package Python software as RPM packages, we try to run the project's test suite during package build. However, we need to test if the software works integrated into Fedora, not with packages downloaded from PyPI into a fresh environment. By running the tests in *current environment*, we can achieve that. If you are interested in the RPM packaging part of this, see Fedora's `%pyproject RPM macros <https://src.fedoraproject.org/rpms/pyproject-rpm-macros>`_. Installation ------------ Install this via ``pip``: .. code-block:: console $ python -m pip install tox-current-env Or install the development version by cloning `the git repository <https://github.com/fedora-python/tox-current-env>`_ and ``pip``-installing locally: .. code-block:: console $ git clone https://github.com/fedora-python/tox-current-env $ cd tox-current-env $ python -m pip install -e . Usage ----- When the plugin is installed, use ``tox`` with ``--current-env``, ``--print-deps-to`` or ``--print-extras-to`` and all the other options as usual. Assuming your ``tox`` is installed on Python 3.7: .. code-block:: console $ tox -e py37 --current-env py37 create: /home/pythonista/projects/holy-grail/tests/.tox/py37 py37 installed: ...list of packages from the current environment... py37 run-test-pre: PYTHONHASHSEED='3333333333' py37 run-test: commands... ...runs tests in current environment's Python... ___________________________________ summary ____________________________________ py37: commands succeeded congratulations :) Attempting to run the ``py36`` environment's test will fail: .. code-block:: console $ tox -e py36 --current-env py36 create: /home/pythonista/projects/holy-grail/tests/.tox/py36 ERROR: InterpreterMismatch: tox_current_env: interpreter versions do not match: in current env: (3, 7, 4, 'final', 0) requested: (3, 6, 9, 'final', 0) ___________________________________ summary ____________________________________ ERROR: py36: InterpreterMismatch: tox_current_env: interpreter versions do not match: in current env: (3, 7, 4, 'final', 0) requested: (3, 6, 9, 'final', 0) To get list of test dependencies, run: .. code-block:: console $ tox -e py37 --print-deps-to - py37 create: /home/pythonista/projects/holy-grail/tests/.tox/py37 py37 installed: ...you can see almost anything here... py37 run-test-pre: PYTHONHASHSEED='3333333333' dep1 dep2 ... ___________________________________ summary ____________________________________ py37: commands succeeded congratulations :) To get a list of names of extras, run: .. code-block:: console $ tox -e py37 --print-extras-to - py37 create: /home/pythonista/projects/holy-grail/tests/.tox/py37 py37 installed: ...you can see almost anything here... py37 run-test-pre: PYTHONHASHSEED='3333333333' extra1 extra2 ... ___________________________________ summary ____________________________________ py37: commands succeeded congratulations :) Caveats, warnings and limitations --------------------------------- tox 4 ~~~~~ The plugin is available also for tox 4. Differences in behavior between tox 3 and 4 are these: - ``--recreate`` is no longer needed when you switch from the plugin back to standard tox. Tox detects it and handles the recreation automatically. - The plugin does not check the requested Python version nor the environment name. If you let it run for multiple environments they'll all use the same Python. - Deprecated ``--print-deps-only`` option is no longer available. Use an isolated environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Running (especially third party software's) tests in your system Python environment is dangerous. Always use this plugin in an isolated environment, such as a Linux container, virtual machine or chroot. You have been warned. Do not rely on virtualenv details ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In order to support the ``python`` command in the ``commands`` section, the current environment invocation of ``tox`` creates a fake virtual environment that just has a symbolic link to the Python executable. The link is named ``python`` even if the real interpreter's name is different (such as ``python3.7`` or ``pypy``). Any other commands are not linked anywhere and it is the users' responsibility to make sure such commands are in ``$PATH`` and use the correct Python. This can lead to slightly different results of tests than invoking them directly, especially if you have assumptions about ``sys.executable`` or other commands in your tests. As a specific example, tests should invoke ``python -m pytest`` rather than assuming the ``pytest`` command is present and uses the correct version of Python. Don't mix current-env and regular tox runs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ tox caches the virtualenvs it creates, and doesn't distinguish between regular virtualenvs and ``--current-env``. Don't mix ``tox --current-env``, ``tox --print-deps-to`` or ``tox --print-extras-to`` runs and regular ``tox`` runs (without the flags provided by this plugin). If you ever need to do this, use tox's ``--recreate/-r`` flag to clear the cache. The plugin should abort with a meaningful error message if this is detected, but in some corner cases (such as running ``tox --current-env``, forcefully killing it before it finished, uninstalling the plugin, and running ``tox``), you will get undefined results (such as installing packages from PyPI into your current environment). Environment variables are passed by default ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Since 0.0.9, all Shell environment variables are passed by default when using this plugin. The `passenv` tox configuration is set to `*`. Read `the documentation for more information about passing environment variables to tox <https://tox.wiki/en/latest/config.html#passenv>`_. tox provisioning ~~~~~~~~~~~~~~~~ The tested projects can specify the `minimal tox version <https://tox.readthedocs.io/en/latest/config.html#conf-minversion>`_ and/or `additional requires <https://tox.readthedocs.io/en/latest/config.html#conf-requires>`_ needed in the environment where ``tox`` is installed. Normally, ``tox`` uses *provisioning* when such requirements are not met. It creates a virtual environment, installs (a newer version of) ``tox`` and the missing packages into that environment and proxies all ``tox`` invocations trough that. Unfortunately, this is undesired for ``tox-current-env``. 1. It is possible to invoke ``tox`` with ``--no-provision`` to prevent the provision entirely. When requirements are missing, ``tox`` fails instead of provisioning. If a path is passed as a value for ``--no-provision``, the requirements will be serialized to the file, as JSON. 2. The requires, if specified, are included in the results of ``tox --print-deps-to``. This only works when they are installed (otherwise see the first point). 3. The minimal tox version, if specified, is included in the results of ``tox --print-deps-to``. This only works when the version requirement is satisfied (otherwise see the first point). The recommend way to handle this is: 1. Run ``tox --no-provision provision.json --print-deps-to=...`` or similar. 2. If the command fails, install requirements from ``provision.json`` to the current environment and try again. Note that the specified requirements are likely to contain `other tox plugins <https://tox.readthedocs.io/en/latest/plugins.html>`_ and many of them might interfere with ``tox-current-env`` in an undesired way. If that is the case, the recommended way is to patch/sed such undesired plugins out of the configuration before running ``tox``. Other limitations and known bugs ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The ``installed:`` line in the output of ``tox --print-deps-to``/``tox --print-extras-to`` shows irrelevant output (based on the content of the real or faked virtual environment). Regardless of any `Python flags <https://docs.python.org/3/using/cmdline.html>`_ used in the shebang of ``tox``, the tests are invoked with ``sys.executable`` without any added flags (unless explicitly invoked with them in the ``commands`` section). The current environment's Python is tested for the major and minor version only. Different interpreters with the same Python version (such as CPython and PyPy) are treated as equal. Only Linux is supported, with special emphasis on Fedora. This plugin might work on other Unix-like systems, but does not work on Microsoft Windows. This is alpha quality software. Use it at your on your own risk. Pull requests with improvements are welcome. Development, issues, support ---------------------------- The development happens on GitHub, at the `fedora-python/tox-current-env <https://github.com/fedora-python/tox-current-env>`_ repository. You can use the `issue tracker <https://github.com/fedora-python/tox-current-env/issues>`_ there for any discussion or send Pull Requests. Tests ~~~~~ In order to run the tests, you'll need ``tox`` and Python from 3.6 to 3.10 installed. The integration tests assume all of them are available. On Fedora, you just need to ``dnf install tox``. Run ``tox`` to invoke the tests. Running tests of this plugin with its own ``--current-env`` flag will most likely blow up. License ------- The ``tox-current-env`` project is licensed under the so-called MIT license, full text available in the `LICENSE <https://github.com/fedora-python/tox-current-env/blob/master/LICENSE>`_ file. Code of Conduct --------------- The ``tox-current-env`` project follows the `Fedora's Code of Conduct <https://docs.fedoraproject.org/en-US/project/code-of-conduct/>`_. 07070100000008000081a4000000000000000000000001664ef79d0000005b000000000000000000000000000000000000001f00000000tox-current-env/pyproject.toml[build-system] requires = [ "setuptools", "wheel"] build-backend = "setuptools.build_meta" 07070100000009000081a4000000000000000000000001664ef79d0000062b000000000000000000000000000000000000001900000000tox-current-env/setup.pyfrom setuptools import setup, find_packages def long_description(): with open("README.rst", encoding="utf-8") as f: return f.read() setup( name="tox-current-env", description="Use current environment instead of virtualenv for tox testenvs", long_description=long_description(), author="Miro Hrončok", author_email="miro@hroncok.cz", url="https://github.com/fedora-python/tox-current-env", license="MIT", version="0.0.11", package_dir={"": "src"}, packages=find_packages("src"), entry_points={"tox": ["current-env = tox_current_env.hooks"]}, install_requires=[ "tox>=3.24", "importlib_metadata; python_version < '3.8'" ], extras_require={ "tests": [ "pytest", "pytest-xdist", "packaging", ], }, python_requires=">=3.6", classifiers=[ "Development Status :: 3 - Alpha", "Framework :: tox", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Software Development :: Testing", ], ) 0707010000000a000081a4000000000000000000000001664ef79d00000000000000000000000000000000000000000000003000000000tox-current-env/src/tox_current_env/__init__.py0707010000000b000081a4000000000000000000000001664ef79d0000009e000000000000000000000000000000000000002d00000000tox-current-env/src/tox_current_env/hooks.pyfrom tox import __version__ as TOX_VERSION if TOX_VERSION[0] == "4": from tox_current_env.hooks4 import * else: from tox_current_env.hooks3 import * 0707010000000c000081a4000000000000000000000001664ef79d0000265d000000000000000000000000000000000000002e00000000tox-current-env/src/tox_current_env/hooks3.pyimport os import shutil import subprocess import sys import tox import warnings import argparse try: import importlib.metadata as importlib_metadata except ImportError: import importlib_metadata @tox.hookimpl def tox_addoption(parser): parser.add_argument( "--current-env", action="store_true", dest="current_env", default=False, help="Run tests in current environment, not creating any virtual environment", ) parser.add_argument( "--print-deps-only", action="store_true", dest="print_deps_only", default=False, help="Deprecated, equivalent to `--print-deps-to -`. Not available with tox 4.", ) parser.add_argument( "--print-deps-to", "--print-deps-to-file", action="store", type=argparse.FileType('w'), metavar="FILE", default=None, help="Don't run tests, only print the dependencies to the given file " + "(use `-` for stdout)", ) parser.add_argument( "--print-extras-to", "--print-extras-to-file", action="store", type=argparse.FileType('w'), metavar="FILE", default=None, help="Don't run tests, only print the names of the required extras to the given file " + "(use `-` for stdout)", ) def _plugin_active(option): return option.current_env or option.print_deps_to or option.print_extras_to def _allow_all_externals(envconfig): for option in ["allowlist_externals", "whitelist_externals"]: # If either is set, we change it, as we cannot have both set at the same time: if getattr(envconfig, option, None): setattr(envconfig, option, "*") break else: # If none was set, we set the new one envconfig.allowlist_externals = "*" @tox.hookimpl def tox_configure(config): """Stores options in the config. Makes all commands external and skips sdist""" if config.option.print_deps_only: warnings.warn( "--print-deps-only is deprecated; use `--print-deps-to -`", DeprecationWarning, ) if not config.option.print_deps_to: config.option.print_deps_to = sys.stdout else: raise tox.exception.ConfigError( "--print-deps-only cannot be used together " + "with --print-deps-to" ) if _plugin_active(config.option): config.skipsdist = True for testenv in config.envconfigs: config.envconfigs[testenv].usedevelop = False _allow_all_externals(config.envconfigs[testenv]) # Because tox 4 no longer reads $TOX_TESTENV_PASSENV, # this plugin always passes all environment variables by default, # even on tox 3. # Unfortunately at this point the set contains actual values, not globs: config.envconfigs[testenv].passenv |= set(os.environ.keys()) # When printing dependencies/extras we don't run any commands. # Unfortunately tox_runtest_pre/tox_runtest_post hooks don't use firstresult=True, # so we cannot override running commands_pre/commands_post. # We empty the lists of commands instead. if config.option.print_deps_to or config.option.print_extras_to: for testenv in config.envconfigs: config.envconfigs[testenv].commands_pre = [] config.envconfigs[testenv].commands_post = [] if (getattr(config.option.print_deps_to, "name", object()) == getattr(config.option.print_extras_to, "name", object())): raise tox.exception.ConfigError( "The paths given to --print-deps-to and --print-extras-to cannot be identical." ) return config class InterpreterMismatch(tox.exception.InterpreterNotFound): """Interpreter version in current env does not match requested version""" def _python_activate_exists(venv): python = venv.envconfig.get_envpython() bindir = os.path.dirname(python) activate = os.path.join(bindir, "activate") return os.path.exists(python), os.path.exists(activate) def is_current_env_link(venv): python, activate = _python_activate_exists(venv) return python and not activate def is_proper_venv(venv): python, activate = _python_activate_exists(venv) return python and activate def is_any_env(venv): python, activate = _python_activate_exists(venv) return python def rm_venv(venv): link = venv.envconfig.get_envpython() shutil.rmtree(os.path.dirname(os.path.dirname(link)), ignore_errors=True) def unsupported_raise(config, venv): if config.option.recreate: return if not _plugin_active(config.option) and is_current_env_link(venv): if hasattr(tox.hookspecs, "tox_cleanup"): raise tox.exception.ConfigError( "Looks like previous --current-env, --print-deps-to or --print-extras-to tox run didn't finish the cleanup. " "Run tox run with --recreate (-r) or manually remove the environment in .tox." ) else: raise tox.exception.ConfigError( "Regular tox run after --current-env, --print-deps-to or --print-extras-to tox run " "is not supported without --recreate (-r)." ) elif config.option.current_env and is_proper_venv(venv): raise tox.exception.ConfigError( "--current-env after regular tox run is not supported without --recreate (-r)." ) @tox.hookimpl def tox_testenv_create(venv, action): """We create a fake virtualenv with just the symbolic link""" config = venv.envconfig.config create_fake_env = check_version = config.option.current_env if config.option.print_deps_to or config.option.print_extras_to: if is_any_env(venv): # We don't need anything return True else: # We need at least some kind of environment, # or tox fails without a python command # We fallback to --current-env behavior, # because it's cheaper, faster and won't install stuff create_fake_env = True if check_version: # With real --current-env, we check this, but not with --print-deps/extras-to only version_info = venv.envconfig.python_info.version_info if version_info is None: raise tox.exception.InterpreterNotFound(venv.envconfig.basepython) if version_info[:2] != sys.version_info[:2]: raise InterpreterMismatch( f"tox_current_env: interpreter versions do not match:\n" + f" in current env: {tuple(sys.version_info)}\n" + f" requested: {version_info}" ) if create_fake_env: # Make sure the `python` command on path is sys.executable. # (We might have e.g. /usr/bin/python3, not `python`.) # Remove the rest of the virtualenv. link = venv.envconfig.get_envpython() target = sys.executable rm_venv(venv) os.makedirs(os.path.dirname(link)) if sys.platform == "win32": # Avoid requiring admin rights on Windows subprocess.check_call(f'mklink /J "{link}" "{target}"', shell=True) else: os.symlink(target, link) # prevent tox from creating the venv return True if not is_proper_venv(venv): rm_venv(venv) return None # let tox handle the rest @tox.hookimpl def tox_package(session, venv): """Fail early when unsupported""" config = venv.envconfig.config unsupported_raise(config, venv) @tox.hookimpl def tox_testenv_install_deps(venv, action): """We don't install anything""" config = venv.envconfig.config unsupported_raise(config, venv) if _plugin_active(config.option): return True def tox_dependencies(config): """Get dependencies of tox itself, 'minversion' and 'requires' config options""" if config.minversion is not None: yield f"tox >= {config.minversion}" yield from config.requires @tox.hookimpl def tox_runtest(venv, redirect): """If --print-deps-to, prints deps instead of running tests. If --print-extras-to, prints extras instead of running tests. Both options can be used together.""" config = venv.envconfig.config unsupported_raise(config, venv) ret = None if config.option.print_deps_to: print( *tox_dependencies(config), *venv.get_resolved_dependencies(), sep="\n", file=config.option.print_deps_to, ) config.option.print_deps_to.flush() ret = True if config.option.print_extras_to: print( *venv.envconfig.extras, sep="\n", file=config.option.print_extras_to, ) config.option.print_extras_to.flush() ret = True return ret @tox.hookimpl def tox_cleanup(session): """Remove the fake virtualenv not to collide with regular tox Collisions can happen anyway (when tox is killed forcefully before this happens) Note that we don't remove real venvs, as recreating them is expensive""" for venv in session.venv_dict.values(): if is_current_env_link(venv): rm_venv(venv) @tox.hookimpl def tox_runenvreport(venv, action): """Prevent using pip to display installed packages, use importlib.metadata instead, but fallback to default without our flags.""" if not _plugin_active(venv.envconfig.config.option): return None return ( "{}=={}".format(d.metadata.get("name"), d.version) for d in sorted( importlib_metadata.distributions(), key=lambda d: d.metadata.get("name") ) ) 0707010000000d000081a4000000000000000000000001664ef79d00001d6f000000000000000000000000000000000000002e00000000tox-current-env/src/tox_current_env/hooks4.pyimport argparse import os import platform import sys import sysconfig from pathlib import Path from typing import Set from tox.config.loader.memory import MemoryLoader from tox.execute.local_sub_process import ( Execute, LocalSubProcessExecuteInstance, ) from tox.plugin import impl from tox.tox_env.python.api import PythonInfo from tox.tox_env.python.runner import PythonRun try: import importlib.metadata as importlib_metadata except ImportError: import importlib_metadata @impl def tox_register_tox_env(register): register.add_run_env(CurrentEnv) register.add_run_env(PrintEnv) @impl def tox_add_option(parser): parser.add_argument( "--current-env", action="store_true", dest="current_env", default=False, help="Run tests in current environment, not creating any virtual environment", ) parser.add_argument( "--print-deps-to", "--print-deps-to-file", action="store", type=argparse.FileType("w"), metavar="FILE", default=False, help="Don't run tests, only print the dependencies to the given file " + "(use `-` for stdout)", ) parser.add_argument( "--print-extras-to", "--print-extras-to-file", action="store", type=argparse.FileType("w"), metavar="FILE", default=False, help="Don't run tests, only print the names of the required extras to the given file " + "(use `-` for stdout)", ) @impl def tox_add_core_config(core_conf, state): opt = state.conf.options if opt.current_env or opt.print_deps_to or opt.print_extras_to: # We do not want to install the main package. # no_package is the same as skipsdist. loader = MemoryLoader(no_package=True) core_conf.loaders.insert(0, loader) if opt.current_env: opt.default_runner = "current-env" return if getattr(opt.print_deps_to, "name", object()) == getattr( opt.print_extras_to, "name", object() ): raise RuntimeError( "The paths given to --print-deps-to and --print-extras-to cannot be identical." ) if opt.print_deps_to or opt.print_extras_to: opt.default_runner = "print-env" return # No options used - switch back to the standard runner # Workaround for: https://github.com/tox-dev/tox/issues/2264 opt.default_runner = "virtualenv" @impl def tox_add_env_config(env_conf, state): opt = state.conf.options # This allows all external commands. # All of them are external for us. # Because tox 4 no longer reads $TOX_TESTENV_PASSENV, # this plugin always passes all environment variables by default. if opt.current_env: allow_external_cmds = MemoryLoader(allowlist_externals=["*"], pass_env=["*"]) env_conf.loaders.insert(0, allow_external_cmds) # For print-deps-to and print-extras-to, use empty # list of commands so the tox does nothing. if opt.print_deps_to or opt.print_extras_to: empty_commands = MemoryLoader(commands=[], commands_pre=[], commands_post=[]) env_conf.loaders.insert(0, empty_commands) class Installer: """Noop installer""" def install(self, *args, **kwargs): return None def installed(self): """Return list of installed packages like `pip freeze`.""" return [ "{}=={}".format(d.metadata.get("name"), d.version) for d in sorted( importlib_metadata.distributions(), key=lambda d: d.metadata.get("name") ) ] class CurrentEnvLocalSubProcessExecutor(Execute): def build_instance( self, request, options, out, err, ): request.env["PATH"] = ":".join( (str(options._env.env_dir / "bin"), request.env.get("PATH", "")) ) return LocalSubProcessExecuteInstance(request, options, out, err) class CurrentEnv(PythonRun): def __init__(self, create_args): self._executor = None self._installer = None self._path = [] super().__init__(create_args) @staticmethod def id(): return "current-env" @property def _default_package_tox_env_type(self): return None @property def _external_pkg_tox_env_type(self): return None @property def _package_tox_env_type(self): return None @property def executor(self): if self._executor is None: self._executor = CurrentEnvLocalSubProcessExecutor(self.options.is_colored) return self._executor def _get_python(self, base_python): return PythonInfo( implementation=sys.implementation, version_info=sys.version_info, version=sys.version, is_64=(platform.architecture()[0] == "64bit"), platform=platform.platform(), extra={"executable": Path(sys.executable)}, ) def create_python_env(self): """Fake Python environment just to make sure all possible commands like python or python3 works.""" bindir = self.env_dir / "bin" if not bindir.exists(): os.mkdir(bindir) for suffix in ( "", f"{sys.version_info.major}", f"{sys.version_info.major}.{sys.version_info.minor}", ): symlink = bindir / f"python{suffix}" if not symlink.exists(): os.symlink(sys.executable, symlink) def env_bin_dir(self): return Path(sysconfig.get_path("scripts")) def env_python(self): return sys.executable def env_site_package_dir(self): return Path(sysconfig.get_path("purelib")) @property def installer(self): return Installer() def prepend_env_var_path(self): return [self.env_bin_dir()] @property def runs_on_platform(self): return sys.platform class PrintEnv(CurrentEnv): def __init__(self, create_args): super().__init__(create_args) if self.options.print_extras_to: if "extras" not in self.conf: # Unfortunately, if there is skipsdist/no_package or skip_install # in the config, this section is not parsed at all so we have to # do it here manually to be able to read its content. self.conf.add_config( keys=["extras"], of_type=Set[str], default=set(), desc="extras to install of the target package", ) def create_python_env(self): """We don't need any environment for this plugin""" return None def prepend_env_var_path(self): """Usage of this method for the core of this plugin is far from perfect but this method is called every time even without recreated environment""" if self.options.print_deps_to: print( *self.core["requires"], *self.conf["deps"].lines(), sep="\n", file=self.options.print_deps_to, ) self.options.print_deps_to.flush() if self.options.print_extras_to: print( *self.conf["extras"], sep="\n", file=self.options.print_extras_to, ) self.options.print_extras_to.flush() @staticmethod def id(): return "print-env" 0707010000000e000041ed000000000000000000000001664ef79d00000000000000000000000000000000000000000000002400000000tox-current-env/src/tox_current_env0707010000000f000041ed000000000000000000000001664ef79d00000000000000000000000000000000000000000000001400000000tox-current-env/src07070100000010000081a4000000000000000000000001664ef79d00000435000000000000000000000000000000000000002200000000tox-current-env/tests/conftest.pyimport os import shutil import pytest from utils import FIXTURES_DIR, TOX4 @pytest.fixture(autouse=True) def projdir(tmp_path, monkeypatch, worker_id): pwd = tmp_path / "projdir" pwd.mkdir() for fname in "tox.ini", "setup.py": shutil.copy(FIXTURES_DIR / fname, pwd) monkeypatch.chdir(pwd) # https://github.com/pypa/pip/issues/5345#issuecomment-386424455 monkeypatch.setenv("XDG_CACHE_HOME", os.path.expanduser(f"~/.cache/pytest-xdist-{worker_id}")) return pwd if TOX4: available_options = ("--print-deps-to-file=-", "--print-deps-to=-") else: available_options = ( "--print-deps-only", "--print-deps-to-file=-", "--print-deps-to=-", ) @pytest.fixture(params=available_options) def print_deps_stdout_arg(request): """Argument for printing deps to stdout""" return request.param @pytest.fixture(params=("--print-extras-to-file=-", "--print-extras-to=-")) def print_extras_stdout_arg(request): """Argument for printing extras to stdout""" return request.param 07070100000011000081a4000000000000000000000001664ef79d0000007b000000000000000000000000000000000000002800000000tox-current-env/tests/fixtures/setup.pyfrom setuptools import setup setup( name="test", extras_require={ "dev": [], "full": [], } ) 07070100000012000081a4000000000000000000000001664ef79d00000193000000000000000000000000000000000000002700000000tox-current-env/tests/fixtures/tox.ini[tox] envlist = py36,py37,py38,py39,py310,py311 [testenv] passenv = XDG_CACHE_HOME deps = six py extras = dev full commands = python -c 'import os, sys; print(os.path.realpath(sys.exec_prefix), "is the exec_prefix")' # we explicitly clear this because the inner tox does not need to know # see https://github.com/fedora-python/tox-current-env/issues/52 setenv = PYTHONPATH= 07070100000013000041ed000000000000000000000001664ef79d00000000000000000000000000000000000000000000001f00000000tox-current-env/tests/fixtures07070100000014000081a4000000000000000000000001664ef79d00004cca000000000000000000000000000000000000002f00000000tox-current-env/tests/test_integration_tox3.pyimport os import re import shutil import subprocess import sys import textwrap import pytest from utils import ( DOT_TOX, NATIVE_EXEC_PREFIX_MSG, NATIVE_EXECUTABLE, NATIVE_SITE_PACKAGES, NATIVE_TOXENV, TOX_VERSION, envs_from_tox_ini, is_available, modify_config, needs_all_pythons, tox, tox_footer, ) if TOX_VERSION.major != 3: pytest.skip("skipping tests for tox 3", allow_module_level=True) def test_native_toxenv_current_env(): result = tox("-e", NATIVE_TOXENV, "--current-env") assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG assert not (DOT_TOX / NATIVE_TOXENV / "lib").is_dir() @needs_all_pythons def test_all_toxenv_current_env(): result = tox("--current-env", check=False) assert NATIVE_EXEC_PREFIX_MSG in result.stdout.splitlines() assert result.stdout.count("InterpreterMismatch:") >= 2 assert result.returncode > 0 @pytest.mark.parametrize("python", ["python3.4", "python3.5"]) def test_missing_toxenv_current_env(python): if is_available(python): pytest.skip(f"Only works if {python} is not available in $PATH") env = python.replace("python", "py").replace(".", "") result = tox("-e", env, "--current-env", check=False) assert f"InterpreterNotFound: {python}" in result.stdout assert "Traceback" not in (result.stderr + result.stdout) assert result.returncode > 0 @needs_all_pythons def test_all_toxenv_current_env_skip_missing(): result = tox("--current-env", "--skip-missing-interpreters", check=False) assert "InterpreterMismatch:" in result.stdout assert "congratulations" in result.stdout assert result.returncode == 0 @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps(toxenv, print_deps_stdout_arg): result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" six py {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) @pytest.mark.parametrize("pre_post", ["pre", "post", "both"]) def test_print_deps_with_commands_pre_post(projdir, toxenv, pre_post, print_deps_stdout_arg): with modify_config(projdir / 'tox.ini') as config: if pre_post == "both": config["testenv"]["commands_pre"] = "echo unexpected" config["testenv"]["commands_post"] = "echo unexpected" else: config["testenv"][f"commands_{pre_post}"] = "echo unexpected" result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" six py {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected assert result.stderr == "" @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_with_tox_minversion(projdir, toxenv, print_deps_stdout_arg): with modify_config(projdir / "tox.ini") as config: config["tox"]["minversion"] = "3.13" result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" tox >= 3.13 six py {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_with_tox_requires(projdir, toxenv, print_deps_stdout_arg): with modify_config(projdir / "tox.ini") as config: config["tox"]["requires"] = "\n setuptools > 30\n pluggy" result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" setuptools > 30 pluggy six py {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_with_tox_minversion_and_requires( projdir, toxenv, print_deps_stdout_arg ): with modify_config(projdir / "tox.ini") as config: config["tox"]["minversion"] = "3.13" config["tox"]["requires"] = "\n setuptools > 30\n pluggy" result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" tox >= 3.13 setuptools > 30 pluggy six py {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_extras(toxenv, print_extras_stdout_arg): result = tox("-e", toxenv, print_extras_stdout_arg) expected = textwrap.dedent( f""" dev full {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) @pytest.mark.parametrize("pre_post", ["pre", "post", "both"]) def test_print_extras_with_commands_pre_post(projdir, toxenv, pre_post, print_extras_stdout_arg): with modify_config(projdir / 'tox.ini') as config: if pre_post == "both": config["testenv"]["commands_pre"] = "echo unexpected" config["testenv"]["commands_post"] = "echo unexpected" else: config["testenv"][f"commands_{pre_post}"] = "echo unexpected" result = tox("-e", toxenv, print_extras_stdout_arg) expected = textwrap.dedent( f""" dev full {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected assert result.stderr == "" @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_only_deprecated(toxenv): result = tox( "-e", toxenv, "--print-deps-only", env={**os.environ, "PYTHONWARNINGS": "always"}, ) waring_text = ( "DeprecationWarning: --print-deps-only is deprecated; " + "use `--print-deps-to -`" ) assert waring_text in result.stderr def test_allenvs_print_deps(print_deps_stdout_arg): result = tox(print_deps_stdout_arg) expected = "" for env in envs_from_tox_ini(): expected += "six\npy\n" expected += tox_footer(spaces=0) + "\n" assert result.stdout == expected def test_allenvs_print_extras(print_extras_stdout_arg): result = tox(print_extras_stdout_arg) expected = "" for env in envs_from_tox_ini(): expected += "dev\nfull\n" expected += tox_footer(spaces=0) + "\n" assert result.stdout == expected @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_to_file(toxenv, tmp_path): depspath = tmp_path / "deps" result = tox("-e", toxenv, "--print-deps-to", str(depspath)) assert depspath.read_text().splitlines() == ["six", "py"] expected = textwrap.dedent( f""" {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_extras_to_file(toxenv, tmp_path): extraspath = tmp_path / "extras" result = tox("-e", toxenv, "--print-extras-to", str(extraspath)) assert extraspath.read_text().splitlines() == ["dev", "full"] expected = textwrap.dedent( f""" {tox_footer(toxenv)} """ ).lstrip() assert result.stdout == expected @pytest.mark.parametrize("option", ("--print-deps-to", "--print-deps-to-file")) def test_allenvs_print_deps_to_file(tmp_path, option): depspath = tmp_path / "deps" result = tox(option, str(depspath)) assert depspath.read_text().splitlines() == ["six", "py"] * len(envs_from_tox_ini()) expected = textwrap.dedent( f""" {tox_footer()} """ ).lstrip() assert result.stdout == expected @pytest.mark.parametrize("option", ("--print-extras-to", "--print-extras-to-file")) def test_allenvs_print_extras_to_file(tmp_path, option): extraspath = tmp_path / "extras" result = tox(option, str(extraspath)) assert extraspath.read_text().splitlines() == ["dev", "full"] * len(envs_from_tox_ini()) expected = textwrap.dedent( f""" {tox_footer()} """ ).lstrip() assert result.stdout == expected def test_allenvs_print_deps_to_existing_file(tmp_path): depspath = tmp_path / "deps" depspath.write_text("nada") _ = tox("--print-deps-to", str(depspath)) lines = depspath.read_text().splitlines() assert "nada" not in lines assert "six" in lines assert "py" in lines def test_allenvs_print_extras_to_existing_file(tmp_path): extraspath = tmp_path / "extras" extraspath.write_text("nada") _ = tox("--print-extras-to", str(extraspath)) lines = extraspath.read_text().splitlines() assert "nada" not in lines assert "dev" in lines assert "full" in lines @pytest.mark.parametrize("deps_stdout", [True, False]) @pytest.mark.parametrize("extras_stdout", [True, False]) def test_allenvs_print_deps_to_file_print_extras_to_other_file( tmp_path, deps_stdout, extras_stdout ): if deps_stdout and extras_stdout: pytest.xfail("Unsupported combination of parameters") depspath = "-" if deps_stdout else tmp_path / "deps" extraspath = "-" if extras_stdout else tmp_path / "extras" result = tox("--print-deps-to", str(depspath), "--print-extras-to", str(extraspath)) if deps_stdout: depslines = result.stdout.splitlines() extraslines = extraspath.read_text().splitlines() elif extras_stdout: depslines = depspath.read_text().splitlines() extraslines = result.stdout.splitlines() else: extraslines = extraspath.read_text().splitlines() depslines = depspath.read_text().splitlines() assert "six" in depslines assert "py" in depslines assert "full" in extraslines assert "dev" in extraslines assert "six" not in extraslines assert "py" not in extraslines assert "full" not in depslines assert "dev" not in depslines def test_print_deps_extras_to_same_file_is_not_possible(tmp_path): depsextraspath = tmp_path / "depsextras" result = tox( "-e", NATIVE_TOXENV, "--print-deps-to", str(depsextraspath), "--print-extras-to", str(depsextraspath), check=False, ) assert result.returncode > 0 assert "cannot be identical" in result.stderr def test_print_deps_extras_to_stdout_is_not_possible( tmp_path, print_deps_stdout_arg, print_extras_stdout_arg, ): result = tox( "-e", NATIVE_TOXENV, print_deps_stdout_arg, print_extras_stdout_arg, check=False, ) assert result.returncode > 0 assert "cannot be identical" in result.stderr def test_print_deps_only_print_deps_to_file_are_mutually_exclusive(): result = tox( "-e", NATIVE_TOXENV, "--print-deps-only", "--print-deps-to", "foobar", check=False, ) assert result.returncode > 0 assert "cannot be used together" in result.stderr @needs_all_pythons def test_regular_run(): result = tox() lines = result.stdout.splitlines()[:5] for line, env in zip(lines, envs_from_tox_ini()): assert f"/.tox/{env} is the exec_prefix" in line assert "congratulations" in result.stdout for env in envs_from_tox_ini(): major, minor = re.match(r"py(\d)(\d+)", env).groups() for pkg in "py", "six", "test": sitelib = DOT_TOX / f"{env}/lib/python{major}.{minor}/site-packages" assert sitelib.is_dir() assert len(list(sitelib.glob(f"{pkg}-*.dist-info"))) == 1 def test_regular_run_native_toxenv(): result = tox("-e", NATIVE_TOXENV) lines = sorted(result.stdout.splitlines()[:1]) assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in lines[0] assert "congratulations" in result.stdout for pkg in "py", "six", "test": sitelib = DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" assert sitelib.is_dir() assert len(list(sitelib.glob(f"{pkg}-*.dist-info"))) == 1 def test_regular_after_current_is_supported(): result = tox("-e", NATIVE_TOXENV, "--current-env") assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG result = tox("-e", NATIVE_TOXENV) assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in result.stdout assert "--recreate" not in result.stderr def test_regular_after_killed_current_is_not_supported(): # fake broken tox run shutil.rmtree(DOT_TOX, ignore_errors=True) (DOT_TOX / NATIVE_TOXENV / "bin").mkdir(parents=True) (DOT_TOX / NATIVE_TOXENV / "bin" / "python").symlink_to(NATIVE_EXECUTABLE) result = tox("-e", NATIVE_TOXENV, check=False) assert result.returncode > 0 assert "--recreate" in result.stderr def test_regular_after_first_print_deps_is_supported(print_deps_stdout_arg): result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg) assert result.stdout.splitlines()[0] == "six" result = tox("-e", NATIVE_TOXENV) lines = sorted(result.stdout.splitlines()[:1]) assert "--recreate" not in result.stderr assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in lines[0] # check that "test" was not installed to current environment shutil.rmtree("./test.egg-info") pip_freeze = subprocess.run( (sys.executable, "-m", "pip", "freeze"), encoding="utf-8", stdout=subprocess.PIPE, ).stdout.splitlines() # XXX when this fails, recreate your current environment assert "test==0.0.0" not in pip_freeze def test_regular_recreate_after_current(): result = tox("-e", NATIVE_TOXENV, "--current-env") assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG result = tox("-re", NATIVE_TOXENV) assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in result.stdout assert "not supported" not in result.stderr assert "--recreate" not in result.stderr def test_current_after_regular_is_not_supported(): result = tox("-e", NATIVE_TOXENV) assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in result.stdout result = tox("-e", NATIVE_TOXENV, "--current-env", check=False) assert result.returncode > 0 assert "not supported" in result.stderr def test_current_recreate_after_regular(): result = tox("-e", NATIVE_TOXENV) assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in result.stdout result = tox("-re", NATIVE_TOXENV, "--current-env") assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG def test_current_after_print_deps(print_deps_stdout_arg): # this is quite fast, so we can do it several times for _ in range(3): result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg) assert "bin/python" not in result.stdout assert "six" in result.stdout result = tox("-re", NATIVE_TOXENV, "--current-env") assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG def test_current_after_print_extras(print_extras_stdout_arg): # this is quite fast, so we can do it several times for _ in range(3): result = tox("-e", NATIVE_TOXENV, print_extras_stdout_arg) assert "bin/python" not in result.stdout assert "full" in result.stdout result = tox("-re", NATIVE_TOXENV, "--current-env") assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG def test_regular_recreate_after_print_deps(print_deps_stdout_arg): result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg) assert "bin/python" not in result.stdout assert "six" in result.stdout result = tox("-re", NATIVE_TOXENV) assert result.stdout.splitlines()[0] != NATIVE_EXEC_PREFIX_MSG sitelib = DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" assert sitelib.is_dir() assert len(list(sitelib.glob("test-*.dist-info"))) == 1 result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg) assert "bin/python" not in result.stdout assert "six" in result.stdout def test_print_deps_without_python_command(tmp_path, print_deps_stdout_arg): bin = tmp_path / "bin" bin.mkdir() tox_link = bin / "tox" tox_path = shutil.which("tox") tox_link.symlink_to(tox_path) env = {**os.environ, "PATH": str(bin)} result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg, env=env) expected = textwrap.dedent( f""" six py {tox_footer(NATIVE_TOXENV)} """ ).lstrip() assert result.stdout == expected @pytest.mark.parametrize("flag", [None, "--print-deps-to=-", "--current-env"]) def test_noquiet_installed_packages(flag): flags = (flag,) if flag else () result = tox("-e", NATIVE_TOXENV, *flags, quiet=False, check=False) assert f"\n{NATIVE_TOXENV} installed: " in result.stdout for line in result.stdout.splitlines(): if line.startswith(f"{NATIVE_TOXENV} installed: "): packages = line.rpartition(" installed: ")[-1].split(",") break # default tox produces output sorted by package names assert packages == sorted( packages, key=lambda p: p.partition("==")[0].partition(" @ ")[0].lower() ) # without a flag, the output must match tox defaults if not flag: assert len(packages) == 3 assert packages[0].startswith("py==") assert packages[1].startswith("six==") assert packages[2].startswith(("test==", "test @ ")) # old and new pip # with our flags, uses the absolutely current environment by default, hence has tox else: assert len([p for p in packages if p.startswith("tox==")]) == 1 assert all(re.match(r"\S+==\S+", p) for p in packages) @pytest.mark.parametrize( "flag", ["--print-deps-to=-", "--print-extras-to=-", "--current-env"] ) @pytest.mark.parametrize("usedevelop", [True, False]) def test_self_is_not_installed(projdir, flag, usedevelop): with modify_config(projdir / "tox.ini") as config: config["testenv"]["usedevelop"] = str(usedevelop) result = tox("-e", NATIVE_TOXENV, flag, quiet=False) assert "test==0.0.0" not in result.stdout assert "test @ file://" not in result.stdout @pytest.mark.parametrize("externals", [None, "allowlist_externals", "whitelist_externals"]) def test_externals(projdir, externals): with modify_config(projdir / 'tox.ini') as config: config['testenv']['commands'] = "echo assertme" if externals is not None: config['testenv'][externals] = "foo" result = tox("-e", NATIVE_TOXENV, "--current-env", quiet=False) assert result.returncode == 0 assert "assertme" in result.stdout assert "test command found but not installed in testenv" not in (result.stdout + result.stderr) @pytest.mark.parametrize("usedevelop", [True, False]) def test_self_is_installed_with_regular_tox(projdir, usedevelop): with modify_config(projdir / "tox.ini") as config: config["testenv"]["usedevelop"] = str(usedevelop) result = tox("-e", NATIVE_TOXENV, quiet=False) assert "test==0.0.0" in result.stdout or "test @ file://" in result.stdout @pytest.mark.parametrize("passenv", [None, "different list", "__var", "*"]) def test_passenv(projdir, passenv): with modify_config(projdir / "tox.ini") as config: config["testenv"]["commands"] = """python -c 'import os; print(os.getenv("__var"))'""" if passenv is not None: existing = config["testenv"].get("passenv", "") + " " config["testenv"]["passenv"] = existing + passenv env = {"__var": "assertme"} result = tox("-e", NATIVE_TOXENV, "--current-env", env=env, quiet=False) assert result.returncode == 0 assert "\nassertme\n" in result.stdout assert "\nNone\n" not in result.stdout 07070100000015000081a4000000000000000000000001664ef79d000038da000000000000000000000000000000000000002f00000000tox-current-env/tests/test_integration_tox4.pyimport os import re import shutil import textwrap import pytest from utils import ( DOT_TOX, NATIVE_EXEC_PREFIX_MSG, NATIVE_SITE_PACKAGES, NATIVE_TOXENV, TOX_VERSION, envs_from_tox_ini, modify_config, needs_all_pythons, prep_tox_output, tox, tox_footer, ) if TOX_VERSION.major != 4: pytest.skip("skipping tests for tox 4", allow_module_level=True) def test_native_toxenv_current_env(): result = tox("-e", NATIVE_TOXENV, "--current-env") assert result.stdout.splitlines()[0] == NATIVE_EXEC_PREFIX_MSG assert not (DOT_TOX / NATIVE_TOXENV / "lib").is_dir() @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps(toxenv, print_deps_stdout_arg): result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" tox six py {tox_footer(toxenv)} """ ).lstrip() assert prep_tox_output(result.stdout) == expected @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) @pytest.mark.parametrize("pre_post", ["pre", "post", "both"]) def test_print_deps_with_commands_pre_post(projdir, toxenv, pre_post, print_deps_stdout_arg): with modify_config(projdir / 'tox.ini') as config: if pre_post == "both": config["testenv"]["commands_pre"] = "echo unexpected" config["testenv"]["commands_post"] = "echo unexpected" else: config["testenv"][f"commands_{pre_post}"] = "echo unexpected" result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" tox six py {tox_footer(toxenv)} """ ).lstrip() assert sorted(prep_tox_output(result.stdout).splitlines()) == sorted( expected.splitlines() ) assert result.stderr == "" @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_with_tox_minversion(projdir, toxenv, print_deps_stdout_arg): with modify_config(projdir / "tox.ini") as config: config["tox"]["minversion"] = "3.13" result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" tox>=3.13 six py {tox_footer(toxenv)} """ ).lstrip() assert prep_tox_output(result.stdout) == expected @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_with_tox_requires(projdir, toxenv, print_deps_stdout_arg): with modify_config(projdir / "tox.ini") as config: config["tox"]["requires"] = "\n setuptools > 30\n pluggy" result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" setuptools>30 pluggy tox six py {tox_footer(toxenv)} """ ).lstrip() assert prep_tox_output(result.stdout) == expected @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_with_tox_minversion_and_requires( projdir, toxenv, print_deps_stdout_arg ): with modify_config(projdir / "tox.ini") as config: config["tox"]["minversion"] = "3.13" config["tox"]["requires"] = "\n setuptools > 30\n pluggy" result = tox("-e", toxenv, print_deps_stdout_arg) expected = textwrap.dedent( f""" setuptools>30 pluggy tox>=3.13 six py {tox_footer(toxenv)} """ ).lstrip() assert prep_tox_output(result.stdout) == expected @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_extras(toxenv, print_extras_stdout_arg): result = tox("-e", toxenv, print_extras_stdout_arg) expected = textwrap.dedent( f""" dev full {tox_footer(toxenv)} """ ).lstrip() assert sorted(prep_tox_output(result.stdout).splitlines()) == sorted( expected.splitlines() ) @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) @pytest.mark.parametrize("pre_post", ["pre", "post", "both"]) def test_print_extras_with_commands_pre_post(projdir, toxenv, pre_post, print_extras_stdout_arg): with modify_config(projdir / 'tox.ini') as config: if pre_post == "both": config["testenv"]["commands_pre"] = "echo unexpected" config["testenv"]["commands_post"] = "echo unexpected" else: config["testenv"][f"commands_{pre_post}"] = "echo unexpected" result = tox("-e", toxenv, print_extras_stdout_arg) expected = textwrap.dedent( f""" dev full {tox_footer(toxenv)} """ ).lstrip() assert sorted(prep_tox_output(result.stdout).splitlines()) == sorted( expected.splitlines() ) assert result.stderr == "" def test_allenvs_print_deps(print_deps_stdout_arg): result = tox(print_deps_stdout_arg) expected = [] for env in envs_from_tox_ini(): expected.extend(("tox", "six", "py", f"{env}: OK")) expected.pop() # The last "py310: OK" is not there expected.append(tox_footer(spaces=0)) expected = ("\n".join(expected)).splitlines() assert sorted(prep_tox_output(result.stdout).splitlines()) == sorted(expected) def test_allenvs_print_extras(print_extras_stdout_arg): result = tox(print_extras_stdout_arg) expected = [] for env in envs_from_tox_ini(): expected.extend(("dev", "full", f"{env}: OK")) expected.pop() # The last "py310: OK" is not there expected.append(tox_footer(spaces=0)) expected = ("\n".join(expected)).splitlines() assert sorted(prep_tox_output(result.stdout).splitlines()) == sorted(expected) @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_deps_to_file(toxenv, tmp_path): depspath = tmp_path / "deps" result = tox("-e", toxenv, "--print-deps-to", str(depspath)) assert sorted(depspath.read_text().splitlines()) == sorted( ["tox", "six", "py"] ) expected = tox_footer(toxenv, spaces=0) + "\n" assert prep_tox_output(result.stdout) == expected @pytest.mark.parametrize("toxenv", envs_from_tox_ini()) def test_print_extras_to_file(toxenv, tmp_path): extraspath = tmp_path / "extras" result = tox("-e", toxenv, "--print-extras-to", str(extraspath)) assert sorted(extraspath.read_text().splitlines()) == sorted(["dev", "full"]) expected = tox_footer(toxenv, spaces=0) + "\n" assert prep_tox_output(result.stdout) == expected @pytest.mark.parametrize("option", ("--print-deps-to", "--print-deps-to-file")) def test_allenvs_print_deps_to_file(tmp_path, option): depspath = tmp_path / "deps" result = tox(option, str(depspath)) assert sorted(depspath.read_text().splitlines()) == sorted( ["tox", "six", "py"] * len(envs_from_tox_ini()) ) expected = "" for env in envs_from_tox_ini()[:-1]: expected += f"{env}: OK\n" expected += tox_footer(spaces=0) + "\n" assert prep_tox_output(result.stdout) == expected @pytest.mark.parametrize("option", ("--print-extras-to", "--print-extras-to-file")) def test_allenvs_print_extras_to_file(tmp_path, option): extraspath = tmp_path / "extras" result = tox(option, str(extraspath)) assert sorted(extraspath.read_text().splitlines()) == sorted( ["dev", "full"] * len(envs_from_tox_ini()) ) expected = "" for env in envs_from_tox_ini()[:-1]: expected += f"{env}: OK\n" expected += tox_footer(spaces=0) + "\n" assert prep_tox_output(result.stdout) == expected def test_allenvs_print_deps_to_existing_file(tmp_path): depspath = tmp_path / "deps" depspath.write_text("nada") _ = tox("--print-deps-to", str(depspath)) lines = depspath.read_text().splitlines() assert "nada" not in lines assert "six" in lines assert "py" in lines def test_allenvs_print_extras_to_existing_file(tmp_path): extraspath = tmp_path / "extras" extraspath.write_text("nada") _ = tox("--print-extras-to", str(extraspath)) lines = extraspath.read_text().splitlines() assert "nada" not in lines assert "dev" in lines assert "full" in lines @pytest.mark.parametrize("deps_stdout", [True, False]) @pytest.mark.parametrize("extras_stdout", [True, False]) def test_allenvs_print_deps_to_file_print_extras_to_other_file( tmp_path, deps_stdout, extras_stdout ): if deps_stdout and extras_stdout: pytest.xfail("Unsupported combination of parameters") depspath = "-" if deps_stdout else tmp_path / "deps" extraspath = "-" if extras_stdout else tmp_path / "extras" result = tox("--print-deps-to", str(depspath), "--print-extras-to", str(extraspath)) if deps_stdout: depslines = result.stdout.splitlines() extraslines = extraspath.read_text().splitlines() elif extras_stdout: depslines = depspath.read_text().splitlines() extraslines = result.stdout.splitlines() else: extraslines = extraspath.read_text().splitlines() depslines = depspath.read_text().splitlines() assert "six" in depslines assert "py" in depslines assert "full" in extraslines assert "dev" in extraslines assert "six" not in extraslines assert "py" not in extraslines assert "full" not in depslines assert "dev" not in depslines def test_print_deps_extras_to_same_file_is_not_possible(tmp_path): depsextraspath = tmp_path / "depsextras" result = tox( "-e", NATIVE_TOXENV, "--print-deps-to", str(depsextraspath), "--print-extras-to", str(depsextraspath), check=False, ) assert result.returncode > 0 assert "cannot be identical" in result.stderr def test_print_deps_extras_to_stdout_is_not_possible( tmp_path, print_deps_stdout_arg, print_extras_stdout_arg, ): result = tox( "-e", NATIVE_TOXENV, print_deps_stdout_arg, print_extras_stdout_arg, check=False, ) assert result.returncode > 0 assert "cannot be identical" in result.stderr @needs_all_pythons def test_regular_run(): result = tox() lines = result.stdout.splitlines()[:5] for line, env in zip(lines, envs_from_tox_ini()): assert f"/.tox/{env} is the exec_prefix" in line assert "congratulations" in result.stdout for env in envs_from_tox_ini(): major, minor = re.match(r"py(\d)(\d+)", env).groups() for pkg in "py", "six", "test": sitelib = DOT_TOX / f"{env}/lib/python{major}.{minor}/site-packages" assert sitelib.is_dir() assert len(list(sitelib.glob(f"{pkg}-*.dist-info"))) == 1 def test_regular_run_native_toxenv(): result = tox("-e", NATIVE_TOXENV) lines = sorted(result.stdout.splitlines()[:1]) assert f"/.tox/{NATIVE_TOXENV} is the exec_prefix" in lines[0] assert "congratulations" in result.stdout for pkg in "py", "six", "test": sitelib = DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" assert sitelib.is_dir() assert len(list(sitelib.glob(f"{pkg}-*.dist-info"))) == 1 def test_print_deps_without_python_command(tmp_path, print_deps_stdout_arg): bin = tmp_path / "bin" bin.mkdir() tox_link = bin / "tox" tox_path = shutil.which("tox") tox_link.symlink_to(tox_path) env = {**os.environ, "PATH": str(bin)} result = tox("-e", NATIVE_TOXENV, print_deps_stdout_arg, env=env) expected = textwrap.dedent( f""" tox six py {tox_footer(NATIVE_TOXENV)} """ ).lstrip() assert prep_tox_output(result.stdout) == expected @pytest.mark.parametrize("flag", ["--print-deps-to=-", "--current-env"]) def test_recreate_environment(flag): flags = (flag,) if flag else () _ = tox("-e", NATIVE_TOXENV, check=False) result = tox("-e", NATIVE_TOXENV, *flags, quiet=False, check=False) assert f"{NATIVE_TOXENV}: recreate env because env type changed" in prep_tox_output( result.stdout ) @pytest.mark.parametrize( "flag", ["--print-deps-to=-", "--print-extras-to=-", "--current-env"] ) @pytest.mark.parametrize("usedevelop", [True, False]) def test_self_is_not_installed(projdir, flag, usedevelop): with modify_config(projdir / "tox.ini") as config: config["testenv"]["usedevelop"] = str(usedevelop) _ = tox("-e", NATIVE_TOXENV, flag, quiet=False) egg_link = DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" / "test.egg-link" dist_info = ( DOT_TOX / f"{NATIVE_TOXENV}/{NATIVE_SITE_PACKAGES}" / "test-0.0.0.dist-info" ) assert not egg_link.exists() assert not dist_info.exists() @pytest.mark.parametrize("usedevelop", [True, False]) def test_self_is_installed_with_regular_tox(projdir, usedevelop): with modify_config(projdir / "tox.ini") as config: config["testenv"]["usedevelop"] = str(usedevelop) result = tox("-e", NATIVE_TOXENV, "-v", quiet=False) assert "test-0.0.0" in result.stdout if usedevelop: assert "test-0.0.0-0.editable" in result.stdout @pytest.mark.parametrize("passenv", [None, "different list", "__var", "*"]) def test_passenv(projdir, passenv): with modify_config(projdir / "tox.ini") as config: config["testenv"]["commands"] = """python -c 'import os; print(os.getenv("__var"))'""" if passenv is not None: existing = config["testenv"].get("passenv", "") + " " config["testenv"]["passenv"] = existing + passenv env = {"__var": "assertme"} result = tox("-e", NATIVE_TOXENV, "--current-env", env=env, quiet=False) assert result.returncode == 0 assert "\nassertme\n" in result.stdout assert "\nNone\n" not in result.stdout @pytest.mark.parametrize("pass_env", [None, "different\nlist", "__var", "*"]) def test_pass_env(projdir, pass_env): with modify_config(projdir / "tox.ini") as config: config["testenv"]["commands"] = """python -c 'import os; print(os.getenv("__var"))'""" if pass_env is not None: config["testenv"]["pass_env"] = pass_env env = {"__var": "assertme"} result = tox("-e", NATIVE_TOXENV, "--current-env", env=env, quiet=False) assert result.returncode == 0 assert "\nassertme\n" in result.stdout assert "\nNone\n" not in result.stdout def test_report_installed(projdir): # tox4 only reports installed when a CI is detected env = {"CI": "true"} result = tox("-e", NATIVE_TOXENV, "--current-env", env=env, quiet=False) assert result.returncode == 0 assert "tox==" in result.stdout assert "pytest==" in result.stdout 07070100000016000081a4000000000000000000000001664ef79d00000ed3000000000000000000000000000000000000001f00000000tox-current-env/tests/utils.pyimport configparser import contextlib import functools import os import pathlib import re import subprocess import sys from configparser import ConfigParser import pytest from packaging.version import parse as ver PYTHON_VERSION_DOT = f"{sys.version_info[0]}.{sys.version_info[1]}" PYTHON_VERSION_NODOT = f"{sys.version_info[0]}{sys.version_info[1]}" NATIVE_TOXENV = f"py{PYTHON_VERSION_NODOT}" NATIVE_SITE_PACKAGES = f"lib/python{PYTHON_VERSION_DOT}/site-packages" NATIVE_EXECUTABLE = str(pathlib.Path(sys.executable).resolve()) FIXTURES_DIR = pathlib.Path(__file__).parent / "fixtures" DOT_TOX = pathlib.Path("./.tox") def _exec_prefix(executable): """Returns sys.exec_prefix for the given executable""" cmd = (executable, "-c", "import sys; print(sys.exec_prefix)") return subprocess.check_output(cmd, encoding="utf-8").strip() NATIVE_EXEC_PREFIX = _exec_prefix(NATIVE_EXECUTABLE) NATIVE_EXEC_PREFIX_MSG = f"{NATIVE_EXEC_PREFIX} is the exec_prefix" def tox(*args, quiet=True, **kwargs): kwargs.setdefault("encoding", "utf-8") kwargs.setdefault("stdout", subprocess.PIPE) kwargs.setdefault("stderr", subprocess.PIPE) kwargs.setdefault("check", True) kwargs.setdefault("cwd", os.getcwd()) q = ("-q",) if quiet else () env = dict(os.environ) env.pop("TOX_WORK_DIR", None) kwargs["env"] = {**env, **kwargs.get("env", {})} try: print("current", os.getcwd(), "running in", kwargs["cwd"]) cp = subprocess.run((sys.executable, "-m", "tox") + q + args, **kwargs) except subprocess.CalledProcessError as e: print(e.stdout, file=sys.stdout) print(e.stderr, file=sys.stderr) raise print(cp.stdout, file=sys.stdout) print(cp.stderr, file=sys.stderr) return cp TOX_VERSION = ver(tox("--version").stdout.split(" ")[0].split("+")[0]) TOX4 = TOX_VERSION.major == 4 @contextlib.contextmanager def modify_config(tox_ini_path): """Context manager that allows modifying the given Tox config file A statement like:: with prepare_config(projdir) as config: will make `config` a ConfigParser instance that is saved at the end of the `with` block. """ config = configparser.ConfigParser() config.read(tox_ini_path) yield config with open(tox_ini_path, "w") as tox_ini_file: config.write(tox_ini_file) @functools.lru_cache(maxsize=8) def is_available(python): try: subprocess.run((python, "--version")) except FileNotFoundError: return False return True @functools.lru_cache() def envs_from_tox_ini(): cp = ConfigParser() cp.read(FIXTURES_DIR / "tox.ini") return cp["tox"]["envlist"].split(",") def tox_footer(envs=None, spaces=8): if envs is None: envs = envs_from_tox_ini() elif isinstance(envs, str): envs = [envs] default_indent = " " * spaces if TOX4: result = "" else: result = "___________________________________ summary ____________________________________\n" for i, env in enumerate(envs): if TOX4: # Skip indentation for the first line indent = default_indent if i > 0 else "" result += f"{indent} {env}: OK\n" else: result += f"{default_indent} {env}: commands succeeded\n" result += f"{default_indent} congratulations :)" return result def prep_tox_output(output): """Remove time info from tox output""" result = re.sub(r" \((\d+\.\d+|\d+) seconds\)", "", output) result = re.sub(r" ✔ in (\d+\.\d+|\d+) seconds", "", result) return result needs_all_pythons = pytest.mark.skipif( not all((is_available(f"python3.{x}") for x in range(6, 12))), reason="This test needs all pythons from 3.6 to 3.11 available in $PATH", ) 07070100000017000041ed000000000000000000000001664ef79d00000000000000000000000000000000000000000000001600000000tox-current-env/tests07070100000018000081a4000000000000000000000001664ef79d0000017f000000000000000000000000000000000000001800000000tox-current-env/tox.ini[tox] # This information is repeated in .github/workflows/main.yaml # (see https://github.com/fedora-python/tox-github-action/issues/8) envlist = py36-tox{324,3},{py37,py38,py39,py310,py311}-tox{324,3,4} [testenv] extras = tests deps= tox324: tox >=3.24,<3.25 tox3: tox < 4 tox4: tox >=4.0.15,< 5 commands = pytest -v {posargs} tests [pytest] addopts = -nauto 07070100000019000041ed000000000000000000000001664ef79d00000000000000000000000000000000000000000000001000000000tox-current-env07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000b00000000TRAILER!!!
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor