Skip to content
Closed
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
36 changes: 36 additions & 0 deletions examples/email_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
"""
Example demonstrating the EMAIL parameter type in Click.

This example shows how to use the EMAIL type to validate email addresses
in command-line applications.
"""

import click


@click.command()
@click.option(
"--email",
type=click.EMAIL,
required=True,
help="Email address to process"
)
@click.option(
"--notify-email",
type=click.EMAIL,
help="Optional notification email address"
)
def send_message(email, notify_email):
"""Send a message to the specified email address."""
click.echo(f"Sending message to: {email}")

if notify_email:
click.echo(f"Notification will be sent to: {notify_email}")

# Simulate sending email
click.echo("✅ Message sent successfully!")


if __name__ == "__main__":
send_message()
1 change: 1 addition & 0 deletions src/click/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from .types import BOOL as BOOL
from .types import Choice as Choice
from .types import DateTime as DateTime
from .types import EMAIL as EMAIL
from .types import File as File
from .types import FLOAT as FLOAT
from .types import FloatRange as FloatRange
Expand Down
43 changes: 43 additions & 0 deletions src/click/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,49 @@ def convert_type(ty: t.Any | None, default: t.Any | None = None) -> ParamType:
#: also be selected by using ``bool`` as a type.
BOOL = BoolParamType()

class EmailParamType(ParamType):
"""A parameter type that validates email addresses.

This type validates that the input is a properly formatted email address
using a simple but effective regex pattern. It accepts standard email
formats like '[email protected]' and '[email protected]'.

.. versionadded:: 8.2
"""
name = "email"

def convert(
self, value: t.Any, param: Parameter | None, ctx: Context | None
) -> str:
import re

if not isinstance(value, str):
value = str(value)

value = value.strip()

# More strict email validation regex
# - No leading/trailing dots in local part
# - No consecutive dots anywhere
# - Domain must have valid structure
email_pattern = r'^[a-zA-Z0-9]([a-zA-Z0-9._+%-]*[a-zA-Z0-9])?@[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?\.[a-zA-Z]{2,}$'

if re.match(email_pattern, value):
return value

self.fail(
_("{value!r} is not a valid email address.").format(value=value),
param,
ctx
)

def __repr__(self) -> str:
return "EMAIL"


#: An email parameter.
EMAIL = EmailParamType()

#: A UUID parameter.
UUID = UUIDParameterType()

Expand Down
83 changes: 83 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,3 +255,86 @@ def test_choice_get_invalid_choice_message():
choice = click.Choice(["a", "b", "c"])
message = choice.get_invalid_choice_message("d", ctx=None)
assert message == "'d' is not one of 'a', 'b', 'c'."


@pytest.mark.parametrize(
"email",
[
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
],
)
def test_email_valid(email):
"""Test that valid email addresses are accepted."""
email_type = click.EMAIL
result = email_type.convert(email, None, None)
assert result == email


@pytest.mark.parametrize(
"invalid_email",
[
"invalid",
"@example.com",
"user@",
"[email protected]",
"user@domain",
"user [email protected]",
"[email protected]",
"user@@example.com",
"",
"[email protected]",
"[email protected]",
"[email protected]",
],
)
def test_email_invalid(invalid_email):
"""Test that invalid email addresses are rejected."""
email_type = click.EMAIL
with pytest.raises(click.BadParameter, match="is not a valid email address"):
email_type.convert(invalid_email, None, None)


def test_email_strips_whitespace():
"""Test that email type strips leading and trailing whitespace."""
email_type = click.EMAIL
result = email_type.convert(" [email protected] ", None, None)
assert result == "[email protected]"


def test_email_converts_non_string():
"""Test that email type converts non-string values to strings first."""
email_type = click.EMAIL
# This would be an invalid email, but tests the conversion behavior
with pytest.raises(click.BadParameter):
email_type.convert(12345, None, None)


def test_email_repr():
"""Test the string representation of EMAIL type."""
email_type = click.EMAIL
assert repr(email_type) == "EMAIL"


def test_email_in_command(runner):
"""Test EMAIL type used in a command."""
@click.command()
@click.option("--email", type=click.EMAIL)
def cmd(email):
click.echo(f"Email: {email}")

# Test valid email
result = runner.invoke(cmd, ["--email", "[email protected]"])
assert result.exit_code == 0
assert "Email: [email protected]" in result.output

# Test invalid email
result = runner.invoke(cmd, ["--email", "invalid-email"])
assert result.exit_code != 0
assert "is not a valid email address" in result.output
Loading