Coverage for utilities/requirements.py: 27%

1"""

2 Requirements Utilities

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

4

5 Define utilities for checking the availability of optional dependencies.

6"""

7

8 from __future__ import annotations

9

10 import functools

11 import shutil

12 import subprocess

13 import typing

14

15 if typing.TYPE_CHECKING:

16 from colour.hints import (

17 Any,

18 Callable,

19 Literal,

20 )

21

22 from 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

49 def 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

93 def 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

130 def 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

167 def 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

204 def 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

242 def 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

279 def 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

316 def 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

367 def 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

404 def 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

441 def 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

478 def 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

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

532 Mapping of requirements to their respective callables.

533"""

534

535

536 def 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