1
0
mirror of https://github.com/fedora-python/tox-current-env.git synced 2024-12-25 09:36:13 +01:00
github.com_fedora-python_to.../src/tox_current_env/hooks.py

249 lines
8.2 KiB
Python
Raw Normal View History

2019-07-17 15:42:23 +02:00
import os
import shutil
import subprocess
2019-07-17 15:42:23 +02:00
import sys
2019-07-17 13:54:09 +02:00
import tox
import warnings
import argparse
2019-07-17 13:54:09 +02:00
try:
import importlib.metadata as importlib_metadata
except ImportError:
import importlib_metadata
2019-07-17 13:54:09 +02:00
2019-07-17 15:42:23 +02:00
@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 -`",
2019-07-17 15:42:23 +02:00
)
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)",
)
2019-07-17 15:42:23 +02:00
@tox.hookimpl
def tox_configure(config):
2019-07-18 11:33:42 +02:00
"""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 config.option.current_env or config.option.print_deps_to or config.option.print_extras_to:
2019-07-17 15:42:23 +02:00
config.skipsdist = True
for testenv in config.envconfigs:
config.envconfigs[testenv].whitelist_externals = "*"
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."
)
2019-07-17 15:42:23 +02:00
return config
2019-07-17 18:59:50 +02:00
class InterpreterMismatch(tox.exception.InterpreterNotFound):
"""Interpreter version in current env does not match requested version"""
2019-07-17 15:42:23 +02:00
2019-07-17 18:59:50 +02:00
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
regular = not (config.option.current_env or config.option.print_deps_to or config.option.print_extras_to)
if regular 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)."
)
2019-07-17 13:54:09 +02:00
@tox.hookimpl
def tox_testenv_create(venv, action):
2019-07-17 18:59:50 +02:00
"""We create a fake virtualenv with just the symbolic link"""
2019-07-17 15:42:23 +02:00
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(
2019-07-17 18:59:50 +02:00
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:
2019-07-17 16:04:55 +02:00
# 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.
2019-07-17 15:42:23 +02:00
link = venv.envconfig.get_envpython()
target = sys.executable
rm_venv(venv)
2019-07-17 15:42:23 +02:00
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
2019-07-17 15:42:23 +02:00
return True
if not is_proper_venv(venv):
rm_venv(venv)
return None # let tox handle the rest
2019-07-17 15:42:23 +02:00
@tox.hookimpl
def tox_package(session, venv):
"""Fail early when unsupported"""
config = venv.envconfig.config
unsupported_raise(config, venv)
2019-07-17 15:42:23 +02:00
@tox.hookimpl
def tox_testenv_install_deps(venv, action):
"""We don't install anything"""
config = venv.envconfig.config
2019-07-17 18:59:50 +02:00
unsupported_raise(config, venv)
if config.option.current_env or config.option.print_deps_to:
2019-07-17 15:42:23 +02:00
return True
@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."""
2019-07-17 18:59:50 +02:00
config = venv.envconfig.config
unsupported_raise(config, venv)
ret = None
if config.option.print_deps_to:
print(
*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."""
option = venv.envconfig.config.option
if not (option.current_env or option.print_deps_only):
return None
return (
"{}=={}".format(d.metadata.get("name"), d.version)
for d in sorted(
importlib_metadata.distributions(), key=lambda d: d.metadata.get("name")
)
)