Skip to content

Commit

Permalink
Standardize import structure
Browse files Browse the repository at this point in the history
Standardize on "import module" format rather than "from module import foo" format
Remove _poetry stub module since we directly depend on the poetry package now
Fix conflicts between modules and parameters both named 'poetry'
Fixes #92
  • Loading branch information
enpaul committed Aug 20, 2024
1 parent 4b38b00 commit 48b71bc
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 133 deletions.
44 changes: 0 additions & 44 deletions tox_poetry_installer/_poetry.py

This file was deleted.

155 changes: 80 additions & 75 deletions tox_poetry_installer/hooks/_tox_on_install_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,48 @@
import collections
import concurrent.futures
import contextlib
import typing
from datetime import datetime
from pathlib import Path
import datetime
import pathlib
from typing import Collection
from typing import Dict
from typing import List
from typing import Sequence
from typing import Set

from packaging.utils import NormalizedName
from poetry.core.packages.dependency import Dependency as PoetryDependency
from poetry.core.packages.package import Package as PoetryPackage
from tox.tox_env.api import ToxEnv as ToxVirtualEnv
from tox.tox_env.package import PackageToxEnv
import cleo.io.null_io
import packaging.utils
import poetry.config.config
import poetry.core.packages.dependency
import poetry.core.packages.package
import poetry.factory
import poetry.installation.executor
import poetry.installation.operations.install
import poetry.poetry
import poetry.utils.env
import tox.tox_env.api
import tox.tox_env.package

from tox_poetry_installer import constants
from tox_poetry_installer import exceptions
from tox_poetry_installer import logger


if typing.TYPE_CHECKING:
from tox_poetry_installer import _poetry
PackageMap = Dict[str, List[poetry.core.packages.package.Package]]

# This is globally disabled to support the usage of the _poetry shadow module
# pylint: disable=import-outside-toplevel


PackageMap = Dict[str, List[PoetryPackage]]


def check_preconditions(venv: ToxVirtualEnv) -> "_poetry.Poetry":
def check_preconditions(venv: tox.tox_env.api.ToxEnv) -> poetry.poetry.Poetry:
"""Check that the local project environment meets expectations"""

# Skip running the plugin for the provisioning environment. The provisioned environment,
# for alternative Tox versions and/or the ``requires`` meta dependencies is specially
# handled by Tox and is out of scope for this plugin. Since one of the ways to install this
# plugin in the first place is via the Tox provisioning environment, it quickly becomes a
# chicken-and-egg problem.
if isinstance(venv, PackageToxEnv):
if isinstance(venv, tox.tox_env.package.PackageToxEnv):
raise exceptions.SkipEnvironment(f"Skipping Tox provisioning env '{venv.name}'")

from tox_poetry_installer import _poetry

try:
return _poetry.Factory().create_poetry(venv.core["tox_root"])
return poetry.factory.Factory().create_poetry(venv.core["tox_root"])
# Support running the plugin when the current tox project does not use Poetry for its
# environment/dependency management.
#
Expand All @@ -62,9 +59,9 @@ def check_preconditions(venv: ToxVirtualEnv) -> "_poetry.Poetry":
def identify_transients(
dep_name: str,
packages: PackageMap,
venv: "_poetry.VirtualEnv",
venv: poetry.utils.env.VirtualEnv,
allow_missing: Sequence[str] = (),
) -> List[PoetryPackage]:
) -> List[poetry.core.packages.package.Package]:
"""Using a pool of packages, identify all transient dependencies of a given package name
:param dep_name: Either the Poetry dependency or the dependency's bare package name to recursively
Expand All @@ -81,10 +78,12 @@ def identify_transients(
"""
searched: Set[str] = set()

def _transients(transient: PoetryDependency) -> List[PoetryPackage]:
def _transients(
transient: poetry.core.packages.dependency.Dependency,
) -> List[poetry.core.packages.package.Package]:
searched.add(transient.name)

results: List[PoetryPackage] = []
results: List[poetry.core.packages.package.Package] = []
for option in packages[transient.name]:
if venv.is_valid_for_marker(option.to_dependency().marker):
for requirement in option.requires:
Expand Down Expand Up @@ -133,65 +132,68 @@ def _transients(transient: PoetryDependency) -> List[PoetryPackage]:

def find_project_deps(
packages: PackageMap,
venv: "_poetry.VirtualEnv",
poetry: "_poetry.Poetry",
venv: poetry.utils.env.VirtualEnv,
project: poetry.poetry.Poetry,
extras: Sequence[str] = (),
) -> List[PoetryPackage]:
) -> List[poetry.core.packages.package.Package]:
"""Find the root project dependencies
Recursively identify the dependencies of the root project package
:param packages: Mapping of all locked package names to their corresponding package object
:param venv: Poetry virtual environment to use for package compatibility checks
:param poetry: Poetry object for the current project
:param project: Poetry object for the current project
:param extras: Sequence of extra names to include the dependencies of
"""

required_dep_names = [
item.name for item in poetry.package.requires if not item.is_optional()
item.name for item in project.package.requires if not item.is_optional()
]

extra_dep_names: List[str] = []
for extra in extras:
logger.info(f"Processing project extra '{extra}'")
try:
extra_dep_names += [
item.name for item in poetry.package.extras[NormalizedName(extra)]
item.name
for item in project.package.extras[
packaging.utils.NormalizedName(extra)
]
]
except KeyError:
raise exceptions.ExtraNotFoundError(
f"Environment specifies project extra '{extra}' which was not found in the lockfile"
) from None

dependencies: List[PoetryPackage] = []
dependencies: List[poetry.core.packages.package.Package] = []
for dep_name in required_dep_names + extra_dep_names:
dependencies += identify_transients(
dep_name.lower(), packages, venv, allow_missing=[poetry.package.name]
dep_name.lower(), packages, venv, allow_missing=[project.package.name]
)

return dedupe_packages(dependencies)


def find_additional_deps(
packages: PackageMap,
venv: "_poetry.VirtualEnv",
poetry: "_poetry.Poetry",
venv: poetry.utils.env.VirtualEnv,
project: poetry.poetry.Poetry,
dep_names: Sequence[str],
) -> List[PoetryPackage]:
) -> List[poetry.core.packages.package.Package]:
"""Find additional dependencies
Recursively identify the dependencies of an arbitrary list of package names
:param packages: Mapping of all locked package names to their corresponding package object
:param venv: Poetry virtual environment to use for package compatibility checks
:param poetry: Poetry object for the current project
:param project: Poetry object for the current project
:param dep_names: Sequence of additional dependency names to recursively find the transient
dependencies for
"""
dependencies: List[PoetryPackage] = []
dependencies: List[poetry.core.packages.package.Package] = []
for dep_name in dep_names:
dependencies += identify_transients(
dep_name.lower(), packages, venv, allow_missing=[poetry.package.name]
dep_name.lower(), packages, venv, allow_missing=[project.package.name]
)

return dedupe_packages(dependencies)
Expand All @@ -200,27 +202,27 @@ def find_additional_deps(
def find_group_deps(
group: str,
packages: PackageMap,
venv: "_poetry.VirtualEnv",
poetry: "_poetry.Poetry",
) -> List[PoetryPackage]:
venv: poetry.utils.env.VirtualEnv,
project: poetry.poetry.Poetry,
) -> List[poetry.core.packages.package.Package]:
"""Find the dependencies belonging to a dependency group
Recursively identify the Poetry dev dependencies
:param group: Name of the dependency group from the project's ``pyproject.toml``
:param packages: Mapping of all locked package names to their corresponding package object
:param venv: Poetry virtual environment to use for package compatibility checks
:param poetry: Poetry object for the current project
:param project: Poetry object for the current project
"""
return find_additional_deps(
packages,
venv,
poetry,
project,
# the type ignore here is due to the difficulties around getting nested data
# from the inherrently unstructured toml structure (which necessarily is flexibly
# typed) but in a situation where there is a meta-structure applied to it (i.e. a
# pyproject.toml structure).
poetry.pyproject.data["tool"]["poetry"] # type: ignore
project.pyproject.data["tool"]["poetry"] # type: ignore
.get("group", {})
.get(group, {})
.get("dependencies", {})
Expand All @@ -229,28 +231,30 @@ def find_group_deps(


def find_dev_deps(
packages: PackageMap, venv: "_poetry.VirtualEnv", poetry: "_poetry.Poetry"
) -> List[PoetryPackage]:
packages: PackageMap,
venv: poetry.utils.env.VirtualEnv,
project: poetry.poetry.Poetry,
) -> List[poetry.core.packages.package.Package]:
"""Find the dev dependencies
Recursively identify the Poetry dev dependencies
:param packages: Mapping of all locked package names to their corresponding package object
:param venv: Poetry virtual environment to use for package compatibility checks
:param poetry: Poetry object for the current project
:param project: Poetry object for the current project
"""
dev_group_deps = find_group_deps("dev", packages, venv, poetry)
dev_group_deps = find_group_deps("dev", packages, venv, project)

# Legacy pyproject.toml poetry format:
legacy_dev_group_deps = find_additional_deps(
packages,
venv,
poetry,
project,
# the type ignore here is due to the difficulties around getting nested data
# from the inherrently unstructured toml structure (which necessarily is flexibly
# typed) but in a situation where there is a meta-structure applied to it (i.e. a
# pyproject.toml structure).
poetry.pyproject.data["tool"]["poetry"].get("dev-dependencies", {}).keys(), # type: ignore
project.pyproject.data["tool"]["poetry"].get("dev-dependencies", {}).keys(), # type: ignore
)

# Poetry 1.2 unions these two toml sections.
Expand All @@ -273,37 +277,38 @@ def _optional_parallelize(parallels: int):


def install_package(
poetry: "_poetry.Poetry",
venv: ToxVirtualEnv,
packages: Collection["_poetry.PoetryPackage"],
project: poetry.poetry.Poetry,
venv: tox.tox_env.api.ToxEnv,
packages: Collection[poetry.core.packages.package.Package],
parallels: int = 0,
):
"""Install a bunch of packages to a virtualenv
:param poetry: Poetry object the packages were sourced from
:param project: Poetry object the packages were sourced from
:param venv: Tox virtual environment to install the packages to
:param packages: List of packages to install to the virtual environment
:param parallels: Number of parallel processes to use for installing dependency packages, or
``None`` to disable parallelization.
"""
from tox_poetry_installer import _poetry

logger.info(f"Installing {len(packages)} packages to environment at {venv.env_dir}")

install_executor = _poetry.Executor(
install_executor = poetry.installation.executor.Executor(
env=convert_virtualenv(venv),
io=_poetry.NullIO(),
pool=poetry.pool,
config=_poetry.Config(),
io=cleo.io.null_io.NullIO(),
pool=project.pool,
config=poetry.config.config.Config(),
)

installed: Set[_poetry.PoetryPackage] = set()
installed: Set[poetry.core.packages.package.Package] = set()

def logged_install(dependency: _poetry.PoetryPackage) -> None:
start = datetime.now()
def logged_install(dependency: poetry.core.packages.package.Package) -> None:
start = datetime.datetime.now()
logger.debug(f"Installing {dependency}")
install_executor.execute([_poetry.Install(package=dependency)])
end = datetime.now()
install_executor.execute(
[poetry.installation.operations.install.Install(package=dependency)]
)
end = datetime.datetime.now()
logger.debug(f"Finished installing {dependency} in {end - start}")

with _optional_parallelize(parallels) as executor:
Expand All @@ -326,36 +331,36 @@ def logged_install(dependency: _poetry.PoetryPackage) -> None:
future.result()


def dedupe_packages(packages: Sequence[PoetryPackage]) -> List[PoetryPackage]:
"""Deduplicates a sequence of PoetryPackages while preserving ordering
def dedupe_packages(
packages: Sequence[poetry.core.packages.package.Package],
) -> List[poetry.core.packages.package.Package]:
"""Deduplicates a sequence of Packages while preserving ordering
Adapted from StackOverflow: https://stackoverflow.com/a/480227
"""
seen: Set[PoetryPackage] = set()
seen: Set[poetry.core.packages.package.Package] = set()
# Make this faster, avoid method lookup below
seen_add = seen.add
return [p for p in packages if not (p in seen or seen_add(p))]


def convert_virtualenv(venv: ToxVirtualEnv) -> "_poetry.VirtualEnv":
def convert_virtualenv(venv: tox.tox_env.api.ToxEnv) -> poetry.utils.env.VirtualEnv:
"""Convert a Tox venv to a Poetry venv
:param venv: Tox ``VirtualEnv`` object representing a tox virtual environment
:returns: Poetry ``VirtualEnv`` object representing a poetry virtual environment
"""
from tox_poetry_installer import _poetry

return _poetry.VirtualEnv(path=Path(venv.env_dir))
return poetry.utils.env.VirtualEnv(path=pathlib.Path(venv.env_dir))


def build_package_map(poetry: "_poetry.Poetry") -> PackageMap:
def build_package_map(project: poetry.poetry.Poetry) -> PackageMap:
"""Build the mapping of package names to objects
:param poetry: Populated poetry object to load locked packages from
:param project: Populated poetry object to load locked packages from
:returns: Mapping of package names to Poetry package objects
"""
packages = collections.defaultdict(list)
for package in poetry.locker.locked_repository().packages:
for package in project.locker.locked_repository().packages:
packages[str(package.name)].append(package)

return packages
Loading

0 comments on commit 48b71bc

Please sign in to comment.