Coverage for utilities/common.py: 52%

1"""

2 Common Utilities

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

4

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

6

7 References

8----------

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

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

11 blob/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/\

16 numpyerrors.html

17"""

18

19 from __future__ import annotations

20

21 import functools

22 import inspect

23 import os

24 import re

25 import types

26 import typing

27 import unicodedata

28 import warnings

29 from contextlib import contextmanager

30 from copy import copy

31 from pprint import pformat

32

33 import numpy as np

34

35 from colour.constants import THRESHOLD_INTEGER

36 from colour.utilities import as_bool

37

38 if 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

51 from colour.hints import TypeVar

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

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

96"""

97

98

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

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

150 class 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

195 class 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

379 CACHE_REGISTRY: CacheRegistry = CacheRegistry()

380"""

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

382 processes.

383"""

384

385

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

431 ignore_numpy_errors = handle_numpy_errors(all="ignore")

432 raise_numpy_errors = handle_numpy_errors(all="raise")

433 print_numpy_errors = handle_numpy_errors(all="print")

434 warn_numpy_errors = handle_numpy_errors(all="warn")

435

436

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

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

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

519 class 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

562 def _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

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

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

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

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

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

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

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

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

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

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

1044 T = TypeVar("T")

1045

1046

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

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

1125 if is_xxhash_installed():

1126 import xxhash

1127

1128 int_digest = xxhash.xxh3_64_intdigest

1129 else:

1130 int_digest = hash # pragma: no cover