Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Read the CLI option environment variables the way they are documented
Two defects in the same block, both of which make a documented option
unusable or inverted:

* the boolean options went through bool(), so CHECK_DELIVERABILITY=false,
  TEST_ENVIRONMENT=0 and any other non-empty value all meant *true* - the
  check the user asked to turn off is the one that runs, including the DNS
  lookups;
* DEFAULT_TIMEOUT was forwarded as a keyword argument, but validate_email
  has no such parameter (the real ones are timeout and the module-level
  DEFAULT_TIMEOUT), so setting it always ended in
  TypeError: validate_email() got an unexpected keyword argument
  'default_timeout'.

The module documents that "keyword arguments to validate_email can be set in
environment variables of the same name but uppercase" and 2.0.0 added the
option reading, so both spellings should mean what the docs say. The falsy
set is a superset of what bool() rejected before (the empty string, plus
0/false/no/off, case- and space-insensitive), so nothing that worked
changes.
  • Loading branch information
feiiiiii5 committed Sep 28, 2026
commit 27c16b57b15b1c77d47ed248f6c49ce691a07340
9 changes: 7 additions & 2 deletions email_validator/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,15 @@ def main(dns_resolver: Optional[_Resolver] = None) -> None:
'ALLOW_DISPLAY_NAME',
'GLOBALLY_DELIVERABLE', 'CHECK_DELIVERABILITY', 'TEST_ENVIRONMENT'):
if varname in os.environ:
options[varname.lower()] = bool(os.environ[varname])
# bool() would make any non-empty string True, so e.g.
# CHECK_DELIVERABILITY=false would enable the check.
options[varname.lower()] = os.environ[varname].strip().lower() not in ('', '0', 'false', 'no', 'off')
for varname in ('DEFAULT_TIMEOUT',):
if varname in os.environ:
options[varname.lower()] = float(os.environ[varname])
# DEFAULT_TIMEOUT is a module-level default read by
# caching_resolver(), not a keyword argument of validate_email.
import email_validator
email_validator.DEFAULT_TIMEOUT = int(os.environ[varname])

if len(sys.argv) == 1:
# Validate the email addresses passed line-by-line on STDIN.
Expand Down
37 changes: 37 additions & 0 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,43 @@ def test_main_multi_input(monkeypatch: pytest.MonkeyPatch, capsys: pytest.Captur
assert test_cases[3] in stdout


@pytest.mark.parametrize('value', ['false', 'False', 'FALSE', '0', 'no', 'off', ' '])
def test_main_falsy_boolean_option_from_env(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], value: str) -> None:
# Options are set from environment variables of the same name in upper
# case. A falsy value there must turn the option off, not on.
import json
test_email = 'test@xkxufoekjvjfjeodlfmdfjcu.com' # this domain does not exist in DNS
monkeypatch.setenv('CHECK_DELIVERABILITY', value)
monkeypatch.setattr('sys.argv', ['email_validator', test_email])
validator_command_line_tool(dns_resolver=RESOLVER)
stdout, _ = capsys.readouterr()
assert json.loads(str(stdout))["normalized"] == test_email


@pytest.mark.parametrize('value', ['false', 'False', '0', 'no', 'off', ' '])
def test_main_falsy_test_environment_from_env(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str], value: str) -> None:
# test_environment off means the reserved "test" domain names are rejected.
monkeypatch.setenv('TEST_ENVIRONMENT', value)
monkeypatch.setattr('sys.argv', ['email_validator', 'me@foo.test'])
validator_command_line_tool(dns_resolver=RESOLVER)
stdout, _ = capsys.readouterr()
assert stdout == 'The part after the @-sign is a special-use or reserved name that cannot be used with email.\n'


def test_main_default_timeout_from_env(monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]) -> None:
import json
import email_validator
test_email = 'google@google.com'
# Set up the restore of the module-level default, which main() overwrites.
monkeypatch.setattr('email_validator.DEFAULT_TIMEOUT', email_validator.DEFAULT_TIMEOUT)
monkeypatch.setenv('DEFAULT_TIMEOUT', '5')
monkeypatch.setattr('sys.argv', ['email_validator', test_email])
validator_command_line_tool(dns_resolver=RESOLVER)
stdout, _ = capsys.readouterr()
assert json.loads(str(stdout))["normalized"] == test_email
assert email_validator.DEFAULT_TIMEOUT == 5


def test_bytes_input() -> None:
input_email = b"testaddr@example.tld"
valid_email = validate_email(input_email, check_deliverability=False)
Expand Down
Loading