# -*- coding: utf-8 -*- """Core interfaces for Conda error solvers.""" from __future__ import annotations __all__ = ['Solver', 'SimpleSolver', 'SolvedError', 'SolverCollection', 'POOL'] import abc import typing from anaconda_navigator.utils import solvers as common_solvers if typing.TYPE_CHECKING: import typing_extensions from .. import types as conda_types TConError = typing.TypeVar('TConError', bound='conda_types.CondaErrorOutput', contravariant=True) class SolverFunction(typing_extensions.Protocol[TConError]): # pylint: disable=too-few-public-methods """General form of a function, that could be used as a solver.""" def __call__(self, error: TConError) -> str: """ Solve Conda error. :param error: Description of the error to solve. :return: Message about what was fixed. """ TInvError = typing.TypeVar('TInvError', bound='conda_types.CondaErrorOutput') class Solver(typing.Generic[TInvError], metaclass=abc.ABCMeta): # pylint: disable=too-few-public-methods """ Abstract base for all Conda error solvers. :param kwargs: Exact values to search for in the error body. """ __slots__ = ('__arguments',) def __init__(self, **kwargs: typing.Any) -> None: """Initialize new :class:`~ErrorSolver` instance.""" self.__arguments: 'typing_extensions.Final[typing.Mapping[str, typing.Any]]' = dict(kwargs) def solve(self, error: 'conda_types.CondaErrorOutput') -> typing.Optional[str]: """ Attempt solution of the `error`. :param error: Description of the error to solve. :return: Message about what was fixed. Might be :code:`None` if the solver is not applicable. """ if self._applicable(error=error): return self._solve(error=typing.cast(TInvError, error)) return None def _applicable(self, error: 'conda_types.CondaErrorOutput') -> bool: """ Check if this solver could be used to solve the `error`. This method might be overwritten to add custom checks. :param error: Description of the error to solve. :return: Check result. """ return all( error.get(key, None) == value for key, value in self.__arguments.items() ) @abc.abstractmethod def _solve(self, error: TInvError) -> str: """ Solve Conda error. :param error: Description of the error to solve. :return: Message about what was fixed. """ class SimpleSolver(typing.Generic[TInvError], Solver[TInvError]): # pylint: disable=too-few-public-methods """ Custom :class:`~ErrorSolver`, which uses external function to solve an error. :param __function__: Function to use for error solving. :param kwargs: Exact values to search for in the error body. """ __slots__ = ('__function',) def __init__(self, __function__: 'SolverFunction[TInvError]', **kwargs: typing.Any) -> None: """Initialize new :class:`~SimpleErrorSolver` instance.""" super().__init__(**kwargs) self.__function: 'typing_extensions.Final[SolverFunction[TInvError]]' = __function__ def _solve(self, error: TInvError) -> str: """ Solve Conda error. :param error: Description of the error to solve. :return: Message about what was fixed. """ return self.__function(error=error) class SolvedError(typing.NamedTuple): """ Record for the error, that was solved. :param error: Description of the error that was solved. :param message: Message about what was fixed. """ error: 'conda_types.CondaErrorOutput' message: str class SolverCollection(common_solvers.SolverCollection[Solver[typing.Any]]): """Collection of solvers.""" __slots__ = ('__errors',) def __init__(self) -> None: """Initialize new :class:`~SolverCollection` instance.""" super().__init__() self.__errors: 'typing_extensions.Final[typing.List[SolvedError]]' = [] def errors(self) -> typing.Iterator[SolvedError]: """ Iterate through errors that were solved by solvers from this pool. Each error description is removed as soon as it is retrieved with this method. """ while self.__errors: yield self.__errors.pop(0) @typing.overload def register_function( self, function: 'SolverFunction[TInvError]', *, filters: typing.Optional[typing.Mapping[str, typing.Any]] = None, tags: typing.Union[str, typing.Iterable[str]] = (), unique_tags: typing.Union[str, typing.Iterable[str]] = (), ) -> 'SolverFunction[TInvError]': """Register function as a solver.""" @typing.overload def register_function( self, function: None = None, *, filters: typing.Optional[typing.Mapping[str, typing.Any]] = None, tags: typing.Union[str, typing.Iterable[str]] = (), unique_tags: typing.Union[str, typing.Iterable[str]] = (), ) -> typing.Callable[['SolverFunction[TInvError]'], 'SolverFunction[TInvError]']: """Register function as a solver.""" def register_function(self, function=None, filters=None, tags=(), unique_tags=()): """ Register new solver. This method can be used as a decorator, as well as direct function. See :meth:`~SolverCollection.register` for more details. :param function: Function that should be registered. :param filters: Values that should be in the error body in order to apply current fix. :param tags: Optional collection of common tags. :param unique_tags: Optional collection of tags, that should be unique only for this particular solver. """ if filters is None: filters = {} def wrapper(item: 'SolverFunction[TInvError]') -> 'SolverFunction[TInvError]': self.register( solver=SimpleSolver[TInvError](item, **filters), tags=tags, unique_tags=unique_tags, ) return item if function is None: return wrapper return wrapper(function) def solve( self, error: 'conda_types.CondaErrorOutput', *, tags: typing.Union[None, str, typing.Iterable[str]] = None, ) -> typing.Optional[SolvedError]: """ Detect and solve issues for the `context`. :param error: Conda error to solve. :param tags: Limit to issue solvers with specific tags. :return: Iterator of details about solved issues. """ solver_record: common_solvers.SolverRecord[Solver[typing.Any]] for solver_record in self.only(tags=tags): message: typing.Optional[str] = solver_record.solver.solve(error) if message is not None: solved_error: SolvedError = SolvedError(error=error, message=message) self.__errors.append(solved_error) return solved_error return None POOL: 'typing_extensions.Final[SolverCollection]' = SolverCollection()