_optional.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. from __future__ import annotations
  2. import importlib
  3. import sys
  4. import types
  5. import warnings
  6. from pandas.util.version import Version
  7. # Update install.rst when updating versions!
  8. VERSIONS = {
  9. "bs4": "4.6.0",
  10. "bottleneck": "1.2.1",
  11. "fsspec": "0.7.4",
  12. "fastparquet": "0.4.0",
  13. "gcsfs": "0.6.0",
  14. "lxml.etree": "4.3.0",
  15. "matplotlib": "2.2.3",
  16. "numexpr": "2.7.0",
  17. "odfpy": "1.3.0",
  18. "openpyxl": "3.0.0",
  19. "pandas_gbq": "0.12.0",
  20. "pyarrow": "0.17.0",
  21. "pytest": "6.0",
  22. "pyxlsb": "1.0.6",
  23. "s3fs": "0.4.0",
  24. "scipy": "1.2.0",
  25. "sqlalchemy": "1.3.0",
  26. "tables": "3.5.1",
  27. "tabulate": "0.8.7",
  28. "xarray": "0.12.3",
  29. "xlrd": "1.2.0",
  30. "xlwt": "1.3.0",
  31. "xlsxwriter": "1.0.2",
  32. "numba": "0.46.0",
  33. }
  34. # A mapping from import name to package name (on PyPI) for packages where
  35. # these two names are different.
  36. INSTALL_MAPPING = {
  37. "bs4": "beautifulsoup4",
  38. "bottleneck": "Bottleneck",
  39. "lxml.etree": "lxml",
  40. "odf": "odfpy",
  41. "pandas_gbq": "pandas-gbq",
  42. "sqlalchemy": "SQLAlchemy",
  43. "jinja2": "Jinja2",
  44. }
  45. def get_version(module: types.ModuleType) -> str:
  46. version = getattr(module, "__version__", None)
  47. if version is None:
  48. # xlrd uses a capitalized attribute name
  49. version = getattr(module, "__VERSION__", None)
  50. if version is None:
  51. raise ImportError(f"Can't determine version for {module.__name__}")
  52. return version
  53. def import_optional_dependency(
  54. name: str,
  55. extra: str = "",
  56. errors: str = "raise",
  57. min_version: str | None = None,
  58. ):
  59. """
  60. Import an optional dependency.
  61. By default, if a dependency is missing an ImportError with a nice
  62. message will be raised. If a dependency is present, but too old,
  63. we raise.
  64. Parameters
  65. ----------
  66. name : str
  67. The module name.
  68. extra : str
  69. Additional text to include in the ImportError message.
  70. errors : str {'raise', 'warn', 'ignore'}
  71. What to do when a dependency is not found or its version is too old.
  72. * raise : Raise an ImportError
  73. * warn : Only applicable when a module's version is to old.
  74. Warns that the version is too old and returns None
  75. * ignore: If the module is not installed, return None, otherwise,
  76. return the module, even if the version is too old.
  77. It's expected that users validate the version locally when
  78. using ``errors="ignore"`` (see. ``io/html.py``)
  79. min_version : str, default None
  80. Specify a minimum version that is different from the global pandas
  81. minimum version required.
  82. Returns
  83. -------
  84. maybe_module : Optional[ModuleType]
  85. The imported module, when found and the version is correct.
  86. None is returned when the package is not found and `errors`
  87. is False, or when the package's version is too old and `errors`
  88. is ``'warn'``.
  89. """
  90. assert errors in {"warn", "raise", "ignore"}
  91. package_name = INSTALL_MAPPING.get(name)
  92. install_name = package_name if package_name is not None else name
  93. msg = (
  94. f"Missing optional dependency '{install_name}'. {extra} "
  95. f"Use pip or conda to install {install_name}."
  96. )
  97. try:
  98. module = importlib.import_module(name)
  99. except ImportError:
  100. if errors == "raise":
  101. raise ImportError(msg) from None
  102. else:
  103. return None
  104. # Handle submodules: if we have submodule, grab parent module from sys.modules
  105. parent = name.split(".")[0]
  106. if parent != name:
  107. install_name = parent
  108. module_to_get = sys.modules[install_name]
  109. else:
  110. module_to_get = module
  111. minimum_version = min_version if min_version is not None else VERSIONS.get(parent)
  112. if minimum_version:
  113. version = get_version(module_to_get)
  114. if Version(version) < Version(minimum_version):
  115. msg = (
  116. f"Pandas requires version '{minimum_version}' or newer of '{parent}' "
  117. f"(version '{version}' currently installed)."
  118. )
  119. if errors == "warn":
  120. warnings.warn(msg, UserWarning)
  121. return None
  122. elif errors == "raise":
  123. raise ImportError(msg)
  124. return module