Coverage for colour/utilities/requirements.py: 100%
49 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 23:01 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 23:01 +1300
1"""
2Requirements Utilities
3======================
5Define utilities for checking the availability of optional dependencies.
6"""
8from __future__ import annotations
10import functools
11import shutil
12import subprocess
13import typing
15if typing.TYPE_CHECKING:
16 from colour.hints import (
17 Any,
18 Callable,
19 Literal,
20 )
22from colour.utilities import CanonicalMapping
24__author__ = "Colour Developers"
25__copyright__ = "Copyright 2013 Colour Developers"
26__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause"
27__maintainer__ = "Colour Developers"
28__email__ = "colour-developers@colour-science.org"
29__status__ = "Production"
31__all__ = [
32 "is_ctlrender_installed",
33 "is_imageio_installed",
34 "is_openimageio_installed",
35 "is_matplotlib_installed",
36 "is_networkx_installed",
37 "is_opencolorio_installed",
38 "is_pandas_installed",
39 "is_pydot_installed",
40 "is_scipy_installed",
41 "is_tqdm_installed",
42 "is_trimesh_installed",
43 "is_xxhash_installed",
44 "REQUIREMENTS_TO_CALLABLE",
45 "required",
46]
49def is_ctlrender_installed(raise_exception: bool = False) -> bool:
50 """
51 Determine whether *ctlrender* is installed and available.
53 Parameters
54 ----------
55 raise_exception
56 Whether to raise an exception if *ctlrender* is unavailable.
58 Returns
59 -------
60 :class:`bool`
61 Whether *ctlrender* is installed.
63 Raises
64 ------
65 :class:`ImportError`
66 If *ctlrender* is not installed.
67 """
69 try: # pragma: no cover
70 stdout = subprocess.run(
71 ["ctlrender", "-help"], # noqa: S607
72 capture_output=True,
73 check=False,
74 ).stdout.decode("utf-8")
76 if "transforms an image using one or more CTL scripts" not in stdout:
77 raise FileNotFoundError # noqa: TRY301
78 except FileNotFoundError as exception: # pragma: no cover
79 if raise_exception:
80 error = (
81 '"ctlrender" related API features are not available: '
82 f'"{exception}".\nSee the installation guide for more information: '
83 "https://www.colour-science.org/installation-guide/"
84 )
86 raise FileNotFoundError(error) from exception
88 return False
89 else:
90 return True
93def is_imageio_installed(raise_exception: bool = False) -> bool:
94 """
95 Determine whether *Imageio* is installed and available.
97 Parameters
98 ----------
99 raise_exception
100 Whether to raise an exception if *Imageio* is unavailable.
102 Returns
103 -------
104 :class:`bool`
105 Whether *Imageio* is installed.
107 Raises
108 ------
109 :class:`ImportError`
110 If *Imageio* is not installed.
111 """
113 try: # pragma: no cover
114 import imageio # noqa: F401, PLC0415
115 except ImportError as exception: # pragma: no cover
116 if raise_exception:
117 error = (
118 '"Imageio" related API features are not available: '
119 f'"{exception}".\nSee the installation guide for more information: '
120 "https://www.colour-science.org/installation-guide/"
121 )
123 raise ImportError(error) from exception
125 return False
126 else:
127 return True
130def is_openimageio_installed(raise_exception: bool = False) -> bool:
131 """
132 Determine whether *OpenImageIO* is installed and available.
134 Parameters
135 ----------
136 raise_exception
137 Whether to raise an exception if *OpenImageIO* is unavailable.
139 Returns
140 -------
141 :class:`bool`
142 Whether *OpenImageIO* is installed.
144 Raises
145 ------
146 :class:`ImportError`
147 If *OpenImageIO* is not installed.
148 """
150 try: # pragma: no cover
151 import OpenImageIO # noqa: F401, PLC0415
152 except ImportError as exception: # pragma: no cover
153 if raise_exception:
154 error = (
155 '"OpenImageIO" related API features are not available: '
156 f'"{exception}".\nSee the installation guide for more information: '
157 "https://www.colour-science.org/installation-guide/"
158 )
160 raise ImportError(error) from exception
162 return False
163 else:
164 return True
167def is_matplotlib_installed(raise_exception: bool = False) -> bool:
168 """
169 Determine whether *Matplotlib* is installed and available.
171 Parameters
172 ----------
173 raise_exception
174 Whether to raise an exception if *Matplotlib* is unavailable.
176 Returns
177 -------
178 :class:`bool`
179 Whether *Matplotlib* is installed.
181 Raises
182 ------
183 :class:`ImportError`
184 If *Matplotlib* is not installed.
185 """
187 try: # pragma: no cover
188 import matplotlib as mpl # noqa: F401, PLC0415
189 except ImportError as exception: # pragma: no cover
190 if raise_exception:
191 error = (
192 '"Matplotlib" related API features are not available: '
193 f'"{exception}".\nSee the installation guide for more information: '
194 "https://www.colour-science.org/installation-guide/"
195 )
197 raise ImportError(error) from exception
199 return False
200 else:
201 return True
204def is_networkx_installed(raise_exception: bool = False) -> bool:
205 """
206 Determine whether *NetworkX* is installed and available.
208 Parameters
209 ----------
210 raise_exception
211 Whether to raise an exception if *NetworkX* is unavailable.
213 Returns
214 -------
215 :class:`bool`
216 Whether *NetworkX* is installed.
218 Raises
219 ------
220 :class:`ImportError`
221 If *NetworkX* is not installed.
222 """
224 try: # pragma: no cover
225 import networkx as nx # noqa: F401, PLC0415
226 except ImportError as exception: # pragma: no cover
227 if raise_exception:
228 error = (
229 '"NetworkX" related API features, e.g., the automatic colour '
230 f'conversion graph, are not available: "{exception}".\nPlease refer '
231 "to the installation guide for more information: "
232 "https://www.colour-science.org/installation-guide/"
233 )
235 raise ImportError(error) from exception
237 return False
238 else:
239 return True
242def is_opencolorio_installed(raise_exception: bool = False) -> bool:
243 """
244 Determine whether *OpenColorIO* is installed and available.
246 Parameters
247 ----------
248 raise_exception
249 Whether to raise an exception if *OpenColorIO* is unavailable.
251 Returns
252 -------
253 :class:`bool`
254 Whether *OpenColorIO* is installed.
256 Raises
257 ------
258 :class:`ImportError`
259 If *OpenColorIO* is not installed.
260 """
262 try: # pragma: no cover
263 import PyOpenColorIO # noqa: F401, PLC0415
264 except ImportError as exception: # pragma: no cover
265 if raise_exception:
266 error = (
267 '"OpenColorIO" related API features are not available: '
268 f'"{exception}".\nSee the installation guide for more information: '
269 "https://www.colour-science.org/installation-guide/"
270 )
272 raise ImportError(error) from exception
274 return False
275 else:
276 return True
279def is_pandas_installed(raise_exception: bool = False) -> bool:
280 """
281 Determine whether *Pandas* is installed and available.
283 Parameters
284 ----------
285 raise_exception
286 Whether to raise an exception if *Pandas* is unavailable.
288 Returns
289 -------
290 :class:`bool`
291 Whether *Pandas* is installed.
293 Raises
294 ------
295 :class:`ImportError`
296 If *Pandas* is not installed.
297 """
299 try: # pragma: no cover
300 import pandas # noqa: F401, ICN001, PLC0415
301 except ImportError as exception: # pragma: no cover
302 if raise_exception:
303 error = (
304 f'"Pandas" related API features are not available: "{exception}".\n'
305 "See the installation guide for more information: "
306 "https://www.colour-science.org/installation-guide/"
307 )
309 raise ImportError(error) from exception
311 return False
312 else:
313 return True
316def is_pydot_installed(raise_exception: bool = False) -> bool:
317 """
318 Determine whether *Pydot* is installed and available.
320 The presence of *Graphviz* will also be tested.
322 Parameters
323 ----------
324 raise_exception
325 Whether to raise an exception if *Pydot* is unavailable.
327 Returns
328 -------
329 :class:`bool`
330 Whether *Pydot* is installed.
332 Raises
333 ------
334 :class:`ImportError`
335 If *Pydot* is not installed.
336 """
338 try: # pragma: no cover
339 import pydot # noqa: F401, PLC0415
341 except ImportError as exception: # pragma: no cover
342 if raise_exception:
343 error = (
344 '"Pydot" related API features are not available: '
345 f'"{exception}".\nSee the installation guide for more information: '
346 "https://www.colour-science.org/installation-guide/"
347 )
349 raise ImportError(error) from exception
351 if shutil.which("fdp") is not None:
352 return True
354 if raise_exception: # pragma: no cover
355 error = (
356 '"Graphviz" is not installed, "Pydot" related API features '
357 "are not available!"
358 "\nSee the installation guide for more information: "
359 "https://www.colour-science.org/installation-guide/"
360 )
362 raise RuntimeError(error)
364 return False # pragma: no cover
367def is_scipy_installed(raise_exception: bool = False) -> bool:
368 """
369 Determine whether *SciPy* is installed and available.
371 Parameters
372 ----------
373 raise_exception
374 Whether to raise an exception if *SciPy* is unavailable.
376 Returns
377 -------
378 :class:`bool`
379 Whether *SciPy* is installed.
381 Raises
382 ------
383 :class:`ImportError`
384 If *SciPy* is not installed.
385 """
387 try: # pragma: no cover
388 import scipy # noqa: F401, PLC0415
389 except ImportError as exception: # pragma: no cover
390 if raise_exception:
391 error = (
392 '"SciPy" related API features are not available: '
393 f'"{exception}".\nSee the installation guide for more information: '
394 "https://www.colour-science.org/installation-guide/"
395 )
397 raise ImportError(error) from exception
399 return False
400 else:
401 return True
404def is_tqdm_installed(raise_exception: bool = False) -> bool:
405 """
406 Determine whether *tqdm* is installed and available.
408 Parameters
409 ----------
410 raise_exception
411 Whether to raise an exception if *tqdm* is unavailable.
413 Returns
414 -------
415 :class:`bool`
416 Whether *tqdm* is installed.
418 Raises
419 ------
420 :class:`ImportError`
421 If *tqdm* is not installed.
422 """
424 try: # pragma: no cover
425 import tqdm # noqa: F401, PLC0415
426 except ImportError as exception: # pragma: no cover
427 if raise_exception:
428 error = (
429 f'"tqdm" related API features are not available: "{exception}".\n'
430 "See the installation guide for more information: "
431 "https://www.colour-science.org/installation-guide/"
432 )
434 raise ImportError(error) from exception
436 return False
437 else:
438 return True
441def is_trimesh_installed(raise_exception: bool = False) -> bool:
442 """
443 Determine whether *Trimesh* is installed and available.
445 Parameters
446 ----------
447 raise_exception
448 Whether to raise an exception if *Trimesh* is unavailable.
450 Returns
451 -------
452 :class:`bool`
453 Whether *Trimesh* is installed.
455 Raises
456 ------
457 :class:`ImportError`
458 If *Trimesh* is not installed.
459 """
461 try: # pragma: no cover
462 import trimesh # noqa: F401, PLC0415
463 except ImportError as exception: # pragma: no cover
464 if raise_exception:
465 error = (
466 '"Trimesh" related API features are not available: '
467 f'"{exception}".\nSee the installation guide for more information: '
468 "https://www.colour-science.org/installation-guide/"
469 )
471 raise ImportError(error) from exception
473 return False
474 else:
475 return True
478def is_xxhash_installed(raise_exception: bool = False) -> bool:
479 """
480 Determine whether *xxhash* is installed and available.
482 Parameters
483 ----------
484 raise_exception
485 Whether to raise an exception if *xxhash* is unavailable.
487 Returns
488 -------
489 :class:`bool`
490 Whether *xxhash* is installed.
492 Raises
493 ------
494 :class:`ImportError`
495 If *xxhash* is not installed.
496 """
498 try: # pragma: no cover
499 import xxhash # noqa: F401, PLC0415
500 except ImportError as exception: # pragma: no cover
501 if raise_exception:
502 error = (
503 '"xxhash" related API features are not available: '
504 f'"{exception}".\nSee the installation guide for more information: '
505 "https://www.colour-science.org/installation-guide/"
506 )
508 raise ImportError(error) from exception
510 return False
511 else:
512 return True
515REQUIREMENTS_TO_CALLABLE: CanonicalMapping = CanonicalMapping(
516 {
517 "ctlrender": is_ctlrender_installed,
518 "Imageio": is_imageio_installed,
519 "OpenImageIO": is_openimageio_installed,
520 "Matplotlib": is_matplotlib_installed,
521 "NetworkX": is_networkx_installed,
522 "OpenColorIO": is_opencolorio_installed,
523 "Pandas": is_pandas_installed,
524 "Pydot": is_pydot_installed,
525 "SciPy": is_scipy_installed,
526 "tqdm": is_tqdm_installed,
527 "trimesh": is_trimesh_installed,
528 "xxhash": is_xxhash_installed,
529 }
530)
531"""
532Mapping of requirements to their respective callables.
533"""
536def required(
537 *requirements: Literal[
538 "ctlrender",
539 "Imageio",
540 "OpenImageIO",
541 "Matplotlib",
542 "NetworkX",
543 "OpenColorIO",
544 "Pandas",
545 "Pydot",
546 "SciPy",
547 "tqdm",
548 "trimesh",
549 "xxhash",
550 ],
551) -> Callable:
552 """
553 Check whether specified ancillary package requirements are satisfied
554 and decorate the function accordingly.
556 Other Parameters
557 ----------------
558 requirements
559 Package requirements to check for satisfaction.
561 Returns
562 -------
563 Callable
564 Decorated function that validates package availability.
565 """
567 def wrapper(function: Callable) -> Callable:
568 """Wrap specified function wrapper."""
570 @functools.wraps(function)
571 def wrapped(*args: Any, **kwargs: Any) -> Any:
572 """Wrap specified function."""
574 for requirement in requirements:
575 REQUIREMENTS_TO_CALLABLE[requirement](raise_exception=True)
577 return function(*args, **kwargs)
579 return wrapped
581 return wrapper