mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Fix global SEND_DEFAULTS merging
Replace generic `combine` with specific `merge_dicts_deep`, `merge_dicts_shallow`, `merge_dicts_one_level` or `concat_lists`, depending on appropriate behavior for each message attribute. Fixes merging global `SEND_DEFAULTS` with message `esp_extra` for ESP APIs that use nested payload structures. And clarifies intent for other properties.
This commit is contained in:
@@ -209,7 +209,10 @@ class SendDefaultsTests(TestBackendTestCase):
|
||||
"tags": ["globaltag"],
|
||||
"track_clicks": True,
|
||||
"track_opens": False,
|
||||
"esp_extra": {"globalextra": "globalsetting"},
|
||||
"esp_extra": {
|
||||
"globalextra": "globalsetting",
|
||||
"deepextra": {"deep1": "globaldeep1", "deep2": "globaldeep2"},
|
||||
},
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -218,7 +221,10 @@ class SendDefaultsTests(TestBackendTestCase):
|
||||
self.message.metadata = {"message": "messagevalue", "other": "override"}
|
||||
self.message.tags = ["messagetag"]
|
||||
self.message.track_clicks = False
|
||||
self.message.esp_extra = {"messageextra": "messagesetting"}
|
||||
self.message.esp_extra = {
|
||||
"messageextra": "messagesetting",
|
||||
"deepextra": {"deep2": "messagedeep2", "deep3": "messagedeep3"},
|
||||
}
|
||||
|
||||
self.message.send()
|
||||
params = self.get_send_params()
|
||||
@@ -234,8 +240,13 @@ class SendDefaultsTests(TestBackendTestCase):
|
||||
self.assertEqual(params["tags"], ["globaltag", "messagetag"])
|
||||
self.assertEqual(params["track_clicks"], False) # message overrides
|
||||
self.assertEqual(params["track_opens"], False) # (no message setting)
|
||||
# esp_extra is deep merged:
|
||||
self.assertEqual(params["globalextra"], "globalsetting")
|
||||
self.assertEqual(params["messageextra"], "messagesetting")
|
||||
self.assertEqual(
|
||||
params["deepextra"],
|
||||
{"deep1": "globaldeep1", "deep2": "messagedeep2", "deep3": "messagedeep3"},
|
||||
)
|
||||
|
||||
# Send another message to make sure original SEND_DEFAULTS unchanged
|
||||
send_mail("subject", "body", "from@example.com", ["to@example.com"])
|
||||
@@ -247,6 +258,9 @@ class SendDefaultsTests(TestBackendTestCase):
|
||||
self.assertEqual(params["track_clicks"], True)
|
||||
self.assertEqual(params["track_opens"], False)
|
||||
self.assertEqual(params["globalextra"], "globalsetting")
|
||||
self.assertEqual(
|
||||
params["deepextra"], {"deep1": "globaldeep1", "deep2": "globaldeep2"}
|
||||
)
|
||||
|
||||
@override_settings(
|
||||
ANYMAIL={
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Tests for the anymail/utils.py module
|
||||
# (not to be confused with utilities for testing found in in tests/utils.py)
|
||||
# (not to be confused with utilities for testing found in tests/utils.py)
|
||||
import base64
|
||||
import copy
|
||||
import pickle
|
||||
@@ -17,12 +17,17 @@ from anymail.utils import (
|
||||
Attachment,
|
||||
CaseInsensitiveCasePreservingDict,
|
||||
EmailAddress,
|
||||
concat_lists,
|
||||
force_non_lazy,
|
||||
force_non_lazy_dict,
|
||||
force_non_lazy_list,
|
||||
get_request_basic_auth,
|
||||
get_request_uri,
|
||||
is_lazy,
|
||||
last,
|
||||
merge_dicts_deep,
|
||||
merge_dicts_one_level,
|
||||
merge_dicts_shallow,
|
||||
parse_address_list,
|
||||
parse_rfc2822date,
|
||||
parse_single_address,
|
||||
@@ -559,3 +564,133 @@ class UnsetValueTests(SimpleTestCase):
|
||||
def test_equality(self):
|
||||
# `is UNSET` is preferred to `== UNSET`, but both should work
|
||||
self.assertEqual(UNSET, UNSET)
|
||||
|
||||
|
||||
class CombinerTests(SimpleTestCase):
|
||||
def test_concat_lists(self):
|
||||
for args, expected in [
|
||||
(([1, 2], [3, 4]), [1, 2, 3, 4]),
|
||||
# Does not flatten:
|
||||
(([1, [11, 12]], [2]), [1, [11, 12], 2]),
|
||||
# UNSET args ignored:
|
||||
((UNSET, [1, 2], UNSET, [3, 4], UNSET), [1, 2, 3, 4]),
|
||||
# None clears previous:
|
||||
(([1, 2], None, [3, 4]), [3, 4]),
|
||||
# Works with other sequence-like types:
|
||||
(([1], (2, 3), {4}), [1, 2, 3, 4]),
|
||||
# Degenerate cases:
|
||||
((), UNSET),
|
||||
((UNSET,), UNSET),
|
||||
((None,), UNSET),
|
||||
(([], None), UNSET),
|
||||
]:
|
||||
with self.subTest(repr(args)):
|
||||
original_args = copy.deepcopy(args)
|
||||
merged = concat_lists(*args)
|
||||
self.assertEqual(merged, expected)
|
||||
# Verify args were not modified:
|
||||
self.assertEqual(args, original_args)
|
||||
|
||||
def test_merge_dicts_shallow(self):
|
||||
for args, expected in [
|
||||
(({"a": 1}, {"b": 2}), {"a": 1, "b": 2}),
|
||||
(
|
||||
({"a": 1, "b": 2}, {"a": 11, "c": 33}, {"c": 3}),
|
||||
{"a": 11, "b": 2, "c": 3},
|
||||
),
|
||||
# shallow merge:
|
||||
(({"a": {"a1": 1}, "b": 2}, {"a": {"a2": 2}}), {"a": {"a2": 2}, "b": 2}),
|
||||
# UNSET args ignored:
|
||||
((UNSET, {"a": 1}, UNSET, {"b": 2}, UNSET), {"a": 1, "b": 2}),
|
||||
# None clears previous:
|
||||
(({"a": 1}, None, {"b": 2}), {"b": 2}),
|
||||
# Degenerate cases:
|
||||
((), UNSET),
|
||||
((UNSET,), UNSET),
|
||||
((None,), UNSET),
|
||||
(({}, None), UNSET),
|
||||
]:
|
||||
with self.subTest(repr(args)):
|
||||
original_args = copy.deepcopy(args)
|
||||
merged = merge_dicts_shallow(*args)
|
||||
self.assertEqual(merged, expected)
|
||||
# Verify args were not modified:
|
||||
self.assertEqual(args, original_args)
|
||||
|
||||
def test_merge_dicts_deep(self):
|
||||
for args, expected in [
|
||||
(({"a": 1}, {"b": 2}), {"a": 1, "b": 2}),
|
||||
(
|
||||
({"a": 1, "b": 2}, {"a": 11, "c": 33}, {"c": 3}),
|
||||
{"a": 11, "b": 2, "c": 3},
|
||||
),
|
||||
# deep merge:
|
||||
(
|
||||
(
|
||||
{"a": {"a1": 1, "a3": {"a3a": 31}}},
|
||||
{"a": {"a2": 2, "a3": {"a3b": 32}}},
|
||||
),
|
||||
{"a": {"a1": 1, "a2": 2, "a3": {"a3a": 31, "a3b": 32}}},
|
||||
),
|
||||
# UNSET (top-level) args ignored:
|
||||
((UNSET, {"a": 1}, UNSET, {"b": 2}, UNSET), {"a": 1, "b": 2}),
|
||||
# None clears previous:
|
||||
(({"a": 1}, None, {"b": 2}), {"b": 2}),
|
||||
# Degenerate cases:
|
||||
((), UNSET),
|
||||
((UNSET,), UNSET),
|
||||
((None,), UNSET),
|
||||
(({}, None), UNSET),
|
||||
]:
|
||||
with self.subTest(repr(args)):
|
||||
original_args = copy.deepcopy(args)
|
||||
merged = merge_dicts_deep(*args)
|
||||
self.assertEqual(merged, expected)
|
||||
# Verify args were not modified:
|
||||
self.assertEqual(args, original_args)
|
||||
|
||||
def test_merge_dicts_one_level(self):
|
||||
for args, expected in [
|
||||
# one-level merge:
|
||||
(
|
||||
(
|
||||
{"a": {"a1": 1, "a3": {"a3a": 31}}},
|
||||
{"a": {"a2": 2, "a3": {"a3b": 32}}},
|
||||
),
|
||||
{"a": {"a1": 1, "a2": 2, "a3": {"a3b": 32}}}, # but not a3a
|
||||
),
|
||||
# UNSET (top-level) args ignored:
|
||||
((UNSET, {"a": {}}, UNSET, {"b": {}}, UNSET), {"a": {}, "b": {}}),
|
||||
# None clears previous:
|
||||
(({"a": {}}, None, {"b": {}}), {"b": {}}),
|
||||
# Degenerate cases:
|
||||
((), UNSET),
|
||||
((UNSET,), UNSET),
|
||||
((None,), UNSET),
|
||||
(({}, None), UNSET),
|
||||
]:
|
||||
with self.subTest(repr(args)):
|
||||
original_args = copy.deepcopy(args)
|
||||
merged = merge_dicts_one_level(*args)
|
||||
self.assertEqual(merged, expected)
|
||||
# Verify args were not modified:
|
||||
self.assertEqual(args, original_args)
|
||||
|
||||
def test_last(self):
|
||||
for args, expected in [
|
||||
((1, 2, 3), 3),
|
||||
# UNSET args ignored:
|
||||
((UNSET, 1, UNSET, 2, UNSET), 2),
|
||||
# None clears previous:
|
||||
((1, 2, None), UNSET),
|
||||
# Degenerate cases:
|
||||
((), UNSET),
|
||||
((UNSET,), UNSET),
|
||||
((None,), UNSET),
|
||||
]:
|
||||
with self.subTest(repr(args)):
|
||||
original_args = copy.deepcopy(args)
|
||||
merged = last(*args)
|
||||
self.assertEqual(merged, expected)
|
||||
# Verify args were not modified:
|
||||
self.assertEqual(args, original_args)
|
||||
|
||||
Reference in New Issue
Block a user