From d84200e4eb4d20116080130c612ba157a4718977 Mon Sep 17 00:00:00 2001 From: Matthias Kestenholz Date: Fri, 9 Aug 2024 17:18:42 +0200 Subject: [PATCH] Fixed #35648 -- Raised NotImplementedError in SafeString.__add__ for non-string RHS. This change ensures SafeString addition operations handle non-string RHS properly, allowing them to implement __radd__ for better compatibility. --- django/utils/safestring.py | 14 +++++--- docs/releases/5.2.txt | 5 ++- tests/utils_tests/test_safestring.py | 51 ++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 5 deletions(-) diff --git a/django/utils/safestring.py b/django/utils/safestring.py index 4eb0207a66a..1ac9b877e95 100644 --- a/django/utils/safestring.py +++ b/django/utils/safestring.py @@ -35,10 +35,16 @@ class SafeString(str, SafeData): Concatenating a safe string with another safe bytestring or safe string is safe. Otherwise, the result is no longer safe. """ - t = super().__add__(rhs) - if isinstance(rhs, SafeData): - return SafeString(t) - return t + if isinstance(rhs, str): + t = super().__add__(rhs) + if isinstance(rhs, SafeData): + t = SafeString(t) + return t + + # Give the rhs object a chance to handle the addition, for example if + # the rhs object's class implements `__radd__`. More details: + # https://docs.python.org/3/reference/datamodel.html#object.__radd__ + return NotImplemented def __str__(self): return self diff --git a/docs/releases/5.2.txt b/docs/releases/5.2.txt index a15e6692053..9b81117095b 100644 --- a/docs/releases/5.2.txt +++ b/docs/releases/5.2.txt @@ -262,7 +262,10 @@ URLs Utilities ~~~~~~~~~ -* ... +* :class:`~django.utils.safestring.SafeString` now raises + :exc:`NotImplementedError` in ``__add__`` for non-string right-hand side + values. This aligns with the :py:class:`str` addition behavior and allows + ``__radd__`` to be used if available. Validators ~~~~~~~~~~ diff --git a/tests/utils_tests/test_safestring.py b/tests/utils_tests/test_safestring.py index eca32ff8f63..2ae8e57b191 100644 --- a/tests/utils_tests/test_safestring.py +++ b/tests/utils_tests/test_safestring.py @@ -132,3 +132,54 @@ class SafeStringTest(SimpleTestCase): for case, expected in cases: with self.subTest(case=case): self.assertRenderEqual("{{ s }}", expected, s=s + case) + + def test_add_obj(self): + + base_str = "strange" + add_str = "hello
" + + class Add: + def __add__(self, other): + return base_str + other + + class AddSafe: + def __add__(self, other): + return mark_safe(base_str) + other + + class Radd: + def __radd__(self, other): + return other + base_str + + class RaddSafe: + def __radd__(self, other): + return other + mark_safe(base_str) + + left_add_expected = f"{base_str}{add_str}" + right_add_expected = f"{add_str}{base_str}" + cases = [ + # Left-add test cases. + (Add(), add_str, left_add_expected, str), + (Add(), mark_safe(add_str), left_add_expected, str), + (AddSafe(), add_str, left_add_expected, str), + (AddSafe(), mark_safe(add_str), left_add_expected, SafeString), + # Right-add test cases. + (add_str, Radd(), right_add_expected, str), + (mark_safe(add_str), Radd(), right_add_expected, str), + (add_str, Radd(), right_add_expected, str), + (mark_safe(add_str), RaddSafe(), right_add_expected, SafeString), + ] + for lhs, rhs, expected, expected_type in cases: + with self.subTest(lhs=lhs, rhs=rhs): + result = lhs + rhs + self.assertEqual(result, expected) + self.assertEqual(type(result), expected_type) + + cases = [ + ("hello", Add()), + ("hello", AddSafe()), + (Radd(), "hello"), + (RaddSafe(), "hello"), + ] + for lhs, rhs in cases: + with self.subTest(lhs=lhs, rhs=rhs), self.assertRaises(TypeError): + lhs + rhs