From a5585f2f01ea25b3c2c0f0d98c705c507c28e3ab Mon Sep 17 00:00:00 2001 From: fei <204683769+feiiiiii5@users.noreply.github.com> Date: Mon, 28 Sep 2026 18:02:56 +0800 Subject: [PATCH] Stop as_dict() from modifying the ValidatedEmail it is called on as_dict() handed back the instance's live __dict__ and then wrote repr() of domain_address into it, so reading the dict replaced the documented ipaddress.IPv4Address object on the object itself. Every later call repr'd the string again, and writing to the returned dict mutated the ValidatedEmail. as_constructor() does the same "make it printable" job with repr(getattr(self, key)) and never mutates, so take a copy instead. The existing test asserted the doubly repr'd value, which only existed because of the mutation; it now asserts the single repr and a new test pins that the attribute survives an as_dict() call. --- email_validator/types.py | 2 +- tests/test_main.py | 27 +++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/email_validator/types.py b/email_validator/types.py index e813065..c285d60 100644 --- a/email_validator/types.py +++ b/email_validator/types.py @@ -121,7 +121,7 @@ def as_constructor(self) -> str: """Convenience method for accessing ValidatedEmail as a dict""" def as_dict(self) -> dict[str, Any]: - d = self.__dict__ + d = dict(self.__dict__) if d.get('domain_address'): d['domain_address'] = repr(d['domain_address']) return d diff --git a/tests/test_main.py b/tests/test_main.py index f11087b..b8c6583 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,3 +1,4 @@ +import ipaddress import typing import pytest @@ -21,8 +22,30 @@ def test_dict_accessor_with_domain_address() -> None: input_email = "me@[127.0.0.1]" valid_email = validate_email(input_email, check_deliverability=False, allow_domain_literal=True) assert valid_email.domain == "[127.0.0.1]" - assert isinstance(valid_email.as_dict(), dict) - assert valid_email.as_dict()["domain_address"] == '"IPv4Address(\'127.0.0.1\')"' + as_dict = valid_email.as_dict() + assert isinstance(as_dict, dict) + assert as_dict["domain_address"] == "IPv4Address('127.0.0.1')" + + +def test_dict_accessor_does_not_modify_validated_email() -> None: + # as_dict() is only for reading: it must not replace the attributes of the + # object it is called on. The domain_address attribute is documented to hold + # the ipaddress.IPv4Address object. + input_email = "me@[127.0.0.1]" + valid_email = validate_email(input_email, check_deliverability=False, allow_domain_literal=True) + domain_address = valid_email.domain_address + assert isinstance(domain_address, ipaddress.IPv4Address) + + first_dict = valid_email.as_dict() + assert first_dict["domain_address"] == "IPv4Address('127.0.0.1')" + + # The object the dict was made from is unchanged, ... + assert valid_email.domain_address is domain_address + assert isinstance(valid_email.domain_address, ipaddress.IPv4Address) + + # ... and asking for the dict again gives the same result rather than + # repr() of a string. + assert valid_email.as_dict() == first_dict def test_main_single_good_input(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None: