From: Christian Heller Date: Thu, 18 Sep 2025 00:45:16 +0000 (+0200) Subject: Move into_endnode_updates into ClientDb. X-Git-Url: https://plomlompom.com/repos/booking/process?a=commitdiff_plain;h=7ece7f61285d51469271f62ca0fbd43236881978;p=ircplom Move into_endnode_updates into ClientDb. --- diff --git a/ircplom/client.py b/ircplom/client.py index cb5c575..14274b6 100644 --- a/ircplom/client.py +++ b/ircplom/client.py @@ -93,41 +93,41 @@ class _Dict(Dict[DictItem]): class _Completable(ABC): - _completed: Any + completed: Any @abstractmethod def complete(self) -> None: - 'Set ._completed to "complete" value if possible of current state.' + 'Set .completed to "complete" value if possible of current state.' class _CompletableStringsCollection(_Completable, Collection): _collected: Collection[str] - _completed: Optional[Collection[str]] = None + completed: Optional[Collection[str]] = None @abstractmethod def _copy_collected(self) -> Collection[str]: pass def complete(self) -> None: - self._completed = self._copy_collected() + self.completed = self._copy_collected() def __len__(self) -> int: - assert self._completed is not None - return len(self._completed) + assert self.completed is not None + return len(self.completed) def __contains__(self, item) -> bool: - assert self._completed is not None + assert self.completed is not None assert isinstance(item, str) - return item in self._completed + return item in self.completed def __iter__(self) -> Iterator[str]: - assert self._completed is not None - yield from self._completed + assert self.completed is not None + yield from self.completed class _CompletableStringsSet(_CompletableStringsCollection, Set): _collected: set[str] - _completed: Optional[set[str]] = None + completed: Optional[set[str]] = None def __init__(self) -> None: self._collected = set() @@ -139,13 +139,13 @@ class _CompletableStringsSet(_CompletableStringsCollection, Set): ) -> None: method = getattr(self._collected, m_name) if on_complete: - assert self._completed is not None - assert (item in self._completed) == exists + assert self.completed is not None + assert (item in self.completed) == exists assert (item in self._collected) == exists method(item) self.complete() else: - assert self._completed is None + assert self.completed is None assert (item in self._collected) == exists method(item) @@ -159,8 +159,8 @@ class _CompletableStringsSet(_CompletableStringsCollection, Set): def intersection(self, *others: Iterable[str]) -> set[str]: 'Compare self to other set(s).' - assert self._completed is not None - result = self._completed + assert self.completed is not None + result = self.completed for other in others: assert isinstance(other, set) result = result.intersection(other) @@ -169,7 +169,7 @@ class _CompletableStringsSet(_CompletableStringsCollection, Set): class _CompletableStringsOrdered(_Clearable, _CompletableStringsCollection): _collected: tuple[str, ...] = tuple() - _completed: Optional[tuple[str, ...]] = tuple() + completed: Optional[tuple[str, ...]] = tuple() def _copy_collected(self) -> tuple[str, ...]: return tuple(self._collected) @@ -185,57 +185,14 @@ class _CompletableStringsOrdered(_Clearable, _CompletableStringsCollection): self.complete() -class IntoEndnodeUpdatesMixin(AutoAttrMixin): - 'Provides .into_endnode_updates exportable for module-external consumers.' - _cache: dict[tuple[str, ...], Any] = {} - - def into_endnode_updates(self, - client_id: str, - path: tuple[str, ...] - ) -> list[tuple[tuple[str, ...], Any]]: - 'Return path-value pairs for any update-worthy sub-elements.' - - def update_unless_cached(path: tuple[str, ...], val: Any): - steps_so_far: list[str] = [] - for step in path[:-1]: - cache_path = tuple(client_id) + tuple(steps_so_far) - if cache_path in self._cache: - del self._cache[cache_path] - steps_so_far += [step] - cache_path = tuple(client_id) + path - if cache_path not in self._cache or val != self._cache[cache_path]: - self._cache[cache_path] = val - return [(path, val)] - return [] - - if isinstance(self, _Completable): - if self._completed is not None: - return update_unless_cached(path, self._completed) - return [] - - if isinstance(self, Dict) and not self._dict: - return update_unless_cached(path, None) - - updates = [] - for key, val in (self._dict.items() if isinstance(self, Dict) - else ((k, getattr(self, k)) - for k in self._deep_annotations())): - p = path + (key,) - if isinstance(val, IntoEndnodeUpdatesMixin): - updates += val.into_endnode_updates(client_id, p) - else: - updates += update_unless_cached(p, val) - return updates - - -class _UpdatingMixin(IntoEndnodeUpdatesMixin): +class _UpdatingMixin: def __init__(self, on_update: Callable, **kwargs) -> None: super().__init__(**kwargs) self._on_update = on_update -class _UpdatingAttrsMixin(_UpdatingMixin): +class _UpdatingAttrsMixin(_UpdatingMixin, AutoAttrMixin): def _make_attr(self, cls: Callable, key: str): return cls(on_update=lambda *steps: self._on_update(key, *steps)) @@ -245,10 +202,17 @@ class _UpdatingAttrsMixin(_UpdatingMixin): if hasattr(self, '_on_update') and key in self._deep_annotations(): self._on_update(key) + def attr_names(self) -> tuple[str, ...]: + 'Names of (deeply) annotated attributes.' + return tuple(self._deep_annotations().keys()) + class _UpdatingDict(_UpdatingMixin, _Dict[DictItem]): _create_if_none: Optional[dict[str, Any]] = None + def __bool__(self) -> bool: + return bool(self._dict) + def __getitem__(self, key: str) -> DictItem: if key not in self._dict: if self._create_if_none is not None: @@ -395,13 +359,13 @@ class IrcConnection(BaseIrcConnection, _ClientIdMixin): class _CompletableTopic(_Completable, Topic): - _completed: Optional[Topic] = None + completed: Optional[Topic] = None def complete(self) -> None: assert self.who is not None copy = _NickUserHost.from_str(str(self.who)) - self._completed = Topic(self.what, - NickUserHost(copy.nick, copy.user, copy.host)) + self.completed = Topic(self.what, + NickUserHost(copy.nick, copy.user, copy.host)) class _Channel(Channel): @@ -627,6 +591,7 @@ class _UpdatingIsupportDict(_UpdatingDict[str]): class _ClientDb(_Clearable, _UpdatingAttrsMixin, SharedClientDbFields): + _updates_cache: dict[tuple[str, ...], Any] = {} _keep_on_clear = set(IrcConnSetup.__annotations__.keys()) caps: _UpdatingDict[_UpdatingServerCapability] channels: _UpdatingChannelsDict @@ -651,6 +616,53 @@ class _ClientDb(_Clearable, _UpdatingAttrsMixin, SharedClientDbFields): attr._create_if_none = {} return attr + def into_endnode_updates(self, path: tuple[str, ...] + ) -> list[tuple[tuple[str, ...], Any]]: + 'Return path-value pairs for update-worthy (sub-)elements at path.' + + def update_unless_cached(path: tuple[str, ...], val: Any + ) -> list[tuple[tuple[str, ...], Any]]: + if path not in self._updates_cache\ + or val != self._updates_cache[path]: + self._updates_cache[path] = val + return [(path, val)] + return [] + + parent = self + cache_path: tuple[str, ...] = tuple() + for step in path[:-1]: + cache_path = cache_path + (step,) + if cache_path in self._updates_cache: + del self._updates_cache[cache_path] + parent = (parent[step] if isinstance(parent, Dict) + else getattr(parent, step)) + last_step = path[-1] + val_at_path = ((parent[last_step] if last_step in parent.keys() + else None) if isinstance(parent, Dict) + else getattr(parent, last_step)) + if not isinstance(val_at_path, _UpdatingMixin): + return update_unless_cached(path, val_at_path) + if isinstance(val_at_path, _Completable): + if val_at_path.completed is not None: + return update_unless_cached(path, val_at_path.completed) + return [] + if isinstance(val_at_path, Dict) and not val_at_path.keys(): + for cache_path in [p for p in self._updates_cache + if len(p) > len(path) + and p[:len(path)] == path]: + del self._updates_cache[cache_path] + return update_unless_cached(path, None) + if isinstance(val_at_path, Dict): + sub_items = val_at_path.keys() + else: + assert isinstance(val_at_path, _UpdatingAttrsMixin) + sub_items = val_at_path.attr_names() + updates = [] + for key in sub_items: + p = path + (key,) + updates += self.into_endnode_updates(p) + return updates + def clear(self) -> None: for key, value in [(k, v) for k, v in self._deep_annotations().items() if k not in self._keep_on_clear]: diff --git a/ircplom/client_tui.py b/ircplom/client_tui.py index 9b41e95..ba7a2f5 100644 --- a/ircplom/client_tui.py +++ b/ircplom/client_tui.py @@ -7,8 +7,8 @@ from ircplom.tui_base import (BaseTui, PromptWidget, TuiEvent, Window, CMD_SHORTCUTS) from ircplom.client import ( AutoAttrMixin, Channel, Client, ClientQueueMixin, Dict, DictItem, - IntoEndnodeUpdatesMixin, IrcConnSetup, LogScope, NewClientEvent, - NickUserHost, ServerCapability, SharedClientDbFields, User) + IrcConnSetup, LogScope, NewClientEvent, NickUserHost, ServerCapability, + SharedClientDbFields, User) CMD_SHORTCUTS['disconnect'] = 'window.disconnect' CMD_SHORTCUTS['join'] = 'window.join' @@ -505,15 +505,5 @@ class ClientKnowingTui(Client): f.write(('>' if kwargs['out'] else '<') + f' {msg}\n') def _on_update(self, *path) -> None: - parent = self.db - for step in path[:-1]: - parent = (parent[step] if isinstance(parent, Dict) - else getattr(parent, step)) - last_step = path[-1] - value = ((parent[last_step] if last_step in parent.keys() - else None) if isinstance(parent, Dict) - else getattr(parent, last_step)) - for path, value in (value.into_endnode_updates(self.client_id, path) - if isinstance(value, IntoEndnodeUpdatesMixin) - else [(path, value)]): + for path, value in self.db.into_endnode_updates(path): self._client_tui_trigger('update_db', update=_Update(path, value)) diff --git a/test.txt b/test.txt index e5a7011..7f52a02 100644 --- a/test.txt +++ b/test.txt @@ -52,9 +52,9 @@ 1,2 $ port set to: [6697] 1,2 $ connection_state set to: [connecting] + 1,2 $ channels cleared 1,2 $ users cleared - 1,2 $ ?!?@? renames ? 1,2 $ users:me:user set to: [plom] 1,2 $ connection_state set to: [connected] @@ -71,10 +71,8 @@ 1,2 $$$ *** Found your hostname (baz.bar.foo) 2 < :*.?.net CAP * LS : foo bar sasl=PLAIN,EXTERNAL baz - 1,2 $ isupport:CHANTYPES set to: [#&] 1,2 $ isupport:PREFIX set to: [(ov)@+] - 2 > CAP REQ :sasl 2 > CAP :LIST @@ -184,6 +182,7 @@ 2 < :bazbaz!~baz@baz.baz PART :#test 4 $ bazbaz!~baz@baz.baz parts 1,2 $ users:2 cleared +, $ ?!?@? renames ? 2 < :bazbaz!~baz@baz.baz JOIN :#test , $ ?!?@? renames ? , $ ?!?@? renames bazbaz @@ -194,6 +193,7 @@ 4 $ bazbaz!~baz@baz.baz quits: Client Quit 1,2 $ users:2 cleared 1,2 $ users:3 cleared +, $ ?!?@? renames ? 2 < :foo!~plom@baz.bar.foo PART :#test 1,2,3,4 $ foo!~plom@baz.bar.foo parts 1,2 $ users:3 cleared @@ -235,7 +235,9 @@ 2 < :*.?.net CAP * LS : foo bar sasl=PLAIN,EXTERNAL baz 2 > CAP REQ :sasl 2 > CAP :LIST + # NB: missing are default settings of isupport:CHANTYPES, :PREFIX + 2 < PING :? 2 > PONG :? 2 < :*.?.net CAP ? ACK :sasl @@ -243,6 +245,7 @@ 1,2 $ caps:bar:data set to: [] 1,2 $ caps:baz:data set to: [] 1,2 $ caps:foo:data set to: [] +1,2 $ caps:sasl:data set to: [] 1,2 $ caps:sasl:data set to: [PLAIN,EXTERNAL] 1,2 $ caps:sasl:enabled set to: [True] 1,2 $ sasl_auth_state set to: [attempting]