From ef24d9cfeecf88565abe5950ee1d4d37817462a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Ma=C5=9Blanka?= <piotr.maslanka@henrietta.com.pl> Date: Sat, 21 Mar 2020 13:58:32 +0100 Subject: [PATCH] add returns to silence_excs --- CHANGELOG.md | 4 ++ README.md | 2 + satella/coding/recast_exceptions.py | 23 ++++---- satella/coding/structures/dictionaries.py | 72 +++++++++++------------ satella/imports.py | 8 +-- tests/test_imports/test_import.py | 5 +- 6 files changed, 59 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9a09c05..628c0d59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ * `LockedStructure` is now generic * added `returns` to `rethrow_as` +* patched `DictObject` to inherit from `UserDict` + instead of `dict` +* imports will log it's problem via warning, and now + to the log anymore # v2.5.12 diff --git a/README.md b/README.md index f7ec1192..58a2f423 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ Satella is an almost-zero-requirements Python 3.5+ library for writing server applications, especially those dealing with mundane but useful things. It also runs on PyPy. +Satella uses [semantic versioning 2.0](https://semver.org/spec/v2.0.0.html). + Satella contains, among other things: * things to help you manage your [application's configuration](satella/configuration) diff --git a/satella/coding/recast_exceptions.py b/satella/coding/recast_exceptions.py index 12f9795a..2ce83f67 100644 --- a/satella/coding/recast_exceptions.py +++ b/satella/coding/recast_exceptions.py @@ -1,5 +1,5 @@ import typing as tp - +import threading from .decorators import wraps __all__ = [ @@ -48,14 +48,6 @@ class rethrow_as: >>> rethrow_as((NameError, ValueError), (OSError, IOError)) If the second value is a None, exception will be silenced. - - If you are using it as a decorator, you can specify what value should the function return - by using the returns kwarg: - - >>> @rethrow_as(KeyError, None, returns=5) - >>> def returns_5(): - >>> raise KeyError() - >>> assert returns_5() == 5 """ __slots__ = ('mapping', 'exception_preprocessor', 'returns', '__exception_remapped') @@ -83,14 +75,18 @@ class rethrow_as: self.mapping = list(pairs) self.exception_preprocessor = exception_preprocessor or repr self.returns = returns - self.__exception_remapped = False - def __call__(self, fun: tp.Callable) -> tp.Any: + # this is threading.local because two threads may execute the same function at the + # same time, and exceptions from one function would leak to another + self.__exception_remapped = threading.local() + + def __call__(self, fun: tp.Callable) -> tp.Callable: @wraps(fun) def inner(*args, **kwargs): with self: v = fun(*args, **kwargs) - if self.__exception_remapped: + if self.__exception_remapped.was_raised: + # This means that the normal flow of execution was interrupted return self.returns else: return v @@ -98,13 +94,14 @@ class rethrow_as: return inner def __enter__(self): + self.__exception_remapped.was_raised = False return self def __exit__(self, exc_type, exc_val, exc_tb): if exc_type is not None: for from_, to in self.mapping: if issubclass(exc_type, from_): - self.__exception_remapped = True + self.__exception_remapped.was_raised = True if to is None: return True else: diff --git a/satella/coding/structures/dictionaries.py b/satella/coding/structures/dictionaries.py index 8ba179a7..c5f3d7e7 100644 --- a/satella/coding/structures/dictionaries.py +++ b/satella/coding/structures/dictionaries.py @@ -1,18 +1,18 @@ import collections.abc +import collections import copy import typing as tp from satella.coding.recast_exceptions import rethrow_as from satella.configuration.schema import Descriptor, descriptor_from_dict from satella.exceptions import ConfigurationValidationError -from ..decorators import for_argument __all__ = ['DictObject', 'apply_dict_object', 'DictionaryView', 'TwoWayDictionary'] K, V, T = tp.TypeVar('K'), tp.TypeVar('V'), tp.TypeVar('T') -class DictObject(dict, tp.Generic[T]): +class DictObject(tp.MutableMapping[str, T]): """ A dictionary wrapper that can be accessed by attributes. @@ -25,18 +25,48 @@ class DictObject(dict, tp.Generic[T]): >>> self.assertEqual(a.test, 5) """ + def __init__(self, *args, **kwargs): + self.__data = dict(*args, **kwargs) + + def __delitem__(self, k: str) -> None: + del self.__data[k] + + def __setitem__(self, k: str, v: T) -> None: + self.__data[k] = v + + def __getitem__(self, item: str) -> T: + return self.__data[item] + + def __iter__(self) -> tp.Iterator[str]: + return iter(self.__data) + + def __len__(self) -> int: + return len(self.__data) + def __copy__(self) -> 'DictObject': - return DictObject(copy.copy(dict(self))) + return DictObject(self.__data.copy()) + + def __eq__(self, other: dict): + if isinstance(other, DictObject): + return self.__data == other.__data + else: + return self.__data == other + + def copy(self) -> 'DictObject': + return DictObject(self.__data.copy()) def __deepcopy__(self, memodict={}) -> 'DictObject': - return DictObject(copy.deepcopy(dict(self), memo=memodict)) + return DictObject(copy.deepcopy(self.__data, memo=memodict)) @rethrow_as(KeyError, AttributeError) def __getattr__(self, item: str) -> T: return self[item] def __setattr__(self, key: str, value: T) -> None: - self[key] = value + if key == '_DictObject__data': + return super().__setattr__(key, value) + else: + self[key] = value @rethrow_as(KeyError, AttributeError) def __delattr__(self, key: str) -> None: @@ -63,7 +93,7 @@ class DictObject(dict, tp.Generic[T]): descriptor = descriptor_from_dict(schema) try: - descriptor(self) + descriptor(self.__data) except ConfigurationValidationError: return False else: @@ -123,27 +153,6 @@ class DictionaryView(collections.abc.MutableMapping, tp.Generic[K, V]): self.dictionaries = [master_dict, *rest_of_dicts] self.propagate_deletes = propagate_deletes - @for_argument(returns=list) - def keys(self) -> tp.AbstractSet[K]: - """ - Returns all keys found in this view - """ - seen_already = set() - for dictionary in self.dictionaries: - for key in dictionary: - if key not in seen_already: - yield key - seen_already.add(key) - - @for_argument(returns=list) - def values(self) -> tp.AbstractSet[V]: - seen_already = set() - for dictionary in self.dictionaries: - for key, value in dictionary.items(): - if key not in seen_already: - yield value - seen_already.add(key) - def __contains__(self, item: K) -> bool: for dictionary in self.dictionaries: if item in dictionary: @@ -158,15 +167,6 @@ class DictionaryView(collections.abc.MutableMapping, tp.Generic[K, V]): yield key seen_already.add(key) - @for_argument(returns=list) - def items(self) -> tp.AbstractSet[tp.Tuple[K, V]]: - seen_already = set() - for dictionary in self.dictionaries: - for key, value in dictionary.items(): - if key not in seen_already: - yield key, value - seen_already.add(key) - def __len__(self) -> int: seen_already = set() i = 0 diff --git a/satella/imports.py b/satella/imports.py index f47352c3..a9dc6fef 100644 --- a/satella/imports.py +++ b/satella/imports.py @@ -1,13 +1,11 @@ import importlib -import logging import os import pkgutil +import warnings import typing as tp __all__ = ['import_from', 'import_class'] -logger = logging.getLogger(__name__) - def import_class(path: str) -> type: """ @@ -76,8 +74,8 @@ def import_from(path: tp.List[str], package_prefix: str, all_: tp.List[str], try: package_ref = module.__all__ except AttributeError: - logger.warning('Module %s does not contain __all__, enumerating it instead', - package_prefix + '.' + modname) + warnings.warn('Module %s does not contain __all__, enumerating it instead' % + (package_prefix + '.' + modname, ), RuntimeWarning) package_ref = dir(module) for item in package_ref: diff --git a/tests/test_imports/test_import.py b/tests/test_imports/test_import.py index 684034b4..e4fc2cd5 100644 --- a/tests/test_imports/test_import.py +++ b/tests/test_imports/test_import.py @@ -2,6 +2,7 @@ import logging import unittest from satella.imports import import_class import subprocess +import warnings logger = logging.getLogger(__name__) @@ -9,7 +10,9 @@ logger = logging.getLogger(__name__) class TestImports(unittest.TestCase): def test_imports(self): import tests.test_imports.importa - tests.test_imports.importa.do_import() + with warnings.catch_warnings() as warns: + tests.test_imports.importa.do_import() + self.assertGreater(len(warns), 0) # this as well checks for the namespace's pollution self.assertEqual(set(tests.test_imports.importa.importb.__all__), -- GitLab