From 23a351bb0b257945397a0ff2fe6b66302c30fdfd Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Sun, 23 Mar 2025 11:58:29 -0400 Subject: [PATCH 1/5] Email --- Pipfile | 2 +- Pipfile.lock | 38 ++-- protovalidate/internal/extra_func.py | 289 +++++++++++++++++++++------ tests/conformance/nonconforming.yaml | 40 ---- 4 files changed, 243 insertions(+), 126 deletions(-) diff --git a/Pipfile b/Pipfile index 0163de0e..574a0825 100644 --- a/Pipfile +++ b/Pipfile @@ -10,10 +10,10 @@ protobuf = "==6.*" [dev-packages] pytest = "*" mypy = "*" -ruff = "*" types-protobuf = "*" exceptiongroup = "*" tomli = "*" +ruff = "*" [requires] python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index 06352bf6..56244f72 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -240,28 +240,28 @@ }, "ruff": { "hashes": [ - "sha256:049a191969a10897fe052ef9cc7491b3ef6de79acd7790af7d7897b7a9bfbcb6", - "sha256:1bc09a7419e09662983b1312f6fa5dab829d6ab5d11f18c3760be7ca521c9329", - "sha256:3191e9116b6b5bbe187447656f0c8526f0d36b6fd89ad78ccaad6bdc2fad7df2", - "sha256:38c23fd9bdec4eb437b4c1e3595905a0a8edfccd63a790f818b28c78fe345639", - "sha256:3c3156d3f4b42e57247275a0a7e15a851c165a4fc89c5e8fa30ea6da4f7407b8", - "sha256:490b1e147c1260545f6d041c4092483e3f6d8eba81dc2875eaebcf9140b53905", - "sha256:6fbb2aed66fe742a6a3a0075ed467a459b7cedc5ae01008340075909d819df1e", - "sha256:7c8661b0be91a38bd56db593e9331beaf9064a79028adee2d5f392674bbc5e88", - "sha256:868364fc23f5aa122b00c6f794211e85f7e78f5dffdf7c590ab90b8c4e69b657", - "sha256:92c0c1ff014351c0b0cdfdb1e35fa83b780f1e065667167bb9502d47ca41e6db", - "sha256:96bc89a5c5fd21a04939773f9e0e276308be0935de06845110f43fd5c2e4ead7", - "sha256:a9352b9d767889ec5df1483f94870564e8102d4d7e99da52ebf564b882cdc2c7", - "sha256:b6c0e8d3d2db7e9f6efd884f44b8dc542d5b6b590fc4bb334fdbc624d93a29a2", - "sha256:bcfa478daf61ac8002214eb2ca5f3e9365048506a9d52b11bea3ecea822bb844", - "sha256:c58bfa00e740ca0a6c43d41fb004cd22d165302f360aaa56f7126d544db31a21", - "sha256:dc67e32bc3b29557513eb7eeabb23efdb25753684b913bebb8a0c62495095acb", - "sha256:e4fd5ff5de5f83e0458a138e8a869c7c5e907541aec32b707f57cf9a5e124445", - "sha256:e55c620690a4a7ee6f1cccb256ec2157dc597d109400ae75bbf944fc9d6462e2" + "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", + "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", + "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", + "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", + "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", + "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", + "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", + "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", + "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", + "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", + "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", + "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", + "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", + "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", + "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", + "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", + "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", + "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.11.0" + "version": "==0.11.2" }, "tomli": { "hashes": [ diff --git a/protovalidate/internal/extra_func.py b/protovalidate/internal/extra_func.py index cf211c75..69a5ab3d 100644 --- a/protovalidate/internal/extra_func.py +++ b/protovalidate/internal/extra_func.py @@ -13,8 +13,8 @@ # limitations under the License. import math +import re import typing -from email.utils import parseaddr from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_address, ip_network from urllib import parse as urlparse @@ -23,10 +23,18 @@ from protovalidate.internal import string_format +# See https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address +_email_regex = re.compile( + "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" +) -def _validate_hostname(host): + +def _validate_hostname( + host, +): if not host: return False + if len(host) > 253: return False @@ -49,24 +57,11 @@ def _validate_hostname(host): return not all_digits -def validate_email(addr): - parts = parseaddr(addr) - if addr != parts[1]: - return False - - addr = parts[1] - if len(addr) > 254: - return False - - parts = addr.split("@") - if len(parts) != 2: - return False - if len(parts[0]) > 64: - return False - return _validate_hostname(parts[1]) - - -def validate_host_and_port(string: str, *, port_required: bool) -> bool: +def validate_host_and_port( + string: str, + *, + port_required: bool, +) -> bool: if not string: return False @@ -75,20 +70,37 @@ def validate_host_and_port(string: str, *, port_required: bool) -> bool: end = string.find("]") after_end = end + 1 if after_end == len(string): # no port - return not port_required and validate_ip(string[1:end], 6) + return not port_required and validate_ip( + string[1:end], + 6, + ) if after_end == split_idx: # port return validate_ip(string[1:end]) and validate_port(string[split_idx + 1 :]) return False # malformed if split_idx == -1: - return not port_required and (_validate_hostname(string) or validate_ip(string, 4)) + return not port_required and ( + _validate_hostname(string) + or validate_ip( + string, + 4, + ) + ) host = string[:split_idx] port = string[split_idx + 1 :] - return (_validate_hostname(host) or validate_ip(host, 4)) and validate_port(port) - - -def validate_port(val: str) -> bool: + return ( + _validate_hostname(host) + or validate_ip( + host, + 4, + ) + ) and validate_port(port) + + +def validate_port( + val: str, +) -> bool: try: port = int(val) return port <= 65535 @@ -96,7 +108,13 @@ def validate_port(val: str) -> bool: return False -def validate_ip(val: typing.Union[str, bytes], version: typing.Optional[int] = None) -> bool: +def validate_ip( + val: typing.Union[ + str, + bytes, + ], + version: typing.Optional[int] = None, +) -> bool: try: if version is None: ip_address(val) @@ -112,42 +130,114 @@ def validate_ip(val: typing.Union[str, bytes], version: typing.Optional[int] = N return False -def is_ip(val: celtypes.Value, version: typing.Optional[celtypes.Value] = None) -> celpy.Result: - if not isinstance(val, (celtypes.BytesType, celtypes.StringType)): +def is_ip( + val: celtypes.Value, + version: typing.Optional[celtypes.Value] = None, +) -> celpy.Result: + if not isinstance( + val, + ( + celtypes.BytesType, + celtypes.StringType, + ), + ): msg = "invalid argument, expected string or bytes" raise celpy.CELEvalError(msg) - if not isinstance(version, celtypes.IntType) and version is not None: + if ( + not isinstance( + version, + celtypes.IntType, + ) + and version is not None + ): msg = "invalid argument, expected int" raise celpy.CELEvalError(msg) - return celtypes.BoolType(validate_ip(val, version)) - - -def is_ip_prefix(val: celtypes.Value, *args) -> celpy.Result: - if not isinstance(val, (celtypes.BytesType, celtypes.StringType)): + return celtypes.BoolType( + validate_ip( + val, + version, + ) + ) + + +def is_ip_prefix( + val: celtypes.Value, + *args, +) -> celpy.Result: + if not isinstance( + val, + ( + celtypes.BytesType, + celtypes.StringType, + ), + ): msg = "invalid argument, expected string or bytes" raise celpy.CELEvalError(msg) version = None strict = celtypes.BoolType(False) - if len(args) == 1 and isinstance(args[0], celtypes.BoolType): + if len(args) == 1 and isinstance( + args[0], + celtypes.BoolType, + ): strict = args[0] - elif len(args) == 1 and isinstance(args[0], celtypes.IntType): + elif len(args) == 1 and isinstance( + args[0], + celtypes.IntType, + ): version = args[0] - elif len(args) == 1 and (not isinstance(args[0], celtypes.BoolType) or not isinstance(args[0], celtypes.IntType)): + elif len(args) == 1 and ( + not isinstance( + args[0], + celtypes.BoolType, + ) + or not isinstance( + args[0], + celtypes.IntType, + ) + ): msg = "invalid argument, expected bool or int" raise celpy.CELEvalError(msg) - elif len(args) == 2 and isinstance(args[0], celtypes.IntType) and isinstance(args[1], celtypes.BoolType): + elif ( + len(args) == 2 + and isinstance( + args[0], + celtypes.IntType, + ) + and isinstance( + args[1], + celtypes.BoolType, + ) + ): version = args[0] strict = args[1] - elif len(args) == 2 and (not isinstance(args[0], celtypes.IntType) or not isinstance(args[1], celtypes.BoolType)): + elif len(args) == 2 and ( + not isinstance( + args[0], + celtypes.IntType, + ) + or not isinstance( + args[1], + celtypes.BoolType, + ) + ): msg = "invalid argument, expected int and bool" raise celpy.CELEvalError(msg) try: if version is None: - ip_network(val, strict=bool(strict)) + ip_network( + val, + strict=bool(strict), + ) elif version == 4: - IPv4Network(val, strict=bool(strict)) + IPv4Network( + val, + strict=bool(strict), + ) elif version == 6: - IPv6Network(val, strict=bool(strict)) + IPv6Network( + val, + strict=bool(strict), + ) else: msg = "invalid argument, expected 4 or 6" raise celpy.CELEvalError(msg) @@ -156,21 +246,35 @@ def is_ip_prefix(val: celtypes.Value, *args) -> celpy.Result: return celtypes.BoolType(False) -def is_email(string: celtypes.Value) -> celpy.Result: - if not isinstance(string, celtypes.StringType): +def is_email( + string: celtypes.Value, +) -> celpy.Result: + if not isinstance( + string, + celtypes.StringType, + ): msg = "invalid argument, expected string" raise celpy.CELEvalError(msg) - return celtypes.BoolType(validate_email(string)) + m = _email_regex.match(string) is not None + return celtypes.BoolType(m) -def is_uri(string: celtypes.Value) -> celpy.Result: +def is_uri( + string: celtypes.Value, +) -> celpy.Result: url = urlparse.urlparse(str(string)) # urlparse correctly reads the scheme from URNs but parses everything # after (except the query string) as the path. if url.scheme == "urn": if not (url.path): return celtypes.BoolType(False) - elif not all([url.scheme, url.netloc, url.path]): + elif not all( + [ + url.scheme, + url.netloc, + url.path, + ] + ): return celtypes.BoolType(False) # If the query string contains percent-encoding, then try to decode it. @@ -181,45 +285,88 @@ def is_uri(string: celtypes.Value) -> celpy.Result: return celtypes.BoolType(True) -def is_uri_ref(string: celtypes.Value) -> celpy.Result: +def is_uri_ref( + string: celtypes.Value, +) -> celpy.Result: url = urlparse.urlparse(str(string)) - if not all([url.scheme, url.path]) and url.fragment: + if ( + not all( + [ + url.scheme, + url.path, + ] + ) + and url.fragment + ): return celtypes.BoolType(False) return celtypes.BoolType(True) -def is_hostname(string: celtypes.Value) -> celpy.Result: - if not isinstance(string, celtypes.StringType): +def is_hostname( + string: celtypes.Value, +) -> celpy.Result: + if not isinstance( + string, + celtypes.StringType, + ): msg = "invalid argument, expected string" raise celpy.CELEvalError(msg) return celtypes.BoolType(_validate_hostname(string)) -def is_host_and_port(string: celtypes.Value, port_required: celtypes.Value) -> celpy.Result: - if not isinstance(string, celtypes.StringType): +def is_host_and_port( + string: celtypes.Value, + port_required: celtypes.Value, +) -> celpy.Result: + if not isinstance( + string, + celtypes.StringType, + ): msg = "invalid argument, expected string" raise celpy.CELEvalError(msg) - if not isinstance(port_required, celtypes.BoolType): + if not isinstance( + port_required, + celtypes.BoolType, + ): msg = "invalid argument, expected bool" raise celpy.CELEvalError(msg) - return celtypes.BoolType(validate_host_and_port(string, port_required=bool(port_required))) - - -def is_nan(val: celtypes.Value) -> celpy.Result: - if not isinstance(val, celtypes.DoubleType): + return celtypes.BoolType( + validate_host_and_port( + string, + port_required=bool(port_required), + ) + ) + + +def is_nan( + val: celtypes.Value, +) -> celpy.Result: + if not isinstance( + val, + celtypes.DoubleType, + ): msg = "invalid argument, expected double" raise celpy.CELEvalError(msg) return celtypes.BoolType(math.isnan(val)) -def is_inf(val: celtypes.Value, sign: typing.Optional[celtypes.Value] = None) -> celpy.Result: - if not isinstance(val, celtypes.DoubleType): +def is_inf( + val: celtypes.Value, + sign: typing.Optional[celtypes.Value] = None, +) -> celpy.Result: + if not isinstance( + val, + celtypes.DoubleType, + ): msg = "invalid argument, expected double" raise celpy.CELEvalError(msg) if sign is None: return celtypes.BoolType(math.isinf(val)) - if not isinstance(sign, celtypes.IntType): + if not isinstance( + sign, + celtypes.IntType, + ): msg = "invalid argument, expected int" raise celpy.CELEvalError(msg) if sign > 0: @@ -230,14 +377,24 @@ def is_inf(val: celtypes.Value, sign: typing.Optional[celtypes.Value] = None) -> return celtypes.BoolType(math.isinf(val)) -def unique(val: celtypes.Value) -> celpy.Result: - if not isinstance(val, celtypes.ListType): +def unique( + val: celtypes.Value, +) -> celpy.Result: + if not isinstance( + val, + celtypes.ListType, + ): msg = "invalid argument, expected list" raise celpy.CELEvalError(msg) return celtypes.BoolType(len(val) == len(set(val))) -def make_extra_funcs(locale: str) -> dict[str, celpy.CELFunction]: +def make_extra_funcs( + locale: str, +) -> dict[ + str, + celpy.CELFunction, +]: # TODO(#257): Fix types and add tests for StringFormat. # For now, ignoring the type. string_fmt = string_format.StringFormat(locale) # type: ignore diff --git a/tests/conformance/nonconforming.yaml b/tests/conformance/nonconforming.yaml index 531cf553..9fc8ea2f 100644 --- a/tests/conformance/nonconforming.yaml +++ b/tests/conformance/nonconforming.yaml @@ -8,46 +8,6 @@ standard_constraints/well_known_types/timestamp: - gte_lte/invalid/above - lte/invalid -library/is_email: - - invalid/left_side_empty - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - invalid/non_ascii - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"ยต@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - invalid/quoted-string/a - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"\"foo bar\"@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - invalid/quoted-string/b - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"\"foo..bar\"@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - invalid/trailing_dot - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"foo@example.com."} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - valid/exhaust_atext - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&'*+-/=?^_`{|}~@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # message: "" - # for_key: false - - valid/label_all_digits - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"foo@0.1.2.3.4.5.6.7.8.9"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # message: "" - # for_key: false library/is_host_and_port: - port_required/false/invalid/port_number_sign # input: [type.googleapis.com/buf.validate.conformance.cases.IsHostAndPort]:{val:"example.com:+0"} From 0fadb128780df5ed989f735fdee6d4ca0b6a0931 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Sun, 23 Mar 2025 12:01:30 -0400 Subject: [PATCH 2/5] Format --- protovalidate/internal/extra_func.py | 262 +++++---------------------- 1 file changed, 47 insertions(+), 215 deletions(-) diff --git a/protovalidate/internal/extra_func.py b/protovalidate/internal/extra_func.py index 69a5ab3d..d14a7da9 100644 --- a/protovalidate/internal/extra_func.py +++ b/protovalidate/internal/extra_func.py @@ -29,12 +29,9 @@ ) -def _validate_hostname( - host, -): +def _validate_hostname(host): if not host: return False - if len(host) > 253: return False @@ -57,11 +54,7 @@ def _validate_hostname( return not all_digits -def validate_host_and_port( - string: str, - *, - port_required: bool, -) -> bool: +def validate_host_and_port(string: str, *, port_required: bool) -> bool: if not string: return False @@ -70,37 +63,20 @@ def validate_host_and_port( end = string.find("]") after_end = end + 1 if after_end == len(string): # no port - return not port_required and validate_ip( - string[1:end], - 6, - ) + return not port_required and validate_ip(string[1:end], 6) if after_end == split_idx: # port return validate_ip(string[1:end]) and validate_port(string[split_idx + 1 :]) return False # malformed if split_idx == -1: - return not port_required and ( - _validate_hostname(string) - or validate_ip( - string, - 4, - ) - ) + return not port_required and (_validate_hostname(string) or validate_ip(string, 4)) host = string[:split_idx] port = string[split_idx + 1 :] - return ( - _validate_hostname(host) - or validate_ip( - host, - 4, - ) - ) and validate_port(port) - - -def validate_port( - val: str, -) -> bool: + return (_validate_hostname(host) or validate_ip(host, 4)) and validate_port(port) + + +def validate_port(val: str) -> bool: try: port = int(val) return port <= 65535 @@ -108,13 +84,7 @@ def validate_port( return False -def validate_ip( - val: typing.Union[ - str, - bytes, - ], - version: typing.Optional[int] = None, -) -> bool: +def validate_ip(val: typing.Union[str, bytes], version: typing.Optional[int] = None) -> bool: try: if version is None: ip_address(val) @@ -130,114 +100,42 @@ def validate_ip( return False -def is_ip( - val: celtypes.Value, - version: typing.Optional[celtypes.Value] = None, -) -> celpy.Result: - if not isinstance( - val, - ( - celtypes.BytesType, - celtypes.StringType, - ), - ): +def is_ip(val: celtypes.Value, version: typing.Optional[celtypes.Value] = None) -> celpy.Result: + if not isinstance(val, (celtypes.BytesType, celtypes.StringType)): msg = "invalid argument, expected string or bytes" raise celpy.CELEvalError(msg) - if ( - not isinstance( - version, - celtypes.IntType, - ) - and version is not None - ): + if not isinstance(version, celtypes.IntType) and version is not None: msg = "invalid argument, expected int" raise celpy.CELEvalError(msg) - return celtypes.BoolType( - validate_ip( - val, - version, - ) - ) - - -def is_ip_prefix( - val: celtypes.Value, - *args, -) -> celpy.Result: - if not isinstance( - val, - ( - celtypes.BytesType, - celtypes.StringType, - ), - ): + return celtypes.BoolType(validate_ip(val, version)) + + +def is_ip_prefix(val: celtypes.Value, *args) -> celpy.Result: + if not isinstance(val, (celtypes.BytesType, celtypes.StringType)): msg = "invalid argument, expected string or bytes" raise celpy.CELEvalError(msg) version = None strict = celtypes.BoolType(False) - if len(args) == 1 and isinstance( - args[0], - celtypes.BoolType, - ): + if len(args) == 1 and isinstance(args[0], celtypes.BoolType): strict = args[0] - elif len(args) == 1 and isinstance( - args[0], - celtypes.IntType, - ): + elif len(args) == 1 and isinstance(args[0], celtypes.IntType): version = args[0] - elif len(args) == 1 and ( - not isinstance( - args[0], - celtypes.BoolType, - ) - or not isinstance( - args[0], - celtypes.IntType, - ) - ): + elif len(args) == 1 and (not isinstance(args[0], celtypes.BoolType) or not isinstance(args[0], celtypes.IntType)): msg = "invalid argument, expected bool or int" raise celpy.CELEvalError(msg) - elif ( - len(args) == 2 - and isinstance( - args[0], - celtypes.IntType, - ) - and isinstance( - args[1], - celtypes.BoolType, - ) - ): + elif len(args) == 2 and isinstance(args[0], celtypes.IntType) and isinstance(args[1], celtypes.BoolType): version = args[0] strict = args[1] - elif len(args) == 2 and ( - not isinstance( - args[0], - celtypes.IntType, - ) - or not isinstance( - args[1], - celtypes.BoolType, - ) - ): + elif len(args) == 2 and (not isinstance(args[0], celtypes.IntType) or not isinstance(args[1], celtypes.BoolType)): msg = "invalid argument, expected int and bool" raise celpy.CELEvalError(msg) try: if version is None: - ip_network( - val, - strict=bool(strict), - ) + ip_network(val, strict=bool(strict)) elif version == 4: - IPv4Network( - val, - strict=bool(strict), - ) + IPv4Network(val, strict=bool(strict)) elif version == 6: - IPv6Network( - val, - strict=bool(strict), - ) + IPv6Network(val, strict=bool(strict)) else: msg = "invalid argument, expected 4 or 6" raise celpy.CELEvalError(msg) @@ -246,35 +144,22 @@ def is_ip_prefix( return celtypes.BoolType(False) -def is_email( - string: celtypes.Value, -) -> celpy.Result: - if not isinstance( - string, - celtypes.StringType, - ): +def is_email(string: celtypes.Value) -> celpy.Result: + if not isinstance(string, celtypes.StringType): msg = "invalid argument, expected string" raise celpy.CELEvalError(msg) m = _email_regex.match(string) is not None return celtypes.BoolType(m) -def is_uri( - string: celtypes.Value, -) -> celpy.Result: +def is_uri(string: celtypes.Value) -> celpy.Result: url = urlparse.urlparse(str(string)) # urlparse correctly reads the scheme from URNs but parses everything # after (except the query string) as the path. if url.scheme == "urn": if not (url.path): return celtypes.BoolType(False) - elif not all( - [ - url.scheme, - url.netloc, - url.path, - ] - ): + elif not all([url.scheme, url.netloc, url.path]): return celtypes.BoolType(False) # If the query string contains percent-encoding, then try to decode it. @@ -285,88 +170,45 @@ def is_uri( return celtypes.BoolType(True) -def is_uri_ref( - string: celtypes.Value, -) -> celpy.Result: +def is_uri_ref(string: celtypes.Value) -> celpy.Result: url = urlparse.urlparse(str(string)) - if ( - not all( - [ - url.scheme, - url.path, - ] - ) - and url.fragment - ): + if not all([url.scheme, url.path]) and url.fragment: return celtypes.BoolType(False) return celtypes.BoolType(True) -def is_hostname( - string: celtypes.Value, -) -> celpy.Result: - if not isinstance( - string, - celtypes.StringType, - ): +def is_hostname(string: celtypes.Value) -> celpy.Result: + if not isinstance(string, celtypes.StringType): msg = "invalid argument, expected string" raise celpy.CELEvalError(msg) return celtypes.BoolType(_validate_hostname(string)) -def is_host_and_port( - string: celtypes.Value, - port_required: celtypes.Value, -) -> celpy.Result: - if not isinstance( - string, - celtypes.StringType, - ): +def is_host_and_port(string: celtypes.Value, port_required: celtypes.Value) -> celpy.Result: + if not isinstance(string, celtypes.StringType): msg = "invalid argument, expected string" raise celpy.CELEvalError(msg) - if not isinstance( - port_required, - celtypes.BoolType, - ): + if not isinstance(port_required, celtypes.BoolType): msg = "invalid argument, expected bool" raise celpy.CELEvalError(msg) - return celtypes.BoolType( - validate_host_and_port( - string, - port_required=bool(port_required), - ) - ) - - -def is_nan( - val: celtypes.Value, -) -> celpy.Result: - if not isinstance( - val, - celtypes.DoubleType, - ): + return celtypes.BoolType(validate_host_and_port(string, port_required=bool(port_required))) + + +def is_nan(val: celtypes.Value) -> celpy.Result: + if not isinstance(val, celtypes.DoubleType): msg = "invalid argument, expected double" raise celpy.CELEvalError(msg) return celtypes.BoolType(math.isnan(val)) -def is_inf( - val: celtypes.Value, - sign: typing.Optional[celtypes.Value] = None, -) -> celpy.Result: - if not isinstance( - val, - celtypes.DoubleType, - ): +def is_inf(val: celtypes.Value, sign: typing.Optional[celtypes.Value] = None) -> celpy.Result: + if not isinstance(val, celtypes.DoubleType): msg = "invalid argument, expected double" raise celpy.CELEvalError(msg) if sign is None: return celtypes.BoolType(math.isinf(val)) - if not isinstance( - sign, - celtypes.IntType, - ): + if not isinstance(sign, celtypes.IntType): msg = "invalid argument, expected int" raise celpy.CELEvalError(msg) if sign > 0: @@ -377,24 +219,14 @@ def is_inf( return celtypes.BoolType(math.isinf(val)) -def unique( - val: celtypes.Value, -) -> celpy.Result: - if not isinstance( - val, - celtypes.ListType, - ): +def unique(val: celtypes.Value) -> celpy.Result: + if not isinstance(val, celtypes.ListType): msg = "invalid argument, expected list" raise celpy.CELEvalError(msg) return celtypes.BoolType(len(val) == len(set(val))) -def make_extra_funcs( - locale: str, -) -> dict[ - str, - celpy.CELFunction, -]: +def make_extra_funcs(locale: str) -> dict[str, celpy.CELFunction]: # TODO(#257): Fix types and add tests for StringFormat. # For now, ignoring the type. string_fmt = string_format.StringFormat(locale) # type: ignore From 574bcfc7b3178bdd8d714ecbd76f7bd195686459 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Sun, 23 Mar 2025 17:41:35 -0400 Subject: [PATCH 3/5] Revert --- Pipfile | 2 +- Pipfile.lock | 38 +++++++++++++++++++------------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Pipfile b/Pipfile index 574a0825..0163de0e 100644 --- a/Pipfile +++ b/Pipfile @@ -10,10 +10,10 @@ protobuf = "==6.*" [dev-packages] pytest = "*" mypy = "*" +ruff = "*" types-protobuf = "*" exceptiongroup = "*" tomli = "*" -ruff = "*" [requires] python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index 56244f72..06352bf6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -240,28 +240,28 @@ }, "ruff": { "hashes": [ - "sha256:0397c2672db015be5aa3d4dac54c69aa012429097ff219392c018e21f5085147", - "sha256:0c543bf65d5d27240321604cee0633a70c6c25c9a2f2492efa9f6d4b8e4199bb", - "sha256:20967168cc21195db5830b9224be0e964cc9c8ecf3b5a9e3ce19876e8d3a96e3", - "sha256:2a2b50ca35457ba785cd8c93ebbe529467594087b527a08d487cf0ee7b3087e9", - "sha256:2c5424cc1c4eb1d8ecabe6d4f1b70470b4f24a0c0171356290b1953ad8f0e272", - "sha256:3170150172a8f994136c0c66f494edf199a0bbea7a409f649e4bc8f4d7084080", - "sha256:52933095158ff328f4c77af3d74f0379e34fd52f175144cefc1b192e7ccd32b4", - "sha256:6e8fb75e14560f7cf53b15bbc55baf5ecbe373dd5f3aab96ff7aa7777edd7630", - "sha256:7c69c74bf53ddcfbc22e6eb2f31211df7f65054bfc1f72288fc71e5f82db3eab", - "sha256:842a472d7b4d6f5924e9297aa38149e5dcb1e628773b70e6387ae2c97a63c58f", - "sha256:869bcf3f9abf6457fbe39b5a37333aa4eecc52a3b99c98827ccc371a8e5b6f1b", - "sha256:86b3a27c38b8fce73bcd262b0de32e9a6801b76d52cdb3ae4c914515f0cef608", - "sha256:955a9ce63483999d9f0b8f0b4a3ad669e53484232853054cc8b9d51ab4c5de74", - "sha256:a3b66a03b248c9fcd9d64d445bafdf1589326bee6fc5c8e92d7562e58883e30f", - "sha256:aca01ccd0eb5eb7156b324cfaa088586f06a86d9e5314b0eb330cb48415097cc", - "sha256:c69e20ea49e973f3afec2c06376eb56045709f0212615c1adb0eda35e8a4e477", - "sha256:ec47591497d5a1050175bdf4e1a4e6272cddff7da88a2ad595e1e326041d8d94", - "sha256:ecf20854cc73f42171eedb66f006a43d0a21bfb98a2523a809931cda569552d9" + "sha256:049a191969a10897fe052ef9cc7491b3ef6de79acd7790af7d7897b7a9bfbcb6", + "sha256:1bc09a7419e09662983b1312f6fa5dab829d6ab5d11f18c3760be7ca521c9329", + "sha256:3191e9116b6b5bbe187447656f0c8526f0d36b6fd89ad78ccaad6bdc2fad7df2", + "sha256:38c23fd9bdec4eb437b4c1e3595905a0a8edfccd63a790f818b28c78fe345639", + "sha256:3c3156d3f4b42e57247275a0a7e15a851c165a4fc89c5e8fa30ea6da4f7407b8", + "sha256:490b1e147c1260545f6d041c4092483e3f6d8eba81dc2875eaebcf9140b53905", + "sha256:6fbb2aed66fe742a6a3a0075ed467a459b7cedc5ae01008340075909d819df1e", + "sha256:7c8661b0be91a38bd56db593e9331beaf9064a79028adee2d5f392674bbc5e88", + "sha256:868364fc23f5aa122b00c6f794211e85f7e78f5dffdf7c590ab90b8c4e69b657", + "sha256:92c0c1ff014351c0b0cdfdb1e35fa83b780f1e065667167bb9502d47ca41e6db", + "sha256:96bc89a5c5fd21a04939773f9e0e276308be0935de06845110f43fd5c2e4ead7", + "sha256:a9352b9d767889ec5df1483f94870564e8102d4d7e99da52ebf564b882cdc2c7", + "sha256:b6c0e8d3d2db7e9f6efd884f44b8dc542d5b6b590fc4bb334fdbc624d93a29a2", + "sha256:bcfa478daf61ac8002214eb2ca5f3e9365048506a9d52b11bea3ecea822bb844", + "sha256:c58bfa00e740ca0a6c43d41fb004cd22d165302f360aaa56f7126d544db31a21", + "sha256:dc67e32bc3b29557513eb7eeabb23efdb25753684b913bebb8a0c62495095acb", + "sha256:e4fd5ff5de5f83e0458a138e8a869c7c5e907541aec32b707f57cf9a5e124445", + "sha256:e55c620690a4a7ee6f1cccb256ec2157dc597d109400ae75bbf944fc9d6462e2" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==0.11.2" + "version": "==0.11.0" }, "tomli": { "hashes": [ From 3aadd1404e59d814e2fd36dff83e1e54a9883aec Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Mon, 24 Mar 2025 10:21:16 -0400 Subject: [PATCH 4/5] Switch to raw string --- protovalidate/internal/extra_func.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protovalidate/internal/extra_func.py b/protovalidate/internal/extra_func.py index d14a7da9..33854241 100644 --- a/protovalidate/internal/extra_func.py +++ b/protovalidate/internal/extra_func.py @@ -25,7 +25,7 @@ # See https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address _email_regex = re.compile( - "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" + r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" ) From a14d3104d4206fcfa464ee1b368b28a94fce700b Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Mon, 24 Mar 2025 10:44:35 -0400 Subject: [PATCH 5/5] Docs --- protovalidate/internal/extra_func.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/protovalidate/internal/extra_func.py b/protovalidate/internal/extra_func.py index 33854241..ee9ed9aa 100644 --- a/protovalidate/internal/extra_func.py +++ b/protovalidate/internal/extra_func.py @@ -145,6 +145,14 @@ def is_ip_prefix(val: celtypes.Value, *args) -> celpy.Result: def is_email(string: celtypes.Value) -> celpy.Result: + """Returns true if the string is an email address, for example "foo@example.com". + + Conforms to the definition for a valid email address from the HTML standard. + Note that this standard willfully deviates from RFC 5322, which allows many + unexpected forms of email addresses and will easily match a typographical + error. + """ + if not isinstance(string, celtypes.StringType): msg = "invalid argument, expected string" raise celpy.CELEvalError(msg)