Repository: joke2k/django-environ
Pull request #443 (2023-03-01 09:40:20)

Release v0.10.0


Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index dbaf61f..218c0b0 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -5,8 +5,16 @@ updates:
   - package-ecosystem: pip
     # setup.py stored in repository root.
     directory: '/'
+    # Raise pull requests for version updates
+    # to pip against the `develop` branch
+    target-branch: develop
     schedule:
-      interval: daily
+      # Check for updates managed by pip once a week
+      interval: weekly
+    # Specify labels for npm pull requests
+    labels:
+      - pip
+      - dependencies
     assignees:
       - sergeyklay
 
@@ -14,7 +22,15 @@ updates:
     # Workflow files stored in the
     # default location of `.github/workflows`
     directory: '/'
+    # Raise pull requests for version updates
+    # to pip against the `develop` branch
+    target-branch: develop
     schedule:
-      interval: daily
+      # Check for updates for GitHub actions once a week
+      interval: weekly
+    # Specify labels for npm pull requests
+    labels:
+      - github_actions
+      - dependencies
     assignees:
       - sergeyklay
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index c2769ef..636ad60 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -2,15 +2,16 @@ name: Build
 
 on:
   push:
-    branches:
-      - develop
-      - main
-      - 'feature/**'
-      - 'release/**'
-      - 'fix/**'
+    branches-ignore:
+      # These should always correspond to pull requests, so ignore them for
+      # the push trigger and let them be triggered by the pull_request
+      # trigger, avoiding running the workflow twice.  This is a minor
+      # optimization so there's no need to ensure this is comprehensive.
+      - 'dependabot/**'
     tags:
       - 'v[0-9]+.[0-9]+.[0-9]+'
 
+  # The branches below must be a subset of the branches above
   pull_request:
     branches:
       - develop
@@ -19,18 +20,14 @@ on:
 jobs:
   build:
     name: Build and test package distribution
-    runs-on: ${{ matrix.os }}
-
-    strategy:
-      matrix:
-        os: [ ubuntu-latest, macos-latest, windows-latest ]
+    runs-on: ubuntu-latest
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3.0.2
+        uses: actions/checkout@v3.3.0
 
       - name: Set up Python 3.10
-        uses: actions/setup-python@v4.0.0
+        uses: actions/setup-python@v4.5.0
         with:
           python-version: '3.10'
 
@@ -67,10 +64,10 @@ jobs:
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3.0.2
+        uses: actions/checkout@v3.3.0
 
       - name: Set up Python 3.10
-        uses: actions/setup-python@v4.0.0
+        uses: actions/setup-python@v4.5.0
         with:
           python-version: '3.10'
 
diff --git a/.github/workflows/change-pr-target.yml b/.github/workflows/change-pr-target.yml
new file mode 100644
index 0000000..eb12a30
--- /dev/null
+++ b/.github/workflows/change-pr-target.yml
@@ -0,0 +1,20 @@
+name: Make sure new PRs are sent to develop
+
+on:
+  pull_request_target:
+    types: [opened, edited]
+
+jobs:
+  check-branch:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: Vankka/pr-target-branch-action@v2
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        with:
+          target: main
+          exclude: develop # Don't prevent going from develop -> main
+          change-to: develop
+          comment: |
+            Your PR was set to target `main`, PRs should be target `develop`
+            The base branch of this PR has been automatically changed to `develop`, please check that there are no merge conflicts.
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 7c5d59f..3261d72 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,13 +2,14 @@ name: CI
 
 on:
   push:
-    branches:
-      - develop
-      - main
-      - 'feature/**'
-      - 'release/**'
-      - 'fix/**'
-
+    branches-ignore:
+      # These should always correspond to pull requests, so ignore them for
+      # the push trigger and let them be triggered by the pull_request
+      # trigger, avoiding running the workflow twice.  This is a minor
+      # optimization so there's no need to ensure this is comprehensive.
+      - 'dependabot/**'
+
+  # The branches below must be a subset of the branches above
   pull_request:
     branches:
       - develop
@@ -46,23 +47,33 @@ jobs:
 
       matrix:
         python:
-          - '3.5'
-          - '3.6'
-          - '3.7'
           - '3.8'
           - '3.9'
           - '3.10'
+          - '3.11'
           - 'pypy-3.7'
         os: [ ubuntu-latest, macos-latest, windows-latest ]
 
+        # These versions are no longer supported by Python team, and may
+        # eventually be dropped from GitHub Actions.  The support of these
+        # versions by django-environ will continue for as long as possible,
+        # and may be discontinued at any time.
+        include:
+          - python: '3.5'
+            os: ubuntu-20.04
+          - python: '3.6'
+            os: ubuntu-20.04
+          - python: '3.7'
+            os: ubuntu-20.04
+
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3.0.2
+        uses: actions/checkout@v3.3.0
         with:
           fetch-depth: 5
 
       - name: Set up Python ${{ matrix.python }}
-        uses: actions/setup-python@v4.0.0
+        uses: actions/setup-python@v4.5.0
         with:
           python-version: ${{ matrix.python }}
 
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..4cf741a
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,60 @@
+name: CodeQL
+
+on:
+  push:
+    branches:
+      - develop
+      - main
+
+  # The branches below must be a subset of the branches above
+  pull_request:
+    branches:
+      - develop
+      - main
+
+  schedule:
+    - cron: '40 22 * * 5'
+      #       |  | | | |
+      #       |  | | | |____ day of the week (0 - 6 or SUN-SAT)
+      #       |  | | |____ month (1 - 12 or JAN-DEC)
+      #       |  | |____ day of the month (1 - 31)
+      #       |  |____ hour (0 - 23)
+      #       |____ minute (0 - 59)
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    # The maximum number of minutes to let a workflow run
+    # before GitHub automatically cancels it. Default: 360
+    timeout-minutes: 30
+
+    strategy:
+      # When set to true, GitHub cancels
+      # all in-progress jobs if any matrix job fails.
+      fail-fast: false
+
+      matrix:
+        language:
+          - python
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v3.3.0
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v2
+      with:
+        languages: ${{ matrix.language }}
+        
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v2
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v2
diff --git a/.github/workflows/cs.yml b/.github/workflows/cs.yml
index bf4e81f..795a153 100644
--- a/.github/workflows/cs.yml
+++ b/.github/workflows/cs.yml
@@ -2,12 +2,12 @@ name: CS
 
 on:
   push:
-    branches:
-      - develop
-      - main
-      - 'feature/**'
-      - 'release/**'
-      - 'fix/**'
+    branches-ignore:
+      # These should always correspond to pull requests, so ignore them for
+      # the push trigger and let them be triggered by the pull_request
+      # trigger, avoiding running the workflow twice.  This is a minor
+      # optimization so there's no need to ensure this is comprehensive.
+      - 'dependabot/**'
 
   pull_request:
     branches:
@@ -19,12 +19,16 @@ jobs:
     runs-on: ubuntu-latest
     name: Code linting
 
+    # The maximum number of minutes to let a workflow run
+    # before GitHub automatically cancels it. Default: 360
+    timeout-minutes: 30
+
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3.0.2
+        uses: actions/checkout@v3.3.0
 
       - name: Set up Python 3.10
-        uses: actions/setup-python@v4.0.0
+        uses: actions/setup-python@v4.5.0
         with:
           python-version: '3.10'
 
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index f89231e..e079bee 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -2,13 +2,14 @@ name: Docs
 
 on:
   push:
-    branches:
-      - develop
-      - main
-      - 'feature/**'
-      - 'release/**'
-      - 'fix/**'
-
+    branches-ignore:
+      # These should always correspond to pull requests, so ignore them for
+      # the push trigger and let them be triggered by the pull_request
+      # trigger, avoiding running the workflow twice.  This is a minor
+      # optimization so there's no need to ensure this is comprehensive.
+      - 'dependabot/**'
+
+  # The branches below must be a subset of the branches above
   pull_request:
     branches:
       - develop
@@ -19,12 +20,16 @@ jobs:
     runs-on: ubuntu-latest
     name: Build and test package documentation
 
+    # The maximum number of minutes to let a workflow run
+    # before GitHub automatically cancels it. Default: 360
+    timeout-minutes: 30
+
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3.0.2
+        uses: actions/checkout@v3.3.0
 
       - name: Set up Python 3.10
-        uses: actions/setup-python@v4.0.0
+        uses: actions/setup-python@v4.5.0
         with:
           python-version: '3.10'
 
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 2c3e17d..6b31049 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -5,8 +5,56 @@ 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.10.0`_ - 2-March-2023
+-------------------------------
+Added
++++++
+- Use the core redis library by default if running Django >= 4.0
+  `#356 <https://github.com/joke2k/django-environ/issues/356>`_.
+- Value of dict can now contain an equal sign
+  `#241 <https://github.com/joke2k/django-environ/pull/241>`_.
+- Added support for Python 3.11.
+- Added ``CONN_HEALTH_CHECKS`` to database base options
+  `#413 <https://github.com/joke2k/django-environ/issues/413>`_.
+- Added ``encoding`` parameter to ``read_env`` with default value 'utf8'
+  `#442 <https://github.com/joke2k/django-environ/pull/442>`_.
+- Added support for Django 4.1
+  `#416 <https://github.com/joke2k/django-environ/issues/416>`_.
+
+Deprecated
+++++++++++
+- Support of Python < 3.6 is deprecated and will be removed
+  in next major version.
+
+Changed
++++++++
+- Used UTF-8 as a encoding when open ``.env`` file.
+- Provided access to ```DB_SCHEMES`` through ``cls`` rather than
+  ``Env`` in ``db_url_config``
+  `#414 <https://github.com/joke2k/django-environ/pull/414>`_.
+- Correct CI workflow to use supported Python versions/OS matrix
+  `#441 <https://github.com/joke2k/django-environ/pull/441>`_.
+- Reworked trigger CI workflows strategy
+  `#440 <https://github.com/joke2k/django-environ/pull/440>`_.
+
+Fixed
++++++
+- Fixed logic of ``Env.get_value()`` to skip parsing only when
+  ``default=None``, not for all default values that coerce to ``False``
+  `#404 <https://github.com/joke2k/django-environ/issues/404>`_.
+- Deleted duplicated include in docs/quickstart.rst
+  `#439 <https://github.com/joke2k/django-environ/pull/439>`_.
+
+Removed
++++++++
+- Removed deprecated ``Env.unicode()``.
+- Removed ``environ.register_schemes`` calls and do not modify global
+  ``urllib.parse.urlparse``'s ``uses_*`` variables as this no longer needed
+  `#246 <https://github.com/joke2k/django-environ/pull/246>`_.
+
+
 `v0.9.0`_ - 15-June-2022
-------------------------------
+------------------------
 Added
 +++++
 - Added support for Postgresql cluster URI
@@ -17,19 +65,16 @@ Added
   `#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
@@ -55,7 +100,7 @@ Fixed
 ---------------------------
 Added
 +++++
-- Log invalid lines when parse .env file
+- Log invalid lines when parse ``.env`` file
   `#283 <https://github.com/joke2k/django-environ/pull/283>`_.
 - Added docker-style file variable support
   `#189 <https://github.com/joke2k/django-environ/issues/189>`_.
@@ -67,7 +112,6 @@ Added
 - Added ``pymemcache`` cache backend for Django 3.2+
   `#335 <https://github.com/joke2k/django-environ/pull/335>`_.
 
-
 Fixed
 +++++
 - Keep newline/tab escapes in quoted strings
@@ -191,13 +235,13 @@ Fixed
 +++++
 - Fix Path subtracting.
 
-
 `v0.4.3`_ - 21-August-2017
 --------------------------
 Changed
 +++++++
 - Rollback the default Environ to ``os.environ``.
 
+
 `v0.4.2`_ - 13-April-2017
 -------------------------
 Added
@@ -212,6 +256,7 @@ Changed
 - Update support for ``django-redis`` urls
   `#109 <https://github.com/joke2k/django-environ/pull/109>`_.
 
+
 `v0.4.1`_ - 13-November-2016
 ----------------------------
 Added
@@ -226,6 +271,7 @@ Changed
 - Fixed support for Oracle urls.
 - Fixed support for ``django-redis``.
 
+
 `v0.4`_ - 23-September-2015
 ---------------------------
 Added
@@ -269,12 +315,14 @@ Changed
 +++++++
 - Rewriting README.rst.
 
+
 v0.2.1 - 19-April-2013
 ----------------------
 Changed
 +++++++
 - ``Env.__call__`` now uses ``Env.get_value`` instance method.
 
+
 v0.2 - 16-April-2013
 --------------------
 Added
@@ -285,6 +333,7 @@ Fixed
 +++++
 - Fixed typos in the documentation.
 
+
 v0.1 - 2-April-2013
 -------------------
 Added
@@ -292,6 +341,7 @@ Added
 - Initial release.
 
 
+.. _v0.10.0: https://github.com/joke2k/django-environ/compare/v0.9.0...develop
 .. _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
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
index f8b10d4..29d555e 100644
--- a/CONTRIBUTING.rst
+++ b/CONTRIBUTING.rst
@@ -5,13 +5,74 @@ If you would like to contribute to ``django-environ``, please take a look at the
 `current issues <https://github.com/joke2k/django-environ/issues>`_.  If there is
 a bug or feature that you want but it isn't listed, make an issue and work on it.
 
-How to Contribute
------------------
+Bug reports
+-----------
+
+*Before raising an issue, please ensure that you are using the latest version
+of django-environ.*
+
+Please provide the following information with your issue to enable us to
+respond as quickly as possible.
+
+* The relevant versions of the packages you are using.
+* The steps to recreate your issue.
+* The full stacktrace if there is an exception.
+* An executable code example where possible
+
+Guidelines for bug reports:
+
+* **Use the GitHub issue search** — check if the issue has already been
+  reported.
+* **Check if the issue has been fixed** — try to reproduce it using the latest
+  ``main`` or ``develop`` branch in the repository.
+* Isolate the problem — create a reduced test case and a live example.
+
+A good bug report shouldn't leave others needing to chase you up for more
+information. Please try to be as detailed as possible in your report. What is
+your environment? What steps will reproduce the issue? What OS experience the
+problem? What would you expect to be the outcome? All these details will help
+people to fix any potential bugs.
+
+Feature requests
+----------------
+
+Feature requests are welcome. But take a moment to find out whether your idea
+fits with the scope and aims of the project. It's up to *you* to make a strong
+case to convince the project's developers of the merits of this feature. Please
+provide as much detail and context as possible.
+
+Pull requests
+-------------
+
+Good pull requests - patches, improvements, new features - are a fantastic
+help. They should remain focused in scope and avoid containing unrelated
+commits.
+
+Follow this process if you'd like your work considered for inclusion in the
+project:
 
 1. Check for open issues or open a fresh issue to start a discussion around a
    feature idea or a bug.
-2. Fork `the repository <https://github.com/joke2k/django-environ>`_ on GitHub
-   to start making your changes to the **develop** branch (or branch off of it).
+2. Fork `the repository <https://github.com/joke2k/django-environ>`_
+   on GitHub to start making your changes to the ``develop`` branch
+   (or branch off of it).
 3. Write a test which shows that the bug was fixed or that the feature works as
    expected.
 4. Send a pull request and bug the maintainer until it gets merged and published.
+
+If you are intending to implement a fairly large feature we'd appreciate if you
+open an issue with GitHub detailing your use case and intended solution to
+discuss how it might impact other work that is in flight.
+
+We also appreciate it if you take the time to update and write tests for any
+changes you submit.
+
+**By submitting a patch, you agree to allow the project owner to license your
+work under the same license as that used by the project.**
+
+Resources
+---------
+
+* `How to Contribute to Open Source <https://opensource.guide/how-to-contribute/>`_
+* `Using Pull Requests <https://help.github.com/articles/about-pull-requests/>`_
+* `Writing good commit messages <http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html>`_
diff --git a/README.rst b/README.rst
index 256f0a1..cf999e2 100644
--- a/README.rst
+++ b/README.rst
@@ -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, 3.2 and 4.0.
+Django 1.11, 2.2, 3., 3.1, 3.2, 4.0 and 4.1.
 
 If you'd like to contribute to ``django-environ`` you're most welcome!
 
diff --git a/docs/conf.py b/docs/conf.py
index 9003ad8..765f994 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -126,6 +126,8 @@ linkcheck_ignore = [
 # In nitpick mode (-n), still ignore any of the following "broken" references
 # to non-types.
 nitpick_ignore = [
+    ('py:func', 'str.rfind'),
+    ('py:func', 'str.find'),
 ]
 
 #
diff --git a/docs/deprecations.rst b/docs/deprecations.rst
index 7318911..1f4313d 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -2,11 +2,21 @@
 Deprecations
 ============
 
+Features deprecated in 0.10.0
+=============================
+
+Python
+------
+
+* Support of Python < 3.6 is deprecated and will be removed
+  in next major version.
+
+
 Features deprecated in 0.9.0
 ============================
 
 Methods
 -------
 
-* The :meth:`.environ.Env.unicode` method is deprecated as it was used
+* The ``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/faq.rst b/docs/faq.rst
index 9e23ef6..129caea 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -5,7 +5,7 @@ FAQ
 
 #. **Can django-environ determine the location of .env file automatically?**
 
-   ``django-environ`` will try to get and read ``.env`` file from the project
+   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
@@ -27,5 +27,19 @@ FAQ
 
 #. **Is .env file going to be imported in settings file?**
 
-   No need to import, ``django-environ`` automatically picks variables
+   No need to import, django-environ automatically picks variables
    from there.
+
+#. **Should I commit my .env file?**
+
+   Credentials should only be accessible on the machines that need access to them.
+   Never commit sensitive information to a repository that is not needed by every
+   development machine and server.
+
+#. **Why is it not overriding existing environment variables?**
+
+   By default, django-environ won't overwrite existing environment variables as
+   it assumes the deployment environment has more knowledge about configuration
+   than the application does. To overwrite existing environment variables you can
+   pass ``overwrite=True`` to :meth:`.environ.Env.read_env`. For more see
+   ":ref:`overwriting-existing-env`"
diff --git a/docs/install.rst b/docs/install.rst
index a70ca1a..3dacca9 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -12,7 +12,7 @@ Requirements
 Installing django-environ
 =========================
 
-``django-environ`` is a Python-only package `hosted_on_pypi`_.
+django-environ is a Python-only package `hosted_on_pypi`_.
 The recommended installation method is `pip`_-installing into a
 :mod:`virtualenv <python:venv>`:
 
@@ -22,7 +22,7 @@ The recommended installation method is `pip`_-installing into a
 
 .. note::
 
-   After installing ``django-environ``, no need to add it to ``INSTALLED_APPS``.
+   After installing django-environ, no need to add it to ``INSTALLED_APPS``.
 
 
 .. _hosted_on_pypi: https://pypi.org/project/django-environ/
@@ -43,7 +43,7 @@ experience might be not as smooth:
    # 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
+This command will download the latest version of django-environ and install
 it to your system.
 
 .. note::
diff --git a/docs/license.rst b/docs/license.rst
index feb5b5d..c67de79 100644
--- a/docs/license.rst
+++ b/docs/license.rst
@@ -2,7 +2,7 @@
 License and Credits
 ===================
 
-``django-environ`` is open source software licensed under the
+django-environ is open source software licensed under the
 `MIT / X11 License <https://choosealicense.com/licenses/mit/>`_.
 The full license text can be also found in the `source code repository <https://github.com/joke2k/django-environ/blob/main/LICENSE.txt>`_.
 
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index f5c5b20..6247383 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -2,10 +2,6 @@
 Quick Start
 ===========
 
-.. include:: ../README.rst
-   :start-after: -code-begin-
-   :end-before: -overview-
-
 Usage
 =====
 
diff --git a/docs/tips.rst b/docs/tips.rst
index a8684eb..1915fac 100644
--- a/docs/tips.rst
+++ b/docs/tips.rst
@@ -55,7 +55,7 @@ See https://perishablepress.com/stop-using-unsafe-characters-in-urls/ for refere
 Smart Casting
 =============
 
-``django-environ`` has a "Smart-casting" enabled by default, if you don't provide a ``cast`` type, it will be detected from ``default`` type.
+django-environ has a "Smart-casting" enabled by default, if you don't provide a ``cast`` type, it will be detected from ``default`` type.
 This could raise side effects (see `#192 <https://github.com/joke2k/django-environ/issues/192>`_).
 To disable it use ``env.smart_cast = False``.
 
@@ -317,6 +317,8 @@ file from the filesystem:
    env.read_env(pathlib.Path(str(BASE_DIR)) / '.env')
 
 
+.. _overwriting-existing-env:
+
 Overwriting existing environment values from env files
 ------------------------------------------------------
 
diff --git a/environ/__init__.py b/environ/__init__.py
index 72a30ba..c79736c 100644
--- a/environ/__init__.py
+++ b/environ/__init__.py
@@ -18,10 +18,10 @@ for details on the use of this package.
 from .environ import *
 
 
-__copyright__ = 'Copyright (C) 2021 Daniele Faraglia'
+__copyright__ = 'Copyright (C) 2013-2022 Daniele Faraglia'
 """The copyright notice of the package."""
 
-__version__ = '0.9.0'
+__version__ = '0.10.0'
 """The version of the package."""
 
 __license__ = 'MIT'
@@ -42,5 +42,6 @@ __maintainer_email__ = 'egrep@protonmail.ch'
 __url__ = 'https://django-environ.readthedocs.org'
 """The URL of the package."""
 
+# pylint: disable=line-too-long
 __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 a4ae366..0cec1e5 100644
--- a/environ/compat.py
+++ b/environ/compat.py
@@ -23,20 +23,27 @@ else:
     DJANGO_VERSION = None
 
     class ImproperlyConfigured(Exception):
-        pass
+        """Django is somehow improperly configured"""
 
-# back compatibility with django postgresql package
-if DJANGO_VERSION is not None and DJANGO_VERSION < (2, 0):
-    DJANGO_POSTGRES = 'django.db.backends.postgresql_psycopg2'
-else:
-    # https://docs.djangoproject.com/en/2.0/releases/2.0/#id1
-    DJANGO_POSTGRES = 'django.db.backends.postgresql'
 
-# back compatibility with redis_cache package
-if find_loader('redis_cache'):
-    REDIS_DRIVER = 'redis_cache.RedisCache'
-else:
-    REDIS_DRIVER = 'django_redis.cache.RedisCache'
+def choose_rediscache_driver():
+    """Backward compatibility for RedisCache driver."""
+    # use built-in support if Django 4+
+    if DJANGO_VERSION is not None and DJANGO_VERSION >= (4, 0):
+        return 'django.core.cache.backends.redis.RedisCache'
+
+    # back compatibility with redis_cache package
+    if find_loader('redis_cache'):
+        return 'redis_cache.RedisCache'
+    return 'django_redis.cache.RedisCache'
+
+
+def choose_postgres_driver():
+    """Backward compatibility for postgresql driver."""
+    old_django = DJANGO_VERSION is not None and DJANGO_VERSION < (2, 0)
+    if old_django:
+        return 'django.db.backends.postgresql_psycopg2'
+    return 'django.db.backends.postgresql'
 
 
 def choose_pymemcache_driver():
@@ -49,4 +56,11 @@ def choose_pymemcache_driver():
     return 'django.core.cache.backends.memcached.PyMemcacheCache'
 
 
+REDIS_DRIVER = choose_rediscache_driver()
+"""The name of the RedisCache driver."""
+
+DJANGO_POSTGRES = choose_postgres_driver()
+"""The name of the PostgreSQL driver."""
+
 PYMEMCACHE_DRIVER = choose_pymemcache_driver()
+"""The name of the Pymemcache driver."""
diff --git a/environ/environ.py b/environ/environ.py
index fc5eebb..9ca00a8 100644
--- a/environ/environ.py
+++ b/environ/environ.py
@@ -17,7 +17,6 @@ import logging
 import os
 import re
 import sys
-import urllib.parse as urlparselib
 import warnings
 from urllib.parse import (
     parse_qs,
@@ -67,6 +66,7 @@ def _cast_urlstr(v):
 
 
 class NoValue:
+    """Represent of no value object."""
 
     def __repr__(self):
         return '<{}>'.format(self.__class__.__name__)
@@ -134,6 +134,7 @@ class Env:
         'ATOMIC_REQUESTS',
         'AUTOCOMMIT',
         'DISABLE_SERVER_SIDE_CURSORS',
+        'CONN_HEALTH_CHECKS',
     ]
 
     DEFAULT_CACHE_ENV = 'CACHE_URL'
@@ -204,8 +205,6 @@ class Env:
     def __contains__(self, var):
         return var in self.ENVIRON
 
-    # Shortcuts
-
     def str(self, var, default=NOTSET, multiline=False):
         """
         :rtype: str
@@ -215,21 +214,6 @@ class Env:
             return re.sub(r'(\\r)?\\n', r'\n', value)
         return value
 
-    def unicode(self, var, default=NOTSET):
-        """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'):
         """
         :rtype: bytes
@@ -373,9 +357,9 @@ class Env:
         :rtype: typing.IO[typing.Any]
         """
 
-        logger.debug("get '{}' casted as '{}' with default '{}'".format(
-            var, cast, default
-        ))
+        logger.debug(
+            "get '%s' casted as '%s' with default '%s'",
+            var, cast, default)
 
         var_name = "{}{}".format(self.prefix, var)
         if var_name in self.scheme:
@@ -426,13 +410,11 @@ class Env:
 
         value = None if default is None and value == '' else value
 
-        if value != default or (parse_default and value):
+        if value != default or (parse_default and value is not None):
             value = self.parse_value(value, cast)
 
         return value
 
-    # Class and static methods
-
     @classmethod
     def parse_value(cls, value, cast):
         """Parse and cast provided value
@@ -444,7 +426,7 @@ class Env:
         """
         if cast is None:
             return value
-        elif cast is bool:
+        if cast is bool:
             try:
                 value = int(value) != 0
             except ValueError:
@@ -457,7 +439,7 @@ class Env:
         elif isinstance(cast, dict):
             key_cast = cast.get('key', str)
             value_cast = cast.get('value', str)
-            value_cast_by_key = cast.get('cast', dict())
+            value_cast_by_key = cast.get('cast', {})
             value = dict(map(
                 lambda kv: (
                     key_cast(kv[0]),
@@ -469,11 +451,12 @@ class Env:
                 [val.split('=') for val in value.split(';') if val]
             ))
         elif cast is dict:
-            value = dict([val.split('=') for val in value.split(',') if val])
+            value = dict([v.split('=', 1) for v in value.split(',') if v])
         elif cast is list:
             value = [x for x in value.split(',') if x]
         elif cast is tuple:
             val = value.strip('(').strip(')').split(',')
+            # pylint: disable=consider-using-generator
             value = tuple([x for x in val if x])
         elif cast is float:
             # clean string
@@ -590,7 +573,7 @@ class Env:
         if url.scheme == 'oracle':
             # Django oracle/base.py strips port and fails on non-string value
             if not config['PORT']:
-                del (config['PORT'])
+                del config['PORT']
             else:
                 config['PORT'] = str(config['PORT'])
 
@@ -608,8 +591,8 @@ class Env:
         else:
             config['ENGINE'] = url.scheme
 
-        if config['ENGINE'] in Env.DB_SCHEMES:
-            config['ENGINE'] = Env.DB_SCHEMES[config['ENGINE']]
+        if config['ENGINE'] in cls.DB_SCHEMES:
+            config['ENGINE'] = cls.DB_SCHEMES[config['ENGINE']]
 
         if not config.get('ENGINE', False):
             warnings.warn("Engine not recognized from url: {}".format(config))
@@ -631,8 +614,7 @@ class Env:
         if not isinstance(url, cls.URL_CLASS):
             if not url:
                 return {}
-            else:
-                url = urlparse(url)
+            url = urlparse(url)
 
         if url.scheme not in cls.CACHE_SCHEMES:
             raise ImproperlyConfigured(
@@ -776,15 +758,15 @@ class Env:
         params = {}  # type: dict
         if url.query:
             params = parse_qs(url.query)
-            if 'EXCLUDED_INDEXES' in params.keys():
+            if 'EXCLUDED_INDEXES' in params:
                 config['EXCLUDED_INDEXES'] \
                     = params['EXCLUDED_INDEXES'][0].split(',')
-            if 'INCLUDE_SPELLING' in params.keys():
+            if 'INCLUDE_SPELLING' in params:
                 config['INCLUDE_SPELLING'] = cls.parse_value(
                     params['INCLUDE_SPELLING'][0],
                     bool
                 )
-            if 'BATCH_SIZE' in params.keys():
+            if 'BATCH_SIZE' in params:
                 config['BATCH_SIZE'] = cls.parse_value(
                     params['BATCH_SIZE'][0],
                     int
@@ -792,8 +774,8 @@ class Env:
 
         if url.scheme == 'simple':
             return config
-        elif url.scheme in ['solr'] + cls.ELASTICSEARCH_FAMILY:
-            if 'KWARGS' in params.keys():
+        if url.scheme in ['solr'] + cls.ELASTICSEARCH_FAMILY:
+            if 'KWARGS' in params:
                 config['KWARGS'] = params['KWARGS'][0]
 
         # remove trailing slash
@@ -804,7 +786,7 @@ class Env:
             config['URL'] = urlunparse(
                 ('http',) + url[1:2] + (path,) + ('', '', '')
             )
-            if 'TIMEOUT' in params.keys():
+            if 'TIMEOUT' in params:
                 config['TIMEOUT'] = cls.parse_value(params['TIMEOUT'][0], int)
             return config
 
@@ -821,7 +803,7 @@ class Env:
             config['URL'] = urlunparse(
                 ('http',) + url[1:2] + (path,) + ('', '', '')
             )
-            if 'TIMEOUT' in params.keys():
+            if 'TIMEOUT' in params:
                 config['TIMEOUT'] = cls.parse_value(params['TIMEOUT'][0], int)
             config['INDEX_NAME'] = index
             return config
@@ -829,15 +811,15 @@ class Env:
         config['PATH'] = '/' + path
 
         if url.scheme == 'whoosh':
-            if 'STORAGE' in params.keys():
+            if 'STORAGE' in params:
                 config['STORAGE'] = params['STORAGE'][0]
-            if 'POST_LIMIT' in params.keys():
+            if 'POST_LIMIT' in params:
                 config['POST_LIMIT'] = cls.parse_value(
                     params['POST_LIMIT'][0],
                     int
                 )
         elif url.scheme == 'xapian':
-            if 'FLAGS' in params.keys():
+            if 'FLAGS' in params:
                 config['FLAGS'] = params['FLAGS'][0]
 
         if engine:
@@ -846,7 +828,8 @@ class Env:
         return config
 
     @classmethod
-    def read_env(cls, env_file=None, overwrite=False, **overrides):
+    def read_env(cls, env_file=None, overwrite=False, encoding='utf8',
+                 **overrides):
         r"""Read a .env file into os.environ.
 
         If not given a path to a dotenv path, does filthy magic stack
@@ -866,11 +849,13 @@ class Env:
             the Django settings module from the Django project root.
         :param overwrite: ``overwrite=True`` will force an overwrite of
             existing environment variables.
+        :param encoding: The encoding to use when reading the environment file.
         :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.
         """
         if env_file is None:
+            # pylint: disable=protected-access
             frame = sys._getframe()
             env_file = os.path.join(
                 os.path.dirname(frame.f_back.f_code.co_filename),
@@ -879,13 +864,13 @@ class Env:
             if not os.path.exists(env_file):
                 logger.info(
                     "%s doesn't exist - if you're not configuring your "
-                    "environment separately, create one." % env_file)
+                    "environment separately, create one.", env_file)
                 return
 
         try:
             if isinstance(env_file, Openable):
                 # Python 3.5 support (wrap path with str).
-                with open(str(env_file)) as f:
+                with open(str(env_file), encoding=encoding) as f:
                     content = f.read()
             else:
                 with env_file as f:
@@ -893,10 +878,10 @@ class Env:
         except OSError:
             logger.info(
                 "%s not found - if you're not configuring your "
-                "environment separately, check this." % env_file)
+                "environment separately, check this.", env_file)
             return
 
-        logger.debug('Read environment variables from: {}'.format(env_file))
+        logger.debug('Read environment variables from: %s', env_file)
 
         def _keep_escaped_format_characters(match):
             """Keep escaped newline/tabs in quoted strings"""
@@ -976,6 +961,7 @@ class Path:
         :param \**kwargs: ``**kwargs`` passed to :py:func:`open`
         :rtype: typing.IO[typing.Any]
         """
+        # pylint: disable=unspecified-encoding
         return open(self(name), *args, **kwargs)
 
     @property
@@ -983,6 +969,7 @@ class Path:
         """Current directory for this Path"""
         return self.__root__
 
+    # pylint: disable=keyword-arg-before-vararg
     def __init__(self, start='', *paths, **kwargs):
 
         super().__init__()
@@ -1016,9 +1003,9 @@ class Path:
     def __sub__(self, other):
         if isinstance(other, int):
             return self.path('../' * other)
-        elif isinstance(other, str):
-            if self.__root__.endswith(other):
-                return Path(self.__root__.rstrip(other))
+        if isinstance(other, str) and self.__root__.endswith(other):
+            return Path(self.__root__.rstrip(other))
+
         raise TypeError(
             "unsupported operand type(s) for -: '{self}' and '{other}' "
             "unless value of {self} ends with value of {other}".format(
@@ -1051,10 +1038,12 @@ class Path:
         return self.__str__()
 
     def rfind(self, *args, **kwargs):
-        return self.__str__().rfind(*args, **kwargs)
+        """Proxy method to :py:func:`str.rfind`"""
+        return str(self).rfind(*args, **kwargs)
 
     def find(self, *args, **kwargs):
-        return self.__str__().find(*args, **kwargs)
+        """Proxy method to :py:func:`str.find`"""
+        return str(self).find(*args, **kwargs)
 
     @staticmethod
     def _absolute_join(base, *paths, **kwargs):
@@ -1063,21 +1052,3 @@ class Path:
             raise ImproperlyConfigured(
                 "Create required path: {}".format(absolute_path))
         return absolute_path
-
-
-def register_scheme(scheme):
-    for method in dir(urlparselib):
-        if method.startswith('uses_'):
-            getattr(urlparselib, method).append(scheme)
-
-
-def register_schemes(schemes):
-    for scheme in schemes:
-        register_scheme(scheme)
-
-
-# Register database and cache schemes in URLs.
-register_schemes(Env.DB_SCHEMES.keys())
-register_schemes(Env.CACHE_SCHEMES.keys())
-register_schemes(Env.SEARCH_SCHEMES.keys())
-register_schemes(Env.EMAIL_SCHEMES.keys())
diff --git a/environ/fileaware_mapping.py b/environ/fileaware_mapping.py
index 6da95f5..0578945 100644
--- a/environ/fileaware_mapping.py
+++ b/environ/fileaware_mapping.py
@@ -45,7 +45,7 @@ class FileAwareMapping(MutableMapping):
             return self.files_cache[key]
         key_file = self.env.get(key + "_FILE")
         if key_file:
-            with open(key_file) as f:
+            with open(key_file, encoding='utf-8') as f:
                 value = f.read()
             if self.cache:
                 self.files_cache[key] = value
diff --git a/setup.py b/setup.py
index 4de0bec..d317bc0 100644
--- a/setup.py
+++ b/setup.py
@@ -10,11 +10,21 @@
 
 import codecs
 import re
+import sys
+import warnings
 from os import path
 
 from setuptools import find_packages, setup
 
 
+if sys.version_info < (3, 6):
+    warnings.warn(
+        "Support of Python < 3.6 is deprecated"
+        "and will be removed in a future release.",
+        DeprecationWarning
+    )
+
+
 def read_file(filepath):
     """Read content from a UTF-8 encoded text file."""
     with codecs.open(filepath, 'rb', 'utf-8') as file_handle:
@@ -131,6 +141,7 @@ CLASSIFIERS = [
     'Framework :: Django :: 3.1',
     'Framework :: Django :: 3.2',
     'Framework :: Django :: 4.0',
+    'Framework :: Django :: 4.1',
 
     'Operating System :: OS Independent',
 
@@ -139,13 +150,13 @@ CLASSIFIERS = [
 
     'Programming Language :: Python',
     'Programming Language :: Python :: 3',
-    'Programming Language :: Python :: 3.4',
     'Programming Language :: Python :: 3.5',
     'Programming Language :: Python :: 3.6',
     'Programming Language :: Python :: 3.7',
     'Programming Language :: Python :: 3.8',
     'Programming Language :: Python :: 3.9',
     'Programming Language :: Python :: 3.10',
+    'Programming Language :: Python :: 3.11',
     'Programming Language :: Python :: Implementation :: CPython',
     'Programming Language :: Python :: Implementation :: PyPy',
 
@@ -217,7 +228,7 @@ if __name__ == '__main__':
         platforms=['any'],
         include_package_data=True,
         zip_safe=False,
-        python_requires='>=3.4,<4',
+        python_requires='>=3.5,<4',
         install_requires=INSTALL_REQUIRES,
         dependency_links=DEPENDENCY_LINKS,
         extras_require=EXTRAS_REQUIRE,
diff --git a/tox.ini b/tox.ini
index 73e6a6b..6251d4a 100644
--- a/tox.ini
+++ b/tox.ini
@@ -18,9 +18,9 @@ envlist =
     docs
     lint
     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}
+    py{35,36,37,38,39,310,311}-django{111,22}
+    py{36,37,38,39,310,311}-django{30,31,32}
+    py{38,39,310,311}-django{40,41}
     pypy-django{111,22,30,31,32}
 
 [gh-actions]
@@ -31,6 +31,7 @@ python =
     3.8: py38
     3.9: py39
     3.10: py310
+    3.11: py311
     pypy-3.7: pypy
 
 [testenv]
@@ -43,6 +44,7 @@ deps =
     django31: Django>=3.1,<3.2
     django32: Django>=3.2,<3.3
     django40: Django>=4.0,<4.1
+    django41: Django>=4.1,<4.2
 commands_pre =
     python -m pip install --upgrade pip
     python -m pip install .
@@ -67,10 +69,26 @@ deps =
     flake8
     flake8-blind-except
     flake8-import-order
+    pylint
 commands_pre =
     python -m pip install --upgrade pip
     python -m pip install .
-commands = flake8 environ setup.py
+commands =
+    flake8 environ setup.py
+    # Format ("f") strings have not been introduced before Python 3.6,
+    # thus disable "consider-using-f-string" at this moment.
+    pylint \
+        --logging-format-style=old \
+        --good-names-rgxs=m[0-9],f,v \
+        --disable=too-few-public-methods \
+        --disable=import-error \
+        --disable=unused-import \
+        --disable=consider-using-f-string \
+        --disable=too-many-locals \
+        --disable=too-many-branches \
+        --disable=too-many-public-methods \
+        --disable=too-many-lines \
+        environ
 
 [testenv:linkcheck]
 description = Check external links in the package documentation


Test patch:
diff --git a/tests/fixtures.py b/tests/fixtures.py
index c7f8a0b..6990d9a 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -25,6 +25,7 @@ class FakeEnv:
     EMAIL = 'smtps://user@domain.com:password@smtp.example.com:587'
     JSON = dict(one='bar', two=2, three=33.44)
     DICT = dict(foo='bar', test='on')
+    DICT_WITH_EQ = dict(key1='sub_key1=sub_value1', key2='value2')
     PATH = '/home/dev'
     EXPORTED = 'exported var'
     SAML_ATTRIBUTE_MAPPING = dict(
@@ -67,6 +68,7 @@ class FakeEnv:
                     STR_LIST_WITH_SPACES=' foo,  bar',
                     EMPTY_LIST='',
                     DICT_VAR='foo=bar,test=on',
+                    DICT_WITH_EQ_VAR='key1=sub_key1=sub_value1,key2=value2',
                     DATABASE_URL=cls.POSTGRES,
                     DATABASE_MYSQL_URL=cls.MYSQL,
                     DATABASE_MYSQL_GIS_URL=cls.MYSQLGIS,
diff --git a/tests/test_cache.py b/tests/test_cache.py
index 132266e..a762c1b 100644
--- a/tests/test_cache.py
+++ b/tests/test_cache.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,7 +12,11 @@ import pytest
 
 import environ.compat
 from environ import Env
-from environ.compat import PYMEMCACHE_DRIVER, REDIS_DRIVER, ImproperlyConfigured
+from environ.compat import (
+    ImproperlyConfigured,
+    PYMEMCACHE_DRIVER,
+    REDIS_DRIVER,
+)
 
 
 def test_base_options_parsing():
@@ -54,7 +58,7 @@ def test_base_options_parsing():
         ('rediscache://host1:6379,host2:6379,host3:9999/1', REDIS_DRIVER,
          ['redis://host1:6379/1', 'redis://host2:6379/1',
           'redis://host3:9999/1']),
-        ('rediscache:///path/to/socket:1', 'django_redis.cache.RedisCache',
+        ('rediscache:///path/to/socket:1', REDIS_DRIVER,
          'unix:///path/to/socket:1'),
         ('memcache:///tmp/memcached.sock',
          'django.core.cache.backends.memcached.MemcachedCache',
@@ -112,6 +116,23 @@ def test_pymemcache_compat(django_version, pymemcache_installed):
                 assert driver == new if pymemcache_installed else old
 
 
+@pytest.mark.parametrize('django_version', ((4, 0), (3, 2), None))
+@pytest.mark.parametrize('redis_cache_installed', (True, False))
+def test_rediscache_compat(django_version, redis_cache_installed):
+    django_new = 'django.core.cache.backends.redis.RedisCache'
+    redis_cache = 'redis_cache.RedisCache'
+    django_old = 'django_redis.cache.RedisCache'
+
+    with mock.patch.object(environ.compat, 'DJANGO_VERSION', django_version):
+        with mock.patch('environ.compat.find_loader') as mock_find_loader:
+            mock_find_loader.return_value = redis_cache_installed
+            driver = environ.compat.choose_rediscache_driver()
+            if django_version and django_version >= (4, 0):
+                assert driver == django_new
+            else:
+                assert driver == redis_cache if redis_cache_installed else django_old
+
+
 def test_redis_parsing():
     url = ('rediscache://127.0.0.1:6379/1?client_class='
            'django_redis.client.DefaultClient&password=secret')
@@ -186,11 +207,11 @@ def test_cache_url_password_using_sub_delims(monkeypatch, chars):
     env = Env()
 
     result = env.cache()
-    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
+    assert result['BACKEND'] == REDIS_DRIVER
     assert result['LOCATION'] == url
 
     result = env.cache_url_config(url)
-    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
+    assert result['BACKEND'] == REDIS_DRIVER
     assert result['LOCATION'] == url
 
     url = 'rediss://enigma:sec{}ret@ondigitalocean.com:25061/2'.format(chars)
@@ -198,11 +219,11 @@ def test_cache_url_password_using_sub_delims(monkeypatch, chars):
     env = Env()
 
     result = env.cache()
-    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
+    assert result['BACKEND'] == REDIS_DRIVER
     assert result['LOCATION'] == url
 
     result = env.cache_url_config(url)
-    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
+    assert result['BACKEND'] == REDIS_DRIVER
     assert result['LOCATION'] == url
 
     url = 'rediss://enigma:{}secret@ondigitalocean.com:25061/2'.format(chars)
@@ -210,11 +231,11 @@ def test_cache_url_password_using_sub_delims(monkeypatch, chars):
     env = Env()
 
     result = env.cache()
-    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
+    assert result['BACKEND'] == REDIS_DRIVER
     assert result['LOCATION'] == url
 
     result = env.cache_url_config(url)
-    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
+    assert result['BACKEND'] == REDIS_DRIVER
     assert result['LOCATION'] == url
 
 
@@ -230,7 +251,7 @@ def test_cache_url_password_using_gen_delims(monkeypatch, chars):
     env = Env()
 
     result = env.cache()
-    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
+    assert result['BACKEND'] == REDIS_DRIVER
     assert result['LOCATION'] == url
 
     url = 'rediss://enigma:sec{}ret@ondigitalocean.com:25061/2'.format(chars)
@@ -238,7 +259,7 @@ def test_cache_url_password_using_gen_delims(monkeypatch, chars):
     env = Env()
 
     result = env.cache()
-    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
+    assert result['BACKEND'] == REDIS_DRIVER
     assert result['LOCATION'] == url
 
     url = 'rediss://enigma:{}secret@ondigitalocean.com:25061/2'.format(chars)
@@ -246,7 +267,7 @@ def test_cache_url_password_using_gen_delims(monkeypatch, chars):
     env = Env()
 
     result = env.cache()
-    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
+    assert result['BACKEND'] == REDIS_DRIVER
     assert result['LOCATION'] == url
 
 
diff --git a/tests/test_db.py b/tests/test_db.py
index 20d807f..c407413 100644
--- a/tests/test_db.py
+++ b/tests/test_db.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
@@ -168,7 +168,6 @@ def test_db_parsing(url, engine, name, host, user, passwd, port):
 
     if host == 'reconnect.com':
         assert config['OPTIONS'] == {'reconnect': 'true'}
-        
 
 
 def test_postgres_complex_db_name_parsing():
diff --git a/tests/test_env.py b/tests/test_env.py
index b5c1e39..396c212 100644
--- a/tests/test_env.py
+++ b/tests/test_env.py
@@ -8,12 +8,15 @@
 
 import os
 from urllib.parse import quote
-from warnings import catch_warnings
 
 import pytest
 
 from environ import Env, Path
-from environ.compat import ImproperlyConfigured, DJANGO_POSTGRES
+from environ.compat import (
+    DJANGO_POSTGRES,
+    ImproperlyConfigured,
+    REDIS_DRIVER,
+)
 from .asserts import assert_type_and_value
 from .fixtures import FakeEnv
 
@@ -71,21 +74,6 @@ 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',
         [
@@ -168,12 +156,23 @@ class TestEnv:
 
     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))
+        assert_type_and_value(
+            tuple,
+            (42, 'Test'),
+            self.env(
+                'MIX_TUPLE',
+                default=(0, ''),
+                cast=lambda t: tuple(
+                    map(
+                        lambda v: int(v) if v.isdigit() else v.strip(),
+                        [c for c in t.strip('()').split(',')]
+                    )
+                ),
+            )
+        )
 
     def test_str_list_with_spaces(self):
         assert_type_and_value(list, [' foo', '  bar'],
@@ -186,6 +185,7 @@ class TestEnv:
 
     def test_dict_value(self):
         assert_type_and_value(dict, FakeEnv.DICT, self.env.dict('DICT_VAR'))
+        assert_type_and_value(dict, FakeEnv.DICT_WITH_EQ, self.env.dict('DICT_WITH_EQ_VAR'))
 
     def test_complex_dict_value(self):
         assert_type_and_value(
@@ -227,6 +227,13 @@ class TestEnv:
         assert url.geturl() == FakeEnv.URL
         assert self.env.url('OTHER_URL', default=None) is None
 
+    def test_url_empty_string_default_value(self):
+        unset_var_name = 'VARIABLE_NOT_SET_IN_ENVIRONMENT'
+        assert unset_var_name not in os.environ
+        url = self.env.url(unset_var_name, '')
+        assert url.__class__ == self.env.URL_CLASS
+        assert url.geturl() == ''
+
     def test_url_encoded_parts(self):
         password_with_unquoted_characters = "#password"
         encoded_url = "mysql://user:%s@127.0.0.1:3306/dbname" % quote(
@@ -291,7 +298,7 @@ class TestEnv:
             (Env.DEFAULT_CACHE_ENV,
              'django.core.cache.backends.memcached.MemcachedCache',
              '127.0.0.1:11211', None),
-            ('CACHE_REDIS', 'django_redis.cache.RedisCache',
+            ('CACHE_REDIS', REDIS_DRIVER,
              'redis://127.0.0.1:6379/1',
              {'CLIENT_CLASS': 'django_redis.client.DefaultClient',
               'PASSWORD': 'secret'}),
diff --git a/tests/test_env.txt b/tests/test_env.txt
index 69dc84e..237489b 100644
--- a/tests/test_env.txt
+++ b/tests/test_env.txt
@@ -1,4 +1,5 @@
 DICT_VAR=foo=bar,test=on
+DICT_WITH_EQ_VAR=key1=sub_key1=sub_value1,key2=value2
 
 # Database variables
 DATABASE_MYSQL_URL=mysql://bea6eb0:69772142@us-cdbr-east.cleardb.com/heroku_97681?reconnect=true
diff --git a/tests/test_fileaware.py b/tests/test_fileaware.py
index b4733bc..f411b7d 100644
--- a/tests/test_fileaware.py
+++ b/tests/test_fileaware.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
@@ -10,9 +10,10 @@ import os
 import tempfile
 from contextlib import contextmanager
 
-import environ
 import pytest
 
+import environ
+
 
 @contextmanager
 def make_temp_file(text):
diff --git a/tests/test_path.py b/tests/test_path.py
index ed33a7c..b97f4ca 100644
--- a/tests/test_path.py
+++ b/tests/test_path.py
@@ -1,15 +1,17 @@
 # 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.
 
 import os
-import pytest
 import sys
 
+import pytest
+
+
 from environ import Path
 from environ.compat import ImproperlyConfigured
 
@@ -71,6 +73,7 @@ def test_comparison(volume):
 
     assert Path('/home') != '/usr'
 
+
 def test_sum():
     """Make sure Path correct handle __add__."""
     assert Path('/') + 'home' == Path('/home')
diff --git a/tests/test_utils.py b/tests/test_utils.py
index f32d7cc..2f3d32e 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,12 +1,13 @@
 # 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.
 
 import pytest
+
 from environ.environ import _cast, _cast_urlstr
 
 
@@ -21,6 +22,7 @@ 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",
     [