From c1202e202fec583fde6015745b255637c2824a3f Mon Sep 17 00:00:00 2001 From: sophia Date: Tue, 12 Aug 2025 09:11:25 -0400 Subject: [PATCH 1/9] Plugin hooks for installers --- conda/plugins/__init__.py | 1 + conda/plugins/hookspec.py | 4 ++++ conda/plugins/manager.py | 25 +++++++++++++++++++++++++ conda/plugins/types.py | 22 ++++++++++++++++++++-- 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/conda/plugins/__init__.py b/conda/plugins/__init__.py index dd46bf272f8..6bf6d95ff36 100644 --- a/conda/plugins/__init__.py +++ b/conda/plugins/__init__.py @@ -29,6 +29,7 @@ CondaAuthHandler, CondaEnvironmentSpecifier, CondaHealthCheck, + CondaInstaller, CondaPostCommand, CondaPostSolve, CondaPostTransactionAction, diff --git a/conda/plugins/hookspec.py b/conda/plugins/hookspec.py index 9207374a429..8ce1c260ca0 100644 --- a/conda/plugins/hookspec.py +++ b/conda/plugins/hookspec.py @@ -22,6 +22,7 @@ CondaEnvironmentExporter, CondaEnvironmentSpecifier, CondaHealthCheck, + CondaInstaller, CondaPostCommand, CondaPostSolve, CondaPostTransactionAction, @@ -730,3 +731,6 @@ def conda_environment_exporters(): ) """ yield from () + + def conda_installers(self) -> Iterable[CondaInstaller]: + yield from () diff --git a/conda/plugins/manager.py b/conda/plugins/manager.py index 8f93aeff66b..2b0bd7c57cb 100644 --- a/conda/plugins/manager.py +++ b/conda/plugins/manager.py @@ -33,6 +33,7 @@ from . import ( environment_exporters, environment_specifiers, + # installers, post_solves, prefix_data_loaders, reporter_backends, @@ -59,6 +60,7 @@ CondaEnvironmentExporter, CondaEnvironmentSpecifier, CondaHealthCheck, + CondaInstaller, CondaPostCommand, CondaPostSolve, CondaPostTransactionAction, @@ -190,6 +192,9 @@ def load_entrypoints(self, group: str, name: str | None = None) -> int: count += 1 return count + @overload + def get_hook_results(self, name: Literal["installers"]) -> list[CondaInstaller]: ... + @overload def get_hook_results( self, name: Literal["subcommands"] @@ -826,6 +831,25 @@ def get_post_transaction_actions( ) for hook in self.get_hook_results("post_transaction_actions") ] + + def get_installer(self, installer_name: str) -> CondaInstaller: + """ + Returns the installer registered for the given installer name. + Raises PluginError if more than one installer is found for the same installer name. + Raises CondaValueError if no installer were found for that installer name. + """ + found = [] + for hook in self.get_hook_results("installers"): + if installer_name in hook.types: + found.append(hook) + if len(found) == 1: + return found[0] + if found: + names = ", ".join([hook.name for hook in found]) + raise PluginError( + f"Too many env installers registered for '{installer_name}': {names}" + ) + raise CondaValueError(f"Could not find env installer for '{installer_name}'.") @functools.cache @@ -846,6 +870,7 @@ def get_plugin_manager() -> CondaPluginManager: *prefix_data_loaders.plugins, *environment_specifiers.plugins, *environment_exporters.plugins, + *installers.plugins, ) plugin_manager.load_entrypoints(spec_name) return plugin_manager diff --git a/conda/plugins/types.py b/conda/plugins/types.py index 3dc60198d1f..0dd340e97a9 100644 --- a/conda/plugins/types.py +++ b/conda/plugins/types.py @@ -12,7 +12,7 @@ from abc import ABC, abstractmethod from contextlib import nullcontext from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Callable, Iterable from requests.auth import AuthBase @@ -28,7 +28,7 @@ from ..common.path import PathType from ..core.path_actions import Action from ..core.solve import Solver - from ..models.environment import Environment + from ..models.environment import Environment, EnvironmentConfig from ..models.match_spec import MatchSpec from ..models.records import PrefixRecord @@ -522,3 +522,21 @@ def __post_init__(self): except AttributeError: # AttributeError: alias is not a string raise PluginError(f"Invalid plugin aliases for {self!r}") + + +@dataclass +class CondaInstaller(CondaPlugin): + """ + **EXPERIMENTAL** + Return type to use when defining a conda installer plugin hook. + For details on how this is used, see + :meth:`~conda.plugins.hookspec.CondaSpecs.conda_installers`. + :param name: name of the installer (e.g., ``pip``) + :param types: the names of the types of packages it can install (e.g. conda, pip). + + """ + + name: str + types: Iterable[str] + install: Callable[[str, list, EnvironmentConfig, dict], dict | None] + dry_run: Callable[[str, list, EnvironmentConfig, dict], dict | None] From 30328da04bc30f2ad1bba5b7bd521629e631e114 Mon Sep 17 00:00:00 2001 From: sophia Date: Tue, 12 Aug 2025 12:30:34 -0400 Subject: [PATCH 2/9] Update installer implementation for new interface --- conda/env/installers/conda.py | 40 +++++++++++++--------------- conda/env/installers/pip.py | 24 ++++++++--------- conda/plugins/installers/__init__.py | 8 ++++++ conda/plugins/installers/conda.py | 17 ++++++++++++ conda/plugins/installers/pypi.py | 34 +++++++++++++++++++++++ conda/plugins/manager.py | 3 ++- 6 files changed, 91 insertions(+), 35 deletions(-) create mode 100644 conda/plugins/installers/__init__.py create mode 100644 conda/plugins/installers/conda.py create mode 100644 conda/plugins/installers/pypi.py diff --git a/conda/env/installers/conda.py b/conda/env/installers/conda.py index e6fca47d2e7..7555028a479 100644 --- a/conda/env/installers/conda.py +++ b/conda/env/installers/conda.py @@ -21,11 +21,12 @@ from argparse import Namespace from ...core.solve import Solver - from ...models.environment import Environment + from ...models.environment import EnvironmentConfig + from ...models.records import PackageRecord def _solve( - prefix: str, specs: list[str], args: Namespace, env: Environment, *_, **kwargs + prefix: str, specs: list[str], env_config: EnvironmentConfig, **kwargs ) -> Solver: """Solve the environment. @@ -37,9 +38,9 @@ def _solve( """ # TODO: support all various ways this happens # Including 'nodefaults' in the channels list disables the defaults - channel_urls = [chan for chan in env.config.channels if chan != "nodefaults"] + channel_urls = [chan for chan in env_config.channels if chan != "nodefaults"] - if "nodefaults" not in env.config.channels: + if "nodefaults" not in env_config.channels: channel_urls.extend(context.channels) _channel_priority_map = prioritize_channels(channel_urls) @@ -54,7 +55,7 @@ def _solve( def dry_run( - specs: list[str], args: Namespace, env: Environment, *_, **kwargs + prefix: str, specs: list[str], env_config: EnvironmentConfig, **kwargs ) -> EnvironmentYaml: """Do a dry run of the environment solve. @@ -64,16 +65,16 @@ def dry_run( :return: Solved environment object :rtype: EnvironmentYaml """ - solver = _solve(tempfile.mkdtemp(), specs, args, env, *_, **kwargs) + solver = _solve(tempfile.mkdtemp(), specs, env_config **kwargs) pkgs = solver.solve_final_state() return EnvironmentYaml( - name=env.name, dependencies=[str(p) for p in pkgs], channels=env.config.channels + name=prefix, dependencies=[str(p) for p in pkgs], channels=env_config.channels ) def install( - prefix: str, specs: list[str], args: Namespace, env: Environment, *_, **kwargs -) -> dict | None: + prefix: str, specs: list, env_config: EnvironmentConfig, **kwargs +) -> dict: """Install packages into a conda environment. This function handles two main paths: @@ -93,27 +94,24 @@ def install( processed, the conda client SHOULD NOT invoke a solver." """ # Handle explicit environments separately per CEP-23 requirements - if env.explicit_packages: + if not specs: + return {} + + # If the specs are package records, these are explicit packages. + if isinstance(specs[0], PackageRecord): from ...misc import install_explicit_packages - - # For explicit environments, we consider any provided specs as user-requested - # All packages in the explicit file are installed, but only user-provided specs - # are recorded in history as explicitly requested - requested_specs = specs if specs else () - # Install explicit packages - bypassing the solver completely return install_explicit_packages( - package_cache_records=env.explicit_packages, + package_cache_records=specs, prefix=prefix, - requested_specs=requested_specs, ) # For regular environments, proceed with the normal solve-based installation - solver = _solve(prefix, specs, args, env, *_, **kwargs) + solver = _solve(prefix, specs, env_config **kwargs) try: unlink_link_transaction = solver.solve_for_transaction( - prune=getattr(args, "prune", False), + prune=getattr(kwargs, "prune", False), update_modifier=UpdateModifier.FREEZE_INSTALLED, ) except (UnsatisfiableError, SystemExit) as exc: @@ -122,7 +120,7 @@ def install( if not getattr(exc, "allow_retry", True): raise unlink_link_transaction = solver.solve_for_transaction( - prune=getattr(args, "prune", False), update_modifier=NULL + prune=getattr(kwargs, "prune", False), update_modifier=NULL ) # Execute the transaction and return success if unlink_link_transaction.nothing_to_do: diff --git a/conda/env/installers/pip.py b/conda/env/installers/pip.py index 85ff3c35d81..35e889ce50d 100644 --- a/conda/env/installers/pip.py +++ b/conda/env/installers/pip.py @@ -10,11 +10,13 @@ from ...env.pip_util import get_pip_installed_packages, pip_subprocess from ...gateways.connection.session import CONDA_SESSION_SCHEMES from ...reporters import get_spinner +from ...models.environment import EnvironmentConfig + log = getLogger(__name__) -def _pip_install_via_requirements(prefix, specs, args, *_, **kwargs): +def _pip_install_via_requirements(prefix, specs): """ Installs the pip dependencies in specs using a temporary pip requirements file. @@ -28,16 +30,7 @@ def _pip_install_via_requirements(prefix, specs, args, *_, **kwargs): See: https://pip.pypa.io/en/stable/user_guide/#requirements-files https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format """ - url_scheme = args.file.split("://", 1)[0] - if url_scheme in CONDA_SESSION_SCHEMES: - pip_workdir = None - else: - try: - pip_workdir = op.dirname(op.abspath(args.file)) - if not os.access(pip_workdir, os.W_OK): - pip_workdir = None - except AttributeError: - pip_workdir = None + pip_workdir = None requirements = None try: # Generate the temporary requirements file @@ -67,6 +60,11 @@ def _pip_install_via_requirements(prefix, specs, args, *_, **kwargs): return get_pip_installed_packages(stdout) -def install(*args, **kwargs): +def install(prefix: str, specs: list[str], config: EnvironmentConfig, **kwargs): with get_spinner("Installing pip dependencies"): - return _pip_install_via_requirements(*args, **kwargs) + return _pip_install_via_requirements(prefix, specs) + + +def dry_run(prefix: str, specs: list[str], config: EnvironmentConfig, **kwargs) -> dict: + print("pretending to (dry-run) install stuff with pip") + return {} \ No newline at end of file diff --git a/conda/plugins/installers/__init__.py b/conda/plugins/installers/__init__.py new file mode 100644 index 00000000000..e658ae9b203 --- /dev/null +++ b/conda/plugins/installers/__init__.py @@ -0,0 +1,8 @@ +# Copyright (C) 2012 Anaconda, Inc +# SPDX-License-Identifier: BSD-3-Clause +"""Register the built-in env_installer hook implementations.""" + +from . import conda, pypi + +#: The list of env_installer plugins for easier registration with pluggy +plugins = [conda, pypi] \ No newline at end of file diff --git a/conda/plugins/installers/conda.py b/conda/plugins/installers/conda.py new file mode 100644 index 00000000000..74a6e117798 --- /dev/null +++ b/conda/plugins/installers/conda.py @@ -0,0 +1,17 @@ +# Copyright (C) 2012 Anaconda, Inc +# SPDX-License-Identifier: BSD-3-Clause +"""Register the native conda installer for conda env files.""" + +from .. import CondaInstaller, hookimpl + + +@hookimpl +def conda_installers(): + from ...env.installers.conda import dry_run, install + + yield CondaInstaller( + name="conda", + types=("conda",), + install=install, + dry_run=dry_run, + ) diff --git a/conda/plugins/installers/pypi.py b/conda/plugins/installers/pypi.py new file mode 100644 index 00000000000..fe418923792 --- /dev/null +++ b/conda/plugins/installers/pypi.py @@ -0,0 +1,34 @@ +# Copyright (C) 2012 Anaconda, Inc +# SPDX-License-Identifier: BSD-3-Clause +"""Register the pip installer for conda env files.""" + +from .. import CondaInstaller, hookimpl +from ...models.environment import EnvironmentConfig + +def uv_install(prefix: str, specs: list[str], config: EnvironmentConfig, **kwargs) -> dict: + print("pretending to install stuff with uv") + return {} + + +def uv_dry_run(prefix: str, specs: list[str], config: EnvironmentConfig, **kwargs) -> dict: + print("pretending to (dry-run) install stuff with uv") + return {} + + +@hookimpl +def conda_installers(): + from ...env.installers.pip import dry_run, install + + yield CondaInstaller( + name="pip", + types=("pip","pypi"), + install=install, + dry_run=dry_run, + ) + + yield CondaInstaller( + name="uv", + types=("uv","pypi"), + install=uv_install, + dry_run=uv_dry_run, + ) \ No newline at end of file diff --git a/conda/plugins/manager.py b/conda/plugins/manager.py index 2b0bd7c57cb..d1269c7bdba 100644 --- a/conda/plugins/manager.py +++ b/conda/plugins/manager.py @@ -33,7 +33,7 @@ from . import ( environment_exporters, environment_specifiers, - # installers, + installers, post_solves, prefix_data_loaders, reporter_backends, @@ -845,6 +845,7 @@ def get_installer(self, installer_name: str) -> CondaInstaller: if len(found) == 1: return found[0] if found: + # TODO: choose preferred plugin for the type of installer names = ", ".join([hook.name for hook in found]) raise PluginError( f"Too many env installers registered for '{installer_name}': {names}" From d269374fe4e8552f0aa093d66d4b9bff937534c4 Mon Sep 17 00:00:00 2001 From: sophia Date: Tue, 12 Aug 2025 14:29:20 -0400 Subject: [PATCH 3/9] Use installer plugins --- conda/cli/main_env_create.py | 19 +++++++++++-------- conda/env/installers/conda.py | 6 +++--- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/conda/cli/main_env_create.py b/conda/cli/main_env_create.py index d357a37fbed..46b515f4266 100644 --- a/conda/cli/main_env_create.py +++ b/conda/cli/main_env_create.py @@ -113,6 +113,7 @@ def configure_parser(sub_parsers: _SubParsersAction, **kwargs) -> ArgumentParser def execute(args: Namespace, parser: ArgumentParser) -> int: from ..auxlib.ish import dals from ..base.context import context, determine_target_prefix + from ..common.constants import NULL from ..common.serialize import json from ..core.prefix_data import PrefixData from ..env.env import print_result @@ -166,13 +167,15 @@ def execute(args: Namespace, parser: ArgumentParser) -> int: context.create_default_packages if not args.no_default_packages else [] ) + cli_args = {k: v for k,v in vars(args).items() if v != NULL and v != None} + if args.dry_run: installer_type = "conda" - installer = get_installer(installer_type) + installer = context.plugin_manager.get_installer(installer_type) pkg_specs = [*env.requested_packages, *args_packages] - solved_env = installer.dry_run(pkg_specs, args, env) + solved_env = installer.dry_run(env.prefix, pkg_specs, env.config, **cli_args) if args.json: print(json.dumps(solved_env.to_dict())) else: @@ -181,21 +184,21 @@ def execute(args: Namespace, parser: ArgumentParser) -> int: else: if args_packages: installer_type = "conda" - installer = get_installer(installer_type) - result[installer_type] = installer.install(prefix, args_packages, args, env) + installer = context.plugin_manager.get_installer(installer_type) + result[installer_type] = installer.install(prefix, args_packages, env.config, **cli_args) # install conda packages installer_type = "conda" - installer = get_installer(installer_type) + installer = context.plugin_manager.get_installer(installer_type) result[installer_type] = installer.install( - prefix, env.requested_packages, args, env + prefix, env.requested_packages, env.config, **cli_args ) # install all other external packages for installer_type, pkg_specs in env.external_packages.items(): try: - installer = get_installer(installer_type) - result[installer_type] = installer.install(prefix, pkg_specs, args, env) + installer = context.plugin_manager.get_installer(installer_type) + result[installer_type] = installer.install(prefix, pkg_specs, env.config, **cli_args) except InvalidInstaller: raise CondaError( dals( diff --git a/conda/env/installers/conda.py b/conda/env/installers/conda.py index 7555028a479..25b9b563c1e 100644 --- a/conda/env/installers/conda.py +++ b/conda/env/installers/conda.py @@ -16,13 +16,13 @@ from ...env.env import EnvironmentYaml from ...exceptions import CondaValueError, UnsatisfiableError from ...models.channel import Channel, prioritize_channels +from ...models.records import PackageRecord if TYPE_CHECKING: from argparse import Namespace from ...core.solve import Solver from ...models.environment import EnvironmentConfig - from ...models.records import PackageRecord def _solve( @@ -65,7 +65,7 @@ def dry_run( :return: Solved environment object :rtype: EnvironmentYaml """ - solver = _solve(tempfile.mkdtemp(), specs, env_config **kwargs) + solver = _solve(prefix, specs, env_config, **kwargs) pkgs = solver.solve_final_state() return EnvironmentYaml( name=prefix, dependencies=[str(p) for p in pkgs], channels=env_config.channels @@ -107,7 +107,7 @@ def install( ) # For regular environments, proceed with the normal solve-based installation - solver = _solve(prefix, specs, env_config **kwargs) + solver = _solve(prefix, specs, env_config, **kwargs) try: unlink_link_transaction = solver.solve_for_transaction( From aa16bec7fa47b66bece8fe254be284eca34fa2a3 Mon Sep 17 00:00:00 2001 From: sophia Date: Tue, 12 Aug 2025 21:07:09 -0400 Subject: [PATCH 4/9] Register uv demo installer for pip --- conda/plugins/installers/pypi.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/conda/plugins/installers/pypi.py b/conda/plugins/installers/pypi.py index fe418923792..3994911075f 100644 --- a/conda/plugins/installers/pypi.py +++ b/conda/plugins/installers/pypi.py @@ -28,7 +28,11 @@ def conda_installers(): yield CondaInstaller( name="uv", - types=("uv","pypi"), + # uv is not meant to be able to install pip packages + # but, for the purpose of this demo "pypi" and "pip" will + # be used interchangeably. This is to avoid implementing + # a new type of dependency in the conda.env.env module. + types=("uv","pypi", "pip"), install=uv_install, dry_run=uv_dry_run, ) \ No newline at end of file From 1da70039228428ca65633960b1d18c9bf3aade3e Mon Sep 17 00:00:00 2001 From: sophia Date: Wed, 13 Aug 2025 12:53:17 -0400 Subject: [PATCH 5/9] Get preferred installer from settings --- conda/plugins/hookspec.py | 1 + conda/plugins/installers/pypi.py | 21 +++++++++++++++++++-- conda/plugins/manager.py | 22 +++++++++++++--------- conda/plugins/types.py | 6 ++++-- 4 files changed, 37 insertions(+), 13 deletions(-) diff --git a/conda/plugins/hookspec.py b/conda/plugins/hookspec.py index 8ce1c260ca0..5dd9ab25e33 100644 --- a/conda/plugins/hookspec.py +++ b/conda/plugins/hookspec.py @@ -732,5 +732,6 @@ def conda_environment_exporters(): """ yield from () + @_hookspec def conda_installers(self) -> Iterable[CondaInstaller]: yield from () diff --git a/conda/plugins/installers/pypi.py b/conda/plugins/installers/pypi.py index 3994911075f..b1cb117aee6 100644 --- a/conda/plugins/installers/pypi.py +++ b/conda/plugins/installers/pypi.py @@ -2,8 +2,9 @@ # SPDX-License-Identifier: BSD-3-Clause """Register the pip installer for conda env files.""" -from .. import CondaInstaller, hookimpl +from .. import CondaInstaller, CondaSetting, hookimpl from ...models.environment import EnvironmentConfig +from ...common.configuration import PrimitiveParameter def uv_install(prefix: str, specs: list[str], config: EnvironmentConfig, **kwargs) -> dict: print("pretending to install stuff with uv") @@ -35,4 +36,20 @@ def conda_installers(): types=("uv","pypi", "pip"), install=uv_install, dry_run=uv_dry_run, - ) \ No newline at end of file + ) + +@hookimpl +def conda_settings(): + yield CondaSetting( + name="pypi_preferred_installer", + description="preferred installer for pip", + parameter=PrimitiveParameter(""), + aliases=("pip_preferred_installer", ), + ) + + yield CondaSetting( + name="pip_preferred_installer", + description="preferred installer for pip", + parameter=PrimitiveParameter(""), + aliases=("pypi_preferred_installer", ), + ) diff --git a/conda/plugins/manager.py b/conda/plugins/manager.py index d1269c7bdba..4e0707caf6d 100644 --- a/conda/plugins/manager.py +++ b/conda/plugins/manager.py @@ -832,25 +832,29 @@ def get_post_transaction_actions( for hook in self.get_hook_results("post_transaction_actions") ] - def get_installer(self, installer_name: str) -> CondaInstaller: + def get_installer(self, installer_type: str) -> CondaInstaller: """ Returns the installer registered for the given installer name. Raises PluginError if more than one installer is found for the same installer name. Raises CondaValueError if no installer were found for that installer name. """ - found = [] + found = {} for hook in self.get_hook_results("installers"): - if installer_name in hook.types: - found.append(hook) + if installer_type in hook.types: + found[hook.name] = hook if len(found) == 1: - return found[0] + return next(iter(found.values())) if found: - # TODO: choose preferred plugin for the type of installer - names = ", ".join([hook.name for hook in found]) + # Choose preferred plugin for the type of installer. Users may set + # their preferred installer for a given type using plugin config. + # This will check for config with the name `_preferred_installer`. + preferred_installer = getattr(context.plugins, f"{installer_type}_preferred_installer", None) + if preferred_installer in found.keys(): + return found[preferred_installer] raise PluginError( - f"Too many env installers registered for '{installer_name}': {names}" + f"Too many env installers registered for '{installer_type}': {', '.join(found.keys())}" ) - raise CondaValueError(f"Could not find env installer for '{installer_name}'.") + raise CondaValueError(f"Could not find env installer for '{installer_type}'.") @functools.cache diff --git a/conda/plugins/types.py b/conda/plugins/types.py index 0dd340e97a9..5505f2379cb 100644 --- a/conda/plugins/types.py +++ b/conda/plugins/types.py @@ -16,8 +16,10 @@ from requests.auth import AuthBase +from .config import PluginConfig from ..exceptions import PluginError from ..models.records import PackageRecord +from ..common.configuration import PrimitiveParameter if TYPE_CHECKING: from argparse import ArgumentParser, Namespace @@ -538,5 +540,5 @@ class CondaInstaller(CondaPlugin): name: str types: Iterable[str] - install: Callable[[str, list, EnvironmentConfig, dict], dict | None] - dry_run: Callable[[str, list, EnvironmentConfig, dict], dict | None] + install: Callable[[str, list, EnvironmentConfig, dict], dict] + dry_run: Callable[[str, list, EnvironmentConfig, dict], dict] From 7d7908893948f556dca09ea4823ecaf1cd8a185c Mon Sep 17 00:00:00 2001 From: sophia Date: Wed, 13 Aug 2025 13:41:45 -0400 Subject: [PATCH 6/9] Automatically register plugin preferred installer setting --- conda/plugins/installers/pypi.py | 16 ---------------- conda/plugins/manager.py | 7 +++++-- conda/plugins/types.py | 11 +++++++++++ 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/conda/plugins/installers/pypi.py b/conda/plugins/installers/pypi.py index b1cb117aee6..b04a618af56 100644 --- a/conda/plugins/installers/pypi.py +++ b/conda/plugins/installers/pypi.py @@ -37,19 +37,3 @@ def conda_installers(): install=uv_install, dry_run=uv_dry_run, ) - -@hookimpl -def conda_settings(): - yield CondaSetting( - name="pypi_preferred_installer", - description="preferred installer for pip", - parameter=PrimitiveParameter(""), - aliases=("pip_preferred_installer", ), - ) - - yield CondaSetting( - name="pip_preferred_installer", - description="preferred installer for pip", - parameter=PrimitiveParameter(""), - aliases=("pypi_preferred_installer", ), - ) diff --git a/conda/plugins/manager.py b/conda/plugins/manager.py index 4e0707caf6d..127a3d68b15 100644 --- a/conda/plugins/manager.py +++ b/conda/plugins/manager.py @@ -849,8 +849,11 @@ def get_installer(self, installer_type: str) -> CondaInstaller: # their preferred installer for a given type using plugin config. # This will check for config with the name `_preferred_installer`. preferred_installer = getattr(context.plugins, f"{installer_type}_preferred_installer", None) - if preferred_installer in found.keys(): - return found[preferred_installer] + if preferred_installer: + if preferred_installer in found.keys(): + return found[preferred_installer] + else: + raise CondaValueError(f"Could not find preferred installer plugin '{preferred_installer}' for installing package from '{installer_type}'.") raise PluginError( f"Too many env installers registered for '{installer_type}': {', '.join(found.keys())}" ) diff --git a/conda/plugins/types.py b/conda/plugins/types.py index 5505f2379cb..d7d4c19f47d 100644 --- a/conda/plugins/types.py +++ b/conda/plugins/types.py @@ -542,3 +542,14 @@ class CondaInstaller(CondaPlugin): types: Iterable[str] install: Callable[[str, list, EnvironmentConfig, dict], dict] dry_run: Callable[[str, list, EnvironmentConfig, dict], dict] + + def __post_init__(self): + # Handle name normalization + super().__post_init__() + + # Configure plugin settings for preferred_installers + PluginConfig.add_plugin_setting( + name=f"{self.name}_preferred_installer", + parameter=PrimitiveParameter(""), + aliases=[f"{t}_preferred_installer" for t in self.types] + ) From 3941353a686dfd2b4663a24b20c2b375db603049 Mon Sep 17 00:00:00 2001 From: sophia Date: Wed, 13 Aug 2025 14:45:42 -0400 Subject: [PATCH 7/9] Register all types as settings --- conda/plugins/installers/pypi.py | 20 +++++++++++--------- conda/plugins/manager.py | 4 +++- conda/plugins/types.py | 11 ++++++----- 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/conda/plugins/installers/pypi.py b/conda/plugins/installers/pypi.py index b04a618af56..65ff9a7f3a2 100644 --- a/conda/plugins/installers/pypi.py +++ b/conda/plugins/installers/pypi.py @@ -8,32 +8,34 @@ def uv_install(prefix: str, specs: list[str], config: EnvironmentConfig, **kwargs) -> dict: print("pretending to install stuff with uv") + for spec in specs: + print(f" - {spec}") return {} def uv_dry_run(prefix: str, specs: list[str], config: EnvironmentConfig, **kwargs) -> dict: print("pretending to (dry-run) install stuff with uv") + for spec in specs: + print(f" - {spec}") return {} @hookimpl def conda_installers(): - from ...env.installers.pip import dry_run, install + from ...env.installers.pip import dry_run as pip_dry_run + from ...env.installers.pip import install as pip_install + # In this demo, swap the meaning of "pip" and "pypi" yield CondaInstaller( - name="pip", + name="pypi", types=("pip","pypi"), - install=install, - dry_run=dry_run, + install=pip_install, + dry_run=pip_dry_run, ) yield CondaInstaller( name="uv", - # uv is not meant to be able to install pip packages - # but, for the purpose of this demo "pypi" and "pip" will - # be used interchangeably. This is to avoid implementing - # a new type of dependency in the conda.env.env module. - types=("uv","pypi", "pip"), + types=("uv", "pip"), install=uv_install, dry_run=uv_dry_run, ) diff --git a/conda/plugins/manager.py b/conda/plugins/manager.py index 127a3d68b15..64edd1dd58c 100644 --- a/conda/plugins/manager.py +++ b/conda/plugins/manager.py @@ -855,7 +855,9 @@ def get_installer(self, installer_type: str) -> CondaInstaller: else: raise CondaValueError(f"Could not find preferred installer plugin '{preferred_installer}' for installing package from '{installer_type}'.") raise PluginError( - f"Too many env installers registered for '{installer_type}': {', '.join(found.keys())}" + f"Too many installers registered for '{installer_type}': {dashlist(found.keys())}" + f"\n\nSet the default installer for package type `{installer_type}` by configuring the setting `{installer_type}_preferred_installer`." + f"\nFor example:\n `export CONDA_PLUGINS_PIP_PREFERRED_INSTALLER=uv`." ) raise CondaValueError(f"Could not find env installer for '{installer_type}'.") diff --git a/conda/plugins/types.py b/conda/plugins/types.py index d7d4c19f47d..1958b344477 100644 --- a/conda/plugins/types.py +++ b/conda/plugins/types.py @@ -548,8 +548,9 @@ def __post_init__(self): super().__post_init__() # Configure plugin settings for preferred_installers - PluginConfig.add_plugin_setting( - name=f"{self.name}_preferred_installer", - parameter=PrimitiveParameter(""), - aliases=[f"{t}_preferred_installer" for t in self.types] - ) + for t in self.types: + PluginConfig.add_plugin_setting( + name=f"{t}_preferred_installer", + parameter=PrimitiveParameter(""), + # aliases=[f"{t}_preferred_installer" for t in self.types] + ) From f64a8579b67c9c2b4195f29f7c9e1cca1ca3d146 Mon Sep 17 00:00:00 2001 From: sophia Date: Thu, 14 Aug 2025 16:01:13 -0400 Subject: [PATCH 8/9] Treat conda installing seperate from plugin installs --- conda/cli/main_env_create.py | 9 +++--- conda/env/installers/pip.py | 4 +-- conda/plugins/installers/__init__.py | 4 +-- conda/plugins/installers/conda.py | 17 ------------ conda/plugins/installers/pip.py | 19 +++++++++++++ conda/plugins/installers/pypi.py | 41 ---------------------------- conda/plugins/installers/uv.py | 29 ++++++++++++++++++++ 7 files changed, 56 insertions(+), 67 deletions(-) delete mode 100644 conda/plugins/installers/conda.py create mode 100644 conda/plugins/installers/pip.py delete mode 100644 conda/plugins/installers/pypi.py create mode 100644 conda/plugins/installers/uv.py diff --git a/conda/cli/main_env_create.py b/conda/cli/main_env_create.py index 46b515f4266..05375a1b50f 100644 --- a/conda/cli/main_env_create.py +++ b/conda/cli/main_env_create.py @@ -171,7 +171,7 @@ def execute(args: Namespace, parser: ArgumentParser) -> int: if args.dry_run: installer_type = "conda" - installer = context.plugin_manager.get_installer(installer_type) + installer = get_installer(installer_type) pkg_specs = [*env.requested_packages, *args_packages] @@ -182,14 +182,13 @@ def execute(args: Namespace, parser: ArgumentParser) -> int: print(solved_env.to_yaml(), end="") else: + # Install conda packages if args_packages: installer_type = "conda" - installer = context.plugin_manager.get_installer(installer_type) + installer = get_installer(installer_type) result[installer_type] = installer.install(prefix, args_packages, env.config, **cli_args) - - # install conda packages installer_type = "conda" - installer = context.plugin_manager.get_installer(installer_type) + installer = get_installer(installer_type) result[installer_type] = installer.install( prefix, env.requested_packages, env.config, **cli_args ) diff --git a/conda/env/installers/pip.py b/conda/env/installers/pip.py index 35e889ce50d..5ae09da3da0 100644 --- a/conda/env/installers/pip.py +++ b/conda/env/installers/pip.py @@ -60,11 +60,11 @@ def _pip_install_via_requirements(prefix, specs): return get_pip_installed_packages(stdout) -def install(prefix: str, specs: list[str], config: EnvironmentConfig, **kwargs): +def install(prefix: str, specs: list[str], *args, **kwargs): with get_spinner("Installing pip dependencies"): return _pip_install_via_requirements(prefix, specs) -def dry_run(prefix: str, specs: list[str], config: EnvironmentConfig, **kwargs) -> dict: +def dry_run(prefix: str, specs: list[str], *args, **kwargs) -> dict: print("pretending to (dry-run) install stuff with pip") return {} \ No newline at end of file diff --git a/conda/plugins/installers/__init__.py b/conda/plugins/installers/__init__.py index e658ae9b203..e9ebd3d119a 100644 --- a/conda/plugins/installers/__init__.py +++ b/conda/plugins/installers/__init__.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: BSD-3-Clause """Register the built-in env_installer hook implementations.""" -from . import conda, pypi +from . import pip, uv #: The list of env_installer plugins for easier registration with pluggy -plugins = [conda, pypi] \ No newline at end of file +plugins = [pip, uv] \ No newline at end of file diff --git a/conda/plugins/installers/conda.py b/conda/plugins/installers/conda.py deleted file mode 100644 index 74a6e117798..00000000000 --- a/conda/plugins/installers/conda.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (C) 2012 Anaconda, Inc -# SPDX-License-Identifier: BSD-3-Clause -"""Register the native conda installer for conda env files.""" - -from .. import CondaInstaller, hookimpl - - -@hookimpl -def conda_installers(): - from ...env.installers.conda import dry_run, install - - yield CondaInstaller( - name="conda", - types=("conda",), - install=install, - dry_run=dry_run, - ) diff --git a/conda/plugins/installers/pip.py b/conda/plugins/installers/pip.py new file mode 100644 index 00000000000..535372b2c1d --- /dev/null +++ b/conda/plugins/installers/pip.py @@ -0,0 +1,19 @@ +# Copyright (C) 2012 Anaconda, Inc +# SPDX-License-Identifier: BSD-3-Clause +"""Register the pip installer for conda env files.""" + +from .. import CondaInstaller, hookimpl + + +@hookimpl +def conda_installers(): + from ...env.installers.pip import dry_run as pip_dry_run + from ...env.installers.pip import install as pip_install + + # In this demo, swap the meaning of "pip" and "pypi" + yield CondaInstaller( + name="pypi_pip", + types=("pip","pypi"), + install=pip_install, + dry_run=pip_dry_run, + ) diff --git a/conda/plugins/installers/pypi.py b/conda/plugins/installers/pypi.py deleted file mode 100644 index 65ff9a7f3a2..00000000000 --- a/conda/plugins/installers/pypi.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (C) 2012 Anaconda, Inc -# SPDX-License-Identifier: BSD-3-Clause -"""Register the pip installer for conda env files.""" - -from .. import CondaInstaller, CondaSetting, hookimpl -from ...models.environment import EnvironmentConfig -from ...common.configuration import PrimitiveParameter - -def uv_install(prefix: str, specs: list[str], config: EnvironmentConfig, **kwargs) -> dict: - print("pretending to install stuff with uv") - for spec in specs: - print(f" - {spec}") - return {} - - -def uv_dry_run(prefix: str, specs: list[str], config: EnvironmentConfig, **kwargs) -> dict: - print("pretending to (dry-run) install stuff with uv") - for spec in specs: - print(f" - {spec}") - return {} - - -@hookimpl -def conda_installers(): - from ...env.installers.pip import dry_run as pip_dry_run - from ...env.installers.pip import install as pip_install - - # In this demo, swap the meaning of "pip" and "pypi" - yield CondaInstaller( - name="pypi", - types=("pip","pypi"), - install=pip_install, - dry_run=pip_dry_run, - ) - - yield CondaInstaller( - name="uv", - types=("uv", "pip"), - install=uv_install, - dry_run=uv_dry_run, - ) diff --git a/conda/plugins/installers/uv.py b/conda/plugins/installers/uv.py new file mode 100644 index 00000000000..1372e31b175 --- /dev/null +++ b/conda/plugins/installers/uv.py @@ -0,0 +1,29 @@ +# Copyright (C) 2012 Anaconda, Inc +# SPDX-License-Identifier: BSD-3-Clause +"""Register the pip installer for conda env files.""" + +from .. import CondaInstaller, hookimpl +from ...models.environment import EnvironmentConfig + +def uv_install(prefix: str, specs: list[str], *args, **kwargs) -> dict: + print("pretending to install stuff with uv") + for spec in specs: + print(f" - {spec}") + return {} + + +def uv_dry_run(prefix: str, specs: list[str], *args, **kwargs) -> dict: + print("pretending to (dry-run) install stuff with uv") + for spec in specs: + print(f" - {spec}") + return {} + + +@hookimpl +def conda_installers(): + yield CondaInstaller( + name="pypi_uv", + types=("uv", "pip", "pypi"), + install=uv_install, + dry_run=uv_dry_run, + ) From 4ecead57b6f2dc4aa915492bf854bfce6fabaa32 Mon Sep 17 00:00:00 2001 From: sophia Date: Thu, 14 Aug 2025 16:16:47 -0400 Subject: [PATCH 9/9] Use plugins in conda install/create/update commands --- conda/cli/install.py | 20 ++++++++++++++++++++ conda/env/env.py | 2 +- conda/models/environment.py | 20 +++++++++----------- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/conda/cli/install.py b/conda/cli/install.py index 166dad66378..3a6bcbf52b3 100644 --- a/conda/cli/install.py +++ b/conda/cli/install.py @@ -17,6 +17,7 @@ from boltons.setutils import IndexedSet +from ..auxlib.ish import dals from ..base.constants import ( REPODATA_FN, ROOT_ENV_NAME, @@ -33,6 +34,7 @@ from ..core.solve import diff_for_unlink_link_precs from ..deprecations import deprecated from ..exceptions import ( + CondaError, CondaEnvException, CondaExitZero, CondaImportError, @@ -42,6 +44,7 @@ CondaValueError, DirectoryNotACondaEnvironmentError, DryRunExit, + InvalidInstaller, NoBaseEnvironmentError, PackageNotInstalledError, PackagesNotFoundError, @@ -425,6 +428,23 @@ def install(args, parser, command="install"): handle_txn(unlink_link_transaction, prefix, args, newenv) + # install all other external packages + for installer_type, pkg_specs in env.external_packages.items(): + try: + installer = context.plugin_manager.get_installer(installer_type) + installer.install(prefix, pkg_specs, env.config) + except InvalidInstaller: + raise CondaError( + dals( + f""" + Unable to install package for {installer_type}. + + Please double check and ensure your dependencies for {installer_type} + are correctly formatted. + """ + ) + ) + def install_clone(args, parser): """Executes an install of a new conda environment by cloning.""" diff --git a/conda/env/env.py b/conda/env/env.py index 23dee7f988e..17b1e73b8dd 100644 --- a/conda/env/env.py +++ b/conda/env/env.py @@ -271,7 +271,7 @@ def to_environment_model(self) -> EnvironmentModel: platform=context.subdir, name=self.name, config=config, - variables=self.variables, + variables=self.variables or {}, external_packages=external_packages, requested_packages=requested_packages, ) diff --git a/conda/models/environment.py b/conda/models/environment.py index fe6b5b882f4..ef71188db7a 100644 --- a/conda/models/environment.py +++ b/conda/models/environment.py @@ -500,16 +500,12 @@ def from_cli( # environment spec plugin. The core conda cli commands are not # ready for that yet. So, use this old way of reading specs from # files. - for fpath in args.file: - try: - specs.extend( - [spec for spec in specs_from_url(fpath) if spec != EXPLICIT_MARKER] - ) - except UnicodeError: - raise CondaError( - "Error reading file, file should be a text file containing packages\n" - "See `conda create --help` for details." - ) + file_envs = [] + for path in args.file: + # parse the file + spec_hook = context.plugin_manager.get_environment_specifier(path) + file_env = spec_hook.environment_spec(path).env + file_envs.append(file_env) # Add default packages if required. If the default package is already # present in the list of specs, don't add it (this will override any @@ -538,7 +534,7 @@ def from_cli( "Cannot mix specifications with conda package filenames" ) - return Environment( + cli_env = Environment( name=args.name, prefix=context.target_prefix, platform=context.subdir, @@ -546,3 +542,5 @@ def from_cli( explicit_packages=explicit_packages, config=EnvironmentConfig.from_context(), ) + + return Environment.merge(cli_env, *file_envs) \ No newline at end of file