diff --git a/README.rst b/README.rst index d457ff3..7f27f6e 100644 --- a/README.rst +++ b/README.rst @@ -15,19 +15,6 @@ This simple Django utility allows you to utilize the `12factor `_ inspired ``DATABASE_URL`` environment variable to configure your Django application. -The ``dj_database_url.config`` method returns a Django database connection -dictionary, populated with all the data specified in your URL. There is -also a `conn_max_age` argument to easily enable Django's connection pool. - -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, and SQLite. - Installation ------------ @@ -38,52 +25,146 @@ Installation is simple:: Usage ----- -1. If ``DATABASES`` is already defined: - -- Configure your database in ``settings.py`` from ``DATABASE_URL``:: +The ``dj_database_url.config()`` method returns a Django database +connection dictionary, populated with all the data specified in your +``DATABASE_URL`` environment variable:: import dj_database_url - DATABASES['default'] = dj_database_url.config(conn_max_age=600) - -- Provide a default:: - - DATABASES['default'] = dj_database_url.config(default='postgres://...') + DATABASES = { + "default": dj_database_url.config(), + # arbitrary environment variable can be used + "replica": dj_database_url.config("REPLICA_URL"), + } + +Given the following environment variables are defined:: + + $ export DATABASE_URL="postgres://user:password@ec2-107-21-253-135.compute-1.amazonaws.com:5431/db-name" + # All the characters which are reserved in URL as per RFC 3986 should be urllib.parse.quote()'ed. + $ export REPLICA_URL="postgres://%23user:%23password@replica-host.com/db-name?timeout=20" + +The above-mentioned code will result in:: + + DATABASES = { + "default": { + "ENGINE": "django.db.backends.postgresql", + "USER": "user", + "PASSWORD": "password", + "HOST": "ec2-107-21-253-135.compute-1.amazonaws.com", + "PORT": 5431, + "NAME": "db-name", + }, + "replica": { + "ENGINE": "django.db.backends.postgresql", + "USER": "#user", + "PASSWORD": "#password", + "HOST": "replica-host.com", + "PORT": "", + "NAME": "db-name", + # Any querystring parameters are automatically parsed and added to `OPTIONS`. + "OPTIONS": { + "timeout": "20", + }, + }, + } + +A default value can be provided which will be used when the environment +variable is not set:: + + DATABASES["default"] = dj_database_url.config(default="sqlite://") + +If you'd rather not use an environment variable, you can pass a URL +directly into ``dj_database_url.parse()``:: + + DATABASES["default"] = dj_database_url.parse("postgres://...") + +You can also pass in any keyword argument that Django's |databases hyperlink|_ setting accepts, +such as |max age hyperlink|_ or |options hyperlink|_:: + + dj_database_url.config(CONN_MAX_AGE=600, TEST={"NAME": "mytestdatabase"}) + # results in: + { + "ENGINE": "django.db.backends.postgresql", + # ... other values coming from DATABASE_URL + "CONN_MAX_AGE": 600, + "TEST": { + "NAME": "mytestdatabase", + }, + } + + # such usage is also possible: + dj_database_url.parse("postgres://...", **{ + "CONN_MAX_AGE": 600, + "TEST": { + "NAME": "mytestdatabase", + }, + "OPTIONS": { + "isolation_level": psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE, + }, + }) + +``OPTIONS`` will be properly merged with the parameters coming from +querystring (keyword argument has higher priority than querystring). + +.. |databases hyperlink| replace:: ``DATABASES`` +.. _databases hyperlink: https://docs.djangoproject.com/en/stable/ref/settings/#databases +.. |max age hyperlink| replace:: ``CONN_MAX_AGE`` +.. _max age hyperlink: https://docs.djangoproject.com/en/stable/ref/settings/#conn-max-age +.. |options hyperlink| replace:: ``OPTIONS`` +.. _options hyperlink: https://docs.djangoproject.com/en/stable/ref/settings/#std:setting-OPTIONS -- Parse an arbitrary Database URL:: - - DATABASES['default'] = dj_database_url.parse('postgres://...', conn_max_age=600) +Supported Databases +------------------- -2. If ``DATABASES`` is not defined: +Support currently exists for PostgreSQL, PostGIS, MySQL, MySQL (GIS), +Oracle, Oracle (GIS), Redshift, CockroachDB, and SQLite. -- Configure your database in ``settings.py`` from ``DATABASE_URL``:: +If you want to use +some non-default backends, you need to register them first:: import dj_database_url - DATABASES = {'default': dj_database_url.config(conn_max_age=600)} - -- Provide a default:: + # registration should be performed only once + dj_database_url.register("mysql.connector.django", "mysql-connector") - DATABASES = {'default': dj_database_url.config(default='postgres://...')} + assert dj_database_url.parse("mysql-connector://user:password@host:port/db-name") == { + "ENGINE": "mysql.connector.django", + # ...other connection params + } -- Parse an arbitrary Database URL:: +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):: - DATABASES = {'default': dj_database_url.parse('postgres://...', conn_max_age=600)} - -The ``conn_max_age`` attribute is the lifetime of a database connection in seconds -and is available in Django 1.6+. If you do not set a value, it will default to ``0`` -which is Django's historical behavior of using a new database connection on each -request. Use ``None`` for unlimited persistent connections. - -Strings passed to `dj_database_url` must be valid URLs; in -particular, special characters must be url-encoded. The following url will raise -a `ValueError`:: - - postgres://user:p#ssword!@localhost/foobar - -and should instead be passed as:: + import dj_database_url - postgres://user:p%23ssword!@localhost/foobar + @dj_database_url.register("sql_server.pyodbc", "mssql") + def stringify_port(config): + config["PORT"] = str(config["PORT"]) + + @dj_database_url.register("django_redshift_backend", "redshift") + 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("django_snowflake", "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 --git a/dj_database_url.py b/dj_database_url.py index d4de026..3f04970 100644 --- a/dj_database_url.py +++ b/dj_database_url.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import os import urllib.parse as urlparse @@ -9,151 +7,189 @@ DJANGO_VERSION = None -# Register database schemes in URLs. -urlparse.uses_netloc.append("postgres") -urlparse.uses_netloc.append("postgresql") -urlparse.uses_netloc.append("pgsql") -urlparse.uses_netloc.append("postgis") -urlparse.uses_netloc.append("mysql") -urlparse.uses_netloc.append("mysql2") -urlparse.uses_netloc.append("mysqlgis") -urlparse.uses_netloc.append("mysql-connector") -urlparse.uses_netloc.append("mssql") -urlparse.uses_netloc.append("mssqlms") -urlparse.uses_netloc.append("spatialite") -urlparse.uses_netloc.append("sqlite") -urlparse.uses_netloc.append("oracle") -urlparse.uses_netloc.append("oraclegis") -urlparse.uses_netloc.append("redshift") -urlparse.uses_netloc.append("cockroach") - -DEFAULT_ENV = "DATABASE_URL" - -SCHEMES = { - "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", -} - -# https://docs.djangoproject.com/en/2.0/releases/2.0/#id1 -if DJANGO_VERSION and DJANGO_VERSION < (2, 0): - SCHEMES["postgres"] = "django.db.backends.postgresql_psycopg2" - SCHEMES["postgresql"] = "django.db.backends.postgresql_psycopg2" - SCHEMES["pgsql"] = "django.db.backends.postgresql_psycopg2" -else: - SCHEMES["postgres"] = "django.db.backends.postgresql" - SCHEMES["postgresql"] = "django.db.backends.postgresql" - SCHEMES["pgsql"] = "django.db.backends.postgresql" - - -def config( - env=DEFAULT_ENV, default=None, engine=None, conn_max_age=0, ssl_require=False -): - """Returns configured DATABASE dictionary from DATABASE_URL.""" - s = os.environ.get(env, default) +def get_postgres_backend(): + # Django deprecated the `django.db.backends.postgresql_psycopg2` in 2.0. + # https://docs.djangoproject.com/en/stable/releases/2.0/#id1 + if DJANGO_VERSION and DJANGO_VERSION < (2, 0): + return "django.db.backends.postgresql_psycopg2" + return "django.db.backends.postgresql" + + +POSTGRES_BACKEND = get_postgres_backend() + + +class ParseError(ValueError): + def __str__(self): + 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): + self.scheme = scheme + + def __str__(self): + return ( + f"Scheme '{self.scheme}://' is unknown." + " Did you forget to register custom backend?" + ) + + +ENGINE_SCHEMES = {} + + +def default_postprocess(parsed_config): + pass + + +class Engine: + def __init__(self, backend, postprocess=default_postprocess): + self.backend = backend + self.postprocess = postprocess + + +def register(backend, schemes=None): + engine = Engine(backend) + schemes = schemes or [backend.rsplit(".")[-1]] + schemes = [schemes] if isinstance(schemes, str) else schemes + for scheme in schemes: + if scheme not in ENGINE_SCHEMES: + urlparse.uses_netloc.append(scheme) + ENGINE_SCHEMES[scheme] = engine + + def inner(func): + engine.postprocess = func + return func + + return inner - if s: - return parse(s, engine, conn_max_age, ssl_require) - return {} +register("django.contrib.gis.db.backends.spatialite") +register("mysql.connector.django", "mysql-connector") +register("django.contrib.gis.db.backends.mysql", "mysqlgis") +register("django.contrib.gis.db.backends.oracle", "oraclegis") +register("django_cockroachdb", "cockroach") -def parse(url, engine=None, conn_max_age=0, ssl_require=False): - """Parses a database URL.""" +@register("django.db.backends.sqlite3", "sqlite") +def default_to_in_memory_db(parsed_config): + # mimic sqlalchemy behaviour + if parsed_config["NAME"] == "": + parsed_config["NAME"] = ":memory:" + + +@register("django.db.backends.oracle") +@register("mssql", "mssqlms") +@register("sql_server.pyodbc", "mssql") +def stringify_port(parsed_config): + parsed_config["PORT"] = str(parsed_config["PORT"]) + + +@register("django.db.backends.mysql", ("mysql", "mysql2")) +def apply_ssl_ca(parsed_config): + options = parsed_config["OPTIONS"] + ca = options.pop("ssl-ca", None) + if ca: + options["ssl"] = {"ca": ca} + + +@register(POSTGRES_BACKEND, ("postgres", "postgresql", "pgsql")) +@register("django.contrib.gis.db.backends.postgis") +@register("django_redshift_backend", "redshift") +def apply_current_schema(parsed_config): + options = parsed_config["OPTIONS"] + schema = options.pop("currentSchema", None) + if schema: + options["options"] = f"-c search_path={schema}" + + +def config(env="DATABASE_URL", default=None, **settings): + """ + Gets a database URL from environmental variable named as 'env' value and parses it. + """ + s = os.environ.get(env, default) + return parse(s, **settings) if s else {} + + +def address_deprecated_arguments(backend, settings): + import warnings + + if backend is not None: + message = ( + "Using positional argument `backend`" + " to override database backend is deprecated." + " Use keyword argument `ENGINE` instead." + ) + warnings.warn(message) + settings["ENGINE"] = backend + + if "engine" in settings: + message = "The `engine` argument is deprecated. Use `ENGINE` instead." + warnings.warn(message) + settings["ENGINE"] = settings.pop("engine") + + if "conn_max_age" in settings: + warnings.warn( + "The `conn_max_age` argument is deprecated. Use `CONN_MAX_AGE` instead." + ) + settings["CONN_MAX_AGE"] = settings.pop("conn_max_age") + + if "ssl_require" in settings: + warnings.warn( + "The `ssl_require` argument is deprecated." + " Use `OPTIONS={'sslmode': 'require'}` instead." + ) + settings.pop("ssl_require") + options = settings.pop("OPTIONS", {}) + options["sslmode"] = "require" + settings["OPTIONS"] = options + return backend + + +def parse(url, backend=None, **settings): + """Parses a database URL and returns configured DATABASE dictionary.""" + + address_deprecated_arguments(backend, settings) 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 = {} - - url = urlparse.urlparse(url) - - # Split query strings from path. - path = url.path[1:] - if "?" in path and not url.query: - path, query = path.split("?", 2) - else: - path, query = path, url.query - query = urlparse.parse_qs(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 url.scheme == "sqlite" and path == "": - path = ":memory:" - - # Handle postgres percent-encoded paths. - hostname = url.hostname or "" - if "%2f" in hostname.lower(): - # Switch to url.netloc to avoid lower cased paths - hostname = url.netloc - if "@" in hostname: - hostname = hostname.rsplit("@", 1)[1] - if ":" in hostname: - hostname = hostname.split(":", 1)[0] - hostname = hostname.replace("%2f", "/").replace("%2F", "/") - - # Lookup specified engine. - engine = SCHEMES[url.scheme] if engine is None else engine - - port = ( - str(url.port) - if url.port - and engine in [SCHEMES["oracle"], SCHEMES["mssql"], SCHEMES["mssqlms"]] - else url.port - ) - - # Update with environment configuration. - parsed_config.update( - { - "NAME": urlparse.unquote(path or ""), + try: + url = urlparse.urlparse(url) + engine = ENGINE_SCHEMES.get(url.scheme) + if engine is None: + raise UnknownSchemeError(url.scheme) + path = url.path[1:] + query = urlparse.parse_qs(url.query) + options = {k: (v[0] if len(v) == 1 else v) for k, v in query.items()} + parsed_config = { + "ENGINE": engine.backend, "USER": urlparse.unquote(url.username or ""), "PASSWORD": urlparse.unquote(url.password or ""), - "HOST": hostname, - "PORT": port or "", - "CONN_MAX_AGE": conn_max_age, + "HOST": urlparse.unquote(url.hostname or ""), + "PORT": url.port or "", + "NAME": urlparse.unquote(path), + "OPTIONS": options, } - ) - - # Pass the query string into OPTIONS. - options = {} - for key, values in query.items(): - if url.scheme == "mysql" and key == "ssl-ca": - options["ssl"] = {"ca": values[-1]} - continue - - options[key] = values[-1] - - if ssl_require: - options["sslmode"] = "require" - - # Support for Postgres Schema URLs - if "currentSchema" in options and engine in ( - "django.contrib.gis.db.backends.postgis", - "django.db.backends.postgresql_psycopg2", - "django.db.backends.postgresql", - "django_redshift_backend", - ): - options["options"] = "-c search_path={0}".format(options.pop("currentSchema")) + except UnknownSchemeError: + raise + except ValueError: + raise ParseError() from None - if options: - parsed_config["OPTIONS"] = options + # Guarantee that config has options, possibly empty, when postprocess() is called + assert isinstance(parsed_config["OPTIONS"], dict) + engine.postprocess(parsed_config) - if engine: - parsed_config["ENGINE"] = engine + # Update the final config with any settings passed in explicitly. + parsed_config["OPTIONS"].update(settings.pop("OPTIONS", {})) + parsed_config.update(**settings) + if not parsed_config["OPTIONS"]: + parsed_config.pop("OPTIONS") return parsed_config diff --git a/test_dj_database_url.py b/test_dj_database_url.py index 2998729..d4f7cf9 100644 --- a/test_dj_database_url.py +++ b/test_dj_database_url.py @@ -1,23 +1,145 @@ import os +import re import unittest - -try: - from django import VERSION as DJANGO_VERSION -except ImportError: - DJANGO_VERSION = None +from unittest.mock import patch +from urllib.parse import uses_netloc import dj_database_url -POSTGIS_URL = "postgis://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn" +URL = "postgres://user:password@localhost/db-name" +EXPECTED_POSTGRES_ENGINE = dj_database_url.get_postgres_backend() -# Django deprecated the `django.db.backends.postgresql_psycopg2` in 2.0. -# https://docs.djangoproject.com/en/2.0/releases/2.0/#id1 -EXPECTED_POSTGRES_ENGINE = "django.db.backends.postgresql" -if DJANGO_VERSION and DJANGO_VERSION < (2, 0): - EXPECTED_POSTGRES_ENGINE = "django.db.backends.postgresql_psycopg2" + +class DeprecatedArgumentsTestSuite(unittest.TestCase): + def test_config_conn_max_age_setting(self): + conn_max_age = 600 + os.environ[ + "DATABASE_URL" + ] = "mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true" + message = ( + "The `conn_max_age` argument is deprecated. Use `CONN_MAX_AGE` instead." + ) + with self.assertWarns(Warning, msg=message): + url = dj_database_url.config(conn_max_age=conn_max_age) + + assert url["CONN_MAX_AGE"] == conn_max_age + del os.environ["DATABASE_URL"] + + def test_parse_conn_max_age_setting(self): + conn_max_age = 600 + url = "mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true" + message = ( + "The `conn_max_age` argument is deprecated. Use `CONN_MAX_AGE` instead." + ) + with self.assertWarns(Warning, msg=message): + url = dj_database_url.parse(url, conn_max_age=conn_max_age) + + assert url["CONN_MAX_AGE"] == conn_max_age + + def test_config_engine_setting(self): + engine = "django_mysqlpool.backends.mysqlpool" + os.environ[ + "DATABASE_URL" + ] = "mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true" + message = "The `engine` argument is deprecated. Use `ENGINE` instead." + with self.assertWarns(Warning, msg=message): + url = dj_database_url.config(engine=engine) + + assert url["ENGINE"] == engine + del os.environ["DATABASE_URL"] + + def test_parse_engine_setting(self): + engine = "django_mysqlpool.backends.mysqlpool" + url = "mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true" + message = ( + "Using positional argument `backend`" + " to override database backend is deprecated." + " Use keyword argument `ENGINE` instead." + ) + with self.assertWarns(Warning, msg=message): + url = dj_database_url.parse(url, engine) + + assert url["ENGINE"] == engine + + def test_pass_ssl_require__handle_and_issue_warning(self): + message = ( + "The `ssl_require` argument is deprecated." + " Use `OPTIONS={'sslmode': 'require'}` instead." + ) + with self.assertWarnsRegex(Warning, re.escape(message)): + config = dj_database_url.parse(URL, ssl_require=True) + + assert config["OPTIONS"] == {'sslmode': 'require'} class DatabaseTestSuite(unittest.TestCase): + def test_credentials_unquoted__raise_value_error(self): + 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): + 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): + expected_message = "Scheme 'unknown-scheme://' is unknown. Did you forget to register custom backend?" + with self.assertRaisesRegex(ValueError, re.escape(expected_message)): + dj_database_url.parse("unknown-scheme://user:password@localhost/foobar") + + def test_provide_test_settings__add_them_to_final_config(self): + settings = { + "TEST": { + "NAME": "mytestdatabase", + }, + } + config = dj_database_url.parse(URL, **settings) + assert config["TEST"] == {"NAME": "mytestdatabase"} + + def test_provide_options__add_them_to_final_config(self): + options = {"options": "-c search_path=other_schema"} + config = dj_database_url.parse(URL, OPTIONS=options) + assert config["OPTIONS"] == options + + def test_provide_clashing_options__use_options_from_kwargs(self): + options = {"reconnect": "false"} + config = dj_database_url.parse(f"{URL}?reconnect=true", OPTIONS=options) + assert config["OPTIONS"]["reconnect"] == "false" + + def test_provide_custom_engine__use_it_in_final_config(self): + engine = "django_mysqlpool.backends.mysqlpool" + config = dj_database_url.parse(URL, ENGINE=engine) + assert config["ENGINE"] == engine + + def test_provide_conn_max_age__use_it_in_final_config(self): + config = dj_database_url.parse(URL, CONN_MAX_AGE=600) + assert config["CONN_MAX_AGE"] == 600 + + @patch("dj_database_url.DJANGO_VERSION", (1, 11, 0, "final", 1)) + def test_django_version_pre_2__use_postgresql_psycopg2_backend(self): + expected = "django.db.backends.postgresql_psycopg2" + assert dj_database_url.get_postgres_backend() == expected + + @patch("dj_database_url.DJANGO_VERSION", (3, 2, 0, "final", 0)) + def test_django_version_post_2__use_postgresql_backend(self): + expected = "django.db.backends.postgresql" + assert dj_database_url.get_postgres_backend() == expected + + def test_register_multiple_times__no_duplicates_in_uses_netloc(self): + # 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)) + def test_postgres_parsing(self): url = "postgres://uf07k1i6d8ia0v:wegauwhgeuioweg@ec2-107-21-253-135.compute-1.amazonaws.com:5431/d8r82722r2kuvn" url = dj_database_url.parse(url) @@ -140,7 +262,6 @@ def test_cleardb_parsing(self): assert url["PORT"] == "" def test_database_url(self): - del os.environ["DATABASE_URL"] a = dj_database_url.config() assert not a @@ -157,6 +278,24 @@ def test_database_url(self): assert url["PASSWORD"] == "wegauwhgeuioweg" assert url["PORT"] == 5431 + def test_sqlite_url(self): + 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): + # 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_empty_sqlite_url(self): url = "sqlite://" url = dj_database_url.parse(url) @@ -171,38 +310,6 @@ def test_memory_sqlite_url(self): assert url["ENGINE"] == "django.db.backends.sqlite3" assert url["NAME"] == ":memory:" - def test_parse_engine_setting(self): - engine = "django_mysqlpool.backends.mysqlpool" - url = "mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true" - url = dj_database_url.parse(url, engine) - - assert url["ENGINE"] == engine - - def test_config_engine_setting(self): - engine = "django_mysqlpool.backends.mysqlpool" - os.environ[ - "DATABASE_URL" - ] = "mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true" - url = dj_database_url.config(engine=engine) - - assert url["ENGINE"] == engine - - def test_parse_conn_max_age_setting(self): - conn_max_age = 600 - url = "mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true" - url = dj_database_url.parse(url, conn_max_age=conn_max_age) - - assert url["CONN_MAX_AGE"] == conn_max_age - - def test_config_conn_max_age_setting(self): - conn_max_age = 600 - os.environ[ - "DATABASE_URL" - ] = "mysql://bea6eb025ca0d8:69772142@us-cdbr-east.cleardb.com/heroku_97681db3eff7580?reconnect=true" - url = dj_database_url.config(conn_max_age=conn_max_age) - - assert url["CONN_MAX_AGE"] == conn_max_age - def test_database_url_with_options(self): # Test full options os.environ[