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