From f6380c66c8d85a15ce3b7f2cea102b8d804c98a8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ma=C5=9Blanka?= <piotr.maslanka@henrietta.com.pl>
Date: Tue, 11 May 2021 13:27:47 +0200
Subject: [PATCH] * added some syntactic sugar for the source dict * now
 unparseable "type" entries will be returned as-is * added
 `ConfigurationMisconfiguredError` , v2.16.2

---
 CHANGELOG.md                                  |  3 ++
 docs/configuration/sources.rst                | 15 ++++++++
 docs/exceptions.rst                           | 36 +++++++++++--------
 satella/__init__.py                           |  2 +-
 satella/configuration/sources/from_dict.py    | 21 +++++++----
 satella/exceptions.py                         |  7 +++-
 .../test_load_source_from_dict.py             |  6 +++-
 7 files changed, 66 insertions(+), 24 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a81c1c0..a9f8f92c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,3 +4,6 @@
 * added `file_contents` schema 
 * added nested configuration sources
 * added `extract_optional`
+* added some syntactic sugar for the source dict
+* now unparseable "type" entries will be returned as-is
+* added `ConfigurationMisconfiguredError`
diff --git a/docs/configuration/sources.rst b/docs/configuration/sources.rst
index 96a0f646..5a310bdd 100644
--- a/docs/configuration/sources.rst
+++ b/docs/configuration/sources.rst
@@ -137,3 +137,18 @@ The result of this execution will be a dictionary:
             "a": 5
         }
     }
+
+If you have only a single argument, you can also do:
+
+.. code-block:: json
+
+    {
+        "type": "DirectorySource",
+        "arg": "/app/config"
+    }
+
+You can put any objects you like as the arguments, note however that if you pass a dictionary, that
+has a key of "type" and it's value is one of recognized sources, an attempt will be made to parse
+it as a child.
+
+Note that in case you pass a dict with a type that is not recognized, a warning will be emitted.
diff --git a/docs/exceptions.rst b/docs/exceptions.rst
index 1710ec68..4a41cd15 100644
--- a/docs/exceptions.rst
+++ b/docs/exceptions.rst
@@ -92,7 +92,7 @@ it's magic
     :members:
 
 ImpossibleError
----------------
+~~~~~~~~~~~~~~~
 
 .. autoclass:: satella.exceptions.ImpossibleError
     :members:
@@ -101,92 +101,98 @@ Note that `ImpossibleError` inherits from `BaseException` instead of the standar
 The thought is, since this is an anomalous exception, it should get to the top of the stack ASAP.
 
 Satella-specific exceptions
-===========================
+---------------------------
 
 BaseSatellaError
-----------------
+~~~~~~~~~~~~~~~~
 
 .. autoclass:: satella.exceptions.BaseSatellaError
     :members:
 
 
 ResourceLockingError
---------------------
+~~~~~~~~~~~~~~~~~~~~
 
 .. autoclass:: satella.exceptions.ResourceLockingError
     :members:
 
 
 ResourceLocked
---------------
+~~~~~~~~~~~~~~
 
 .. autoclass:: satella.exceptions.ResourceLocked
     :members:
 
 
 ResourceNotLocked
------------------
+~~~~~~~~~~~~~~~~~
 
 .. autoclass:: satella.exceptions.ResourceNotLocked
     :members:
 
 
 WouldWaitMore
--------------
+~~~~~~~~~~~~~
 
 .. autoclass:: satella.exceptions.WouldWaitMore
     :members:
 
 
 PreconditionError
------------------
+~~~~~~~~~~~~~~~~~
 
 .. autoclass:: satella.exceptions.PreconditionError
     :members:
 
 
 ConfigurationError
-------------------
+~~~~~~~~~~~~~~~~~~
 
 .. autoclass:: satella.exceptions.ConfigurationError
     :members:
 
 
 ConfigurationSchemaError
-------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~
 
 .. autoclass:: satella.exceptions.ConfigurationSchemaError
     :members:
 
 
+ConfigurationMisconfiguredError
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. autoclass:: satella.exceptions.ConfigurationMisconfiguredError
+    :members:
+
 ConfigurationValidationError
-----------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 .. autoclass:: satella.exceptions.ConfigurationValidationError
     :members:
 
 Empty
------
+~~~~~
 
 .. autoclass:: satella.exceptions.empty
     :members:
 
 MetricAlreadyExists
--------------------
+~~~~~~~~~~~~~~~~~~~
 
 .. autoclass:: satella.exceptions.MetricAlreadyExists
     :members:
 
 
 AlreadyAllocated
-----------------
+~~~~~~~~~~~~~~~~
 
 .. autoclass:: satella.exceptions.AlreadyAllocated
     :members:
 
 
 ProcessFailed
--------------
+~~~~~~~~~~~~~
 
 .. autoclass:: satella.exceptions.ProcessFailed
     :members:
diff --git a/satella/__init__.py b/satella/__init__.py
index 413aa79f..b34264c0 100644
--- a/satella/__init__.py
+++ b/satella/__init__.py
@@ -1 +1 @@
-__version__ = '2.16.2a6'
+__version__ = '2.16.2'
diff --git a/satella/configuration/sources/from_dict.py b/satella/configuration/sources/from_dict.py
index ee563941..ecfe8cf7 100644
--- a/satella/configuration/sources/from_dict.py
+++ b/satella/configuration/sources/from_dict.py
@@ -1,11 +1,11 @@
 import copy
 import importlib
+import warnings
 
 from satella.coding.recast_exceptions import rethrow_as
 from satella.configuration import sources
 from satella.configuration.sources.base import BaseSource
-from satella.configuration.sources.object_from import BuildObjectFrom
-from satella.exceptions import ConfigurationError
+from satella.exceptions import ConfigurationError, ConfigurationMisconfiguredError
 
 __all__ = [
     'load_source_from_dict',
@@ -46,7 +46,11 @@ def load_source_from_dict(dct: dict) -> BaseSource:
     """
     dct = copy.copy(dct)
     type_ = dct.pop('type')  # type: str
-    args = dct.pop('args', [])  # type: tp.List
+    if 'arg' in dct:
+        args = dct.pop('arg'),
+    else:
+        args = dct.pop('args', [])  # type: tp.List
+
     optional = dct.pop('optional', False)  # type: bool
 
     def to_arg(arg):
@@ -57,15 +61,20 @@ def load_source_from_dict(dct: dict) -> BaseSource:
             elif a_type in sources.__dict__:
                 return load_source_from_dict(arg)
             else:
-                raise ValueError(
-                    'unrecognized argument type %s' % (arg['type'],))
+                warnings.warn(
+                    'Caught %s attempting to parse a dict with type, returning original value' % (
+                        e,), UserWarning)
+                return arg
         else:
             return arg
 
     args = map(to_arg, args)
     kwargs = {k: to_arg(v) for k, v in dct.items()}
 
-    s = sources.__dict__[type_](*args, **kwargs)
+    try:
+        s = sources.__dict__[type_](*args, **kwargs)
+    except KeyError as e:
+        raise ConfigurationMisconfiguredError('unknown type %s' % (type_, ))
 
     if optional:
         s = sources.OptionalSource(s)
diff --git a/satella/exceptions.py b/satella/exceptions.py
index 0bea7338..49f50e6b 100644
--- a/satella/exceptions.py
+++ b/satella/exceptions.py
@@ -6,7 +6,8 @@ __all__ = ['BaseSatellaError', 'ResourceLockingError', 'ResourceNotLocked', 'Res
            'ConfigurationValidationError', 'ConfigurationError', 'ConfigurationSchemaError',
            'PreconditionError', 'MetricAlreadyExists', 'BaseSatellaException', 'CustomException',
            'CodedCustomException', 'CodedCustomExceptionMetaclass', 'WouldWaitMore',
-           'ProcessFailed', 'AlreadyAllocated', 'Empty', 'ImpossibleError']
+           'ProcessFailed', 'AlreadyAllocated', 'Empty', 'ImpossibleError',
+           'ConfigurationMisconfiguredError']
 
 
 class CustomException(Exception):
@@ -181,6 +182,10 @@ class ConfigurationSchemaError(ConfigurationError):
     """Schema mismatch to what was seen"""
 
 
+class ConfigurationMisconfiguredError(ConfigurationError):
+    """Configuration was improperly passed to Satella"""
+
+
 class ConfigurationValidationError(ConfigurationSchemaError):
     """A validator failed"""
 
diff --git a/tests/test_configuration/test_load_source_from_dict.py b/tests/test_configuration/test_load_source_from_dict.py
index 35a371ad..d5083509 100644
--- a/tests/test_configuration/test_load_source_from_dict.py
+++ b/tests/test_configuration/test_load_source_from_dict.py
@@ -17,6 +17,10 @@ INNER_DATA = [
             ]
         }
     },
+    {
+        'type': 'JSONSource',
+        'arg': '{"d": 10}'
+    },
     {
         'type': 'JSONSource',
         'args': ['{"a": 5}']
@@ -70,7 +74,7 @@ DICT_DATA = {
 
 class TestLoadSourceFromDict(SourceTestCase):
     def test_lsf(self):
-        output = {'a': 5, 'b': 5, 'c': 21, 'test': {'a': 5}}
+        output = {'a': 5, 'b': 5, 'c': 21, 'test': {'a': 5}, 'd': 10}
         self.assertSourceHas(load_source_from_dict(DICT_DATA), output)
         self.assertSourceHas(load_source_from_list(INNER_DATA), output)
 
-- 
GitLab