Skip to content

Commit f5dc4e2

Browse files
committed
snapshot(refactor[typing]): Improve type overrides with generics
why: Remove the need for type: ignore comments on property overrides what: - Use Generic base classes with covariant type parameters - Add properly typed overrides for inherited properties - Define a clear SnapshotType union type for shared operations - Improve type safety in filter_snapshot with better type checks
1 parent 7592b43 commit f5dc4e2

File tree

1 file changed

+84
-40
lines changed

1 file changed

+84
-40
lines changed

src/libtmux/snapshot.py

+84-40
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,8 @@
66
- **License**: MIT
77
- **Description**: Snapshot data structure for tmux objects
88
9-
Note on type checking:
10-
The snapshot classes intentionally override properties from parent classes with
11-
slightly different return types (covariant types - e.g., returning WindowSnapshot
12-
instead of Window). This is type-safe at runtime but causes mypy warnings. We use
13-
type: ignore[override] comments on these properties and add proper typing.
9+
This module provides hierarchical snapshots of tmux objects (Server, Session,
10+
Window, Pane) that are immutable and maintain the relationships between objects.
1411
"""
1512

1613
from __future__ import annotations
@@ -32,28 +29,74 @@
3229
from libtmux.session import Session
3330
from libtmux.window import Window
3431

35-
if t.TYPE_CHECKING:
36-
PaneT = t.TypeVar("PaneT", bound=Pane, covariant=True)
37-
WindowT = t.TypeVar("WindowT", bound=Window, covariant=True)
38-
SessionT = t.TypeVar("SessionT", bound=Session, covariant=True)
39-
ServerT = t.TypeVar("ServerT", bound=Server, covariant=True)
32+
# Define type variables for generic typing
33+
PaneT = t.TypeVar("PaneT", bound=Pane, covariant=True)
34+
WindowT = t.TypeVar("WindowT", bound=Window, covariant=True)
35+
SessionT = t.TypeVar("SessionT", bound=Session, covariant=True)
36+
ServerT = t.TypeVar("ServerT", bound=Server, covariant=True)
4037

38+
# Forward references for type definitions
39+
ServerSnapshot_t = t.TypeVar("ServerSnapshot_t", bound="ServerSnapshot")
40+
SessionSnapshot_t = t.TypeVar("SessionSnapshot_t", bound="SessionSnapshot")
41+
WindowSnapshot_t = t.TypeVar("WindowSnapshot_t", bound="WindowSnapshot")
42+
PaneSnapshot_t = t.TypeVar("PaneSnapshot_t", bound="PaneSnapshot")
4143

42-
# Make base classes implement Sealable
44+
45+
# Make base classes implement Sealable and use Generics
4346
class _SealablePaneBase(Pane, Sealable):
4447
"""Base class for sealable pane classes."""
4548

4649

47-
class _SealableWindowBase(Window, Sealable):
48-
"""Base class for sealable window classes."""
50+
class _SealableWindowBase(Window, Sealable, t.Generic[PaneT]):
51+
"""Base class for sealable window classes with generic pane type."""
52+
53+
@property
54+
def panes(self) -> QueryList[PaneT]:
55+
"""Return panes with the appropriate generic type."""
56+
return t.cast(QueryList[PaneT], super().panes)
57+
58+
@property
59+
def active_pane(self) -> PaneT | None:
60+
"""Return active pane with the appropriate generic type."""
61+
return t.cast(t.Optional[PaneT], super().active_pane)
62+
63+
64+
class _SealableSessionBase(Session, Sealable, t.Generic[WindowT, PaneT]):
65+
"""Base class for sealable session classes with generic window and pane types."""
66+
67+
@property
68+
def windows(self) -> QueryList[WindowT]:
69+
"""Return windows with the appropriate generic type."""
70+
return t.cast(QueryList[WindowT], super().windows)
71+
72+
@property
73+
def active_window(self) -> WindowT | None:
74+
"""Return active window with the appropriate generic type."""
75+
return t.cast(t.Optional[WindowT], super().active_window)
76+
77+
@property
78+
def active_pane(self) -> PaneT | None:
79+
"""Return active pane with the appropriate generic type."""
80+
return t.cast(t.Optional[PaneT], super().active_pane)
4981

5082

51-
class _SealableSessionBase(Session, Sealable):
52-
"""Base class for sealable session classes."""
83+
class _SealableServerBase(Server, Sealable, t.Generic[SessionT, WindowT, PaneT]):
84+
"""Generic base for sealable server with typed session, window, and pane."""
85+
86+
@property
87+
def sessions(self) -> QueryList[SessionT]:
88+
"""Return sessions with the appropriate generic type."""
89+
return t.cast(QueryList[SessionT], super().sessions)
5390

91+
@property
92+
def windows(self) -> QueryList[WindowT]:
93+
"""Return windows with the appropriate generic type."""
94+
return t.cast(QueryList[WindowT], super().windows)
5495

55-
class _SealableServerBase(Server, Sealable):
56-
"""Base class for sealable server classes."""
96+
@property
97+
def panes(self) -> QueryList[PaneT]:
98+
"""Return panes with the appropriate generic type."""
99+
return t.cast(QueryList[PaneT], super().panes)
57100

58101

59102
@frozen_dataclass_sealable
@@ -251,7 +294,7 @@ def from_pane(
251294

252295

253296
@frozen_dataclass_sealable
254-
class WindowSnapshot(_SealableWindowBase):
297+
class WindowSnapshot(_SealableWindowBase[PaneSnapshot]):
255298
"""A read-only snapshot of a tmux window.
256299
257300
This maintains compatibility with the original Window class but prevents
@@ -404,7 +447,7 @@ def from_window(
404447

405448

406449
@frozen_dataclass_sealable
407-
class SessionSnapshot(_SealableSessionBase):
450+
class SessionSnapshot(_SealableSessionBase[WindowSnapshot, PaneSnapshot]):
408451
"""A read-only snapshot of a tmux session.
409452
410453
This maintains compatibility with the original Session class but prevents
@@ -551,7 +594,9 @@ def from_session(
551594

552595

553596
@frozen_dataclass_sealable
554-
class ServerSnapshot(_SealableServerBase):
597+
class ServerSnapshot(
598+
_SealableServerBase[SessionSnapshot, WindowSnapshot, PaneSnapshot]
599+
):
555600
"""A read-only snapshot of a server.
556601
557602
Examples
@@ -690,6 +735,10 @@ def from_server(
690735
return snapshot
691736

692737

738+
# Define a Union type for snapshot classes
739+
SnapshotType = t.Union[ServerSnapshot, SessionSnapshot, WindowSnapshot, PaneSnapshot]
740+
741+
693742
def _create_session_snapshot_safely(
694743
session: Session, include_content: bool, server_snapshot: ServerSnapshot
695744
) -> SessionSnapshot | None:
@@ -741,12 +790,9 @@ def _create_session_snapshot_safely(
741790

742791

743792
def filter_snapshot(
744-
snapshot: ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot,
745-
filter_func: t.Callable[
746-
[ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot],
747-
bool,
748-
],
749-
) -> ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot | None:
793+
snapshot: SnapshotType,
794+
filter_func: t.Callable[[SnapshotType], bool],
795+
) -> SnapshotType | None:
750796
"""Filter a snapshot hierarchy based on a filter function.
751797
752798
This will prune the snapshot tree, removing any objects that don't match the filter.
@@ -755,24 +801,24 @@ def filter_snapshot(
755801
756802
Parameters
757803
----------
758-
snapshot : ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot
804+
snapshot : SnapshotType
759805
The snapshot to filter
760806
filter_func : Callable
761807
A function that takes a snapshot object and returns True to keep it
762808
or False to filter it out
763809
764810
Returns
765811
-------
766-
ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot | None
812+
SnapshotType | None
767813
A new filtered snapshot, or None if everything was filtered out
768814
"""
769815
if isinstance(snapshot, ServerSnapshot):
770-
filtered_sessions = []
816+
filtered_sessions: list[SessionSnapshot] = []
771817

772818
for sess in snapshot.sessions_snapshot:
773819
session_copy = filter_snapshot(sess, filter_func)
774-
if session_copy is not None:
775-
filtered_sessions.append(t.cast(SessionSnapshot, session_copy))
820+
if session_copy is not None and isinstance(session_copy, SessionSnapshot):
821+
filtered_sessions.append(session_copy)
776822

777823
if not filter_func(snapshot) and not filtered_sessions:
778824
return None
@@ -793,12 +839,12 @@ def filter_snapshot(
793839
return server_copy
794840

795841
if isinstance(snapshot, SessionSnapshot):
796-
filtered_windows = []
842+
filtered_windows: list[WindowSnapshot] = []
797843

798844
for w in snapshot.windows_snapshot:
799845
window_copy = filter_snapshot(w, filter_func)
800-
if window_copy is not None:
801-
filtered_windows.append(t.cast(WindowSnapshot, window_copy))
846+
if window_copy is not None and isinstance(window_copy, WindowSnapshot):
847+
filtered_windows.append(window_copy)
802848

803849
if not filter_func(snapshot) and not filtered_windows:
804850
return None
@@ -808,8 +854,6 @@ def filter_snapshot(
808854
return session_copy
809855

810856
if isinstance(snapshot, WindowSnapshot):
811-
filtered_panes = []
812-
813857
filtered_panes = [p for p in snapshot.panes_snapshot if filter_func(p)]
814858

815859
if not filter_func(snapshot) and not filtered_panes:
@@ -828,15 +872,15 @@ def filter_snapshot(
828872

829873

830874
def snapshot_to_dict(
831-
snapshot: ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot | t.Any,
875+
snapshot: SnapshotType | t.Any,
832876
) -> dict[str, t.Any]:
833877
"""Convert a snapshot to a dictionary, avoiding circular references.
834878
835879
This is useful for serializing snapshots to JSON or other formats.
836880
837881
Parameters
838882
----------
839-
snapshot : ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot | Any
883+
snapshot : SnapshotType | Any
840884
The snapshot to convert to a dictionary
841885
842886
Returns
@@ -914,7 +958,7 @@ def snapshot_active_only(
914958
"""
915959

916960
def is_active(
917-
obj: ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot,
961+
obj: SnapshotType,
918962
) -> bool:
919963
"""Return True if the object is active."""
920964
if isinstance(obj, PaneSnapshot):
@@ -927,4 +971,4 @@ def is_active(
927971
if filtered is None:
928972
error_msg = "No active objects found!"
929973
raise ValueError(error_msg)
930-
return t.cast("ServerSnapshot", filtered)
974+
return t.cast(ServerSnapshot, filtered)

0 commit comments

Comments
 (0)