easy-exit-calls 1.0.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,33 @@
1
+ """
2
+ This module provides a simple way to register exit handlers for your Python applications.
3
+
4
+ The main class is ExitCallHandler, which is used to register and manage exit handlers.
5
+
6
+ Included is a decorator, register_exit_handler, which can be used to register an exit handler for a function. This is a
7
+ convenient way to register exit handlers for functions.
8
+
9
+ Example:
10
+ >>> from easy_exit_calls import register_exit_handler, ExitCallHandler
11
+ >>>
12
+ >>> @register_exit_handler()
13
+ ... def my_exit_handler():
14
+ ... print("Exiting...")
15
+ ...
16
+ >>> # Create an instance of ExitCallHandler
17
+ >>> eh = ExitCallHandler()
18
+ >>>
19
+ >>> # Register an exit handler
20
+ >>> eh.register_handler(my_exit_handler)
21
+ >>>
22
+ >>> # Call the exit handlers
23
+ >>> eh.call_handlers()
24
+ Exiting...
25
+ """
26
+
27
+ from easy_exit_calls.classes import ExitCallHandler
28
+ from easy_exit_calls.decorator import register_exit_handler
29
+
30
+ __all__ = [
31
+ 'ExitCallHandler',
32
+ 'register_exit_handler',
33
+ ]
@@ -0,0 +1,477 @@
1
+ import atexit
2
+ import threading
3
+ import traceback
4
+ from uuid import uuid4, UUID
5
+ from easy_exit_calls.log_engine import ROOT_LOGGER, Loggable
6
+ from easy_exit_calls.helpers.handler_list import HandlerList
7
+
8
+ MOD_LOGGER = ROOT_LOGGER.get_child('components.exit_calls')
9
+
10
+
11
+ class ExitCallHandler(Loggable):
12
+ """
13
+ A singleton class for managing exit handlers that are executed when the program terminates.
14
+
15
+ This class allows registration, execution, and removal of exit handlers. Handlers can be executed in
16
+ **either** LIFO (Last-In-First-Out) or FIFO (First-In-First-Out) order. It integrates with Python's `atexit`
17
+ module to ensure handlers are executed upon normal program termination.
18
+
19
+ Properties:
20
+ fifo (bool):
21
+ If True, handlers execute in FIFO order; otherwise, LIFO.
22
+
23
+ handler_keys (list):
24
+ A list of unique keys representing registered handlers.
25
+
26
+ handlers (list):
27
+ A list of registered handlers.
28
+
29
+ handler_uuids (list):
30
+ A list of UUIDs for registered handlers.
31
+
32
+ lifo (bool):
33
+ Indicates whether handlers execute in LIFO order.
34
+
35
+ registered_with_atexit (bool):
36
+ Indicates whether the handler is registered with `atexit`.
37
+
38
+ Methods:
39
+ call_handlers():
40
+ Executes all registered exit handlers.
41
+
42
+ clear_handlers():
43
+ Removes all registered exit handlers.
44
+
45
+ find_handler_by_uuid(uuid):
46
+ Retrieves a handler by its UUID.
47
+
48
+ function_registered(func):
49
+ Checks if a function is already registered as an exit handler.
50
+
51
+ has_key(key):
52
+ Determines if a handler with a given key exists.
53
+
54
+ has_uuid(uuid):
55
+ Determines if a handler with a given UUID exists.
56
+
57
+ register_handler(func, *args, return_func=False, **kwargs):
58
+ Registers a function as an exit handler.
59
+
60
+ register_self_with_atexit():
61
+ Registers the handler with Python’s `atexit` module.
62
+
63
+ unregister_all():
64
+ Unregisters all exit handlers.
65
+
66
+ unregister_all_with_name(name):
67
+ Unregisters all handlers with a specific name.
68
+
69
+ unregister_by_uuid(uuid):
70
+ Removes a registered handler by UUID.
71
+
72
+ unregister_handler(func, *args, **kwargs):
73
+ Unregisters a specific exit handler by function reference and arguments.
74
+
75
+ unregister_self_with_atexit():
76
+ Removes this handler from Python’s `atexit` module.
77
+
78
+ Example Usage:
79
+ ```python
80
+ handler = ExitCallHandler()
81
+
82
+ def cleanup():
83
+ print("Cleaning up!")
84
+
85
+ handler.register_handler(cleanup)
86
+ ```
87
+ """
88
+ _instance = None
89
+
90
+ def __new__(cls, *args, **kwargs):
91
+ if cls._instance is None:
92
+ cls._instance = super(ExitCallHandler, cls).__new__(cls, *args, **kwargs)
93
+ # Use a list to store handler dictionaries
94
+ cls._instance._handlers = HandlerList(
95
+ on_empty=cls._instance.unregister_self_with_atexit,
96
+ on_non_empty=cls._instance.register_self_with_atexit
97
+ )
98
+
99
+ cls._instance._lifo = True
100
+ cls._instance._lock = threading.Lock()
101
+
102
+ # Register cleanup method to run on exit
103
+ atexit.register(cls._instance._cleanup)
104
+
105
+ return cls._instance
106
+
107
+ def __init__(self, fifo=False):
108
+
109
+ self.__registered_with_atexit = False
110
+
111
+ if isinstance(fifo, bool) and fifo:
112
+ self._lifo = False
113
+
114
+
115
+ if not hasattr(self, '_initialized'):
116
+ super().__init__(MOD_LOGGER)
117
+ self._initialized = True
118
+
119
+ def _check_for_key(self, key):
120
+ """
121
+ Checks if a key already exists in the registered handlers.
122
+
123
+ Parameters:
124
+ key (tuple):
125
+ A tuple containing the function, arguments, and keyword arguments of the handler.
126
+
127
+ Returns:
128
+ bool:
129
+ True if the key exists, False otherwise.
130
+ """
131
+ for entry in self.handlers:
132
+ existing_key = self._get_key(entry['func'], entry['args'], entry['kwargs'])
133
+ if existing_key == key:
134
+ return True
135
+
136
+ def _cleanup(self):
137
+ """
138
+ Executes all registered exit handlers.
139
+ """
140
+ log = self.method_logger
141
+
142
+ handlers = reversed(self._handlers) if self.lifo else self._handlers
143
+
144
+ for entry in list(handlers):
145
+ name = ''
146
+ if 'handler_info' in entry:
147
+ info = entry['handler_info']
148
+ # Build a name based on module and function name
149
+ name = f"{info.get('module', '')}."
150
+ if info.get('name'):
151
+ name += info['name']
152
+ else:
153
+ name = "Unknown"
154
+
155
+ log.debug(f"Running exit handler {name}")
156
+
157
+ func = entry['func']
158
+ args = entry['args']
159
+ kwargs = entry['kwargs']
160
+
161
+ log.debug(f"Running exit handler {name} | Function: {func} | Args: {args} | Kwargs: {kwargs}")
162
+
163
+ try:
164
+ func(*args, **kwargs)
165
+ except Exception as e:
166
+ log.error(f"Error while running exit handler {func}: {e}")
167
+ log.debug(f"Traceback:\n{traceback.format_exc()}")
168
+ raise e from e
169
+
170
+ def _get_key(self, func: callable, args: tuple, kwargs: dict):
171
+ """
172
+ Create a unique key from a function, its arguments, and keyword arguments.
173
+
174
+ Parameters:
175
+ func (callable):
176
+ The function to be called.
177
+
178
+ args (tuple):
179
+ Positional arguments to be passed to the function.
180
+
181
+ kwargs (dict):
182
+ Keyword arguments to be passed to the function.
183
+
184
+ Returns:
185
+ tuple:
186
+ A tuple containing the function, arguments, and keyword arguments.
187
+ """
188
+ return (func, args, frozenset(kwargs.items()))
189
+
190
+ @property
191
+ def fifo(self) -> bool:
192
+ """
193
+ Get the FIFO mode for the handler.
194
+
195
+ Returns:
196
+ bool:
197
+ True if FIFO mode is enabled, False otherwise.
198
+
199
+ """
200
+ return not self._lifo
201
+
202
+ @fifo.setter
203
+ def fifo(self, new: bool) -> None:
204
+ """
205
+ Set the FIFO mode for the handler.
206
+
207
+ Parameters:
208
+ new (bool):
209
+ True to set FIFO mode, False to set LIFO mode.
210
+
211
+ Returns:
212
+ None
213
+
214
+ Raises:
215
+ TypeError:
216
+ If the new value is not a boolean.
217
+ """
218
+ if not isinstance(new, bool):
219
+ raise TypeError("fifo must be a boolean")
220
+
221
+ self._lifo = not new
222
+
223
+ @property
224
+ def handler_keys(self) -> list[tuple]:
225
+ """
226
+ Returns a list of tuples containing the keys for all registered handlers.
227
+
228
+ Returns:
229
+ list[tuple]:
230
+ A list of tuples containing the keys for all registered handlers;
231
+ each tuple contains the function, arguments, and keyword arguments.
232
+ """
233
+ return [self._get_key(entry['func'], entry['args'], entry['kwargs']) for entry in self.handlers]
234
+
235
+ @property
236
+ def handlers(self) -> HandlerList:
237
+ """
238
+ Returns a list of dictionaries representing the registered exit handlers.
239
+
240
+ Returns:
241
+ HandlerList:
242
+ A list of dictionaries representing the registered exit handlers.
243
+ """
244
+ return self._handlers
245
+
246
+ @property
247
+ def handler_uuids(self) -> list[UUID]:
248
+ """
249
+ Returns a list of UUIDs for all registered handlers.
250
+
251
+ Returns:
252
+ list[UUID]:
253
+ A list of UUIDs for all registered handlers.
254
+ """
255
+ return [entry.get('uuid') for entry in self.handlers]
256
+
257
+ @property
258
+ def lifo(self):
259
+ """
260
+ Get the LIFO status of the exit call handler.
261
+
262
+ Returns:
263
+ bool: True if the exit call handler is in LIFO mode, False otherwise.
264
+ """
265
+ return self._lifo
266
+
267
+ @lifo.setter
268
+ def lifo(self, new) -> None:
269
+ """
270
+ Set the LIFO status of the exit call handler.
271
+
272
+ If the value is True, handlers will execute in LIFO order. If False, handlers will execute in FIFO order.
273
+
274
+ Parameters:
275
+ new (bool):
276
+ The new LIFO status to set.
277
+
278
+ Returns:
279
+ None
280
+ """
281
+ if not isinstance(new,bool):
282
+ raise ValueError("lifo must be a boolean")
283
+
284
+ self._lifo = new
285
+
286
+ @property
287
+ def registered_with_atexit(self) -> bool:
288
+ """
289
+ Indicates whether the handler is registered with the `atexit` module.
290
+
291
+ Returns:
292
+ bool:
293
+ True if the handler is registered with `atexit`, False otherwise.
294
+ """
295
+ return self.__registered_with_atexit
296
+
297
+ def call_handlers(self) -> None:
298
+ """
299
+ Calls all registered exit handlers.
300
+
301
+ Returns:
302
+ None
303
+ """
304
+ log = self.method_logger
305
+
306
+ if not self.handlers:
307
+ log.warning("No exit handlers registered")
308
+ return
309
+
310
+ log.debug("Calling exit handlers")
311
+ self._cleanup()
312
+
313
+ def clear_handlers(self) -> None:
314
+ """
315
+ Removes all registered exit handlers.
316
+
317
+ Note:
318
+ Once all handlers are removed, the handler unregisters itself from the `atexit` module.
319
+
320
+ Returns:
321
+ None
322
+ """
323
+ log = self.method_logger
324
+
325
+ log.debug("Clearing all exit handlers")
326
+ self._handlers.clear()
327
+ log.debug("All exit handlers cleared")
328
+
329
+ def find_handler_by_uuid(self, uuid):
330
+ log = self.method_logger
331
+
332
+ if not isinstance(uuid, (UUID, str)):
333
+ log.error("UUID must be a UUID object or a string")
334
+ raise ValueError("UUID must be a UUID object or a string")
335
+
336
+ if isinstance(uuid, str):
337
+ uuid = UUID(uuid)
338
+
339
+ for entry in self.handlers:
340
+ if entry.get('uuid') and entry['uuid'] == uuid:
341
+ return entry
342
+
343
+ def function_registered(self, func):
344
+ return any([entry['func'] == func for entry in self.handlers])
345
+
346
+ def has_key(self, key):
347
+ return key in self.handler_keys
348
+
349
+ def has_uuid(self, uuid):
350
+ return any([entry.get('uuid') == uuid for entry in self.handlers])
351
+
352
+ def register_handler(self, func, *args, return_func=False, **kwargs):
353
+ log = self.method_logger
354
+
355
+ log.debug(f"Registering exit handler {func} | Args: {args} | Kwargs: {kwargs}")
356
+
357
+ # Check if the function is already registered
358
+ log.debug(f"Checking if handler {func} is already registered")
359
+
360
+ # Create a unique key from the function, its args, and kwargs
361
+ new_key = self._get_key(func, args, kwargs)
362
+
363
+ log.debug(f"New key: {new_key}")
364
+
365
+ if self.has_key(new_key):
366
+ log.warning(f"Handler {func} is already registered")
367
+ return func
368
+
369
+
370
+ info = {
371
+ 'handler_info': {
372
+ 'module': func.__module__,
373
+ 'name': func.__name__,
374
+ },
375
+ 'func': func,
376
+ 'args': args,
377
+ 'kwargs': kwargs,
378
+ 'uuid': uuid4()
379
+ }
380
+
381
+ with self._lock:
382
+ # Otherwise, add the handler
383
+ self.handlers.append(info)
384
+ log.debug(f"Handler {func} registered successfully")
385
+
386
+ return info['uuid']
387
+
388
+ def register_self_with_atexit(self):
389
+ log = self.method_logger
390
+
391
+ log.debug('Checking if registered with atexit already...')
392
+
393
+ if not self.registered_with_atexit:
394
+ log.debug('Registering with atexit...')
395
+ atexit.register(self._cleanup)
396
+ self.__registered_with_atexit = True
397
+
398
+ def unregister_all(self):
399
+ self.clear_handlers()
400
+
401
+ def unregister_all_with_name(self, name: str) -> None:
402
+ """
403
+ Unregister all exit handlers with a specific name.
404
+
405
+ Parameters:
406
+ name (str):
407
+ The name of the exit handlers to unregister.
408
+
409
+ Returns:
410
+ None
411
+ """
412
+ log = self.method_logger
413
+
414
+ log.debug(f"Unregistering all exit handlers with name {name}")
415
+
416
+ for entry in self._handlers:
417
+ if 'handler_info' in entry:
418
+ info = entry['handler_info']
419
+ if info.get('name') == name:
420
+ log.debug(f"Removing handler {info.get('module', '')}.{info.get('name')}")
421
+ self._handlers.remove(entry)
422
+
423
+ log.debug(f"All handlers with name {name} unregistered successfully")
424
+
425
+ def unregister_by_uuid(self, uuid):
426
+ log = self.method_logger
427
+
428
+ log.debug(f"Unregistering exit handler with UUID {uuid}")
429
+
430
+ handler = self.find_handler_by_uuid(uuid)
431
+
432
+ if handler:
433
+ log.debug(f"Removing handler {handler['func']}")
434
+ self._handlers.remove(handler)
435
+ log.debug(f"Handler with UUID {uuid} unregistered successfully")
436
+ return True
437
+
438
+ log.warning(f"Handler with UUID {uuid} not found")
439
+
440
+ return False
441
+
442
+ def unregister_handler(self, func, *args, **kwargs):
443
+ log = self.method_logger
444
+
445
+ log.debug(f"Unregistering exit handler {func} | Args: {args} | Kwargs: {kwargs}")
446
+
447
+ # Create a unique key from the function, its args, and kwargs
448
+ key = (func, args, frozenset(kwargs.items()))
449
+
450
+ log.debug(f"Key: {key}")
451
+
452
+ for entry in self._handlers:
453
+ existing_key = (entry['func'], entry['args'], frozenset(entry['kwargs'].items()))
454
+ if existing_key == key:
455
+ log.debug(f"Removing handler {func}")
456
+ self._handlers.remove(entry)
457
+ log.debug(f"Handler {func} unregistered successfully")
458
+ return func
459
+
460
+ log.warning(f"Handler {func} not found")
461
+ return None
462
+
463
+ def unregister_self_with_atexit(self):
464
+ log = self.method_logger
465
+
466
+ log.debug('Checking if registered with atexit already...')
467
+
468
+ if self.registered_with_atexit:
469
+ log.debug('Unregistering with atexit...')
470
+ atexit.unregister(self._cleanup)
471
+ self.__registered_with_atexit = False
472
+ return True
473
+ else:
474
+ log.warning('ExitCallHandler is not registered with atexit!')
475
+
476
+ return False
477
+