django-service-urls 3.1 & 3.2 - configure django settings with url-strings

Release Notes: django-service-urls 3.1 and 3.2

django-service-urls is a Django settings helper that lets you configure
databases, caches, email, mailers, storages, and task backends with single URL
strings (e.g., postgres://user:pass@host/db).

Links

Highlights

Security: connection password no longer leaked by the UrlInfo repr (3.1)

UrlInfo.__repr__() masked the password field as *** precisely to keep
credentials out of logs and tracebacks, but printed location verbatim, and
location holds the raw netloc, user:pass@host:port. The masked password was
therefore exposed in cleartext wherever a UrlInfo was logged.

The repr now strips the userinfo from location, in both the single-host and
multi-host forms. The attribute itself is unchanged, so code reading
UrlInfo.location still gets the full netloc:

info = parse_url("postgres://user:passwd@db:5432/app")
info.location  # 'user:passwd@db:5432'  -- unchanged
repr(info)  # ... password=***, ... location='db:5432' ...

MAILERS support via the new mailer service (3.1)

Django 6.1 replaces the flat EMAIL_* settings with MAILERS, a dictionary
mapping aliases to {"BACKEND": ..., "OPTIONS": {...}} entries. The new mailer
service parses it the same way as DATABASES and CACHES:

MAILERS = {
    "default": "smtps://user:pass@smtp.example.net?timeout=30",
}
# Produces:
# {
#     "default": {
#         "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
#         "OPTIONS": {
#             "host": "smtp.example.net",
#             "username": "user",
#             "password": "pass",
#             "use_tls": True,
#             "timeout": 30,
#         },
#     },
# }

It accepts the same URL schemes as the email service and is exported as
django_service_urls.mailer for direct use.

The email service is unchanged and still handles EMAIL_BACKEND.

Python 3.15 support (3.1)

On Python 3.15 and later, the package activates through a PEP 829 start file
(django_service_urls.start) instead of the .pth file, which is still shipped
for older Python versions.

Activation happens exactly once either way, and existing installs need no
changes. An explicit import django_service_urls.loads still works too.

Errors instead of silently ignored settings (3.1)

A setting that the installed Django version cannot use now raises
ImproperlyConfigured at startup instead of being silently ignored:

  • MAILERS on Django < 6.1: “The MAILERS setting requires Django 6.1 or later;
    use EMAIL_BACKEND instead.”
  • An EMAIL_BACKEND service URL on Django 2028+, where the EMAIL_* settings are
    removed: "EMAIL_BACKEND service URLs rely on the EMAIL_ settings removed in
    Django 2028; use the MAILERS setting instead."* In 3.1 the message said
    Django 7.0.

A plain backend import path in EMAIL_BACKEND is still left untouched.

Better typing support (3.2)

Typing has been improved throughout the library, which now type-checks cleanly
under mypy, pyright, pyrefly and ty.

Bug Fixes

  • ValidationError discarded the contents of a nested error (3.1). When a
    dict value was a ValidationError with its own error_dict (and therefore a
    None message), that None was stored and the nested errors were lost. The
    nested error is now rendered as a string, so error_dict values are always
    strings, as its type annotation now states:

    inner = ValidationError({"nested": "boom"})
    ValidationError({"k": inner}).error_dict
    # before: {'k': None}
    # after:  {'k': "{'nested': 'boom'}"}
    
  • Iterating a ValidationError with neither a message nor an error_dict
    (3.1) yielded a single None, contradicting the annotated yield type. It now
    yields nothing.

Compatibility

  • Python: 3.10, 3.11, 3.12, 3.13, 3.14, 3.15
  • Django: 5.2, 6.0, 6.1, main

Upgrading

Most users can upgrade without any changes. Check for the following:

  1. Do not define both MAILERS and the EMAIL_* settings (3.1). Django 6.1
    itself rejects that combination with
    ImproperlyConfigured: Deprecated email settings are not allowed when MAILERS is defined: .... Use MAILERS on Django 6.1+ or EMAIL_BACKEND on older
    versions, not both.

    If EMAIL_BACKEND is a service URL, the error lists every EMAIL_* setting,
    not just EMAIL_BACKEND, because the URL is expanded into all of them before
    Django’s check runs. Removing EMAIL_BACKEND clears the whole list.

  2. Migrating to MAILERS (3.1): the EMAIL_* settings are deprecated in
    Django 6.1 and removed in Django 2028. A MAILERS entry accepts the same URL as
    EMAIL_BACKEND, so migrating usually just means moving the string:

    # before
    EMAIL_BACKEND = "smtps://user:pass@smtp.example.net?timeout=30"
    
    # after
    MAILERS = {"default": "smtps://user:pass@smtp.example.net?timeout=30"}
    

    If your URL uses use_localtime, remove it and set the global
    EMAIL_USE_LOCALTIME setting instead.

  3. Code that parses repr(UrlInfo) (3.1): the location field is now
    redacted. Read the location attribute instead; it is unchanged.

  4. Code that matches the ImproperlyConfigured message text (3.2): the
    message now says “Django 2028” where 3.1 said “Django 7.0”.

  5. Settings dicts holding values other than URLs, config dicts or None
    (3.2): such values are now passed through instead of becoming {} or raising
    AttributeError.

  6. Type checking (3.2): Service.parse now declares the values a settings
    dict may hold (ConfigInput), so code passing other values may see new type
    errors. New code can import the type aliases from django_service_urls or
    django_service_urls.types.