Coverage for utilities/verbose.py: 72%

218 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-16 22:49 +1300

1""" 

2Verbose 

3======= 

4 

5Define verbose output, logging, and warning management utilities. 

6""" 

7 

8from __future__ import annotations 

9 

10import functools 

11import logging 

12import os 

13import sys 

14import traceback 

15import typing 

16import warnings 

17from collections import defaultdict 

18from contextlib import contextmanager, suppress 

19from itertools import chain 

20from textwrap import TextWrapper 

21from warnings import filterwarnings, formatwarning, warn 

22 

23import numpy as np 

24 

25if typing.TYPE_CHECKING: 

26 from colour.hints import ( 

27 Any, 

28 Callable, 

29 ClassVar, 

30 Dict, 

31 Generator, 

32 List, 

33 Literal, 

34 Mapping, 

35 Self, 

36 TextIO, 

37 Type, 

38 ) 

39 

40from colour.hints import LiteralWarning, cast 

41 

42__author__ = "Colour Developers" 

43__copyright__ = "Copyright 2013 Colour Developers" 

44__license__ = "BSD-3-Clause - https://opensource.org/licenses/BSD-3-Clause" 

45__maintainer__ = "Colour Developers" 

46__email__ = "colour-developers@colour-science.org" 

47__status__ = "Production" 

48 

49__all__ = [ 

50 "LOGGER", 

51 "MixinLogging", 

52 "ColourWarning", 

53 "ColourUsageWarning", 

54 "ColourRuntimeWarning", 

55 "message_box", 

56 "show_warning", 

57 "warning", 

58 "runtime_warning", 

59 "usage_warning", 

60 "filter_warnings", 

61 "as_bool", 

62 "suppress_warnings", 

63 "suppress_stdout", 

64 "numpy_print_options", 

65 "ANCILLARY_COLOUR_SCIENCE_PACKAGES", 

66 "ANCILLARY_RUNTIME_PACKAGES", 

67 "ANCILLARY_DEVELOPMENT_PACKAGES", 

68 "ANCILLARY_EXTRAS_PACKAGES", 

69 "describe_environment", 

70 "multiline_str", 

71 "multiline_repr", 

72] 

73 

74LOGGER = logging.getLogger(__name__) 

75 

76 

77class MixinLogging: 

78 """ 

79 Provide logging capabilities through mixin inheritance. 

80 

81 This mixin extends class functionality to enable structured logging, 

82 allowing consistent logging behaviour across the codebase. 

83 

84 Attributes 

85 ---------- 

86 - :func:`~colour.utilities.MixinLogging.MAPPING_LOGGING_LEVEL_TO_CALLABLE` 

87 

88 Methods 

89 ------- 

90 - :func:`~colour.utilities.MixinLogging.log` 

91 """ 

92 

93 MAPPING_LOGGING_LEVEL_TO_CALLABLE: ClassVar = { # pyright: ignore 

94 "critical": LOGGER.critical, 

95 "error": LOGGER.error, 

96 "warning": LOGGER.warning, 

97 "info": LOGGER.info, 

98 "debug": LOGGER.debug, 

99 } 

100 

101 def log( 

102 self, 

103 message: str, 

104 verbosity: Literal[ 

105 "critical", 

106 "error", 

107 "warning", 

108 "info", 

109 "debug", 

110 ] = "info", 

111 ) -> None: 

112 """ 

113 Log the specified message using the specified verbosity level. 

114 

115 Parameters 

116 ---------- 

117 message 

118 Message to log. 

119 verbosity 

120 Verbosity level. 

121 """ 

122 

123 self.MAPPING_LOGGING_LEVEL_TO_CALLABLE[verbosity]( # pyright: ignore 

124 "%s: %s", 

125 self.name, # pyright: ignore 

126 message, 

127 ) 

128 

129 

130class ColourWarning(Warning): 

131 """ 

132 Define the base class for *Colour* warnings. 

133 

134 This class serves as the foundational warning type for the *Colour* 

135 library, inheriting from the standard :class:`Warning` class to provide 

136 consistent warning behaviour across the library. 

137 """ 

138 

139 

140class ColourUsageWarning(Warning): 

141 """ 

142 Define the base class for *Colour* usage warnings. 

143 

144 This class serves as the foundation for all usage-related warnings in the 

145 *Colour* library, providing a consistent interface for alerting users to 

146 non-critical issues during runtime operations. It is a subclass of the 

147 :class:`colour.utilities.ColourWarning` class. 

148 """ 

149 

150 

151class ColourRuntimeWarning(Warning): 

152 """ 

153 Define the base class for *Colour* runtime warnings. 

154 

155 This class serves as the foundation for all runtime warnings in the 

156 *Colour* library, providing a consistent interface for alerting users to 

157 runtime issues. It is a subclass of the 

158 :class:`colour.utilities.ColourWarning` class. 

159 """ 

160 

161 

162def message_box( 

163 message: str, 

164 width: int = 79, 

165 padding: int = 3, 

166 print_callable: Callable = print, 

167) -> None: 

168 """ 

169 Print a message inside a formatted box. 

170 

171 Parameters 

172 ---------- 

173 message 

174 Message to print inside the box. 

175 width 

176 Width of the message box in characters. 

177 padding 

178 Number of spaces for padding on each side of the message. 

179 print_callable 

180 Callable used to print the formatted message box. 

181 

182 Examples 

183 -------- 

184 >>> message = ( 

185 ... "Lorem ipsum dolor sit amet, consectetur adipiscing elit, " 

186 ... "sed do eiusmod tempor incididunt ut labore et dolore magna " 

187 ... "aliqua." 

188 ... ) 

189 >>> message_box(message, width=75) 

190 =========================================================================== 

191 * * 

192 * Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do * 

193 * eiusmod tempor incididunt ut labore et dolore magna aliqua. * 

194 * * 

195 =========================================================================== 

196 >>> message_box(message, width=60) 

197 ============================================================ 

198 * * 

199 * Lorem ipsum dolor sit amet, consectetur adipiscing * 

200 * elit, sed do eiusmod tempor incididunt ut labore et * 

201 * dolore magna aliqua. * 

202 * * 

203 ============================================================ 

204 >>> message_box(message, width=75, padding=16) 

205 =========================================================================== 

206 * * 

207 * Lorem ipsum dolor sit amet, consectetur * 

208 * adipiscing elit, sed do eiusmod tempor * 

209 * incididunt ut labore et dolore magna * 

210 * aliqua. * 

211 * * 

212 =========================================================================== 

213 """ 

214 

215 ideal_width = width - padding * 2 - 2 

216 

217 def inner(text: str) -> str: 

218 """Format and pads inner text for the message box.""" 

219 

220 return ( 

221 f"*{' ' * padding}" 

222 f"{text}{' ' * (width - len(text) - padding * 2 - 2)}" 

223 f"{' ' * padding}*" 

224 ) 

225 

226 print_callable("=" * width) 

227 print_callable(inner("")) 

228 

229 wrapper = TextWrapper( 

230 width=ideal_width, break_long_words=False, replace_whitespace=False 

231 ) 

232 

233 lines = [wrapper.wrap(line) for line in message.split("\n")] 

234 for line in chain(*[" " if len(line) == 0 else line for line in lines]): 

235 print_callable(inner(line.expandtabs())) 

236 

237 print_callable(inner("")) 

238 print_callable("=" * width) 

239 

240 

241def show_warning( 

242 message: Warning | str, 

243 category: Type[Warning], 

244 filename: str, 

245 lineno: int, 

246 file: TextIO | None = None, 

247 line: str | None = None, 

248) -> None: 

249 """ 

250 Display a warning message with enhanced formatting that enables 

251 traceback printing. 

252 

253 This definition is activated by setting the 

254 *COLOUR_SCIENCE__COLOUR__SHOW_WARNINGS_WITH_TRACEBACK* environment 

255 variable before importing *colour*. 

256 

257 Parameters 

258 ---------- 

259 message 

260 Warning message. 

261 category 

262 :class:`Warning` sub-class. 

263 filename 

264 File path to read the line at ``lineno`` from if ``line`` is 

265 None. 

266 lineno 

267 Line number to read the line at in ``filename`` if ``line`` is 

268 None. 

269 file 

270 :class:`file` object to write the warning to, defaults to 

271 :attr:`sys.stderr` attribute. 

272 line 

273 Source code to be included in the warning message. 

274 

275 Notes 

276 ----- 

277 - Setting the 

278 *COLOUR_SCIENCE__COLOUR__SHOW_WARNINGS_WITH_TRACEBACK* 

279 environment variable replaces the :func:`warnings.showwarning` 

280 definition with the :func:`colour.utilities.show_warning` 

281 definition, providing complete traceback from the point where 

282 the warning occurred. 

283 """ 

284 

285 frame_range = (1, None) 

286 

287 file = sys.stderr if file is None else file 

288 

289 if file is None: 

290 return 

291 

292 try: 

293 # Generating a traceback to print useful warning origin. 

294 frame_in, frame_out = frame_range 

295 

296 try: 

297 raise ZeroDivisionError # noqa: TRY301 

298 except ZeroDivisionError: 

299 exception_traceback = sys.exc_info()[2] 

300 frame = ( 

301 exception_traceback.tb_frame.f_back 

302 if exception_traceback is not None 

303 else None 

304 ) 

305 while frame_in and frame is not None: 

306 frame = frame.f_back 

307 frame_in -= 1 

308 

309 traceback.print_stack(frame, frame_out, file) 

310 file.write(formatwarning(message, category, filename, lineno, line)) 

311 except (OSError, UnicodeError): 

312 pass 

313 

314 

315if os.environ.get( # pragma: no cover 

316 "COLOUR_SCIENCE__COLOUR__SHOW_WARNINGS_WITH_TRACEBACK" 

317): 

318 warnings.showwarning = show_warning # pragma: no cover 

319 

320 

321def warning(*args: Any, **kwargs: Any) -> None: 

322 """ 

323 Issue a warning. 

324 

325 Other Parameters 

326 ---------------- 

327 args 

328 Warning message and optional arguments for message formatting. 

329 kwargs 

330 Keyword arguments for controlling warning behaviour, including 

331 category, stacklevel, and source filtering options. 

332 

333 Examples 

334 -------- 

335 >>> warning("This is a warning!") # doctest: +SKIP 

336 """ 

337 

338 kwargs["category"] = kwargs.get("category", ColourWarning) 

339 

340 warn(*args, **kwargs) 

341 

342 

343def runtime_warning(*args: Any, **kwargs: Any) -> None: 

344 """ 

345 Issue a runtime warning. 

346 

347 Other Parameters 

348 ---------------- 

349 args 

350 Warning message and optional arguments for message formatting. 

351 kwargs 

352 Keyword arguments for controlling warning behaviour. 

353 

354 Examples 

355 -------- 

356 >>> runtime_warning("This is a runtime warning!") # doctest: +SKIP 

357 """ 

358 

359 kwargs["category"] = ColourRuntimeWarning 

360 

361 warning(*args, **kwargs) 

362 

363 

364def usage_warning(*args: Any, **kwargs: Any) -> None: 

365 """ 

366 Issue a usage warning. 

367 

368 Other Parameters 

369 ---------------- 

370 args 

371 Warning message and optional arguments for message formatting. 

372 kwargs 

373 Keyword arguments for controlling warning behaviour. 

374 

375 Examples 

376 -------- 

377 >>> usage_warning("This is an usage warning!") # doctest: +SKIP 

378 """ 

379 

380 kwargs["category"] = ColourUsageWarning 

381 

382 warning(*args, **kwargs) 

383 

384 

385def filter_warnings( 

386 colour_runtime_warnings: bool | LiteralWarning | None = None, 

387 colour_usage_warnings: bool | LiteralWarning | None = None, 

388 colour_warnings: bool | LiteralWarning | None = None, 

389 python_warnings: bool | LiteralWarning | None = None, 

390) -> None: 

391 """ 

392 Filter *Colour* and optionally overall Python warnings. 

393 

394 The possible values for all the actions, i.e., each argument, are as 

395 follows: 

396 

397 - *None* (No action is taken) 

398 - *True* (*ignore*) 

399 - *False* (*default*) 

400 - *error* 

401 - *ignore* 

402 - *always* 

403 - *default* 

404 - *module* 

405 - *once* 

406 

407 Parameters 

408 ---------- 

409 colour_runtime_warnings 

410 Whether to filter *Colour* runtime warnings using the specified 

411 action value. 

412 colour_usage_warnings 

413 Whether to filter *Colour* usage warnings using the specified 

414 action value. 

415 colour_warnings 

416 Whether to filter *Colour* warnings, this also filters *Colour* 

417 usage and runtime warnings using the specified action value. 

418 python_warnings 

419 Whether to filter *Python* warnings using the specified action 

420 value. 

421 

422 Examples 

423 -------- 

424 Filtering *Colour* runtime warnings: 

425 

426 >>> filter_warnings(colour_runtime_warnings=True) 

427 

428 Filtering *Colour* usage warnings: 

429 

430 >>> filter_warnings(colour_usage_warnings=True) 

431 

432 Filtering *Colour* warnings: 

433 

434 >>> filter_warnings(colour_warnings=True) 

435 

436 Filtering all the *Colour* and also Python warnings: 

437 

438 >>> filter_warnings(python_warnings=True) 

439 

440 Enabling all the *Colour* and Python warnings: 

441 

442 >>> filter_warnings(*[False] * 4) 

443 

444 Enabling all the *Colour* and Python warnings using the *default* action: 

445 

446 >>> filter_warnings(*["default"] * 4) 

447 

448 Setting back the default state: 

449 

450 >>> filter_warnings(colour_runtime_warnings=True) 

451 """ 

452 

453 for action, category in [ 

454 (colour_warnings, ColourWarning), 

455 (colour_runtime_warnings, ColourRuntimeWarning), 

456 (colour_usage_warnings, ColourUsageWarning), 

457 (python_warnings, Warning), 

458 ]: 

459 if action is None: 

460 continue 

461 

462 if isinstance(action, str): 

463 action = cast("LiteralWarning", str(action)) # noqa: PLW2901 

464 else: 

465 action = "ignore" if action else "default" # noqa: PLW2901 

466 

467 filterwarnings(action, category=category) 

468 

469 

470def as_bool(a: str) -> bool: 

471 """ 

472 Convert the specified string to a boolean value. 

473 

474 The following string values evaluate to *True*: "1", "On", and "True". 

475 All other string values evaluate to *False*. 

476 

477 Parameters 

478 ---------- 

479 a 

480 String to convert to boolean. 

481 

482 Returns 

483 ------- 

484 :class:`bool` 

485 Boolean representation of the specified string. 

486 

487 Examples 

488 -------- 

489 >>> as_bool("1") 

490 True 

491 >>> as_bool("On") 

492 True 

493 >>> as_bool("True") 

494 True 

495 >>> as_bool("0") 

496 False 

497 >>> as_bool("Off") 

498 False 

499 >>> as_bool("False") 

500 False 

501 """ 

502 

503 return a.lower() in ["1", "on", "true"] 

504 

505 

506# Defaulting to filter *Colour* runtime warnings. 

507filter_warnings( 

508 colour_runtime_warnings=as_bool( 

509 os.environ.get("COLOUR_SCIENCE__FILTER_RUNTIME_WARNINGS", "True") 

510 ) 

511) 

512 

513if ( 

514 os.environ.get("COLOUR_SCIENCE__FILTER_USAGE_WARNINGS") is not None 

515): # pragma: no cover 

516 filter_warnings( 

517 colour_usage_warnings=as_bool( 

518 os.environ["COLOUR_SCIENCE__FILTER_USAGE_WARNINGS"] 

519 ) 

520 ) 

521 

522if ( 

523 os.environ.get("COLOUR_SCIENCE__FILTER_COLOUR_WARNINGS") is not None 

524): # pragma: no cover 

525 filter_warnings( 

526 colour_usage_warnings=as_bool( 

527 os.environ["COLOUR_SCIENCE__FILTER_WARNINGS"], 

528 ) 

529 ) 

530 

531if ( 

532 os.environ.get("COLOUR_SCIENCE__FILTER_PYTHON_WARNINGS") is not None 

533): # pragma: no cover 

534 filter_warnings( 

535 colour_usage_warnings=as_bool( 

536 os.environ["COLOUR_SCIENCE__FILTER_PYTHON_WARNINGS"] 

537 ) 

538 ) 

539 

540 

541@contextmanager 

542def suppress_warnings( 

543 colour_runtime_warnings: bool | LiteralWarning | None = None, 

544 colour_usage_warnings: bool | LiteralWarning | None = None, 

545 colour_warnings: bool | LiteralWarning | None = None, 

546 python_warnings: bool | LiteralWarning | None = None, 

547) -> Generator: 

548 """ 

549 Suppress *Colour* and optionally overall Python warnings within a 

550 context. 

551 

552 The possible values for all the actions, i.e., each argument, are as 

553 follows: 

554 

555 - *None* (No action is taken) 

556 - *True* (*ignore*) 

557 - *False* (*default*) 

558 - *error* 

559 - *ignore* 

560 - *always* 

561 - *default* 

562 - *module* 

563 - *once* 

564 

565 Parameters 

566 ---------- 

567 colour_runtime_warnings 

568 Whether to filter *Colour* runtime warnings according to the 

569 specified action value. 

570 colour_usage_warnings 

571 Whether to filter *Colour* usage warnings according to the 

572 specified action value. 

573 colour_warnings 

574 Whether to filter *Colour* warnings, this also filters *Colour* 

575 usage and runtime warnings according to the specified action 

576 value. 

577 python_warnings 

578 Whether to filter *Python* warnings according to the specified 

579 action value. 

580 """ 

581 

582 filters = warnings.filters 

583 show_warnings = warnings.showwarning 

584 

585 filter_warnings( 

586 colour_warnings=colour_warnings, 

587 colour_runtime_warnings=colour_runtime_warnings, 

588 colour_usage_warnings=colour_usage_warnings, 

589 python_warnings=python_warnings, 

590 ) 

591 

592 try: 

593 yield 

594 finally: 

595 warnings.filters = filters 

596 warnings.showwarning = show_warnings 

597 

598 

599class suppress_stdout: 

600 """ 

601 Define a context manager and decorator to temporarily suppress standard output. 

602 

603 Examples 

604 -------- 

605 >>> with suppress_stdout(): 

606 ... print("Hello World!") 

607 >>> print("Hello World!") 

608 Hello World! 

609 """ 

610 

611 def __enter__(self) -> Self: 

612 """ 

613 Redirect standard output to null device upon context manager entry. 

614 """ 

615 

616 self._stdout = sys.stdout 

617 sys.stdout = open(os.devnull, "w") 

618 

619 return self 

620 

621 def __exit__(self, *args: Any) -> None: 

622 """ 

623 Restore standard output upon context manager exit. 

624 """ 

625 

626 sys.stdout.close() 

627 sys.stdout = self._stdout 

628 

629 def __call__(self, function: Callable) -> Callable: # pragma: no cover 

630 """Call the wrapped definition with suppressed output.""" 

631 

632 @functools.wraps(function) 

633 def wrapper(*args: Any, **kwargs: Any) -> Callable: 

634 with self: 

635 return function(*args, **kwargs) 

636 

637 return wrapper 

638 

639 

640@contextmanager 

641def numpy_print_options(*args: Any, **kwargs: Any) -> Generator: 

642 """ 

643 Implement a context manager for temporarily modifying *NumPy* array 

644 print options. 

645 

646 Other Parameters 

647 ---------------- 

648 args 

649 Positional arguments passed to :func:`numpy.set_printoptions`. 

650 kwargs 

651 Keyword arguments passed to :func:`numpy.set_printoptions`. 

652 

653 Examples 

654 -------- 

655 >>> np.array([np.pi]) # doctest: +ELLIPSIS 

656 array([ 3.1415926...]) 

657 >>> with numpy_print_options(formatter={"float": "{:0.1f}".format}): 

658 ... np.array([np.pi]) 

659 array([3.1]) 

660 """ 

661 

662 options = np.get_printoptions() 

663 np.set_printoptions(*args, **kwargs) 

664 try: 

665 yield 

666 finally: 

667 np.set_printoptions(**options) 

668 

669 

670ANCILLARY_COLOUR_SCIENCE_PACKAGES: Dict[str, str] = {} 

671""" 

672Ancillary *colour-science.org* packages to describe. 

673 

674ANCILLARY_COLOUR_SCIENCE_PACKAGES 

675""" 

676 

677ANCILLARY_RUNTIME_PACKAGES: Dict[str, str] = {} 

678""" 

679Ancillary runtime packages to describe. 

680 

681ANCILLARY_RUNTIME_PACKAGES 

682""" 

683 

684ANCILLARY_DEVELOPMENT_PACKAGES: Dict[str, str] = {} 

685""" 

686Ancillary development packages to describe. 

687 

688ANCILLARY_DEVELOPMENT_PACKAGES 

689""" 

690 

691ANCILLARY_EXTRAS_PACKAGES: Dict[str, str] = {} 

692""" 

693Ancillary extras packages to describe. 

694 

695ANCILLARY_EXTRAS_PACKAGES 

696""" 

697 

698 

699def describe_environment( 

700 runtime_packages: bool = True, 

701 development_packages: bool = False, 

702 extras_packages: bool = False, 

703 print_environment: bool = True, 

704 **kwargs: Any, 

705) -> defaultdict: 

706 """ 

707 Describe the *Colour* runtime environment, including interpreter details 

708 and package versions. 

709 

710 Parameters 

711 ---------- 

712 runtime_packages 

713 Whether to return the runtime packages versions. 

714 development_packages 

715 Whether to return the development packages versions. 

716 extras_packages 

717 Whether to return the extras packages versions. 

718 print_environment 

719 Whether to print the environment. 

720 

721 Other Parameters 

722 ---------------- 

723 padding 

724 {:func:`colour.utilities.message_box`}, 

725 Padding on each side of the message. 

726 print_callable 

727 {:func:`colour.utilities.message_box`}, 

728 Callable used to print the message box. 

729 width 

730 {:func:`colour.utilities.message_box`}, 

731 Message box width. 

732 

733 Returns 

734 ------- 

735 :class:`collections.defaultdict` 

736 Environment. 

737 

738 Examples 

739 -------- 

740 >>> environment = describe_environment(width=75) # doctest: +SKIP 

741 =========================================================================== 

742 * * 

743 * Interpreter : * 

744 * python : 3.12.4 (main, Jun 6 2024, 18:26:44) [Clang 15.0.0 * 

745 * (clang-1500.3.9.4)] * 

746 * * 

747 * colour-science.org : * 

748 * colour : v0.4.3-282-gcb450ff50 * 

749 * * 

750 * Runtime : * 

751 * imageio : 2.35.1 * 

752 * matplotlib : 3.9.2 * 

753 * networkx : 3.3 * 

754 * numpy : 2.1.1 * 

755 * pandas : 2.2.3 * 

756 * pydot : 3.0.2 * 

757 * PyOpenColorIO : 2.3.2 * 

758 * scipy : 1.14.1 * 

759 * tqdm : 4.66.5 * 

760 * trimesh : 4.4.9 * 

761 * OpenImageIO : 2.5.14.0 * 

762 * xxhash : 3.5.0 * 

763 * * 

764 =========================================================================== 

765 >>> environment = describe_environment(True, True, True, width=75) 

766 ... # doctest: +SKIP 

767 =========================================================================== 

768 * * 

769 * Interpreter : * 

770 * python : 3.12.4 (main, Jun 6 2024, 18:26:44) [Clang 15.0.0 * 

771 * (clang-1500.3.9.4)] * 

772 * * 

773 * colour-science.org : * 

774 * colour : v0.4.3-282-gcb450ff50 * 

775 * * 

776 * Runtime : * 

777 * imageio : 2.35.1 * 

778 * matplotlib : 3.9.2 * 

779 * networkx : 3.3 * 

780 * numpy : 2.1.1 * 

781 * pandas : 2.2.3 * 

782 * pydot : 3.0.2 * 

783 * PyOpenColorIO : 2.3.2 * 

784 * scipy : 1.14.1 * 

785 * tqdm : 4.66.5 * 

786 * trimesh : 4.4.9 * 

787 * OpenImageIO : 2.5.14.0 * 

788 * xxhash : 3.5.0 * 

789 * * 

790 * Development : * 

791 * biblib-simple : 0.1.2 * 

792 * coverage : 6.5.0 * 

793 * coveralls : 4.0.1 * 

794 * invoke : 2.2.0 * 

795 * pre-commit : 3.8.0 * 

796 * pydata-sphinx-theme : 0.15.4 * 

797 * pyright : 1.1.382.post1 * 

798 * pytest : 8.3.3 * 

799 * pytest-cov : 5.0.0 * 

800 * restructuredtext-lint : 1.4.0 * 

801 * sphinxcontrib-bibtex : 2.6.3 * 

802 * toml : 0.10.2 * 

803 * twine : 5.1.1 * 

804 * * 

805 * Extras : * 

806 * ipywidgets : 8.1.5 * 

807 * notebook : 7.2.2 * 

808 * * 

809 =========================================================================== 

810 """ 

811 

812 environment: defaultdict = defaultdict(dict) 

813 

814 environment["Interpreter"]["python"] = sys.version 

815 

816 import subprocess # noqa: PLC0415 

817 

818 import colour # noqa: PLC0415 

819 

820 # TODO: Implement support for "pyproject.toml" file whenever "TOML" is 

821 # supported in the standard library. 

822 # NOTE: A few clauses are not reached and a few packages are not available 

823 # during continuous integration and are thus ignored for coverage. 

824 try: # pragma: no cover 

825 output = subprocess.check_output( 

826 ["git", "describe"], # noqa: S607 

827 cwd=colour.__path__[0], 

828 stderr=subprocess.STDOUT, 

829 ).strip() 

830 version = output.decode("utf-8") 

831 except Exception: # pragma: no cover # noqa: BLE001 

832 version = colour.__version__ 

833 

834 environment["colour-science.org"]["colour"] = version 

835 environment["colour-science.org"].update(ANCILLARY_COLOUR_SCIENCE_PACKAGES) 

836 

837 if runtime_packages: 

838 for package in [ 

839 "imageio", 

840 "matplotlib", 

841 "networkx", 

842 "numpy", 

843 "pandas", 

844 "pydot", 

845 "PyOpenColorIO", 

846 "scipy", 

847 "tqdm", 

848 "trimesh", 

849 ]: 

850 with suppress(ImportError): 

851 namespace = __import__(package) 

852 with suppress(AttributeError): 

853 environment["Runtime"][package] = namespace.__version__ 

854 

855 # OpenImageIO 

856 with suppress(ImportError): # pragma: no cover 

857 namespace = __import__("OpenImageIO") 

858 environment["Runtime"]["OpenImageIO"] = namespace.VERSION_STRING 

859 

860 # xxhash 

861 with suppress(ImportError): # pragma: no cover 

862 namespace = __import__("xxhash") 

863 environment["Runtime"]["xxhash"] = namespace.version.VERSION 

864 

865 environment["Runtime"].update(ANCILLARY_RUNTIME_PACKAGES) 

866 

867 def _get_package_version(package: str, mapping: Mapping) -> str: 

868 """Return specified package version.""" 

869 

870 namespace = __import__(package) 

871 

872 if package in mapping: 

873 import pkg_resources # noqa: PLC0415 

874 

875 distributions = list(pkg_resources.working_set) 

876 

877 for distribution in distributions: 

878 if distribution.project_name == mapping[package]: 

879 return distribution.version 

880 

881 return namespace.__version__ 

882 

883 if development_packages: 

884 mapping = { 

885 "biblib.bib": "biblib-simple", 

886 "pre_commit": "pre-commit", 

887 "pydata_sphinx_theme": "pydata-sphinx-theme", 

888 "pytest_cov": "pytest-cov", 

889 "pytest_xdist": "pytest-xdist", 

890 "restructuredtext_lint": "restructuredtext-lint", 

891 "sphinxcontrib.bibtex": "sphinxcontrib-bibtex", 

892 } 

893 for package in [ 

894 "biblib.bib", 

895 "coverage", 

896 "coveralls", 

897 "invoke", 

898 "jupyter", 

899 "pre_commit", 

900 "pydata_sphinx_theme", 

901 "pyright", 

902 "pytest", 

903 "pytest_cov", 

904 "pytest_xdist", 

905 "restructuredtext_lint", 

906 "sphinxcontrib.bibtex", 

907 "toml", 

908 "twine", 

909 ]: 

910 try: 

911 version = _get_package_version(package, mapping) 

912 package = mapping.get(package, package) # noqa: PLW2901 

913 

914 environment["Development"][package] = version 

915 except Exception: # pragma: no cover # noqa: BLE001, PERF203, S112 

916 continue 

917 

918 environment["Development"].update(ANCILLARY_DEVELOPMENT_PACKAGES) 

919 

920 if extras_packages: 

921 mapping = {} 

922 for package in ["ipywidgets", "notebook"]: 

923 try: 

924 version = _get_package_version(package, mapping) 

925 package = mapping.get(package, package) # noqa: PLW2901 

926 

927 environment["Extras"][package] = version 

928 except Exception: # pragma: no cover # noqa: BLE001, PERF203, S112 

929 continue 

930 

931 environment["Extras"].update(ANCILLARY_EXTRAS_PACKAGES) 

932 

933 if print_environment: 

934 message = "" 

935 for category in ( 

936 "Interpreter", 

937 "colour-science.org", 

938 "Runtime", 

939 "Development", 

940 "Extras", 

941 ): 

942 elements = environment.get(category) 

943 if not elements: 

944 continue 

945 

946 message += f"{category} :\n" 

947 for key, value in elements.items(): 

948 lines = value.split("\n") 

949 message += f" {key} : {lines.pop(0)}\n" 

950 indentation = len(f" {key} : ") 

951 for line in lines: # pragma: no cover 

952 message += f"{' ' * indentation}{line}\n" 

953 

954 message += "\n" 

955 

956 message_box(message.strip(), **kwargs) 

957 

958 return environment 

959 

960 

961def multiline_str( 

962 object_: Any, 

963 attributes: List[dict], 

964 header_underline: str = "=", 

965 section_underline: str = "-", 

966 separator: str = " : ", 

967) -> str: 

968 """ 

969 Generate a formatted multi-line string representation of the specified 

970 object. 

971 

972 Parameters 

973 ---------- 

974 object_ 

975 Object to format into a string representation. 

976 attributes 

977 Attributes to format, provided as a list of dictionaries with 

978 formatting specifications. 

979 header_underline 

980 Underline character to use for header sections. 

981 section_underline 

982 Underline character to use for subsections. 

983 separator 

984 Separator to use when formatting the attributes and their values. 

985 

986 Returns 

987 ------- 

988 :class:`str` 

989 Formatted multi-line string representation. 

990 

991 Examples 

992 -------- 

993 >>> class Data: 

994 ... def __init__(self, a: str, b: int, c: list): 

995 ... self._a = a 

996 ... self._b = b 

997 ... self._c = c 

998 ... 

999 ... def __str__(self) -> str: 

1000 ... return multiline_str( 

1001 ... self, 

1002 ... [ 

1003 ... { 

1004 ... "formatter": lambda x: ( 

1005 ... f"Object - {self.__class__.__name__}" 

1006 ... ), 

1007 ... "header": True, 

1008 ... }, 

1009 ... {"line_break": True}, 

1010 ... {"label": "Data", "section": True}, 

1011 ... {"line_break": True}, 

1012 ... {"label": "String", "section": True}, 

1013 ... {"name": "_a", "label": 'String "a"'}, 

1014 ... {"line_break": True}, 

1015 ... {"label": "Integer", "section": True}, 

1016 ... {"name": "_b", "label": 'Integer "b"'}, 

1017 ... {"line_break": True}, 

1018 ... {"label": "List", "section": True}, 

1019 ... { 

1020 ... "name": "_c", 

1021 ... "label": 'List "c"', 

1022 ... "formatter": lambda x: "; ".join(x), 

1023 ... }, 

1024 ... ], 

1025 ... ) 

1026 >>> print(Data("Foo", 1, ["John", "Doe"])) 

1027 Object - Data 

1028 ============= 

1029 <BLANKLINE> 

1030 Data 

1031 ---- 

1032 <BLANKLINE> 

1033 String 

1034 ------ 

1035 String "a" : Foo 

1036 <BLANKLINE> 

1037 Integer 

1038 ------- 

1039 Integer "b" : 1 

1040 <BLANKLINE> 

1041 List 

1042 ---- 

1043 List "c" : John; Doe 

1044 """ 

1045 

1046 attribute_defaults = { 

1047 "name": None, 

1048 "label": None, 

1049 "formatter": str, 

1050 "header": False, 

1051 "section": False, 

1052 "line_break": False, 

1053 } 

1054 

1055 try: 

1056 justify = max( 

1057 len(attribute["label"]) 

1058 for attribute in attributes 

1059 if ( 

1060 attribute.get("label") 

1061 and not attribute.get("header") 

1062 and not attribute.get("section") 

1063 ) 

1064 ) 

1065 except ValueError: # pragma: no cover 

1066 justify = 0 

1067 

1068 representation = [] 

1069 for attribute in attributes: 

1070 attribute = dict(attribute_defaults, **attribute) # noqa: PLW2901 

1071 

1072 if not attribute["line_break"]: 

1073 if attribute["name"] is not None: 

1074 formatted = attribute["formatter"](getattr(object_, attribute["name"])) 

1075 else: 

1076 formatted = attribute["formatter"](None) 

1077 

1078 if ( 

1079 attribute["label"] is not None 

1080 and not attribute["header"] 

1081 and not attribute["section"] 

1082 ): 

1083 lines = formatted.splitlines() 

1084 if len(lines) > 1: 

1085 for i, line in enumerate(lines[1:]): 

1086 lines[i + 1] = f"{'':{justify}}{' ' * len(separator)}{line}" 

1087 formatted = "\n".join(lines) 

1088 

1089 representation.append( 

1090 f"{attribute['label']:{justify}}{separator}{formatted}", 

1091 ) 

1092 elif attribute["label"] is not None and ( 

1093 attribute["header"] or attribute["section"] 

1094 ): 

1095 representation.append(attribute["label"]) 

1096 else: 

1097 representation.append(f"{formatted}") 

1098 

1099 if attribute["header"]: 

1100 representation.append(header_underline * len(representation[-1])) 

1101 

1102 if attribute["section"]: 

1103 representation.append(section_underline * len(representation[-1])) 

1104 else: 

1105 representation.append("") 

1106 

1107 return "\n".join(representation) 

1108 

1109 

1110def multiline_repr( 

1111 object_: Any, 

1112 attributes: List[dict], 

1113 reduce_array_representation: bool = True, 

1114) -> str: 

1115 """ 

1116 Generate an evaluable string representation of the specified object. 

1117 

1118 Parameters 

1119 ---------- 

1120 object_ 

1121 Object to format. 

1122 attributes 

1123 Attributes to format. 

1124 reduce_array_representation 

1125 Whether to remove the *Numpy* `array(` and `)` affixes. 

1126 

1127 Returns 

1128 ------- 

1129 :class:`str` 

1130 (Almost) evaluable string representation. 

1131 

1132 Examples 

1133 -------- 

1134 >>> class Data: 

1135 ... def __init__(self, a: str, b: int, c: list): 

1136 ... self._a = a 

1137 ... self._b = b 

1138 ... self._c = c 

1139 ... 

1140 ... def __repr__(self) -> str: 

1141 ... return multiline_repr( 

1142 ... self, 

1143 ... [ 

1144 ... {"name": "_a"}, 

1145 ... {"name": "_b"}, 

1146 ... { 

1147 ... "name": "_c", 

1148 ... "formatter": lambda x: repr(x) 

1149 ... .replace("[", "(") 

1150 ... .replace("]", ")"), 

1151 ... }, 

1152 ... ], 

1153 ... ) 

1154 >>> Data("Foo", 1, ["John", "Doe"]) 

1155 Data('Foo', 

1156 1, 

1157 ('John', 'Doe')) 

1158 """ 

1159 

1160 attribute_defaults = {"name": None, "formatter": repr} 

1161 

1162 justify = len(f"{object_.__class__.__name__}") + 1 

1163 

1164 def _format(attribute: dict) -> str: 

1165 """Format specified attribute and its value.""" 

1166 

1167 if attribute["name"] is not None: 

1168 value = attribute["formatter"](getattr(object_, attribute["name"])) 

1169 else: 

1170 value = attribute["formatter"](None) 

1171 

1172 if value is None: 

1173 return str(None) 

1174 

1175 if reduce_array_representation and value.startswith("array("): 

1176 lines = value.splitlines() 

1177 for i, line in enumerate(lines): 

1178 lines[i] = line[6:] 

1179 value = "\n".join(lines)[:-1] 

1180 

1181 lines = value.splitlines() 

1182 

1183 if len(lines) > 1: 

1184 for i, line in enumerate(lines[1:]): 

1185 lines[i + 1] = f"{'':{justify}}{line}" 

1186 

1187 return "\n".join(lines) 

1188 

1189 attribute = dict(attribute_defaults, **attributes.pop(0)) 

1190 

1191 representation = [f"{object_.__class__.__name__}({_format(attribute)}"] 

1192 

1193 for attribute in attributes: 

1194 attribute = dict(attribute_defaults, **attribute) # noqa: PLW2901 

1195 

1196 representation.append(f"{'':{justify}}{_format(attribute)}") 

1197 

1198 return "{})".format(",\n".join(representation))