1
0
mirror of https://github.com/fedora-python/tox-current-env.git synced 2025-12-22 15:59:26 +01:00
Files
github.com_fedora-python_to…/src/tox_current_env/hooks4.py
Lumír 'Frenzy' Balhar daf3872164 Support report of installed packages in CI (#64)
* Support report of installed packages in CI

Fixes: https://github.com/fedora-python/tox-current-env/issues/63

Co-authored-by: Miro Hrončok <miro@hroncok.cz>
2023-01-07 11:40:07 +01:00

256 lines
7.4 KiB
Python

import 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"