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