Skip to content

Commit 97672b1

Browse files
Drop Python 3.9 support (#384)
Closes #380. To be merged at the end of the month.
1 parent 704eb0e commit 97672b1

File tree

10 files changed

+40
-161
lines changed

10 files changed

+40
-161
lines changed

.github/workflows/ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
strategy:
1818
fail-fast: false
1919
matrix:
20-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
20+
python-version: ["3.10", "3.11", "3.12", "3.13"]
2121
resolution: ["highest", "lowest-direct"]
2222
env:
2323
# Shared env variables for all the tests

.python-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
3.9
1+
3.10

protovalidate/internal/extra_func.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
# limitations under the License.
1414

1515
import math
16-
import typing
1716
from urllib import parse as urlparse
1817

1918
import celpy
@@ -42,7 +41,7 @@ def cel_get_field(message: celtypes.Value, field_name: celtypes.Value) -> celpy.
4241
return field_to_cel(message.msg, message.desc.fields_by_name[field_name])
4342

4443

45-
def cel_is_ip(val: celtypes.Value, ver: typing.Optional[celtypes.Value] = None) -> celpy.Result:
44+
def cel_is_ip(val: celtypes.Value, ver: celtypes.Value | None = None) -> celpy.Result:
4645
"""Return True if the string is an IPv4 or IPv6 address, optionally limited to a specific version.
4746
4847
Version 0 or None means either 4 or 6. Passing a version other than 0, 4, or 6 always returns False.
@@ -307,7 +306,7 @@ def cel_is_nan(val: celtypes.Value) -> celpy.Result:
307306
return celtypes.BoolType(math.isnan(val))
308307

309308

310-
def cel_is_inf(val: celtypes.Value, sign: typing.Optional[celtypes.Value] = None) -> celpy.Result:
309+
def cel_is_inf(val: celtypes.Value, sign: celtypes.Value | None = None) -> celpy.Result:
311310
if not isinstance(val, celtypes.DoubleType):
312311
msg = "invalid argument, expected double"
313312
raise celpy.CELEvalError(msg)
@@ -326,7 +325,7 @@ def cel_is_inf(val: celtypes.Value, sign: typing.Optional[celtypes.Value] = None
326325

327326

328327
def cel_unique(val: celtypes.Value) -> celpy.Result:
329-
if not isinstance(val, (celtypes.ListType, list)):
328+
if not isinstance(val, celtypes.ListType | list):
330329
msg = "invalid argument, expected list"
331330
raise celpy.CELEvalError(msg)
332331
return celtypes.BoolType(len(val) == len(set(val)))
@@ -512,7 +511,7 @@ class Ipv6:
512511
_double_colon_at: int # Number of 16-bit pieces found when double colon was found.
513512
_double_colon_seen: bool
514513
_dotted_raw: str # Dotted notation for right-most 32 bits.
515-
_dotted_addr: typing.Optional[Ipv4] # Dotted notation successfully parsed as Ipv4.
514+
_dotted_addr: Ipv4 | None # Dotted notation successfully parsed as Ipv4.
516515
_zone_id_found: bool
517516
_prefix_len: int # 0 -128
518517

protovalidate/internal/rules.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ def _get_type_name(fd: typing.Any) -> str:
125125
return md["name"]
126126

127127

128-
def _get_type_ctor(fd: typing.Any) -> typing.Optional[typing.Callable[..., celtypes.Value]]:
128+
def _get_type_ctor(fd: typing.Any) -> typing.Callable[..., celtypes.Value] | None:
129129
md = _FIELD_DESC_METADATA_MAP.get(fd)
130130
if md is None:
131131
return None
@@ -312,19 +312,19 @@ def validate(self, ctx: RuleContext, _: message.Message):
312312
class CelRunner:
313313
runner: celpy.Runner
314314
rule: validate_pb2.Rule
315-
rule_value: typing.Optional[typing.Any] = None
316-
rule_cel: typing.Optional[celtypes.Value] = None
317-
rule_path: typing.Optional[validate_pb2.FieldPath] = None
315+
rule_value: typing.Any | None = None
316+
rule_cel: celtypes.Value | None = None
317+
rule_path: validate_pb2.FieldPath | None = None
318318

319319

320320
class CelRules(Rules):
321321
"""A rule that has rules written in CEL."""
322322

323323
_cel: list[CelRunner]
324-
_rules: typing.Optional[message.Message] = None
325-
_rules_cel: typing.Optional[celtypes.Value] = None
324+
_rules: message.Message | None = None
325+
_rules_cel: celtypes.Value | None = None
326326

327-
def __init__(self, rules: typing.Optional[message.Message]):
327+
def __init__(self, rules: message.Message | None):
328328
self._cel = []
329329
if rules is not None:
330330
self._rules = rules
@@ -334,8 +334,8 @@ def _validate_cel(
334334
self,
335335
ctx: RuleContext,
336336
*,
337-
this_value: typing.Optional[typing.Any] = None,
338-
this_cel: typing.Optional[celtypes.Value] = None,
337+
this_value: typing.Any | None = None,
338+
this_cel: celtypes.Value | None = None,
339339
for_key: bool = False,
340340
):
341341
activation: dict[str, celtypes.Value] = {}
@@ -379,8 +379,8 @@ def add_rule(
379379
funcs: dict[str, celpy.CELFunction],
380380
rules: validate_pb2.Rule,
381381
*,
382-
rule_field: typing.Optional[descriptor.FieldDescriptor] = None,
383-
rule_path: typing.Optional[validate_pb2.FieldPath] = None,
382+
rule_field: descriptor.FieldDescriptor | None = None,
383+
rule_path: validate_pb2.FieldPath | None = None,
384384
):
385385
ast = env.compile(rules.expression)
386386
prog = env.program(ast, functions=funcs)
@@ -430,7 +430,7 @@ class MessageRules(CelRules):
430430

431431
_oneofs: list[MessageOneofRule]
432432

433-
def __init__(self, rules: typing.Optional[message.Message], desc: descriptor.Descriptor):
433+
def __init__(self, rules: message.Message | None, desc: descriptor.Descriptor):
434434
super().__init__(rules)
435435
self._oneofs = []
436436
self._desc = desc
@@ -467,7 +467,7 @@ def add_oneof(
467467
self._oneofs.append(MessageOneofRule(fields, required=rule.required))
468468

469469

470-
def check_field_type(field: descriptor.FieldDescriptor, expected: int, wrapper_name: typing.Optional[str] = None):
470+
def check_field_type(field: descriptor.FieldDescriptor, expected: int, wrapper_name: str | None = None):
471471
if field.type != expected and (
472472
field.type != descriptor.FieldDescriptor.TYPE_MESSAGE or field.message_type.full_name != wrapper_name
473473
):
@@ -713,7 +713,7 @@ def validate(self, ctx: RuleContext, message: message.Message):
713713
class RepeatedRules(FieldRules):
714714
"""Rules for a repeated field."""
715715

716-
_item_rules: typing.Optional[FieldRules] = None
716+
_item_rules: FieldRules | None = None
717717

718718
_items_rules_suffix: typing.ClassVar[list[validate_pb2.FieldPathElement]] = [
719719
_field_to_element(
@@ -730,7 +730,7 @@ def __init__(
730730
funcs: dict[str, celpy.CELFunction],
731731
field: descriptor.FieldDescriptor,
732732
field_level: validate_pb2.FieldRules,
733-
item_rules: typing.Optional[FieldRules],
733+
item_rules: FieldRules | None,
734734
):
735735
super().__init__(env, funcs, field, field_level)
736736
if item_rules is not None:
@@ -760,8 +760,8 @@ def validate(self, ctx: RuleContext, message: message.Message):
760760
class MapRules(FieldRules):
761761
"""Rules for a map field."""
762762

763-
_key_rules: typing.Optional[FieldRules] = None
764-
_value_rules: typing.Optional[FieldRules] = None
763+
_key_rules: FieldRules | None = None
764+
_value_rules: FieldRules | None = None
765765

766766
_key_rules_suffix: typing.ClassVar[list[validate_pb2.FieldPathElement]] = [
767767
_field_to_element(validate_pb2.MapRules.DESCRIPTOR.fields_by_number[validate_pb2.MapRules.KEYS_FIELD_NUMBER]),
@@ -783,8 +783,8 @@ def __init__(
783783
funcs: dict[str, celpy.CELFunction],
784784
field: descriptor.FieldDescriptor,
785785
field_level: validate_pb2.FieldRules,
786-
key_rules: typing.Optional[FieldRules],
787-
value_rules: typing.Optional[FieldRules],
786+
key_rules: FieldRules | None,
787+
value_rules: FieldRules | None,
788788
):
789789
super().__init__(env, funcs, field, field_level)
790790
if key_rules is not None:
@@ -850,7 +850,7 @@ class RuleFactory:
850850

851851
_env: celpy.Environment
852852
_funcs: dict[str, celpy.CELFunction]
853-
_cache: dict[descriptor.Descriptor, typing.Union[list[Rules], Exception]]
853+
_cache: dict[descriptor.Descriptor, list[Rules] | Exception]
854854

855855
def __init__(self, funcs: dict[str, celpy.CELFunction]):
856856
self._env = celpy.Environment(runner_class=InterpretedRunner)
@@ -1022,7 +1022,7 @@ def _new_field_rule(
10221022

10231023
def _new_rules(self, desc: descriptor.Descriptor) -> list[Rules]:
10241024
result: list[Rules] = []
1025-
rule: typing.Optional[Rules] = None
1025+
rule: Rules | None = None
10261026
all_msg_oneof_fields = set()
10271027
if desc.GetOptions().HasExtension(validate_pb2.message): # type: ignore
10281028
message_level = desc.GetOptions().Extensions[validate_pb2.message] # type: ignore

protovalidate/internal/string_format.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import math
1616
import re
1717
from decimal import Decimal
18-
from typing import Optional, Union
1918

2019
import celpy
2120
from celpy import celtypes
@@ -88,7 +87,7 @@ def format(self, fmt: celtypes.Value, args: celtypes.Value) -> celpy.Result:
8887

8988
return celtypes.StringType(result)
9089

91-
def __validate_number(self, arg: Union[celtypes.DoubleType, celtypes.IntType, celtypes.UintType]) -> Optional[str]:
90+
def __validate_number(self, arg: celtypes.DoubleType | celtypes.IntType | celtypes.UintType) -> str | None:
9291
if math.isnan(arg):
9392
return "NaN"
9493
if math.isinf(arg):
@@ -122,7 +121,7 @@ def __format_exponential(self, arg: celtypes.Value, precision: int) -> str:
122121
raise celpy.CELEvalError(msg)
123122

124123
def __format_int(self, arg: celtypes.Value) -> str:
125-
if isinstance(arg, (celtypes.IntType, celtypes.UintType, celtypes.DoubleType)):
124+
if isinstance(arg, celtypes.IntType | celtypes.UintType | celtypes.DoubleType):
126125
result = self.__validate_number(arg)
127126
if result is not None:
128127
return result
@@ -134,7 +133,7 @@ def __format_int(self, arg: celtypes.Value) -> str:
134133
raise celpy.CELEvalError(msg)
135134

136135
def __format_hex(self, arg: celtypes.Value) -> str:
137-
if isinstance(arg, (celtypes.IntType, celtypes.UintType)):
136+
if isinstance(arg, celtypes.IntType | celtypes.UintType):
138137
return f"{arg:x}"
139138
if isinstance(arg, celtypes.BytesType):
140139
return arg.hex()
@@ -147,7 +146,7 @@ def __format_hex(self, arg: celtypes.Value) -> str:
147146
raise celpy.CELEvalError(msg)
148147

149148
def __format_oct(self, arg: celtypes.Value) -> str:
150-
if isinstance(arg, (celtypes.IntType, celtypes.UintType)):
149+
if isinstance(arg, celtypes.IntType | celtypes.UintType):
151150
return f"{arg:o}"
152151
msg = (
153152
"error during formatting: octal clause can only be used on integers, was given "
@@ -156,7 +155,7 @@ def __format_oct(self, arg: celtypes.Value) -> str:
156155
raise celpy.CELEvalError(msg)
157156

158157
def __format_bin(self, arg: celtypes.Value) -> str:
159-
if isinstance(arg, (celtypes.IntType, celtypes.UintType, celtypes.BoolType)):
158+
if isinstance(arg, celtypes.IntType | celtypes.UintType | celtypes.BoolType):
160159
return f"{arg:b}"
161160
msg = (
162161
"error during formatting: only integers and bools can be formatted as binary, was given "

pyproject.toml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,8 @@ license = "Apache-2.0"
1010
license-files = ["LICENSE"]
1111
keywords = ["validate", "protobuf", "protocol buffer"]
1212
# Keep our minimum version synced with ./.python-version.
13-
requires-python = ">=3.9"
13+
requires-python = ">=3.10"
1414
classifiers = [
15-
"Programming Language :: Python :: 3.9",
1615
"Programming Language :: Python :: 3.10",
1716
"Programming Language :: Python :: 3.11",
1817
"Programming Language :: Python :: 3.12",

test/conformance/runner.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
from buf.validate.conformance.harness import harness_pb2
5555

5656

57-
def run_test_case(tc: typing.Any, result: typing.Optional[harness_pb2.TestResult] = None) -> harness_pb2.TestResult:
57+
def run_test_case(tc: typing.Any, result: harness_pb2.TestResult | None = None) -> harness_pb2.TestResult:
5858
if result is None:
5959
result = harness_pb2.TestResult()
6060
# Run the validator
@@ -76,7 +76,7 @@ def run_test_case(tc: typing.Any, result: typing.Optional[harness_pb2.TestResult
7676
def run_any_test_case(
7777
pool: descriptor_pool.DescriptorPool,
7878
tc: any_pb2.Any,
79-
result: typing.Optional[harness_pb2.TestResult] = None,
79+
result: harness_pb2.TestResult | None = None,
8080
) -> harness_pb2.TestResult:
8181
type_name = tc.type_url.split("/")[-1]
8282
desc: descriptor.Descriptor = pool.FindMessageTypeByName(type_name)

test/test_format.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from collections.abc import Iterable, MutableMapping
1616
from itertools import chain
17-
from typing import Any, Optional
17+
from typing import Any
1818

1919
import celpy
2020
import pytest
@@ -62,15 +62,15 @@ def build_variables(bindings: MutableMapping[str, eval_pb2.ExprValue]) -> dict[A
6262
return binder
6363

6464

65-
def get_expected_result(test: simple_pb2.SimpleTest) -> Optional[str]:
65+
def get_expected_result(test: simple_pb2.SimpleTest) -> str | None:
6666
if test.HasField("value"):
6767
val = test.value
6868
if val.HasField("string_value"):
6969
return val.string_value
7070
return None
7171

7272

73-
def get_eval_error_message(test: simple_pb2.SimpleTest) -> Optional[str]:
73+
def get_eval_error_message(test: simple_pb2.SimpleTest) -> str | None:
7474
if test.HasField("eval_error"):
7575
err_set = test.eval_error
7676
if len(err_set.errors) == 1:

test/test_validate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ def check_compilation_errors(validator: protovalidate.Validator, msg: message.Me
238238
def _compare_violations(actual: list[rules.Violation], expected: list[rules.Violation]):
239239
"""Compares two lists of violations. The violations are expected to be in the expected order also."""
240240
assert len(actual) == len(expected)
241-
for a, e in zip(actual, expected):
241+
for a, e in zip(actual, expected, strict=True):
242242
assert a.proto.message == e.proto.message
243243
assert a.proto.rule_id == e.proto.rule_id
244244
assert a.proto.for_key == e.proto.for_key

0 commit comments

Comments
 (0)