8000 snapshot(refactor[typing]): Improve type overrides with generics by tony · Pull Request #590 · tmux-python/libtmux · GitHub
[go: up one dir, main page]

Skip to content

snapshot(refactor[typing]): Improve type overrides with generics #590

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 44 commits into
base: snapshots
Choose a base branch
from
Draft
Changes from 1 commit
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
ea5b0c4
.tool-versions(uv,python) uv 0.6.12 -> 0.6.14, python 3.13.2 -> 3.13.3
tony Apr 12, 2025
88e62d8
py(deps[dev]) Bump dev packages
tony Apr 12, 2025
4c49754
py(deps[dev]) Bump dev packages
tony Apr 19, 2025
93f2e75
py(deps[dev]) Bump dev packages
tony Apr 26, 2025
b9a5cb7
pyproject(mypy) Add mypy override for frozen_dataclass method-assign
tony Feb 28, 2025
477e24b
pyproject(ruff) Ignore B010 set-attr-with-constant rule for frozen_da…
tony Feb 28, 2025
19a331c
pyproject(mypy) Add mypy override for `frozen_dataclass_sealable` `me…
tony Feb 28, 2025
e5be3f4
pyproject.toml(chore[mypy]): Exclude frozen_dataclass_sealable test f…
tony Mar 1, 2025
a929424
pyproject.toml(chore[lint,types]): Exclude frozen_dataclass_sealable …
tony Mar 1, 2025
7046533
frozen_dataclass(feat): Add `frozen_dataclass`
tony Feb 28, 2025
112709f
frozen_dataclass_sealable(feat): Add `frozen_dataclass_sealable`
tony Feb 28, 2025
b6d41dc
docs(frozen_dataclass) Add to `internals`
tony Feb 28, 2025
c244be5
docs(frozen_dataclass_sealable) Add to `internals`
tony Feb 28, 2025
50be574
WIP: Snapshot
tony Feb 28, 2025
7e75203
test(Snapshot): Replace MagicMock with pytest fixtures
tony Mar 1, 2025
d9a6d8a
docs(ServerSnapshot): Fix doctest examples in snapshot.py
tony Mar 1, 2025
df905f6
docs(Pane): Fix send_keys method doctest example
tony Mar 1, 2025
239d401
src/libtmux/snapshot.py uv run ruff check --select ALL src/libtmux/sn…
tony Mar 1, 2025
82deff8
test/test_snapshot.py: uv run ruff check --select ALL src/libtmux/sna…
tony Mar 1, 2025
9a940df
chore[mypy]: Add snapshot module override
tony Mar 1, 2025
e806d88
refactor(snapshot): Add explicit type ignores for seal methods
tony Mar 1, 2025
c8b202d
test(snapshot): Add type annotation to mock_filter function
tony Mar 1, 2025
f52f617
Revert "chore[mypy]: Add snapshot module override"
tony Mar 1, 2025
8997ba1
snapshot(refactor[Snapshot]): Fix dataclass field order and enhance s…
tony Mar 1, 2025
b42672b
mypy(config[snapshot]): Add override for property/field conflicts
tony Mar 1, 2025
13f7624
test(fix[PaneSnapshot]): Specify capture_content flag in tests
tony Mar 1, 2025
1eb0307
snapshot.py(style[exceptions]): Fix linting issues identified by ruff
tony Mar 1, 2025
b4f2db5
snapshot.py(refactor[performance]): Extract helper function for sessi…
tony Mar 1, 2025
00151d7
notes(2025-03-02) Add architecture notes
tony Mar 2, 2025
d6421c8
frozen_dataclass_sealable fix imports from `typing`
tony Mar 2, 2025
47aaec8
pyproject(mypy) Add mypy override for frozen_dataclass method-assign
tony Feb 28, 2025
83088eb
snapshot(refactor[typing]): Improve type overrides with generics
tony Mar 2, 2025
314a89e
docs: proposal for snapshot.py refactoring into package structure
tony Mar 2, 2025
a30f674
snapshot: New architecture, part 0: Remove old snapshot.py
tony Mar 2, 2025
0a6b46d
snapshot: New architecture, part 1: Add new architecture
tony Mar 2, 2025
2d12f0e
mypy(config[snapshot]): Update module pattern to include submodules
tony Mar 2, 2025
5237944
notes(2025-03-02) Updates to architecture notes
tony Mar 2, 2025
91b57f7
notes(2025-03-02[architecture-plan]) Update with typing ideas
tony Mar 2, 2025
ec6db9b
snapshot(factory): Implement type-safe factory and fluent API
tony Mar 2, 2025
729430d
notes(2025-03-02[architecture-plan]) Update for latest changes
tony Mar 2, 2025
2077a36
docs(snapshot[README]) Add README
tony Mar 2, 2025
fe23292
docs(snapshot[README]): add comprehensive doctest-based README for sn…
tony Mar 2, 2025
f6fde52
docs(snapshot/README): Comprehensive overhaul with doctest integration
tony Mar 2, 2025
ac85da4
docs(snapshot/README): Add Capabilities and Limitations table
tony Mar 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
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
  • Loading branch information
tony committed Apr 26, 2025
commit 83088eb8751b4899d56b0ebb3f0df404a253256f
124 changes: 84 additions & 40 deletions src/libtmux/snapshot.py
10000
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,8 @@
- **License**: MIT
- **Description**: Snapshot data structure for tmux objects

Note on type checking:
The snapshot classes intentionally override properties from parent classes with
slightly different return types (covariant types - e.g., returning WindowSnapshot
instead of Window). This is type-safe at runtime but causes mypy warnings. We use
type: ignore[override] comments on these properties and add proper typing.
This module provides hierarchical snapshots of tmux objects (Server, Session,
Window, Pane) that are immutable and maintain the relationships between objects.
"""

from __future__ import annotations
Expand All @@ -32,28 +29,74 @@
from libtmux.session import Session
from libtmux.window import Window

if t.TYPE_CHECKING:
PaneT = t.TypeVar("PaneT", bound=Pane, covariant=True)
WindowT = t.TypeVar("WindowT", bound=Window, covariant=True)
SessionT = t.TypeVar("SessionT", bound=Session, covariant=True)
ServerT = t.TypeVar("ServerT", bound=Server, covariant=True)
# Define type variables for generic typing
PaneT = t.TypeVar("PaneT", bound=Pane, covariant=True)
WindowT = t.TypeVar("WindowT", bound=Window, covariant=True)
SessionT = t.TypeVar("SessionT", bound=Session, covariant=True)
ServerT = t.TypeVar("ServerT", bound=Server, covariant=True)

# Forward references for type definitions
ServerSnapshot_t = t.TypeVar("ServerSnapshot_t", bound="ServerSnapshot")
SessionSnapshot_t = t.TypeVar("SessionSnapshot_t", bound="SessionSnapshot")
WindowSnapshot_t = t.TypeVar("WindowSnapshot_t", bound="WindowSnapshot")
PaneSnapshot_t = t.TypeVar("PaneSnapshot_t", bound="PaneSnapshot")

# Make base classes implement Sealable

# Make base classes implement Sealable and use Generics
class _SealablePaneBase(Pane, Sealable):
"""Base class for sealable pane classes."""


class _SealableWindowBase(Window, Sealable):
"""Base class for sealable window classes."""
class _SealableWindowBase(Window, Sealable, t.Generic[PaneT]):
"""Base class for sealable window classes with generic pane type."""

@property
def panes(self) -> QueryList[PaneT]:
"""Return panes with the appropriate generic type."""
return t.cast(QueryList[PaneT], super().panes)

@property
def active_pane(self) -> PaneT | None:
"""Return active pane with the appropriate generic type."""
return t.cast(t.Optional[PaneT], super().active_pane)


class _SealableSessionBase(Session, Sealable, t.Generic[WindowT, PaneT]):
"""Base class for sealable session classes with generic window and pane types."""

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

@property
def active_window(self) -> WindowT | None:
"""Return active window with the appropriate generic type."""
return t.cast(t.Optional[WindowT], super().active_window)

@property
def active_pane(self) -> PaneT | None:
"""Return active pane with the appropriate generic type."""
return t.cast(t.Optional[PaneT], super().active_pane)


class _SealableSessionBase(Session, Sealable):
"""Base class for sealable session classes."""
class _SealableServerBase(Server, Sealable, t.Generic[SessionT, WindowT, PaneT]):
"""Generic base for sealable server with typed session, window, and pane."""

@property
def sessions(self) -> QueryList[SessionT]:
"""Return sessions with the appropriate generic type."""
return t.cast(QueryList[SessionT], super().sessions)

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

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


@frozen_dataclass_sealable
Expand Down Expand Up @@ -251,7 +294,7 @@ def from_pane(


@frozen_dataclass_sealable
class WindowSnapshot(_SealableWindowBase):
class WindowSnapshot(_SealableWindowBase[PaneSnapshot]):
"""A read-only snapshot of a tmux window.

This maintains compatibility with the original Window class but prevents
Expand Down Expand Up @@ -404,7 +447,7 @@ def from_window(


@frozen_dataclass_sealable
class SessionSnapshot(_SealableSessionBase):
class SessionSnapshot(_SealableSessionBase[WindowSnapshot, PaneSnapshot]):
"""A read-only snapshot of a tmux session.

This maintains compatibility with the original Session class but prevents
Expand Down Expand Up @@ -551,7 +594,9 @@ def from_session(


@frozen_dataclass_sealable
class ServerSnapshot(_SealableServerBase):
class ServerSnapshot(
_SealableServerBase[SessionSnapshot, WindowSnapshot, PaneSnapshot]
):
"""A read-only snapshot of a server.

Examples
Expand Down Expand Up @@ -690,6 +735,10 @@ def from_server(
return snapshot


# Define a Union type for snapshot classes
SnapshotType = t.Union[ServerSnapshot, SessionSnapshot, WindowSnapshot, PaneSnapshot]


def _create_session_snapshot_safely(
session: Session, include_content: bool, server_snapshot: ServerSnapshot
) -> SessionSnapshot | None:
Expand Down Expand Up @@ -741,12 +790,9 @@ def _create_session_snapshot_safely(


def filter_snapshot(
snapshot: ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot,
filter_func: t.Callable[
[ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot],
bool,
],
) -> ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot | None:
snapshot: SnapshotType,
filter_func: t.Callable[[SnapshotType], bool],
) -> SnapshotType | None:
"""Filter a snapshot hierarchy based on a filter function.

This will prune the snapshot tree, removing any objects that don't match the filter.
Expand All @@ -755,24 +801,24 @@ def filter_snapshot(

Parameters
----------
snapshot : ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot
snapshot : SnapshotType
The snapshot to filter
filter_func : Callable
A function that takes a snapshot object and returns True to keep it
or False to filter it out

Returns
-------
ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot | None
SnapshotType | None
A new filtered snapshot, or None if everything was filtered out
"""
if isinstance(snapshot, ServerSnapshot):
filtered_sessions = []
filtered_sessions: list[SessionSnapshot] = []

for sess in snapshot.sessions_snapshot:
session_copy = filter_snapshot(sess, filter_func)
if session_copy is not None:
filtered_sessions.append(t.cast(SessionSnapshot, session_copy))
if session_copy is not None and isinstance(session_copy, SessionSnapshot):
filtered_sessions.append(session_copy)

if not filter_func(snapshot) and not filtered_sessions:
return None
Expand All @@ -793,12 +839,12 @@ def filter_snapshot(
return server_copy

if isinstance(snapshot, SessionSnapshot):
filtered_windows = []
filtered_windows: list[WindowSnapshot] = []

for w in snapshot.windows_snapshot:
window_copy = filter_snapshot(w, filter_func)
if window_copy is not None:
filtered_windows.append(t.cast(WindowSnapshot, window_copy))
if window_copy is not None and isinstance(window_copy, WindowSnapshot):
filtered_windows.append(window_copy)

if not filter_func(snapshot) and not filtered_windows:
return None
Expand All @@ -808,8 +854,6 @@ def filter_snapshot(
return session_copy

if isinstance(snapshot, WindowSnapshot):
filtered_panes = []

filtered_panes = [p for p in snapshot.panes_snapshot if filter_func(p)]

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


def snapshot_to_dict(
snapshot: ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot | t.Any,
snapshot: SnapshotType | t.Any,
) -> dict[str, t.Any]:
"""Convert a snapshot to a dictionary, avoiding circular references.

This is useful for serializing snapshots to JSON or other formats.

Parameters
----------
snapshot : ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot | Any
snapshot : SnapshotType | Any
The snapshot to convert to a dictionary

Returns
Expand Down Expand Up @@ -914,7 +958,7 @@ def snapshot_active_only(
"""

def is_active(
obj: ServerSnapshot | SessionSnapshot | WindowSnapshot | PaneSnapshot,
obj: SnapshotType,
) -> bool:
"""Return True if the object is active."""
if isinstance(obj, PaneSnapshot):
Expand All @@ -927,4 +971,4 @@ def is_active(
if filtered is None:
error_msg = "No active objects found!"
raise ValueError(error_msg)
return t.cast("ServerSnapshot", filtered)
return t.cast(ServerSnapshot, filtered)
0