Source code for projspec.proj.conda_project

import os

import yaml

from projspec.proj import ProjectSpec
from projspec.utils import AttrDict, _yaml_no_jinja

# lockfile format is as produced by conda-lock; may be a standalone thing to find?
# https://github.com/conda/conda-lock


[docs] class CondaProject(ProjectSpec): """Tool for encapsulating, running, and reproducing projects with conda environments.""" icon = "📦" # not a spec, but a howto: spec_doc = "https://conda-incubator.github.io/conda-project/tutorial.html" @staticmethod def _create(path: str) -> None: name = os.path.basename(path) env_file = "environment.yml" with open(f"{path}/{env_file}", "wt") as f: f.write("channels:\n - conda-forge\ndependencies:\n - python >=3.10\n") with open(f"{path}/conda-project.yml", "wt") as f: f.write( f"name: {name}\n" "environments:\n" f" default:\n" f" - {env_file}\n" "variables: {}\n" "commands: {}\n" ) def match(self) -> bool: # TODO: a .condarc or environment.yml file is actually enough, e.g., # https://github.com/conda-incubator/conda-project/tree/main/examples/condarc-settings # but we could argue that such are not really _useful_ projects; but can you # ever see a .condarc otherwise? return not {"conda-project.yml", "conda-meta.yaml"}.isdisjoint( self.proj.basenames ) def parse(self) -> None: from projspec.artifact.process import Process from projspec.artifact.python_env import CondaEnv, LockFile from projspec.content.environment import Environment, Precision, Stack from projspec.content.executable import Command try: with self.proj.fs.open(f"{self.proj.url}/conda-project.yml") as f: meta = _yaml_no_jinja(f) except FileNotFoundError: with self.proj.fs.open(f"{self.proj.url}/conda-project.yaml") as f: meta = _yaml_no_jinja(f) envs = AttrDict() locks = AttrDict() runtimes = AttrDict() cmds = AttrDict() procs = AttrDict() for env_name, fspec in meta.get("environments", {}).items(): try: fnames = [fspec] if isinstance(fspec, str) else fspec channels = [] packages = [] for fname in fnames: with self.proj.fs.open(f"{self.proj.url}/{fname}") as f: env = _yaml_no_jinja(f) channels.extend( [_ for _ in env.get("channels", []) if _ not in channels] ) packages.extend( [ _ for _ in env.get("dependencies", []) if _ not in packages ] ) runtime = CondaEnv( proj=self.proj, cmd=["conda", "project", "prepare", env_name], fn=f"{self.proj.url}/./envs/{env_name}/", ) runtimes[env_name] = runtime lock_fname = f"{self.proj.url}/conda-lock.{env_name}.yml" lock = LockFile( proj=self.proj, cmd=["conda", "project", "lock", env_name], fn=lock_fname, ) locks[env_name] = lock # TODO: process data.metadata.souces[:] if it exists - it means the packages # are defined in another file in the project if self.proj.fs.exists(lock_fname): with self.proj.fs.open(lock_fname) as f: data = yaml.load(f, Loader=yaml.CSafeLoader) lpackages = list( { f"{p['name']} =={p['version']}" for p in data.get("package", []) } ) envs[f"{env_name}.lock"] = Environment( proj=self.proj, channels=[], packages=lpackages, stack=Stack.CONDA, precision=Precision.LOCK, ) env = Environment( proj=self.proj, channels=channels, packages=packages, stack=Stack.CONDA, precision=Precision.SPEC, ) envs[env_name] = env except FileNotFoundError: pass for name, cmd in meta.get("commands", {}).items(): prc = Process( proj=self.proj, cmd=["conda", "project", "run", name], ) procs[name] = prc cmds[name] = Command(proj=self.proj, cmd=cmd) cont = AttrDict(environment=envs, command=cmds) arts = AttrDict(lock_file=locks, conda_env=runtimes, process=procs) self._contents = cont self._artifacts = arts