Skip to content

Allow testing under both Python 3.6 and 3.15+ #3656

@hroncok

Description

@hroncok

What's the problem this feature will solve?

As far as I can see, it is not possible to test under both Python 3.6 and 3.15 at the same time.

Following #3005 and https://tox.wiki/en/latest/faq.html#testing-end-of-life-python-versions, when I use the provided config snippet:

[tox]
# https://tox.wiki/en/stable/faq.html#testing-end-of-life-python-versions
requires = virtualenv<20.22.0
env_list = py36,py315

[testenv]
# this is not needed to reproduce, but it makes the reproducer only one file:
skip_install = true
# any dependency will do, to trigger pip installation:
deps = pytest
commands =
    python --version

The Python 3.15 environment blows up (as the old virtualenv has pip that does not support it):

Details
$ tox -e py315
ROOT: will run in automatically provisioned tox, host /usr/bin/python3 is missing [requires (has)]: virtualenv<20.22.0 (20.35.4)
ROOT: provision> .tox/.tox/bin/python -m tox -e py315
py315: install_deps> python -I -m pip install pytest
Collecting pytest
  Downloading pytest-9.0.2-py3-none-any.whl (374 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 374.8/374.8 kB 4.2 MB/s eta 0:00:00

py315: exit 2 (1.23 seconds) ...> python -I -m pip install pytest pid=548437
.../.tox/py315/lib/python3.15/site-packages/pip/_vendor/pyparsing/core.py:5332: SyntaxWarning: 'return' in a 'finally' block
  return self.__class__.__name__ + ": " + retString
ERROR: Exception:
Traceback (most recent call last):
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/cli/base_command.py", line 169, in exc_logging_wrapper
    status = run_func(*args)
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/cli/req_command.py", line 248, in wrapper
    return func(self, options, args)
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/commands/install.py", line 377, in run
    requirement_set = resolver.resolve(
        reqs, check_supported_wheels=not options.target_dir
    )
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/resolution/resolvelib/resolver.py", line 92, in resolve
  py315: FAIL code 2 (1.26 seconds)
  evaluation failed :( (1.30 seconds)
    result = self._result = resolver.resolve(
                            ~~~~~~~~~~~~~~~~^
        collected.requirements, max_rounds=limit_how_complex_resolution_can_be
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_vendor/resolvelib/resolvers.py", line 546, in resolve
    state = resolution.resolve(requirements, max_rounds=max_rounds)
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_vendor/resolvelib/resolvers.py", line 397, in resolve
    self._add_to_criteria(self.state.criteria, r, parent=None)
    ~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_vendor/resolvelib/resolvers.py", line 173, in _add_to_criteria
    if not criterion.candidates:
           ^^^^^^^^^^^^^^^^^^^^
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_vendor/resolvelib/structs.py", line 156, in __bool__
    return bool(self._sequence)
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py", line 155, in __bool__
    return any(self)
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py", line 143, in <genexpr>
    return (c for c in iterator if id(c) not in self._incompatible_ids)
                       ^^^^^^^^
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py", line 47, in _iter_built
    candidate = func()
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/resolution/resolvelib/factory.py", line 206, in _make_candidate_from_link
    self._link_candidate_cache[link] = LinkCandidate(
                                       ~~~~~~~~~~~~~^
        link,
        ^^^^^
    ...<3 lines>...
        version=version,
        ^^^^^^^^^^^^^^^^
    )
    ^
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 293, in __init__
    super().__init__(
    ~~~~~~~~~~~~~~~~^
        link=link,
        ^^^^^^^^^^
    ...<4 lines>...
        version=version,
        ^^^^^^^^^^^^^^^^
    )
    ^
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 156, in __init__
    self.dist = self._prepare()
                ~~~~~~~~~~~~~^^
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 225, in _prepare
    dist = self._prepare_distribution()
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/resolution/resolvelib/candidates.py", line 304, in _prepare_distribution
    return preparer.prepare_linked_requirement(self._ireq, parallel_builds=True)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/operations/prepare.py", line 516, in prepare_linked_requirement
    return self._prepare_linked_requirement(req, parallel_builds)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/operations/prepare.py", line 631, in _prepare_linked_requirement
    dist = _get_prepared_distribution(
        req,
    ...<3 lines>...
        self.check_build_deps,
    )
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/operations/prepare.py", line 72, in _get_prepared_distribution
    return abstract_dist.get_metadata_distribution()
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/distributions/wheel.py", line 26, in get_metadata_distribution
    return get_wheel_distribution(wheel, canonicalize_name(self.req.name))
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/metadata/__init__.py", line 105, in get_wheel_distribution
    return select_backend().Distribution.from_wheel(wheel, canonical_name)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/metadata/importlib/_dists.py", line 134, in from_wheel
    dist = WheelDistribution.from_zipfile(zf, name, wheel.location)
  File ".../.tox/py315/lib/python3.15/site-packages/pip/_internal/metadata/importlib/_dists.py", line 74, in from_zipfile
    return cls(files, info_location)
TypeError: Can't instantiate abstract class WheelDistribution without an implementation for abstract method 'locate_file'

Describe the solution you'd like

I'd like to be able to use tox to test both old and new Pythons.

Some solutions are already described in #3005

Ideally I would like to limit the virtualenv<20.22.0 requirement for old test environments. Or fallback to venv for them.

Alternative Solutions

There is https://github.com/tox-dev/tox-venv but it is deprecated and archived. Also, adding it to tox.requires also fails:

Details
$ tox -l
ROOT: will run in automatically provisioned tox, host /usr/bin/python3 is missing [requires (has)]: tox-venv
ROOT: venv> /usr/bin/uv venv -p /usr/bin/python3 --allow-existing --python-preference system .../.tox/.tox
ROOT: install_deps> /usr/bin/uv pip install tox tox-venv
ROOT: provision> .tox/.tox/bin/python -m tox -l
Traceback (most recent call last):
  File "<frozen runpy>", line 198, in _run_module_as_main
  File "<frozen runpy>", line 88, in _run_code
  File ".../.tox/.tox/lib64/python3.14/site-packages/tox/__main__.py", line 6, in <module>
    run()
    ~~~^^
  File ".../.tox/.tox/lib64/python3.14/site-packages/tox/run.py", line 23, in run
    result = main(sys.argv[1:] if args is None else args)
  File ".../.tox/.tox/lib64/python3.14/site-packages/tox/run.py", line 42, in main
    state = setup_state(args)
  File ".../.tox/.tox/lib64/python3.14/site-packages/tox/run.py", line 56, in setup_state
    options = get_options(*args)
  File ".../.tox/.tox/lib64/python3.14/site-packages/tox/config/cli/parse.py", line 40, in get_options
    guess_verbosity, log_handler, source = _get_base(args)
                                           ~~~~~~~~~^^^^^^
  File ".../.tox/.tox/lib64/python3.14/site-packages/tox/config/cli/parse.py", line 65, in _get_base
    MANAGER.load_plugins(source.path)
    ~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
  File ".../.tox/.tox/lib64/python3.14/site-packages/tox/plugin/manager.py", line 118, in load_plugins
    self._register_plugins(inline)
    ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^
  File ".../.tox/.tox/lib64/python3.14/site-packages/tox/plugin/manager.py", line 54, in _register_plugins
    self._load_external_plugins()
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
  File ".../.tox/.tox/lib64/python3.14/site-packages/tox/plugin/manager.py", line 82, in _load_external_plugins
    self.manager.load_setuptools_entrypoints(NAME)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File ".../.tox/.tox/lib64/python3.14/site-packages/pluggy/_manager.py", line 416, in load_setuptools_entrypoints
    plugin = ep.load()
  File "/usr/lib64/python3.14/importlib/metadata/__init__.py", line 179, in load
    module = import_module(match.group('module'))
  File "/usr/lib64/python3.14/importlib/__init__.py", line 88, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1398, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1371, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1342, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 938, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 759, in exec_module
  File "<frozen importlib._bootstrap>", line 491, in _call_with_frames_removed
  File ".../.tox/.tox/lib64/python3.14/site-packages/tox_venv/hooks.py", line 5, in <module>
    from tox.venv import cleanup_for_venv
ModuleNotFoundError: No module named 'tox.venv'

Additional context

See #3005 and pypa/virtualenv#2548 (comment)

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions