Repository: joke2k/django-environ
Pull request #545 (2024-11-08 19:05:17)

Release v0.12.0


Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0779431..342d5ce 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -24,17 +24,17 @@ jobs:
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3.6.0
+        uses: actions/checkout@v4.2.2
 
-      - name: Set up Python 3.10
-        uses: actions/setup-python@v4.7.0
+      - name: Set up Python 3.12
+        uses: actions/setup-python@v5.3.0
         with:
-          python-version: '3.10'
+          python-version: '3.12'
 
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          pip install tox tox-gh-actions
+          pip install tox tox-gh-actions setuptools
 
       - name: Check MANIFEST.in for completeness
         run: tox -e manifest
@@ -44,7 +44,7 @@ jobs:
 
       - name: Archive build artifacts
         if: success()
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           # To ensure that jobs don't overwrite existing artifacts,
           # use a different name per job.
@@ -64,12 +64,12 @@ jobs:
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3.6.0
+        uses: actions/checkout@v4.2.2
 
-      - name: Set up Python 3.10
-        uses: actions/setup-python@v4.7.0
+      - name: Set up Python 3.12
+        uses: actions/setup-python@v5.3.0
         with:
-          python-version: '3.10'
+          python-version: '3.12'
 
       - name: Install in dev mode
         run: python -m pip install -e .
diff --git a/.github/workflows/change-pr-target.yml b/.github/workflows/change-pr-target.yml
index eb12a30..af419d1 100644
--- a/.github/workflows/change-pr-target.yml
+++ b/.github/workflows/change-pr-target.yml
@@ -8,7 +8,7 @@ jobs:
   check-branch:
     runs-on: ubuntu-latest
     steps:
-      - uses: Vankka/pr-target-branch-action@v2
+      - uses: Vankka/pr-target-branch-action@v3
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         with:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 5ecdb2f..0d80bfe 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -47,38 +47,29 @@ jobs:
 
       matrix:
         python:
-          - '3.8'
           - '3.9'
           - '3.10'
           - '3.11'
-          - 'pypy-3.7'
+          - '3.12'
+          - '3.13'
+          - 'pypy-3.10'
         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.6'
-            os: ubuntu-20.04
-          - python: '3.7'
-            os: ubuntu-20.04
-
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3.6.0
+        uses: actions/checkout@v4.2.2
         with:
           fetch-depth: 5
 
       - name: Set up Python ${{ matrix.python }}
-        uses: actions/setup-python@v4.7.0
+        uses: actions/setup-python@v5.3.0
         with:
           python-version: ${{ matrix.python }}
 
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          python -m pip install tox tox-gh-actions
+          python -m pip install tox tox-gh-actions setuptools
 
       - name: Setuptools self-test
         run: |
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 07dd200..d8236f3 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -45,16 +45,16 @@ jobs:
 
     steps:
     - name: Checkout repository
-      uses: actions/checkout@v3.6.0
+      uses: actions/checkout@v4.2.2
 
     # Initializes the CodeQL tools for scanning.
     - name: Initialize CodeQL
-      uses: github/codeql-action/init@v2
+      uses: github/codeql-action/init@v3
       with:
         languages: ${{ matrix.language }}
         
     - name: Autobuild
-      uses: github/codeql-action/autobuild@v2
+      uses: github/codeql-action/autobuild@v3
 
     - name: Perform CodeQL Analysis
-      uses: github/codeql-action/analyze@v2
+      uses: github/codeql-action/analyze@v3
diff --git a/.github/workflows/cs.yml b/.github/workflows/cs.yml
index fc596be..30d41b9 100644
--- a/.github/workflows/cs.yml
+++ b/.github/workflows/cs.yml
@@ -25,17 +25,17 @@ jobs:
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3.6.0
+        uses: actions/checkout@v4.2.2
 
-      - name: Set up Python 3.10
-        uses: actions/setup-python@v4.7.0
+      - name: Set up Python 3.12
+        uses: actions/setup-python@v5.3.0
         with:
-          python-version: '3.10'
+          python-version: '3.12'
 
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          pip install tox tox-gh-actions
+          pip install tox tox-gh-actions setuptools
 
       - name: Lint with tox
         run: tox -e lint
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index e03e0d8..0ae19c3 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -26,17 +26,17 @@ jobs:
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3.6.0
+        uses: actions/checkout@v4.2.2
 
-      - name: Set up Python 3.10
-        uses: actions/setup-python@v4.7.0
+      - name: Set up Python 3.12
+        uses: actions/setup-python@v5.3.0
         with:
-          python-version: '3.10'
+          python-version: '3.12'
 
       - name: Install dependencies
         run: |
           python -m pip install --upgrade pip
-          pip install tox tox-gh-actions
+          pip install tox tox-gh-actions setuptools
 
       - name: Check external links in the package documentation
         run: tox -e linkcheck
@@ -46,7 +46,7 @@ jobs:
 
       - name: Archive docs artifacts
         if: always()
-        uses: actions/upload-artifact@v3
+        uses: actions/upload-artifact@v4
         with:
           name: docs
           path: docs
diff --git a/.gitignore b/.gitignore
index 5862901..ec6473a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/.readthedocs.yml b/.readthedocs.yml
index 27f9368..191e999 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
@@ -17,7 +17,7 @@ build:
   tools:
     # Keep version in sync with tox.ini (testenv:docs) and
     # docs.yml (GitHub Action Workflow).
-    python: '3.10'
+    python: '3.12'
 
 python:
   install:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 57c4c8d..9ccc182 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -5,8 +5,41 @@ 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.12.0`_ - 8-November-2024
+-----------------------------
+Fixed
++++++
+- Include prefix in the ``ImproperlyConfigured`` error message
+  `#513 <https://github.com/joke2k/django-environ/issues/513>`_.
+
+Added
++++++
+- Add support for Python 3.12 and 3.13
+  `#538 <https://github.com/joke2k/django-environ/issues/538>`_.
+- Add support for Django 5.1
+  `#535 <https://github.com/joke2k/django-environ/issues/535>`_.
+- Add support for Django CockroachDB driver
+  `#509 <https://github.com/joke2k/django-environ/issues/509>`_.
+- Add support for Django Channels
+  `#266 <https://github.com/joke2k/django-environ/issues/266>`_.
+
+Changed
++++++++
+- Disabled inline comments handling by default due to potential side effects.
+  While the feature itself is useful, the project's philosophy dictates that
+  it should not be enabled by default for all users
+  `#499 <https://github.com/joke2k/django-environ/issues/499>`_.
+
+Removed
++++++++
+- Removed support of Python 3.6, 3.7 and 3.8
+  `#538 <https://github.com/joke2k/django-environ/issues/538>`_.
+- Removed support of Django 1.x.
+  `#538 <https://github.com/joke2k/django-environ/issues/538>`_.
+
+
 `v0.11.2`_ - 1-September-2023
--------------------------------
+-----------------------------
 Fixed
 +++++
 - Revert "Add variable expansion." feature
@@ -31,7 +64,7 @@ Added
   `#463 <https://github.com/joke2k/django-environ/pull/463>`_.
 - Added variable expansion
   `#468 <https://github.com/joke2k/django-environ/pull/468>`_.
-- Added capability to handle comments after #, after quoted values,
+- Added capability to handle comments after ``#``, after quoted values,
   like ``KEY= 'part1 # part2' # comment``
   `#475 <https://github.com/joke2k/django-environ/pull/475>`_.
 - Added support for ``interpolate`` parameter
@@ -388,6 +421,7 @@ Added
 - Initial release.
 
 
+.. _v0.12.0: https://github.com/joke2k/django-environ/compare/v0.11.2...v0.12.0
 .. _v0.11.2: https://github.com/joke2k/django-environ/compare/v0.11.1...v0.11.2
 .. _v0.11.1: https://github.com/joke2k/django-environ/compare/v0.11.0...v0.11.1
 .. _v0.11.0: https://github.com/joke2k/django-environ/compare/v0.10.0...v0.11.0
@@ -405,4 +439,4 @@ Added
 .. _v0.4.1: https://github.com/joke2k/django-environ/compare/v0.4...v0.4.1
 .. _v0.4: https://github.com/joke2k/django-environ/compare/v0.3.1...v0.4
 .. _v0.3.1: https://github.com/joke2k/django-environ/compare/v0.3...v0.3.1
-.. _v0.3: https://github.com/joke2k/django-environ/compare/v0.2.1...v0.3
\ No newline at end of file
+.. _v0.3: https://github.com/joke2k/django-environ/compare/v0.2.1...v0.3
diff --git a/LICENSE.txt b/LICENSE.txt
index 0bd208d..97cfd65 100644
--- a/LICENSE.txt
+++ b/LICENSE.txt
@@ -1,4 +1,4 @@
-Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
diff --git a/MANIFEST.in b/MANIFEST.in
index 9c2f064..694e938 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/README.rst b/README.rst
index 528d1df..ff8e012 100644
--- a/README.rst
+++ b/README.rst
@@ -97,7 +97,8 @@ approach, some connection strings are expressed as url, so this package can pars
 it and return a ``urllib.parse.ParseResult``. These strings from ``os.environ``
 are loaded from a ``.env`` file and filled in ``os.environ`` with ``setdefault``
 method, to avoid to overwrite the real environ.
-A similar approach is used in `Two Scoops of Django <https://www.feldroy.com/books/two-scoops-of-django-3-x>`_
+A similar approach is used in
+`Two Scoops of Django <https://web.archive.org/web/20240121133956/https://www.feldroy.com/books/two-scoops-of-django-3-x>`_
 book and explained in `12factor-django <https://wellfire.co/learn/easier-12-factor-django>`_
 article.
 
@@ -126,8 +127,8 @@ its documentation lives at `Read the Docs <https://django-environ.readthedocs.io
 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.6+, and officially supports
-Django 1.11, 2.2, 3.0, 3.1, 3.2, 4.0, 4.1 and 4.2.
+It’s rigorously tested on Python 3.9+, and officially supports
+Django 2.2, 3.0, 3.1, 3.2, 4.0, 4.1, 4.2, 5.0, and 5.1.
 
 If you'd like to contribute to ``django-environ`` you're most welcome!
 
diff --git a/SECURITY.rst b/SECURITY.rst
index 16ffdce..f3ea149 100644
--- a/SECURITY.rst
+++ b/SECURITY.rst
@@ -6,5 +6,5 @@ Reporting a Vulnerability
 -------------------------
 
 If you discover a security vulnerability within ``django-environ``, please
-send an e-mail to Serghei Iakovlev via egrep@protonmail.ch. All security
+send an e-mail to Serghei Iakovlev via oss@serghei.pl. All security
 vulnerabilities will be promptly addressed.
diff --git a/docs/Makefile b/docs/Makefile
index 887c0bb..343425b 100644
--- a/docs/Makefile
+++ b/docs/Makefile
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/docs/conf.py b/docs/conf.py
index 8beac1f..0e91d43 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021-2023, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/docs/docutils.conf b/docs/docutils.conf
index 4ed0bf5..d624b45 100644
--- a/docs/docutils.conf
+++ b/docs/docutils.conf
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/docs/install.rst b/docs/install.rst
index 3dacca9..b1a73c1 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -6,8 +6,8 @@ Installation
 Requirements
 ============
 
-* `Django <https://www.djangoproject.com/>`_ >= 1.11
-* `Python <https://www.python.org/>`_ >= 3.5
+* `Django <https://www.djangoproject.com/>`_ >= 2.2
+* `Python <https://www.python.org/>`_ >= 3.9
 
 Installing django-environ
 =========================
diff --git a/docs/tips.rst b/docs/tips.rst
index 20b8c3e..66538c4 100644
--- a/docs/tips.rst
+++ b/docs/tips.rst
@@ -2,6 +2,71 @@
 Tips
 ====
 
+Handling Inline Comments in .env Files
+======================================
+
+``django-environ`` provides an optional feature to parse inline comments in ``.env``
+files. This is controlled by the ``parse_comments`` parameter in the ``read_env``
+method.
+
+Modes
+-----
+
+- **Enabled (``parse_comments=True``)**: Inline comments starting with ``#`` will be ignored.
+- **Disabled (``parse_comments=False``)**: The entire line, including comments, will be read as the value.
+- **Default**: The behavior is the same as when ``parse_comments=False``.
+
+Side Effects
+------------
+
+While this feature can be useful for adding context to your ``.env`` files,
+it can introduce unexpected behavior. For example, if your value includes
+a ``#`` symbol, it will be truncated when ``parse_comments=True``.
+
+Why Disabled by Default?
+------------------------
+
+In line with the project's philosophy of being explicit and avoiding unexpected behavior,
+this feature is disabled by default. If you understand the implications and find the feature
+useful, you can enable it explicitly.
+
+Example
+-------
+
+Here is an example demonstrating the different modes of handling inline comments.
+
+**.env file contents**:
+
+.. code-block:: shell
+
+   # .env file contents
+   BOOL_TRUE_WITH_COMMENT=True # This is a comment
+   STR_WITH_HASH=foo#bar # This is also a comment
+
+**Python code**:
+
+.. code-block:: python
+
+   import environ
+
+   # Using parse_comments=True
+   env = environ.Env()
+   env.read_env(parse_comments=True)
+   print(env('BOOL_TRUE_WITH_COMMENT'))  # Output: True
+   print(env('STR_WITH_HASH'))  # Output: foo
+
+   # Using parse_comments=False
+   env = environ.Env()
+   env.read_env(parse_comments=False)
+   print(env('BOOL_TRUE_WITH_COMMENT'))  # Output: True # This is a comment
+   print(env('STR_WITH_HASH'))  # Output: foo#bar # This is also a comment
+
+   # Using default behavior
+   env = environ.Env()
+   env.read_env()
+   print(env('BOOL_TRUE_WITH_COMMENT'))  # Output: True # This is a comment
+   print(env('STR_WITH_HASH'))  # Output: foo#bar # This is also a comment
+
 
 Docker-style file based variables
 =================================
diff --git a/environ/__init__.py b/environ/__init__.py
index 28d4a5a..8d29d2f 100644
--- a/environ/__init__.py
+++ b/environ/__init__.py
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021-2023, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
@@ -21,7 +21,7 @@ from .environ import *
 __copyright__ = 'Copyright (C) 2013-2023 Daniele Faraglia'
 """The copyright notice of the package."""
 
-__version__ = '0.11.2'
+__version__ = '0.12.0'
 """The version of the package."""
 
 __license__ = 'MIT'
@@ -36,7 +36,7 @@ __author_email__ = 'daniele.faraglia@gmail.com'
 __maintainer__ = 'Serghei Iakovlev'
 """The maintainer of the package."""
 
-__maintainer_email__ = 'egrep@protonmail.ch'
+__maintainer_email__ = 'oss@serghei.pl'
 """The email of the maintainer of the package."""
 
 __url__ = 'https://django-environ.readthedocs.org'
diff --git a/environ/compat.py b/environ/compat.py
index 49b5b48..55953fe 100644
--- a/environ/compat.py
+++ b/environ/compat.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-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/environ/environ.py b/environ/environ.py
index f74884b..5536f2c 100644
--- a/environ/environ.py
+++ b/environ/environ.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-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
@@ -115,6 +115,7 @@ class Env:
         'psql': DJANGO_POSTGRES,
         'pgsql': DJANGO_POSTGRES,
         'postgis': 'django.contrib.gis.db.backends.postgis',
+        'cockroachdb': 'django_cockroachdb',
         'mysql': 'django.db.backends.mysql',
         'mysql2': 'django.db.backends.mysql',
         'mysql-connector': 'mysql.connector.django',
@@ -189,6 +190,13 @@ class Env:
                             for s in ('', 's')]
     CLOUDSQL = 'cloudsql'
 
+    DEFAULT_CHANNELS_ENV = "CHANNELS_URL"
+    CHANNELS_SCHEMES = {
+        "inmemory": "channels.layers.InMemoryChannelLayer",
+        "redis": "channels_redis.core.RedisChannelLayer",
+        "redis+pubsub": "channels_redis.pubsub.RedisPubSubChannelLayer"
+    }
+
     def __init__(self, **scheme):
         self.smart_cast = True
         self.escape_proxy = False
@@ -337,6 +345,19 @@ class Env:
             engine=engine
         )
 
+    def channels_url(self, var=DEFAULT_CHANNELS_ENV, default=NOTSET,
+                     backend=None):
+        """Returns a config dictionary, defaulting to CHANNELS_URL.
+
+        :rtype: dict
+        """
+        return self.channels_url_config(
+            self.url(var, default=default),
+            backend=backend
+        )
+
+    channels = channels_url
+
     def path(self, var, default=NOTSET, **kwargs):
         """
         :rtype: Path
@@ -388,7 +409,7 @@ class Env:
             value = self.ENVIRON[var_name]
         except KeyError as exc:
             if default is self.NOTSET:
-                error_msg = f'Set the {var} environment variable'
+                error_msg = f'Set the {var_name} environment variable'
                 raise ImproperlyConfigured(error_msg) from exc
 
             value = default
@@ -736,6 +757,33 @@ class Env:
 
         return config
 
+    @classmethod
+    def channels_url_config(cls, url, backend=None):
+        """Parse an arbitrary channels URL.
+
+        :param urllib.parse.ParseResult or str url:
+            Email URL to parse.
+        :param str or None backend:
+            If None, the backend is evaluates from the ``url``.
+        :return: Parsed channels URL.
+        :rtype: dict
+        """
+        config = {}
+        url = urlparse(url) if not isinstance(url, cls.URL_CLASS) else url
+
+        if backend:
+            config["BACKEND"] = backend
+        elif url.scheme not in cls.CHANNELS_SCHEMES:
+            raise ImproperlyConfigured(f"Invalid channels schema {url.scheme}")
+        else:
+            config["BACKEND"] = cls.CHANNELS_SCHEMES[url.scheme]
+            if url.scheme in ("redis", "redis+pubsub"):
+                config["CONFIG"] = {
+                    "hosts": [url._replace(scheme="redis").geturl()]
+                }
+
+        return config
+
     @classmethod
     def _parse_common_search_params(cls, url):
         cfg = {}
@@ -809,7 +857,7 @@ class Env:
         :param urllib.parse.ParseResult or str url:
             Search URL to parse.
         :param str or None engine:
-            If None, the engine is evaluates from the ``url``.
+            If None, the engine is evaluating from the ``url``.
         :return: Parsed search URL.
         :rtype: dict
         """
@@ -862,8 +910,8 @@ class Env:
         return config
 
     @classmethod
-    def read_env(cls, env_file=None, overwrite=False, encoding='utf8',
-                 **overrides):
+    def read_env(cls, env_file=None, overwrite=False, parse_comments=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
@@ -883,6 +931,8 @@ class Env:
             the Django settings module from the Django project root.
         :param overwrite: ``overwrite=True`` will force an overwrite of
             existing environment variables.
+        :param parse_comments: Determines whether to recognize and ignore
+           inline comments in the .env file. Default is False.
         :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
@@ -927,22 +977,40 @@ class Env:
         for line in content.splitlines():
             m1 = re.match(r'\A(?:export )?([A-Za-z_0-9]+)=(.*)\Z', line)
             if m1:
+
+                # Example:
+                #
+                # line: KEY_499=abc#def
+                # key:  KEY_499
+                # val:  abc#def
                 key, val = m1.group(1), m1.group(2)
-                # Look for value in quotes, ignore post-# comments
-                # (outside quotes)
-                m2 = re.match(r"\A\s*'(?<!\\)(.*)'\s*(#.*\s*)?\Z", val)
-                if m2:
-                    val = m2.group(1)
+
+                if not parse_comments:
+                    # Default behavior
+                    #
+                    # Look for value in single quotes
+                    m2 = re.match(r"\A'(.*)'\Z", val)
+                    if m2:
+                        val = m2.group(1)
                 else:
-                    # For no quotes, find value, ignore comments
-                    # after the first #
-                    m2a = re.match(r"\A(.*?)(#.*\s*)?\Z", val)
-                    if m2a:
-                        val = m2a.group(1)
+                    # Ignore post-# comments (outside quotes).
+                    # Something like ['val'  # comment] becomes ['val'].
+                    m2 = re.match(r"\A\s*'(?<!\\)(.*)'\s*(#.*\s*)?\Z", val)
+                    if m2:
+                        val = m2.group(1)
+                    else:
+                        # For no quotes, find value, ignore comments
+                        # after the first #
+                        m2a = re.match(r"\A(.*?)(#.*\s*)?\Z", val)
+                        if m2a:
+                            val = m2a.group(1)
+
+                # Look for value in double quotes
                 m3 = re.match(r'\A"(.*)"\Z', val)
                 if m3:
                     val = re.sub(r'\\(.)', _keep_escaped_format_characters,
                                  m3.group(1))
+
                 overrides[key] = str(val)
             elif not line or line.startswith('#'):
                 # ignore warnings for empty line-breaks or comments
diff --git a/environ/fileaware_mapping.py b/environ/fileaware_mapping.py
index 0578945..9d59e0a 100644
--- a/environ/fileaware_mapping.py
+++ b/environ/fileaware_mapping.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-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/setup.py b/setup.py
index 3052575..e927217 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
 #
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021-2023, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
@@ -135,9 +135,6 @@ CLASSIFIERS = [
     'Development Status :: 5 - Production/Stable',
 
     'Framework :: Django',
-    'Framework :: Django :: 1.11',
-    'Framework :: Django :: 2.0',
-    'Framework :: Django :: 2.1',
     'Framework :: Django :: 2.2',
     'Framework :: Django :: 3.0',
     'Framework :: Django :: 3.1',
@@ -145,6 +142,8 @@ CLASSIFIERS = [
     'Framework :: Django :: 4.0',
     'Framework :: Django :: 4.1',
     'Framework :: Django :: 4.2',
+    'Framework :: Django :: 5.0',
+    'Framework :: Django :: 5.1',
 
     'Operating System :: OS Independent',
 
@@ -153,12 +152,11 @@ CLASSIFIERS = [
 
     'Programming Language :: Python',
     'Programming Language :: Python :: 3',
-    '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 :: 3.12',
+    'Programming Language :: Python :: 3.13',
     'Programming Language :: Python :: Implementation :: CPython',
     'Programming Language :: Python :: Implementation :: PyPy',
 
@@ -183,11 +181,12 @@ EXTRAS_REQUIRE = {
     'testing': [
         'coverage[toml]>=5.0a4',  # Code coverage measurement for Python
         'pytest>=4.6.11',  # Our tests framework
+        'setuptools>=71.0.0',  # Needed as a dependency for some tests
     ],
     # Dependencies that are required to build documentation
     'docs': [
-        'furo>=2021.8.17b43,==2021.8.*',  # Sphinx documentation theme
-        'sphinx>=3.5.0',  # Python documentation generator
+        'furo>=2024.8.6',  # Sphinx documentation theme
+        'sphinx>=5.0',  # Python documentation generator
         'sphinx-notfound-page',  # Create a custom 404 page
     ],
 }
@@ -230,7 +229,7 @@ if __name__ == '__main__':
         platforms=['any'],
         include_package_data=True,
         zip_safe=False,
-        python_requires='>=3.6,<4',
+        python_requires='>=3.9,<4',
         install_requires=INSTALL_REQUIRES,
         dependency_links=DEPENDENCY_LINKS,
         extras_require=EXTRAS_REQUIRE,
diff --git a/tox.ini b/tox.ini
index d236737..a0b8dc2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021-2023, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
@@ -18,26 +18,23 @@ envlist =
     docs
     lint
     manifest
-    py{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,42}
-    pypy-django{111,22,30,31,32}
+    py{39,310,311,312,313}-django{22,30,31,32,40,41,42}
+    py{310,311,312,313}-django{50,51}
+    pypy-django{22,30,31,32}
 
 [gh-actions]
 python =
-    3.6: py36
-    3.7: py37
-    3.8: py38
     3.9: py39
     3.10: py310
     3.11: py311
-    pypy-3.7: pypy
+    3.12: py312
+    3.13: py313
+    pypy-3.10: pypy
 
 [testenv]
 description = Unit tests
 extras = testing
 deps =
-    django111: Django>=1.11,<2
     django22: Django>=2.2,<3.0
     django30: Django>=3.0,<3.1
     django31: Django>=3.1,<3.2
@@ -91,7 +88,7 @@ commands =
 description = Check external links in the package documentation
 # Keep basepython in sync with .readthedocs.yml and docs.yml
 # (GitHub Action Workflow).
-basepython = python3.10
+basepython = python3.12
 extras = docs
 commands =
     {envpython} -m sphinx \
@@ -109,7 +106,7 @@ isolated_build = true
 description = Build package documentation (HTML)
 # Keep basepython in sync with .readthedocs.yml and docs.yml
 # (GitHub Action Workflow).
-basepython = python3.10
+basepython = python3.12
 extras = docs
 commands =
     {envpython} -m sphinx \


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
index 19f3cf2..c5fd823 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/tests/asserts.py b/tests/asserts.py
index 3389e52..00d94b2 100644
--- a/tests/asserts.py
+++ b/tests/asserts.py
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/tests/conftest.py b/tests/conftest.py
index 38550f7..ffdbf29 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/tests/fixtures.py b/tests/fixtures.py
index 69e5e90..dc9fbd4 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.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-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/tests/test_cache.py b/tests/test_cache.py
index a8aff16..58e57e0 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-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/tests/test_channels.py b/tests/test_channels.py
new file mode 100644
index 0000000..64485a6
--- /dev/null
+++ b/tests/test_channels.py
@@ -0,0 +1,25 @@
+# This file is part of the django-environ.
+#
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
+# 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.
+
+from environ import Env
+
+
+def test_channels_parsing():
+    url = "inmemory://"
+    result = Env.channels_url_config(url)
+    assert result["BACKEND"] == "channels.layers.InMemoryChannelLayer"
+
+    url = "redis://user:password@localhost:6379/0"
+    result = Env.channels_url_config(url)
+    assert result["BACKEND"] == "channels_redis.core.RedisChannelLayer"
+    assert result["CONFIG"]["hosts"][0] == "redis://user:password@localhost:6379/0"
+
+    url = "redis+pubsub://user:password@localhost:6379/0"
+    result = Env.channels_url_config(url)
+    assert result["BACKEND"] == "channels_redis.pubsub.RedisPubSubChannelLayer"
+    assert result["CONFIG"]["hosts"][0] == "redis://user:password@localhost:6379/0"
diff --git a/tests/test_db.py b/tests/test_db.py
index 8101a4a..e26c535 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-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
@@ -59,6 +59,14 @@ from environ.compat import DJANGO_POSTGRES
          '',
          ''
          ),
+        # cockroachdb://username:secret@test.example.com:26258/dbname
+        ('cockroachdb://username:secret@test.example.com:26258/dbname',
+         'django_cockroachdb',
+         'dbname',
+         'test.example.com',
+         'username',
+         'secret',
+         26258),
         # mysqlgis://user:password@host:port/dbname
         ('mysqlgis://enigma:secret@example.com:5431/dbname',
          'django.contrib.gis.db.backends.mysql',
@@ -156,6 +164,7 @@ from environ.compat import DJANGO_POSTGRES
         'postgis',
         'postgres_cluster',
         'postgres_no_ports',
+        'cockroachdb',
         'mysqlgis',
         'cleardb',
         'mysql_no_password',
@@ -188,6 +197,16 @@ def test_db_parsing(url, engine, name, host, user, passwd, port):
         assert config['OPTIONS'] == {'reconnect': 'true'}
 
 
+def test_custom_db_engine():
+    """Override ENGINE determined from schema."""
+    env_url = 'postgres://enigma:secret@example.com:5431/dbname'
+
+    engine = 'mypackage.backends.whatever'
+    url = Env.db_url_config(env_url, engine=engine)
+
+    assert url['ENGINE'] == engine
+
+
 def test_postgres_complex_db_name_parsing():
     """Make sure we can use complex postgres host."""
     env_url = (
diff --git a/tests/test_email.py b/tests/test_email.py
index 30d3813..1c9a875 100644
--- a/tests/test_email.py
+++ b/tests/test_email.py
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
@@ -22,3 +22,13 @@ def test_smtp_parsing():
     assert url['EMAIL_PORT'] == 587
     assert url['EMAIL_USE_TLS'] is True
     assert url['EMAIL_FILE_PATH'] == ''
+
+
+def test_custom_email_backend():
+    """Override EMAIL_BACKEND determined from schema."""
+    url = 'smtps://user@domain.com:password@smtp.example.com:587'
+
+    backend = 'mypackage.backends.whatever'
+    url = Env.email_url_config(url, backend=backend)
+
+    assert url['EMAIL_BACKEND'] == backend
diff --git a/tests/test_env.py b/tests/test_env.py
index 0b105f5..5f0f4c9 100644
--- a/tests/test_env.py
+++ b/tests/test_env.py
@@ -1,12 +1,13 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021-2022, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # 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 tempfile
 from urllib.parse import quote
 
 import pytest
@@ -21,6 +22,59 @@ from .asserts import assert_type_and_value
 from .fixtures import FakeEnv
 
 
+@pytest.mark.parametrize(
+        'variable,value,raw_value,parse_comments',
+        [
+            # parse_comments=True
+            ('BOOL_TRUE_STRING_LIKE_BOOL_WITH_COMMENT', 'True', "'True' # comment\n", True),
+            ('BOOL_TRUE_BOOL_WITH_COMMENT', 'True ', "True # comment\n", True),
+            ('STR_QUOTED_IGNORE_COMMENT', 'foo', " 'foo' # comment\n", True),
+            ('STR_QUOTED_INCLUDE_HASH', 'foo # with hash', "'foo # with hash' # not comment\n", True),
+            ('SECRET_KEY_1', '"abc', '"abc#def"\n', True),
+            ('SECRET_KEY_2', 'abc', 'abc#def\n', True),
+            ('SECRET_KEY_3', 'abc#def', "'abc#def'\n",  True),
+
+            # parse_comments=False
+            ('BOOL_TRUE_STRING_LIKE_BOOL_WITH_COMMENT', "'True' # comment", "'True' # comment\n", False),
+            ('BOOL_TRUE_BOOL_WITH_COMMENT', 'True # comment', "True # comment\n", False),
+            ('STR_QUOTED_IGNORE_COMMENT', " 'foo' # comment", " 'foo' # comment\n", False),
+            ('STR_QUOTED_INCLUDE_HASH', "'foo # with hash' # not comment", "'foo # with hash' # not comment\n", False),
+            ('SECRET_KEY_1', 'abc#def', '"abc#def"\n', False),
+            ('SECRET_KEY_2', 'abc#def', 'abc#def\n', False),
+            ('SECRET_KEY_3', 'abc#def', "'abc#def'\n",  False),
+
+            # parse_comments is not defined (default behavior)
+            ('BOOL_TRUE_STRING_LIKE_BOOL_WITH_COMMENT', "'True' # comment", "'True' # comment\n", None),
+            ('BOOL_TRUE_BOOL_WITH_COMMENT', 'True # comment', "True # comment\n", None),
+            ('STR_QUOTED_IGNORE_COMMENT', " 'foo' # comment", " 'foo' # comment\n", None),
+            ('STR_QUOTED_INCLUDE_HASH', "'foo # with hash' # not comment", "'foo # with hash' # not comment\n", None),
+            ('SECRET_KEY_1', 'abc#def', '"abc#def"\n', None),
+            ('SECRET_KEY_2', 'abc#def', 'abc#def\n', None),
+            ('SECRET_KEY_3', 'abc#def', "'abc#def'\n",  None),
+        ],
+    )
+def test_parse_comments(variable, value, raw_value, parse_comments):
+    old_environ = os.environ
+
+    with tempfile.TemporaryDirectory() as temp_dir:
+        env_path = os.path.join(temp_dir, '.env')
+
+        with open(env_path, 'w') as f:
+            f.write(f'{variable}={raw_value}\n')
+            f.flush()
+
+            env = Env()
+            Env.ENVIRON = {}
+            if parse_comments is None:
+                env.read_env(env_path)
+            else:
+                env.read_env(env_path, parse_comments=parse_comments)
+
+            assert env(variable) == value
+
+    os.environ = old_environ
+
+
 class TestEnv:
     def setup_method(self, method):
         """
@@ -112,10 +166,8 @@ class TestEnv:
         [
             (True, 'BOOL_TRUE_STRING_LIKE_INT'),
             (True, 'BOOL_TRUE_STRING_LIKE_BOOL'),
-            (True, 'BOOL_TRUE_STRING_LIKE_BOOL_WITH_COMMENT'),
             (True, 'BOOL_TRUE_INT'),
             (True, 'BOOL_TRUE_BOOL'),
-            (True, 'BOOL_TRUE_BOOL_WITH_COMMENT'),
             (True, 'BOOL_TRUE_STRING_1'),
             (True, 'BOOL_TRUE_STRING_2'),
             (True, 'BOOL_TRUE_STRING_3'),
@@ -341,8 +393,6 @@ class TestEnv:
 
     def test_smart_cast(self):
         assert self.env.get_value('STR_VAR', default='string') == 'bar'
-        assert self.env.get_value('STR_QUOTED_IGNORE_COMMENT', default='string') == 'foo'
-        assert self.env.get_value('STR_QUOTED_INCLUDE_HASH', default='string') == 'foo # with hash'
         assert self.env.get_value('BOOL_TRUE_STRING_LIKE_INT', default=True)
         assert not self.env.get_value(
             'BOOL_FALSE_STRING_LIKE_INT',
@@ -357,6 +407,13 @@ class TestEnv:
         self.env.prefix = 'PREFIX_'
         assert self.env('TEST') == 'foo'
 
+    def test_prefix_and_not_present_without_default(self):
+        self.env.prefix = 'PREFIX_'
+        with pytest.raises(ImproperlyConfigured) as excinfo:
+            self.env('not_present')
+        assert str(excinfo.value) == 'Set the PREFIX_not_present environment variable'
+        assert excinfo.value.__cause__ is not None
+
 
 class TestFileEnv(TestEnv):
     def setup_method(self, method):
diff --git a/tests/test_env.txt b/tests/test_env.txt
index d5480bf..39ab896 100644
--- a/tests/test_env.txt
+++ b/tests/test_env.txt
@@ -25,8 +25,6 @@ BOOL_TRUE_STRING_3='yes'
 BOOL_TRUE_STRING_4='y'
 BOOL_TRUE_STRING_5='true'
 BOOL_TRUE_BOOL=True
-BOOL_TRUE_STRING_LIKE_BOOL_WITH_COMMENT='True' # comment
-BOOL_TRUE_BOOL_WITH_COMMENT=True # comment
 BOOL_FALSE_STRING_LIKE_INT='0'
 BOOL_FALSE_INT=0
 BOOL_FALSE_STRING_LIKE_BOOL='False'
@@ -47,8 +45,6 @@ INT_VAR=42
 STR_LIST_WITH_SPACES= foo,  spaces
 STR_LIST_WITH_SPACES_QUOTED=' foo','  quoted'
 STR_VAR=bar
-STR_QUOTED_IGNORE_COMMENT= 'foo' # comment
-STR_QUOTED_INCLUDE_HASH='foo # with hash' # not comment
 MULTILINE_STR_VAR=foo\nbar
 MULTILINE_QUOTED_STR_VAR="---BEGIN---\r\n---END---"
 MULTILINE_ESCAPED_STR_VAR=---BEGIN---\\n---END---
diff --git a/tests/test_fileaware.py b/tests/test_fileaware.py
index f411b7d..00442ae 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-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/tests/test_path.py b/tests/test_path.py
index b97f4ca..5d868d4 100644
--- a/tests/test_path.py
+++ b/tests/test_path.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-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/tests/test_schema.py b/tests/test_schema.py
index 7a7f62e..45f0e98 100644
--- a/tests/test_schema.py
+++ b/tests/test_schema.py
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
diff --git a/tests/test_search.py b/tests/test_search.py
index a6d8f06..38f11f7 100644
--- a/tests/test_search.py
+++ b/tests/test_search.py
@@ -1,6 +1,6 @@
 # This file is part of the django-environ.
 #
-# Copyright (c) 2021, Serghei Iakovlev <egrep@protonmail.ch>
+# Copyright (c) 2021-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view
@@ -86,6 +86,16 @@ def test_elasticsearch_parsing(url, engine, scheme):
     assert url["URL"].startswith(scheme + ":")
 
 
+def test_custom_search_engine():
+    """Override ENGINE determined from schema."""
+    env_url = 'elasticsearch://127.0.0.1:9200/index'
+
+    engine = 'mypackage.backends.whatever'
+    url = Env.db_url_config(env_url, engine=engine)
+
+    assert url['ENGINE'] == engine
+
+
 @pytest.mark.parametrize('storage', ['file', 'ram'])
 def test_whoosh_parsing(whoosh_url, storage):
     post_limit = 128 * 1024 * 1024
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 2f3d32e..0200717 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.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-2024, Serghei Iakovlev <oss@serghei.pl>
 # Copyright (c) 2013-2021, Daniele Faraglia <daniele.faraglia@gmail.com>
 #
 # For the full copyright and license information, please view