Repository: joke2k/django-environ
Pull request #394 (2022-06-15 09:51:00)

Release v0.9.0


Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index fe71189..c2769ef 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -27,17 +27,17 @@ jobs:
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v2.3.5
+        uses: actions/checkout@v3.0.2
 
-      - name: Set up Python 3.9
-        uses: actions/setup-python@v2.2.2
+      - name: Set up Python 3.10
+        uses: actions/setup-python@v4.0.0
         with:
-          python-version: '3.9'
+          python-version: '3.10'
 
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          pip --use-feature=in-tree-build install tox tox-gh-actions
+          pip install tox tox-gh-actions
 
       - name: Check MANIFEST.in for completeness
         run: tox -e manifest
@@ -47,7 +47,7 @@ jobs:
 
       - name: Archive build artifacts
         if: success()
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
           # To ensure that jobs don't overwrite existing artifacts,
           # use a different name per job.
@@ -67,12 +67,12 @@ jobs:
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v2.3.5
+        uses: actions/checkout@v3.0.2
 
-      - name: Set up Python 3.9
-        uses: actions/setup-python@v2.2.2
+      - name: Set up Python 3.10
+        uses: actions/setup-python@v4.0.0
         with:
-          python-version: '3.9'
+          python-version: '3.10'
 
       - name: Install in dev mode
         run: python -m pip install -e .
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c836edc..7c5d59f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -51,18 +51,18 @@ jobs:
           - '3.7'
           - '3.8'
           - '3.9'
-          - '3.10.0-beta - 3.10'
+          - '3.10'
           - 'pypy-3.7'
         os: [ ubuntu-latest, macos-latest, windows-latest ]
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v2.3.5
+        uses: actions/checkout@v3.0.2
         with:
           fetch-depth: 5
 
       - name: Set up Python ${{ matrix.python }}
-        uses: actions/setup-python@v2.2.2
+        uses: actions/setup-python@v4.0.0
         with:
           python-version: ${{ matrix.python }}
 
diff --git a/.github/workflows/cs.yml b/.github/workflows/cs.yml
index e4283d1..bf4e81f 100644
--- a/.github/workflows/cs.yml
+++ b/.github/workflows/cs.yml
@@ -21,12 +21,12 @@ jobs:
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v2.3.5
+        uses: actions/checkout@v3.0.2
 
-      - name: Set up Python 3.9
-        uses: actions/setup-python@v2.2.2
+      - name: Set up Python 3.10
+        uses: actions/setup-python@v4.0.0
         with:
-          python-version: '3.9'
+          python-version: '3.10'
 
       - name: Install dependencies
         run: |
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 5c68593..f89231e 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -21,12 +21,12 @@ jobs:
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v2.3.5
+        uses: actions/checkout@v3.0.2
 
-      - name: Set up Python 3.8
-        uses: actions/setup-python@v2.2.2
+      - name: Set up Python 3.10
+        uses: actions/setup-python@v4.0.0
         with:
-          python-version: '3.8'
+          python-version: '3.10'
 
       - name: Install dependencies
         run: |
@@ -41,7 +41,7 @@ jobs:
 
       - name: Archive docs artifacts
         if: always()
-        uses: actions/upload-artifact@v2
+        uses: actions/upload-artifact@v3
         with:
           name: docs
           path: docs
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 78a2022..27f9368 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -6,13 +6,20 @@
 # For the full copyright and license information, please view
 # the LICENSE.txt file that was distributed with this source code.
 
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
 ---
 version: 2
-python:
-  # Keep version in sync with tox.ini (testenv:docs) and
-  # docs.yml (GitHub Action Workflow).
-  version: '3.8'
 
+build:
+  os: ubuntu-20.04
+  tools:
+    # Keep version in sync with tox.ini (testenv:docs) and
+    # docs.yml (GitHub Action Workflow).
+    python: '3.10'
+
+python:
   install:
     - method: pip
       path: .
diff --git a/AUTHORS.rst b/AUTHORS.rst
index 6bf3982..0a910fa 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -13,7 +13,7 @@ The existence of ``django-environ`` would have been impossible without these
 projects:
 
 - `rconradharris/envparse <https://github.com/rconradharris/envparse>`_
-- `jacobian/dj-database-url <https://github.com/jacobian/dj-database-url>`_
+- `jazzband/dj-database-url <https://github.com/jazzband/dj-database-url>`_
 - `migonzalvar/dj-email-url <https://github.com/migonzalvar/dj-email-url>`_
 - `ghickman/django-cache-url <https://github.com/ghickman/django-cache-url>`_
 - `dstufft/dj-search-url <https://github.com/dstufft/dj-search-url>`_
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index dd34e76..2c3e17d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -5,8 +5,44 @@ All notable changes to this project will be documented in this file.
 The format is inspired by `Keep a Changelog <https://keepachangelog.com/en/1.0.0/>`_
 and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0.html>`_.
 
-`v0.8.1`_ - 20-October-2021
+`v0.9.0`_ - 15-June-2022
+------------------------------
+Added
++++++
+- Added support for Postgresql cluster URI
+  `#355 <https://github.com/joke2k/django-environ/pull/355>`_.
+- Added support for Django 4.0
+  `#371 <https://github.com/joke2k/django-environ/issues/371>`_.
+- Added support for prefixed variables
+  `#362 <https://github.com/joke2k/django-environ/issues/362>`_.
+- Amended documentation.
+
+
+Deprecated
+++++++++++
+- ``Env.unicode()`` is deprecated and will be removed in the next
+  major release. Use ``Env.str()`` instead.
+
+
+Changed
++++++++
+- Attach cause to ``ImproperlyConfigured`` exception
+  `#360 <https://github.com/joke2k/django-environ/issues/360>`_.
+
 
+Fixed
++++++
+- Fixed ``_cast_urlstr`` unquoting
+  `#357 <https://github.com/joke2k/django-environ/issues/357>`_.
+- Fixed documentation regarding unsafe characters in URLs
+  `#220 <https://github.com/joke2k/django-environ/issues/220>`_.
+- Fixed ``environ.Path.__eq__()`` to compare paths correctly
+  `#86 <https://github.com/joke2k/django-environ/issues/86>`_,
+  `#197 <https://github.com/joke2k/django-environ/issues/197>`_.
+
+
+`v0.8.1`_ - 20-October-2021
+---------------------------
 Fixed
 +++++
 - Fixed "Invalid line" spam logs on blank lines in env file
@@ -16,7 +52,7 @@ Fixed
 
 
 `v0.8.0`_ - 17-October-2021
-------------------------------
+---------------------------
 Added
 +++++
 - Log invalid lines when parse .env file
@@ -108,7 +144,7 @@ Added
 - Support for Django 2.1 & 2.2.
 - Added tox.ini targets.
 - Added secure redis backend URLs via ``rediss://``.
-- Add ``cast=str`` to ``str()`` method.
+- Added ``cast=str`` to ``str()`` method.
 
 Fixed
 +++++
@@ -149,7 +185,7 @@ Added
 - Support for Elasticsearch2.
 - Support for Mysql-connector.
 - Support for ``pyodbc``.
-- Add ``__contains__`` feature to Environ class.
+- Added ``__contains__`` feature to Environ class.
 
 Fixed
 +++++
@@ -171,7 +207,7 @@ Added
 
 Changed
 +++++++
-- Fix uwsgi settings reload problem
+- Fixed uwsgi settings reload problem
   `#55 <https://github.com/joke2k/django-environ/issues/55>`_.
 - Update support for ``django-redis`` urls
   `#109 <https://github.com/joke2k/django-environ/pull/109>`_.
@@ -184,26 +220,26 @@ Added
 
 Changed
 +++++++
-- Fix for unsafe characters into URLs.
+- Fixed for unsafe characters into URLs.
 - Clarifying warning on missing or unreadable file.
   Thanks to `@nickcatal <https://github.com/nickcatal>`_.
-- Fix support for Oracle urls.
-- Fix support for ``django-redis``.
+- Fixed support for Oracle urls.
+- Fixed support for ``django-redis``.
 
 `v0.4`_ - 23-September-2015
 ---------------------------
 Added
 +++++
 - New email schemes - ``smtp+ssl`` and ``smtp+tls`` (``smtps`` would be deprecated).
-- Add tuple support. Thanks to `@anonymouzz <https://github.com/anonymouzz>`_.
-- Add LDAP url support for database. Thanks to
+- Added tuple support. Thanks to `@anonymouzz <https://github.com/anonymouzz>`_.
+- Added LDAP url support for database. Thanks to
   `django-ldapdb/django-ldapdb <https://github.com/django-ldapdb/django-ldapdb>`_.
 
 Changed
 +++++++
-- Fix non-ascii values (broken in Python 2.x).
+- Fixed non-ascii values (broken in Python 2.x).
 - ``redis_cache`` replaced by ``django_redis``.
-- Fix psql/pgsql url.
+- Fixed psql/pgsql url.
 
 
 `v0.3.1`_ - 19 Sep 2015
@@ -225,9 +261,9 @@ Fixed
 ----------------------
 Added
 +++++
-- Add cache url support.
-- Add email url support.
-- Add search url support.
+- Added cache url support.
+- Added email url support.
+- Added search url support.
 
 Changed
 +++++++
@@ -243,7 +279,7 @@ v0.2 - 16-April-2013
 --------------------
 Added
 +++++
-- Add advanced float parsing (comma and dot symbols to separate thousands and decimals).
+- Added advanced float parsing (comma and dot symbols to separate thousands and decimals).
 
 Fixed
 +++++
@@ -256,6 +292,7 @@ Added
 - Initial release.
 
 
+.. _v0.9.0: https://github.com/joke2k/django-environ/compare/v0.8.1...v0.9.0
 .. _v0.8.1: https://github.com/joke2k/django-environ/compare/v0.8.0...v0.8.1
 .. _v0.8.0: https://github.com/joke2k/django-environ/compare/v0.7.0...v0.8.0
 .. _v0.7.0: https://github.com/joke2k/django-environ/compare/v0.6.0...v0.7.0
diff --git a/README.rst b/README.rst
index 99a52ef..256f0a1 100644
--- a/README.rst
+++ b/README.rst
@@ -104,8 +104,8 @@ article.
 
 Using ``django-environ`` you can stop to make a lot of unversioned
 ``settings_*.py`` to configure your app.
-See `cookiecutter-django <https://github.com/pydanny/cookiecutter-django>`_ for
-a concrete example on using with a django project.
+See `cookiecutter-django <https://github.com/cookiecutter/cookiecutter-django>`_
+for a concrete example on using with a django project.
 
 **Feature Support**
 
@@ -127,7 +127,7 @@ the code on `GitHub <https://github.com/joke2k/django-environ>`_,
 and the latest release on `PyPI <https://pypi.org/project/django-environ/>`_.
 
 It’s rigorously tested on Python 3.5+, and officially supports
-Django 1.11, 2.2, 3.0, 3.1 and 3.2.
+Django 1.11, 2.2, 3.0, 3.1, 3.2 and 4.0.
 
 If you'd like to contribute to ``django-environ`` you're most welcome!
 
diff --git a/docs/Makefile b/docs/Makefile
index 2b50c27..887c0bb 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/docs/api.rst b/docs/api.rst
new file mode 100644
index 0000000..e1556ab
--- /dev/null
+++ b/docs/api.rst
@@ -0,0 +1,46 @@
+=============
+API Reference
+=============
+
+.. currentmodule:: environ
+
+
+The ``__init__`` module
+=======================
+
+.. automodule:: environ
+    :members:
+    :special-members:
+    :no-undoc-members:
+
+
+The ``compat`` module
+======================
+
+.. automodule:: environ.compat
+    :members:
+    :no-undoc-members:
+
+
+The ``environ`` module
+======================
+
+.. autoclass:: environ.Env
+    :members:
+    :no-undoc-members:
+
+.. autoclass:: environ.FileAwareEnv
+    :members:
+    :no-undoc-members:
+
+.. autoclass:: environ.Path
+    :members:
+    :no-undoc-members:
+
+
+The ``fileaware_mapping`` module
+================================
+
+.. autoclass:: environ.fileaware_mapping.FileAwareMapping
+    :members:
+    :no-undoc-members:
diff --git a/docs/conf.py b/docs/conf.py
index 1753614..9003ad8 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,19 +1,26 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
 # the LICENSE.txt file that was distributed with this source code.
 
 #
-# -- Utils -----------------------------------------------------
+# -- Utils ---------------------------------------------------------
 #
 
 import codecs
 import os
+import sys
 import re
 
+from datetime import date
+
+
+PROJECT_DIR = os.path.abspath('..')
+sys.path.insert(0, PROJECT_DIR)
+
 
 def read_file(filepath):
     """Read content from a UTF-8 encoded text file."""
@@ -23,9 +30,7 @@ def read_file(filepath):
 
 def find_version(meta_file):
     """Extract ``__version__`` from meta_file."""
-    here = os.path.abspath(os.path.dirname(__file__))
-    contents = read_file(os.path.join(here, meta_file))
-
+    contents = read_file(os.path.join(PROJECT_DIR, meta_file))
     meta_match = re.search(
         r"^__version__\s+=\s+['\"]([^'\"]*)['\"]",
         contents,
@@ -35,7 +40,7 @@ def find_version(meta_file):
     if meta_match:
         return meta_match.group(1)
     raise RuntimeError(
-        'Unable to find __version__ string in package meta file')
+        "Unable to find __version__ string in package meta file")
 
 
 #
@@ -43,9 +48,9 @@ def find_version(meta_file):
 #
 
 # General information about the project.
-project = 'django-environ'
-copyright = '2013-2021, Daniele Faraglia and other contributors'
-author = u"Daniele Faraglia"
+project = "django-environ"
+copyright = f'2013-{date.today().year}, Daniele Faraglia and other contributors'
+author = u"Daniele Faraglia \\and Serghei Iakovlev"
 
 #
 # -- General configuration ---------------------------------------------------
@@ -56,6 +61,7 @@ extensions = [
     "sphinx.ext.doctest",
     "sphinx.ext.intersphinx",
     "sphinx.ext.todo",
+    "sphinx.ext.viewcode",
     "notfound.extension",
 ]
 
@@ -75,7 +81,7 @@ master_doc = "index"
 
 # The version info
 # The short X.Y version.
-release = find_version('../environ/__init__.py')
+release = find_version(os.path.join("environ", "__init__.py"))
 version = release.rsplit(u".", 1)[0]
 # The full version, including alpha/beta/rc tags.
 
@@ -90,9 +96,42 @@ default_role = "any"
 # If true, '()' will be appended to :func: etc. cross-reference text.
 add_function_parentheses = True
 
+#
+# -- Options for autodoc ---------------------------------------------------
+#
+
+# This value selects if automatically documented members are sorted alphabetical
+# (value 'alphabetical'), by member type (value 'groupwise') or by source order
+# (value 'bysource'). The default is alphabetical.
+#
+# Note that for source order, the module must be a Python module with the
+# source code available.
+autodoc_member_order = 'bysource'
+
+#
+# -- Options for linkcheck ---------------------------------------------------
+#
+
+linkcheck_ignore = [
+    # We run into GitHub's rate limits.
+    r"https://github.com/.*/(issues|pull)/\d+",
+    # Do not check links to compare tags.
+    r"https://github.com/joke2k/django-environ/compare/.*",
+]
+
+#
+# -- Options for nitpick -----------------------------------------------------
+#
+
+# In nitpick mode (-n), still ignore any of the following "broken" references
+# to non-types.
+nitpick_ignore = [
+]
+
 #
 # -- Options for extlinks ----------------------------------------------------
 #
+
 extlinks = {
     "pypi": ("https://pypi.org/project/%s/", ""),
 }
@@ -100,6 +139,7 @@ extlinks = {
 #
 # -- Options for intersphinx -------------------------------------------------
 #
+
 intersphinx_mapping = {
     "python": ("https://docs.python.org/3", None),
     "sphinx": ("https://www.sphinx-doc.org/en/master", None),
@@ -108,14 +148,15 @@ intersphinx_mapping = {
 #
 # -- Options for TODOs -------------------------------------------------------
 #
+
 todo_include_todos = True
 
-# -- Options for HTML output ----------------------------------------------
+# -- Options for HTML output -------------------------------------------------
 
 # html_favicon = None
 
 html_theme = "furo"
-html_title = "django-environ"
+html_title = project
 
 html_theme_options = {}
 
@@ -151,7 +192,7 @@ html_show_copyright = True
 htmlhelp_basename = "django-environ-doc"
 
 #
-# -- Options for manual page output ---------------------------------------
+# -- Options for manual page output ------------------------------------------
 #
 
 # One entry per manual page. List of tuples
@@ -161,7 +202,7 @@ man_pages = [
 ]
 
 #
-# -- Options for Texinfo output -------------------------------------------
+# -- Options for Texinfo output ----------------------------------------------
 #
 
 # Grouping the document tree into Texinfo files. List of tuples
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
new file mode 100644
index 0000000..7318911
--- /dev/null
+++ b/docs/deprecations.rst
@@ -0,0 +1,12 @@
+============
+Deprecations
+============
+
+Features deprecated in 0.9.0
+============================
+
+Methods
+-------
+
+* The :meth:`.environ.Env.unicode` method is deprecated as it was used
+  for Python 2.x only. Use :meth:`.environ.Env.str` instead.
diff --git a/docs/docutils.conf b/docs/docutils.conf
index 841ea73..4ed0bf5 100644
--- a/docs/docutils.conf
+++ b/docs/docutils.conf
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/docs/faq.rst b/docs/faq.rst
new file mode 100644
index 0000000..9e23ef6
--- /dev/null
+++ b/docs/faq.rst
@@ -0,0 +1,31 @@
+===
+FAQ
+===
+
+
+#. **Can django-environ determine the location of .env file automatically?**
+
+   ``django-environ`` will try to get and read ``.env`` file from the project
+   root if you haven't specified the path for it when call :meth:`.environ.Env.read_env`.
+   However, this is not the recommended way. When it is possible always specify
+   the path tho ``.env`` file. Alternatively, you can use a trick with a
+   environment variable pointing to the actual location of ``.env`` file.
+   For details see ":ref:`multiple-env-files-label`".
+
+#. **What (where) is the root part of the project, is it part of the project where are settings?**
+
+   Where your ``manage.py`` file is (that is your project root directory).
+
+#. **What kind of file should .env be?**
+
+   ``.env`` is a plain text file.
+
+#. **Should name of the file be simply .env (or something.env)?**
+
+   Just ``.env``. However, this is not a strict rule, but just a common
+   practice. Formally, you can use any filename.
+
+#. **Is .env file going to be imported in settings file?**
+
+   No need to import, ``django-environ`` automatically picks variables
+   from there.
diff --git a/docs/getting-started.rst b/docs/getting-started.rst
deleted file mode 100644
index 007e92f..0000000
--- a/docs/getting-started.rst
+++ /dev/null
@@ -1,137 +0,0 @@
-===============
-Getting Started
-===============
-
-Installation
-============
-
-
-Requirements
-------------
-
-* `Django <https://www.djangoproject.com/>`_ >= 1.11
-* `Python <https://www.python.org/>`_ >= 3.4
-
-Installing django-environ
-_________________________
-
-``django-environ`` is a Python-only package `hosted on PyPI <https://pypi.org/project/django-environ/>`_.
-The recommended installation method is `pip <https://pip.pypa.io/en/stable/>`_-installing into a virtualenv:
-
-.. code-block:: console
-
-   $ python -m pip install django-environ
-
-.. note::
-
-   After installing ``django-environ``, no need to add it to ``INSTALLED_APPS``.
-
-Unstable version
-________________
-
-The master of all the material is the Git repository at https://github.com/joke2k/django-environ.
-So, you can also install the latest unreleased development version directly from the
-``develop`` branch on GitHub. It is a work-in-progress of a future stable release so the
-experience might be not as smooth.:
-
-.. code-block:: console
-
-   $ pip install -e git://github.com/joke2k/django-environ.git#egg=django-environ
-   # OR
-   $ pip install --upgrade https://github.com/joke2k/django-environ.git/archive/develop.tar.gz
-
-This command will download the latest version of ``django-environ`` and install
-it to your system.
-
-.. note::
-
-   The ``develop`` branch will always contain the latest unstable version, so the experience
-   might be not as smooth. If you wish to check older versions or formal, tagged release,
-   please switch to the relevant `tag <https://github.com/joke2k/django-environ/tags>`_.
-
-More information about ``pip`` and PyPI can be found here:
-
-* `Install pip <https://pip.pypa.io/en/latest/installing/>`_
-* `Python Packaging User Guide <https://packaging.python.org/>`_
-
-Usage
-=====
-
-Create a ``.env`` file in project root directory. The file format can be understood
-from the example below:
-
-.. code-block:: shell
-
-   DEBUG=on
-   SECRET_KEY=your-secret-key
-   DATABASE_URL=psql://user:un-githubbedpassword@127.0.0.1:8458/database
-   SQLITE_URL=sqlite:///my-local-sqlite.db
-   CACHE_URL=memcache://127.0.0.1:11211,127.0.0.1:11212,127.0.0.1:11213
-   REDIS_URL=rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient&password=ungithubbed-secret
-
-And use it with ``settings.py`` as follows:
-
-.. include:: ../README.rst
-   :start-after: -code-begin-
-   :end-before: -overview-
-
-The ``.env`` file should be specific to the environment and not checked into
-version control, it is best practice documenting the ``.env`` file with an example.
-For example, you can also add ``.env.dist`` with a template of your variables to
-the project repo. This file should describe the mandatory variables for the
-Django application, and it can be committed to version control.  This provides a
-useful reference and speeds up the on-boarding process for new team members, since
-the time to dig through the codebase to find out what has to be set up is reduced.
-
-A good ``.env.dist`` could look like this:
-
-.. code-block:: shell
-
-   # SECURITY WARNING: don't run with the debug turned on in production!
-   DEBUG=True
-
-   # Should robots.txt allow everything to be crawled?
-   ALLOW_ROBOTS=False
-
-   # SECURITY WARNING: keep the secret key used in production secret!
-   SECRET_KEY=secret
-
-   # A list of all the people who get code error notifications.
-   ADMINS="John Doe <john@example.com>, Mary <mary@example.com>"
-
-   # A list of all the people who should get broken link notifications.
-   MANAGERS="Blake <blake@cyb.org>, Alice Judge <alice@cyb.org>"
-
-   # By default, Django will send system email from root@localhost.
-   # However, some mail providers reject all email from this address.
-   SERVER_EMAIL=webmaster@example.com
-
-FAQ
-===
-
-#. **Can django-environ determine the location of .env file automatically?**
-
-   ``django-environ`` will try to get and read ``.env`` file from the project
-   root if you haven't specified the path for it when call ``read_env``.
-   However, this is not the recommended way. When it is possible always specify
-   the path tho ``.env`` file. Alternatively, you can use a trick with a
-   environment variable pointing to the actual location of .env file.
-   For details see ":ref:`multiple-env-files-label`".
-
-#. **What (where) is the root part of the project, is it part of the project where are settings?**
-
-   Where your ``manage.py`` file is (that is your project root directory).
-
-#. **What kind of file should .env be?**
-
-   ``.env`` is a plain text file.
-
-#. **Should name of the file be simply .env (or something.env)?**
-
-   Just ``.env``. However, this is not a strict rule, but just a common
-   practice. Formally, you can use any filename.
-
-#. **Is .env file going to be imported in settings file?**
-
-   No need to import, ``django-environ`` automatically picks variables
-   from there.
diff --git a/docs/index.rst b/docs/index.rst
index 08aaf75..0142a3b 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -15,36 +15,73 @@ Overview
    :start-after: -overview-
    :end-before: -project-information-
 
-Quick Start
-===========
-
-.. include:: ../README.rst
-   :start-after: -code-begin-
-   :end-before: -overview-
-
-.. include:: ../README.rst
-   :start-after: -support-
-
 ----
 
 Full Table of Contents
 ======================
 
+The User Guide
+--------------
+
+This part of the documentation, which is mostly prose, begins with some
+background information about django-environ, then focuses on step-by-step
+instructions for getting the most out of django-environ.
+
+.. toctree::
+   :maxdepth: 2
+
+   install
+   quickstart
+
+
+The Community Guide
+-------------------
+
+This part of the documentation, which is mostly prose, details the
+django-environ ecosystem and community.
+
 .. toctree::
    :maxdepth: 2
 
-   getting-started
+   faq
    types
    tips
+
+.. toctree::
+   :maxdepth: 1
+
+   deprecations
+   changelog
+
+
+The API Documentation / Guide
+-----------------------------
+
+If you are looking for information on a specific function, class, or method,
+this part of the documentation is for you.
+
+.. toctree::
+   :maxdepth: 2
+
+   api
+
+
+The Contributor Guide
+---------------------
+
+If you want to contribute to the project, this part of the documentation is for
+you.
+
+.. toctree::
+   :maxdepth: 3
+
    contributing
    backers
+   license
+
+.. include:: ../README.rst
+   :start-after: -support-
 
 .. include:: ../README.rst
    :start-after: -project-information-
    :end-before: -support-
-
-.. toctree::
-   :maxdepth: 1
-
-   license
-   changelog
diff --git a/docs/install.rst b/docs/install.rst
new file mode 100644
index 0000000..a70ca1a
--- /dev/null
+++ b/docs/install.rst
@@ -0,0 +1,58 @@
+============
+Installation
+============
+
+
+Requirements
+============
+
+* `Django <https://www.djangoproject.com/>`_ >= 1.11
+* `Python <https://www.python.org/>`_ >= 3.5
+
+Installing django-environ
+=========================
+
+``django-environ`` is a Python-only package `hosted_on_pypi`_.
+The recommended installation method is `pip`_-installing into a
+:mod:`virtualenv <python:venv>`:
+
+.. code-block:: console
+
+   $ python -m pip install django-environ
+
+.. note::
+
+   After installing ``django-environ``, no need to add it to ``INSTALLED_APPS``.
+
+
+.. _hosted_on_pypi: https://pypi.org/project/django-environ/
+.. _pip: https://pip.pypa.io/en/stable/
+
+
+Unstable version
+================
+
+The master of all the material is the Git repository at https://github.com/joke2k/django-environ.
+So, you can also install the latest unreleased development version directly from the
+``develop`` branch on GitHub. It is a work-in-progress of a future stable release so the
+experience might be not as smooth:
+
+.. code-block:: console
+
+   $ pip install -e git://github.com/joke2k/django-environ.git#egg=django-environ
+   # OR
+   $ pip install --upgrade https://github.com/joke2k/django-environ.git/archive/develop.tar.gz
+
+This command will download the latest version of ``django-environ`` and install
+it to your system.
+
+.. note::
+
+   The ``develop`` branch will always contain the latest unstable version, so the experience
+   might be not as smooth. If you wish to check older versions or formal, tagged release,
+   please switch to the relevant `tag <https://github.com/joke2k/django-environ/tags>`_.
+
+More information about ``pip`` and PyPI can be found here:
+
+* `Install pip <https://pip.pypa.io/en/latest/installing/>`_
+* `Python Packaging User Guide <https://packaging.python.org/en/latest/>`_
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
new file mode 100644
index 0000000..f5c5b20
--- /dev/null
+++ b/docs/quickstart.rst
@@ -0,0 +1,59 @@
+===========
+Quick Start
+===========
+
+.. include:: ../README.rst
+   :start-after: -code-begin-
+   :end-before: -overview-
+
+Usage
+=====
+
+Create a ``.env`` file in project root directory. The file format can be understood
+from the example below:
+
+.. code-block:: shell
+
+   DEBUG=on
+   SECRET_KEY=your-secret-key
+   DATABASE_URL=psql://user:un-githubbedpassword@127.0.0.1:8458/database
+   SQLITE_URL=sqlite:///my-local-sqlite.db
+   CACHE_URL=memcache://127.0.0.1:11211,127.0.0.1:11212,127.0.0.1:11213
+   REDIS_URL=rediscache://127.0.0.1:6379/1?client_class=django_redis.client.DefaultClient&password=ungithubbed-secret
+
+And use it with ``settings.py`` as follows:
+
+.. include:: ../README.rst
+   :start-after: -code-begin-
+   :end-before: -overview-
+
+The ``.env`` file should be specific to the environment and not checked into
+version control, it is best practice documenting the ``.env`` file with an example.
+For example, you can also add ``.env.dist`` with a template of your variables to
+the project repo. This file should describe the mandatory variables for the
+Django application, and it can be committed to version control.  This provides a
+useful reference and speeds up the on-boarding process for new team members, since
+the time to dig through the codebase to find out what has to be set up is reduced.
+
+A good ``.env.dist`` could look like this:
+
+.. code-block:: shell
+
+   # SECURITY WARNING: don't run with the debug turned on in production!
+   DEBUG=True
+
+   # Should robots.txt allow everything to be crawled?
+   ALLOW_ROBOTS=False
+
+   # SECURITY WARNING: keep the secret key used in production secret!
+   SECRET_KEY=secret
+
+   # A list of all the people who get code error notifications.
+   ADMINS="John Doe <john@example.com>, Mary <mary@example.com>"
+
+   # A list of all the people who should get broken link notifications.
+   MANAGERS="Blake <blake@cyb.org>, Alice Judge <alice@cyb.org>"
+
+   # By default, Django will send system email from root@localhost.
+   # However, some mail providers reject all email from this address.
+   SERVER_EMAIL=webmaster@example.com
diff --git a/docs/tips.rst b/docs/tips.rst
index dbb762f..a8684eb 100644
--- a/docs/tips.rst
+++ b/docs/tips.rst
@@ -10,7 +10,7 @@ Docker (swarm) and Kubernetes are two widely used platforms that store their
 secrets in tmpfs inside containers as individual files, providing a secure way
 to be able to share configuration data between containers.
 
-Use ``environ.FileAwareEnv`` rather than ``environ.Env`` to first look for
+Use :class:`.environ.FileAwareEnv` rather than :class:`.environ.Env` to first look for
 environment variables with ``_FILE`` appended. If found, their contents will be
 read from the file system and used instead.
 
@@ -42,7 +42,8 @@ the example ``docker-compose.yml`` for would contain:
 Using unsafe characters in URLs
 ===============================
 
-In order to use unsafe characters you have to encode with ``urllib.parse.encode`` before you set into ``.env`` file.
+In order to use unsafe characters you have to encode with :py:func:`urllib.parse.quote`
+before you set into ``.env`` file. Encode only the value (i.e. the password) not the whole url.
 
 .. code-block:: shell
 
@@ -76,7 +77,7 @@ For redis cache, multiple master/slave or shard locations can be configured as f
 Email settings
 ==============
 
-In order to set email configuration for django you can use this code:
+In order to set email configuration for Django you can use this code:
 
 .. code-block:: python
 
@@ -125,6 +126,50 @@ You can use something like this to handle similar cases.
    ADMINS = tuple(parseaddr(email) for email in env.list('DJANGO_ADMINS'))
 
 
+.. _complex_dict_format:
+
+Complex dict format
+===================
+
+Sometimes we need to get a bit more complex dict type than usual. For example,
+consider Djangosaml2's ``SAML_ATTRIBUTE_MAPPING``:
+
+.. code-block:: python
+
+   SAML_ATTRIBUTE_MAPPING = {
+       'uid': ('username', ),
+       'mail': ('email', ),
+       'cn': ('first_name', ),
+       'sn': ('last_name', ),
+   }
+
+A dict of this format can be obtained as shown below:
+
+**.env file**:
+
+.. code-block:: shell
+
+   # .env file contents
+   SAML_ATTRIBUTE_MAPPING="uid=username;mail=email;cn=first_name;sn=last_name;"
+
+**settings.py file**:
+
+.. code-block:: python
+
+   # settings.py file contents
+   import environ
+
+
+   env = environ.Env()
+
+   # {'uid': ('username',), 'mail': ('email',), 'cn': ('first_name',), 'sn': ('last_name',)}
+   SAML_ATTRIBUTE_MAPPING = env.dict(
+       'SAML_ATTRIBUTE_MAPPING',
+       cast={'value': tuple},
+       default={}
+   )
+
+
 Multiline value
 ===============
 
@@ -199,7 +244,7 @@ Escape Proxy
 ============
 
 If you're having trouble with values starting with dollar sign ($) without the intention of proxying the value to
-another, You should enbale the ``escape_proxy`` and prepend a backslash to it.
+another, You should enable the ``escape_proxy`` and prepend a backslash to it.
 
 .. code-block:: python
 
@@ -249,7 +294,8 @@ while ``./manage.py runserver`` uses ``.env``.
 Using Path objects when reading env
 -----------------------------------
 
-It is possible to use of ``pathlib.Path`` objects when reading environment file from the filesystem:
+It is possible to use of :py:class:`pathlib.Path` objects when reading environment
+file from the filesystem:
 
 .. code-block:: python
 
@@ -274,11 +320,40 @@ It is possible to use of ``pathlib.Path`` objects when reading environment file
 Overwriting existing environment values from env files
 ------------------------------------------------------
 
-If you want variables set within your env files to take higher precidence than
+If you want variables set within your env files to take higher precedence than
 an existing set environment variable, use the ``overwrite=True`` argument of
-``read_env``. For example:
+:meth:`.environ.Env.read_env`. For example:
 
 .. code-block:: python
 
    env = environ.Env()
    env.read_env(BASE_DIR('.env'), overwrite=True)
+
+
+Handling prefixes
+=================
+
+Sometimes it is desirable to be able to prefix all environment variables. For
+example, if you are using Django, you may want to prefix all environment
+variables with ``DJANGO_``. This can be done by setting the ``prefix``
+to desired prefix. For example:
+
+**.env file**:
+
+.. code-block:: shell
+
+   # .env file contents
+   DJANGO_TEST="foo"
+
+**settings.py file**:
+
+.. code-block:: python
+
+   # settings.py file contents
+   import environ
+
+
+   env = environ.Env()
+   env.prefix = 'DJANGO_'
+
+   env.str('TEST')  # foo
diff --git a/docs/types.rst b/docs/types.rst
index 292ed64..3fcdcbb 100644
--- a/docs/types.rst
+++ b/docs/types.rst
@@ -2,61 +2,181 @@
 Supported types
 ===============
 
-* ``str``
-* ``bool``
-* ``int``
-* ``float``
-* ``json``
-* ``list``: ``(FOO=a,b,c)``
-* ``tuple``: ``(FOO=(a,b,c))``
-* ``dict``: ``(BAR=key=val,foo=bar)``, ``environ.Env(BAR=(dict, {}))``
-* ``dict``: ``(BAR=key=val;foo=1.1;baz=True)``, ``environ.Env(BAR=(dict(value=unicode, cast=dict(foo=float,baz=bool)), {}))``
-* ``url``
-* ``path``: ``(environ.Path)``
-* ``db_url``
-
-  * PostgreSQL: ``postgres://``, ``pgsql://``, ``psql://`` or ``postgresql://``
-  * PostGIS: ``postgis://``
-  * MySQL: ``mysql://`` or ``mysql2://``
-  * MySQL for GeoDjango: ``mysqlgis://``
-  * MySQL Connector Python from Oracle: ``mysql-connector://``
-  * SQLite: ``sqlite://``
-  * SQLite with SpatiaLite for GeoDjango: ``spatialite://``
-  * Oracle: ``oracle://``
-  * Microsoft SQL Server: ``mssql://``
-  * PyODBC: ``pyodbc://``
-  * Amazon Redshift: ``redshift://``
-  * LDAP: ``ldap://``
-
-* ``cache_url``
-
-  * Database: ``dbcache://``
-  * Dummy: ``dummycache://``
-  * File: ``filecache://``
-  * Memory: ``locmemcache://``
-  * Memcached:
-    * ``memcache://`` (uses ``python-memcached`` backend, deprecated in Django 3.2)
-    * ``pymemcache://`` (uses ``pymemcache`` backend if Django >=3.2 and package is installed, otherwise will use ``pylibmc`` backend to keep config backwards compatibility)
-    * ``pylibmc://``
-  * Redis: ``rediscache://``, ``redis://``, or ``rediss://``
-
-* ``search_url``
-
-  * Elasticsearch: ``elasticsearch://``
-  * Elasticsearch2: ``elasticsearch2://``
-  * Elasticsearch5: ``elasticsearch5://``
-  * Elasticsearch7: ``elasticsearch7://``
-  * Solr: ``solr://``
-  * Whoosh: ``whoosh://``
-  * Xapian: ``xapian://``
-  * Simple cache: ``simple://``
-
-* ``email_url``
-
-  * SMTP: ``smtp://``
-  * SMTP+SSL: ``smtp+ssl://``
-  * SMTP+TLS: ``smtp+tls://``
-  * Console mail: ``consolemail://``
-  * File mail: ``filemail://``
-  * LocMem mail: ``memorymail://``
-  * Dummy mail: ``dummymail://``
+The following are all type-casting methods of :py:class:`.environ.Env`.
+
+* :py:meth:`~.environ.Env.str`
+* :py:meth:`~.environ.Env.bool`
+* :py:meth:`~.environ.Env.int`
+* :py:meth:`~.environ.Env.float`
+* :py:meth:`~.environ.Env.json`
+* :py:meth:`~.environ.Env.url`
+* :py:meth:`~.environ.Env.list`: (accepts values like ``(FOO=a,b,c)``)
+* :py:meth:`~.environ.Env.tuple`:  (accepts values like ``(FOO=(a,b,c))``)
+* :py:meth:`~.environ.Env.path`:  (accepts values like ``(environ.Path)``)
+* :py:meth:`~.environ.Env.dict`:   (see below, ":ref:`environ-env-dict`" section)
+* :py:meth:`~.environ.Env.db_url` (see below, ":ref:`environ-env-db-url`" section)
+* :py:meth:`~.environ.Env.cache_url` (see below, ":ref:`environ-env-cache-url`" section)
+* :py:meth:`~.environ.Env.search_url` (see below, ":ref:`environ-env-search-url`" section)
+* :py:meth:`~.environ.Env.email_url` (see below, ":ref:`environ-env-email-url`" section)
+
+
+.. _environ-env-dict:
+
+``environ.Env.dict``
+======================
+
+:py:class:`.environ.Env` may parse complex variables like with the complex type-casting.
+For example:
+
+.. code-block:: python
+
+   import environ
+
+
+   env = environ.Env()
+
+   # {'key': 'val', 'foo': 'bar'}
+   env.parse_value('key=val,foo=bar', dict)
+
+   # {'key': 'val', 'foo': 1.1, 'baz': True}
+   env.parse_value(
+       'key=val;foo=1.1;baz=True',
+       dict(value=str, cast=dict(foo=float,baz=bool))
+  )
+
+For more detailed example see ":ref:`complex_dict_format`".
+
+
+.. _environ-env-db-url:
+
+``environ.Env.db_url``
+======================
+
+:py:meth:`~.environ.Env.db_url` supports the following URL schemas:
+
+.. glossary::
+
+    Amazon Redshift
+      **Database Backend:** ``django_redshift_backend``
+
+      **URL schema:** ``redshift://``
+
+    LDAP
+      **Database Backend:** ``ldapdb.backends.ldap``
+
+      **URL schema:** ``ldap://host:port/dn?attrs?scope?filter?exts``
+
+    MSSQL
+      **Database Backend:** ``sql_server.pyodbc``
+
+      **URL schema:** ``mssql://user:password@host:port/dbname``
+
+      With MySQL you can use the following schemas: ``mysql``, ``mysql2``.
+
+    MySQL (GIS)
+      **Database Backend:** ``django.contrib.gis.db.backends.mysql``
+
+      **URL schema:** ``mysqlgis://user:password@host:port/dbname``
+
+    MySQL
+      **Database Backend:** ``django.db.backends.mysql``
+
+      **URL schema:** ``mysql://user:password@host:port/dbname``
+
+    MySQL Connector Python from Oracle
+      **Database Backend:** ``mysql.connector.django``
+
+      **URL schema:** ``mysql-connector://``
+
+    Oracle
+      **Database Backend:** ``django.db.backends.oracle``
+
+      **URL schema:** ``oracle://user:password@host:port/dbname``
+
+    PostgreSQL
+      **Database Backend:** ``django.db.backends.postgresql``
+
+      **URL schema:** ``postgres://user:password@host:port/dbname``
+
+      With PostgreSQL you can use the following schemas: ``postgres``, ``postgresql``, ``psql``, ``pgsql``, ``postgis``.
+      You can also use UNIX domain sockets path instead of hostname. For example: ``postgres://path/dbname``.
+      The ``django.db.backends.postgresql_psycopg2`` will be used if the Django version is less than ``2.0``.
+
+    PostGIS
+      **Database Backend:** ``django.contrib.gis.db.backends.postgis``
+
+      **URL schema:** ``postgis://user:password@host:port/dbname``
+
+    PyODBC
+      **Database Backend:** ``sql_server.pyodbc``
+
+      **URL schema:** ``pyodbc://``
+
+    SQLite
+      **Database Backend:** ``django.db.backends.sqlite3``
+
+      **URL schema:** ``sqlite:////absolute/path/to/db/file``
+
+      SQLite connects to file based databases. URL schemas ``sqlite://`` or
+      ``sqlite://:memory:`` means the database is in the memory (not a file on disk).
+
+    SpatiaLite
+      **Database Backend:** ``django.contrib.gis.db.backends.spatialite``
+
+      **URL schema:** ``spatialite:///PATH``
+
+      SQLite connects to file based databases. URL schemas ``sqlite://`` or
+      ``sqlite://:memory:`` means the database is in the memory (not a file on disk).
+
+
+.. _environ-env-cache-url:
+
+``environ.Env.cache_url``
+=========================
+
+:py:meth:`~.environ.Env.cache_url` supports the following URL schemas:
+
+* Database: ``dbcache://``
+* Dummy: ``dummycache://``
+* File: ``filecache://``
+* Memory: ``locmemcache://``
+* Memcached:
+
+  * ``memcache://`` (uses ``python-memcached`` backend, deprecated in Django 3.2)
+  * ``pymemcache://`` (uses ``pymemcache`` backend if Django >=3.2 and package is installed, otherwise will use ``pylibmc`` backend to keep config backwards compatibility)
+  * ``pylibmc://``
+
+* Redis: ``rediscache://``, ``redis://``, or ``rediss://``
+
+
+.. _environ-env-search-url:
+
+``environ.Env.search_url``
+==========================
+
+:py:meth:`~.environ.Env.search_url` supports the following URL schemas:
+
+* Elasticsearch: ``elasticsearch://``
+* Elasticsearch2: ``elasticsearch2://``
+* Elasticsearch5: ``elasticsearch5://``
+* Elasticsearch7: ``elasticsearch7://``
+* Solr: ``solr://``
+* Whoosh: ``whoosh://``
+* Xapian: ``xapian://``
+* Simple cache: ``simple://``
+
+
+.. _environ-env-email-url:
+
+``environ.Env.email_url``
+==========================
+
+:py:meth:`~.environ.Env.email_url` supports the following URL schemas:
+
+* SMTP: ``smtp://``
+* SMTP+SSL: ``smtp+ssl://``
+* SMTP+TLS: ``smtp+tls://``
+* Console mail: ``consolemail://``
+* File mail: ``filemail://``
+* LocMem mail: ``memorymail://``
+* Dummy mail: ``dummymail://``
diff --git a/environ/__init__.py b/environ/__init__.py
index 97733f4..72a30ba 100644
--- a/environ/__init__.py
+++ b/environ/__init__.py
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
@@ -11,31 +11,36 @@
 This module tracks the version of the package as well as the base
 package info used by various functions within django-environ.
 
-Misc variables:
-
-    __copyright__
-    __version__
-    __license__
-    __author__
-    __author_email__
-    __maintainer__
-    __maintainer_email__
-    __url__
-    __description__
-
-Refer to the `documentation <https://django-environ.readthedocs.org/>`_
+Refer to the `documentation <https://django-environ.readthedocs.io/en/latest/>`_
 for details on the use of this package.
-"""
+"""  # noqa: E501
 
 from .environ import *
 
 
 __copyright__ = 'Copyright (C) 2021 Daniele Faraglia'
-__version__ = '0.8.1'
+"""The copyright notice of the package."""
+
+__version__ = '0.9.0'
+"""The version of the package."""
+
 __license__ = 'MIT'
+"""The license of the package."""
+
 __author__ = 'Daniele Faraglia'
+"""The author of the package."""
+
 __author_email__ = 'daniele.faraglia@gmail.com'
+"""The email of the author of the package."""
+
 __maintainer__ = 'Serghei Iakovlev'
+"""The maintainer of the package."""
+
 __maintainer_email__ = 'egrep@protonmail.ch'
+"""The email of the maintainer of the package."""
+
 __url__ = 'https://django-environ.readthedocs.org'
+"""The URL of the package."""
+
 __description__ = 'A package that allows you to utilize 12factor inspired environment variables to configure your Django application.'  # noqa: E501
+"""The description of the package."""
diff --git a/environ/compat.py b/environ/compat.py
index 8c259f8..a4ae366 100644
--- a/environ/compat.py
+++ b/environ/compat.py
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
@@ -39,8 +39,8 @@ else:
     REDIS_DRIVER = 'django_redis.cache.RedisCache'
 
 
-# back compatibility for pymemcache
 def choose_pymemcache_driver():
+    """Backward compatibility for pymemcache."""
     old_django = DJANGO_VERSION is not None and DJANGO_VERSION < (3, 2)
     if old_django or not find_loader('pymemcache'):
         # The original backend choice for the 'pymemcache' scheme is
diff --git a/environ/environ.py b/environ/environ.py
index acdfb15..fc5eebb 100644
--- a/environ/environ.py
+++ b/environ/environ.py
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
@@ -12,6 +12,7 @@ variables to configure your Django application.
 """
 
 import ast
+import itertools
 import logging
 import os
 import re
@@ -21,6 +22,7 @@ import warnings
 from urllib.parse import (
     parse_qs,
     ParseResult,
+    unquote,
     unquote_plus,
     urlparse,
     urlunparse,
@@ -61,7 +63,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:
@@ -72,13 +74,32 @@ class NoValue:
 
 class Env:
     """Provide scheme-based lookups of environment variables so that each
-    caller doesn't have to pass in `cast` and `default` parameters.
+    caller doesn't have to pass in ``cast`` and ``default`` parameters.
 
     Usage:::
 
-        env = Env(MAIL_ENABLED=bool, SMTP_LOGIN=(str, 'DEFAULT'))
-        if env('MAIL_ENABLED'):
-            ...
+        import environ
+        import os
+
+        env = environ.Env(
+            # set casting, default value
+            MAIL_ENABLED=(bool, False),
+            SMTP_LOGIN=(str, 'DEFAULT')
+        )
+
+        # Set the project base directory
+        BASE_DIR = os.path.dirname(
+            os.path.dirname(os.path.abspath(__file__))
+        )
+
+        # Take environment variables from .env file
+        environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
+
+        # False if not in os.environ due to casting above
+        MAIL_ENABLED = env('MAIL_ENABLED')
+
+        # 'DEFAULT' if not in os.environ due to casting above
+        SMTP_LOGIN = env('SMTP_LOGIN')
     """
 
     ENVIRON = os.environ
@@ -169,6 +190,7 @@ class Env:
     def __init__(self, **scheme):
         self.smart_cast = True
         self.escape_proxy = False
+        self.prefix = ""
         self.scheme = scheme
 
     def __call__(self, var, cast=None, default=NOTSET, parse_default=False):
@@ -197,6 +219,15 @@ class Env:
         """Helper for python2
         :rtype: unicode
         """
+        warnings.warn(
+            '`%s.unicode` is deprecated, use `%s.str` instead' % (
+                self.__class__.__name__,
+                self.__class__.__name__,
+            ),
+            DeprecationWarning,
+            stacklevel=2
+        )
+
         return self.get_value(var, cast=str, default=default)
 
     def bytes(self, var, default=NOTSET, encoding='utf8'):
@@ -260,7 +291,7 @@ class Env:
 
     def url(self, var, default=NOTSET):
         """
-        :rtype: urlparse.ParseResult
+        :rtype: urllib.parse.ParseResult
         """
         return self.get_value(
             var,
@@ -330,20 +361,25 @@ class Env:
     def get_value(self, var, cast=None, default=NOTSET, parse_default=False):
         """Return value for given environment variable.
 
-        :param var: Name of variable.
-        :param cast: Type to cast return value as.
-        :param default: If var not present in environ, return this instead.
-        :param parse_default: force to parse default..
-
-        :returns: Value from environment or default (if set)
+        :param str var:
+            Name of variable.
+        :param collections.abc.Callable or None cast:
+            Type to cast return value as.
+        :param default:
+             If var not present in environ, return this instead.
+        :param bool parse_default:
+            Force to parse default.
+        :returns: Value from environment or default (if set).
+        :rtype: typing.IO[typing.Any]
         """
 
         logger.debug("get '{}' casted as '{}' with default '{}'".format(
             var, cast, default
         ))
 
-        if var in self.scheme:
-            var_info = self.scheme[var]
+        var_name = "{}{}".format(self.prefix, var)
+        if var_name in self.scheme:
+            var_info = self.scheme[var_name]
 
             try:
                 has_default = len(var_info) == 2
@@ -364,11 +400,11 @@ class Env:
                     cast = var_info
 
         try:
-            value = self.ENVIRON[var]
-        except KeyError:
+            value = self.ENVIRON[var_name]
+        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
 
@@ -456,15 +492,29 @@ class Env:
 
     @classmethod
     def db_url_config(cls, url, engine=None):
-        """Pulled from DJ-Database-URL, parse an arbitrary Database URL.
-
-        Support currently exists for PostgreSQL, PostGIS, MySQL, Oracle and
-        SQLite.
-
-        SQLite connects to file based databases. The same URL format is used,
-        omitting the hostname, and using the "file" portion as the filename of
-        the database. This has the effect of four slashes being present for an
-        absolute file path.
+        """Parse an arbitrary database URL.
+
+        Supports the following URL schemas:
+
+        * PostgreSQL: ``postgres[ql]?://`` or ``p[g]?sql://``
+        * PostGIS: ``postgis://``
+        * MySQL: ``mysql://`` or ``mysql2://``
+        * MySQL (GIS): ``mysqlgis://``
+        * MySQL Connector Python from Oracle: ``mysql-connector://``
+        * SQLite: ``sqlite://``
+        * SQLite with SpatiaLite for GeoDjango: ``spatialite://``
+        * Oracle: ``oracle://``
+        * Microsoft SQL Server: ``mssql://``
+        * PyODBC: ``pyodbc://``
+        * Amazon Redshift: ``redshift://``
+        * LDAP: ``ldap://``
+
+        :param urllib.parse.ParseResult or str url:
+            Database URL to parse.
+        :param str or None engine:
+            If None, the database engine is evaluates from the ``url``.
+        :return: Parsed database URL.
+        :rtype: dict
         """
         if not isinstance(url, cls.URL_CLASS):
             if url == 'sqlite://:memory:':
@@ -501,13 +551,30 @@ class Env:
             if url.port:
                 path += ':{port}'.format(port=url.port)
 
+        user_host = url.netloc.rsplit('@', 1)
+        if url.scheme in cls.POSTGRES_FAMILY and ',' in user_host[-1]:
+            # Parsing postgres cluster dsn
+            hinfo = list(
+                itertools.zip_longest(
+                    *(
+                        host.rsplit(':', 1)
+                        for host in user_host[-1].split(',')
+                    )
+                )
+            )
+            hostname = ','.join(hinfo[0])
+            port = ','.join(filter(None, hinfo[1])) if len(hinfo) == 2 else ''
+        else:
+            hostname = url.hostname
+            port = url.port
+
         # Update with environment configuration.
         config.update({
             'NAME': path or '',
             'USER': _cast_urlstr(url.username) or '',
             'PASSWORD': _cast_urlstr(url.password) or '',
-            'HOST': url.hostname or '',
-            'PORT': _cast_int(url.port) or '',
+            'HOST': hostname or '',
+            'PORT': _cast_int(port) or '',
         })
 
         if (
@@ -552,11 +619,14 @@ class Env:
 
     @classmethod
     def cache_url_config(cls, url, backend=None):
-        """Pulled from DJ-Cache-URL, parse an arbitrary Cache URL.
+        """Parse an arbitrary cache URL.
 
-        :param url:
-        :param backend:
-        :return:
+        :param urllib.parse.ParseResult or str url:
+            Cache URL to parse.
+        :param str or None backend:
+            If None, the backend is evaluates from the ``url``.
+        :return: Parsed cache URL.
+        :rtype: dict
         """
         if not isinstance(url, cls.URL_CLASS):
             if not url:
@@ -625,7 +695,15 @@ class Env:
 
     @classmethod
     def email_url_config(cls, url, backend=None):
-        """Parses an email URL."""
+        """Parse an arbitrary email URL.
+
+        :param urllib.parse.ParseResult or str url:
+            Email URL to parse.
+        :param str or None backend:
+            If None, the backend is evaluates from the ``url``.
+        :return: Parsed email URL.
+        :rtype: dict
+        """
 
         config = {}
 
@@ -670,6 +748,16 @@ class Env:
 
     @classmethod
     def search_url_config(cls, url, engine=None):
+        """Parse an arbitrary search URL.
+
+        :param urllib.parse.ParseResult or str url:
+            Search URL to parse.
+        :param str or None engine:
+            If None, the engine is evaluates from the ``url``.
+        :return: Parsed search URL.
+        :rtype: dict
+        """
+
         config = {}
 
         url = urlparse(url) if not isinstance(url, cls.URL_CLASS) else url
@@ -759,26 +847,26 @@ class Env:
 
     @classmethod
     def read_env(cls, env_file=None, overwrite=False, **overrides):
-        """Read a .env file into os.environ.
+        r"""Read a .env file into os.environ.
 
         If not given a path to a dotenv path, does filthy magic stack
         backtracking to find the dotenv in the same directory as the file that
-        called read_env.
+        called ``read_env``.
 
         Existing environment variables take precedent and are NOT overwritten
         by the file content. ``overwrite=True`` will force an overwrite of
         existing environment variables.
 
         Refs:
-        - https://wellfire.co/learn/easier-12-factor-django
-        - https://gist.github.com/bennylope/2999704
 
-        :param env_file: The path to the `.env` file your application should
+        * https://wellfire.co/learn/easier-12-factor-django
+
+        :param env_file: The path to the ``.env`` file your application should
             use. If a path is not provided, `read_env` will attempt to import
             the Django settings module from the Django project root.
         :param overwrite: ``overwrite=True`` will force an overwrite of
             existing environment variables.
-        :param **overrides: Any additional keyword arguments provided directly
+        :param \**overrides: Any additional keyword arguments provided directly
             to read_env will be added to the environment. If the key matches an
             existing environment variable, the value will be overridden.
         """
@@ -881,13 +969,12 @@ class Path:
         return self.__class__(self.__root__, *paths, **kwargs)
 
     def file(self, name, *args, **kwargs):
-        """Open a file.
-
-        :param name: Filename appended to self.root
-        :param args: passed to open()
-        :param kwargs: passed to open()
+        r"""Open a file.
 
-        :rtype: file
+        :param str name: Filename appended to :py:attr:`~root`
+        :param \*args: ``*args`` passed to :py:func:`open`
+        :param \**kwargs: ``**kwargs`` passed to :py:func:`open`
+        :rtype: typing.IO[typing.Any]
         """
         return open(self(name), *args, **kwargs)
 
@@ -914,7 +1001,9 @@ class Path:
         return self._absolute_join(self.__root__, *paths, **kwargs)
 
     def __eq__(self, other):
-        return self.__root__ == other.__root__
+        if isinstance(other, Path):
+            return self.__root__ == other.__root__
+        return self.__root__ == other
 
     def __ne__(self, other):
         return not self.__eq__(other)
diff --git a/environ/fileaware_mapping.py b/environ/fileaware_mapping.py
index e26ee8d..6da95f5 100644
--- a/environ/fileaware_mapping.py
+++ b/environ/fileaware_mapping.py
@@ -1,17 +1,27 @@
+# This file is part of the django-environ.
+#
+# Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
+#
+# For the full copyright and license information, please view
+# the LICENSE.txt file that was distributed with this source code.
+
+"""Docker-style file variable support module."""
+
 import os
 from collections.abc import MutableMapping
 
 
 class FileAwareMapping(MutableMapping):
     """
-    A mapping that wraps os.environ, first checking for the existance of a key
+    A mapping that wraps os.environ, first checking for the existence of a key
     appended with ``_FILE`` whenever reading a value. If a matching file key is
     found then the value is instead read from the file system at this location.
 
     By default, values read from the file system are cached so future lookups
     do not hit the disk again.
 
-    A ``_FILE`` key has higher precidence than a value is set directly in the
+    A ``_FILE`` key has higher precedence than a value is set directly in the
     environment, and an exception is raised if the file can not be found.
     """
 
diff --git a/setup.py b/setup.py
index 24e8a85..4de0bec 100644
--- a/setup.py
+++ b/setup.py
@@ -130,6 +130,7 @@ CLASSIFIERS = [
     'Framework :: Django :: 3.0',
     'Framework :: Django :: 3.1',
     'Framework :: Django :: 3.2',
+    'Framework :: Django :: 4.0',
 
     'Operating System :: OS Independent',
 
diff --git a/tox.ini b/tox.ini
index 4877c14..73e6a6b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -20,6 +20,7 @@ envlist =
     manifest
     py{35,36,37,38,39,310}-django{111,22}
     py{36,37,38,39,310}-django{30,31,32}
+    py{38,39,310}-django{40}
     pypy-django{111,22,30,31,32}
 
 [gh-actions]
@@ -41,6 +42,7 @@ deps =
     django30: Django>=3.0,<3.1
     django31: Django>=3.1,<3.2
     django32: Django>=3.2,<3.3
+    django40: Django>=4.0,<4.1
 commands_pre =
     python -m pip install --upgrade pip
     python -m pip install .
@@ -74,7 +76,7 @@ commands = flake8 environ setup.py
 description = Check external links in the package documentation
 # Keep basepython in sync with .readthedocs.yml and docs.yml
 # (GitHub Action Workflow).
-basepython = python3.8
+basepython = python3.10
 extras = docs
 commands =
     {envpython} -m sphinx \
@@ -92,7 +94,7 @@ isolated_build = true
 description = Build package documentation (HTML)
 # Keep basepython in sync with .readthedocs.yml and docs.yml
 # (GitHub Action Workflow).
-basepython = python3.8
+basepython = python3.10
 extras = docs
 commands =
     {envpython} -m sphinx \


Test patch:
diff --git a/tests/fixtures.py b/tests/fixtures.py
index 25213ac..c7f8a0b 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
@@ -27,6 +27,12 @@ class FakeEnv:
     DICT = dict(foo='bar', test='on')
     PATH = '/home/dev'
     EXPORTED = 'exported var'
+    SAML_ATTRIBUTE_MAPPING = dict(
+        uid=('username',),
+        mail=('email',),
+        cn=('first_name',),
+        sn=('last_name',)
+    )
 
     @classmethod
     def generate_data(cls):
@@ -57,6 +63,7 @@ class FakeEnv:
                     ESCAPED_VAR=r'\$baz',
                     INT_LIST='42,33',
                     INT_TUPLE='(42,33)',
+                    MIX_TUPLE='(42,Test)',
                     STR_LIST_WITH_SPACES=' foo,  bar',
                     EMPTY_LIST='',
                     DICT_VAR='foo=bar,test=on',
@@ -75,4 +82,7 @@ class FakeEnv:
                     URL_VAR=cls.URL,
                     JSON_VAR=json.dumps(cls.JSON),
                     PATH_VAR=cls.PATH,
-                    EXPORTED_VAR=cls.EXPORTED)
+                    EXPORTED_VAR=cls.EXPORTED,
+                    SAML_ATTRIBUTE_MAPPING='uid=username;mail=email;cn=first_name;sn=last_name;',
+                    PREFIX_TEST='foo',
+                    )
diff --git a/tests/test_db.py b/tests/test_db.py
index c495850..20d807f 100644
--- a/tests/test_db.py
+++ b/tests/test_db.py
@@ -17,52 +17,73 @@ from environ.compat import DJANGO_POSTGRES
 @pytest.mark.parametrize(
     'url,engine,name,host,user,passwd,port',
     [
-        ('postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.'
-         'compute-1.amazonaws.com:5431/d8r82722r2kuvn',
+        # postgres://user:password@host:port/dbname
+        ('postgres://enigma:secret@example.com:5431/dbname',
          DJANGO_POSTGRES,
-         'd8r82722r2kuvn',
-         'ec2-107-21-253-135.compute-1.amazonaws.com',
-         'uf07k1i6d8ia0v',
-         'wegauwhgeuioweg',
+         'dbname',
+         'example.com',
+         'enigma',
+         'secret',
          5431),
-        ('postgres:////var/run/postgresql/db',
+        # postgres://path/dbname
+        ('postgres:////var/run/postgresql/dbname',
          DJANGO_POSTGRES,
-         'db',
+         'dbname',
          '/var/run/postgresql',
          '',
          '',
          ''),
-        ('postgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.'
-         'compute-1.amazonaws.com:5431/d8r82722r2kuvn',
+        # postgis://user:password@host:port/dbname
+        ('postgis://enigma:secret@example.com:5431/dbname',
          'django.contrib.gis.db.backends.postgis',
-         'd8r82722r2kuvn',
-         'ec2-107-21-253-135.compute-1.amazonaws.com',
-         'uf07k1i6d8ia0v',
-         'wegauwhgeuioweg',
+         'dbname',
+         'example.com',
+         'enigma',
+         'secret',
          5431),
-        ('mysqlgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.'
-         'compute-1.amazonaws.com:5431/d8r82722r2kuvn',
+        # postgres://user:password@host:port,host:port,host:port/dbname
+        ('postgres://username:p@ss:12,wor:34d@host1:111,22.55.44.88:222,[2001:db8::1234]:333/db',
+         DJANGO_POSTGRES,
+         'db',
+         'host1,22.55.44.88,[2001:db8::1234]',
+         'username',
+         'p@ss:12,wor:34d',
+         '111,222,333'
+         ),
+        # postgres://host,host,host/dbname
+        ('postgres://node1,node2,node3/db',
+         DJANGO_POSTGRES,
+         'db',
+         'node1,node2,node3',
+         '',
+         '',
+         ''
+         ),
+        # mysqlgis://user:password@host:port/dbname
+        ('mysqlgis://enigma:secret@example.com:5431/dbname',
          'django.contrib.gis.db.backends.mysql',
-         'd8r82722r2kuvn',
-         'ec2-107-21-253-135.compute-1.amazonaws.com',
-         'uf07k1i6d8ia0v',
-         'wegauwhgeuioweg',
+         'dbname',
+         'example.com',
+         'enigma',
+         'secret',
          5431),
-        ('mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com'
-         '/heroku_97681db3eff7580?reconnect=true',
+        # mysql://user:password@host/dbname?options
+        ('mysql://enigma:secret@reconnect.com/dbname?reconnect=true',
          'django.db.backends.mysql',
-         'heroku_97681db3eff7580',
-         'us-cdbr-east.cleardb.com',
-         'bea6eb025ca0d8',
-         '69772142',
+         'dbname',
+         'reconnect.com',
+         'enigma',
+         'secret',
          ''),
-        ('mysql://travis@localhost/test_db',
+        # mysql://user@host/dbname
+        ('mysql://enigma@localhost/dbname',
          'django.db.backends.mysql',
-         'test_db',
+         'dbname',
          'localhost',
-         'travis',
+         'enigma',
          '',
          ''),
+        # sqlite://
         ('sqlite://',
          'django.db.backends.sqlite3',
          ':memory:',
@@ -70,6 +91,7 @@ from environ.compat import DJANGO_POSTGRES
          '',
          '',
          ''),
+        # sqlite:////absolute/path/to/db/file
         ('sqlite:////full/path/to/your/file.sqlite',
          'django.db.backends.sqlite3',
          '/full/path/to/your/file.sqlite',
@@ -77,6 +99,7 @@ from environ.compat import DJANGO_POSTGRES
          '',
          '',
          ''),
+        # sqlite://:memory:
         ('sqlite://:memory:',
          'django.db.backends.sqlite3',
          ':memory:',
@@ -84,19 +107,39 @@ from environ.compat import DJANGO_POSTGRES
          '',
          '',
          ''),
-        ('ldap://cn=admin,dc=nodomain,dc=org:'
-         'some_secret_password@ldap.nodomain.org/',
+        # ldap://user:password@host
+        ('ldap://cn=admin,dc=nodomain,dc=org:secret@example.com',
          'ldapdb.backends.ldap',
-         'ldap://ldap.nodomain.org',
-         'ldap.nodomain.org',
+         'ldap://example.com',
+         'example.com',
          'cn=admin,dc=nodomain,dc=org',
-         'some_secret_password',
+         'secret',
+         ''),
+        # mysql://user:password@host/dbname
+        ('mssql://enigma:secret@example.com/dbname'
+         '?driver=ODBC Driver 13 for SQL Server',
+         'sql_server.pyodbc',
+         'dbname',
+         'example.com',
+         'enigma',
+         'secret',
          ''),
+        # mysql://user:password@host:port/dbname
+        ('mssql://enigma:secret@amazonaws.com\\insnsnss:12345/dbname'
+         '?driver=ODBC Driver 13 for SQL Server',
+         'sql_server.pyodbc',
+         'dbname',
+         'amazonaws.com\\insnsnss',
+         'enigma',
+         'secret',
+         12345),
     ],
     ids=[
         'postgres',
         'postgres_unix_domain',
         'postgis',
+        'postgres_cluster',
+        'postgres_no_ports',
         'mysqlgis',
         'cleardb',
         'mysql_no_password',
@@ -104,6 +147,8 @@ from environ.compat import DJANGO_POSTGRES
         'sqlite_file',
         'sqlite_memory',
         'ldap',
+        'mssql',
+        'mssql_port',
     ],
 )
 def test_db_parsing(url, engine, name, host, user, passwd, port):
@@ -118,6 +163,13 @@ def test_db_parsing(url, engine, name, host, user, passwd, port):
         assert config['USER'] == user
         assert config['HOST'] == host
 
+    if engine == 'sql_server.pyodbc':
+        assert config['OPTIONS'] == {'driver': 'ODBC Driver 13 for SQL Server'}
+
+    if host == 'reconnect.com':
+        assert config['OPTIONS'] == {'reconnect': 'true'}
+        
+
 
 def test_postgres_complex_db_name_parsing():
     """Make sure we can use complex postgres host."""
diff --git a/tests/test_env.py b/tests/test_env.py
index 0be9a60..b5c1e39 100644
--- a/tests/test_env.py
+++ b/tests/test_env.py
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
@@ -8,6 +8,7 @@
 
 import os
 from urllib.parse import quote
+from warnings import catch_warnings
 
 import pytest
 
@@ -45,6 +46,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
@@ -69,6 +71,21 @@ class TestEnv:
             assert self.env(var) == val
         assert self.env.str(var, multiline=multiline) == val
 
+    def test_unicode(self, recwarn):
+        actual = self.env.unicode('CYRILLIC_VAR', default='фуубар')
+        expected = self.env.str('CYRILLIC_VAR', default='фуубар')
+
+        assert actual == expected
+        assert len(recwarn) == 1
+        w = recwarn.pop(DeprecationWarning)
+        assert issubclass(w.category, DeprecationWarning)
+        assert str(w.message) == '`%s.unicode` is deprecated, use `%s.str` instead' %(
+            self.env.__class__.__name__,
+            self.env.__class__.__name__,
+        )
+        assert w.filename
+        assert w.lineno
+
     @pytest.mark.parametrize(
         'var,val,default',
         [
@@ -139,11 +156,25 @@ class TestEnv:
         assert_type_and_value(list, [42, 33], self.env('INT_LIST', cast=[int]))
         assert_type_and_value(list, [42, 33], self.env.list('INT_LIST', int))
 
-    def test_int_tuple(self):
+    def test_int_list_cast_tuple(self):
         assert_type_and_value(tuple, (42, 33), self.env('INT_LIST', cast=(int,)))
         assert_type_and_value(tuple, (42, 33), self.env.tuple('INT_LIST', int))
         assert_type_and_value(tuple, ('42', '33'), self.env.tuple('INT_LIST'))
 
+    def test_int_tuple(self):
+        assert_type_and_value(tuple, (42, 33), self.env('INT_TUPLE', cast=(int,)))
+        assert_type_and_value(tuple, (42, 33), self.env.tuple('INT_TUPLE', int))
+        assert_type_and_value(tuple, ('42', '33'), self.env.tuple('INT_TUPLE'))
+
+    def test_mix_tuple_issue_387(self):
+        """Cast a tuple of mixed types.
+        
+        Casts a string like "(42,Test)" to a tuple like  (42, 'Test').
+        See: https://github.com/joke2k/django-environ/issues/387 for details."""
+        caster = lambda v: int(v) if v.isdigit() else v.strip()
+        cast = lambda t: tuple(map(caster, [c for c in t.strip('()').split(',')]))
+        assert_type_and_value(tuple, (42, 'Test'), self.env( 'MIX_TUPLE', default=(0, ''), cast=cast))
+
     def test_str_list_with_spaces(self):
         assert_type_and_value(list, [' foo', '  bar'],
                               self.env('STR_LIST_WITH_SPACES', cast=[str]))
@@ -156,6 +187,13 @@ class TestEnv:
     def test_dict_value(self):
         assert_type_and_value(dict, FakeEnv.DICT, self.env.dict('DICT_VAR'))
 
+    def test_complex_dict_value(self):
+        assert_type_and_value(
+            dict,
+            FakeEnv.SAML_ATTRIBUTE_MAPPING,
+            self.env.dict('SAML_ATTRIBUTE_MAPPING', cast={'value': tuple})
+        )
+
     @pytest.mark.parametrize(
         'value,cast,expected',
         [
@@ -304,6 +342,10 @@ class TestEnv:
     def test_exported(self):
         assert self.env('EXPORTED_VAR') == FakeEnv.EXPORTED
 
+    def test_prefix(self):
+        self.env.prefix = 'PREFIX_'
+        assert self.env('TEST') == 'foo'
+
 
 class TestFileEnv(TestEnv):
     def setup_method(self, method):
diff --git a/tests/test_env.txt b/tests/test_env.txt
index 78422b1..69dc84e 100644
--- a/tests/test_env.txt
+++ b/tests/test_env.txt
@@ -49,10 +49,17 @@ MULTILINE_ESCAPED_STR_VAR=---BEGIN---\\n---END---
 INT_LIST=42,33
 CYRILLIC_VAR=фуубар
 INT_TUPLE=(42,33)
+MIX_TUPLE=(42,Test)
 DATABASE_ORACLE_TNS_URL=oracle://user:password@sid
 DATABASE_ORACLE_URL=oracle://user:password@host:1521/sid
 DATABASE_REDSHIFT_URL=redshift://user:password@examplecluster.abc123xyz789.us-west-2.redshift.amazonaws.com:5439/dev
 DATABASE_CUSTOM_BACKEND_URL=custom.backend://user:password@example.com:5430/database
 
+# Djangosaml2's SAML_ATTRIBUTE_MAPPING
+SAML_ATTRIBUTE_MAPPING="uid=username;mail=email;cn=first_name;sn=last_name;"
+
 # Exports
 export EXPORTED_VAR="exported var"
+
+# Prefixed
+PREFIX_TEST='foo'
diff --git a/tests/test_path.py b/tests/test_path.py
index 502cf78..ed33a7c 100644
--- a/tests/test_path.py
+++ b/tests/test_path.py
@@ -47,7 +47,7 @@ def test_repr(volume):
         assert root.__repr__() == '<Path:/home>'
 
 
-def test_comparison():
+def test_comparison(volume):
     root = Path('/home')
     assert root.__eq__(Path('/home'))
     assert root in Path('/')
@@ -62,6 +62,14 @@ def test_comparison():
     assert Path('/home/foo/').__fspath__() == str(Path('/home/foo/'))
     assert ~Path('/home') == Path('/')
 
+    if sys.platform == 'win32':
+        assert Path('/home') == '{}home'.format(volume)
+        assert '{}home'.format(volume) == Path('/home')
+    else:
+        assert Path('/home') == '/home'
+        assert '/home' == Path('/home')
+
+    assert Path('/home') != '/usr'
 
 def test_sum():
     """Make sure Path correct handle __add__."""
diff --git a/tests/test_search.py b/tests/test_search.py
index a81471e..0992bf9 100644
--- a/tests/test_search.py
+++ b/tests/test_search.py
@@ -43,6 +43,12 @@ def test_solr_multicore_parsing(solr_url):
          'elasticsearch5_backend.Elasticsearch5SearchEngine'),
         ('elasticsearch7://127.0.0.1:9200/index',
          'elasticsearch7_backend.Elasticsearch7SearchEngine'),
+    ],
+    ids=[
+        'elasticsearch',
+        'elasticsearch2',
+        'elasticsearch5',
+        'elasticsearch7',
     ]
 )
 def test_elasticsearch_parsing(url, engine):
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