Skip to content

Commit 44aa552

Browse files
authored
Add type annotations (#99)
This is my first time using type hints so hopefully it's right. * Added type annotations to the exported methods, some of the main internal methods, and the ValidatedEmail class. * ValidatedEmail's ascii_email, ascii_local_part, ascii_domain, mx, and mx_fallback_type attributes now no longer are set by the class's __init__ method, although they are always filled in by validate_email. * Make the main module's exports explicit to solve implicit re-export lint warning/error. * Added py.typed. * Added `mypy` to tests. * Made a -dev4 release to test. Closes #98.
1 parent 7f838b8 commit 44aa552

File tree

11 files changed

+73
-40
lines changed

11 files changed

+73
-40
lines changed

.github/workflows/test_and_build.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ jobs:
2323
- name: Lint with flake8
2424
run: |
2525
make lint
26+
- name: Check typing with mypy
27+
run: |
28+
make typing
2629
- name: Test with pytest
2730
run: |
2831
make test

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ install:
1515
- make install
1616

1717
script:
18+
- make typing
1819
- make lint
1920
- make test
2021

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
2.0.0-dev1
1+
2.0.0-dev4
22
----------
33

44
This is a pre-release for version 2.0.0.
55

6-
There are no significant changes to which email addresses are considered valid/invalid, but there are many changes in error messages and internal improvements to the library, and Python 3.7+ is now required.
6+
There are no significant changes to which email addresses are considered valid/invalid, but there are many changes in error messages and internal improvements to the library including the addition of type annotations, and Python 3.7+ is now required.
77

88
* Python 2.x and 3.x versions through 3.6, and dnspython 1.x, are no longer supported. Python 3.7+ with dnspython 2.x are now required.
99
* The dnspython package is no longer required if DNS checks are not used, although it will install automatically.
@@ -12,6 +12,7 @@ There are no significant changes to which email addresses are considered valid/i
1212
* Some other error messages have changed to not repeat the email address in the error message.
1313
* The library has been reorganized internally into smaller modules.
1414
* The tests have been reorganized and expanded. Deliverability tests now mostly use captured DNS responses so they can be run off-line.
15+
* Type annotations have been added to the exported methods and the ValidatedEmail class and some internal methods.
1516

1617
Version 1.3.1 (January 21, 2023)
1718
--------------------------------

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ lint:
1111
#python setup.py check -rms
1212
flake8 --ignore=E501,E126,W503 email_validator tests
1313

14+
.PHONY: typing
15+
typing:
16+
mypy email_validator/*.py tests/*.py
17+
1418
.PHONY: test
1519
test:
1620
PYTHONPATH=.:$PYTHONPATH pytest --cov=email_validator
@@ -21,7 +25,7 @@ testcov: test
2125
@coverage html
2226

2327
.PHONY: all
24-
all: testcov lint
28+
all: typing testcov lint
2529

2630
.PHONY: clean
2731
clean:

email_validator/__init__.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
# -*- coding: utf-8 -*-
22

33
# Export the main method, helper methods, and the public data types.
4-
from .exceptions_types import * # noqa: F401,F403
5-
from .validate_email import validate_email # noqa: F401
4+
from .exceptions_types import ValidatedEmail, EmailNotValidError, \
5+
EmailSyntaxError, EmailUndeliverableError
6+
from .validate_email import validate_email
7+
8+
9+
__all__ = ["validate_email",
10+
"ValidatedEmail", "EmailNotValidError",
11+
"EmailSyntaxError", "EmailUndeliverableError",
12+
"caching_resolver"]
613

714

815
def caching_resolver(*args, **kwargs):

email_validator/deliverability.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1+
from typing import Optional, Any, Dict
2+
13
from .exceptions_types import EmailUndeliverableError
24

35
import dns.resolver
46
import dns.exception
57

68

7-
def caching_resolver(*, timeout=None, cache=None):
9+
def caching_resolver(*, timeout: Optional[int] = None, cache=None):
810
if timeout is None:
911
from . import DEFAULT_TIMEOUT
1012
timeout = DEFAULT_TIMEOUT
1113
resolver = dns.resolver.Resolver()
12-
resolver.cache = cache or dns.resolver.LRUCache()
13-
resolver.lifetime = timeout # timeout, in seconds
14+
resolver.cache = cache or dns.resolver.LRUCache() # type: ignore
15+
resolver.lifetime = timeout # type: ignore # timeout, in seconds
1416
return resolver
1517

1618

17-
def validate_email_deliverability(domain, domain_i18n, timeout=None, dns_resolver=None):
19+
def validate_email_deliverability(domain: str, domain_i18n: str, timeout: Optional[int] = None, dns_resolver=None):
1820
# Check that the domain resolves to an MX record. If there is no MX record,
1921
# try an A or AAAA record which is a deprecated fallback for deliverability.
2022
# Raises an EmailUndeliverableError on failure. On success, returns a dict
@@ -30,7 +32,7 @@ def validate_email_deliverability(domain, domain_i18n, timeout=None, dns_resolve
3032
dns_resolver = dns.resolver.get_default_resolver()
3133
dns_resolver.lifetime = timeout
3234

33-
deliverability_info = {}
35+
deliverability_info: Dict[str, Any] = {}
3436

3537
try:
3638
try:

email_validator/exceptions_types.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from typing import Optional
2+
3+
14
class EmailNotValidError(ValueError):
25
"""Parent class of all exceptions raised by this module."""
36
pass
@@ -18,42 +21,42 @@ class ValidatedEmail(object):
1821
and other information."""
1922

2023
"""The email address that was passed to validate_email. (If passed as bytes, this will be a string.)"""
21-
original_email = None
24+
original_email: str
2225

2326
"""The normalized email address, which should always be used in preferance to the original address.
2427
The normalized address converts an IDNA ASCII domain name to Unicode, if possible, and performs
2528
Unicode normalization on the local part and on the domain (if originally Unicode). It is the
2629
concatenation of the local_part and domain attributes, separated by an @-sign."""
27-
email = None
30+
email: str
2831

2932
"""The local part of the email address after Unicode normalization."""
30-
local_part = None
33+
local_part: str
3134

3235
"""The domain part of the email address after Unicode normalization or conversion to
3336
Unicode from IDNA ascii."""
34-
domain = None
37+
domain: str
3538

3639
"""If not None, a form of the email address that uses 7-bit ASCII characters only."""
37-
ascii_email = None
40+
ascii_email: Optional[str]
3841

3942
"""If not None, the local part of the email address using 7-bit ASCII characters only."""
40-
ascii_local_part = None
43+
ascii_local_part: Optional[str]
4144

42-
"""If not None, a form of the domain name that uses 7-bit ASCII characters only."""
43-
ascii_domain = None
45+
"""A form of the domain name that uses 7-bit ASCII characters only."""
46+
ascii_domain: str
4447

4548
"""If True, the SMTPUTF8 feature of your mail relay will be required to transmit messages
4649
to this address. This flag is True just when ascii_local_part is missing. Otherwise it
4750
is False."""
48-
smtputf8 = None
51+
smtputf8: bool
4952

5053
"""If a deliverability check is performed and if it succeeds, a list of (priority, domain)
5154
tuples of MX records specified in the DNS for the domain."""
52-
mx = None
55+
mx: list
5356

5457
"""If no MX records are actually specified in DNS and instead are inferred, through an obsolete
5558
mechanism, from A or AAAA records, the value is the type of DNS record used instead (`A` or `AAAA`)."""
56-
mx_fallback_type = None
59+
mx_fallback_type: str
5760

5861
"""Tests use this constructor."""
5962
def __init__(self, **kwargs):
@@ -92,13 +95,13 @@ def __eq__(self, other):
9295
self.email == other.email
9396
and self.local_part == other.local_part
9497
and self.domain == other.domain
95-
and self.ascii_email == other.ascii_email
96-
and self.ascii_local_part == other.ascii_local_part
97-
and self.ascii_domain == other.ascii_domain
98+
and getattr(self, 'ascii_email', None) == getattr(other, 'ascii_email', None)
99+
and getattr(self, 'ascii_local_part', None) == getattr(other, 'ascii_local_part', None)
100+
and getattr(self, 'ascii_domain', None) == getattr(other, 'ascii_domain', None)
98101
and self.smtputf8 == other.smtputf8
99-
and repr(sorted(self.mx) if self.mx else self.mx)
100-
== repr(sorted(other.mx) if other.mx else other.mx)
101-
and self.mx_fallback_type == other.mx_fallback_type
102+
and repr(sorted(self.mx) if getattr(self, 'mx', None) else None)
103+
== repr(sorted(other.mx) if getattr(other, 'mx', None) else None)
104+
and getattr(self, 'mx_fallback_type', None) == getattr(other, 'mx_fallback_type', None)
102105
)
103106

104107
"""This helps producing the README."""

email_validator/syntax.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def safe_character_display(c):
3030
return unicodedata.name(c, h)
3131

3232

33-
def validate_email_local_part(local, allow_smtputf8=True, allow_empty_local=False):
33+
def validate_email_local_part(local: str, allow_smtputf8: bool = True, allow_empty_local: bool = False):
3434
"""Validates the syntax of the local part of an email address."""
3535

3636
if len(local) == 0:

email_validator/validate_email.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
1+
from typing import Optional, Union
2+
13
from .exceptions_types import EmailSyntaxError, ValidatedEmail
24
from .syntax import validate_email_local_part, validate_email_domain_part, get_length_reason
35
from .rfc_constants import EMAIL_MAX_LENGTH
46

57

68
def validate_email(
7-
email,
9+
email: Union[str, bytes],
810
# /, # not supported in Python 3.6, 3.7
911
*,
10-
allow_smtputf8=None,
11-
allow_empty_local=False,
12-
check_deliverability=None,
13-
test_environment=None,
14-
globally_deliverable=None,
15-
timeout=None,
16-
dns_resolver=None
17-
):
12+
allow_smtputf8: Optional[bool] = None,
13+
allow_empty_local: bool = False,
14+
check_deliverability: Optional[bool] = None,
15+
test_environment: Optional[bool] = None,
16+
globally_deliverable: Optional[bool] = None,
17+
timeout: Optional[int] = None,
18+
dns_resolver: Optional[object] = None
19+
) -> ValidatedEmail:
1820
"""
1921
Validates an email address, raising an EmailNotValidError if the address is not valid or returning a dict of
2022
information when the address is valid. The email argument can be a str or a bytes instance,
@@ -70,7 +72,11 @@ def validate_email(
7072

7173
# If the email address has an ASCII form, add it.
7274
if not ret.smtputf8:
73-
ret.ascii_email = ret.ascii_local_part + "@" + ret.ascii_domain
75+
if not ret.ascii_domain:
76+
raise Exception("Missing ASCII domain.")
77+
ret.ascii_email = (ret.ascii_local_part or "") + "@" + ret.ascii_domain
78+
else:
79+
ret.ascii_email = None
7480

7581
# If the email address has an ASCII representation, then we assume it may be
7682
# transmitted in ASCII (we can't assume SMTPUTF8 will be used on all hops to

setup.cfg

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = email_validator
3-
version = 2.0.0-dev1
3+
version = 2.0.0-dev4
44
description = A robust email address syntax and deliverability validation library.
55
long_description = file: README.md
66
long_description_content_type = text/markdown
@@ -29,6 +29,9 @@ install_requires =
2929
idna>=2.0.0
3030
python_requires = >=3.7
3131

32+
[options.package_data]
33+
* = py.typed
34+
3235
[options.entry_points]
3336
console_scripts =
3437
email_validator=email_validator:main

0 commit comments

Comments
 (0)