Repository: joke2k/django-environ

Issue:
Attach cause to ImproperlyConfigured exception raised for missing environment variables
I'd like to propose to add the original exception as the cause of the `ImproperlyConfigured` exception that's raised when an environment variable is missing using `raise ... from ...`:

```py
        try:
            value = self.ENVIRON[var]
        except KeyError as exc:
            if default is self.NOTSET:
                error_msg = "Set the {} environment variable".format(var)
                raise ImproperlyConfigured(error_msg) from exc
```

The original:

https://github.com/joke2k/django-environ/blob/509cf7737363a56e4464962dbae4ab72b801c7b3/environ/environ.py#L366-L371

Note: Since it's a two-line change, I've opened #361 to introduce this change. I know that the proposal hasn't been discussed yet, but I figured not a lot of work is lost if it's rejected.

## Background
The main difference is that this will mark the original `KeyError` as the explicit cause of the `ImproperlyConfigered`, instead of marking the `ImproperlyConfigured` as another exception that "happened to happen" during the handling of the `KeyError`. (A better explanation is given by Martijn Pieters [here](https://stackoverflow.com/questions/24752395/python-raise-from-usage/24752607#24752607)).


## Differences for end-users
The most notable difference is the output. The proposed change would change the traceback output to:

```python-traceback
Traceback (most recent call last):
  (...)
KeyError: 'missing_key'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  (...)
environ.compat.ImproperlyConfigured: Set the missing_key environment variable
```

instead of 

```python-traceback
Traceback (most recent call last):
  (...)
KeyError: 'missing_key'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  (...)
environ.compat.ImproperlyConfigured: Set the missing_key environment variable
```

Notice that the middle line that indicates the relationship between the two exceptions has changed.

Additionally, the original exception will now be attached to the `ImproperlyConfigured` exception under the `__cause__` attribute instead of the `__context__` attribute.

Comments:

Patch:
diff --git a/environ/environ.py b/environ/environ.py
index acdfb15..ccbca70 100644
--- a/environ/environ.py
+++ b/environ/environ.py
@@ -21,6 +21,7 @@ import warnings
 from urllib.parse import (
     parse_qs,
     ParseResult,
+    unquote,
     unquote_plus,
     urlparse,
     urlunparse,
@@ -61,7 +62,7 @@ def _cast_int(v):
 
 
 def _cast_urlstr(v):
-    return unquote_plus(v) if isinstance(v, str) else v
+    return unquote(v) if isinstance(v, str) else v
 
 
 class NoValue:
@@ -365,10 +366,10 @@ class Env:
 
         try:
             value = self.ENVIRON[var]
-        except KeyError:
+        except KeyError as exc:
             if default is self.NOTSET:
                 error_msg = "Set the {} environment variable".format(var)
-                raise ImproperlyConfigured(error_msg)
+                raise ImproperlyConfigured(error_msg) from exc
 
             value = default
 


Test patch:
diff --git a/tests/test_env.py b/tests/test_env.py
index 59110e3..d154708 100644
--- a/tests/test_env.py
+++ b/tests/test_env.py
@@ -45,6 +45,7 @@ class TestEnv:
         with pytest.raises(ImproperlyConfigured) as excinfo:
             self.env('not_present')
         assert str(excinfo.value) == 'Set the not_present environment variable'
+        assert excinfo.value.__cause__ is not None
 
     def test_contains(self):
         assert 'STR_VAR' in self.env
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 523a72d..f32d7cc 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -7,7 +7,7 @@
 # the LICENSE.txt file that was distributed with this source code.
 
 import pytest
-from environ.environ import _cast
+from environ.environ import _cast, _cast_urlstr
 
 
 @pytest.mark.parametrize(
@@ -20,3 +20,20 @@ def test_cast(literal):
 
     See https://github.com/joke2k/django-environ/issues/200 for details."""
     assert _cast(literal) == literal
+
+@pytest.mark.parametrize(
+    "quoted_url_str,expected_unquoted_str",
+    [
+        ("Le-%7BFsIaYnaQw%7Da2B%2F%5BV8bS+", "Le-{FsIaYnaQw}a2B/[V8bS+"),
+        ("my_test-string+", "my_test-string+"),
+        ("my%20test%20string+", "my test string+")
+    ]
+)
+def test_cast_urlstr(quoted_url_str, expected_unquoted_str):
+    """Make sure that a url str that contains plus sign literals does not get unquoted incorrectly
+    Plus signs should not be converted to spaces, since spaces are encoded with %20 in URIs
+
+    see https://github.com/joke2k/django-environ/issues/357 for details.
+    related to https://github.com/joke2k/django-environ/pull/69"""
+
+    assert _cast_urlstr(quoted_url_str) == expected_unquoted_str