|
4 | 4 | and set default directories to work with. |
5 | 5 | """ |
6 | 6 |
|
| 7 | +from __future__ import annotations |
| 8 | + |
| 9 | +import asyncio |
7 | 10 | import configparser |
8 | 11 | import copy |
| 12 | +import inspect |
9 | 13 | import json |
10 | 14 | import logging |
11 | 15 | import os |
12 | 16 | import shutil |
13 | 17 | from functools import lru_cache, partial |
14 | 18 | from pathlib import Path |
15 | | -from typing import Callable, Optional, Union |
| 19 | +from typing import Awaitable, Callable, Optional, Union |
16 | 20 | from urllib.parse import ParseResult, urlparse, urlunparse |
17 | 21 |
|
18 | 22 | import requests |
@@ -169,3 +173,72 @@ def _check_dict_structure(d1: dict, d2: dict) -> bool: |
169 | 173 | if _check_dict_structure(settings, settings_copy): |
170 | 174 | with open(p, "w") as sf: |
171 | 175 | json.dump(settings_copy, sf) |
| 176 | + |
| 177 | + |
| 178 | +class Observer: |
| 179 | + """ |
| 180 | + A helper class implementing the observer pattern supporting both |
| 181 | + synchronous and asynchronous notification calls and both synchronous and |
| 182 | + asynchronous callback functions. |
| 183 | + """ |
| 184 | + |
| 185 | + # The class here should be derived from typing.Generic[P] |
| 186 | + # with P = ParamSpec("P"), and the notify/anotify functions should use |
| 187 | + # *args: P.args, **kwargs: P.kwargs. |
| 188 | + # However, ParamSpec is Python 3.10+ (PEP 612), so we can't use that yet. |
| 189 | + |
| 190 | + def __init__(self): |
| 191 | + self._listeners: list[Callable[..., Awaitable[None] | None]] = [] |
| 192 | + self._secondary_listeners: list[Callable[..., Awaitable[None] | None]] = [] |
| 193 | + self._final_listeners: list[Callable[..., Awaitable[None] | None]] = [] |
| 194 | + super().__init__() |
| 195 | + |
| 196 | + def subscribe( |
| 197 | + self, |
| 198 | + fn: Callable[..., Awaitable[None] | None], |
| 199 | + secondary: bool = False, |
| 200 | + final: bool = False, |
| 201 | + ): |
| 202 | + if final: |
| 203 | + self._final_listeners.append(fn) |
| 204 | + elif secondary: |
| 205 | + self._secondary_listeners.append(fn) |
| 206 | + else: |
| 207 | + self._listeners.append(fn) |
| 208 | + |
| 209 | + async def anotify( |
| 210 | + self, *args, secondary: bool = False, final: bool = False, **kwargs |
| 211 | + ) -> None: |
| 212 | + awaitables: list[Awaitable] = [] |
| 213 | + listeners = ( |
| 214 | + self._secondary_listeners |
| 215 | + if secondary |
| 216 | + else self._final_listeners if final else self._listeners |
| 217 | + ) |
| 218 | + for notify_function in listeners: |
| 219 | + result = notify_function(*args, **kwargs) |
| 220 | + if result is not None and inspect.isawaitable(result): |
| 221 | + awaitables.append(result) |
| 222 | + if awaitables: |
| 223 | + await self._await_all(awaitables) |
| 224 | + |
| 225 | + @staticmethod |
| 226 | + async def _await_all(awaitables: list[Awaitable]): |
| 227 | + for awaitable in asyncio.as_completed(awaitables): |
| 228 | + await awaitable |
| 229 | + |
| 230 | + def notify( |
| 231 | + self, *args, secondary: bool = False, final: bool = False, **kwargs |
| 232 | + ) -> None: |
| 233 | + awaitables: list[Awaitable] = [] |
| 234 | + listeners = ( |
| 235 | + self._secondary_listeners |
| 236 | + if secondary |
| 237 | + else self._final_listeners if final else self._listeners |
| 238 | + ) |
| 239 | + for notify_function in listeners: |
| 240 | + result = notify_function(*args, **kwargs) |
| 241 | + if result is not None and inspect.isawaitable(result): |
| 242 | + awaitables.append(result) |
| 243 | + if awaitables: |
| 244 | + asyncio.run(self._await_all(awaitables)) |
0 commit comments