1313import contextlib
1414import copy
1515import typing as t
16- from dataclasses import dataclass , field
16+ from dataclasses import field
1717from datetime import datetime
1818from types import TracebackType
1919
20+ from libtmux ._internal .frozen_dataclass_sealable import frozen_dataclass_sealable
2021from libtmux ._internal .query_list import QueryList
2122from libtmux .pane import Pane
2223from libtmux .server import Server
2728 pass
2829
2930
30- @dataclass
31+ @frozen_dataclass_sealable
3132class PaneSnapshot (Pane ):
3233 """A read-only snapshot of a tmux pane.
3334
@@ -37,19 +38,9 @@ class PaneSnapshot(Pane):
3738 # Fields only present in snapshot
3839 pane_content : list [str ] | None = None
3940 created_at : datetime = field (default_factory = datetime .now )
40- window_snapshot : WindowSnapshot | None = None
41- _read_only : bool = field (default = False , repr = False )
42-
43- def __post_init__ (self ) -> None :
44- """Make instance effectively read-only after initialization."""
45- object .__setattr__ (self , "_read_only" , True )
46-
47- def __setattr__ (self , name : str , value : t .Any ) -> None :
48- """Prevent attribute modification after initialization."""
49- if hasattr (self , "_read_only" ) and self ._read_only :
50- error_msg = f"Cannot modify '{ name } ' on read-only PaneSnapshot"
51- raise AttributeError (error_msg )
52- super ().__setattr__ (name , value )
41+ window_snapshot : WindowSnapshot | None = field (
42+ default = None , metadata = {"mutable_during_init" : True }
43+ )
5344
5445 def __enter__ (self ) -> PaneSnapshot :
5546 """Context manager entry point."""
@@ -116,8 +107,7 @@ def from_pane(
116107 with contextlib .suppress (Exception ):
117108 pane_content = pane .capture_pane ()
118109
119- # Gather fields from the parent Pane class
120- # We need to use object.__setattr__ to bypass our own __setattr__ override
110+ # Create a new snapshot instance
121111 snapshot = cls (server = pane .server )
122112
123113 # Copy all relevant attributes from the original pane
@@ -130,10 +120,13 @@ def from_pane(
130120 object .__setattr__ (snapshot , "window_snapshot" , window_snapshot )
131121 object .__setattr__ (snapshot , "created_at" , datetime .now ())
132122
123+ # Seal the snapshot
124+ snapshot .seal ()
125+
133126 return snapshot
134127
135128
136- @dataclass
129+ @frozen_dataclass_sealable
137130class WindowSnapshot (Window ):
138131 """A read-only snapshot of a tmux window.
139132
@@ -142,20 +135,12 @@ class WindowSnapshot(Window):
142135
143136 # Fields only present in snapshot
144137 created_at : datetime = field (default_factory = datetime .now )
145- session_snapshot : SessionSnapshot | None = None
146- panes_snapshot : list [PaneSnapshot ] = field (default_factory = list )
147- _read_only : bool = field (default = False , repr = False )
148-
149- def __post_init__ (self ) -> None :
150- """Make instance effectively read-only after initialization."""
151- object .__setattr__ (self , "_read_only" , True )
152-
153- def __setattr__ (self , name : str , value : t .Any ) -> None :
154- """Prevent attribute modification after initialization."""
155- if hasattr (self , "_read_only" ) and self ._read_only :
156- error_msg = f"Cannot modify '{ name } ' on read-only WindowSnapshot"
157- raise AttributeError (error_msg )
158- super ().__setattr__ (name , value )
138+ session_snapshot : SessionSnapshot | None = field (
139+ default = None , metadata = {"mutable_during_init" : True }
140+ )
141+ panes_snapshot : list [PaneSnapshot ] = field (
142+ default_factory = list , metadata = {"mutable_during_init" : True }
143+ )
159144
160145 def __enter__ (self ) -> WindowSnapshot :
161146 """Context manager entry point."""
@@ -216,57 +201,48 @@ def from_window(
216201 WindowSnapshot
217202 A read-only snapshot of the window
218203 """
219- # Create a new window snapshot instance
204+ # Create the window snapshot first (without panes)
220205 snapshot = cls (server = window .server )
221206
222- # Copy all relevant attributes from the original window
207+ # Copy window attributes
223208 for name , value in vars (window ).items ():
224- if not name .startswith ("_" ) and name not in [ "panes" , "session" ]:
209+ if not name .startswith ("_" ): # Skip private attributes
225210 object .__setattr__ (snapshot , name , copy .deepcopy (value ))
226211
227212 # Set snapshot-specific fields
228213 object .__setattr__ (snapshot , "created_at" , datetime .now ())
229214 object .__setattr__ (snapshot , "session_snapshot" , session_snapshot )
230215
231- # Now snapshot all panes
216+ # Snapshot panes (after session_snapshot is set to maintain bi-directional links)
232217 panes_snapshot = []
233- for p in window .panes :
218+ for pane in window .panes :
234219 pane_snapshot = PaneSnapshot .from_pane (
235- p , capture_content = capture_content , window_snapshot = snapshot
220+ pane , capture_content = capture_content , window_snapshot = snapshot
236221 )
237222 panes_snapshot .append (pane_snapshot )
238-
239223 object .__setattr__ (snapshot , "panes_snapshot" , panes_snapshot )
240224
225+ # Seal the snapshot to prevent further modifications
226+ snapshot .seal ()
227+
241228 return snapshot
242229
243230
244- @dataclass
231+ @frozen_dataclass_sealable
245232class SessionSnapshot (Session ):
246233 """A read-only snapshot of a tmux session.
247234
248235 This maintains compatibility with the original Session class but prevents modification.
249236 """
250237
251- # Make server field optional by giving it a default value
252- server : t .Any = None # type: ignore
253-
254238 # Fields only present in snapshot
255239 created_at : datetime = field (default_factory = datetime .now )
256- server_snapshot : ServerSnapshot | None = None
257- windows_snapshot : list [WindowSnapshot ] = field (default_factory = list )
258- _read_only : bool = field (default = False , repr = False )
259-
260- def __post_init__ (self ) -> None :
261- """Make instance effectively read-only after initialization."""
262- object .__setattr__ (self , "_read_only" , True )
263-
264- def __setattr__ (self , name : str , value : t .Any ) -> None :
265- """Prevent attribute modification after initialization."""
266- if hasattr (self , "_read_only" ) and self ._read_only :
267- error_msg = f"Cannot modify '{ name } ' on read-only SessionSnapshot"
268- raise AttributeError (error_msg )
269- super ().__setattr__ (name , value )
240+ server_snapshot : ServerSnapshot | None = field (
241+ default = None , metadata = {"mutable_during_init" : True }
242+ )
243+ windows_snapshot : list [WindowSnapshot ] = field (
244+ default_factory = list , metadata = {"mutable_during_init" : True }
245+ )
270246
271247 def __enter__ (self ) -> SessionSnapshot :
272248 """Context manager entry point."""
@@ -299,10 +275,10 @@ def server(self) -> ServerSnapshot | None:
299275 @property
300276 def active_window (self ) -> WindowSnapshot | None :
301277 """Return the active window snapshot, if any."""
302- for window in self . windows_snapshot :
303- if getattr (window , "window_active" , "0" ) == "1" :
304- return window
305- return None
278+ active_windows = [
279+ w for w in self . windows_snapshot if getattr (w , "window_active" , "0" ) == "1"
280+ ]
281+ return active_windows [ 0 ] if active_windows else None
306282
307283 @property
308284 def active_pane (self ) -> PaneSnapshot | None :
@@ -334,41 +310,34 @@ def from_session(
334310 SessionSnapshot
335311 A read-only snapshot of the session
336312 """
337- # Create a new empty instance using __new__ to bypass __init__
338- snapshot = cls .__new__ (cls )
339-
340- # Initialize _read_only to False to allow setting attributes
341- object .__setattr__ (snapshot , "_read_only" , False )
313+ # Create the session snapshot first (without windows)
314+ snapshot = cls (server = session .server )
342315
343- # Copy all relevant attributes from the original session
316+ # Copy session attributes
344317 for name , value in vars (session ).items ():
345- if not name .startswith ("_" ) and name not in [ "server" , "windows" ]:
318+ if not name .startswith ("_" ): # Skip private attributes
346319 object .__setattr__ (snapshot , name , copy .deepcopy (value ))
347320
348321 # Set snapshot-specific fields
349322 object .__setattr__ (snapshot , "created_at" , datetime .now ())
350323 object .__setattr__ (snapshot , "server_snapshot" , server_snapshot )
351324
352- # Initialize empty lists
353- object .__setattr__ (snapshot , "windows_snapshot" , [])
354-
355- # Now snapshot all windows
325+ # Snapshot windows (after server_snapshot is set to maintain bi-directional links)
356326 windows_snapshot = []
357- for w in session .windows :
327+ for window in session .windows :
358328 window_snapshot = WindowSnapshot .from_window (
359- w , capture_content = capture_content , session_snapshot = snapshot
329+ window , capture_content = capture_content , session_snapshot = snapshot
360330 )
361331 windows_snapshot .append (window_snapshot )
362-
363332 object .__setattr__ (snapshot , "windows_snapshot" , windows_snapshot )
364333
365- # Finally, set _read_only to True to prevent future modifications
366- object . __setattr__ ( snapshot , "_read_only" , True )
334+ # Seal the snapshot to prevent further modifications
335+ snapshot . seal ( )
367336
368337 return snapshot
369338
370339
371- @dataclass
340+ @frozen_dataclass_sealable
372341class ServerSnapshot (Server ):
373342 """A read-only snapshot of a tmux server.
374343
@@ -377,21 +346,15 @@ class ServerSnapshot(Server):
377346
378347 # Fields only present in snapshot
379348 created_at : datetime = field (default_factory = datetime .now )
380- sessions_snapshot : list [SessionSnapshot ] = field (default_factory = list )
381- windows_snapshot : list [WindowSnapshot ] = field (default_factory = list )
382- panes_snapshot : list [PaneSnapshot ] = field (default_factory = list )
383- _read_only : bool = field (default = False , repr = False )
384-
385- def __post_init__ (self ) -> None :
386- """Make instance effectively read-only after initialization."""
387- object .__setattr__ (self , "_read_only" , True )
388-
389- def __setattr__ (self , name : str , value : t .Any ) -> None :
390- """Prevent attribute modification after initialization."""
391- if hasattr (self , "_read_only" ) and self ._read_only :
392- error_msg = f"Cannot modify '{ name } ' on read-only ServerSnapshot"
393- raise AttributeError (error_msg )
394- super ().__setattr__ (name , value )
349+ sessions_snapshot : list [SessionSnapshot ] = field (
350+ default_factory = list , metadata = {"mutable_during_init" : True }
351+ )
352+ windows_snapshot : list [WindowSnapshot ] = field (
353+ default_factory = list , metadata = {"mutable_during_init" : True }
354+ )
355+ panes_snapshot : list [PaneSnapshot ] = field (
356+ default_factory = list , metadata = {"mutable_during_init" : True }
357+ )
395358
396359 def __enter__ (self ) -> ServerSnapshot :
397360 """Context manager entry point."""
@@ -415,10 +378,10 @@ def is_alive(self) -> bool:
415378 """Return False as snapshot servers are not connected to a live tmux instance."""
416379 return False
417380
418- def raise_if_dead (self ) -> t . NoReturn :
381+ def raise_if_dead (self ) -> None :
419382 """Raise exception as snapshots are not connected to a live server."""
420383 error_msg = "ServerSnapshot is not connected to a live tmux server"
421- raise NotImplementedError (error_msg )
384+ raise ConnectionError (error_msg )
422385
423386 @property
424387 def sessions (self ) -> QueryList [SessionSnapshot ]:
@@ -441,40 +404,31 @@ def from_server(
441404 ) -> ServerSnapshot :
442405 """Create a ServerSnapshot from a live Server.
443406
444- Examples
445- --------
446- >>> server_snap = ServerSnapshot.from_server(server)
447- >>> isinstance(server_snap, ServerSnapshot)
448- True
449- >>> # Check if it preserves the class hierarchy relationship
450- >>> isinstance(server_snap, type(server))
451- True
452- >>> # Snapshot is read-only
453- >>> try:
454- ... server_snap.cmd("list-sessions")
455- ... except NotImplementedError:
456- ... print("Cannot execute commands on snapshot")
457- Cannot execute commands on snapshot
458- >>> # Check that server is correctly snapshotted
459- >>> server_snap.socket_name == server.socket_name
460- True
461-
462407 Parameters
463408 ----------
464409 server : Server
465410 Live server to snapshot
466411 include_content : bool, optional
467- Whether to capture the current content of all panes
412+ Whether to capture the current content of all panes, by default True
468413
469414 Returns
470415 -------
471416 ServerSnapshot
472417 A read-only snapshot of the server
418+
419+ Examples
420+ --------
421+ The ServerSnapshot.from_server method creates a snapshot of the server:
422+
423+ ```python
424+ server_snap = ServerSnapshot.from_server(server)
425+ isinstance(server_snap, ServerSnapshot) # True
426+ ```
473427 """
474- # Create a new server snapshot instance
428+ # Create the server snapshot (without sessions, windows, or panes)
475429 snapshot = cls ()
476430
477- # Copy all relevant attributes from the original server
431+ # Copy server attributes
478432 for name , value in vars (server ).items ():
479433 if not name .startswith ("_" ) and name not in [
480434 "sessions" ,
@@ -486,26 +440,34 @@ def from_server(
486440 # Set snapshot-specific fields
487441 object .__setattr__ (snapshot , "created_at" , datetime .now ())
488442
489- # Now snapshot all sessions
443+ # Snapshot all sessions, windows, and panes
490444 sessions_snapshot = []
491445 windows_snapshot = []
492446 panes_snapshot = []
493447
494- for s in server .sessions :
448+ # First, snapshot all sessions
449+ for session in server .sessions :
495450 session_snapshot = SessionSnapshot .from_session (
496- s , capture_content = include_content , server_snapshot = snapshot
451+ session ,
452+ capture_content = include_content ,
453+ server_snapshot = snapshot ,
497454 )
498455 sessions_snapshot .append (session_snapshot )
499456
500- # Also collect all windows and panes for quick access
501- windows_snapshot .extend (session_snapshot .windows_snapshot )
502- for w in session_snapshot .windows_snapshot :
503- panes_snapshot .extend (w .panes_snapshot )
457+ # Collect window and pane snapshots
458+ for window in session_snapshot .windows :
459+ windows_snapshot .append (window )
460+ for pane in window .panes :
461+ panes_snapshot .append (pane )
504462
463+ # Set all collected snapshots
505464 object .__setattr__ (snapshot , "sessions_snapshot" , sessions_snapshot )
506465 object .__setattr__ (snapshot , "windows_snapshot" , windows_snapshot )
507466 object .__setattr__ (snapshot , "panes_snapshot" , panes_snapshot )
508467
468+ # Seal the snapshot to prevent further modifications
469+ snapshot .seal ()
470+
509471 return snapshot
510472
511473
0 commit comments