Update module content to conform to code quality requirements

Add missing exception
Add missing docstrings
Lint and blacken
Fix broken handling of poetrys "unsafe" packages
This commit is contained in:
Ethan Paul 2020-09-23 20:34:22 -04:00
parent deea0b9c7a
commit c9c750d5e4
No known key found for this signature in database
GPG Key ID: C5F5542B54A4D9C6

View File

@ -1,17 +1,27 @@
from pathlib import Path """Tox plugin for installing environments using Poetry
This plugin makes use of the ``tox_testenv_install_deps`` Tox plugin hook to replace the default
installation functionality to install dependencies from the Poetry lockfile for the project. It
does this by using ``poetry`` to read in the lockfile, identify necessary dependencies, and then
use Poetry's ``PipInstaller`` class to install those packages into the Tox environment.
"""
import logging import logging
from typing import Dict, List from pathlib import Path
from typing import Dict
from typing import List
from typing import Optional
from typing import Tuple
from poetry.factory import Factory from poetry.factory import Factory as PoetryFactory
from poetry.factory import Poetry from poetry.factory import Poetry
from poetry.packages import Package from poetry.installation.pip_installer import PipInstaller as PoetryPipInstaller
from poetry.installation.pip_installer import PipInstaller from poetry.io.null_io import NullIO as PoetryNullIO
from poetry.io.null_io import NullIO from poetry.packages import Package as PoetryPackage
from poetry.utils.env import VirtualEnv from poetry.puzzle.provider import Provider as PoetryProvider
from poetry.utils.env import VirtualEnv as PoetryVirtualEnv
from tox import hookimpl
from tox.action import Action as ToxAction from tox.action import Action as ToxAction
from tox.venv import VirtualEnv as ToxVirtualEnv from tox.venv import VirtualEnv as ToxVirtualEnv
from tox import hookimpl
__title__ = "tox-poetry-installer" __title__ = "tox-poetry-installer"
@ -22,53 +32,105 @@ __license__ = "MIT"
__authors__ = ["Ethan Paul <e@enp.one>"] __authors__ = ["Ethan Paul <e@enp.one>"]
PEP440_VERSION_DELIMITERS: Tuple[str, ...] = ("~=", "==", "!=", ">", "<")
class ToxPoetryInstallerException(Exception):
"""Error while installing locked dependencies to the test environment"""
class NoLockedDependencyError(ToxPoetryInstallerException):
"""Cannot install a package that is not in the lockfile"""
def _make_poetry(venv: ToxVirtualEnv) -> Poetry: def _make_poetry(venv: ToxVirtualEnv) -> Poetry:
return Factory().create_poetry(venv.envconfig.config.toxinidir) """Helper to make a poetry object from a toxenv"""
return PoetryFactory().create_poetry(venv.envconfig.config.toxinidir)
def _find_locked_dependencies(poetry: Poetry, dependency_name: str) -> List[Package]: def _find_locked_dependencies(
packages: Dict[str, Package] = { poetry: Poetry, dependency_name: str
) -> List[PoetryPackage]:
"""Using a poetry object identify all dependencies of a specific dependency
:param poetry: Populated poetry object which can be used to build a populated locked
repository object.
:param dependency_name: Bare name (without version) of the dependency to fetch the transient
dependencies of.
:returns: List of packages that need to be installed for the requested dependency.
.. note:: The package corresponding to the dependency named by ``dependency_name`` is included
in the list of returned packages.
"""
packages: Dict[str, PoetryPackage] = {
package.name: package package.name: package
for package in poetry.locker.locked_repository(True).packages for package in poetry.locker.locked_repository(True).packages
} }
try: try:
top_level = packages[dependency_name] top_level = packages[dependency_name]
def find_transients(name: str) -> List[PoetryPackage]:
if name in PoetryProvider.UNSAFE_PACKAGES:
return []
transients = [packages[name]]
for dep in packages[name].requires:
transients += find_transients(dep.name)
return transients
return find_transients(top_level.name)
except KeyError: except KeyError:
raise if any(delimiter in dependency_name for delimiter in PEP440_VERSION_DELIMITERS):
message = "specifying a version in the tox environment definition is incompatible with installing from a lockfile"
else:
message = (
"no version of the package was found in the current project's lockfile"
)
def find_transients(name: str) -> List[Package]: raise NoLockedDependencyError(
transients = [packages[name]] f"Cannot install requirement '{dependency_name}': {message}"
for dep in packages[name].requires: ) from None
transients += find_transients(dep.name)
return transients
return find_transients(top_level.name)
@hookimpl @hookimpl
def tox_testenv_install_deps(venv: ToxVirtualEnv, action: ToxAction): def tox_testenv_install_deps(
venv: ToxVirtualEnv, action: ToxAction
) -> Optional[List[PoetryPackage]]:
"""Install the dependencies for the current environment
Loads the local Poetry environment and the corresponding lockfile then pulls the dependencies
specified by the Tox environment. Finally these dependencies are installed into the Tox
environment using the Poetry ``PipInstaller`` backend.
:param venv: Tox virtual environment object with configuration for the local Tox environment.
:param action: Tox action object
"""
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
if action.name == venv.envconfig.config.isolated_build_env: if action.name == venv.envconfig.config.isolated_build_env:
logger.debug(f"Environment {action.name} is isolated build environment; skipping Poetry-based dependency installation") logger.debug(
f"Environment {action.name} is isolated build environment; skipping Poetry-based dependency installation"
)
return None return None
poetry = _make_poetry(venv) poetry = _make_poetry(venv)
logger.debug(f"Loaded project pyproject.toml from {poetry.file}") logger.debug(f"Loaded project pyproject.toml from {poetry.file}")
dependencies = [] dependencies: List[PoetryPackage] = []
for env_dependency in venv.envconfig.deps: for env_dependency in venv.envconfig.deps:
dependencies += _find_locked_dependencies(poetry, env_dependency.name) dependencies += _find_locked_dependencies(poetry, env_dependency.name)
logger.debug(f"Identified {len(dependencies)} dependencies for environment {action.name}") logger.debug(
f"Identified {len(dependencies)} dependencies for environment {action.name}"
)
installer = PipInstaller( installer = PoetryPipInstaller(
env=VirtualEnv(path=Path(venv.envconfig.envdir)), env=PoetryVirtualEnv(path=Path(venv.envconfig.envdir)),
io=NullIO(), io=PoetryNullIO(),
pool=poetry.pool pool=poetry.pool,
) )
for dependency in dependencies: for dependency in dependencies: