_optional.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. from __future__ import annotations
  2. import importlib
  3. import sys
  4. from typing import (
  5. TYPE_CHECKING,
  6. Literal,
  7. overload,
  8. )
  9. import warnings
  10. from pandas.util._exceptions import find_stack_level
  11. from pandas.util.version import Version
  12. if TYPE_CHECKING:
  13. import types
  14. # Update install.rst, actions-311-minimum_versions.yaml,
  15. # deps_minimum.toml & pyproject.toml when updating versions!
  16. VERSIONS = {
  17. "adbc-driver-postgresql": "1.2.0",
  18. "adbc-driver-sqlite": "1.2.0",
  19. "bs4": "4.12.3",
  20. "bottleneck": "1.4.2",
  21. "fastparquet": "2024.11.0",
  22. "fsspec": "2024.10.0",
  23. "html5lib": "1.1",
  24. "hypothesis": "6.116.0",
  25. "gcsfs": "2024.10.0",
  26. "jinja2": "3.1.5",
  27. "lxml.etree": "5.3.0",
  28. "matplotlib": "3.9.3",
  29. "numba": "0.60.0",
  30. "numexpr": "2.10.2",
  31. "odfpy": "1.4.1",
  32. "openpyxl": "3.1.5",
  33. "psycopg2": "2.9.10", # (dt dec pq3 ext lo64)
  34. "pymysql": "1.1.1",
  35. "pyarrow": "13.0.0",
  36. "pyiceberg": "0.8.1",
  37. "pyreadstat": "1.2.8",
  38. "pytest": "8.3.4",
  39. "python-calamine": "0.3.0",
  40. "pytz": "2024.2",
  41. "pyxlsb": "1.0.10",
  42. "s3fs": "2024.10.0",
  43. "scipy": "1.14.1",
  44. "sqlalchemy": "2.0.36",
  45. "tables": "3.10.1",
  46. "tabulate": "0.9.0",
  47. "xarray": "2024.10.0",
  48. "xlrd": "2.0.1",
  49. "xlsxwriter": "3.2.0",
  50. "zstandard": "0.23.0",
  51. "qtpy": "2.4.2",
  52. "pyqt5": "5.15.9",
  53. }
  54. # A mapping from import name to package name (on PyPI) for packages where
  55. # these two names are different.
  56. INSTALL_MAPPING = {
  57. "bs4": "beautifulsoup4",
  58. "bottleneck": "Bottleneck",
  59. "jinja2": "Jinja2",
  60. "lxml.etree": "lxml",
  61. "odf": "odfpy",
  62. "python_calamine": "python-calamine",
  63. "sqlalchemy": "SQLAlchemy",
  64. "tables": "pytables",
  65. }
  66. def get_version(module: types.ModuleType) -> str:
  67. version = getattr(module, "__version__", None)
  68. if version is None:
  69. raise ImportError(f"Can't determine version for {module.__name__}")
  70. if module.__name__ == "psycopg2":
  71. # psycopg2 appends " (dt dec pq3 ext lo64)" to it's version
  72. version = version.split()[0]
  73. return version
  74. @overload
  75. def import_optional_dependency(
  76. name: str,
  77. extra: str = ...,
  78. min_version: str | None = ...,
  79. *,
  80. errors: Literal["raise"] = ...,
  81. ) -> types.ModuleType: ...
  82. @overload
  83. def import_optional_dependency(
  84. name: str,
  85. extra: str = ...,
  86. min_version: str | None = ...,
  87. *,
  88. errors: Literal["warn", "ignore"],
  89. ) -> types.ModuleType | None: ...
  90. def import_optional_dependency(
  91. name: str,
  92. extra: str = "",
  93. min_version: str | None = None,
  94. *,
  95. errors: Literal["raise", "warn", "ignore"] = "raise",
  96. ) -> types.ModuleType | None:
  97. """
  98. Import an optional dependency.
  99. By default, if a dependency is missing an ImportError with a nice
  100. message will be raised. If a dependency is present, but too old,
  101. we raise.
  102. Parameters
  103. ----------
  104. name : str
  105. The module name.
  106. extra : str
  107. Additional text to include in the ImportError message.
  108. errors : str {'raise', 'warn', 'ignore'}
  109. What to do when a dependency is not found or its version is too old.
  110. * raise : Raise an ImportError
  111. * warn : Only applicable when a module's version is to old.
  112. Warns that the version is too old and returns None
  113. * ignore: If the module is not installed, return None, otherwise,
  114. return the module, even if the version is too old.
  115. It's expected that users validate the version locally when
  116. using ``errors="ignore"`` (see. ``io/html.py``)
  117. min_version : str, default None
  118. Specify a minimum version that is different from the global pandas
  119. minimum version required.
  120. Returns
  121. -------
  122. maybe_module : Optional[ModuleType]
  123. The imported module, when found and the version is correct.
  124. None is returned when the package is not found and `errors`
  125. is False, or when the package's version is too old and `errors`
  126. is ``'warn'`` or ``'ignore'``.
  127. """
  128. assert errors in {"warn", "raise", "ignore"}
  129. package_name = INSTALL_MAPPING.get(name)
  130. install_name = package_name if package_name is not None else name
  131. msg = (
  132. f"`Import {install_name}` failed. {extra} "
  133. f"Use pip or conda to install the {install_name} package."
  134. )
  135. try:
  136. module = importlib.import_module(name)
  137. except ImportError as err:
  138. if errors == "raise":
  139. raise ImportError(msg) from err
  140. return None
  141. # Handle submodules: if we have submodule, grab parent module from sys.modules
  142. parent = name.split(".")[0]
  143. if parent != name:
  144. install_name = parent
  145. module_to_get = sys.modules[install_name]
  146. else:
  147. module_to_get = module
  148. minimum_version = min_version if min_version is not None else VERSIONS.get(parent)
  149. if minimum_version:
  150. version = get_version(module_to_get)
  151. if version and Version(version) < Version(minimum_version):
  152. msg = (
  153. f"Pandas requires version '{minimum_version}' or newer of '{parent}' "
  154. f"(version '{version}' currently installed)."
  155. )
  156. if errors == "warn":
  157. warnings.warn(
  158. msg,
  159. UserWarning,
  160. stacklevel=find_stack_level(),
  161. )
  162. return None
  163. elif errors == "raise":
  164. raise ImportError(msg)
  165. else:
  166. return None
  167. return module