|
1 | | -""" |
2 | | -Compatibility abstractions for optional dependencies. |
3 | | -
|
4 | | -This module centralizes fallback logic for safely importing optional dependencies |
5 | | -like ``psutil``, ``opentelemetry``, and TOML parsers. It provides standardized |
6 | | -access points for these modules, avoiding scattered ``try-except`` blocks across |
7 | | -the codebase. Maintainers should import optional dependencies from this module |
8 | | -rather than attempting direct imports elsewhere. |
| 1 | +"""Compatibility abstractions for optional dependencies and packaging errors. |
| 2 | +
|
| 3 | +This module centralizes fallback logic for safely importing optional |
| 4 | +dependencies such as ``psutil``, ``opentelemetry``, and TOML parsers. |
| 5 | +By providing standardized module-level variables that resolve to either |
| 6 | +the imported module or ``None``, it prevents scattered ``try-except`` |
| 7 | +blocks and import errors across the library. |
| 8 | +
|
| 9 | +In addition to optional dependencies, it abstracts differences in |
| 10 | +packaging-related setup exception classes. It resolves and exposes |
| 11 | +tools to check or raise setup errors compatible with both legacy |
| 12 | +``distutils`` and modern ``setuptools``. |
9 | 13 | """ |
10 | 14 |
|
11 | 15 | from __future__ import annotations |
12 | 16 |
|
13 | 17 | import importlib |
14 | 18 | import types |
15 | | -from typing import Annotated |
| 19 | +from typing import Annotated, NoReturn |
16 | 20 |
|
17 | | -__all__ = ["DistutilsSetupError", "maturin", "opentelemetry_trace", "psutil", "tomllib"] |
| 21 | +__all__ = [ |
| 22 | + "is_distutils_setup_error", |
| 23 | + "maturin", |
| 24 | + "opentelemetry_trace", |
| 25 | + "psutil", |
| 26 | + "raise_distutils_setup_error", |
| 27 | + "tomllib", |
| 28 | +] |
18 | 29 |
|
19 | 30 | _maturin: types.ModuleType | None = None |
20 | 31 | try: |
|
45 | 56 | except ImportError: |
46 | 57 | _psutil = None |
47 | 58 |
|
48 | | -_DistutilsSetupError: type[Exception] |
49 | | -try: |
50 | | - import setuptools.errors |
51 | 59 |
|
52 | | - _DistutilsSetupError = setuptools.errors.SetupError |
53 | | -except ImportError: |
| 60 | +def raise_distutils_setup_error( |
| 61 | + message: str, from_exception: Exception | None = None |
| 62 | +) -> NoReturn: |
| 63 | + """Dynamically resolve and raise the appropriate setup error class. |
| 64 | +
|
| 65 | + Resolves the setup error class by attempting to import |
| 66 | + ``setuptools.errors.SetupError``, falling back to |
| 67 | + ``distutils.errors.DistutilsSetupError``, and ultimately using the |
| 68 | + builtin ``Exception`` if neither is available. This avoids import-time |
| 69 | + monkeypatching race conditions with ``setuptools``. |
| 70 | +
|
| 71 | + Example: |
| 72 | + >>> try: |
| 73 | + ... # Perform some custom setup or build operation |
| 74 | + ... pass |
| 75 | + ... except Exception as err: |
| 76 | + ... raise_distutils_setup_error("Setup failed", from_exception=err) |
| 77 | +
|
| 78 | + :param message: The descriptive error message to associate with the |
| 79 | + raised exception. |
| 80 | + :param from_exception: An optional exception instance to chain via |
| 81 | + ``raise ... from``. |
| 82 | + :raises Exception: The resolved setup error or a standard base |
| 83 | + ``Exception``. |
| 84 | + """ |
54 | 85 | try: |
55 | | - import distutils.errors |
| 86 | + import setuptools.errors # noqa: PLC0415 |
56 | 87 |
|
57 | | - _DistutilsSetupError = distutils.errors.DistutilsSetupError |
| 88 | + exc_class = setuptools.errors.SetupError |
58 | 89 | except ImportError: |
59 | | - _DistutilsSetupError = Exception |
| 90 | + try: |
| 91 | + import distutils.errors # noqa: PLC0415 |
| 92 | + |
| 93 | + exc_class = distutils.errors.DistutilsSetupError |
| 94 | + except ImportError: |
| 95 | + exc_class = Exception |
| 96 | + |
| 97 | + if from_exception is not None: |
| 98 | + raise exc_class(message) from from_exception |
| 99 | + raise exc_class(message) |
| 100 | + |
| 101 | + |
| 102 | +def is_distutils_setup_error(error: Exception) -> bool: |
| 103 | + """Verify if an exception instance matches the resolved SetupError. |
| 104 | +
|
| 105 | + Checks the given exception against ``setuptools.errors.SetupError`` and |
| 106 | + ``distutils.errors.DistutilsSetupError`` if their respective modules |
| 107 | + can be imported. |
| 108 | +
|
| 109 | + Example: |
| 110 | + >>> try: |
| 111 | + ... # Run packaging tool logic |
| 112 | + ... pass |
| 113 | + ... except Exception as err: |
| 114 | + ... if is_distutils_setup_error(err): |
| 115 | + ... print("Packaging setup error detected") |
| 116 | +
|
| 117 | + :param error: The exception instance to evaluate. |
| 118 | + :returns: ``True`` if the error is a setup error instance; otherwise |
| 119 | + ``False``. |
| 120 | + """ |
| 121 | + try: |
| 122 | + import setuptools.errors # noqa: PLC0415 |
| 123 | + |
| 124 | + if isinstance(error, setuptools.errors.SetupError): |
| 125 | + return True |
| 126 | + except ImportError: |
| 127 | + pass |
| 128 | + |
| 129 | + try: |
| 130 | + import distutils.errors # noqa: PLC0415 |
| 131 | + |
| 132 | + if isinstance(error, distutils.errors.DistutilsSetupError): |
| 133 | + return True |
| 134 | + except ImportError: |
| 135 | + pass |
| 136 | + |
| 137 | + return False |
| 138 | + |
60 | 139 |
|
61 | 140 | maturin: Annotated[ |
62 | 141 | types.ModuleType | None, |
|
83 | 162 | "``pyproject.toml``. Provides the standard library ``tomllib``, the " |
84 | 163 | "third-party ``tomli``, or ``None``.", |
85 | 164 | ] = _tomllib |
86 | | - |
87 | | -DistutilsSetupError: Annotated[ |
88 | | - type[Exception], |
89 | | - "The SetupError exception class resolved from setuptools or distutils.", |
90 | | -] = _DistutilsSetupError |
|
0 commit comments