Repository: joke2k/django-environ
Pull request #418 (2022-09-16 11:31:16)

Add CONN_HEALTH_CHECKS to _DB_BASE_OPTIONS
https://docs.djangoproject.com/en/4.1/ref/settings/#conn-health-checks


Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index de37adc..c2769ef 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -11,7 +11,6 @@ on:
     tags:
       - 'v[0-9]+.[0-9]+.[0-9]+'
 
-  # The branches below must be a subset of the branches above
   pull_request:
     branches:
       - develop
@@ -22,10 +21,6 @@ jobs:
     name: Build and test package distribution
     runs-on: ${{ matrix.os }}
 
-    # The maximum number of minutes to let a workflow run
-    # before GitHub automatically cancels it. Default: 360
-    timeout-minutes: 30
-
     strategy:
       matrix:
         os: [ ubuntu-latest, macos-latest, windows-latest ]
@@ -35,7 +30,7 @@ jobs:
         uses: actions/checkout@v3.0.2
 
       - name: Set up Python 3.10
-        uses: actions/setup-python@v4.1.0
+        uses: actions/setup-python@v4.0.0
         with:
           python-version: '3.10'
 
@@ -75,7 +70,7 @@ jobs:
         uses: actions/checkout@v3.0.2
 
       - name: Set up Python 3.10
-        uses: actions/setup-python@v4.1.0
+        uses: actions/setup-python@v4.0.0
         with:
           python-version: '3.10'
 
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index d182c15..7c5d59f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,7 +9,6 @@ on:
       - 'release/**'
       - 'fix/**'
 
-  # The branches below must be a subset of the branches above
   pull_request:
     branches:
       - develop
@@ -53,7 +52,6 @@ jobs:
           - '3.8'
           - '3.9'
           - '3.10'
-          - '3.11.0-beta.3 - 3.11.0'
           - 'pypy-3.7'
         os: [ ubuntu-latest, macos-latest, windows-latest ]
 
@@ -64,7 +62,7 @@ jobs:
           fetch-depth: 5
 
       - name: Set up Python ${{ matrix.python }}
-        uses: actions/setup-python@v4.1.0
+        uses: actions/setup-python@v4.0.0
         with:
           python-version: ${{ matrix.python }}
 
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
deleted file mode 100644
index e40849a..0000000
--- a/.github/workflows/codeql.yml
+++ /dev/null
@@ -1,60 +0,0 @@
-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
-
-    # 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 1268e24..bf4e81f 100644
--- a/.github/workflows/cs.yml
+++ b/.github/workflows/cs.yml
@@ -19,16 +19,12 @@ 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
 
       - name: Set up Python 3.10
-        uses: actions/setup-python@v4.1.0
+        uses: actions/setup-python@v4.0.0
         with:
           python-version: '3.10'
 
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 5661906..f89231e 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -9,7 +9,6 @@ on:
       - 'release/**'
       - 'fix/**'
 
-  # The branches below must be a subset of the branches above
   pull_request:
     branches:
       - develop
@@ -20,16 +19,12 @@ 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
 
       - name: Set up Python 3.10
-        uses: actions/setup-python@v4.1.0
+        uses: actions/setup-python@v4.0.0
         with:
           python-version: '3.10'
 
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 53da79b..2c3e17d 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -5,41 +5,8 @@ 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`_ - 00-Unreleased-2022
--------------------------------
-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
-
-Deprecated
-++++++++++
-- Support of Python < 3.6 is deprecated and will be removed
-  in next major version.
-
-Changed
-+++++++
-- Use UTF-8 as a encoding when open ``.env`` file.
-
-Fixed
-+++++
-- Fix 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>`_.
-
-Removed
-+++++++
-- Removed deprecated ``Env.unicode()``.
-- Remove ``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
@@ -50,16 +17,19 @@ 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
@@ -85,7 +55,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>`_.
@@ -97,6 +67,7 @@ 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
@@ -220,13 +191,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
@@ -241,7 +212,6 @@ Changed
 - Update support for ``django-redis`` urls
   `#109 <https://github.com/joke2k/django-environ/pull/109>`_.
 
-
 `v0.4.1`_ - 13-November-2016
 ----------------------------
 Added
@@ -256,7 +226,6 @@ Changed
 - Fixed support for Oracle urls.
 - Fixed support for ``django-redis``.
 
-
 `v0.4`_ - 23-September-2015
 ---------------------------
 Added
@@ -300,14 +269,12 @@ 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
@@ -318,7 +285,6 @@ Fixed
 +++++
 - Fixed typos in the documentation.
 
-
 v0.1 - 2-April-2013
 -------------------
 Added
@@ -326,7 +292,6 @@ 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/docs/conf.py b/docs/conf.py
index 765f994..9003ad8 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -126,8 +126,6 @@ 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 1f4313d..7318911 100644
--- a/docs/deprecations.rst
+++ b/docs/deprecations.rst
@@ -2,21 +2,11 @@
 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 ``environ.Env.unicode`` method is deprecated as it was used
+* 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/faq.rst b/docs/faq.rst
index 129caea..9e23ef6 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,19 +27,5 @@ 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 3dacca9..a70ca1a 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 c67de79..feb5b5d 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/tips.rst b/docs/tips.rst
index 1915fac..a8684eb 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,8 +317,6 @@ 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 c79736c..72a30ba 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) 2013-2022 Daniele Faraglia'
+__copyright__ = 'Copyright (C) 2021 Daniele Faraglia'
 """The copyright notice of the package."""
 
-__version__ = '0.10.0'
+__version__ = '0.9.0'
 """The version of the package."""
 
 __license__ = 'MIT'
@@ -42,6 +42,5 @@ __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 0cec1e5..a4ae366 100644
--- a/environ/compat.py
+++ b/environ/compat.py
@@ -23,27 +23,20 @@ else:
     DJANGO_VERSION = None
 
     class ImproperlyConfigured(Exception):
-        """Django is somehow improperly configured"""
-
-
-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'
+        pass
 
+# 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'
 
-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'
+# 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_pymemcache_driver():
@@ -56,11 +49,4 @@ 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 41b063f..f134fd0 100644
--- a/environ/environ.py
+++ b/environ/environ.py
@@ -17,6 +17,7 @@ import logging
 import os
 import re
 import sys
+import urllib.parse as urlparselib
 import warnings
 from urllib.parse import (
     parse_qs,
@@ -66,7 +67,6 @@ 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,6 +205,8 @@ class Env:
     def __contains__(self, var):
         return var in self.ENVIRON
 
+    # Shortcuts
+
     def str(self, var, default=NOTSET, multiline=False):
         """
         :rtype: str
@@ -213,6 +216,21 @@ 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
@@ -356,9 +374,9 @@ class Env:
         :rtype: typing.IO[typing.Any]
         """
 
-        logger.debug(
-            "get '%s' casted as '%s' with default '%s'",
-            var, cast, default)
+        logger.debug("get '{}' casted as '{}' with default '{}'".format(
+            var, cast, default
+        ))
 
         var_name = "{}{}".format(self.prefix, var)
         if var_name in self.scheme:
@@ -409,11 +427,13 @@ class Env:
 
         value = None if default is None and value == '' else value
 
-        if value != default or (parse_default and value is not None):
+        if value != default or (parse_default and value):
             value = self.parse_value(value, cast)
 
         return value
 
+    # Class and static methods
+
     @classmethod
     def parse_value(cls, value, cast):
         """Parse and cast provided value
@@ -425,7 +445,7 @@ class Env:
         """
         if cast is None:
             return value
-        if cast is bool:
+        elif cast is bool:
             try:
                 value = int(value) != 0
             except ValueError:
@@ -438,7 +458,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', {})
+            value_cast_by_key = cast.get('cast', dict())
             value = dict(map(
                 lambda kv: (
                     key_cast(kv[0]),
@@ -450,12 +470,11 @@ class Env:
                 [val.split('=') for val in value.split(';') if val]
             ))
         elif cast is dict:
-            value = dict([v.split('=', 1) for v in value.split(',') if v])
+            value = dict([val.split('=') for val in value.split(',') if val])
         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
@@ -572,7 +591,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'])
 
@@ -613,7 +632,8 @@ class Env:
         if not isinstance(url, cls.URL_CLASS):
             if not url:
                 return {}
-            url = urlparse(url)
+            else:
+                url = urlparse(url)
 
         if url.scheme not in cls.CACHE_SCHEMES:
             raise ImproperlyConfigured(
@@ -757,15 +777,15 @@ class Env:
         params = {}  # type: dict
         if url.query:
             params = parse_qs(url.query)
-            if 'EXCLUDED_INDEXES' in params:
+            if 'EXCLUDED_INDEXES' in params.keys():
                 config['EXCLUDED_INDEXES'] \
                     = params['EXCLUDED_INDEXES'][0].split(',')
-            if 'INCLUDE_SPELLING' in params:
+            if 'INCLUDE_SPELLING' in params.keys():
                 config['INCLUDE_SPELLING'] = cls.parse_value(
                     params['INCLUDE_SPELLING'][0],
                     bool
                 )
-            if 'BATCH_SIZE' in params:
+            if 'BATCH_SIZE' in params.keys():
                 config['BATCH_SIZE'] = cls.parse_value(
                     params['BATCH_SIZE'][0],
                     int
@@ -773,8 +793,8 @@ class Env:
 
         if url.scheme == 'simple':
             return config
-        if url.scheme in ['solr'] + cls.ELASTICSEARCH_FAMILY:
-            if 'KWARGS' in params:
+        elif url.scheme in ['solr'] + cls.ELASTICSEARCH_FAMILY:
+            if 'KWARGS' in params.keys():
                 config['KWARGS'] = params['KWARGS'][0]
 
         # remove trailing slash
@@ -785,7 +805,7 @@ class Env:
             config['URL'] = urlunparse(
                 ('http',) + url[1:2] + (path,) + ('', '', '')
             )
-            if 'TIMEOUT' in params:
+            if 'TIMEOUT' in params.keys():
                 config['TIMEOUT'] = cls.parse_value(params['TIMEOUT'][0], int)
             return config
 
@@ -802,7 +822,7 @@ class Env:
             config['URL'] = urlunparse(
                 ('http',) + url[1:2] + (path,) + ('', '', '')
             )
-            if 'TIMEOUT' in params:
+            if 'TIMEOUT' in params.keys():
                 config['TIMEOUT'] = cls.parse_value(params['TIMEOUT'][0], int)
             config['INDEX_NAME'] = index
             return config
@@ -810,15 +830,15 @@ class Env:
         config['PATH'] = '/' + path
 
         if url.scheme == 'whoosh':
-            if 'STORAGE' in params:
+            if 'STORAGE' in params.keys():
                 config['STORAGE'] = params['STORAGE'][0]
-            if 'POST_LIMIT' in params:
+            if 'POST_LIMIT' in params.keys():
                 config['POST_LIMIT'] = cls.parse_value(
                     params['POST_LIMIT'][0],
                     int
                 )
         elif url.scheme == 'xapian':
-            if 'FLAGS' in params:
+            if 'FLAGS' in params.keys():
                 config['FLAGS'] = params['FLAGS'][0]
 
         if engine:
@@ -852,7 +872,6 @@ class Env:
             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),
@@ -861,13 +880,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), encoding='utf-8') as f:
+                with open(str(env_file)) as f:
                     content = f.read()
             else:
                 with env_file as f:
@@ -875,10 +894,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: %s', env_file)
+        logger.debug('Read environment variables from: {}'.format(env_file))
 
         def _keep_escaped_format_characters(match):
             """Keep escaped newline/tabs in quoted strings"""
@@ -958,7 +977,6 @@ 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
@@ -966,7 +984,6 @@ class Path:
         """Current directory for this Path"""
         return self.__root__
 
-    # pylint: disable=keyword-arg-before-vararg
     def __init__(self, start='', *paths, **kwargs):
 
         super().__init__()
@@ -1000,9 +1017,9 @@ class Path:
     def __sub__(self, other):
         if isinstance(other, int):
             return self.path('../' * other)
-        if isinstance(other, str) and self.__root__.endswith(other):
-            return Path(self.__root__.rstrip(other))
-
+        elif isinstance(other, str):
+            if 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(
@@ -1035,12 +1052,10 @@ class Path:
         return self.__str__()
 
     def rfind(self, *args, **kwargs):
-        """Proxy method to :py:func:`str.rfind`"""
-        return str(self).rfind(*args, **kwargs)
+        return self.__str__().rfind(*args, **kwargs)
 
     def find(self, *args, **kwargs):
-        """Proxy method to :py:func:`str.find`"""
-        return str(self).find(*args, **kwargs)
+        return self.__str__().find(*args, **kwargs)
 
     @staticmethod
     def _absolute_join(base, *paths, **kwargs):
@@ -1049,3 +1064,21 @@ 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 0578945..6da95f5 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, encoding='utf-8') as f:
+            with open(key_file) as f:
                 value = f.read()
             if self.cache:
                 self.files_cache[key] = value
diff --git a/setup.py b/setup.py
index 900ffe6..4de0bec 100644
--- a/setup.py
+++ b/setup.py
@@ -10,21 +10,11 @@
 
 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:
@@ -149,13 +139,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',
 
@@ -227,7 +217,7 @@ if __name__ == '__main__':
         platforms=['any'],
         include_package_data=True,
         zip_safe=False,
-        python_requires='>=3.5,<4',
+        python_requires='>=3.4,<4',
         install_requires=INSTALL_REQUIRES,
         dependency_links=DEPENDENCY_LINKS,
         extras_require=EXTRAS_REQUIRE,
diff --git a/tox.ini b/tox.ini
index 22a4442..73e6a6b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -18,9 +18,9 @@ envlist =
     docs
     lint
     manifest
-    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}
+    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]
@@ -31,7 +31,6 @@ python =
     3.8: py38
     3.9: py39
     3.10: py310
-    3.11: py311
     pypy-3.7: pypy
 
 [testenv]
@@ -68,26 +67,10 @@ 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
-    # 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
+commands = flake8 environ setup.py
 
 [testenv:linkcheck]
 description = Check external links in the package documentation


Test patch:
diff --git a/tests/fixtures.py b/tests/fixtures.py
index 6990d9a..c7f8a0b 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -25,7 +25,6 @@ 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(
@@ -68,7 +67,6 @@ 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 a762c1b..132266e 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-2022, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021, 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,11 +12,7 @@ import pytest
 
 import environ.compat
 from environ import Env
-from environ.compat import (
-    ImproperlyConfigured,
-    PYMEMCACHE_DRIVER,
-    REDIS_DRIVER,
-)
+from environ.compat import PYMEMCACHE_DRIVER, REDIS_DRIVER, ImproperlyConfigured
 
 
 def test_base_options_parsing():
@@ -58,7 +54,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', REDIS_DRIVER,
+        ('rediscache:///path/to/socket:1', 'django_redis.cache.RedisCache',
          'unix:///path/to/socket:1'),
         ('memcache:///tmp/memcached.sock',
          'django.core.cache.backends.memcached.MemcachedCache',
@@ -116,23 +112,6 @@ 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')
@@ -207,11 +186,11 @@ def test_cache_url_password_using_sub_delims(monkeypatch, chars):
     env = Env()
 
     result = env.cache()
-    assert result['BACKEND'] == REDIS_DRIVER
+    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
     assert result['LOCATION'] == url
 
     result = env.cache_url_config(url)
-    assert result['BACKEND'] == REDIS_DRIVER
+    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
     assert result['LOCATION'] == url
 
     url = 'rediss://enigma:sec{}ret@ondigitalocean.com:25061/2'.format(chars)
@@ -219,11 +198,11 @@ def test_cache_url_password_using_sub_delims(monkeypatch, chars):
     env = Env()
 
     result = env.cache()
-    assert result['BACKEND'] == REDIS_DRIVER
+    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
     assert result['LOCATION'] == url
 
     result = env.cache_url_config(url)
-    assert result['BACKEND'] == REDIS_DRIVER
+    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
     assert result['LOCATION'] == url
 
     url = 'rediss://enigma:{}secret@ondigitalocean.com:25061/2'.format(chars)
@@ -231,11 +210,11 @@ def test_cache_url_password_using_sub_delims(monkeypatch, chars):
     env = Env()
 
     result = env.cache()
-    assert result['BACKEND'] == REDIS_DRIVER
+    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
     assert result['LOCATION'] == url
 
     result = env.cache_url_config(url)
-    assert result['BACKEND'] == REDIS_DRIVER
+    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
     assert result['LOCATION'] == url
 
 
@@ -251,7 +230,7 @@ def test_cache_url_password_using_gen_delims(monkeypatch, chars):
     env = Env()
 
     result = env.cache()
-    assert result['BACKEND'] == REDIS_DRIVER
+    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
     assert result['LOCATION'] == url
 
     url = 'rediss://enigma:sec{}ret@ondigitalocean.com:25061/2'.format(chars)
@@ -259,7 +238,7 @@ def test_cache_url_password_using_gen_delims(monkeypatch, chars):
     env = Env()
 
     result = env.cache()
-    assert result['BACKEND'] == REDIS_DRIVER
+    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
     assert result['LOCATION'] == url
 
     url = 'rediss://enigma:{}secret@ondigitalocean.com:25061/2'.format(chars)
@@ -267,7 +246,7 @@ def test_cache_url_password_using_gen_delims(monkeypatch, chars):
     env = Env()
 
     result = env.cache()
-    assert result['BACKEND'] == REDIS_DRIVER
+    assert result['BACKEND'] == 'django_redis.cache.RedisCache'
     assert result['LOCATION'] == url
 
 
diff --git a/tests/test_db.py b/tests/test_db.py
index c407413..20d807f 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-2022, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021, 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,6 +168,7 @@ 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 396c212..b5c1e39 100644
--- a/tests/test_env.py
+++ b/tests/test_env.py
@@ -8,15 +8,12 @@
 
 import os
 from urllib.parse import quote
+from warnings import catch_warnings
 
 import pytest
 
 from environ import Env, Path
-from environ.compat import (
-    DJANGO_POSTGRES,
-    ImproperlyConfigured,
-    REDIS_DRIVER,
-)
+from environ.compat import ImproperlyConfigured, DJANGO_POSTGRES
 from .asserts import assert_type_and_value
 from .fixtures import FakeEnv
 
@@ -74,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',
         [
@@ -156,23 +168,12 @@ 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."""
-        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(',')]
-                    )
-                ),
-            )
-        )
+        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'],
@@ -185,7 +186,6 @@ 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,13 +227,6 @@ 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(
@@ -298,7 +291,7 @@ class TestEnv:
             (Env.DEFAULT_CACHE_ENV,
              'django.core.cache.backends.memcached.MemcachedCache',
              '127.0.0.1:11211', None),
-            ('CACHE_REDIS', REDIS_DRIVER,
+            ('CACHE_REDIS', 'django_redis.cache.RedisCache',
              '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 237489b..69dc84e 100644
--- a/tests/test_env.txt
+++ b/tests/test_env.txt
@@ -1,5 +1,4 @@
 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 f411b7d..b4733bc 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-2022, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021, 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,8 @@ import os
 import tempfile
 from contextlib import contextmanager
 
-import pytest
-
 import environ
+import pytest
 
 
 @contextmanager
diff --git a/tests/test_path.py b/tests/test_path.py
index b97f4ca..ed33a7c 100644
--- a/tests/test_path.py
+++ b/tests/test_path.py
@@ -1,16 +1,14 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021, 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 sys
-
 import pytest
-
+import sys
 
 from environ import Path
 from environ.compat import ImproperlyConfigured
@@ -73,7 +71,6 @@ 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 2f3d32e..f32d7cc 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,13 +1,12 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021, 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
 
 
@@ -22,7 +21,6 @@ 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",
     [