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