Environment Specifiers#
Conda can create environments from several file formats. Currently, conda natively supports creating environments from:
For more information on how to manage conda environments, see the `Managing environments`_ documentation.
Example plugin#
The available readers can be extended with additional plugins via the conda_environment_specifiers
hook.
Hint
To see a fully functioning example of a Environment Spec backend,
checkout the yaml_file
module.
- conda_environment_specifiers()#
EXPERIMENTAL Register new conda env spec type
The example below defines a type of conda env file called "random". It can parse a file with the file extension .random. This plugin will ignore whatever is in the input environment file and produce an environment with a random name and with random packages.
Example:
import json import random from pathlib import Path from subprocess import run from conda import plugins from ...plugins.types import EnvironmentSpecBase from conda.env.env import Environment packages = ["python", "numpy", "scipy", "matplotlib", "pandas", "scikit-learn"] class RandomSpec(EnvironmentSpecBase): extensions = {".random"} def __init__(self, filename: str): self.filename = filename def can_handle(self): # Return early if no filename was provided if self.filename is None: return False # Extract the file extension (e.g., '.txt' or '' if no extension) file_ext = os.path.splitext(self.filename)[1] # Check if the file has a supported extension and exists return any( spec_ext == file_ext and os.path.exists(self.filename) for spec_ext in RandomSpec.extensions ) def env(self): return Environment( name="".join(random.choice("0123456789abcdef") for i in range(6)), dependencies=[random.choice(packages) for i in range(6)], ) @plugins.hookimpl def conda_environment_specifiers(): yield plugins.CondaEnvSpec( name="random", environment_spec=RandomSpec, )
Defining EnvironmentSpecBase
#
The first class we define is a subclass of EnvironmentSpecBase
. The
base class is an abstract base class which requires us to define our own implementations
of its abstract methods:
can_handle
Determines if the defined plugin can read and operate on the provided file.env
Expresses the provided environment file as a conda environment object.
The class may also define the boolean class variable detection_supported. When set to
True
, the plugin will be included in the environment spec type discovery process. Otherwise,
the plugin will only be able to be used when it is specifically selected. By default, this
value is True
.`
Be sure to be very specific when implementing the can_handle
method. It should only
return a True
if the file can be parsed by the plugin. Making the can_handle
method too permissive in the types of files it handles may lead to conflicts with other
plugins. If multiple installed plugins are able to can_handle
the same file type,
conda will return an error to the user.
Registering the plugin hook#
In order to make the plugin available to conda, it must be registered with the plugin
manager. Define a function with the plugins.hookimpl
decorator to register
our plugin which returns our class wrapped in a
CondaEnvironmentSpecifier
object. Note, that by default
autodetection is enabled.
@plugins.hookimpl
def conda_environment_specifiers():
yield plugins.CondaEnvSpec(
name="random",
environment_spec=RandomSpec,
)
Using the Plugin#
Once this plugin is registered, users will be able to create environments from the types of files specified by the plugin. For example to create a random environment using the plugin defined above:
conda env create --file /doesnt/matter/any/way.random
Plugin detection#
When conda is trying to determine which environment spec plugin to use it will loop through all
registered plugins and call their can_handle
function. If one (and only one) plugin returns a
True
value, conda will use that plugin to read the provided environment spec. However, if multiple
plugins are detected an error will be raised.
Plugin authors may explicitly disable their plugin from being detected by disabling autodetection in their plugin class
class RandomSpec(EnvironmentSpecBase):
detection_supported = False
def __init__(self, filename: str):
self.filename = filename
def can_handle(self):
return True
def env(self):
return Environment(name="random-environment", dependencies=["python", "numpy"])
End users can bypass environment spec plugin detection and explicitly request a plugin to be used by configuring conda to use a particular installed plugin. This can be done by either:
cli by providing the
--env-spec
flag, orenvironment variable by setting the
CONDA_ENV_SPEC
environment variable, or.condarc
by setting theenvironment_specifier
config field
Another example plugin#
In this example, we want to build a more realistic environemnt spec plugin. This
plugin has a scheme which expresses what it expects a valid environment file to
contain. In this example, a valid environment file is a .json
file that defines:
an environment name (required)
a list of conda dependencies
import os
from pydantic import BaseModel
from conda.plugins import CondaEnvironmentSpecifier, hookimpl
from conda.plugins.types import EnvironmentSpecBase
from conda.env.env import Environment
class MySimpleEnvironment(BaseModel):
"""An model representing an environment file."""
# required
name: str
# optional
conda_deps: list[str] = []
class MySimpleSpec(EnvironmentSpecBase):
def __init__(self, filename=None):
self.filename = filename
def _parse_data(self) -> MySimpleEnvironment:
""" "Validate and convert the provided file into a MySimpleEnvironment"""
with open(self.filename, "rb") as fp:
json_data = fp.read()
return MySimpleEnvironment.model_validate_json(json_data)
def can_handle(self) -> bool:
"""
Validates loader can process environment definition.
This can handle if:
* the file exists
* the file can be read
* the data can be parsed as JSON into a MySimpleEnvironment object
:return: True if the file can be parsed and handled, False otherwise
"""
if not os.path.exists(self.filename):
return False
try:
self._parse_data()
except Exception:
return False
return True
@property
def env(self) -> Environment:
"""Returns the Environment representation of the environment spec file"""
data = self._parse_data()
return Environment(
name=data.name,
dependencies=data.conda_deps,
)
@hookimpl
def conda_environment_specifiers():
yield CondaEnvironmentSpecifier(
name="mysimple",
environment_spec=MySimpleSpec,
)
We can test this out by trying to create a conda environment with a new file
that is compatible with the definied spec. Create a file testenv.json
{
"name": "mysimpletest",
"conda_deps": ["numpy", "pandas"]
}
Then, create the environment
$ conda env create --file testenv.json