From 571fcf30a2e903a4ea928629d2ff3a6d8ceee7bf Mon Sep 17 00:00:00 2001 From: Arjun Date: Fri, 31 Jul 2026 18:00:26 +0530 Subject: [PATCH 1/2] Validate phone number types and support iterables --- care/utils/models/validators.py | 12 +++++++++- .../tests/test_phone_number_validator.py | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/care/utils/models/validators.py b/care/utils/models/validators.py index 58624d1143..426d1de908 100644 --- a/care/utils/models/validators.py +++ b/care/utils/models/validators.py @@ -104,10 +104,20 @@ class PhoneNumberValidator(RegexValidator): } def __init__(self, types: Iterable[str], *args, **kwargs): - if not isinstance(types, Iterable) or isinstance(types, str) or len(types) == 0: + if not isinstance(types, Iterable) or isinstance(types, str): msg = "The `types` argument must be a non-empty iterable." raise ValueError(msg) + types = tuple(types) + if not types: + msg = "The `types` argument must be a non-empty iterable." + raise ValueError(msg) + + unsupported_types = [type_ for type_ in types if type_ not in self.regex_map] + if unsupported_types: + msg = f"Unsupported phone number type(s): {', '.join(unsupported_types)}." + raise ValueError(msg) + self.types = types self.message = f"Invalid phone number. Must be one of the following types: {', '.join(self.types)}. Received: %(value)s" self.code = "invalid_phone_number" diff --git a/care/utils/tests/test_phone_number_validator.py b/care/utils/tests/test_phone_number_validator.py index 5f378c9954..14efdba8f6 100644 --- a/care/utils/tests/test_phone_number_validator.py +++ b/care/utils/tests/test_phone_number_validator.py @@ -130,3 +130,27 @@ def test_invalid_support_numbers(self): for number in self.invalid_support_numbers: with self.assertRaises(ValidationError, msg=f"Failed for {number}"): self.support_validator(number) + + def test_types_must_be_non_empty_iterable(self): + invalid_types = ["mobile", ()] + + for types in invalid_types: + with self.assertRaisesMessage( + ValueError, + "The `types` argument must be a non-empty iterable.", + ): + PhoneNumberValidator(types=types) + + def test_unsupported_types_raise_value_error(self): + with self.assertRaisesMessage( + ValueError, + "Unsupported phone number type(s): pager.", + ): + PhoneNumberValidator(types=("mobile", "pager")) + + def test_types_accepts_generator(self): + types = (type_ for type_ in ("mobile", "landline")) + validator = PhoneNumberValidator(types=types) + + self.assertIsNone(validator("+919876543210")) + self.assertIsNone(validator("+914902626488")) From fdc291d1d1d25d178883dbb9b340eaa32c476459 Mon Sep 17 00:00:00 2001 From: Arjun Date: Fri, 31 Jul 2026 18:19:31 +0530 Subject: [PATCH 2/2] Validate phone number types as collections --- care/utils/models/validators.py | 18 +++++++------ .../tests/test_phone_number_validator.py | 25 ++++++++++++++----- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/care/utils/models/validators.py b/care/utils/models/validators.py index 426d1de908..9b7a3e3581 100644 --- a/care/utils/models/validators.py +++ b/care/utils/models/validators.py @@ -1,5 +1,5 @@ import re -from collections.abc import Iterable +from collections.abc import Collection, Iterable from fractions import Fraction from pathlib import Path @@ -103,19 +103,23 @@ class PhoneNumberValidator(RegexValidator): "support": support_number_regex, } - def __init__(self, types: Iterable[str], *args, **kwargs): - if not isinstance(types, Iterable) or isinstance(types, str): - msg = "The `types` argument must be a non-empty iterable." + def __init__(self, types: Collection[str], *args, **kwargs): + if not isinstance(types, Collection) or isinstance(types, str): + msg = "The `types` argument must be a non-empty collection." raise ValueError(msg) types = tuple(types) if not types: - msg = "The `types` argument must be a non-empty iterable." + msg = "The `types` argument must be a non-empty collection." raise ValueError(msg) - unsupported_types = [type_ for type_ in types if type_ not in self.regex_map] + unsupported_types = [ + type_ + for type_ in types + if not isinstance(type_, str) or type_ not in self.regex_map + ] if unsupported_types: - msg = f"Unsupported phone number type(s): {', '.join(unsupported_types)}." + msg = f"Unsupported phone number type(s): {', '.join(str(type_) for type_ in unsupported_types)}." raise ValueError(msg) self.types = types diff --git a/care/utils/tests/test_phone_number_validator.py b/care/utils/tests/test_phone_number_validator.py index 14efdba8f6..f29b020e6e 100644 --- a/care/utils/tests/test_phone_number_validator.py +++ b/care/utils/tests/test_phone_number_validator.py @@ -131,13 +131,13 @@ def test_invalid_support_numbers(self): with self.assertRaises(ValidationError, msg=f"Failed for {number}"): self.support_validator(number) - def test_types_must_be_non_empty_iterable(self): - invalid_types = ["mobile", ()] + def test_types_must_be_non_empty_collection(self): + invalid_types = ["mobile", (), (type_ for type_ in ("mobile",))] for types in invalid_types: with self.assertRaisesMessage( ValueError, - "The `types` argument must be a non-empty iterable.", + "The `types` argument must be a non-empty collection.", ): PhoneNumberValidator(types=types) @@ -148,9 +148,22 @@ def test_unsupported_types_raise_value_error(self): ): PhoneNumberValidator(types=("mobile", "pager")) - def test_types_accepts_generator(self): - types = (type_ for type_ in ("mobile", "landline")) - validator = PhoneNumberValidator(types=types) + def test_unhashable_types_raise_value_error(self): + with self.assertRaisesMessage( + ValueError, + "Unsupported phone number type(s): [].", + ): + PhoneNumberValidator(types=([],)) + + def test_non_string_types_raise_value_error(self): + with self.assertRaisesMessage( + ValueError, + "Unsupported phone number type(s): 1.", + ): + PhoneNumberValidator(types=(1,)) + + def test_types_accepts_reiterable_collection(self): + validator = PhoneNumberValidator(types=["mobile", "landline"]) self.assertIsNone(validator("+919876543210")) self.assertIsNone(validator("+914902626488"))