Source code for conda_workspaces.resolver
"""Feature-to-environment resolver.
Takes a ``WorkspaceConfig`` and resolves which conda/PyPI packages
need to be installed for a given environment by composing its
constituent features.
"""
from __future__ import annotations
import logging
from dataclasses import dataclass, field
from typing import TYPE_CHECKING
from .exceptions import (
PlatformError,
)
if TYPE_CHECKING:
from .models import Channel, MatchSpec, PyPIDependency, WorkspaceConfig
log = logging.getLogger(__name__)
[docs]
@dataclass
class ResolvedEnvironment:
"""The fully resolved dependency set for a single environment.
This is what the environment manager uses to install or update
a project-local conda environment.
"""
name: str
conda_dependencies: dict[str, MatchSpec] = field(default_factory=dict)
pypi_dependencies: dict[str, PyPIDependency] = field(default_factory=dict)
channels: list[Channel] = field(default_factory=list)
platforms: list[str] = field(default_factory=list)
activation_scripts: list[str] = field(default_factory=list)
activation_env: dict[str, str] = field(default_factory=dict)
system_requirements: dict[str, str] = field(default_factory=dict)
channel_priority: str | None = None
[docs]
def resolve_environment(
config: WorkspaceConfig,
env_name: str,
platform: str | None = None,
) -> ResolvedEnvironment:
"""Resolve an environment by composing its features.
Merges conda deps, PyPI deps, channels, activation scripts/env,
and system requirements across all features in the environment.
If *platform* is given, target-specific overrides are included
and platform support is validated.
"""
env = config.get_environment(env_name)
# Validate platform if provided
if platform and config.platforms and platform not in config.platforms:
raise PlatformError(platform, config.platforms)
resolved = ResolvedEnvironment(
name=env_name,
channel_priority=config.channel_priority,
)
# Merge dependencies
resolved.conda_dependencies = config.merged_conda_dependencies(env, platform)
resolved.pypi_dependencies = config.merged_pypi_dependencies(env, platform)
resolved.channels = config.merged_channels(env)
# Merge platforms: intersect feature platforms with workspace platforms
feature_platforms: set[str] = set()
features = config.resolve_features(env)
for feat in features:
if feat.platforms:
if not feature_platforms:
feature_platforms = set(feat.platforms)
else:
feature_platforms &= set(feat.platforms)
if feature_platforms:
resolved.platforms = sorted(feature_platforms)
else:
if any(f.platforms for f in features):
log.warning(
"Feature platform intersection for environment '%s' is empty; "
"falling back to workspace platforms",
env_name,
)
resolved.platforms = list(config.platforms)
# Merge activation and system requirements
for feat in features:
resolved.activation_scripts.extend(feat.activation_scripts)
resolved.activation_env.update(feat.activation_env)
resolved.system_requirements.update(feat.system_requirements)
return resolved
[docs]
def resolve_all_environments(
config: WorkspaceConfig,
platform: str | None = None,
) -> dict[str, ResolvedEnvironment]:
"""Resolve all environments in the workspace.
Returns a dict mapping environment name to its resolved deps.
"""
return {
name: resolve_environment(config, name, platform)
for name in config.environments
}