Coverage for utilities/common.py: 52%
186 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-16 22:49 +1300
1"""
2Common Utilities
3================
5Provide common utility objects that don't fall in any specific category.
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"""
19from __future__ import annotations
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
33import numpy as np
35from colour.constants import THRESHOLD_INTEGER
36from colour.utilities import as_bool
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 )
51from colour.hints import TypeVar
52from colour.utilities import CanonicalMapping, Lookup, is_xxhash_installed
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"
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]
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"""
99def is_caching_enabled() -> bool:
100 """
101 Determine whether *Colour* caching is enabled.
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.
108 Returns
109 -------
110 :class:`bool`
111 Whether *Colour* caching is enabled.
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 """
123 return _CACHING_ENABLED
126def set_caching_enable(enable: bool) -> None:
127 """
128 Set the *Colour* caching enabled state.
130 Parameters
131 ----------
132 enable
133 Whether to enable *Colour* caching.
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 """
145 global _CACHING_ENABLED # noqa: PLW0603
147 _CACHING_ENABLED = enable
150class caching_enable:
151 """
152 Define a context manager and decorator to temporarily set the *Colour*
153 caching enabled state.
155 Parameters
156 ----------
157 enable
158 Whether to enable or disable *Colour* caching.
159 """
161 def __init__(self, enable: bool) -> None:
162 self._enable = enable
163 self._previous_state = is_caching_enabled()
165 def __enter__(self) -> Self:
166 """
167 Enter the caching context and set the *Colour* caching state.
168 """
170 set_caching_enable(self._enable)
172 return self
174 def __exit__(self, *args: Any) -> None:
175 """
176 Exit the caching context manager and restore the previous *Colour*
177 caching state.
178 """
180 set_caching_enable(self._previous_state)
182 def __call__(self, function: Callable) -> Callable:
183 """
184 Decorate and call the specified function with caching control.
185 """
187 @functools.wraps(function)
188 def wrapper(*args: Any, **kwargs: Any) -> Any:
189 with self:
190 return function(*args, **kwargs)
192 return wrapper
195class CacheRegistry:
196 """
197 Provide a registry for managing mapping-based caches.
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.
203 Attributes
204 ----------
205 - :attr:`~colour.utilities.CacheRegistry.registry`
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`
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 """
236 def __init__(self) -> None:
237 self._registry: dict = {}
239 @property
240 def registry(self) -> dict:
241 """
242 Getter for the cache registry.
244 Returns
245 -------
246 :class:`dict`
247 Cache registry containing cached computation results.
248 """
250 return self._registry
252 def __str__(self) -> str:
253 """
254 Return a formatted string representation of the cache registry.
256 Returns
257 -------
258 :class:`str`
259 Formatted string representation.
260 """
262 return pformat(
263 {
264 name: f"{len(self._registry[name])} item(s)"
265 for name in sorted(self._registry)
266 }
267 )
269 def register_cache(self, name: str) -> dict:
270 """
271 Register a new cache with the specified name in the registry.
273 Parameters
274 ----------
275 name
276 Cache name for the registry.
278 Returns
279 -------
280 :class:`dict`
281 Registered cache.
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 """
295 self._registry[name] = {}
297 return self._registry[name]
299 def unregister_cache(self, name: str) -> None:
300 """
301 Unregister the cache with the specified name from the registry.
303 Parameters
304 ----------
305 name
306 Cache name in the registry.
308 Notes
309 -----
310 - The cache is cleared before being unregistered.
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 """
329 self.clear_cache(name)
331 del self._registry[name]
333 def clear_cache(self, name: str) -> None:
334 """
335 Clear the cache with the specified name.
337 Parameters
338 ----------
339 name
340 Cache name in the registry.
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 """
354 self._registry[name].clear()
356 def clear_all_caches(self) -> None:
357 """
358 Clear all caches in the registry.
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 """
375 for key in self._registry:
376 self.clear_cache(key)
379CACHE_REGISTRY: CacheRegistry = CacheRegistry()
380"""
381*Colour* cache registry referencing all the caches used for repetitive or long
382processes.
383"""
386def handle_numpy_errors(**kwargs: Any) -> Callable:
387 """
388 Handle *Numpy* errors through function decoration.
390 Other Parameters
391 ----------------
392 kwargs
393 Keyword arguments passed to :func:`numpy.seterr` to control
394 error handling behaviour.
396 Returns
397 -------
398 Callable
399 Decorated function with specified *Numpy* error handling.
401 References
402 ----------
403 :cite:`Kienzle2011a`
405 Examples
406 --------
407 >>> import numpy
408 >>> @handle_numpy_errors(all="ignore")
409 ... def f():
410 ... 1 / numpy.zeros(3)
411 >>> f()
412 """
414 keyword_arguments = kwargs
416 def wrapper(function: Callable) -> Callable:
417 """Wrap specified function wrapper."""
419 @functools.wraps(function)
420 def wrapped(*args: Any, **kwargs: Any) -> Any:
421 """Wrap specified function."""
423 with np.errstate(**keyword_arguments):
424 return function(*args, **kwargs)
426 return wrapped
428 return wrapper
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")
437def ignore_python_warnings(function: Callable) -> Callable:
438 """
439 Decorate a function to ignore *Python* warnings.
441 Parameters
442 ----------
443 function
444 Function to decorate.
446 Returns
447 -------
448 Callable
449 Decorated function that suppresses *Python* warnings during
450 execution.
452 Examples
453 --------
454 >>> @ignore_python_warnings
455 ... def f():
456 ... warnings.warn("This is an ignored warning!")
457 >>> f()
458 """
460 @functools.wraps(function)
461 def wrapper(*args: Any, **kwargs: Any) -> Any:
462 """Wrap specified function."""
464 with warnings.catch_warnings():
465 warnings.simplefilter("ignore")
467 return function(*args, **kwargs)
469 return wrapper
472def attest(condition: bool | DTypeBoolean, message: str = "") -> None:
473 """
474 Provide the ``assert`` statement functionality without being disabled by
475 optimised Python execution.
477 Parameters
478 ----------
479 condition
480 Condition to attest/assert.
481 message
482 Message to display when the assertion fails.
483 """
485 if not condition:
486 raise AssertionError(message)
489def batch(sequence: Sequence, k: int | Literal[3] = 3) -> Generator:
490 """
491 Generate batches from the specified sequence.
493 Parameters
494 ----------
495 sequence
496 Sequence to create batches from.
497 k
498 Batch size.
500 Yields
501 ------
502 Generator
503 Batch generator.
505 Examples
506 --------
507 >>> batch(tuple(range(10)), 3) # doctest: +ELLIPSIS
508 <generator object batch at 0x...>
509 """
511 for i in range(0, len(sequence), k):
512 yield sequence[i : i + k]
515_MULTIPROCESSING_ENABLED: bool = True
516"""*Colour* multiprocessing state."""
519class disable_multiprocessing:
520 """
521 Define a context manager and decorator to temporarily disable *Colour*
522 multiprocessing state.
523 """
525 def __enter__(self) -> Self:
526 """
527 Disable *Colour* multiprocessing state upon entering the context
528 manager.
529 """
531 global _MULTIPROCESSING_ENABLED # noqa: PLW0603
533 _MULTIPROCESSING_ENABLED = False
535 return self
537 def __exit__(self, *args: Any) -> None:
538 """
539 Enable *Colour* multiprocessing state upon exiting the context
540 manager.
541 """
543 global _MULTIPROCESSING_ENABLED # noqa: PLW0603
545 _MULTIPROCESSING_ENABLED = True
547 def __call__(self, function: Callable) -> Callable:
548 """
549 Execute the decorated function with optional multiprocessing support.
550 """
552 @functools.wraps(function)
553 def wrapper(*args: Any, **kwargs: Any) -> Any:
554 """Wrap specified function."""
556 with self:
557 return function(*args, **kwargs)
559 return wrapper
562def _initializer(kwargs: Any) -> None:
563 """
564 Initialize a multiprocessing pool worker process.
566 Ensure that worker processes on *Windows* correctly inherit the current
567 domain-range scale configuration from the parent process.
569 Parameters
570 ----------
571 kwargs
572 Initialization arguments for configuring the worker process state.
573 """
575 # NOTE: No coverage information is available as this code is executed in
576 # sub-processes.
578 import colour.utilities.array # pragma: no cover # noqa: PLC0415
580 colour.utilities.array._DOMAIN_RANGE_SCALE = kwargs.get( # noqa: SLF001
581 "scale", "reference"
582 ) # pragma: no cover
584 import colour.algebra.common # pragma: no cover # noqa: PLC0415
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
594@contextmanager
595def multiprocessing_pool(*args: Any, **kwargs: Any) -> Generator:
596 """
597 Provide a context manager for a multiprocessing pool.
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.
607 Yields
608 ------
609 Generator
610 Multiprocessing pool context manager.
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 """
623 from colour.algebra import get_sdiv_mode, is_spow_enabled # noqa: PLC0415
624 from colour.utilities import get_domain_range_scale # noqa: PLC0415
626 class _DummyPool:
627 """
628 A dummy multiprocessing pool that does not perform multiprocessing.
630 Other Parameters
631 ----------------
632 args
633 Arguments.
634 kwargs
635 Keywords arguments.
636 """
638 def __init__(self, *args: Any, **kwargs: Any) -> None:
639 pass
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."""
649 return [func(a) for a in iterable]
651 def terminate(self) -> None:
652 """Terminate the process."""
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 )
663 pool_factory: Callable
664 if _MULTIPROCESSING_ENABLED:
665 import multiprocessing # noqa: PLC0415
667 pool_factory = multiprocessing.Pool
668 else:
669 pool_factory = _DummyPool
671 pool = pool_factory(*args, **kwargs)
673 try:
674 yield pool
675 finally:
676 pool.terminate()
679def is_iterable(a: Any) -> bool:
680 """
681 Determine whether the specified variable :math:`a` is iterable.
683 Parameters
684 ----------
685 a
686 Variable :math:`a` to check for iterability.
688 Returns
689 -------
690 :class:`bool`
691 Whether the variable :math:`a` is iterable.
693 Examples
694 --------
695 >>> is_iterable([1, 2, 3])
696 True
697 >>> is_iterable(1)
698 False
699 """
701 return isinstance(a, str) or (bool(getattr(a, "__iter__", False)))
704def is_numeric(a: Any) -> bool:
705 """
706 Determine whether the specified variable :math:`a` is a
707 :class:`Real`-like variable.
709 Parameters
710 ----------
711 a
712 Variable :math:`a` to test.
714 Returns
715 -------
716 :class:`bool`
717 Whether variable :math:`a` is a :class:`Real`-like variable.
719 Examples
720 --------
721 >>> is_numeric(1)
722 True
723 >>> is_numeric((1,))
724 False
725 """
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 )
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.
758 Parameters
759 ----------
760 a
761 Variable :math:`a` to test.
763 Returns
764 -------
765 :class:`bool`
766 Whether variable :math:`a` is an :class:`numpy.integer`-like
767 variable.
769 Notes
770 -----
771 - The determination threshold is defined by the
772 :attr:`colour.algebra.common.THRESHOLD_INTEGER` attribute.
774 Examples
775 --------
776 >>> is_integer(1)
777 True
778 >>> is_integer(1.01)
779 False
780 """
782 return abs(a - np.around(a)) <= THRESHOLD_INTEGER
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.
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.
798 Returns
799 -------
800 :class:`bool`
801 Whether the type of the specified element is present in the
802 specified mapping types.
803 """
805 return isinstance(element, tuple({type(element) for element in mapping.values()}))
808def filter_kwargs(function: Callable, **kwargs: Any) -> dict:
809 """
810 Filter keyword arguments incompatible with the specified function
811 signature.
813 Parameters
814 ----------
815 function
816 Callable to filter the incompatible keyword arguments against.
818 Other Parameters
819 ----------------
820 kwargs
821 Keyword arguments to be filtered.
823 Returns
824 -------
825 dict
826 Filtered keyword arguments compatible with the function signature.
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 """
844 kwargs = copy(kwargs)
846 try:
847 args = list(inspect.signature(function).parameters.keys())
848 except ValueError: # pragma: no cover
849 return {}
851 for key in set(kwargs.keys()) - set(args):
852 kwargs.pop(key)
854 return kwargs
857def filter_mapping(mapping: Mapping, names: str | Sequence[str]) -> dict:
858 """
859 Filter the specified mapping with specified names.
861 Parameters
862 ----------
863 mapping
864 Mapping to filter.
865 names
866 Name for the mapping elements to filter or a sequence of names.
868 Returns
869 -------
870 dict
871 Filtered mapping containing only the specified elements.
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.
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 """
895 def filter_mapping_with_name(mapping: Mapping, name: str) -> dict:
896 """
897 Filter specified mapping with the specified name.
899 Parameters
900 ----------
901 mapping
902 Mapping to filter.
903 name
904 Name for the specified mapping elements.
906 Returns
907 -------
908 dict
909 Filtered mapping elements.
910 """
912 keys = list(mapping.keys())
914 if isinstance(mapping, CanonicalMapping):
915 keys += list(mapping.lower_keys())
916 keys += list(mapping.slugified_keys())
917 keys += list(mapping.canonical_keys())
919 elements = [mapping[key] for key in keys if name == key]
921 lookup = Lookup(mapping)
923 return {lookup.first_key_from_value(element): element for element in elements}
925 names = [str(names)] if isinstance(names, str) else names
927 filtered_mapping = {}
929 for filterer in names:
930 filtered_mapping.update(filter_mapping_with_name(mapping, filterer))
932 return filtered_mapping
935def first_item(a: Iterable) -> Any:
936 """
937 Return the first item from the specified iterable.
939 Parameters
940 ----------
941 a
942 Iterable to retrieve the first item from.
944 Returns
945 -------
946 :class:`object`
947 First item from the iterable.
949 Raises
950 ------
951 :class:`StopIteration`
952 If the iterable is empty.
954 Examples
955 --------
956 >>> a = range(10)
957 >>> first_item(a)
958 0
959 """
961 return next(iter(a))
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.
969 Parameters
970 ----------
971 definition
972 Definition to be copied.
973 name
974 Optional name for the definition copy.
976 Returns
977 -------
978 Callable
979 Copy of the specified definition.
980 """
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()
992 return copy
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.
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.
1017 Returns
1018 -------
1019 :class:`str`
1020 Method optionally lower cased.
1022 Raises
1023 ------
1024 :class:`ValueError`
1025 If the method does not exist.
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 """
1035 valid_methods = tuple([str(valid_method) for valid_method in valid_methods])
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))
1041 return method_lower if as_lowercase else method
1044T = TypeVar("T")
1047def optional(value: T | None, default: T) -> T:
1048 """
1049 Return the specified value or a default if the value is *None*.
1051 Parameters
1052 ----------
1053 value
1054 Optional argument value.
1055 default
1056 Default argument value if ``value`` is *None*.
1058 Returns
1059 -------
1060 T
1061 Argument value.
1063 Examples
1064 --------
1065 >>> optional("Foo", "Bar")
1066 'Foo'
1067 >>> optional(None, "Bar")
1068 'Bar'
1069 """
1071 if value is None:
1072 return default
1074 return value
1077def slugify(object_: Any, allow_unicode: bool = False) -> str:
1078 """
1079 Generate a *SEO* friendly and human-readable slug from the specified
1080 object.
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.
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.
1094 Returns
1095 -------
1096 :class:`str`
1097 Generated slug.
1099 References
1100 ----------
1101 :cite:`DjangoSoftwareFoundation2022`
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 """
1109 value = str(object_)
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 )
1120 value = re.sub(r"[^\w\s-]", "", value.lower())
1122 return re.sub(r"[-\s]+", "-", value).strip("-_")
1125if is_xxhash_installed():
1126 import xxhash
1128 int_digest = xxhash.xxh3_64_intdigest
1129else:
1130 int_digest = hash # pragma: no cover