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