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()#
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 environment(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.environment
Expresses the provided environment file as a conda environment object.
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.
@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
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 environment(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