-
Notifications
You must be signed in to change notification settings - Fork 55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature: Add phone number validator #203
Merged
Merged
Changes from 6 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
133fb3e
feature: Add phone number validator
mZbZ ea7054f
comment and import cleanup
mZbZ 798a247
comment and import cleanup
mZbZ f1f36fd
fix: Add typing extension fallback for python 3.8
mZbZ b3728cb
fix: Lint
mZbZ 52bcef6
fix: Make mypy happy
mZbZ b3d12f7
chore: Move json schema tests to json schema file
mZbZ File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
from typing import Any, Optional, Union | ||
|
||
try: | ||
from typing import Annotated | ||
except ImportError: | ||
# Python 3.8 | ||
from typing_extensions import Annotated | ||
|
||
|
||
import phonenumbers | ||
import pytest | ||
from phonenumbers import PhoneNumber | ||
from pydantic import BaseModel, TypeAdapter, ValidationError | ||
|
||
from pydantic_extra_types.phone_numbers import PhoneNumberValidator | ||
|
||
Number = Annotated[Union[str, PhoneNumber], PhoneNumberValidator()] | ||
NANumber = Annotated[ | ||
Union[str, PhoneNumber], | ||
PhoneNumberValidator( | ||
supported_regions=['US', 'CA'], | ||
default_region='US', | ||
), | ||
] | ||
UKNumber = Annotated[ | ||
Union[str, PhoneNumber], | ||
PhoneNumberValidator( | ||
supported_regions=['GB'], | ||
default_region='GB', | ||
number_format='E164', | ||
), | ||
] | ||
|
||
number_adapter = TypeAdapter(Number) | ||
|
||
|
||
class Numbers(BaseModel): | ||
phone_number: Optional[Number] = None | ||
na_number: Optional[NANumber] = None | ||
uk_number: Optional[UKNumber] = None | ||
|
||
|
||
def test_validator_constructor() -> None: | ||
PhoneNumberValidator() | ||
PhoneNumberValidator(supported_regions=['US', 'CA'], default_region='US') | ||
PhoneNumberValidator(supported_regions=['GB'], default_region='GB', number_format='E164') | ||
with pytest.raises(ValueError, match='Invalid default region code: XX'): | ||
PhoneNumberValidator(default_region='XX') | ||
with pytest.raises(ValueError, match='Invalid number format: XX'): | ||
PhoneNumberValidator(number_format='XX') | ||
with pytest.raises(ValueError, match='Invalid supported region code: XX'): | ||
PhoneNumberValidator(supported_regions=['XX']) | ||
|
||
|
||
# Note: the 555 area code will result in an invalid phone number | ||
def test_valid_phone_number() -> None: | ||
Numbers(phone_number='+1 901 555 1212') | ||
|
||
|
||
def test_when_extension_provided() -> None: | ||
Numbers(phone_number='+1 901 555 1212 ext 12533') | ||
|
||
|
||
def test_when_phonenumber_instance() -> None: | ||
phone_number = phonenumbers.parse('+1 901 555 1212', region='US') | ||
numbers = Numbers(phone_number=phone_number) | ||
assert numbers.phone_number == 'tel:+1-901-555-1212' | ||
# Additional validation is still performed on the instance | ||
with pytest.raises(ValidationError, match='value is not from a supported region'): | ||
Numbers(uk_number=phone_number) | ||
|
||
|
||
@pytest.mark.parametrize('invalid_number', ['', '123', 12, object(), '55 121']) | ||
def test_invalid_phone_number(invalid_number: Any) -> None: | ||
# Use a TypeAdapter to test the validation logic for None otherwise | ||
# optional fields will not attempt to validate | ||
with pytest.raises(ValidationError, match='value is not a valid phone number'): | ||
number_adapter.validate_python(invalid_number) | ||
|
||
|
||
def test_formats_phone_number() -> None: | ||
result = Numbers(phone_number='+1 901 555 1212 ext 12533', uk_number='+44 20 7946 0958') | ||
assert result.phone_number == 'tel:+1-901-555-1212;ext=12533' | ||
assert result.uk_number == '+442079460958' | ||
|
||
|
||
def test_default_region() -> None: | ||
result = Numbers(na_number='901 555 1212') | ||
assert result.na_number == 'tel:+1-901-555-1212' | ||
with pytest.raises(ValidationError, match='value is not a valid phone number'): | ||
Numbers(phone_number='901 555 1212') | ||
|
||
|
||
def test_supported_regions() -> None: | ||
assert Numbers(na_number='+1 901 555 1212') | ||
assert Numbers(uk_number='+44 20 7946 0958') | ||
with pytest.raises(ValidationError, match='value is not from a supported region'): | ||
Numbers(na_number='+44 20 7946 0958') | ||
|
||
|
||
def test_parse_error() -> None: | ||
with pytest.raises(ValidationError, match='value is not a valid phone number'): | ||
Numbers(phone_number='555 1212') | ||
|
||
|
||
def test_parsed_but_not_a_valid_number() -> None: | ||
with pytest.raises(ValidationError, match='value is not a valid phone number'): | ||
Numbers(phone_number='+1 555-1212') | ||
|
||
|
||
def test_json_schema() -> None: | ||
assert Numbers.model_json_schema() == { | ||
'title': 'Numbers', | ||
'type': 'object', | ||
'properties': { | ||
'phone_number': { | ||
'title': 'Phone Number', | ||
'anyOf': [ | ||
{'type': 'string', 'format': 'phone'}, | ||
{'type': 'null'}, | ||
], | ||
'default': None, | ||
}, | ||
'na_number': { | ||
'title': 'Na Number', | ||
'anyOf': [ | ||
{'type': 'string', 'format': 'phone'}, | ||
{'type': 'null'}, | ||
], | ||
'default': None, | ||
}, | ||
'uk_number': { | ||
'title': 'Uk Number', | ||
'anyOf': [ | ||
{'type': 'string', 'format': 'phone'}, | ||
{'type': 'null'}, | ||
], | ||
'default': None, | ||
}, | ||
}, | ||
} | ||
|
||
class SomethingElse(BaseModel): | ||
phone_number: Number | ||
|
||
assert SomethingElse.model_json_schema() == { | ||
'title': 'SomethingElse', | ||
'type': 'object', | ||
'properties': { | ||
'phone_number': { | ||
'title': 'Phone Number', | ||
'type': 'string', | ||
'format': 'phone', | ||
} | ||
}, | ||
'required': ['phone_number'], | ||
} | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing work @mZbZ but can you migrate this part of the tests to
tests/test_json_schema.py
in the tests directory so we can keep the tree of tests follow a patternThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you
No problem
FYI, there is still a codecov report that is still running...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
somethings its get bugged and stick like this you can try another change or an empty commit and its will be fixed