Coverage for colour/utilities/common.py: 97%

185 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-15 19:39 +1300

1""" 

2Common Utilities 

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

4 

5Provide common utility objects that don't fall in any specific category. 

6 

7References 

8---------- 

9- :cite:`DjangoSoftwareFoundation2022` : Django Software Foundation. (2022). 

10 slugify. Retrieved June 1, 2022, from https://github.com/django/django/\ 

11blob/0dd29209091280ccf34e07c9468746c396b7778e/django/utils/text.py#L400 

12- :cite:`Kienzle2011a` : Kienzle, P., Patel, N., & Krycka, J. (2011). 

13 refl1d.numpyerrors - Refl1D v0.6.19 documentation. Retrieved January 30, 

14 2015, from 

15 http://www.reflectometry.org/danse/docs/refl1d/_modules/refl1d/\ 

16numpyerrors.html 

17""" 

18 

19from __future__ import annotations 

20 

21import functools 

22import inspect 

23import os 

24import re 

25import types 

26import typing 

27import unicodedata 

28import warnings 

29from contextlib import contextmanager 

30from copy import copy 

31from pprint import pformat 

32 

33import numpy as np 

34 

35from colour.constants import THRESHOLD_INTEGER 

36from colour.utilities import as_bool 

37 

38if typing.TYPE_CHECKING: 

39 from colour.hints import ( 

40 Any, 

41 Callable, 

42 DTypeBoolean, 

43 Generator, 

44 Iterable, 

45 Literal, 

46 Mapping, 

47 Self, 

48 Sequence, 

49 ) 

50 

51from colour.hints import TypeVar 

52from colour.utilities import CanonicalMapping, Lookup, is_xxhash_installed 

53 

54__author__ = "Colour Developers" 

55__copyright__ = "Copyright 2013 Colour Developers" 

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

57__maintainer__ = "Colour Developers" 

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

59__status__ = "Production" 

60 

61__all__ = [ 

62 "is_caching_enabled", 

63 "set_caching_enable", 

64 "caching_enable", 

65 "CacheRegistry", 

66 "CACHE_REGISTRY", 

67 "handle_numpy_errors", 

68 "ignore_numpy_errors", 

69 "raise_numpy_errors", 

70 "print_numpy_errors", 

71 "warn_numpy_errors", 

72 "ignore_python_warnings", 

73 "attest", 

74 "batch", 

75 "disable_multiprocessing", 

76 "multiprocessing_pool", 

77 "is_iterable", 

78 "is_numeric", 

79 "is_integer", 

80 "is_sibling", 

81 "filter_kwargs", 

82 "filter_mapping", 

83 "first_item", 

84 "copy_definition", 

85 "validate_method", 

86 "optional", 

87 "slugify", 

88 "int_digest", 

89] 

90 

91_CACHING_ENABLED: bool = not as_bool( 

92 os.environ.get("COLOUR_SCIENCE__DISABLE_CACHING", "False") 

93) 

94""" 

95Global variable storing the current *Colour* caching enabled state. 

96""" 

97 

98 

99def is_caching_enabled() -> bool: 

100 """ 

101 Determine whether *Colour* caching is enabled. 

102 

103 The caching state is controlled by the global 

104 *COLOUR_SCIENCE__DISABLE_CACHING* environment variable and can be 

105 temporarily modified using the :func:`set_caching_enable` function or the 

106 :class:`caching_enable` context manager. 

107 

108 Returns 

109 ------- 

110 :class:`bool` 

111 Whether *Colour* caching is enabled. 

112 

113 Examples 

114 -------- 

115 >>> with caching_enable(False): 

116 ... is_caching_enabled() 

117 False 

118 >>> with caching_enable(True): 

119 ... is_caching_enabled() 

120 True 

121 """ 

122 

123 return _CACHING_ENABLED 

124 

125 

126def set_caching_enable(enable: bool) -> None: 

127 """ 

128 Set the *Colour* caching enabled state. 

129 

130 Parameters 

131 ---------- 

132 enable 

133 Whether to enable *Colour* caching. 

134 

135 Examples 

136 -------- 

137 >>> with caching_enable(True): 

138 ... print(is_caching_enabled()) 

139 ... set_caching_enable(False) 

140 ... print(is_caching_enabled()) 

141 True 

142 False 

143 """ 

144 

145 global _CACHING_ENABLED # noqa: PLW0603 

146 

147 _CACHING_ENABLED = enable 

148 

149 

150class caching_enable: 

151 """ 

152 Define a context manager and decorator to temporarily set the *Colour* 

153 caching enabled state. 

154 

155 Parameters 

156 ---------- 

157 enable 

158 Whether to enable or disable *Colour* caching. 

159 """ 

160 

161 def __init__(self, enable: bool) -> None: 

162 self._enable = enable 

163 self._previous_state = is_caching_enabled() 

164 

165 def __enter__(self) -> Self: 

166 """ 

167 Enter the caching context and set the *Colour* caching state. 

168 """ 

169 

170 set_caching_enable(self._enable) 

171 

172 return self 

173 

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

175 """ 

176 Exit the caching context manager and restore the previous *Colour* 

177 caching state. 

178 """ 

179 

180 set_caching_enable(self._previous_state) 

181 

182 def __call__(self, function: Callable) -> Callable: 

183 """ 

184 Decorate and call the specified function with caching control. 

185 """ 

186 

187 @functools.wraps(function) 

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

189 with self: 

190 return function(*args, **kwargs) 

191 

192 return wrapper 

193 

194 

195class CacheRegistry: 

196 """ 

197 Provide a registry for managing mapping-based caches. 

198 

199 The registry maintains a collection of named caches that can be 

200 registered, cleared, and unregistered. Each cache operates as a 

201 dictionary-like mapping for storing key-value pairs. 

202 

203 Attributes 

204 ---------- 

205 - :attr:`~colour.utilities.CacheRegistry.registry` 

206 

207 Methods 

208 ------- 

209 - :meth:`~colour.SpectralShape.__init__` 

210 - :meth:`~colour.SpectralShape.__str__` 

211 - :meth:`~colour.SpectralShape.register_cache` 

212 - :meth:`~colour.SpectralShape.unregister_cache` 

213 - :meth:`~colour.SpectralShape.clear_cache` 

214 - :meth:`~colour.SpectralShape.clear_all_caches` 

215 

216 Examples 

217 -------- 

218 >>> cache_registry = CacheRegistry() 

219 >>> cache_a = cache_registry.register_cache("Cache A") 

220 >>> cache_a["Foo"] = "Bar" 

221 >>> cache_b = cache_registry.register_cache("Cache B") 

222 >>> cache_b["John"] = "Doe" 

223 >>> cache_b["Luke"] = "Skywalker" 

224 >>> print(cache_registry) 

225 {'Cache A': '1 item(s)', 'Cache B': '2 item(s)'} 

226 >>> cache_registry.clear_cache("Cache A") 

227 >>> print(cache_registry) 

228 {'Cache A': '0 item(s)', 'Cache B': '2 item(s)'} 

229 >>> cache_registry.unregister_cache("Cache B") 

230 >>> print(cache_registry) 

231 {'Cache A': '0 item(s)'} 

232 >>> print(cache_b) 

233 {} 

234 """ 

235 

236 def __init__(self) -> None: 

237 self._registry: dict = {} 

238 

239 @property 

240 def registry(self) -> dict: 

241 """ 

242 Getter for the cache registry. 

243 

244 Returns 

245 ------- 

246 :class:`dict` 

247 Cache registry containing cached computation results. 

248 """ 

249 

250 return self._registry 

251 

252 def __str__(self) -> str: 

253 """ 

254 Return a formatted string representation of the cache registry. 

255 

256 Returns 

257 ------- 

258 :class:`str` 

259 Formatted string representation. 

260 """ 

261 

262 return pformat( 

263 { 

264 name: f"{len(self._registry[name])} item(s)" 

265 for name in sorted(self._registry) 

266 } 

267 ) 

268 

269 def register_cache(self, name: str) -> dict: 

270 """ 

271 Register a new cache with the specified name in the registry. 

272 

273 Parameters 

274 ---------- 

275 name 

276 Cache name for the registry. 

277 

278 Returns 

279 ------- 

280 :class:`dict` 

281 Registered cache. 

282 

283 Examples 

284 -------- 

285 >>> cache_registry = CacheRegistry() 

286 >>> cache_a = cache_registry.register_cache("Cache A") 

287 >>> cache_a["Foo"] = "Bar" 

288 >>> cache_b = cache_registry.register_cache("Cache B") 

289 >>> cache_b["John"] = "Doe" 

290 >>> cache_b["Luke"] = "Skywalker" 

291 >>> print(cache_registry) 

292 {'Cache A': '1 item(s)', 'Cache B': '2 item(s)'} 

293 """ 

294 

295 self._registry[name] = {} 

296 

297 return self._registry[name] 

298 

299 def unregister_cache(self, name: str) -> None: 

300 """ 

301 Unregister the cache with the specified name from the registry. 

302 

303 Parameters 

304 ---------- 

305 name 

306 Cache name in the registry. 

307 

308 Notes 

309 ----- 

310 - The cache is cleared before being unregistered. 

311 

312 Examples 

313 -------- 

314 >>> cache_registry = CacheRegistry() 

315 >>> cache_a = cache_registry.register_cache("Cache A") 

316 >>> cache_a["Foo"] = "Bar" 

317 >>> cache_b = cache_registry.register_cache("Cache B") 

318 >>> cache_b["John"] = "Doe" 

319 >>> cache_b["Luke"] = "Skywalker" 

320 >>> print(cache_registry) 

321 {'Cache A': '1 item(s)', 'Cache B': '2 item(s)'} 

322 >>> cache_registry.unregister_cache("Cache B") 

323 >>> print(cache_registry) 

324 {'Cache A': '1 item(s)'} 

325 >>> print(cache_b) 

326 {} 

327 """ 

328 

329 self.clear_cache(name) 

330 

331 del self._registry[name] 

332 

333 def clear_cache(self, name: str) -> None: 

334 """ 

335 Clear the cache with the specified name. 

336 

337 Parameters 

338 ---------- 

339 name 

340 Cache name in the registry. 

341 

342 Examples 

343 -------- 

344 >>> cache_registry = CacheRegistry() 

345 >>> cache_a = cache_registry.register_cache("Cache A") 

346 >>> cache_a["Foo"] = "Bar" 

347 >>> print(cache_registry) 

348 {'Cache A': '1 item(s)'} 

349 >>> cache_registry.clear_cache("Cache A") 

350 >>> print(cache_registry) 

351 {'Cache A': '0 item(s)'} 

352 """ 

353 

354 self._registry[name].clear() 

355 

356 def clear_all_caches(self) -> None: 

357 """ 

358 Clear all caches in the registry. 

359 

360 Examples 

361 -------- 

362 >>> cache_registry = CacheRegistry() 

363 >>> cache_a = cache_registry.register_cache("Cache A") 

364 >>> cache_a["Foo"] = "Bar" 

365 >>> cache_b = cache_registry.register_cache("Cache B") 

366 >>> cache_b["John"] = "Doe" 

367 >>> cache_b["Luke"] = "Skywalker" 

368 >>> print(cache_registry) 

369 {'Cache A': '1 item(s)', 'Cache B': '2 item(s)'} 

370 >>> cache_registry.clear_all_caches() 

371 >>> print(cache_registry) 

372 {'Cache A': '0 item(s)', 'Cache B': '0 item(s)'} 

373 """ 

374 

375 for key in self._registry: 

376 self.clear_cache(key) 

377 

378 

379CACHE_REGISTRY: CacheRegistry = CacheRegistry() 

380""" 

381*Colour* cache registry referencing all the caches used for repetitive or long 

382processes. 

383""" 

384 

385 

386def handle_numpy_errors(**kwargs: Any) -> Callable: 

387 """ 

388 Handle *Numpy* errors through function decoration. 

389 

390 Other Parameters 

391 ---------------- 

392 kwargs 

393 Keyword arguments passed to :func:`numpy.seterr` to control 

394 error handling behaviour. 

395 

396 Returns 

397 ------- 

398 Callable 

399 Decorated function with specified *Numpy* error handling. 

400 

401 References 

402 ---------- 

403 :cite:`Kienzle2011a` 

404 

405 Examples 

406 -------- 

407 >>> import numpy 

408 >>> @handle_numpy_errors(all="ignore") 

409 ... def f(): 

410 ... 1 / numpy.zeros(3) 

411 >>> f() 

412 """ 

413 

414 keyword_arguments = kwargs 

415 

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

417 """Wrap specified function wrapper.""" 

418 

419 @functools.wraps(function) 

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

421 """Wrap specified function.""" 

422 

423 with np.errstate(**keyword_arguments): 

424 return function(*args, **kwargs) 

425 

426 return wrapped 

427 

428 return wrapper 

429 

430 

431ignore_numpy_errors = handle_numpy_errors(all="ignore") 

432raise_numpy_errors = handle_numpy_errors(all="raise") 

433print_numpy_errors = handle_numpy_errors(all="print") 

434warn_numpy_errors = handle_numpy_errors(all="warn") 

435 

436 

437def ignore_python_warnings(function: Callable) -> Callable: 

438 """ 

439 Decorate a function to ignore *Python* warnings. 

440 

441 Parameters 

442 ---------- 

443 function 

444 Function to decorate. 

445 

446 Returns 

447 ------- 

448 Callable 

449 Decorated function that suppresses *Python* warnings during 

450 execution. 

451 

452 Examples 

453 -------- 

454 >>> @ignore_python_warnings 

455 ... def f(): 

456 ... warnings.warn("This is an ignored warning!") 

457 >>> f() 

458 """ 

459 

460 @functools.wraps(function) 

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

462 """Wrap specified function.""" 

463 

464 with warnings.catch_warnings(): 

465 warnings.simplefilter("ignore") 

466 

467 return function(*args, **kwargs) 

468 

469 return wrapper 

470 

471 

472def attest(condition: bool | DTypeBoolean, message: str = "") -> None: 

473 """ 

474 Provide the ``assert`` statement functionality without being disabled by 

475 optimised Python execution. 

476 

477 Parameters 

478 ---------- 

479 condition 

480 Condition to attest/assert. 

481 message 

482 Message to display when the assertion fails. 

483 """ 

484 

485 if not condition: 

486 raise AssertionError(message) 

487 

488 

489def batch(sequence: Sequence, k: int | Literal[3] = 3) -> Generator: 

490 """ 

491 Generate batches from the specified sequence. 

492 

493 Parameters 

494 ---------- 

495 sequence 

496 Sequence to create batches from. 

497 k 

498 Batch size. 

499 

500 Yields 

501 ------ 

502 Generator 

503 Batch generator. 

504 

505 Examples 

506 -------- 

507 >>> batch(tuple(range(10)), 3) # doctest: +ELLIPSIS 

508 <generator object batch at 0x...> 

509 """ 

510 

511 for i in range(0, len(sequence), k): 

512 yield sequence[i : i + k] 

513 

514 

515_MULTIPROCESSING_ENABLED: bool = True 

516"""*Colour* multiprocessing state.""" 

517 

518 

519class disable_multiprocessing: 

520 """ 

521 Define a context manager and decorator to temporarily disable *Colour* 

522 multiprocessing state. 

523 """ 

524 

525 def __enter__(self) -> Self: 

526 """ 

527 Disable *Colour* multiprocessing state upon entering the context 

528 manager. 

529 """ 

530 

531 global _MULTIPROCESSING_ENABLED # noqa: PLW0603 

532 

533 _MULTIPROCESSING_ENABLED = False 

534 

535 return self 

536 

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

538 """ 

539 Enable *Colour* multiprocessing state upon exiting the context 

540 manager. 

541 """ 

542 

543 global _MULTIPROCESSING_ENABLED # noqa: PLW0603 

544 

545 _MULTIPROCESSING_ENABLED = True 

546 

547 def __call__(self, function: Callable) -> Callable: 

548 """ 

549 Execute the decorated function with optional multiprocessing support. 

550 """ 

551 

552 @functools.wraps(function) 

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

554 """Wrap specified function.""" 

555 

556 with self: 

557 return function(*args, **kwargs) 

558 

559 return wrapper 

560 

561 

562def _initializer(kwargs: Any) -> None: 

563 """ 

564 Initialize a multiprocessing pool worker process. 

565 

566 Ensure that worker processes on *Windows* correctly inherit the current 

567 domain-range scale configuration from the parent process. 

568 

569 Parameters 

570 ---------- 

571 kwargs 

572 Initialization arguments for configuring the worker process state. 

573 """ 

574 

575 # NOTE: No coverage information is available as this code is executed in 

576 # sub-processes. 

577 

578 import colour.utilities.array # pragma: no cover # noqa: PLC0415 

579 

580 colour.utilities.array._DOMAIN_RANGE_SCALE = kwargs.get( # noqa: SLF001 

581 "scale", "reference" 

582 ) # pragma: no cover 

583 

584 import colour.algebra.common # pragma: no cover # noqa: PLC0415 

585 

586 colour.algebra.common._SDIV_MODE = kwargs.get( # noqa: SLF001 

587 "sdiv_mode", "Ignore Zero Conversion" 

588 ) # pragma: no cover 

589 colour.algebra.common._SPOW_ENABLED = kwargs.get( # noqa: SLF001 

590 "spow_enabled", True 

591 ) # pragma: no cover 

592 

593 

594@contextmanager 

595def multiprocessing_pool(*args: Any, **kwargs: Any) -> Generator: 

596 """ 

597 Provide a context manager for a multiprocessing pool. 

598 

599 Other Parameters 

600 ---------------- 

601 args 

602 Arguments passed to the multiprocessing pool constructor. 

603 kwargs 

604 Keyword arguments passed to the multiprocessing pool 

605 constructor. 

606 

607 Yields 

608 ------ 

609 Generator 

610 Multiprocessing pool context manager. 

611 

612 Examples 

613 -------- 

614 >>> from functools import partial 

615 >>> def _add(a, b): 

616 ... return a + b 

617 >>> with multiprocessing_pool() as pool: 

618 ... pool.map(partial(_add, b=2), range(10)) 

619 ... # doctest: +SKIP 

620 [2, 3, 4, 5, 6, 7, 8, 9, 10, 11] 

621 """ 

622 

623 from colour.algebra import get_sdiv_mode, is_spow_enabled # noqa: PLC0415 

624 from colour.utilities import get_domain_range_scale # noqa: PLC0415 

625 

626 class _DummyPool: 

627 """ 

628 A dummy multiprocessing pool that does not perform multiprocessing. 

629 

630 Other Parameters 

631 ---------------- 

632 args 

633 Arguments. 

634 kwargs 

635 Keywords arguments. 

636 """ 

637 

638 def __init__(self, *args: Any, **kwargs: Any) -> None: 

639 pass 

640 

641 def map( 

642 self, 

643 func: Callable, 

644 iterable: Sequence, 

645 chunksize: int | None = None, # noqa: ARG002 

646 ) -> list[Any]: 

647 """Apply specified function to each element of the specified iterable.""" 

648 

649 return [func(a) for a in iterable] 

650 

651 def terminate(self) -> None: 

652 """Terminate the process.""" 

653 

654 kwargs["initializer"] = _initializer 

655 kwargs["initargs"] = ( 

656 { 

657 "scale": get_domain_range_scale(), 

658 "sdiv_mode": get_sdiv_mode(), 

659 "spow_enabled": is_spow_enabled(), 

660 }, 

661 ) 

662 

663 pool_factory: Callable 

664 if _MULTIPROCESSING_ENABLED: 

665 import multiprocessing # noqa: PLC0415 

666 

667 pool_factory = multiprocessing.Pool 

668 else: 

669 pool_factory = _DummyPool 

670 

671 pool = pool_factory(*args, **kwargs) 

672 

673 try: 

674 yield pool 

675 finally: 

676 pool.terminate() 

677 

678 

679def is_iterable(a: Any) -> bool: 

680 """ 

681 Determine whether the specified variable :math:`a` is iterable. 

682 

683 Parameters 

684 ---------- 

685 a 

686 Variable :math:`a` to check for iterability. 

687 

688 Returns 

689 ------- 

690 :class:`bool` 

691 Whether the variable :math:`a` is iterable. 

692 

693 Examples 

694 -------- 

695 >>> is_iterable([1, 2, 3]) 

696 True 

697 >>> is_iterable(1) 

698 False 

699 """ 

700 

701 return isinstance(a, str) or (bool(getattr(a, "__iter__", False))) 

702 

703 

704def is_numeric(a: Any) -> bool: 

705 """ 

706 Determine whether the specified variable :math:`a` is a 

707 :class:`Real`-like variable. 

708 

709 Parameters 

710 ---------- 

711 a 

712 Variable :math:`a` to test. 

713 

714 Returns 

715 ------- 

716 :class:`bool` 

717 Whether variable :math:`a` is a :class:`Real`-like variable. 

718 

719 Examples 

720 -------- 

721 >>> is_numeric(1) 

722 True 

723 >>> is_numeric((1,)) 

724 False 

725 """ 

726 

727 return isinstance( 

728 a, 

729 ( 

730 int, 

731 float, 

732 complex, 

733 np.integer, 

734 np.int8, 

735 np.int8, 

736 np.int16, 

737 np.int32, 

738 np.int64, 

739 np.uint8, 

740 np.uint16, 

741 np.uint32, 

742 np.uint64, 

743 np.floating, 

744 np.float16, 

745 np.float32, 

746 np.float64, 

747 np.complex64, 

748 np.complex128, 

749 ), # pyright: ignore 

750 ) 

751 

752 

753def is_integer(a: Any) -> bool: 

754 """ 

755 Determine whether the specified variable :math:`a` is an 

756 :class:`numpy.integer`-like variable under the specified threshold. 

757 

758 Parameters 

759 ---------- 

760 a 

761 Variable :math:`a` to test. 

762 

763 Returns 

764 ------- 

765 :class:`bool` 

766 Whether variable :math:`a` is an :class:`numpy.integer`-like 

767 variable. 

768 

769 Notes 

770 ----- 

771 - The determination threshold is defined by the 

772 :attr:`colour.algebra.common.THRESHOLD_INTEGER` attribute. 

773 

774 Examples 

775 -------- 

776 >>> is_integer(1) 

777 True 

778 >>> is_integer(1.01) 

779 False 

780 """ 

781 

782 return abs(a - np.around(a)) <= THRESHOLD_INTEGER 

783 

784 

785def is_sibling(element: Any, mapping: Mapping) -> bool: 

786 """ 

787 Determine whether the type of the specified element is present in the 

788 specified mapping types. 

789 

790 Parameters 

791 ---------- 

792 element 

793 Element to check whether its type is present in the mapping 

794 types. 

795 mapping 

796 Mapping types to check against. 

797 

798 Returns 

799 ------- 

800 :class:`bool` 

801 Whether the type of the specified element is present in the 

802 specified mapping types. 

803 """ 

804 

805 return isinstance(element, tuple({type(element) for element in mapping.values()})) 

806 

807 

808def filter_kwargs(function: Callable, **kwargs: Any) -> dict: 

809 """ 

810 Filter keyword arguments incompatible with the specified function 

811 signature. 

812 

813 Parameters 

814 ---------- 

815 function 

816 Callable to filter the incompatible keyword arguments against. 

817 

818 Other Parameters 

819 ---------------- 

820 kwargs 

821 Keyword arguments to be filtered. 

822 

823 Returns 

824 ------- 

825 dict 

826 Filtered keyword arguments compatible with the function signature. 

827 

828 Examples 

829 -------- 

830 >>> def fn_a(a): 

831 ... return a 

832 >>> def fn_b(a, b=0): 

833 ... return a, b 

834 >>> def fn_c(a, b=0, c=0): 

835 ... return a, b, c 

836 >>> fn_a(1, **filter_kwargs(fn_a, b=2, c=3)) 

837 1 

838 >>> fn_b(1, **filter_kwargs(fn_b, b=2, c=3)) 

839 (1, 2) 

840 >>> fn_c(1, **filter_kwargs(fn_c, b=2, c=3)) 

841 (1, 2, 3) 

842 """ 

843 

844 kwargs = copy(kwargs) 

845 

846 try: 

847 args = list(inspect.signature(function).parameters.keys()) 

848 except ValueError: # pragma: no cover 

849 return {} 

850 

851 for key in set(kwargs.keys()) - set(args): 

852 kwargs.pop(key) 

853 

854 return kwargs 

855 

856 

857def filter_mapping(mapping: Mapping, names: str | Sequence[str]) -> dict: 

858 """ 

859 Filter the specified mapping with specified names. 

860 

861 Parameters 

862 ---------- 

863 mapping 

864 Mapping to filter. 

865 names 

866 Name for the mapping elements to filter or a sequence of names. 

867 

868 Returns 

869 ------- 

870 dict 

871 Filtered mapping containing only the specified elements. 

872 

873 Notes 

874 ----- 

875 - If the mapping is a :class:`colour.utilities.CanonicalMapping` 

876 instance, then the lower, slugified and canonical keys are also 

877 used for matching. 

878 - To honour the filterers ordering, the return value is a 

879 :class:`dict` instance. 

880 

881 Examples 

882 -------- 

883 >>> class Element: 

884 ... pass 

885 >>> mapping = { 

886 ... "Element A": Element(), 

887 ... "Element B": Element(), 

888 ... "Element C": Element(), 

889 ... "Not Element C": Element(), 

890 ... } 

891 >>> filter_mapping(mapping, "Element A") # doctest: +ELLIPSIS 

892 {'Element A': <colour.utilities.common.Element object at 0x...>} 

893 """ 

894 

895 def filter_mapping_with_name(mapping: Mapping, name: str) -> dict: 

896 """ 

897 Filter specified mapping with the specified name. 

898 

899 Parameters 

900 ---------- 

901 mapping 

902 Mapping to filter. 

903 name 

904 Name for the specified mapping elements. 

905 

906 Returns 

907 ------- 

908 dict 

909 Filtered mapping elements. 

910 """ 

911 

912 keys = list(mapping.keys()) 

913 

914 if isinstance(mapping, CanonicalMapping): 

915 keys += list(mapping.lower_keys()) 

916 keys += list(mapping.slugified_keys()) 

917 keys += list(mapping.canonical_keys()) 

918 

919 elements = [mapping[key] for key in keys if name == key] 

920 

921 lookup = Lookup(mapping) 

922 

923 return {lookup.first_key_from_value(element): element for element in elements} 

924 

925 names = [str(names)] if isinstance(names, str) else names 

926 

927 filtered_mapping = {} 

928 

929 for filterer in names: 

930 filtered_mapping.update(filter_mapping_with_name(mapping, filterer)) 

931 

932 return filtered_mapping 

933 

934 

935def first_item(a: Iterable) -> Any: 

936 """ 

937 Return the first item from the specified iterable. 

938 

939 Parameters 

940 ---------- 

941 a 

942 Iterable to retrieve the first item from. 

943 

944 Returns 

945 ------- 

946 :class:`object` 

947 First item from the iterable. 

948 

949 Raises 

950 ------ 

951 :class:`StopIteration` 

952 If the iterable is empty. 

953 

954 Examples 

955 -------- 

956 >>> a = range(10) 

957 >>> first_item(a) 

958 0 

959 """ 

960 

961 return next(iter(a)) 

962 

963 

964def copy_definition(definition: Callable, name: str | None = None) -> Callable: 

965 """ 

966 Copy a definition using the same code, globals, defaults, closure, and 

967 name. 

968 

969 Parameters 

970 ---------- 

971 definition 

972 Definition to be copied. 

973 name 

974 Optional name for the definition copy. 

975 

976 Returns 

977 ------- 

978 Callable 

979 Copy of the specified definition. 

980 """ 

981 

982 copy = types.FunctionType( 

983 definition.__code__, 

984 definition.__globals__, 

985 str(name or definition.__name__), 

986 definition.__defaults__, 

987 definition.__closure__, 

988 ) 

989 copy.__dict__.update(definition.__dict__) 

990 copy.__annotations__ = definition.__annotations__.copy() 

991 

992 return copy 

993 

994 

995@functools.cache 

996def validate_method( 

997 method: str, 

998 valid_methods: tuple, 

999 message: str = '"{0}" method is invalid, it must be one of {1}!', 

1000 as_lowercase: bool = True, 

1001) -> str: 

1002 """ 

1003 Validate whether the specified method exists in the specified valid 

1004 methods and optionally return the method lower cased. 

1005 

1006 Parameters 

1007 ---------- 

1008 method 

1009 Method to validate. 

1010 valid_methods 

1011 Valid methods. 

1012 message 

1013 Message for the exception. 

1014 as_lowercase 

1015 Whether to convert the specified method to lower case or not. 

1016 

1017 Returns 

1018 ------- 

1019 :class:`str` 

1020 Method optionally lower cased. 

1021 

1022 Raises 

1023 ------ 

1024 :class:`ValueError` 

1025 If the method does not exist. 

1026 

1027 Examples 

1028 -------- 

1029 >>> validate_method("Valid", ("Valid", "Yes", "Ok")) 

1030 'valid' 

1031 >>> validate_method("Valid", ("Valid", "Yes", "Ok"), as_lowercase=False) 

1032 'Valid' 

1033 """ 

1034 

1035 valid_methods = tuple([str(valid_method) for valid_method in valid_methods]) 

1036 

1037 method_lower = method.lower() 

1038 if method_lower not in [valid_method.lower() for valid_method in valid_methods]: 

1039 raise ValueError(message.format(method, valid_methods)) 

1040 

1041 return method_lower if as_lowercase else method 

1042 

1043 

1044T = TypeVar("T") 

1045 

1046 

1047def optional(value: T | None, default: T) -> T: 

1048 """ 

1049 Return the specified value or a default if the value is *None*. 

1050 

1051 Parameters 

1052 ---------- 

1053 value 

1054 Optional argument value. 

1055 default 

1056 Default argument value if ``value`` is *None*. 

1057 

1058 Returns 

1059 ------- 

1060 T 

1061 Argument value. 

1062 

1063 Examples 

1064 -------- 

1065 >>> optional("Foo", "Bar") 

1066 'Foo' 

1067 >>> optional(None, "Bar") 

1068 'Bar' 

1069 """ 

1070 

1071 if value is None: 

1072 return default 

1073 

1074 return value 

1075 

1076 

1077def slugify(object_: Any, allow_unicode: bool = False) -> str: 

1078 """ 

1079 Generate a *SEO* friendly and human-readable slug from the specified 

1080 object. 

1081 

1082 Convert to ASCII if ``allow_unicode`` is *False*. Convert spaces or 

1083 repeated dashes to single dashes. Remove characters that are not 

1084 alphanumerics, underscores, or hyphens. Convert to lowercase. Strip 

1085 leading and trailing whitespace, dashes, and underscores. 

1086 

1087 Parameters 

1088 ---------- 

1089 object_ 

1090 Object to convert to a slug. 

1091 allow_unicode 

1092 Whether to allow unicode characters in the generated slug. 

1093 

1094 Returns 

1095 ------- 

1096 :class:`str` 

1097 Generated slug. 

1098 

1099 References 

1100 ---------- 

1101 :cite:`DjangoSoftwareFoundation2022` 

1102 

1103 Examples 

1104 -------- 

1105 >>> slugify(" Jack & Jill like numbers 1,2,3 and 4 and silly characters ?%.$!/") 

1106 'jack-jill-like-numbers-123-and-4-and-silly-characters' 

1107 """ 

1108 

1109 value = str(object_) 

1110 

1111 if allow_unicode: 

1112 value = unicodedata.normalize("NFKC", value) 

1113 else: 

1114 value = ( 

1115 unicodedata.normalize("NFKD", value) 

1116 .encode("ascii", "ignore") 

1117 .decode("ascii") 

1118 ) 

1119 

1120 value = re.sub(r"[^\w\s-]", "", value.lower()) 

1121 

1122 return re.sub(r"[-\s]+", "-", value).strip("-_") 

1123 

1124 

1125if is_xxhash_installed(): 

1126 import xxhash 

1127 

1128 int_digest = xxhash.xxh3_64_intdigest 

1129else: 

1130 int_digest = hash # pragma: no cover