From 7b23788fea5cb7a0be67d55b1a824ea26a4c9688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ma=C5=9Blanka?= <piotr.maslanka@henrietta.com.pl> Date: Mon, 30 Dec 2019 21:13:12 +0100 Subject: [PATCH] multiple changes --- CHANGELOG.md | 5 +- docs/coding/concurrent.rst | 7 +- docs/coding/functions.rst | 1 + docs/coding/structures.rst | 13 ++-- docs/configuration/index.rst | 1 + docs/configuration/schema.rst | 5 +- docs/configuration/sources.rst | 6 +- docs/exception_handling.rst | 72 +++++++++++++++++++ docs/exception_handling/index.rst | 13 ---- docs/index.rst | 2 +- docs/json.rst | 1 + satella/__init__.py | 2 +- satella/exception_handling/__init__.py | 2 + satella/exception_handling/dump_to_file.py | 10 +-- .../exception_handling/exception_handlers.py | 39 +++++++--- satella/exception_handling/global_eh.py | 10 +-- satella/exception_handling/memerrhandler.py | 27 ++++--- 17 files changed, 159 insertions(+), 57 deletions(-) create mode 100644 docs/exception_handling.rst delete mode 100644 docs/exception_handling/index.rst diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e001f4..a5f40b00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # v2.2.2 -* _TBA_ +* more docs +* fixed `MemoryErrorExceptionHandler` not being importable from `satella.exception_handling` +* fixed the custom_hook for `MemoryErrorExceptionHandler`: it won't + kill everything if the custom_hook returns True # v2.2.1 diff --git a/docs/coding/concurrent.rst b/docs/coding/concurrent.rst index f0684364..d6d67905 100644 --- a/docs/coding/concurrent.rst +++ b/docs/coding/concurrent.rst @@ -1,20 +1,21 @@ +========================== Concurrent data structures ========================== CallableGroup -------------- +============= .. autoclass:: satella.coding.concurrent.CallableGroup :members: LockedDataset -------------- +============= .. autoclass:: satella.coding.concurrent.LockedDataset :members: TerminableThread ----------------- +================ Note that _force=True_ is not available on PyPy. If an attempt to use it on PyPy is made, ``RuntimeError`` will be thrown. diff --git a/docs/coding/functions.rst b/docs/coding/functions.rst index 3c5c4bcb..1cd67d44 100644 --- a/docs/coding/functions.rst +++ b/docs/coding/functions.rst @@ -1,3 +1,4 @@ +========= Functions ========= diff --git a/docs/coding/structures.rst b/docs/coding/structures.rst index 2deb7ccc..267f28ec 100644 --- a/docs/coding/structures.rst +++ b/docs/coding/structures.rst @@ -1,10 +1,11 @@ +========== Structures ========== The following is a guide to all the data structures that Satella defines. Heap ----- +==== This essentially allows you to have a heap object that will pretty much behave like the `heapq <https://docs.python.org/2/library/heapq.html>` library. @@ -13,7 +14,7 @@ behave like the `heapq <https://docs.python.org/2/library/heapq.html>` library. :members: TimeBasedHeap ---------- +============= Time-based heap is a good structure if you have many callbacks set to fire at a particular time in the future. It functions very like a normal Heap. @@ -22,7 +23,7 @@ time in the future. It functions very like a normal Heap. :members: typednamedtuple ---------------- +=============== It's a named tuple, but it has typed fields. You will get a TypeError if you try to assign something else there. @@ -30,7 +31,7 @@ try to assign something else there. .. autofunction:: satella.coding.structures.typednamedtuple OmniHashableMixin ------------------ +================= If you need quick __hash__ and __eq__ operators from listed fields of the class. @@ -38,7 +39,7 @@ If you need quick __hash__ and __eq__ operators from listed fields of the class. :members: Singleton ---------- +========= Makes the resulting object's ``__init__()`` be called at most once, then caches the object and returns the same upon each instantiation. @@ -46,7 +47,7 @@ upon each instantiation. .. autofunction:: satella.coding.structures.Singleton DictObject ----------- +========== DictObject is an object constructed out of a dict, that allows it's values to be obtained as getattr(), and not only getitem(). diff --git a/docs/configuration/index.rst b/docs/configuration/index.rst index 5b45df2d..c00a54bb 100644 --- a/docs/configuration/index.rst +++ b/docs/configuration/index.rst @@ -1,3 +1,4 @@ +============= Configuration ============= diff --git a/docs/configuration/schema.rst b/docs/configuration/schema.rst index 201924bd..45d5a02e 100644 --- a/docs/configuration/schema.rst +++ b/docs/configuration/schema.rst @@ -1,5 +1,6 @@ -Schema validation -================= +=============================== +Configuration schema validation +=============================== As noted in index_, your configuration is mostly supposed to be a dict. To validate your schema, you should instantiate a Descriptor. Descriptor reflects how your config is nested. diff --git a/docs/configuration/sources.rst b/docs/configuration/sources.rst index 0a98dc66..ab3b653e 100644 --- a/docs/configuration/sources.rst +++ b/docs/configuration/sources.rst @@ -1,5 +1,6 @@ -Sources -======= +===================== +Configuration sources +===================== At the core of your config files, there are Sources. A Source is a single source of configuration - it could be an environment variable, or a particular file, or a directory full of these files. @@ -34,6 +35,7 @@ Then there are abstract sources of configuration. .. autoclass:: satella.configuration.sources.MergingSource :members: +In order to actually load the configuration, use the method ``provide()``. JSON schema diff --git a/docs/exception_handling.rst b/docs/exception_handling.rst new file mode 100644 index 00000000..548c2b06 --- /dev/null +++ b/docs/exception_handling.rst @@ -0,0 +1,72 @@ +================== +Exception handling +================== + +Satella provides a rich functionality to register exception hooks. + +Writing your own exception handlers +=================================== + +To write your own exception handlers, subclass the following class: + +.. autoclass:: satella.exception_handling.BaseExceptionHandler + :members: + +And then instantiate it and call ``install()``. + +If you got a callable of signature [type, BaseException, types.TracebackType] (type, value, traceback) that +returns True upon a request to swallow the exception, you can convert it to a Satella exception handler in two ways. + +First: + +:: + + a = FunctionExceptionHandler(exception_handle) + a.install() + +Or + +:: + + @exception_handler + def exception_handle(type, value, traceback) -> bool + ... + + exception_handle().install() + +.. autofunction:: satella.exception_handling.exception_handler + +.. autoclass:: satella.exception_handling.FunctionExceptionHandler + :members: + +Pre-defined exception handlers +============================== + +MemoryErrorExceptionHandler +--------------------------- + +.. autoclass:: satella.exception_handling.MemoryErrorExceptionHandler + +This exception hook kills the entire process if a `MemoryError` is spotted, under the rule that it's better to fail +early than admit undefined behaviour. + +DumpToFileHandler +----------------- + +.. autoclass:: satella.exception_handling.DumpToFileHandler + +A handler that will dump each stack frame of the exception, along with it's variables, to a file (or stdout/stderr). +Two file handles/names are admitted: + +* **human_readables** - where human-readable form of the exception and it's values will be output +* **trace_pickles** - where pickled `Traceback`s from the exception will be put. + +You can throw there either: + +* a **str** - file of given name will be created and data will be output to it +* a file-like object supporting ``write()`` (and optionally ``flush()``) - data will be output to it +* a None - output will be piped to /dev/null + +Note that the arguments are lists, so you can specify multiple target sources. + + diff --git a/docs/exception_handling/index.rst b/docs/exception_handling/index.rst deleted file mode 100644 index 0ea44485..00000000 --- a/docs/exception_handling/index.rst +++ /dev/null @@ -1,13 +0,0 @@ -Exception handling -================== - -Satella provides a rich functionality to register exception hooks. - -Writing your own exception handlers ------------------------------------ - -To write your own exception handlers, subclass the following class: - -.. autoclass:: satella.exception_handling.BaseExceptionHandler - :members: - diff --git a/docs/index.rst b/docs/index.rst index 30e19e9e..8a69a3f3 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,7 +14,7 @@ Welcome to satella's documentation! coding/concurrent instrumentation/traceback instrumentation/metrics - exception_handling/index + exception_handling json posix recipes diff --git a/docs/json.rst b/docs/json.rst index 5cab7d12..f9844a89 100644 --- a/docs/json.rst +++ b/docs/json.rst @@ -1,3 +1,4 @@ +==== JSON ==== diff --git a/satella/__init__.py b/satella/__init__.py index 17ff25bc..5ce5a63d 100644 --- a/satella/__init__.py +++ b/satella/__init__.py @@ -1,2 +1,2 @@ # coding=UTF-8 -__version__ = '2.2.2a1' +__version__ = '2.2.2a2' diff --git a/satella/exception_handling/__init__.py b/satella/exception_handling/__init__.py index 7a4e20bb..39502e18 100644 --- a/satella/exception_handling/__init__.py +++ b/satella/exception_handling/__init__.py @@ -1,10 +1,12 @@ from .dump_to_file import * from .exception_handlers import * from .global_eh import * +from .memerrhandler import MemoryErrorExceptionHandler __all__ = [ 'GlobalExcepthook', 'BaseExceptionHandler', 'exception_handler', 'FunctionExceptionHandler', 'NORMAL_PRIORITY', 'ALWAYS_LAST', 'ALWAYS_FIRST', + 'MemoryErrorExceptionHandler', 'DumpToFileHandler', 'AsStream' ] diff --git a/satella/exception_handling/dump_to_file.py b/satella/exception_handling/dump_to_file.py index 1ade0230..c32af116 100644 --- a/satella/exception_handling/dump_to_file.py +++ b/satella/exception_handling/dump_to_file.py @@ -12,12 +12,12 @@ from .exception_handlers import BaseExceptionHandler logger = logging.getLogger(__name__) __all__ = [ - 'DumpToFileHandler', 'AsStream' + 'DumpToFileHandler' ] AsStreamTypeAccept = tp.Union[str, tp.IO, None] AsStreamTypeAcceptHR = tp.Union[str, tp.TextIO] -AsStreamTypeAcceptpIN = tp.Union[str, tp.BinaryIO] +AsStreamTypeAcceptIN = tp.Union[str, tp.BinaryIO] class AsStream: @@ -80,11 +80,13 @@ class AsStream: class DumpToFileHandler(BaseExceptionHandler): """ - Write the stack trace to a stream-file + Write the stack trace to a stream-file. + + Note that your file-like objects you throw into that must support only .write() and optionally .flush() """ def __init__(self, human_readables: tp.Iterable[AsStreamTypeAcceptHR], - trace_pickles: tp.Iterable[AsStreamTypeAcceptpIN] = None): + trace_pickles: tp.Iterable[AsStreamTypeAcceptIN] = None): """ Handler that dumps an exception to a file. diff --git a/satella/exception_handling/exception_handlers.py b/satella/exception_handling/exception_handlers.py index cecc9f68..e36c4420 100644 --- a/satella/exception_handling/exception_handlers.py +++ b/satella/exception_handling/exception_handlers.py @@ -1,5 +1,5 @@ -import types import typing as tp +import types from abc import abstractmethod __all__ = [ @@ -8,7 +8,8 @@ __all__ = [ 'exception_handler', 'ALWAYS_FIRST', 'ALWAYS_LAST', - 'NORMAL_PRIORITY' + 'NORMAL_PRIORITY', + 'ExceptionHandlerCallable' ] ALWAYS_FIRST = -1000 @@ -16,28 +17,50 @@ NORMAL_PRIORITY = 0 ALWAYS_LAST = 1000 +ExceptionHandlerCallable = tp.Callable[[type, BaseException, types.TracebackType], bool] + + class BaseExceptionHandler: def __init__(self, priority=NORMAL_PRIORITY): + """ + Instantiate an exception handler with provided priority. + Handlers with smaller priorities run sooner. + + :param priority: Priority to use for this handler + """ self.priority = priority - def install(self): + def install(self) -> 'BaseExceptionHandler': + """ + Register this handler to run upon exceptions + """ from .global_eh import GlobalExcepthook GlobalExcepthook().add_hook(self) return self def uninstall(self): + """ + Unregister this handler to run on exceptions + """ from .global_eh import GlobalExcepthook GlobalExcepthook().remove_hook(self) @abstractmethod - def handle_exception(self, type_: tp.Optional[type], value, + def handle_exception(self, type_: tp.Callable[[type, BaseException, types.TracebackType], None], value, traceback: types.TracebackType) -> tp.Optional[bool]: - """Return True to intercept the exception. It won't be propagated to other handlers.""" + """ + Return True to intercept the exception, so that it won't be propagated to other handlers. + """ pass class FunctionExceptionHandler(BaseExceptionHandler): - def __init__(self, fun: tp.Callable, priority: int = NORMAL_PRIORITY): + """ + A exception handler to make callables of given signature into Satella's exception handlers. + + Your exception handler must return a bool, whether to intercept the exception and not propagate it. + """ + def __init__(self, fun: ExceptionHandlerCallable, priority: int = NORMAL_PRIORITY): super(FunctionExceptionHandler, self).__init__(priority) self.fun = fun @@ -52,7 +75,7 @@ def exception_handler(priority: int = NORMAL_PRIORITY): Convert a callable to an FunctionExceptionHandler. Usage >>> @exception_handler(priority=-10) - >>> def handle_exc(type, val, traceback): + >>> def handle_exc(type_, val, traceback): >>> ... :return: ExceptionHandler instance @@ -61,7 +84,7 @@ def exception_handler(priority: int = NORMAL_PRIORITY): if not isinstance(priority, int): raise TypeError('Did you forget to use it as @exception_handler() ?') - def outer(fun): + def outer(fun: ExceptionHandlerCallable) -> FunctionExceptionHandler: return FunctionExceptionHandler(fun, priority=priority) return outer diff --git a/satella/exception_handling/global_eh.py b/satella/exception_handling/global_eh.py index 015a213b..2d9b72a6 100644 --- a/satella/exception_handling/global_eh.py +++ b/satella/exception_handling/global_eh.py @@ -36,7 +36,7 @@ class GlobalExcepthook: self.installed_hooks.remove(hook) def add_hook(self, new_hook: tp.Union[ - tp.Callable, BaseExceptionHandler]) -> BaseExceptionHandler: + tp.Callable, BaseExceptionHandler]) -> BaseExceptionHandler: """ Register a hook to fire in case of an exception. @@ -78,7 +78,7 @@ class GlobalExcepthook: my_self.__except_handle( *sys.exc_info()) # by now, it's our handler :D except AttributeError: - eh = None # Python interpreter is in an advanced state of shutdown, just let it go + pass # Python interpreter is in an advanced state of shutdown, just let it go raise # re-raise if running on debug @@ -88,13 +88,13 @@ class GlobalExcepthook: threading.Thread.__init__ = init - def __except_handle(self, type, value, traceback) -> None: + def __except_handle(self, type_, value, traceback) -> None: hooks_to_run = self.installed_hooks + [self.old_excepthook] - for hook in sorted(hooks_to_run, key=lambda hook: hook.priority): + for hook in sorted(hooks_to_run, key=lambda h: h.priority): # noinspection PyBroadException try: - if hook.handle_exception(type, value, traceback): + if hook.handle_exception(type_, value, traceback): break except Exception as e: tb = Traceback() diff --git a/satella/exception_handling/memerrhandler.py b/satella/exception_handling/memerrhandler.py index 03abea26..74f808aa 100644 --- a/satella/exception_handling/memerrhandler.py +++ b/satella/exception_handling/memerrhandler.py @@ -1,23 +1,27 @@ -import logging import sys import time import typing as tp from satella.posix import suicide -from .exception_handlers import BaseExceptionHandler, ALWAYS_FIRST - -logger = logging.getLogger(__name__) +from .exception_handlers import BaseExceptionHandler, ALWAYS_FIRST, ExceptionHandlerCallable class MemoryErrorExceptionHandler(BaseExceptionHandler): - def __init__(self, custom_hook: tp.Callable = lambda type, value, traceback: None, + """ + A handler that terminates the entire process (or process group) is a MemoryError is seen. + + `custom_hook` is an exception callable to implement you own behavior. If it returns True, + then MemoryErrorExceptionHandler won't kill anyone. + """ + def __init__(self, custom_hook: ExceptionHandlerCallable = lambda type_, value, traceback: False, kill_pg: bool = False): """ - :param kill_pg: kill entire process group, if applicable + :param kill_pg: whether to kill entire process group, if applicable """ super(MemoryErrorExceptionHandler, self).__init__() self.priority = ALWAYS_FIRST # always run first! - self._free_on_memoryerror = {'a': bytearray(1024 * 2)} + # so that we have some spare space in case a MemoryError is thrown + self._free_on_memory_error = {'a': bytearray(1024 * 2)} self.custom_hook = custom_hook self.kill_pg = kill_pg self.installed = False @@ -29,15 +33,16 @@ class MemoryErrorExceptionHandler(BaseExceptionHandler): from .global_eh import GlobalExcepthook GlobalExcepthook().add_hook(self) - def handle_exception(self, type, value, traceback) -> tp.Optional[bool]: - if not issubclass(type, MemoryError): + def handle_exception(self, type_, value, traceback) -> tp.Optional[bool]: + if not issubclass(type_, MemoryError): return - del self._free_on_memoryerror['a'] + del self._free_on_memory_error['a'] # noinspection PyBroadException try: - self.custom_hook(type, value, traceback) + if self.custom_hook(type_, value, traceback): + return except Exception as e: pass -- GitLab