"""Setuptools Pth and Pip Post Install Module."""
from __future__ import annotations
__all__ = (
"BuildPy",
"Develop",
"EasyInstall",
"InstallLib",
)
import filecmp
import itertools
import logging
import sys
import warnings
import zipfile
from contextvars import ContextVar
from typing import TYPE_CHECKING
try:
# nodeps[pth] extras
import setuptools # type: ignore[attr-defined]
from setuptools.command.build_py import build_py # type: ignore[attr-defined]
from setuptools.command.develop import develop # type: ignore[attr-defined]
from setuptools.command.easy_install import easy_install # type: ignore[attr-defined]
from setuptools.command.install_lib import install_lib # type: ignore[attr-defined]
except ModuleNotFoundError:
setuptools = object
build_py = object
develop = object
easy_install = object
install_lib = object
try:
if "_in_process.py" not in sys.argv[0]:
# Avoids failing when asking for build requirements and distutils.core is not available since pip patch it
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=UserWarning, message="Setuptools is replacing distutils.")
# Must be imported after setuptools
# noinspection PyCompatibility
import pip._internal.cli.base_command
import pip._internal.metadata
import pip._internal.models.direct_url
import pip._internal.models.scheme
import pip._internal.operations.install.wheel
import pip._internal.req.req_install
import pip._internal.req.req_uninstall
except ModuleNotFoundError:
pip = object
try:
# nodeps[pth] extras
import pipx.commands.common # type: ignore[attr-defined]
from pipx.commands.common import run_post_install_actions as pipx_run_post_install # type: ignore[attr-defined]
except ModuleNotFoundError:
pass
from ..modules import ColorLogger, Path, Project
if TYPE_CHECKING:
import pathlib
# noinspection PyCompatibility
from pip._internal.cli.base_command import Command
try: # noqa: SIM105
from pipx.venv import Venv # type: ignore[attr-defined]
except ModuleNotFoundError:
pass
_NODEPS_PIP_POST_INSTALL = {}
"""Holds the context with wheels installed and paths to package installed to be used in post install"""
[docs]
class BuildPy(build_py):
"""Build py with pth files installed."""
[docs]
def run(self):
"""Run build py."""
super().run()
self.outputs = []
self.outputs = _copy_pths(self, self.build_lib)
[docs]
def get_outputs(self, include_bytecode=1):
"""Get outputs."""
return itertools.chain(build_py.get_outputs(self, 0), self.outputs)
[docs]
class Develop(develop):
"""PTH Develop Install."""
[docs]
def run(self):
"""Run develop."""
super().run()
_copy_pths(self, self.install_dir)
[docs]
class EasyInstall(easy_install):
"""PTH Easy Install."""
[docs]
def run(self, *args, **kwargs):
"""Run easy install."""
super().run(*args, **kwargs)
_copy_pths(self, self.install_dir)
[docs]
class InstallLib(install_lib):
"""PTH Install Library."""
[docs]
def run(self):
"""Run Install Library."""
super().run()
self.outputs = []
self.outputs = _copy_pths(self, self.install_dir)
[docs]
def get_outputs(self):
"""Get outputs."""
return itertools.chain(install_lib.get_outputs(self), self.outputs)
def _copy_pths(self: BuildPy | Develop | EasyInstall | InstallLib, directory: str) -> list[str]:
log = ColorLogger.logger()
outputs = []
data = self.get_outputs() if isinstance(self, (BuildPy | InstallLib)) else self.outputs
for source in data:
if source.endswith(".pth"):
destination = Path(directory, Path(source).name)
if not destination.is_file() or not filecmp.cmp(source, destination):
destination = str(destination)
msg = f"{self.__class__.__name__}: {str(Path(sys.executable).resolve())[-4:]}"
log.info(
msg,
extra={"extra": f"{source} -> {destination}"},
)
self.copy_file(source, destination)
outputs.append(destination)
return outputs
def _run_post_install_actions(
venv: Venv,
package_name: str,
local_bin_dir: pathlib.Path,
venv_dir: pathlib.Path,
include_dependencies: bool,
*,
force: bool) -> None:
pipx_run_post_install(venv=venv, package_name=package_name, local_bin_dir=local_bin_dir,
venv_dir=venv_dir, include_dependencies=include_dependencies, force=force)
print(venv, package_name, local_bin_dir, venv_dir)
# Project(app_paths).post()
def _pip_base_command(self: Command, args: list[str]) -> int:
"""Post install pip patch."""
try:
with self.main_context():
rv = self._main(args)
if rv == 0 and self.__class__.__name__ == "InstallCommand":
for path in _NODEPS_PIP_POST_INSTALL.values():
Project(path).post()
return rv
finally:
logging.shutdown()
def _pip_install_wheel(
name: str,
wheel_path: str,
scheme: pip._internal.models.scheme.Scheme,
req_description: str,
pycompile: bool = True,
warn_script_location: bool = True,
direct_url: pip._internal.models.direct_url.DirectUrl | None = None,
requested: bool = False,
):
"""Pip install wheel patch to post install."""
with zipfile.ZipFile(wheel_path) as z, pip._internal.operations.install.wheel.req_error_context(req_description):
pip._internal.operations.install.wheel._install_wheel(
name=name,
wheel_zip=z,
wheel_path=wheel_path,
scheme=scheme,
pycompile=pycompile,
warn_script_location=warn_script_location,
direct_url=direct_url,
requested=requested,
)
global _NODEPS_PIP_POST_INSTALL # noqa: PLW0602
_NODEPS_PIP_POST_INSTALL[name] = Path(scheme.purelib, name)
def _pip_uninstall_req(self, auto_confirm: bool = False, verbose: bool = False):
"""Pip uninstall patch to post install."""
assert self.req # noqa: S101
Project(self.req.name).post(uninstall=True)
dist = pip._internal.metadata.get_default_environment().get_distribution(self.req.name)
if not dist:
pip._internal.req.req_install.logger.warning("Skipping %s as it is not installed.", self.name)
return None
pip._internal.req.req_install.logger.info("Found existing installation: %s", dist)
uninstalled_pathset = pip._internal.req.req_uninstall.UninstallPathSet.from_dist(dist)
uninstalled_pathset.remove(auto_confirm, verbose)
return uninstalled_pathset
def _setuptools_build_quiet(self, importable) -> None:
"""Setuptools build py patch to quiet build."""
if ContextVar('NODEPS_QUIET') is True:
return
if importable not in self._already_warned:
self._Warning.emit(importable=importable)
self._already_warned.add(importable)
if "pip._internal.operations.install.wheel" in sys.modules:
pip._internal.operations.install.wheel.install_wheel = _pip_install_wheel
pip._internal.cli.base_command.Command.main = _pip_base_command
pip._internal.req.req_install.InstallRequirement.uninstall = _pip_uninstall_req
if "pipx.commands.common" in sys.modules:
# noinspection PyUnboundLocalVariable,PyUnresolvedReferences
pipx.commands.common.run_post_install_actions = _run_post_install_actions
if "setuptools.command.build_py" in sys.modules:
# noinspection PyUnresolvedReferences
setuptools.command.build_py._IncludePackageDataAbuse.warn = _setuptools_build_quiet