diff --git a/conda/env/specs/__init__.py b/conda/env/specs/__init__.py index a61aac17307..2194d083fae 100644 --- a/conda/env/specs/__init__.py +++ b/conda/env/specs/__init__.py @@ -10,7 +10,7 @@ from ...exceptions import ( EnvironmentFileExtensionNotValid, EnvironmentFileNotFound, - EnvSpecPluginNotDetected, + EnvironmentSpecPluginNotDetected, SpecNotFound, ) from ...gateways.connection.session import CONDA_SESSION_SCHEMES @@ -26,7 +26,7 @@ @deprecated( "25.9", "26.3", - addendum="Use conda.base.context.plugin_manager.get_environment_specifer_handler.", + addendum="Use conda.base.context.plugin_manager.get_environment_specifiers.", ) def get_spec_class_from_file(filename: str) -> FileSpecTypes: """ @@ -50,7 +50,9 @@ def get_spec_class_from_file(filename: str) -> FileSpecTypes: ) if file_exists: if ext == "" or ext not in all_valid_exts: - raise EnvironmentFileExtensionNotValid(filename) + raise EnvironmentFileExtensionNotValid( + filename, extensions=list(all_valid_exts) + ) elif ext in YamlFileSpec.extensions: return YamlFileSpec elif ext in RequirementsSpec.extensions: @@ -62,19 +64,19 @@ def get_spec_class_from_file(filename: str) -> FileSpecTypes: @deprecated.argument( "25.9", "26.3", "directory", addendum="Specify the full path in filename" ) -def detect( - filename: str | None = None, -) -> SpecTypes: +def detect(filename: str | None = None) -> SpecTypes: """ Return the appropriate spec type to use. :raises SpecNotFound: Raised if no suitable spec class could be found given the input """ + if filename is None: + raise SpecNotFound("No filename provided") + try: - spec_hook = context.plugin_manager.get_environment_specifier_handler( + spec_hook = context.plugin_manager.get_environment_specifiers( filename=filename, ) - except EnvSpecPluginNotDetected as e: + return spec_hook.environment_spec(filename) + except EnvironmentSpecPluginNotDetected as e: raise SpecNotFound(e.message) - - return spec_hook.environment_spec(filename) diff --git a/conda/exceptions.py b/conda/exceptions.py index 1c221d357d3..262c945ae9d 100644 --- a/conda/exceptions.py +++ b/conda/exceptions.py @@ -1251,8 +1251,13 @@ def __init__(self, filename: os.PathLike, *args, **kwargs): class EnvironmentFileExtensionNotValid(CondaEnvException): - def __init__(self, filename: os.PathLike, *args, **kwargs): - msg = f"'{filename}' file extension must be one of '.txt', '.yaml' or '.yml'" + def __init__( + self, filename: os.PathLike, extensions: list | None = None, *args, **kwargs + ): + if extensions is None: + extensions = [".txt", ".yaml", ".yml"] + extensions_str = ", ".join(extensions) + msg = f"'{filename}' file extension must be one of: {extensions_str}" self.filename = filename super().__init__(msg, *args, **kwargs) @@ -1272,16 +1277,7 @@ def __init__(self, username: str, packagename: str, *args, **kwargs): super().__init__(msg, *args, **kwargs) -class SpecNotFound(CondaError): - def __init__(self, msg: str, *args, **kwargs): - super().__init__(msg, *args, **kwargs) - - -class PluginError(CondaError): - pass - - -class EnvSpecPluginNotDetected(CondaError): +class EnvironmentSpecPluginNotDetected(CondaError): def __init__(self, name, plugin_names, *args, **kwargs): self.name = name msg = dals( @@ -1295,6 +1291,15 @@ def __init__(self, name, plugin_names, *args, **kwargs): super().__init__(msg, *args, **kwargs) +class SpecNotFound(CondaError): + def __init__(self, msg: str, *args, **kwargs): + super().__init__(msg, *args, **kwargs) + + +class PluginError(CondaError): + pass + + def maybe_raise(error: BaseException, context: Context): if isinstance(error, CondaMultiError): groups = groupby(lambda e: isinstance(e, ClobberError), error.errors) diff --git a/conda/plugins/manager.py b/conda/plugins/manager.py index 4b73a7d1609..6646a35ee6f 100644 --- a/conda/plugins/manager.py +++ b/conda/plugins/manager.py @@ -12,6 +12,7 @@ import functools import logging +import os from importlib.metadata import distributions from inspect import getmodule, isclass from typing import TYPE_CHECKING, overload @@ -24,7 +25,8 @@ from ..deprecations import deprecated from ..exceptions import ( CondaValueError, - EnvSpecPluginNotDetected, + EnvironmentSpecPluginNotDetected, + EnvironmentFileExtensionNotValid, PluginError, ) from . import ( @@ -487,17 +489,42 @@ def load_settings(self) -> None: for name, (parameter, aliases) in self.get_settings().items(): add_plugin_setting(name, parameter, aliases) - def get_environment_specifier_handler( - self, filename: str - ) -> CondaEnvironmentSpecifier: + def get_environment_specifiers(self, filename: str) -> CondaEnvironmentSpecifier: hooks = self.get_hook_results("environment_specifiers") - for hook in hooks: - if hook.environment_spec(filename).can_handle(): - return hook + if filename.startswith("file://"): + filename = filename[len("file://") :] - # raise error if no plugins found that can read the environment file - hook_names = [h.name for h in hooks] - raise EnvSpecPluginNotDetected(name=filename, plugin_names=hook_names) + # Check extensions + hook_extensions = set().union( + *(hook.environment_spec.extensions for hook in hooks) + ) + _, ext = os.path.splitext(filename) + if ext == "" or ext not in hook_extensions: + raise EnvironmentFileExtensionNotValid(filename, extensions=hook_extensions) + + # Find a spec that can handle the filename + capable_hooks = [ + hook for hook in hooks if hook.environment_spec(filename).can_handle() + ] + if len(capable_hooks) == 1: + return capable_hooks[0] + elif len(capable_hooks) > 1: + raise PluginError( + dals( + f""" + Multiple plugins found that can handle the environment file '{filename}': + + {", ".join([hook.name for hook in capable_hooks])} + + Please make sure that you don't have any incompatible plugins installed. + """ + ) + ) + else: + # raise error if no plugins found that can read the environment file + raise EnvironmentSpecPluginNotDetected( + name=filename, plugin_names=[hook.name for hook in hooks] + ) @functools.cache diff --git a/tests/plugins/test_env_specs.py b/tests/plugins/test_env_specs.py index f091b7d28c6..3d96fc09a52 100644 --- a/tests/plugins/test_env_specs.py +++ b/tests/plugins/test_env_specs.py @@ -4,7 +4,7 @@ from conda import plugins from conda.env.env import Environment -from conda.exceptions import EnvSpecPluginNotDetected +from conda.exceptions import EnvironmentSpecPluginNotDetected from conda.plugins.types import CondaEnvironmentSpecifier, EnvironmentSpecBase @@ -57,5 +57,5 @@ def test_raises_an_error_if_file_is_unhandleable(dummy_random_spec_plugin): """ Ensures that our dummy random spec does not recognize non-".random" files """ - with pytest.raises(EnvSpecPluginNotDetected): + with pytest.raises(EnvironmentSpecPluginNotDetected): dummy_random_spec_plugin.get_environment_specifier_handler("test.random-not")