_optional.py 5.0 KB

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