1from types import TracebackType
2from typing import Optional
3from typing import Tuple
4from typing import Type
5
6
7class _DeferredImportExceptionContextManager(object):
8    """Context manager to defer exceptions from imports.
9
10    Catches :exc:`ImportError` and :exc:`SyntaxError`.
11    If any exception is caught, this class raises an :exc:`ImportError` when being checked.
12
13    """
14
15    def __init__(self) -> None:
16        self._deferred: Optional[Tuple[Exception, str]] = None
17
18    def __enter__(self) -> "_DeferredImportExceptionContextManager":
19        """Enter the context manager.
20
21        Returns:
22            Itself.
23
24        """
25        return self
26
27    def __exit__(
28        self,
29        exc_type: Optional[Type[Exception]],
30        exc_value: Optional[Exception],
31        traceback: Optional[TracebackType],
32    ) -> Optional[bool]:
33        """Exit the context manager.
34
35        Args:
36            exc_type:
37                Raised exception type. :obj:`None` if nothing is raised.
38            exc_value:
39                Raised exception object. :obj:`None` if nothing is raised.
40            traceback:
41                Associated traceback. :obj:`None` if nothing is raised.
42
43        Returns:
44            :obj:`None` if nothing is deferred, otherwise :obj:`True`.
45            :obj:`True` will suppress any exceptions avoiding them from propagating.
46
47        """
48        if isinstance(exc_value, (ImportError, SyntaxError)):
49            if isinstance(exc_value, ImportError):
50                message = (
51                    "Tried to import '{}' but failed. Please make sure that the package is "
52                    "installed correctly to use this feature. Actual error: {}."
53                ).format(exc_value.name, exc_value)
54            elif isinstance(exc_value, SyntaxError):
55                message = (
56                    "Tried to import a package but failed due to a syntax error in {}. Please "
57                    "make sure that the Python version is correct to use this feature. Actual "
58                    "error: {}."
59                ).format(exc_value.filename, exc_value)
60            else:
61                assert False
62
63            self._deferred = (exc_value, message)
64            return True
65        return None
66
67    def is_successful(self) -> bool:
68        """Return whether the context manager has caught any exceptions.
69
70        Returns:
71            :obj:`True` if no exceptions are caught, :obj:`False` otherwise.
72
73        """
74        return self._deferred is None
75
76    def check(self) -> None:
77        """Check whether the context manger has caught any exceptions.
78
79        Raises:
80            :exc:`ImportError`:
81                If any exception was caught from the caught exception.
82
83        """
84        if self._deferred is not None:
85            exc_value, message = self._deferred
86            raise ImportError(message) from exc_value
87
88
89def try_import() -> _DeferredImportExceptionContextManager:
90    """Create a context manager that can wrap imports of optional packages to defer exceptions.
91
92    Returns:
93        Deferred import context manager.
94
95    """
96    return _DeferredImportExceptionContextManager()
97