13
13
import contextlib
14
14
import copy
15
15
import typing as t
16
- from dataclasses import dataclass , field
16
+ from dataclasses import field
17
17
from datetime import datetime
18
18
from types import TracebackType
19
19
20
+ from libtmux ._internal .frozen_dataclass_sealable import frozen_dataclass_sealable
20
21
from libtmux ._internal .query_list import QueryList
21
22
from libtmux .pane import Pane
22
23
from libtmux .server import Server
27
28
pass
28
29
29
30
30
- @dataclass
31
+ @frozen_dataclass_sealable
31
32
class PaneSnapshot (Pane ):
32
33
"""A read-only snapshot of a tmux pane.
33
34
@@ -37,19 +38,9 @@ class PaneSnapshot(Pane):
37
38
# Fields only present in snapshot
38
39
pane_content : list [str ] | None = None
39
40
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
+ )
53
44
54
45
def __enter__ (self ) -> PaneSnapshot :
55
46
"""Context manager entry point."""
@@ -116,8 +107,7 @@ def from_pane(
116
107
with contextlib .suppress (Exception ):
117
108
pane_content = pane .capture_pane ()
118
109
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
121
111
snapshot = cls (server = pane .server )
122
112
123
113
# Copy all relevant attributes from the original pane
@@ -130,10 +120,13 @@ def from_pane(
130
120
object .__setattr__ (snapshot , "window_snapshot" , window_snapshot )
131
121
object .__setattr__ (snapshot , "created_at" , datetime .now ())
132
122
123
+ # Seal the snapshot
124
+ snapshot .seal ()
125
+
133
126
return snapshot
134
127
135
128
136
- @dataclass
129
+ @frozen_dataclass_sealable
137
130
class WindowSnapshot (Window ):
138
131
"""A read-only snapshot of a tmux window.
139
132
@@ -142,20 +135,12 @@ class WindowSnapshot(Window):
142
135
143
136
# Fields only present in snapshot
144
137
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
+ )
159
144
160
145
def __enter__ (self ) -> WindowSnapshot :
161
146
"""Context manager entry point."""
@@ -216,57 +201,48 @@ def from_window(
216
201
WindowSnapshot
217
202
A read-only snapshot of the window
218
203
"""
219
- # Create a new window snapshot instance
204
+ # Create the window snapshot first (without panes)
220
205
snapshot = cls (server = window .server )
221
206
222
- # Copy all relevant attributes from the original window
207
+ # Copy window attributes
223
208
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
225
210
object .__setattr__ (snapshot , name , copy .deepcopy (value ))
226
211
227
212
# Set snapshot-specific fields
228
213
object .__setattr__ (snapshot , "created_at" , datetime .now ())
229
214
object .__setattr__ (snapshot , "session_snapshot" , session_snapshot )
230
215
231
- # Now snapshot all panes
216
+ # Snapshot panes (after session_snapshot is set to maintain bi-directional links)
232
217
panes_snapshot = []
233
- for p in window .panes :
218
+ for pane in window .panes :
234
219
pane_snapshot = PaneSnapshot .from_pane (
235
- p , capture_content = capture_content , window_snapshot = snapshot
220
+ pane , capture_content = capture_content , window_snapshot = snapshot
236
221
)
237
222
panes_snapshot .append (pane_snapshot )
238
-
239
223
object .__setattr__ (snapshot , "panes_snapshot" , panes_snapshot )
240
224
225
+ # Seal the snapshot to prevent further modifications
226
+ snapshot .seal ()
227
+
241
228
return snapshot
242
229
243
230
244
- @dataclass
231
+ @frozen_dataclass_sealable
245
232
class SessionSnapshot (Session ):
246
233
"""A read-only snapshot of a tmux session.
247
234
248
235
This maintains compatibility with the original Session class but prevents modification.
249
236
"""
250
237
251
- # Make server field optional by giving it a default value
252
- server : t .Any = None # type: ignore
253
-
254
238
# Fields only present in snapshot
255
239
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
+ )
270
246
271
247
def __enter__ (self ) -> SessionSnapshot :
272
248
"""Context manager entry point."""
@@ -299,10 +275,10 @@ def server(self) -> ServerSnapshot | None:
299
275
@property
300
276
def active_window (self ) -> WindowSnapshot | None :
301
277
"""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
306
282
307
283
@property
308
284
def active_pane (self ) -> PaneSnapshot | None :
@@ -334,41 +310,34 @@ def from_session(
334
310
SessionSnapshot
335
311
A read-only snapshot of the session
336
312
"""
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 )
342
315
343
- # Copy all relevant attributes from the original session
316
+ # Copy session attributes
344
317
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
346
319
object .__setattr__ (snapshot , name , copy .deepcopy (value ))
347
320
348
321
# Set snapshot-specific fields
349
322
object .__setattr__ (snapshot , "created_at" , datetime .now ())
350
323
object .__setattr__ (snapshot , "server_snapshot" , server_snapshot )
351
324
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)
356
326
windows_snapshot = []
357
- for w in session .windows :
327
+ for window in session .windows :
358
328
window_snapshot = WindowSnapshot .from_window (
359
- w , capture_content = capture_content , session_snapshot = snapshot
329
+ window , capture_content = capture_content , session_snapshot = snapshot
360
330
)
361
331
windows_snapshot .append (window_snapshot )
362
-
363
332
object .__setattr__ (snapshot , "windows_snapshot" , windows_snapshot )
364
333
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 ( )
367
336
368
337
return snapshot
369
338
370
339
371
- @dataclass
340
+ @frozen_dataclass_sealable
372
341
class ServerSnapshot (Server ):
373
342
"""A read-only snapshot of a tmux server.
374
343
@@ -377,21 +346,15 @@ class ServerSnapshot(Server):
377
346
378
347
# Fields only present in snapshot
379
348
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
+ )
395
358
396
359
def __enter__ (self ) -> ServerSnapshot :
397
360
"""Context manager entry point."""
@@ -415,10 +378,10 @@ def is_alive(self) -> bool:
415
378
"""Return False as snapshot servers are not connected to a live tmux instance."""
416
379
return False
417
380
418
- def raise_if_dead (self ) -> t . NoReturn :
381
+ def raise_if_dead (self ) -> None :
419
382
"""Raise exception as snapshots are not connected to a live server."""
420
383
error_msg = "ServerSnapshot is not connected to a live tmux server"
421
- raise NotImplementedError (error_msg )
384
+ raise ConnectionError (error_msg )
422
385
423
386
@property
424
387
def sessions (self ) -> QueryList [SessionSnapshot ]:
@@ -441,40 +404,31 @@ def from_server(
441
404
) -> ServerSnapshot :
442
405
"""Create a ServerSnapshot from a live Server.
443
406
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
-
462
407
Parameters
463
408
----------
464
409
server : Server
465
410
Live server to snapshot
466
411
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
468
413
469
414
Returns
470
415
-------
471
416
ServerSnapshot
472
417
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
+ ```
473
427
"""
474
- # Create a new server snapshot instance
428
+ # Create the server snapshot (without sessions, windows, or panes)
475
429
snapshot = cls ()
476
430
477
- # Copy all relevant attributes from the original server
431
+ # Copy server attributes
478
432
for name , value in vars (server ).items ():
479
433
if not name .startswith ("_" ) and name not in [
480
434
"sessions" ,
@@ -486,26 +440,34 @@ def from_server(
486
440
# Set snapshot-specific fields
487
441
object .__setattr__ (snapshot , "created_at" , datetime .now ())
488
442
489
- # Now snapshot all sessions
443
+ # Snapshot all sessions, windows, and panes
490
444
sessions_snapshot = []
491
445
windows_snapshot = []
492
446
panes_snapshot = []
493
447
494
- for s in server .sessions :
448
+ # First, snapshot all sessions
449
+ for session in server .sessions :
495
450
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 ,
497
454
)
498
455
sessions_snapshot .append (session_snapshot )
499
456
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 )
504
462
463
+ # Set all collected snapshots
505
464
object .__setattr__ (snapshot , "sessions_snapshot" , sessions_snapshot )
506
465
object .__setattr__ (snapshot , "windows_snapshot" , windows_snapshot )
507
466
object .__setattr__ (snapshot , "panes_snapshot" , panes_snapshot )
508
467
468
+ # Seal the snapshot to prevent further modifications
469
+ snapshot .seal ()
470
+
509
471
return snapshot
510
472
511
473
0 commit comments