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