Skip to content

Commit

Permalink
Implement support for callbacks in `colour.continuous.AbstractContinu…
Browse files Browse the repository at this point in the history
…ousFunction` class.
  • Loading branch information
KelSolaar committed Apr 30, 2023
1 parent 8e0c9c1 commit ffbb0a0
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 14 deletions.
34 changes: 25 additions & 9 deletions colour/colorimetry/spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,19 @@ def __init__(
self._display_name: str = self.name
self.display_name = kwargs.get("display_name", self._display_name)

self._shape: SpectralShape | None = None

def _on_domain_changed(
self, name: str, value: ArrayLike
) -> NDArrayFloat:
"""Invalidate *self._shape* when *self._domain* is changed."""
if name == "_domain":
self._shape = None

return value

self.register_callback("on_domain_changed", _on_domain_changed)

@property
def display_name(self) -> str:
"""
Expand Down Expand Up @@ -836,17 +849,20 @@ def shape(self) -> SpectralShape:
SpectralShape(500.0, 600.0, 10.0)
"""

wavelengths = self.wavelengths
wavelengths_interval = interval(wavelengths)
if wavelengths_interval.size != 1:
runtime_warning(
f'"{self.name}" spectral distribution is not uniform, using '
f"minimum interval!"
if self._shape is None:
wavelengths = self.wavelengths
wavelengths_interval = interval(wavelengths)
if wavelengths_interval.size != 1:
runtime_warning(
f'"{self.name}" spectral distribution is not uniform, '
"using minimum interval!"
)

self._shape = SpectralShape(
wavelengths[0], wavelengths[-1], min(wavelengths_interval)
)

return SpectralShape(
wavelengths[0], wavelengths[-1], min(wavelengths_interval)
)
return self._shape

def interpolate(
self,
Expand Down
3 changes: 2 additions & 1 deletion colour/continuous/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
Type,
)
from colour.utilities import (
MixinCallback,
as_float,
attest,
closest,
Expand All @@ -49,7 +50,7 @@
]


class AbstractContinuousFunction(ABC):
class AbstractContinuousFunction(ABC, MixinCallback):
"""
Define the base class for abstract continuous function.
Expand Down
6 changes: 2 additions & 4 deletions colour/continuous/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -948,8 +948,7 @@ def _fill_domain_nan(
variable.
"""

self._domain = fill_nan(self._domain, method, default)
self._function = None # Invalidate the underlying continuous function.
self.domain = fill_nan(self.domain, method, default)

def _fill_range_nan(
self,
Expand All @@ -974,8 +973,7 @@ def _fill_range_nan(
variable.
"""

self._range = fill_nan(self._range, method, default)
self._function = None # Invalidate the underlying continuous function.
self.range = fill_nan(self.range, method, default)

def arithmetical_operation(
self,
Expand Down
8 changes: 8 additions & 0 deletions colour/utilities/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
LazyCanonicalMapping,
Node,
)
from .callback import (
Callback,
MixinCallback,
)
from .common import (
CacheRegistry,
CACHE_REGISTRY,
Expand Down Expand Up @@ -124,6 +128,10 @@
"LazyCanonicalMapping",
"Node",
]
__all__ += [
"Callback",
"MixinCallback",
]
__all__ += [
"CacheRegistry",
"CACHE_REGISTRY",
Expand Down
168 changes: 168 additions & 0 deletions colour/utilities/callback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""
Callback Management
===================
Defines the callback management objects.
"""

from __future__ import annotations

from dataclasses import dataclass

from colour.hints import (
Any,
Callable,
)

__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "New BSD License - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"

__all__ = [
"Callback",
"MixinCallback",
]


@dataclass
class Callback:
"""
Define a callback.
Parameters
----------
name
Callback name.
function
Callback callable.
"""

name: str
function: Callable


class MixinCallback:
"""
A mixin providing support for callbacks.
Attributes
----------
- :attr:`~colour.utilities.MixinCallback.callbacks`
- :attr:`~colour.utilities.MixinCallback.__setattr__`
Methods
-------
- :meth:`~colour.utilities.MixinCallback.register_callback`
- :meth:`~colour.utilities.MixinCallback.unregister_callback`
Examples
--------
>>> class WithCallback(MixinCallback):
... def __init__(self):
... super().__init__()
... self.attribute_a = "a"
...
>>> with_callback = WithCallback()
>>> def _on_attribute_a_changed(self, name: str, value: str) -> str:
... if name == "attribute_a":
... value = value.upper()
... return value
>>> with_callback.register_callback(
... "on_attribute_a_changed", _on_attribute_a_changed
... )
>>> with_callback.attribute_a = "a"
>>> with_callback.attribute_a
'A'
"""

def __init__(self) -> None:
super().__init__()

self._callbacks: list = []

@property
def callbacks(self) -> list:
"""
Getter property for the callbacks.
Returns
-------
:class:`list`
Callbacks.
"""

return self._callbacks

def __setattr__(self, name: str, value: Any) -> None:
"""
Set given value to the attribute with given name.
Parameters
----------
attribute
Attribute to set the value of.
value
Value to set the attribute with.
"""

if hasattr(self, "_callbacks"):
for callback in self._callbacks:
value = callback.function(self, name, value)

super().__setattr__(name, value)

def register_callback(self, name: str, function: Callable) -> None:
"""
Register the callback with given name.
Parameters
----------
name
Callback name.
function
Callback callable.
Examples
--------
>>> class WithCallback(MixinCallback):
... def __init__(self):
... super().__init__()
...
>>> with_callback = WithCallback()
>>> with_callback.register_callback("callback", lambda *args: None)
>>> with_callback.callbacks # doctest: +SKIP
[Callback(name='callback', function=<function <lambda> at 0x10fcf3420>)]
"""

self._callbacks.append(Callback(name, function))

def unregister_callback(self, name: str) -> None:
"""
Unregister the callback with given name.
Parameters
----------
name
Callback name.
Examples
--------
>>> class WithCallback(MixinCallback):
... def __init__(self):
... super().__init__()
...
>>> with_callback = WithCallback()
>>> with_callback.register_callback("callback", lambda s, n, v: v)
>>> with_callback.callbacks # doctest: +SKIP
[Callback(name='callback', function=<function <lambda> at 0x10fcf3420>)]
>>> with_callback.unregister_callback("callback")
>>> with_callback.callbacks
[]
"""

self._callbacks = [
callback for callback in self._callbacks if callback.name != name
]
109 changes: 109 additions & 0 deletions colour/utilities/tests/test_callback.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# !/usr/bin/env python
"""Define the unit tests for the :mod:`colour.utilities.callback` module."""

from __future__ import annotations

import unittest

from colour.utilities import MixinCallback

__author__ = "Colour Developers"
__copyright__ = "Copyright 2013 Colour Developers"
__license__ = "New BSD License - https://opensource.org/licenses/BSD-3-Clause"
__maintainer__ = "Colour Developers"
__email__ = "colour-developers@colour-science.org"
__status__ = "Production"

__all__ = [
"TestMixinCallback",
]


class TestMixinCallback(unittest.TestCase):
"""
Define :class:`colour.utilities.callback.MixinCallback` class unit
tests methods.
"""

def setUp(self):
"""Initialise the common tests attributes."""

class WithCallback(MixinCallback):
"""Test :class:`MixinCallback` class."""

def __init__(self):
super().__init__()

self.attribute_a = "a"

self._with_callback = WithCallback()

def _on_attribute_a_changed(self, name: str, value: str) -> str:
"""Transform *self._attribute_a* to uppercase."""

if name == "attribute_a":
value = value.upper()

if getattr(self, name) != "a":
raise RuntimeError(
'"self" was not able to retrieve class instance value!'
)

return value

self._on_attribute_a_changed = _on_attribute_a_changed

def test_required_attributes(self):
"""Test the presence of required attributes."""

required_attributes = ("callbacks",)

for attribute in required_attributes:
self.assertIn(attribute, dir(MixinCallback))

def test_required_methods(self):
"""Test the presence of required methods."""

required_methods = (
"__init__",
"register_callback",
"unregister_callback",
)

for method in required_methods:
self.assertIn(method, dir(MixinCallback))

def test_register_callback(self):
"""
Test :class:`colour.utilities.callback.MixinCallback.register_callback`
method.
"""

self._with_callback.register_callback(
"on_attribute_a_changed", self._on_attribute_a_changed
)

self._with_callback.attribute_a = "a"
self.assertEqual(self._with_callback.attribute_a, "A")
self.assertEqual(len(self._with_callback.callbacks), 1)

def test_unregister_callback(self):
"""
Test :class:`colour.utilities.callback.MixinCallback.unregister_callback`
method.
"""

if len(self._with_callback.callbacks) == 0:
self._with_callback.register_callback(
"on_attribute_a_changed", self._on_attribute_a_changed
)

self.assertEqual(len(self._with_callback.callbacks), 1)
self._with_callback.unregister_callback("on_attribute_a_changed")
self.assertEqual(len(self._with_callback.callbacks), 0)
self._with_callback.attribute_a = "a"
self.assertEqual(self._with_callback.attribute_a, "a")


if __name__ == "__main__":
unittest.main()
Loading

0 comments on commit ffbb0a0

Please sign in to comment.