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

1""" 

2Requirements Utilities 

3====================== 

4 

5Define utilities for checking the availability of optional dependencies. 

6""" 

7 

8from __future__ import annotations 

9 

10import functools 

11import shutil 

12import subprocess 

13import typing 

14 

15if typing.TYPE_CHECKING: 

16 from colour.hints import ( 

17 Any, 

18 Callable, 

19 Literal, 

20 ) 

21 

22from colour.utilities import CanonicalMapping 

23 

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" 

30 

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] 

47 

48 

49def is_ctlrender_installed(raise_exception: bool = False) -> bool: 

50 """ 

51 Determine whether *ctlrender* is installed and available. 

52 

53 Parameters 

54 ---------- 

55 raise_exception 

56 Whether to raise an exception if *ctlrender* is unavailable. 

57 

58 Returns 

59 ------- 

60 :class:`bool` 

61 Whether *ctlrender* is installed. 

62 

63 Raises 

64 ------ 

65 :class:`ImportError` 

66 If *ctlrender* is not installed. 

67 """ 

68 

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") 

75 

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 ) 

85 

86 raise FileNotFoundError(error) from exception 

87 

88 return False 

89 else: 

90 return True 

91 

92 

93def is_imageio_installed(raise_exception: bool = False) -> bool: 

94 """ 

95 Determine whether *Imageio* is installed and available. 

96 

97 Parameters 

98 ---------- 

99 raise_exception 

100 Whether to raise an exception if *Imageio* is unavailable. 

101 

102 Returns 

103 ------- 

104 :class:`bool` 

105 Whether *Imageio* is installed. 

106 

107 Raises 

108 ------ 

109 :class:`ImportError` 

110 If *Imageio* is not installed. 

111 """ 

112 

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 ) 

122 

123 raise ImportError(error) from exception 

124 

125 return False 

126 else: 

127 return True 

128 

129 

130def is_openimageio_installed(raise_exception: bool = False) -> bool: 

131 """ 

132 Determine whether *OpenImageIO* is installed and available. 

133 

134 Parameters 

135 ---------- 

136 raise_exception 

137 Whether to raise an exception if *OpenImageIO* is unavailable. 

138 

139 Returns 

140 ------- 

141 :class:`bool` 

142 Whether *OpenImageIO* is installed. 

143 

144 Raises 

145 ------ 

146 :class:`ImportError` 

147 If *OpenImageIO* is not installed. 

148 """ 

149 

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 ) 

159 

160 raise ImportError(error) from exception 

161 

162 return False 

163 else: 

164 return True 

165 

166 

167def is_matplotlib_installed(raise_exception: bool = False) -> bool: 

168 """ 

169 Determine whether *Matplotlib* is installed and available. 

170 

171 Parameters 

172 ---------- 

173 raise_exception 

174 Whether to raise an exception if *Matplotlib* is unavailable. 

175 

176 Returns 

177 ------- 

178 :class:`bool` 

179 Whether *Matplotlib* is installed. 

180 

181 Raises 

182 ------ 

183 :class:`ImportError` 

184 If *Matplotlib* is not installed. 

185 """ 

186 

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 ) 

196 

197 raise ImportError(error) from exception 

198 

199 return False 

200 else: 

201 return True 

202 

203 

204def is_networkx_installed(raise_exception: bool = False) -> bool: 

205 """ 

206 Determine whether *NetworkX* is installed and available. 

207 

208 Parameters 

209 ---------- 

210 raise_exception 

211 Whether to raise an exception if *NetworkX* is unavailable. 

212 

213 Returns 

214 ------- 

215 :class:`bool` 

216 Whether *NetworkX* is installed. 

217 

218 Raises 

219 ------ 

220 :class:`ImportError` 

221 If *NetworkX* is not installed. 

222 """ 

223 

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 ) 

234 

235 raise ImportError(error) from exception 

236 

237 return False 

238 else: 

239 return True 

240 

241 

242def is_opencolorio_installed(raise_exception: bool = False) -> bool: 

243 """ 

244 Determine whether *OpenColorIO* is installed and available. 

245 

246 Parameters 

247 ---------- 

248 raise_exception 

249 Whether to raise an exception if *OpenColorIO* is unavailable. 

250 

251 Returns 

252 ------- 

253 :class:`bool` 

254 Whether *OpenColorIO* is installed. 

255 

256 Raises 

257 ------ 

258 :class:`ImportError` 

259 If *OpenColorIO* is not installed. 

260 """ 

261 

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 ) 

271 

272 raise ImportError(error) from exception 

273 

274 return False 

275 else: 

276 return True 

277 

278 

279def is_pandas_installed(raise_exception: bool = False) -> bool: 

280 """ 

281 Determine whether *Pandas* is installed and available. 

282 

283 Parameters 

284 ---------- 

285 raise_exception 

286 Whether to raise an exception if *Pandas* is unavailable. 

287 

288 Returns 

289 ------- 

290 :class:`bool` 

291 Whether *Pandas* is installed. 

292 

293 Raises 

294 ------ 

295 :class:`ImportError` 

296 If *Pandas* is not installed. 

297 """ 

298 

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 ) 

308 

309 raise ImportError(error) from exception 

310 

311 return False 

312 else: 

313 return True 

314 

315 

316def is_pydot_installed(raise_exception: bool = False) -> bool: 

317 """ 

318 Determine whether *Pydot* is installed and available. 

319 

320 The presence of *Graphviz* will also be tested. 

321 

322 Parameters 

323 ---------- 

324 raise_exception 

325 Whether to raise an exception if *Pydot* is unavailable. 

326 

327 Returns 

328 ------- 

329 :class:`bool` 

330 Whether *Pydot* is installed. 

331 

332 Raises 

333 ------ 

334 :class:`ImportError` 

335 If *Pydot* is not installed. 

336 """ 

337 

338 try: # pragma: no cover 

339 import pydot # noqa: F401, PLC0415 

340 

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 ) 

348 

349 raise ImportError(error) from exception 

350 

351 if shutil.which("fdp") is not None: 

352 return True 

353 

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 ) 

361 

362 raise RuntimeError(error) 

363 

364 return False # pragma: no cover 

365 

366 

367def is_scipy_installed(raise_exception: bool = False) -> bool: 

368 """ 

369 Determine whether *SciPy* is installed and available. 

370 

371 Parameters 

372 ---------- 

373 raise_exception 

374 Whether to raise an exception if *SciPy* is unavailable. 

375 

376 Returns 

377 ------- 

378 :class:`bool` 

379 Whether *SciPy* is installed. 

380 

381 Raises 

382 ------ 

383 :class:`ImportError` 

384 If *SciPy* is not installed. 

385 """ 

386 

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 ) 

396 

397 raise ImportError(error) from exception 

398 

399 return False 

400 else: 

401 return True 

402 

403 

404def is_tqdm_installed(raise_exception: bool = False) -> bool: 

405 """ 

406 Determine whether *tqdm* is installed and available. 

407 

408 Parameters 

409 ---------- 

410 raise_exception 

411 Whether to raise an exception if *tqdm* is unavailable. 

412 

413 Returns 

414 ------- 

415 :class:`bool` 

416 Whether *tqdm* is installed. 

417 

418 Raises 

419 ------ 

420 :class:`ImportError` 

421 If *tqdm* is not installed. 

422 """ 

423 

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 ) 

433 

434 raise ImportError(error) from exception 

435 

436 return False 

437 else: 

438 return True 

439 

440 

441def is_trimesh_installed(raise_exception: bool = False) -> bool: 

442 """ 

443 Determine whether *Trimesh* is installed and available. 

444 

445 Parameters 

446 ---------- 

447 raise_exception 

448 Whether to raise an exception if *Trimesh* is unavailable. 

449 

450 Returns 

451 ------- 

452 :class:`bool` 

453 Whether *Trimesh* is installed. 

454 

455 Raises 

456 ------ 

457 :class:`ImportError` 

458 If *Trimesh* is not installed. 

459 """ 

460 

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 ) 

470 

471 raise ImportError(error) from exception 

472 

473 return False 

474 else: 

475 return True 

476 

477 

478def is_xxhash_installed(raise_exception: bool = False) -> bool: 

479 """ 

480 Determine whether *xxhash* is installed and available. 

481 

482 Parameters 

483 ---------- 

484 raise_exception 

485 Whether to raise an exception if *xxhash* is unavailable. 

486 

487 Returns 

488 ------- 

489 :class:`bool` 

490 Whether *xxhash* is installed. 

491 

492 Raises 

493 ------ 

494 :class:`ImportError` 

495 If *xxhash* is not installed. 

496 """ 

497 

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 ) 

507 

508 raise ImportError(error) from exception 

509 

510 return False 

511 else: 

512 return True 

513 

514 

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""" 

534 

535 

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. 

555 

556 Other Parameters 

557 ---------------- 

558 requirements 

559 Package requirements to check for satisfaction. 

560 

561 Returns 

562 ------- 

563 Callable 

564 Decorated function that validates package availability. 

565 """ 

566 

567 def wrapper(function: Callable) -> Callable: 

568 """Wrap specified function wrapper.""" 

569 

570 @functools.wraps(function) 

571 def wrapped(*args: Any, **kwargs: Any) -> Any: 

572 """Wrap specified function.""" 

573 

574 for requirement in requirements: 

575 REQUIREMENTS_TO_CALLABLE[requirement](raise_exception=True) 

576 

577 return function(*args, **kwargs) 

578 

579 return wrapped 

580 

581 return wrapper