Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions docs/fixers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,31 @@ Django 5.1

`Release Notes <https://docs.djangoproject.com/en/5.1/releases/5.1/>`__

``BadHeaderError``
~~~~~~~~~~~~~~~~~~

**Name:** ``bad_header_error``

Replaces ``django.core.mail.BadHeaderError`` with Python's built-in ``ValueError``.
Django's ``BadHeaderError`` has been deprecated in favor of the standard ``ValueError``.

.. code-block:: diff

-from django.core.mail import BadHeaderError, send_mail
+from django.core.mail import send_mail

-raise BadHeaderError("Invalid header")
+raise ValueError("Invalid header")

.. code-block:: diff


try:
send_mail(...)
-except BadHeaderError:
+except ValueError:
pass

.. _check_constraint_condition:

``CheckConstraint`` ``condition`` argument
Expand Down
84 changes: 84 additions & 0 deletions src/django_upgrade/fixers/bad_header_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""
Swap django.core.mail.BadHeaderError with ValueError:
https://docs.djangoproject.com/en/5.1/releases/5.1/#features-deprecated-in-5-1
"""

from __future__ import annotations

import ast
from collections.abc import Iterable, MutableMapping
from functools import partial
from weakref import WeakKeyDictionary

from tokenize_rt import Offset

from django_upgrade.ast import ast_start_offset, is_rewritable_import_from
from django_upgrade.data import Fixer, State, TokenFunc
from django_upgrade.tokens import find_and_replace_name, update_import_names

fixer = Fixer(
__name__,
min_version=(5, 1),
)

MODULE = "django.core.mail"
OLD_NAME = "BadHeaderError"
NEW_NAME = "ValueError"

# Track aliased imports like "from django.core.mail import BadHeaderError as BHE"
# Maps state to the set of alias names that should be replaced with ValueError
aliased_names: MutableMapping[State, set[str]] = WeakKeyDictionary()


@fixer.register(ast.ImportFrom)
def visit_ImportFrom(
state: State,
node: ast.ImportFrom,
parents: tuple[ast.AST, ...],
) -> Iterable[tuple[Offset, TokenFunc]]:
if node.module == MODULE and is_rewritable_import_from(node):
# Check if BadHeaderError is in the imports
aliases_to_track = set()
for alias in node.names:
if alias.name == OLD_NAME:
# Track the alias name if it exists
if alias.asname is not None:
aliases_to_track.add(alias.asname)
# Remove BadHeaderError from the import
name_map = {OLD_NAME: ""}
if aliases_to_track:
# Merge with any previously recorded aliases for this state
existing = aliased_names.get(state)
if existing is None:
aliased_names[state] = aliases_to_track
else:
existing.update(aliases_to_track)
yield (
ast_start_offset(node),
partial(
update_import_names,
node=node,
name_map=name_map,
),
)
break


@fixer.register(ast.Name)
def visit_Name(
state: State,
node: ast.Name,
parents: tuple[ast.AST, ...],
) -> Iterable[tuple[Offset, TokenFunc]]:
# Check if this is a direct usage of BadHeaderError (unaliased import)
if node.id == OLD_NAME and OLD_NAME in state.from_imports[MODULE]:
yield (
ast_start_offset(node),
partial(find_and_replace_name, name=OLD_NAME, new=NEW_NAME),
)
# Check if this is an aliased usage (e.g., BHE when imported as BadHeaderError as BHE)
elif node.id in aliased_names.get(state, set()):
yield (
ast_start_offset(node),
partial(find_and_replace_name, name=node.id, new=NEW_NAME),
)
162 changes: 162 additions & 0 deletions tests/fixers/test_bad_header_error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
from __future__ import annotations

from functools import partial

from django_upgrade.data import Settings
from tests.fixers import tools

settings = Settings(target_version=(5, 1))
check_noop = partial(tools.check_noop, settings=settings)
check_transformed = partial(tools.check_transformed, settings=settings)


def test_no_bad_header_error():
check_noop(
"""\
from django.core.mail import send_mail

send_mail('subject', 'message', '[email protected]', ['[email protected]'])
""",
)


def test_bad_header_error_simple():
check_transformed(
"""\
from django.core.mail import BadHeaderError

raise BadHeaderError("Invalid header")
""",
"""\

raise ValueError("Invalid header")
""",
)


def test_bad_header_error_with_other_imports():
check_transformed(
"""\
from django.core.mail import BadHeaderError, send_mail

try:
send_mail('subject', 'message', '[email protected]', ['[email protected]'])
except BadHeaderError:
pass
""",
"""\
from django.core.mail import send_mail

try:
send_mail('subject', 'message', '[email protected]', ['[email protected]'])
except ValueError:
pass
""",
)


def test_bad_header_error_in_except():
check_transformed(
"""\
from django.core.mail import BadHeaderError

try:
do_something()
except BadHeaderError as e:
handle_error(e)
""",
"""\

try:
do_something()
except ValueError as e:
handle_error(e)
""",
)


def test_bad_header_error_raise_with_message():
check_transformed(
"""\
from django.core.mail import BadHeaderError

if invalid_header:
raise BadHeaderError("Header contains newline")
""",
"""\

if invalid_header:
raise ValueError("Header contains newline")
""",
)


def test_bad_header_error_multiple_uses():
check_transformed(
"""\
from django.core.mail import BadHeaderError, EmailMessage

try:
msg = EmailMessage()
msg.send()
except BadHeaderError:
raise BadHeaderError("Invalid header found")
""",
"""\
from django.core.mail import EmailMessage

try:
msg = EmailMessage()
msg.send()
except ValueError:
raise ValueError("Invalid header found")
""",
)


def test_bad_header_error_with_alias():
check_transformed(
"""\
from django.core.mail import BadHeaderError as BHE

raise BHE("Invalid")
""",
"""\

raise ValueError("Invalid")
""",
)


def test_bad_header_error_not_from_django():
check_noop(
"""\
from myapp.exceptions import BadHeaderError

raise BadHeaderError("Custom error")
""",
)


def test_bad_header_error_builtin_ValueError_already_used():
check_transformed(
"""\
from django.core.mail import BadHeaderError

try:
int("not a number")
except ValueError:
pass

raise BadHeaderError("Invalid header")
""",
"""\

try:
int("not a number")
except ValueError:
pass

raise ValueError("Invalid header")
""",
)