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
4 changes: 2 additions & 2 deletions .github/workflows/pycqa.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ name: pycqa
on:
workflow_dispatch:
push:
branches: [master]
branches: [master, release]
pull_request:
branches: [master]
branches: [master, release]
jobs:
tooling:
runs-on: ubuntu-latest
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/sast.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ name: sast
on:
workflow_dispatch:
push:
branches: ["master"]
branches: ["master", "release"]
pull_request:
branches: ["master"]
branches: ["master", "release"]

jobs:
sast:
Expand Down
8 changes: 5 additions & 3 deletions src/validators/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def email(
(Literal[True]): If `value` is a valid eMail.
(ValidationError): If `value` is an invalid eMail.
"""
if not value or value.count("@") != 1:
if not value or "@" not in value:
return False

username_part, domain_part = value.rsplit("@", 1)
Expand All @@ -68,6 +68,8 @@ def email(
if domain_part.startswith("[") and domain_part.endswith("]"):
# ref: RFC 5321
domain_part = domain_part.lstrip("[").rstrip("]")
if ipv6_address and domain_part.startswith("IPv6:"):
domain_part = domain_part[5:]
else:
return False

Expand All @@ -91,8 +93,8 @@ def email(
+ r"(\.[\u0100-\u017F\u0180-\u024F\u00A0-\u00FF0-9a-z!#$%&'*+/=?^_`{}|~\-]+)*$"
# quoted-string
+ r'|^"('
+ r"[\u0100-\u017F\u0180-\u024F\u00A0-\u00FF\001-\010\013\014\016-\037"
+ r"!#-\[\]-\177]|\\[\011.]"
+ r"[\u0100-\u017F\u0180-\u024F\u00A0-\u00FF"
+ r"!#-\[\]-~]|\\[\011.]"
+ r')*")$',
username_part,
re.IGNORECASE,
Expand Down
9 changes: 8 additions & 1 deletion src/validators/ip_address.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""IP Address."""

# cosmetic: no functional change (CI trigger demo)
# standard
from ipaddress import (
AddressValueError,
Expand Down Expand Up @@ -88,6 +89,10 @@ def ipv4(
if cidr:
if strict and value.count("/") != 1:
raise ValueError("IPv4 address was expected in CIDR notation")
if strict:
_, prefix = value.split("/")
if not prefix.isdigit() or not (0 <= int(prefix) <= 32):
raise ValueError("CIDR prefix must be a number between 0 and 32")
return IPv4Network(value, strict=not host_bit) and _check_private_ip(value, private)
return IPv4Address(value) and _check_private_ip(value, private)
except (ValueError, AddressValueError, NetmaskValueError):
Expand All @@ -113,7 +118,7 @@ def ipv6(value: str, /, *, cidr: bool = True, strict: bool = False, host_bit: bo

Args:
value:
IP address string to validate.
IP address string to validate. It may be enclosed in brackets.
cidr:
IP address string may contain CIDR annotation.
strict:
Expand All @@ -129,6 +134,8 @@ def ipv6(value: str, /, *, cidr: bool = True, strict: bool = False, host_bit: bo
"""
if not value:
return False
if value.startswith("[") and value.endswith("]"):
value = value[1:-1]
try:
if cidr:
if strict and value.count("/") != 1:
Expand Down
1 change: 0 additions & 1 deletion tests/test_email.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ def test_returns_true_on_valid_email(value: str):
("example@inv-.alid-.com",),
("example@inv-.-alid.com",),
("john56789.john56789.john56789.john56789.john56789.john56789.john5@example.com",),
('"test@test"@example.com',),
# Quoted-string format (CR not allowed)
('"\\\012"@here.com',),
# Non-quoted space/semicolon not allowed
Expand Down
20 changes: 20 additions & 0 deletions tests/test_email_control_character.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""HU-02: reject ASCII control characters in quoted SMTP local parts."""

# external
import pytest

import validators


@pytest.mark.parametrize(
"control_code",
[1, 2, 7, 11, 12, 14, 27, 31, 127],
)

def test_email_rejects_control_characters(control_code: int):
"""Reject selected ASCII control characters in quoted local parts."""
email = f'"user{chr(control_code)}name"@example.com'

result = validators.email(email)

assert isinstance(result, validators.ValidationError), repr(result)
20 changes: 20 additions & 0 deletions tests/test_email_ipv6_literal.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""HU-02: accept the IPv6: prefix in an SMTP address literal."""

# external
import pytest

import validators


@pytest.mark.parametrize(
("email",),
[
("usuario@[IPv6:2001:db8::1]", ),
],
)
def test_email_accepts_ipv6_literal_with_prefix(email: str):
"""Email accepts ipv6 literal with prefix."""
result = validators.email(
email, ipv6_address=True
)
assert result is True, repr(result)
19 changes: 19 additions & 0 deletions tests/test_email_quoted_at_sign.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"""HU-02: accept @ inside a quoted local part."""

# external
import pytest

import validators


@pytest.mark.parametrize(
("email",),
[
('"user@name"@example.com',),
('"test@test"@example.com',),
],
)
def test_email_accepts_at_sign_inside_quoted_local_part(email: str):
"""The email accepts at sign inside quoted local part character."""
result = validators.email(email)
assert result is True, repr(result)
18 changes: 18 additions & 0 deletions tests/test_ipv4_strict_cidr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""HU-04: require a prefix length in the documented strict CIDR mode."""

# external
import pytest

import validators


@pytest.mark.parametrize(
("ip",),
[
("192.168.1.0/255.255.255.0", ),
],
)
def test_ipv4_strict_cidr_rejects_dotted_netmask(ip: str):
"""The network is valid, but its representation is not CIDR prefix notation."""
result = validators.ipv4(ip, strict=True)
assert isinstance(result, validators.ValidationError), repr(result)
24 changes: 24 additions & 0 deletions tests/test_ipv6_brackets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""HU-04: proposed enhancement; brackets are URL host syntax.

This test expresses a new requirement, not a defect in the current contract.
"""

# external
import pytest

import validators


@pytest.mark.parametrize(
("ip",),
[
("[::1]",),
("[2001:db8::1]",),
("[2001:0db8:0000:0000:0000:0000:0000:0001]",),
("[::ffff:192.0.2.128]",),
],
)
def test_ipv6_accepts_address_in_brackets(ip: str):
"""Accept an IPv6 address enclosed in brackets."""
result = validators.ipv6(ip)
assert result is True, repr(result)
11 changes: 11 additions & 0 deletions tests/test_url_utf8_query_parameter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""HA-01. Test URL validation with UTF-8 query parameters."""

# local
from validators import url


def test_url_with_utf8_query_parameter():
"""Test that URL validator accepts UTF-8 characters in query parameters."""
result = url("https://domain.com/search?q=caña")

assert result is True