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