{"id":15014434,"url":"https://github.com/andyrids/ipymediator","last_synced_at":"2025-12-26T10:57:32.247Z","repository":{"id":231919228,"uuid":"783007969","full_name":"andyrids/ipymediator","owner":"andyrids","description":"Communication between ipywidgets for use in Jupyter/IPython","archived":false,"fork":false,"pushed_at":"2024-05-16T22:09:01.000Z","size":175,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-13T00:17:29.021Z","etag":null,"topics":["ipython","ipywidgets","jupyter-notebook","mediator","poetry","traitlets"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/andyrids.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-04-06T17:19:10.000Z","updated_at":"2025-01-30T21:23:53.000Z","dependencies_parsed_at":"2024-04-15T22:25:38.360Z","dependency_job_id":"6758c0f9-17e5-4539-b497-2c950e567267","html_url":"https://github.com/andyrids/ipymediator","commit_stats":null,"previous_names":["andyrids/ipydialog","andyrids/ipydialogs","andyrids/ipymediator"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyrids%2Fipymediator","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyrids%2Fipymediator/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyrids%2Fipymediator/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/andyrids%2Fipymediator/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/andyrids","download_url":"https://codeload.github.com/andyrids/ipymediator/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243313409,"owners_count":20271178,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ipython","ipywidgets","jupyter-notebook","mediator","poetry","traitlets"],"created_at":"2024-09-24T19:45:37.887Z","updated_at":"2025-12-26T10:57:32.197Z","avatar_url":"https://github.com/andyrids.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ipymediator: communication between ipywidgets\n\n## Introduction\n\n\u003c!-- [!TIP] [!IMPORTANT] [!WARNING] [!CAUTION] --\u003e\n\n\u003e [!NOTE]\n\u003e This package is a a work in progress.\n\nThis package was created in an attempt to utlise the Mediator behavioral design pattern, to facilitate communication between ipywidgets HTML widgets, using a Mediator and Component interface. Concrete Component objects utilise these widgets through composition and automatically pass messages to a Mediator's notify method, by leaveraging its widget's `observe` method. A Component's widget, traits to observe and Mediator are passed as paramters on initialisation.\n\nThe package was inspired by [ipyfilechooser](https://github.com/crahan/ipyfilechooser/tree/master) and [leafmap](https://github.com/opengeos/leafmap) and was designed as part of a solution a specific project involving a custom ipyleaflet WidgetControl implimentation for geospatial data import.\n\n[**ipywidgets**](https://ipywidgets.readthedocs.io/en/stable/) provides interactive HTML widgets for Jupyter notebooks and the IPython kernel.\n\n[**traitlets**](https://traitlets.readthedocs.io/en/stable/using_traitlets.html) allows the user to define classes that have attributes (traits) with type checking and dynamically computed default values. Traitlets also implements the observer pattern and is used by ipywidgets extensively.\n\n\u003e [!TIP]\n\u003e Mediator dialogs built using this interface will be added to the dialogs module.\n\n## Install\n\nCurrently this package can be installed with the following command:\n\n`pip install git+https://github.com/AndyRids/ipymediator.git@main`\n\nThis package was built using [**Poetry**](https://python-poetry.org/docs/), which is a tool for dependency management and packaging in Python. The reposititory can be cloned and have the necessary dependancies installed using the following commands:\n\n```bash\ngit clone https://github.com/AndyRids/ipymediator.git\ncd ipymediator\npoetry install\n```\n\n\n## Interface\n\n|#| Class         | Information                                                               |\n|-| ------------- | ------------------------------------------------------------------------- |\n|1| ABCTraitsMeta | Metaclass combining ABCMeta[^1] and MetaHasTraits[^2] metaclasses         |\n|2| ABCTraits     | Helper class that has ABCTraitsMeta as its metaclass                      |\n|3| Mediator      | Abstract class defining the Mediator interface                            | \n|4| Component     | Concrete class which communicates with a Mediator                         |\n\n[^1]: [ABCMeta](https://docs.python.org/3/library/abc.html#abc.ABCMeta)\n[^2]: [traitlets](https://traitlets.readthedocs.io/en/stable/api.html#traitlets.HasTraits)\n\n### 1. *class* ipymediator.ABCTraitsMeta\n\nCombines standard library abc.ABCMeta metaclass and traitlets.MetaHasTraits metaclass in order to facilitate the creation of an [abstract base class](https://docs.python.org/3/glossary.html#term-abstract-base-class) (ABC), which can define a Mediator interface, and also have trait attributes provided by the traitlets library.\n\n[^3]: [Abstract base classes](https://docs.python.org/3/glossary.html#term-abstract-base-class)\n\n### 2. *class* ipymediator.ABCTraits\n\nA helper class that has ABCTraitsMeta as its metaclass. With this class, an abstract base class can be created by simply deriving from ABCTraits avoiding sometimes confusing metaclass usage.\n\n```python\nfrom abc import abstractmethod\nfrom ipymediator.interface import ABCTraits\n\nclass CustomABC(ABCTraits):\n    pass\n```\n\nThe type of ABCTraits is still `ABCTraitsMeta` - therefore inheriting from `ABCTraits` requires  precautions regarding metaclass usage, as multiple inheritance may lead to metaclass conflicts. The metaclass allows the inheritence of `traitlets.HasTraits` alongside `ABCTraits` without conflicts.\n\n```python\nfrom abc import abstractmethod\nfrom ipymediator.interface import ABCTraits\nfrom traitlets import HasTraits\n\nclass ABCWithTraits(ABCTraits, HasTraits):\n    pass\n```\n\nThis class would also allow the definition of an interface using an ABC that would allow inheritence of widgets from `ipywidgets` library, which utilise the `traitlets.HasTraits` class.\n\n### 3. *class* ipymediator.Mediator\n\nAn abstract class, which impliments the Mediator interface. It requires the overriding of a `notify` abstract method, which is used by concrete Components to send messages to a Mediator.\n\n#### Methods:\n\n```python\n@abc.abstractmethod\ndef notify(reference: Union[str, Component], change: dict) -\u003e None\n```\n*reference* - Reference for a Component's widget notifying the Mediator. Will be a Component instance reference or a Component's `widget_name` property value.\n\n*change* - Observed trait change `dict` from a Component's widget `observe` function. \n\n\nAbstract method for notifying a concrete Mediator of observed trait value changes, which belong to a concrete Component. The change `dict` passed to `notify` is in the following format[^4]:\n\n[^4]: [traitlets callbacks](https://traitlets.readthedocs.io/en/stable/api.html#traitlets.observe)\n\n```python\n{\n  \"owner\": object, # The HasTraits instance\n  \"new\": 6, # The new value\n  \"old\": 5, # The old value\n  \"name\": \"foo\", # The name of the changed trait\n  \"type\": 'change', # Event type of the notification\n}\n```\n\n#### Example Concrete Mediator:\n\n```python\nfrom ipymediator.enumerations import Value\nfrom ipymediator.interface import Mediator, Component\nfrom ipymediator.utils import singlenotifydispatch\nfrom ipywidgets import widgets as w\nfrom traitlets import HasTraits, traitlets\n\nclass Dialog(Mediator, HasTraits):\n    \"\"\"Receives messages from Button and Text widgets and passes an action\n    to carry out, to the appropriate widget on message receipt\"\"\"\n\n    button_counter = traitlets.Integer(default_value=0, help=\"button clicks\").tag(config=True)\n\n    def __init__(self):\n        super(Dialog, self).__init__()\n\n        # Component adds Bool trait called value to all Button widgets by default,\n        # which toggles True/False on each click (like ToggleButton widget)\n        self.button_submit = Component(\n            mediator=self, widget=w.Button(), widget_name=\"button_submit\", names=(\"value\",))\n\n        # Component __init__ sets \"value\" trait to be observed by default\n        self.message_clicks = Component(\n            mediator=self, widget=w.Text(), widget_name=\"message_clicks\")\n        \n        self.message_value = Component(\n            mediator=self, widget=w.Text(), widget_name=\"message_value\", names=(\"disabled\",))\n\n        # Component allows changes to widget property traits through \n        # bracket notation (see Component info for details)\n        self.button_submit[\"description\"] = \"Click Me\"\n        self.button_submit[\"layout\"].width = \"300px\"\n        self.button_submit[\"style\"].font_weight = \"bold\"\n        self.button_submit[\"style\"].button_color = \"#f8edeb\"\n\n        self.message_clicks[\"style\"].width = \"300px\"\n        self.message_value[\"style\"].width = \"300px\"\n\n        self.container = w.VBox(\n            children=(\n                self.button_submit.widget,\n                self.message_clicks.widget,\n                self.message_value.widget\n            )\n        )\n\n    @singlenotifydispatch\n    def notify(self, reference: str, change: Value) -\u003e None:\n        \"\"\"Receives messages from concrete Components.\n        singlenotifydispatch wrapper allows overloading of notify\n        based on reference string value.\n        \n        Params:\n            reference (str): Reference value passed by Component. Can be used to \n                differentiate message origin.\n            \n            change (Value): trait value change dict passed by traitlets observe function.\n        \"\"\"\n        pass\n           \n    @notify.register(\"button_submit\")\n    def _(self, reference: str, change: Value) -\u003e None:\n        \"\"\"If reference == button_submit\"\"\"\n        # button_submit.widget was clicked\n        self.button_counter += 1\n        self.message_clicks[\"value\"] = f\"Button clicks: {self.button_counter}\"\n        self.message_value[\"value\"] = f\"Button value is {self.button_submit['value']}\"\n        self.message_value[\"disabled\"] = self.button_submit[\"value\"]\n\n    @notify.register(\"message_value\")\n    def _(self, reference: str, change: Value) -\u003e None:\n        \"\"\"If reference == message_value\"\"\"\n        # message_value.widget disabled trait change\n        message = (\"enabled\", \"disabled\")[int(change[\"new\"])]\n        print(f\"message_value.widget was {message}\")\n```\n![Mediator Example](https://raw.githubusercontent.com/AndyRids/ipymediator/main/examples/images/mediator_example.png)\n\n### 3. *class* ipymediator.Component\n\nA concrete Component class, which communicates with a concrete Mediator implimentning the Mediator interface. Messages are passed to the Mediator by utilising a widget's observe method, which is set to use the Component's `observe_handler` method as a callback function. This callback is triggered on changes to specified widget trait(s).\n\nComponent inherits `ipymediator.ABCTraits` and can therefore be extended to inherit from `traitlets.HasTraits` and be given traits of its own without metaclass conflicts.\n\n#### Methods:\n\n```python\ndef __init__(\n    mediator: Mediator, \n    widget: DOMWidget, \n    widget_name: str = None, \n    names: tuple[str, ...] = (\"value\",),\n    notify_self: bool = False) -\u003e None\n```\n*mediator* - Reference to a Concrete Mediator.\n\n*widget* - A widget from ipywidgets library.\n\n*widget_name* - Optional name for the Component's widget. A default value of the widget  `__name__` + \"Component\" is used.\n\n*names* - Trait names of the widget passed as the widget parameter.\n\n*notify_self* - Determines the reference value passed to the Mediator's notify function - self (True) or widget_name (False).\n\nThe initialisation logic causes the Mediator's `notify` method to be called on changes to any of the widget traits passed as the `names` parameter. `names` must contain traits  held by the object passed to the `widget` parameter or a `ValueError` is raised.\n\n```python\ndef observe_handler(change: Union[Value, Options]) -\u003e None\n```\nUsed as the widgets `observe` method callback function to pass a observed trait(s) change `dict` to a Mediator through its `notify` method.\n\n#### Overriden Dunder Methods:\n\n```python\ndef __new__(cls, **kwargs) -\u003e Component\n```\nOverriden to add a bool 'value' trait (traitlets) to any `ipywidgets.Button` passed as the `widget` initialisation parameter and assigns an `on_click` function to toggle the Button 'value' trait between True and False - Replicating 'value' trait behaviour of `ipywidgets.ToggleButton`.\n\n\u003e [!NOTE]\n\u003e Will likely move this logic to `ABCTraitsMeta` metaclass.\n\n```python\ndef __call__(trait: str, *args)\n```\nOverriden to facilitate retrieval of widget trait values by leveraging `__getitem__`, which directs the call to the Component's `widget` property trait values.\n\n\u003e [!INFO]\n\u003e Used in `__init__` to validate widget trait names passed to `names` parameter.\n\n```python\ndef __contains__(trait: str, *args)\n```\nOverriden to redirect trait membership tests with `in` and `not in` to the Component's `widget` property - e.g. `\"value\" in my_component`\n\n```python\ndef __getitem__(self, trait: str)\n```\nOverriden to enable retreival of widget trait values on Component instance `__call__` or using using bracket notation.\n\n```python\ndef __setitem__(self, trait: str, value) -\u003e None\n```\nOverriden to enable setting of widget trait values through bracket notation.\n\n```python\ndef __str__(self) -\u003e str\n```\nReturns the value of the `widget_name` property.\n\n### Example Concrete Component:\n\n```python\nfrom functools import singledispatchmethod\nfrom IPython.display import display\nfrom ipymediator.enumerations import Value\nfrom ipymediator.interface import Mediator, Component\nfrom ipywidgets import widgets as w\n\nclass ConcreteMediator(Mediator):\n    @singledispatchmethod\n    def notify(self, reference: Union[str, Component], change: Value):\n        pass\n\n    @notify.register\n    def _(self, reference: str, change: Value):\n        print(f\"\"\"Message sent from: {reference}\n            {reference=}\"\"\", end=\"\\n\\n\")\n\n    @notify.register\n    def _(self, reference: Component, change: Value):\n        print(f\"\"\"Message sent from: {reference.widget_name}\n            {reference=}\"\"\", end=\"\\n\\n\")\n\n# concrete Mediator instance\nmediator = ConcreteMediator()\n\n# component_one passes widget_name as reference to Mediator\ncomponent_one = Component(\n    mediator=mediator,\n    widget=w.Button(description=\"component_one\"),\n    widget_name=\"component_one\",\n    names=(\"value\",),\n    notify_self=False)\n\n# change button_color of  ipywidgets Button to cyan\ncomponent_one[\"style\"].button_color = \"cyan\"\n\n# component_two passes self as reference to Mediator\ncomponent_two = Component(\n    mediator=mediator,\n    widget=w.Button(description=\"component_two\"),\n    widget_name=\"component_two\",\n    names=(\"value\",),\n    notify_self=True)\n\n# change button_color of  ipywidgets Button to lime\ncomponent_two[\"style\"].button_color = \"lime\"\n\n# display Component widgets (Button x 2) in a VBox\ndisplay(w.VBox((component_one.widget, component_two.widget)))\n```\n![Mediator Example](https://raw.githubusercontent.com/AndyRids/ipymediator/main/examples/images/component_example.png)\n\n## Utility Functions\n\n### 1. *class* ipymediator.utils.singlenotifydispatch\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandyrids%2Fipymediator","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fandyrids%2Fipymediator","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fandyrids%2Fipymediator/lists"}