Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-dj-database-url for 
openSUSE:Factory checked in at 2025-07-14 10:52:09
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-dj-database-url (Old)
 and      /work/SRC/openSUSE:Factory/.python-dj-database-url.new.7373 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-dj-database-url"

Mon Jul 14 10:52:09 2025 rev:12 rq:1292511 version:3.0.1

Changes:
--------
--- 
/work/SRC/openSUSE:Factory/python-dj-database-url/python-dj-database-url.changes
    2024-10-24 15:43:26.321824967 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-dj-database-url.new.7373/python-dj-database-url.changes
  2025-07-14 10:57:58.336226937 +0200
@@ -1,0 +2,9 @@
+Sun Jul 13 13:38:08 UTC 2025 - Dirk Müller <dmuel...@suse.com>
+
+- update to 3.0.1:
+  * Re-drop dependency on `typing_extensions`
+- update to 3.0.0:
+  * Drop dependency on `typing_extensions`
+  * Fix type errors
+
+-------------------------------------------------------------------

Old:
----
  dj_database_url-2.3.0.tar.gz

New:
----
  dj_database_url-3.0.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-dj-database-url.spec ++++++
--- /var/tmp/diff_new_pack.eu1s4K/_old  2025-07-14 10:57:58.956252641 +0200
+++ /var/tmp/diff_new_pack.eu1s4K/_new  2025-07-14 10:57:58.960252807 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package python-dj-database-url
 #
-# Copyright (c) 2024 SUSE LLC
+# Copyright (c) 2025 SUSE LLC
 #
 # All modifications and additions to the file contributed by third parties
 # remain the property of their copyright owners, unless otherwise agreed
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-dj-database-url
-Version:        2.3.0
+Version:        3.0.1
 Release:        0
 Summary:        Utility to use database URLs in Django applications
 License:        BSD-3-Clause
@@ -34,7 +34,6 @@
 BuildRequires:  fdupes
 BuildRequires:  python-rpm-macros
 Requires:       python-Django > 4.2
-Requires:       python-typing_extensions >= 3.10
 BuildArch:      noarch
 %python_subpackages
 

++++++ dj_database_url-2.3.0.tar.gz -> dj_database_url-3.0.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dj_database_url-2.3.0/PKG-INFO 
new/dj_database_url-3.0.1/PKG-INFO
--- old/dj_database_url-2.3.0/PKG-INFO  2024-10-23 12:01:44.752528700 +0200
+++ new/dj_database_url-3.0.1/PKG-INFO  2025-07-02 11:29:20.456748700 +0200
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: dj-database-url
-Version: 2.3.0
+Version: 3.0.1
 Summary: Use Database URLs in your Django Application.
 Home-page: https://github.com/jazzband/dj-database-url
 Author: Original Author: Kenneth Reitz, Maintained by: JazzBand Community
@@ -13,6 +13,7 @@
 Classifier: Framework :: Django :: 4.2
 Classifier: Framework :: Django :: 5.0
 Classifier: Framework :: Django :: 5.1
+Classifier: Framework :: Django :: 5.2
 Classifier: Intended Audience :: Developers
 Classifier: License :: OSI Approved :: BSD License
 Classifier: Operating System :: OS Independent
@@ -21,15 +22,25 @@
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
 Description-Content-Type: text/x-rst
 License-File: LICENSE
 Requires-Dist: Django>=4.2
-Requires-Dist: typing_extensions>=3.10.0.0
+Dynamic: author
+Dynamic: classifier
+Dynamic: description
+Dynamic: description-content-type
+Dynamic: home-page
+Dynamic: license
+Dynamic: license-file
+Dynamic: platform
+Dynamic: project-url
+Dynamic: requires-dist
+Dynamic: summary
 
 DJ-Database-URL
 ~~~~~~~~~~~~~~~
@@ -55,12 +66,6 @@
 If you'd rather not use an environment variable, you can pass a URL in directly
 instead to ``dj_database_url.parse``.
 
-Supported Databases
--------------------
-
-Support currently exists for PostgreSQL, PostGIS, MySQL, MySQL (GIS),
-Oracle, Oracle (GIS), Redshift, CockroachDB, Timescale, Timescale (GIS) and 
SQLite.
-
 Installation
 ------------
 
@@ -181,6 +186,63 @@
     DATABASES['default'] = dj_database_url.config(default='postgres://...', 
test_options={'NAME': 'mytestdatabase'})
 
 
+Supported Databases
+-------------------
+
+Support currently exists for PostgreSQL, PostGIS, MySQL, MySQL (GIS),
+Oracle, Oracle (GIS), Redshift, CockroachDB, Timescale, Timescale (GIS) and 
SQLite.
+
+If you want to use
+some non-default backends, you need to register them first:
+
+.. code-block:: python
+
+    import dj_database_url
+
+    # registration should be performed only once
+    dj_database_url.register("mysql-connector", "mysql.connector.django")
+
+    assert 
dj_database_url.parse("mysql-connector://user:password@host:port/db-name") == {
+        "ENGINE": "mysql.connector.django",
+        # ...other connection params
+    }
+
+Some backends need further config adjustments (e.g. oracle and mssql
+expect ``PORT`` to be a string). For such cases you can provide a
+post-processing function to ``register()`` (note that ``register()`` is
+used as a **decorator(!)** in this case):
+
+.. code-block:: python
+
+    import dj_database_url
+
+    @dj_database_url.register("mssql", "sql_server.pyodbc")
+    def stringify_port(config):
+        config["PORT"] = str(config["PORT"])
+
+    @dj_database_url.register("redshift", "django_redshift_backend")
+    def apply_current_schema(config):
+        options = config["OPTIONS"]
+        schema = options.pop("currentSchema", None)
+        if schema:
+            options["options"] = f"-c search_path={schema}"
+
+    @dj_database_url.register("snowflake", "django_snowflake")
+    def adjust_snowflake_config(config):
+        config.pop("PORT", None)
+        config["ACCOUNT"] = config.pop("HOST")
+        name, _, schema = config["NAME"].partition("/")
+        if schema:
+            config["SCHEMA"] = schema
+            config["NAME"] = name
+        options = config.get("OPTIONS", {})
+        warehouse = options.pop("warehouse", None)
+        if warehouse:
+            config["WAREHOUSE"] = warehouse
+        role = options.pop("role", None)
+        if role:
+            config["ROLE"] = role
+
 URL schema
 ----------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dj_database_url-2.3.0/README.rst 
new/dj_database_url-3.0.1/README.rst
--- old/dj_database_url-2.3.0/README.rst        2024-10-23 12:01:36.000000000 
+0200
+++ new/dj_database_url-3.0.1/README.rst        2025-07-02 11:29:13.000000000 
+0200
@@ -22,12 +22,6 @@
 If you'd rather not use an environment variable, you can pass a URL in directly
 instead to ``dj_database_url.parse``.
 
-Supported Databases
--------------------
-
-Support currently exists for PostgreSQL, PostGIS, MySQL, MySQL (GIS),
-Oracle, Oracle (GIS), Redshift, CockroachDB, Timescale, Timescale (GIS) and 
SQLite.
-
 Installation
 ------------
 
@@ -148,6 +142,63 @@
     DATABASES['default'] = dj_database_url.config(default='postgres://...', 
test_options={'NAME': 'mytestdatabase'})
 
 
+Supported Databases
+-------------------
+
+Support currently exists for PostgreSQL, PostGIS, MySQL, MySQL (GIS),
+Oracle, Oracle (GIS), Redshift, CockroachDB, Timescale, Timescale (GIS) and 
SQLite.
+
+If you want to use
+some non-default backends, you need to register them first:
+
+.. code-block:: python
+
+    import dj_database_url
+
+    # registration should be performed only once
+    dj_database_url.register("mysql-connector", "mysql.connector.django")
+
+    assert 
dj_database_url.parse("mysql-connector://user:password@host:port/db-name") == {
+        "ENGINE": "mysql.connector.django",
+        # ...other connection params
+    }
+
+Some backends need further config adjustments (e.g. oracle and mssql
+expect ``PORT`` to be a string). For such cases you can provide a
+post-processing function to ``register()`` (note that ``register()`` is
+used as a **decorator(!)** in this case):
+
+.. code-block:: python
+
+    import dj_database_url
+
+    @dj_database_url.register("mssql", "sql_server.pyodbc")
+    def stringify_port(config):
+        config["PORT"] = str(config["PORT"])
+
+    @dj_database_url.register("redshift", "django_redshift_backend")
+    def apply_current_schema(config):
+        options = config["OPTIONS"]
+        schema = options.pop("currentSchema", None)
+        if schema:
+            options["options"] = f"-c search_path={schema}"
+
+    @dj_database_url.register("snowflake", "django_snowflake")
+    def adjust_snowflake_config(config):
+        config.pop("PORT", None)
+        config["ACCOUNT"] = config.pop("HOST")
+        name, _, schema = config["NAME"].partition("/")
+        if schema:
+            config["SCHEMA"] = schema
+            config["NAME"] = name
+        options = config.get("OPTIONS", {})
+        warehouse = options.pop("warehouse", None)
+        if warehouse:
+            config["WAREHOUSE"] = warehouse
+        role = options.pop("role", None)
+        if role:
+            config["ROLE"] = role
+
 URL schema
 ----------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dj_database_url-2.3.0/dj_database_url/__init__.py 
new/dj_database_url-3.0.1/dj_database_url/__init__.py
--- old/dj_database_url-2.3.0/dj_database_url/__init__.py       2024-10-23 
12:01:36.000000000 +0200
+++ new/dj_database_url-3.0.1/dj_database_url/__init__.py       2025-07-02 
11:29:13.000000000 +0200
@@ -1,50 +1,13 @@
 import logging
 import os
 import urllib.parse as urlparse
-from typing import Any, Dict, Optional, Union
-
-from typing_extensions import TypedDict
+from typing import Any, Callable, Dict, List, Optional, TypedDict, Union
 
 DEFAULT_ENV = "DATABASE_URL"
-
-SCHEMES = {
-    "postgres": "django.db.backends.postgresql",
-    "postgresql": "django.db.backends.postgresql",
-    "pgsql": "django.db.backends.postgresql",
-    "postgis": "django.contrib.gis.db.backends.postgis",
-    "mysql": "django.db.backends.mysql",
-    "mysql2": "django.db.backends.mysql",
-    "mysqlgis": "django.contrib.gis.db.backends.mysql",
-    "mysql-connector": "mysql.connector.django",
-    "mssql": "sql_server.pyodbc",
-    "mssqlms": "mssql",
-    "spatialite": "django.contrib.gis.db.backends.spatialite",
-    "sqlite": "django.db.backends.sqlite3",
-    "oracle": "django.db.backends.oracle",
-    "oraclegis": "django.contrib.gis.db.backends.oracle",
-    "redshift": "django_redshift_backend",
-    "cockroach": "django_cockroachdb",
-    "timescale": "timescale.db.backends.postgresql",
-    "timescalegis": "timescale.db.backends.postgis",
-}
-
-SCHEMES_WITH_SEARCH_PATH = [
-    "postgres",
-    "postgresql",
-    "pgsql",
-    "postgis",
-    "redshift",
-    "timescale",
-    "timescalegis",
-]
-
-# Register database schemes in URLs.
-for key in SCHEMES.keys():
-    urlparse.uses_netloc.append(key)
-del key  # pyright: ignore[reportPossiblyUnboundVariable]
+ENGINE_SCHEMES: Dict[str, "Engine"] = {}
 
 
-# From https://docs.djangoproject.com/en/4.0/ref/settings/#databases
+# From https://docs.djangoproject.com/en/stable/ref/settings/#databases
 class DBConfig(TypedDict, total=False):
     ATOMIC_REQUESTS: bool
     AUTOCOMMIT: bool
@@ -62,11 +25,109 @@
     USER: str
 
 
+PostprocessCallable = Callable[[DBConfig], None]
+OptionType = Union[int, str, bool]
+
+
+class ParseError(ValueError):
+    def __str__(self) -> str:
+        return (
+            "This string is not a valid url, possibly because some of its 
parts"
+            " is not properly urllib.parse.quote()'ed."
+        )
+
+
+class UnknownSchemeError(ValueError):
+    def __init__(self, scheme: str) -> None:
+        self.scheme = scheme
+
+    def __str__(self) -> str:
+        schemes = ", ".join(sorted(ENGINE_SCHEMES.keys()))
+        return (
+            f"Scheme '{self.scheme}://' is unknown."
+            " Did you forget to register custom backend?"
+            f" Following schemes have registered backends: {schemes}."
+        )
+
+
+def default_postprocess(parsed_config: DBConfig) -> None:
+    pass
+
+
+class Engine:
+    def __init__(
+        self,
+        backend: str,
+        postprocess: PostprocessCallable = default_postprocess,
+    ) -> None:
+        self.backend = backend
+        self.postprocess = postprocess
+
+
+def register(
+    scheme: str, backend: str
+) -> Callable[[PostprocessCallable], PostprocessCallable]:
+    engine = Engine(backend)
+    if scheme not in ENGINE_SCHEMES:
+        urlparse.uses_netloc.append(scheme)
+    ENGINE_SCHEMES[scheme] = engine
+
+    def inner(func: PostprocessCallable) -> PostprocessCallable:
+        engine.postprocess = func
+        return func
+
+    return inner
+
+
+register("spatialite", "django.contrib.gis.db.backends.spatialite")
+register("mysql-connector", "mysql.connector.django")
+register("mysqlgis", "django.contrib.gis.db.backends.mysql")
+register("oraclegis", "django.contrib.gis.db.backends.oracle")
+register("cockroach", "django_cockroachdb")
+
+
+@register("sqlite", "django.db.backends.sqlite3")
+def default_to_in_memory_db(parsed_config: DBConfig) -> None:
+    # mimic sqlalchemy behaviour
+    if not parsed_config.get("NAME"):
+        parsed_config["NAME"] = ":memory:"
+
+
+@register("oracle", "django.db.backends.oracle")
+@register("mssqlms", "mssql")
+@register("mssql", "sql_server.pyodbc")
+def stringify_port(parsed_config: DBConfig) -> None:
+    parsed_config["PORT"] = str(parsed_config.get("PORT", ""))
+
+
+@register("mysql", "django.db.backends.mysql")
+@register("mysql2", "django.db.backends.mysql")
+def apply_ssl_ca(parsed_config: DBConfig) -> None:
+    options = parsed_config.get("OPTIONS", {})
+    ca = options.pop("ssl-ca", None)
+    if ca:
+        options["ssl"] = {"ca": ca}
+
+
+@register("postgres", "django.db.backends.postgresql")
+@register("postgresql", "django.db.backends.postgresql")
+@register("pgsql", "django.db.backends.postgresql")
+@register("postgis", "django.contrib.gis.db.backends.postgis")
+@register("redshift", "django_redshift_backend")
+@register("timescale", "timescale.db.backends.postgresql")
+@register("timescalegis", "timescale.db.backends.postgis")
+def apply_current_schema(parsed_config: DBConfig) -> None:
+    options = parsed_config.get("OPTIONS", {})
+    schema = options.pop("currentSchema", None)
+    if schema:
+        options["options"] = f"-c search_path={schema}"
+
+
 def config(
     env: str = DEFAULT_ENV,
     default: Optional[str] = None,
     engine: Optional[str] = None,
-    conn_max_age: Optional[int] = 0,
+    conn_max_age: int = 0,
     conn_health_checks: bool = False,
     disable_server_side_cursors: bool = False,
     ssl_require: bool = False,
@@ -77,7 +138,7 @@
 
     if s is None:
         logging.warning(
-            "No %s environment variable set, and so no databases setup" % env
+            "No %s environment variable set, and so no databases setup", env
         )
 
     if s:
@@ -97,107 +158,95 @@
 def parse(
     url: str,
     engine: Optional[str] = None,
-    conn_max_age: Optional[int] = 0,
+    conn_max_age: int = 0,
     conn_health_checks: bool = False,
     disable_server_side_cursors: bool = False,
     ssl_require: bool = False,
     test_options: Optional[Dict[str, Any]] = None,
 ) -> DBConfig:
-    """Parses a database URL."""
+    """Parses a database URL and returns configured DATABASE dictionary."""
+    settings = _convert_to_settings(
+        engine,
+        conn_max_age,
+        conn_health_checks,
+        disable_server_side_cursors,
+        ssl_require,
+        test_options,
+    )
+
     if url == "sqlite://:memory:":
         # this is a special case, because if we pass this URL into
         # urlparse, urlparse will choke trying to interpret "memory"
         # as a port number
-        return {"ENGINE": SCHEMES["sqlite"], "NAME": ":memory:"}
+        return {"ENGINE": ENGINE_SCHEMES["sqlite"].backend, "NAME": ":memory:"}
         # note: no other settings are required for sqlite
 
-    # otherwise parse the url as normal
-    parsed_config: DBConfig = {}
-
-    if test_options is None:
-        test_options = {}
-
-    spliturl = urlparse.urlsplit(url)
-
-    # Split query strings from path.
-    path = spliturl.path[1:]
-    query = urlparse.parse_qs(spliturl.query)
-
-    # If we are using sqlite and we have no path, then assume we
-    # want an in-memory database (this is the behaviour of sqlalchemy)
-    if spliturl.scheme == "sqlite" and path == "":
-        path = ":memory:"
-
-    # Handle postgres percent-encoded paths.
-    hostname = spliturl.hostname or ""
-    if "%" in hostname:
-        # Switch to url.netloc to avoid lower cased paths
-        hostname = spliturl.netloc
-        if "@" in hostname:
-            hostname = hostname.rsplit("@", 1)[1]
-        # Use URL Parse library to decode % encodes
-        hostname = urlparse.unquote(hostname)
-
-    # Lookup specified engine.
-    if engine is None:
-        engine = SCHEMES.get(spliturl.scheme)
-        if engine is None:
-            raise ValueError(
-                "No support for '%s'. We support: %s"
-                % (spliturl.scheme, ", ".join(sorted(SCHEMES.keys())))
-            )
-
-    port = (
-        str(spliturl.port)
-        if spliturl.port
-        and engine in (SCHEMES["oracle"], SCHEMES["mssql"], SCHEMES["mssqlms"])
-        else spliturl.port
-    )
-
-    # Update with environment configuration.
-    parsed_config.update(
-        {
-            "NAME": urlparse.unquote(path or ""),
-            "USER": urlparse.unquote(spliturl.username or ""),
-            "PASSWORD": urlparse.unquote(spliturl.password or ""),
-            "HOST": hostname,
-            "PORT": port or "",
-            "CONN_MAX_AGE": conn_max_age,
-            "CONN_HEALTH_CHECKS": conn_health_checks,
-            "DISABLE_SERVER_SIDE_CURSORS": disable_server_side_cursors,
-            "ENGINE": engine,
+    try:
+        split_result = urlparse.urlsplit(url)
+        engine_obj = ENGINE_SCHEMES.get(split_result.scheme)
+        if engine_obj is None:
+            raise UnknownSchemeError(split_result.scheme)
+        path = split_result.path[1:]
+        query = urlparse.parse_qs(split_result.query)
+        options = {k: _parse_option_values(v) for k, v in query.items()}
+        parsed_config: DBConfig = {
+            "ENGINE": engine_obj.backend,
+            "USER": urlparse.unquote(split_result.username or ""),
+            "PASSWORD": urlparse.unquote(split_result.password or ""),
+            "HOST": urlparse.unquote(split_result.hostname or ""),
+            "PORT": split_result.port or "",
+            "NAME": urlparse.unquote(path),
+            "OPTIONS": options,
         }
-    )
-    if test_options:
-        parsed_config.update(
-            {
-                'TEST': test_options,
-            }
-        )
+    except UnknownSchemeError:
+        raise
+    except ValueError:
+        raise ParseError() from None
+
+    # Guarantee that config has options, possibly empty, when postprocess() is 
called
+    assert isinstance(parsed_config["OPTIONS"], dict)
+    engine_obj.postprocess(parsed_config)
+
+    # Update the final config with any settings passed in explicitly.
+    parsed_config["OPTIONS"].update(settings.pop("OPTIONS", {}))
+    parsed_config.update(settings)
 
-    # Pass the query string into OPTIONS.
-    options: Dict[str, Any] = {}
-    for key, values in query.items():
-        if spliturl.scheme == "mysql" and key == "ssl-ca":
-            options["ssl"] = {"ca": values[-1]}
-            continue
-
-        value = values[-1]
-        if value.isdigit():
-            options[key] = int(value)
-        elif value.lower() in ("true", "false"):
-            options[key] = value.lower() == "true"
-        else:
-            options[key] = value
-
-    if ssl_require:
-        options["sslmode"] = "require"
-
-    # Support for Postgres Schema URLs
-    if "currentSchema" in options and spliturl.scheme in 
SCHEMES_WITH_SEARCH_PATH:
-        options["options"] = "-c 
search_path={0}".format(options.pop("currentSchema"))
+    if not parsed_config["OPTIONS"]:
+        parsed_config.pop("OPTIONS")
+    return parsed_config
 
-    if options:
-        parsed_config["OPTIONS"] = options
 
-    return parsed_config
+def _parse_option_values(values: List[str]) -> Union[OptionType, 
List[OptionType]]:
+    parsed_values = [_parse_value(v) for v in values]
+    return parsed_values[0] if len(parsed_values) == 1 else parsed_values
+
+
+def _parse_value(value: str) -> OptionType:
+    if value.isdigit():
+        return int(value)
+    if value.lower() in ("true", "false"):
+        return value.lower() == "true"
+    return value
+
+
+def _convert_to_settings(
+    engine: Optional[str],
+    conn_max_age: int,
+    conn_health_checks: bool,
+    disable_server_side_cursors: bool,
+    ssl_require: bool,
+    test_options: Optional[dict[str, Any]],
+) -> DBConfig:
+    settings: DBConfig = {
+        "CONN_MAX_AGE": conn_max_age,
+        "CONN_HEALTH_CHECKS": conn_health_checks,
+        "DISABLE_SERVER_SIDE_CURSORS": disable_server_side_cursors,
+    }
+    if engine:
+        settings["ENGINE"] = engine
+    if ssl_require:
+        settings["OPTIONS"] = {}
+        settings["OPTIONS"]["sslmode"] = "require"
+    if test_options:
+        settings["TEST"] = test_options
+    return settings
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/dj_database_url-2.3.0/dj_database_url.egg-info/PKG-INFO 
new/dj_database_url-3.0.1/dj_database_url.egg-info/PKG-INFO
--- old/dj_database_url-2.3.0/dj_database_url.egg-info/PKG-INFO 2024-10-23 
12:01:44.000000000 +0200
+++ new/dj_database_url-3.0.1/dj_database_url.egg-info/PKG-INFO 2025-07-02 
11:29:20.000000000 +0200
@@ -1,6 +1,6 @@
-Metadata-Version: 2.1
+Metadata-Version: 2.4
 Name: dj-database-url
-Version: 2.3.0
+Version: 3.0.1
 Summary: Use Database URLs in your Django Application.
 Home-page: https://github.com/jazzband/dj-database-url
 Author: Original Author: Kenneth Reitz, Maintained by: JazzBand Community
@@ -13,6 +13,7 @@
 Classifier: Framework :: Django :: 4.2
 Classifier: Framework :: Django :: 5.0
 Classifier: Framework :: Django :: 5.1
+Classifier: Framework :: Django :: 5.2
 Classifier: Intended Audience :: Developers
 Classifier: License :: OSI Approved :: BSD License
 Classifier: Operating System :: OS Independent
@@ -21,15 +22,25 @@
 Classifier: Topic :: Software Development :: Libraries :: Python Modules
 Classifier: Programming Language :: Python
 Classifier: Programming Language :: Python :: 3
-Classifier: Programming Language :: Python :: 3.9
 Classifier: Programming Language :: Python :: 3.10
 Classifier: Programming Language :: Python :: 3.11
 Classifier: Programming Language :: Python :: 3.12
 Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
 Description-Content-Type: text/x-rst
 License-File: LICENSE
 Requires-Dist: Django>=4.2
-Requires-Dist: typing_extensions>=3.10.0.0
+Dynamic: author
+Dynamic: classifier
+Dynamic: description
+Dynamic: description-content-type
+Dynamic: home-page
+Dynamic: license
+Dynamic: license-file
+Dynamic: platform
+Dynamic: project-url
+Dynamic: requires-dist
+Dynamic: summary
 
 DJ-Database-URL
 ~~~~~~~~~~~~~~~
@@ -55,12 +66,6 @@
 If you'd rather not use an environment variable, you can pass a URL in directly
 instead to ``dj_database_url.parse``.
 
-Supported Databases
--------------------
-
-Support currently exists for PostgreSQL, PostGIS, MySQL, MySQL (GIS),
-Oracle, Oracle (GIS), Redshift, CockroachDB, Timescale, Timescale (GIS) and 
SQLite.
-
 Installation
 ------------
 
@@ -181,6 +186,63 @@
     DATABASES['default'] = dj_database_url.config(default='postgres://...', 
test_options={'NAME': 'mytestdatabase'})
 
 
+Supported Databases
+-------------------
+
+Support currently exists for PostgreSQL, PostGIS, MySQL, MySQL (GIS),
+Oracle, Oracle (GIS), Redshift, CockroachDB, Timescale, Timescale (GIS) and 
SQLite.
+
+If you want to use
+some non-default backends, you need to register them first:
+
+.. code-block:: python
+
+    import dj_database_url
+
+    # registration should be performed only once
+    dj_database_url.register("mysql-connector", "mysql.connector.django")
+
+    assert 
dj_database_url.parse("mysql-connector://user:password@host:port/db-name") == {
+        "ENGINE": "mysql.connector.django",
+        # ...other connection params
+    }
+
+Some backends need further config adjustments (e.g. oracle and mssql
+expect ``PORT`` to be a string). For such cases you can provide a
+post-processing function to ``register()`` (note that ``register()`` is
+used as a **decorator(!)** in this case):
+
+.. code-block:: python
+
+    import dj_database_url
+
+    @dj_database_url.register("mssql", "sql_server.pyodbc")
+    def stringify_port(config):
+        config["PORT"] = str(config["PORT"])
+
+    @dj_database_url.register("redshift", "django_redshift_backend")
+    def apply_current_schema(config):
+        options = config["OPTIONS"]
+        schema = options.pop("currentSchema", None)
+        if schema:
+            options["options"] = f"-c search_path={schema}"
+
+    @dj_database_url.register("snowflake", "django_snowflake")
+    def adjust_snowflake_config(config):
+        config.pop("PORT", None)
+        config["ACCOUNT"] = config.pop("HOST")
+        name, _, schema = config["NAME"].partition("/")
+        if schema:
+            config["SCHEMA"] = schema
+            config["NAME"] = name
+        options = config.get("OPTIONS", {})
+        warehouse = options.pop("warehouse", None)
+        if warehouse:
+            config["WAREHOUSE"] = warehouse
+        role = options.pop("role", None)
+        if role:
+            config["ROLE"] = role
+
 URL schema
 ----------
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/dj_database_url-2.3.0/dj_database_url.egg-info/requires.txt 
new/dj_database_url-3.0.1/dj_database_url.egg-info/requires.txt
--- old/dj_database_url-2.3.0/dj_database_url.egg-info/requires.txt     
2024-10-23 12:01:44.000000000 +0200
+++ new/dj_database_url-3.0.1/dj_database_url.egg-info/requires.txt     
2025-07-02 11:29:20.000000000 +0200
@@ -1,2 +1 @@
 Django>=4.2
-typing_extensions>=3.10.0.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dj_database_url-2.3.0/setup.py 
new/dj_database_url-3.0.1/setup.py
--- old/dj_database_url-2.3.0/setup.py  2024-10-23 12:01:36.000000000 +0200
+++ new/dj_database_url-3.0.1/setup.py  2025-07-02 11:29:13.000000000 +0200
@@ -6,7 +6,7 @@
 
 setup(
     name="dj-database-url",
-    version="2.3.0",
+    version="3.0.1",
     url="https://github.com/jazzband/dj-database-url";,
     license="BSD",
     author="Original Author: Kenneth Reitz, Maintained by: JazzBand Community",
@@ -14,7 +14,7 @@
     long_description=readme,
     long_description_content_type="text/x-rst",
     packages=["dj_database_url"],
-    install_requires=["Django>=4.2", "typing_extensions >= 3.10.0.0"],
+    install_requires=["Django>=4.2"],
     include_package_data=True,
     package_data={
         "dj_database_url": ["py.typed"],
@@ -32,6 +32,7 @@
         "Framework :: Django :: 4.2",
         "Framework :: Django :: 5.0",
         "Framework :: Django :: 5.1",
+        "Framework :: Django :: 5.2",
         "Intended Audience :: Developers",
         "License :: OSI Approved :: BSD License",
         "Operating System :: OS Independent",
@@ -40,10 +41,10 @@
         "Topic :: Software Development :: Libraries :: Python Modules",
         "Programming Language :: Python",
         "Programming Language :: Python :: 3",
-        "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 :: 3.14",
     ],
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/dj_database_url-2.3.0/tests/test_dj_database_url.py 
new/dj_database_url-3.0.1/tests/test_dj_database_url.py
--- old/dj_database_url-2.3.0/tests/test_dj_database_url.py     2024-10-23 
12:01:36.000000000 +0200
+++ new/dj_database_url-3.0.1/tests/test_dj_database_url.py     2025-07-02 
11:29:13.000000000 +0200
@@ -1,8 +1,10 @@
 # pyright: reportTypedDictNotRequiredAccess=false
 
 import os
+import re
 import unittest
 from unittest import mock
+from urllib.parse import uses_netloc
 
 import dj_database_url
 
@@ -10,9 +12,10 @@
 
 
 class DatabaseTestSuite(unittest.TestCase):
-    def test_postgres_parsing(self):
-        url = 
"postgres://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+    def test_postgres_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"postgres://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "django.db.backends.postgresql"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -21,9 +24,10 @@
         assert url["PASSWORD"] == "wegauwhgeuioweg"
         assert url["PORT"] == 5431
 
-    def test_postgres_unix_socket_parsing(self):
-        url = "postgres://%2Fvar%2Frun%2Fpostgresql/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+    def test_postgres_unix_socket_parsing(self) -> None:
+        url = dj_database_url.parse(
+            "postgres://%2Fvar%2Frun%2Fpostgresql/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "django.db.backends.postgresql"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -32,8 +36,9 @@
         assert url["PASSWORD"] == ""
         assert url["PORT"] == ""
 
-        url = "postgres://%2FUsers%2Fpostgres%2FRuN/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+        url = dj_database_url.parse(
+            "postgres://%2FUsers%2Fpostgres%2FRuN/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "django.db.backends.postgresql"
         assert url["HOST"] == "/Users/postgres/RuN"
@@ -41,9 +46,10 @@
         assert url["PASSWORD"] == ""
         assert url["PORT"] == ""
 
-    def test_postgres_google_cloud_parsing(self):
-        url = 
"postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@%2Fcloudsql%2Fproject_id%3Aregion%3Ainstance_id/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+    def test_postgres_google_cloud_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@%2Fcloudsql%2Fproject_id%3Aregion%3Ainstance_id/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "django.db.backends.postgresql"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -52,9 +58,10 @@
         assert url["PASSWORD"] == "wegauwhgeuioweg"
         assert url["PORT"] == ""
 
-    def test_ipv6_parsing(self):
-        url = 
"postgres://ieRaekei9wilaim7:wegauwhgeuioweg@[2001:db8:1234::1234:5678:90af]:5431/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+    def test_ipv6_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"postgres://ieRaekei9wilaim7:wegauwhgeuioweg@[2001:db8:1234::1234:5678:90af]:5431/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "django.db.backends.postgresql"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -63,9 +70,10 @@
         assert url["PASSWORD"] == "wegauwhgeuioweg"
         assert url["PORT"] == 5431
 
-    def test_postgres_search_path_parsing(self):
-        url = 
"postgres://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
-        url = dj_database_url.parse(url)
+    def test_postgres_search_path_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"postgres://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
+        )
         assert url["ENGINE"] == "django.db.backends.postgresql"
         assert url["NAME"] == "d8r82722r2kuvn"
         assert url["HOST"] == "ec2-107-21-253-135.compute-1.amazonaws.com"
@@ -75,9 +83,10 @@
         assert url["OPTIONS"]["options"] == "-c search_path=otherschema"
         assert "currentSchema" not in url["OPTIONS"]
 
-    def test_postgres_parsing_with_special_characters(self):
-        url = 
"postgres://%23user:%23passw...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/%23database"
-        url = dj_database_url.parse(url)
+    def test_postgres_parsing_with_special_characters(self) -> None:
+        url = dj_database_url.parse(
+            
"postgres://%23user:%23passw...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/%23database"
+        )
 
         assert url["ENGINE"] == "django.db.backends.postgresql"
         assert url["NAME"] == "#database"
@@ -86,9 +95,10 @@
         assert url["PASSWORD"] == "#password"
         assert url["PORT"] == 5431
 
-    def test_postgres_parsing_with_int_bool_str_query_string(self):
-        url = 
"postgres://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?server_side_binding=true&timeout=20&service=my_service&passfile=.my_pgpass"
-        url = dj_database_url.parse(url)
+    def test_postgres_parsing_with_int_bool_str_query_string(self) -> None:
+        url = dj_database_url.parse(
+            
"postgres://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?server_side_binding=true&timeout=20&service=my_service&passfile=.my_pgpass"
+        )
 
         assert url["ENGINE"] == "django.db.backends.postgresql"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -101,9 +111,10 @@
         assert url["OPTIONS"]["service"] == "my_service"
         assert url["OPTIONS"]["passfile"] == ".my_pgpass"
 
-    def test_postgis_parsing(self):
-        url = 
"postgis://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+    def test_postgis_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"postgis://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "django.contrib.gis.db.backends.postgis"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -112,9 +123,10 @@
         assert url["PASSWORD"] == "wegauwhgeuioweg"
         assert url["PORT"] == 5431
 
-    def test_postgis_search_path_parsing(self):
-        url = 
"postgis://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
-        url = dj_database_url.parse(url)
+    def test_postgis_search_path_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"postgis://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
+        )
         assert url["ENGINE"] == "django.contrib.gis.db.backends.postgis"
         assert url["NAME"] == "d8r82722r2kuvn"
         assert url["HOST"] == "ec2-107-21-253-135.compute-1.amazonaws.com"
@@ -124,9 +136,10 @@
         assert url["OPTIONS"]["options"] == "-c search_path=otherschema"
         assert "currentSchema" not in url["OPTIONS"]
 
-    def test_mysql_gis_parsing(self):
-        url = 
"mysqlgis://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+    def test_mysql_gis_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"mysqlgis://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "django.contrib.gis.db.backends.mysql"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -135,9 +148,10 @@
         assert url["PASSWORD"] == "wegauwhgeuioweg"
         assert url["PORT"] == 5431
 
-    def test_mysql_connector_parsing(self):
-        url = 
"mysql-connector://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+    def test_mysql_connector_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"mysql-connector://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "mysql.connector.django"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -146,7 +160,7 @@
         assert url["PASSWORD"] == "wegauwhgeuioweg"
         assert url["PORT"] == 5431
 
-    def test_config_test_options(self):
+    def test_config_test_options(self) -> None:
         with mock.patch.dict(
             os.environ,
             {
@@ -160,9 +174,10 @@
 
         assert url['TEST']['NAME'] == 'mytestdatabase'
 
-    def test_cleardb_parsing(self):
-        url = 
"mysql://bea6eb025ca0d8:69772...@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true"
-        url = dj_database_url.parse(url)
+    def test_cleardb_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"mysql://bea6eb025ca0d8:69772...@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true"
+        )
 
         assert url["ENGINE"] == "django.db.backends.mysql"
         assert url["NAME"] == "heroku_97681db3eff7580"
@@ -171,7 +186,7 @@
         assert url["PASSWORD"] == "69772142"
         assert url["PORT"] == ""
 
-    def test_database_url(self):
+    def test_database_url(self) -> None:
         with mock.patch.dict(os.environ, clear=True):
             a = dj_database_url.config()
         assert not a
@@ -191,28 +206,46 @@
         assert url["PASSWORD"] == "wegauwhgeuioweg"
         assert url["PORT"] == 5431
 
-    def test_empty_sqlite_url(self):
-        url = "sqlite://"
-        url = dj_database_url.parse(url)
+    def test_empty_sqlite_url(self) -> None:
+        url = dj_database_url.parse("sqlite://")
 
         assert url["ENGINE"] == "django.db.backends.sqlite3"
         assert url["NAME"] == ":memory:"
 
-    def test_memory_sqlite_url(self):
-        url = "sqlite://:memory:"
-        url = dj_database_url.parse(url)
+    def test_memory_sqlite_url(self) -> None:
+        url = dj_database_url.parse("sqlite://:memory:")
 
         assert url["ENGINE"] == "django.db.backends.sqlite3"
         assert url["NAME"] == ":memory:"
 
-    def test_parse_engine_setting(self):
+    def test_sqlite_relative_url(self) -> None:
+        url = "sqlite:///db.sqlite3"
+        config = dj_database_url.parse(url)
+
+        assert config["ENGINE"] == "django.db.backends.sqlite3"
+        assert config["NAME"] == "db.sqlite3"
+
+    def test_sqlite_absolute_url(self) -> None:
+        # 4 slashes are needed:
+        # two are part of scheme
+        # one separates host:port from path
+        # and the fourth goes to "NAME" value
+        url = "sqlite:////db.sqlite3"
+        config = dj_database_url.parse(url)
+
+        assert config["ENGINE"] == "django.db.backends.sqlite3"
+        assert config["NAME"] == "/db.sqlite3"
+
+    def test_parse_engine_setting(self) -> None:
         engine = "django_mysqlpool.backends.mysqlpool"
-        url = 
"mysql://bea6eb025ca0d8:69772...@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true"
-        url = dj_database_url.parse(url, engine)
+        url = dj_database_url.parse(
+            
"mysql://bea6eb025ca0d8:69772...@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true",
+            engine,
+        )
 
         assert url["ENGINE"] == engine
 
-    def test_config_engine_setting(self):
+    def test_config_engine_setting(self) -> None:
         engine = "django_mysqlpool.backends.mysqlpool"
         with mock.patch.dict(
             os.environ,
@@ -224,14 +257,16 @@
 
         assert url["ENGINE"] == engine
 
-    def test_parse_conn_max_age_setting(self):
+    def test_parse_conn_max_age_setting(self) -> None:
         conn_max_age = 600
-        url = 
"mysql://bea6eb025ca0d8:69772...@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true"
-        url = dj_database_url.parse(url, conn_max_age=conn_max_age)
+        url = dj_database_url.parse(
+            
"mysql://bea6eb025ca0d8:69772...@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true",
+            conn_max_age=conn_max_age,
+        )
 
         assert url["CONN_MAX_AGE"] == conn_max_age
 
-    def test_config_conn_max_age_setting(self):
+    def test_config_conn_max_age_setting(self) -> None:
         conn_max_age = 600
         with mock.patch.dict(
             os.environ,
@@ -243,7 +278,7 @@
 
         assert url["CONN_MAX_AGE"] == conn_max_age
 
-    def test_database_url_with_options(self):
+    def test_database_url_with_options(self) -> None:
         # Test full options
         with mock.patch.dict(
             os.environ,
@@ -274,7 +309,7 @@
             url = dj_database_url.config()
         assert "OPTIONS" not in url
 
-    def test_mysql_database_url_with_sslca_options(self):
+    def test_mysql_database_url_with_sslca_options(self) -> None:
         with mock.patch.dict(
             os.environ,
             {
@@ -301,9 +336,8 @@
             url = dj_database_url.config()
         assert "OPTIONS" not in url
 
-    def test_oracle_parsing(self):
-        url = "oracle://scott:tiger@oraclehost:1521/hr"
-        url = dj_database_url.parse(url)
+    def test_oracle_parsing(self) -> None:
+        url = dj_database_url.parse("oracle://scott:tiger@oraclehost:1521/hr")
 
         assert url["ENGINE"] == "django.db.backends.oracle"
         assert url["NAME"] == "hr"
@@ -312,9 +346,8 @@
         assert url["PASSWORD"] == "tiger"
         assert url["PORT"] == "1521"
 
-    def test_oracle_gis_parsing(self):
-        url = "oraclegis://scott:tiger@oraclehost:1521/hr"
-        url = dj_database_url.parse(url)
+    def test_oracle_gis_parsing(self) -> None:
+        url = 
dj_database_url.parse("oraclegis://scott:tiger@oraclehost:1521/hr")
 
         assert url["ENGINE"] == "django.contrib.gis.db.backends.oracle"
         assert url["NAME"] == "hr"
@@ -323,14 +356,13 @@
         assert url["PASSWORD"] == "tiger"
         assert url["PORT"] == 1521
 
-    def test_oracle_dsn_parsing(self):
-        url = (
+    def test_oracle_dsn_parsing(self) -> None:
+        url = dj_database_url.parse(
             "oracle://scott:tiger@/"
             "(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)"
             "(HOST=oraclehost)(PORT=1521)))"
             "(CONNECT_DATA=(SID=hr)))"
         )
-        url = dj_database_url.parse(url)
 
         assert url["ENGINE"] == "django.db.backends.oracle"
         assert url["USER"] == "scott"
@@ -346,9 +378,8 @@
 
         assert url["NAME"] == dsn
 
-    def test_oracle_tns_parsing(self):
-        url = "oracle://scott:tiger@/tnsname"
-        url = dj_database_url.parse(url)
+    def test_oracle_tns_parsing(self) -> None:
+        url = dj_database_url.parse("oracle://scott:tiger@/tnsname")
 
         assert url["ENGINE"] == "django.db.backends.oracle"
         assert url["USER"] == "scott"
@@ -357,9 +388,10 @@
         assert url["HOST"] == ""
         assert url["PORT"] == ""
 
-    def test_redshift_parsing(self):
-        url = 
"redshift://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5439/d8r82722r2kuvn?currentSchema=otherschema"
-        url = dj_database_url.parse(url)
+    def test_redshift_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"redshift://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5439/d8r82722r2kuvn?currentSchema=otherschema"
+        )
 
         assert url["ENGINE"] == "django_redshift_backend"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -370,9 +402,10 @@
         assert url["OPTIONS"]["options"] == "-c search_path=otherschema"
         assert "currentSchema" not in url["OPTIONS"]
 
-    def test_mssql_parsing(self):
-        url = 
"mssql://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com/d8r82722r2kuvn?driver=ODBC
 Driver 13 for SQL Server"
-        url = dj_database_url.parse(url)
+    def test_mssql_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"mssql://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com/d8r82722r2kuvn?driver=ODBC
 Driver 13 for SQL Server"
+        )
 
         assert url["ENGINE"] == "sql_server.pyodbc"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -383,9 +416,10 @@
         assert url["OPTIONS"]["driver"] == "ODBC Driver 13 for SQL Server"
         assert "currentSchema" not in url["OPTIONS"]
 
-    def test_mssql_instance_port_parsing(self):
-        url = 
"mssql://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com\\insnsnss:12345/d8r82722r2kuvn?driver=ODBC
 Driver 13 for SQL Server"
-        url = dj_database_url.parse(url)
+    def test_mssql_instance_port_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"mssql://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com\\insnsnss:12345/d8r82722r2kuvn?driver=ODBC
 Driver 13 for SQL Server"
+        )
 
         assert url["ENGINE"] == "sql_server.pyodbc"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -396,9 +430,10 @@
         assert url["OPTIONS"]["driver"] == "ODBC Driver 13 for SQL Server"
         assert "currentSchema" not in url["OPTIONS"]
 
-    def test_cockroach(self):
-        url = 
"cockroach://testuser:testpass@testhost:26257/cockroach?sslmode=verify-full&sslrootcert=/certs/ca.crt&sslcert=/certs/client.myprojectuser.crt&sslkey=/certs/client.myprojectuser.key"
-        url = dj_database_url.parse(url)
+    def test_cockroach(self) -> None:
+        url = dj_database_url.parse(
+            
"cockroach://testuser:testpass@testhost:26257/cockroach?sslmode=verify-full&sslrootcert=/certs/ca.crt&sslcert=/certs/client.myprojectuser.crt&sslkey=/certs/client.myprojectuser.key"
+        )
         assert url['ENGINE'] == 'django_cockroachdb'
         assert url['NAME'] == 'cockroach'
         assert url['HOST'] == 'testhost'
@@ -410,9 +445,10 @@
         assert url['OPTIONS']['sslcert'] == '/certs/client.myprojectuser.crt'
         assert url['OPTIONS']['sslkey'] == '/certs/client.myprojectuser.key'
 
-    def test_mssqlms_parsing(self):
-        url = 
"mssqlms://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com/d8r82722r2kuvn?driver=ODBC
 Driver 13 for SQL Server"
-        url = dj_database_url.parse(url)
+    def test_mssqlms_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"mssqlms://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com/d8r82722r2kuvn?driver=ODBC
 Driver 13 for SQL Server"
+        )
 
         assert url["ENGINE"] == "mssql"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -423,9 +459,10 @@
         assert url["OPTIONS"]["driver"] == "ODBC Driver 13 for SQL Server"
         assert "currentSchema" not in url["OPTIONS"]
 
-    def test_timescale_parsing(self):
-        url = 
"timescale://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+    def test_timescale_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"timescale://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "timescale.db.backends.postgresql"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -434,9 +471,10 @@
         assert url["PASSWORD"] == "wegauwhgeuioweg"
         assert url["PORT"] == 5431
 
-    def test_timescale_unix_socket_parsing(self):
-        url = "timescale://%2Fvar%2Frun%2Fpostgresql/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+    def test_timescale_unix_socket_parsing(self) -> None:
+        url = dj_database_url.parse(
+            "timescale://%2Fvar%2Frun%2Fpostgresql/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "timescale.db.backends.postgresql"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -445,8 +483,9 @@
         assert url["PASSWORD"] == ""
         assert url["PORT"] == ""
 
-        url = "timescale://%2FUsers%2Fpostgres%2FRuN/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+        url = dj_database_url.parse(
+            "timescale://%2FUsers%2Fpostgres%2FRuN/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "timescale.db.backends.postgresql"
         assert url["HOST"] == "/Users/postgres/RuN"
@@ -454,9 +493,10 @@
         assert url["PASSWORD"] == ""
         assert url["PORT"] == ""
 
-    def test_timescale_ipv6_parsing(self):
-        url = 
"timescale://ieRaekei9wilaim7:wegauwhgeuioweg@[2001:db8:1234::1234:5678:90af]:5431/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+    def test_timescale_ipv6_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"timescale://ieRaekei9wilaim7:wegauwhgeuioweg@[2001:db8:1234::1234:5678:90af]:5431/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "timescale.db.backends.postgresql"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -465,9 +505,10 @@
         assert url["PASSWORD"] == "wegauwhgeuioweg"
         assert url["PORT"] == 5431
 
-    def test_timescale_search_path_parsing(self):
-        url = 
"timescale://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
-        url = dj_database_url.parse(url)
+    def test_timescale_search_path_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"timescale://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
+        )
         assert url["ENGINE"] == "timescale.db.backends.postgresql"
         assert url["NAME"] == "d8r82722r2kuvn"
         assert url["HOST"] == "ec2-107-21-253-135.compute-1.amazonaws.com"
@@ -477,9 +518,10 @@
         assert url["OPTIONS"]["options"] == "-c search_path=otherschema"
         assert "currentSchema" not in url["OPTIONS"]
 
-    def test_timescale_parsing_with_special_characters(self):
-        url = 
"timescale://%23user:%23passw...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/%23database"
-        url = dj_database_url.parse(url)
+    def test_timescale_parsing_with_special_characters(self) -> None:
+        url = dj_database_url.parse(
+            
"timescale://%23user:%23passw...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/%23database"
+        )
 
         assert url["ENGINE"] == "timescale.db.backends.postgresql"
         assert url["NAME"] == "#database"
@@ -488,9 +530,10 @@
         assert url["PASSWORD"] == "#password"
         assert url["PORT"] == 5431
 
-    def test_timescalegis_parsing(self):
-        url = 
"timescalegis://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+    def test_timescalegis_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"timescalegis://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "timescale.db.backends.postgis"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -499,9 +542,10 @@
         assert url["PASSWORD"] == "wegauwhgeuioweg"
         assert url["PORT"] == 5431
 
-    def test_timescalegis_unix_socket_parsing(self):
-        url = "timescalegis://%2Fvar%2Frun%2Fpostgresql/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+    def test_timescalegis_unix_socket_parsing(self) -> None:
+        url = dj_database_url.parse(
+            "timescalegis://%2Fvar%2Frun%2Fpostgresql/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "timescale.db.backends.postgis"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -510,8 +554,9 @@
         assert url["PASSWORD"] == ""
         assert url["PORT"] == ""
 
-        url = "timescalegis://%2FUsers%2Fpostgres%2FRuN/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+        url = dj_database_url.parse(
+            "timescalegis://%2FUsers%2Fpostgres%2FRuN/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "timescale.db.backends.postgis"
         assert url["HOST"] == "/Users/postgres/RuN"
@@ -519,9 +564,10 @@
         assert url["PASSWORD"] == ""
         assert url["PORT"] == ""
 
-    def test_timescalegis_ipv6_parsing(self):
-        url = 
"timescalegis://ieRaekei9wilaim7:wegauwhgeuioweg@[2001:db8:1234::1234:5678:90af]:5431/d8r82722r2kuvn"
-        url = dj_database_url.parse(url)
+    def test_timescalegis_ipv6_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"timescalegis://ieRaekei9wilaim7:wegauwhgeuioweg@[2001:db8:1234::1234:5678:90af]:5431/d8r82722r2kuvn"
+        )
 
         assert url["ENGINE"] == "timescale.db.backends.postgis"
         assert url["NAME"] == "d8r82722r2kuvn"
@@ -530,9 +576,10 @@
         assert url["PASSWORD"] == "wegauwhgeuioweg"
         assert url["PORT"] == 5431
 
-    def test_timescalegis_search_path_parsing(self):
-        url = 
"timescalegis://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
-        url = dj_database_url.parse(url)
+    def test_timescalegis_search_path_parsing(self) -> None:
+        url = dj_database_url.parse(
+            
"timescalegis://uf07k1i6d8ia0v:wegauwhgeuio...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn?currentSchema=otherschema"
+        )
         assert url["ENGINE"] == "timescale.db.backends.postgis"
         assert url["NAME"] == "d8r82722r2kuvn"
         assert url["HOST"] == "ec2-107-21-253-135.compute-1.amazonaws.com"
@@ -542,9 +589,10 @@
         assert url["OPTIONS"]["options"] == "-c search_path=otherschema"
         assert "currentSchema" not in url["OPTIONS"]
 
-    def test_timescalegis_parsing_with_special_characters(self):
-        url = 
"timescalegis://%23user:%23passw...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/%23database"
-        url = dj_database_url.parse(url)
+    def test_timescalegis_parsing_with_special_characters(self) -> None:
+        url = dj_database_url.parse(
+            
"timescalegis://%23user:%23passw...@ec2-107-21-253-135.compute-1.amazonaws.com:5431/%23database"
+        )
 
         assert url["ENGINE"] == "timescale.db.backends.postgis"
         assert url["NAME"] == "#database"
@@ -553,7 +601,7 @@
         assert url["PASSWORD"] == "#password"
         assert url["PORT"] == 5431
 
-    def test_persistent_connection_variables(self):
+    def test_persistent_connection_variables(self) -> None:
         url = dj_database_url.parse(
             "sqlite://myfile.db", conn_max_age=600, conn_health_checks=True
         )
@@ -561,7 +609,7 @@
         assert url["CONN_MAX_AGE"] == 600
         assert url["CONN_HEALTH_CHECKS"] is True
 
-    def test_sqlite_memory_persistent_connection_variables(self):
+    def test_sqlite_memory_persistent_connection_variables(self) -> None:
         # memory sqlite ignores connection.close(), so persistent connection
         # variables aren’t required
         url = dj_database_url.parse(
@@ -575,13 +623,13 @@
         os.environ,
         {"DATABASE_URL": 
"postgres://user:passw...@instance.amazonaws.com:5431/d8r8?"},
     )
-    def test_persistent_connection_variables_config(self):
+    def test_persistent_connection_variables_config(self) -> None:
         url = dj_database_url.config(conn_max_age=600, conn_health_checks=True)
 
         assert url["CONN_MAX_AGE"] == 600
         assert url["CONN_HEALTH_CHECKS"] is True
 
-    def test_no_env_variable(self):
+    def test_no_env_variable(self) -> None:
         with self.assertLogs() as cm:
             with mock.patch.dict(os.environ, clear=True):
                 url = dj_database_url.config()
@@ -590,19 +638,46 @@
             'WARNING:root:No DATABASE_URL environment variable set, and so no 
databases setup'
         ], cm.output
 
-    def test_bad_url_parsing(self):
-        with self.assertRaisesRegex(ValueError, "No support for 'foo'. We 
support: "):
-            dj_database_url.parse("foo://bar")
+    def test_credentials_unquoted__raise_value_error(self) -> None:
+        expected_message = (
+            "This string is not a valid url, possibly because some of its 
parts "
+            r"is not properly urllib.parse.quote()'ed."
+        )
+        with self.assertRaisesRegex(ValueError, re.escape(expected_message)):
+            
dj_database_url.parse("postgres://user:passw#ord!@localhost/foobar")
+
+    def test_credentials_quoted__ok(self) -> None:
+        url = "postgres://user%40domain:p%23ssword!@localhost/foobar"
+        config = dj_database_url.parse(url)
+        assert config["USER"] == "user@domain"
+        assert config["PASSWORD"] == "p#ssword!"
+
+    def test_unknown_scheme__raise_value_error(self) -> None:
+        expected_message = (
+            "Scheme 'unknown-scheme://' is unknown. "
+            "Did you forget to register custom backend? Following schemes have 
registered backends:"
+        )
+        with self.assertRaisesRegex(ValueError, re.escape(expected_message)):
+            
dj_database_url.parse("unknown-scheme://user:password@localhost/foobar")
+
+    def test_register_multiple_times__no_duplicates_in_uses_netloc(self) -> 
None:
+        # make sure that when register() function is misused,
+        # it won't pollute urllib.parse.uses_netloc list with duplicates.
+        # Otherwise, it might cause performance issue if some code assumes that
+        # that list is short and performs linear search on it.
+        dj_database_url.register("django.contrib.db.backends.bag_end", 
"bag-end")
+        dj_database_url.register("django.contrib.db.backends.bag_end", 
"bag-end")
+        assert len(uses_netloc) == len(set(uses_netloc))
 
     @mock.patch.dict(
         os.environ,
         {"DATABASE_URL": 
"postgres://user:passw...@instance.amazonaws.com:5431/d8r8?"},
     )
-    def test_ssl_require(self):
+    def test_ssl_require(self) -> None:
         url = dj_database_url.config(ssl_require=True)
         assert url["OPTIONS"] == {'sslmode': 'require'}
 
-    def test_options_int_values(self):
+    def test_options_int_values(self) -> None:
         """Ensure that options with integer values are parsed correctly."""
         url = dj_database_url.parse(
             "mysql://user:pw@127.0.0.1:15036/db?connect_timout=3"
@@ -613,7 +688,7 @@
         os.environ,
         {"DATABASE_URL": 
"postgres://user:passw...@instance.amazonaws.com:5431/d8r8?"},
     )
-    def test_server_side_cursors__config(self):
+    def test_server_side_cursors__config(self) -> None:
         url = dj_database_url.config(disable_server_side_cursors=True)
 
         assert url["DISABLE_SERVER_SIDE_CURSORS"] is True

Reply via email to