_optional.py 5.2 KB

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