Source code for conda_workspaces.context

"""Workspace context — lazy properties for conda & workspace state.

Provides a namespace of lazily-evaluated properties that downstream
code can use without importing conda at module level.  This keeps
import-time overhead negligible.
"""

from __future__ import annotations

import os
from pathlib import Path
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from conda.models.environment import Environment

    from .models import WorkspaceConfig


[docs] class WorkspaceContext: """Lazy-evaluated context for the current workspace. Properties are resolved on first access and cached. Conda imports are deferred to keep plugin load time under 1 ms. """ def __init__(self, config: WorkspaceConfig | None = None) -> None: self._config = config self._cache: dict[str, object] = {} @property def config(self) -> WorkspaceConfig: """The parsed workspace configuration.""" if self._config is None: from .manifests import detect_and_parse _, self._config = detect_and_parse() return self._config @property def root(self) -> Path: """Workspace root directory.""" return Path(self.config.root) @property def envs_dir(self) -> Path: """Directory where project-local environments are stored.""" return self.root / self.config.envs_dir @property def platform(self) -> str: """Current conda subdir (e.g. ``osx-arm64``).""" if "platform" not in self._cache: from conda.base.context import context self._cache["platform"] = context.subdir return self._cache["platform"] # type: ignore[return-value] @property def root_prefix(self) -> Path: """Conda root prefix (base environment).""" if "root_prefix" not in self._cache: from conda.base.context import context self._cache["root_prefix"] = Path(context.root_prefix) return self._cache["root_prefix"] # type: ignore[return-value]
[docs] def env_prefix(self, env_name: str) -> Path: """Return the prefix path for a named environment.""" return self.envs_dir / env_name
[docs] def env_exists(self, env_name: str) -> bool: """Check whether the prefix is a valid conda environment.""" from conda.core.envs_manager import PrefixData prefix = self.env_prefix(env_name) return PrefixData(str(prefix)).is_environment()
[docs] def envs_from_manifest( self, env_name: str, *, requested_platforms: tuple[str, ...] = (), ) -> list[Environment]: """Build ``Environment`` objects from the workspace manifest. Produces an :class:`~conda.models.environment.Environment` per target platform with ``requested_packages`` populated from the manifest's declared specs (no solver, no installed packages required) — the novel capability of ``conda workspace export`` vs. ``conda export``, which always operates on an installed prefix. When the manifest declares platforms conda doesn't know, falls back to :attr:`platform` so the export still produces something useful rather than crashing on validation. This is the natural entry point for third-party exporter plugins (or other tooling) that want to turn a ``conda.toml`` into a list of :class:`Environment` objects without going through the CLI. """ from .export import envs_from_manifest return envs_from_manifest( self, env_name, requested_platforms=requested_platforms )
[docs] def envs_from_prefix( self, env_name: str, *, requested_platforms: tuple[str, ...] = (), from_history: bool = False, no_builds: bool = False, ignore_channels: bool = False, ) -> list[Environment]: """Build ``Environment`` objects from an installed workspace prefix. Thin wrapper around the same :meth:`Environment.from_prefix` + :meth:`Environment.extrapolate` pair that :func:`conda.cli.main_export.execute` uses; the only workspace-specific pieces are the prefix lookup (:meth:`env_prefix`) and the :class:`EnvironmentNotInstalledError` guard. When *requested_platforms* is empty or equals ``(self.platform,)``, a single :class:`Environment` for the host platform is returned. Otherwise one :class:`Environment` per requested platform is produced via :meth:`Environment.extrapolate`. """ from .export import envs_from_prefix return envs_from_prefix( self, env_name, requested_platforms=requested_platforms, from_history=from_history, no_builds=no_builds, ignore_channels=ignore_channels, )
[docs] def envs_from_lockfile( self, env_name: str, *, requested_platforms: tuple[str, ...] = (), ) -> list[Environment]: """Load ``Environment`` objects from the workspace ``conda.lock``. Delegates to :class:`~conda_workspaces.lockfile.CondaLockLoader`, the same entry point conda uses when it reads ``--file conda.lock`` through :meth:`Environment.from_cli_with_file_envs`. When *requested_platforms* is empty, every platform present in the lockfile is returned. Otherwise the list is filtered and :class:`PlatformError` is raised for any requested platform the lockfile does not contain. """ from .export import envs_from_lockfile return envs_from_lockfile( self, env_name, requested_platforms=requested_platforms )
[docs] class CondaContext: """Lazy-evaluated namespace exposed as ``conda.*`` in task templates. Attribute access is deferred so conda internals load only when a template references a variable. """ def __init__(self, manifest_path: Path | None = None) -> None: self._manifest_path = manifest_path @property def platform(self) -> str: """The conda platform/subdir string, e.g. ``linux-64`` or ``osx-arm64``.""" from conda.base.context import context return context.subdir @property def environment_name(self) -> str: """Name of the currently active conda environment, or ``"base"``.""" from conda.base.context import context if context.active_prefix: return Path(context.active_prefix).name return "base" @property def environment(self) -> _EnvironmentProxy: """Allows ``{{ conda.environment.name }}`` in templates.""" return _EnvironmentProxy(self.environment_name) @property def prefix(self) -> str: """Absolute path to the target conda environment prefix.""" from conda.base.context import context return str(context.target_prefix) @property def version(self) -> str: """The installed conda version string.""" from conda import __version__ return __version__ @property def manifest_path(self) -> str: """Path to the task definition file, or empty string if unknown.""" return str(self._manifest_path) if self._manifest_path else "" @property def init_cwd(self) -> str: """The working directory at the time of context creation.""" return os.getcwd() @property def is_win(self) -> bool: """True when running on Windows.""" from conda.base.constants import on_win return on_win @property def is_unix(self) -> bool: """True when running on a Unix-like system (Linux or macOS).""" from conda.base.constants import on_win return not on_win @property def is_osx(self) -> bool: """True when the host platform is macOS.""" from conda.base.context import context return context.platform == "osx" @property def is_linux(self) -> bool: """True when the host platform is Linux.""" from conda.base.context import context return context.platform == "linux"
class _EnvironmentProxy: """Allows ``{{ conda.environment.name }}`` in templates.""" def __init__(self, name: str) -> None: self.name = name
[docs] def build_template_context( manifest_path: Path | None = None, task_args: dict[str, str] | None = None, ) -> dict[str, object]: """Build the full Jinja2 template context dict. The returned dict contains: - ``conda``: a :class:`CondaContext` instance - ``pixi``: alias to the same context (for pixi.toml compatibility) - Any user-supplied task argument values """ ctx = CondaContext(manifest_path=manifest_path) result: dict[str, object] = {"conda": ctx, "pixi": ctx} if task_args: result.update(task_args) return result