Coverage for src / bioimageio / spec / conda_env.py: 67%
67 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-08 13:52 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-08 13:52 +0000
1"""Representation of conda environment.yaml files for bioimageio specifications."""
3import warnings
4from typing import Any, Callable, List, Optional, Union, cast
6from pydantic import BaseModel, Field, field_validator, model_validator
9class PipDeps(BaseModel):
10 """Pip dependencies to include in conda dependecies"""
12 pip: List[str] = Field(default_factory=list)
14 @field_validator("pip", mode="after")
15 @classmethod
16 def _remove_empty_and_sort(cls, value: List[str]) -> List[str]:
17 return sorted((vs for v in value if (vs := v.strip())))
19 def __lt__(self, other: Any):
20 if isinstance(other, PipDeps):
21 return len(self.pip) < len(other.pip)
22 else:
23 return False
25 def __gt__(self, other: Any):
26 if isinstance(other, PipDeps):
27 return len(self.pip) > len(other.pip)
28 else:
29 return False
32class CondaEnv(BaseModel):
33 """Represenation of the content of a conda environment.yaml file"""
35 name: Optional[str] = None
36 channels: List[str] = Field(default_factory=list)
37 dependencies: List[Union[str, PipDeps]] = Field(
38 default_factory=cast(Callable[[], List[Union[str, PipDeps]]], list)
39 )
41 @field_validator("name", mode="after")
42 def _ensure_valid_conda_env_name(cls, value: Optional[str]) -> Optional[str]:
43 if value is None:
44 return None
46 for illegal in ("/", " ", ":", "#"):
47 value = value.replace(illegal, "")
49 return value or "empty"
51 @property
52 def wo_name(self):
53 return self.model_construct(**{k: v for k, v in self if k != "name"})
55 def _get_version_pin(self, package: str):
56 """Helper to return any version pin for **package**
58 TODO: improve: interprete version pin and return structured information.
59 """
60 for d in self.dependencies:
61 if isinstance(d, PipDeps):
62 for p in d.pip:
63 if p.startswith(package):
64 return p[len(package) :]
65 elif d.startswith(package):
66 return d[len(package) :]
67 elif "::" in d and (d_wo_channel := d.split("::", 1)[-1]).startswith(
68 package
69 ):
70 return d_wo_channel[len(package) :]
72 def get_pip_deps(self) -> List[str]:
73 """Get the pip dependencies of this conda env."""
74 for dep in self.dependencies:
75 if isinstance(dep, PipDeps):
76 return dep.pip
78 return []
81class BioimageioCondaEnv(CondaEnv):
82 """A special `CondaEnv` that
83 - automatically adds bioimageio specific dependencies
84 - sorts dependencies
85 """
87 @model_validator(mode="after")
88 def _normalize_bioimageio_conda_env(self):
89 """update a conda env such that we have bioimageio.core and sorted dependencies"""
90 for req_channel in ("conda-forge", "nodefaults"):
91 if req_channel not in self.channels:
92 self.channels.append(req_channel)
94 if "defaults" in self.channels:
95 warnings.warn("removing 'defaults' from conda-channels")
96 self.channels.remove("defaults")
98 if "pip" not in self.dependencies:
99 self.dependencies.append("pip")
101 for dep in self.dependencies:
102 if isinstance(dep, PipDeps):
103 pip_section = dep
104 pip_section.pip.sort()
105 break
106 else:
107 pip_section = None
109 if (
110 pip_section is None
111 or not any(pd.startswith("bioimageio.core") for pd in pip_section.pip)
112 ) and not any(
113 d.startswith("bioimageio.core")
114 or d.startswith("conda-forge::bioimageio.core")
115 for d in self.dependencies
116 if not isinstance(d, PipDeps)
117 ):
118 self.dependencies.append("conda-forge::bioimageio.core")
120 self.dependencies.sort()
121 return self