Source code for projspec.proj.conda_workspace

import os

import toml

from projspec.proj import ParseFailed, ProjectSpec
from projspec.proj.pixi import envs_from_lock, extract_tasks
from projspec.utils import AttrDict, PickleableTomlDecoder


[docs] class CondaWorkspace(ProjectSpec): """A workspace managed by conda-workspaces (``conda.toml``). conda-workspaces brings multi-environment workspace management and task execution to the conda CLI as a plugin (``conda workspace`` / ``conda task``). The manifest is a conda-native sibling of pixi.toml; the lockfile (``conda.lock``) is rattler-lock v6 with a ``version: 1`` byte identifying it as conda-workspaces-owned. """ icon = "🧰" spec_doc = ( "https://conda-incubator.github.io/conda-workspaces/reference/conda-toml-spec/" ) def _load_meta(self) -> dict: """Merge metadata from ``pyproject.toml`` (``[tool.conda]``) and ``conda.toml``. ``conda.toml`` wins on overlap, mirroring conda-workspaces' own precedence rule. """ meta = self.proj.pyproject.get("tool", {}).get("conda", {}) if "conda.toml" in self.proj.basenames: try: with self.proj.get_file("conda.toml", text=True) as f: meta = { **meta, **toml.loads(f.read(), decoder=PickleableTomlDecoder()), } except (OSError, ValueError, UnicodeDecodeError, FileNotFoundError): pass return meta def match(self) -> bool: # A bare conda.toml without [workspace] is permitted by the spec # as a tasks-only manifest; it does not constitute a workspace # on its own, so do not match it here. return ( bool(self.proj.pyproject.get("tool", {}).get("conda", {})) or "conda.toml" in self.proj.basenames ) def parse(self) -> None: from projspec.artifact.python_env import CondaEnv, LockFile from projspec.content.environment import Environment, Precision, Stack meta = self._load_meta() if not meta.get("workspace"): raise ParseFailed arts = AttrDict() conts = AttrDict() procs = AttrDict() commands = AttrDict() run_cmd = ("conda", "task", "run") env_flag = "-e" extract_tasks( meta, procs, commands, self.proj, run_cmd=run_cmd, env_flag=env_flag ) if "environments" in meta and "feature" in meta: for env_name, details in meta["environments"].items(): feat: dict = {} feats = set( details if isinstance(details, list) else details["features"] ) for fname in feats: feat.update(meta["feature"].get(fname, {})) if isinstance(details, list) or not details.get("no-default-feature"): feat.update(meta) extract_tasks( feat, procs, commands, self.proj, env=env_name, run_cmd=run_cmd, env_flag=env_flag, ) if procs: arts["process"] = procs if commands: conts["commands"] = commands # `envs-dir` is in the conda.toml spec but conda-workspaces' own # parser does not yet read it from TOML (uses the model default). # Reading it here keeps projspec aligned with the published spec. envs_dir = meta.get("workspace", {}).get("envs-dir", ".conda/envs") if "conda.lock" in self.proj.basenames: conts["environments"] = AttrDict() arts["conda_env"] = AttrDict() with self.proj.fs.open(self.proj.basenames["conda.lock"], "rb") as f: lock_envs = envs_from_lock(f) for env_name, details in lock_envs.items(): arts["conda_env"][env_name] = CondaEnv( proj=self.proj, fn=f"{self.proj.url}/{envs_dir}/{env_name}", cmd=["conda", "workspace", "install", "-e", env_name], ) conts["environments"][env_name] = Environment( proj=self.proj, packages=details["packages"], stack=Stack.CONDA, precision=Precision.LOCK, channels=details["channels"], ) arts["lock_file"] = LockFile( proj=self.proj, fn=f"{self.proj.url}/conda.lock", cmd=["conda", "workspace", "lock"], ) self._artifacts = arts self._contents = conts @staticmethod def _create(path: str) -> None: name = os.path.basename(path) with open(f"{path}/conda.toml", "wt") as f: f.write( f"""[workspace] name = "{name}" channels = ["conda-forge"] platforms = ["osx-arm64", "linux-64", "win-64"] version = "0.1.0" [tasks] hello = "echo 'hello world'" [dependencies] python = ">=3.10" """ )