diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 046d2cc..07fcd7c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/conformance.yaml b/.github/workflows/conformance.yaml index 277915d..752e3fc 100644 --- a/.github/workflows/conformance.yaml +++ b/.github/workflows/conformance.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/Pipfile b/Pipfile index ae7cf1b..fa4894f 100644 --- a/Pipfile +++ b/Pipfile @@ -17,4 +17,4 @@ exceptiongroup = "*" tomli = "*" [requires] -python_version = "3.10" +python_version = "3.8" diff --git a/Pipfile.lock b/Pipfile.lock index 9d1d622..50799e5 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "4d528deff7028686ac5fac50920324fefd22fcf168e33a2494110a75fe03296d" + "sha256": "689a69b69eb47c8c0cef034d3ec46bd52c1c933a8b6a9c23b0ebba3bcb4ddf53" }, "pipfile-spec": 6, "requires": { - "python_version": "3.10" + "python_version": "3.8" }, "sources": [ { @@ -186,6 +186,14 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==2.8.2" }, + "pytz": { + "hashes": [ + "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b", + "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7" + ], + "markers": "python_version < '3.9'", + "version": "==2023.3.post1" + }, "pyyaml": { "hashes": [ "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", diff --git a/protovalidate/internal/constraints.py b/protovalidate/internal/constraints.py index 864d498..283c3a8 100644 --- a/protovalidate/internal/constraints.py +++ b/protovalidate/internal/constraints.py @@ -62,7 +62,7 @@ def unwrap(msg: message.Message) -> celtypes.Value: } -def _msg_to_cel(msg: message.Message) -> dict[str, celtypes.Value]: +def _msg_to_cel(msg: message.Message) -> typing.Dict[str, celtypes.Value]: ctor = _MSG_TYPE_URL_TO_CTOR.get(msg.DESCRIPTOR.full_name) if ctor is not None: return ctor(msg) @@ -214,16 +214,21 @@ def validate(self, ctx: ConstraintContext, message: message.Message): # noqa: A class CelConstraintRules(ConstraintRules): """A constraint that has rules written in CEL.""" - _runners: list[tuple[celpy.Runner, expression_pb2.Constraint | private_pb2.Constraint]] + _runners: typing.List[typing.Tuple[celpy.Runner, typing.Union[expression_pb2.Constraint, private_pb2.Constraint]]] _rules_cel: celtypes.Value = None - def __init__(self, rules: message.Message | None): + def __init__(self, rules: typing.Optional[message.Message]): self._runners = [] if rules is not None: self._rules_cel = _msg_to_cel(rules) def _validate_cel( - self, ctx: ConstraintContext, field_name: str, activation: dict[str, typing.Any], *, for_key: bool = False + self, + ctx: ConstraintContext, + field_name: str, + activation: typing.Dict[str, typing.Any], + *, + for_key: bool = False, ): activation["rules"] = self._rules_cel activation["now"] = celtypes.TimestampType(datetime.datetime.now(tz=datetime.timezone.utc)) @@ -241,8 +246,8 @@ def _validate_cel( def add_rule( self, env: celpy.Environment, - funcs: dict[str, celpy.CELFunction], - rules: expression_pb2.Constraint | private_pb2.Constraint, + funcs: typing.Dict[str, celpy.CELFunction], + rules: typing.Union[expression_pb2.Constraint, private_pb2.Constraint], ): ast = env.compile(rules.expression) prog = env.program(ast, functions=funcs) @@ -256,7 +261,7 @@ def validate(self, ctx: ConstraintContext, message: message.Message): self._validate_cel(ctx, "", {"this": _msg_to_cel(message)}) -def check_field_type(field: descriptor.FieldDescriptor, expected: int, wrapper_name: str | None = None): +def check_field_type(field: descriptor.FieldDescriptor, expected: int, wrapper_name: typing.Optional[str] = None): if field.type != expected and ( field.type != descriptor.FieldDescriptor.TYPE_MESSAGE or field.message_type.full_name != wrapper_name ): @@ -273,7 +278,7 @@ class FieldConstraintRules(CelConstraintRules): def __init__( self, env: celpy.Environment, - funcs: dict[str, celpy.CELFunction], + funcs: typing.Dict[str, celpy.CELFunction], field: descriptor.FieldDescriptor, field_level: validate_pb2.FieldConstraints, ): @@ -320,13 +325,13 @@ def _validate_value(self, ctx: ConstraintContext, field_path: str, val: typing.A class AnyConstraintRules(FieldConstraintRules): """Rules for an Any field.""" - _in: list[str] = [] # noqa: RUF012 - _not_in: list[str] = [] # noqa: RUF012 + _in: typing.List[str] = [] # noqa: RUF012 + _not_in: typing.List[str] = [] # noqa: RUF012 def __init__( self, env: celpy.Environment, - funcs: dict[str, celpy.CELFunction], + funcs: typing.Dict[str, celpy.CELFunction], field: descriptor.FieldDescriptor, field_level: validate_pb2.FieldConstraints, ): @@ -362,7 +367,7 @@ class EnumConstraintRules(FieldConstraintRules): def __init__( self, env: celpy.Environment, - funcs: dict[str, celpy.CELFunction], + funcs: typing.Dict[str, celpy.CELFunction], field: descriptor.FieldDescriptor, field_level: validate_pb2.FieldConstraints, ): @@ -387,15 +392,15 @@ def validate(self, ctx: ConstraintContext, message: message.Message): class RepeatedConstraintRules(FieldConstraintRules): """Rules for a repeated field.""" - _item_rules: FieldConstraintRules | None = None + _item_rules: typing.Optional[FieldConstraintRules] = None def __init__( self, env: celpy.Environment, - funcs: dict[str, celpy.CELFunction], + funcs: typing.Dict[str, celpy.CELFunction], field: descriptor.FieldDescriptor, field_level: validate_pb2.FieldConstraints, - item_rules: FieldConstraintRules | None, + item_rules: typing.Optional[FieldConstraintRules], ): super().__init__(env, funcs, field, field_level) if item_rules is not None: @@ -422,17 +427,17 @@ def validate(self, ctx: ConstraintContext, message: message.Message): class MapConstraintRules(FieldConstraintRules): """Rules for a map field.""" - _key_rules: FieldConstraintRules | None = None - _value_rules: FieldConstraintRules | None = None + _key_rules: typing.Optional[FieldConstraintRules] = None + _value_rules: typing.Optional[FieldConstraintRules] = None def __init__( self, env: celpy.Environment, - funcs: dict[str, celpy.CELFunction], + funcs: typing.Dict[str, celpy.CELFunction], field: descriptor.FieldDescriptor, field_level: validate_pb2.FieldConstraints, - key_rules: FieldConstraintRules | None, - value_rules: FieldConstraintRules | None, + key_rules: typing.Optional[FieldConstraintRules], + value_rules: typing.Optional[FieldConstraintRules], ): super().__init__(env, funcs, field, field_level) if key_rules is not None: @@ -480,15 +485,15 @@ class ConstraintFactory: """Factory for creating and caching constraints.""" _env: celpy.Environment - _funcs: dict[str, celpy.CELFunction] - _cache: dict[descriptor.Descriptor, list[ConstraintRules] | Exception] + _funcs: typing.Dict[str, celpy.CELFunction] + _cache: typing.Dict[descriptor.Descriptor, typing.Union[typing.List[ConstraintRules], Exception]] - def __init__(self, funcs: dict[str, celpy.CELFunction]): + def __init__(self, funcs: typing.Dict[str, celpy.CELFunction]): self._env = celpy.Environment() self._funcs = funcs self._cache = {} - def get(self, descriptor: descriptor.Descriptor) -> list[ConstraintRules]: + def get(self, descriptor: descriptor.Descriptor) -> typing.List[ConstraintRules]: if descriptor not in self._cache: try: self._cache[descriptor] = self._new_constraints(descriptor) @@ -647,9 +652,9 @@ def _new_field_constraint( item_rule = self._new_scalar_field_constraint(field, rules.repeated.items) return RepeatedConstraintRules(self._env, self._funcs, field, rules, item_rule) - def _new_constraints(self, desc: descriptor.Descriptor) -> list[ConstraintRules]: - result: list[ConstraintRules] = [] - constraint: ConstraintRules | None = None + def _new_constraints(self, desc: descriptor.Descriptor) -> typing.List[ConstraintRules]: + result: typing.List[ConstraintRules] = [] + constraint: typing.Optional[ConstraintRules] = None if validate_pb2.message in desc.GetOptions().Extensions: message_level = desc.GetOptions().Extensions[validate_pb2.message] if message_level.disabled: diff --git a/protovalidate/internal/extra_func.py b/protovalidate/internal/extra_func.py index b9fb6ad..88f0de0 100644 --- a/protovalidate/internal/extra_func.py +++ b/protovalidate/internal/extra_func.py @@ -13,6 +13,7 @@ # limitations under the License. import math +import typing from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_address, ip_network from urllib import parse as urlparse @@ -59,8 +60,8 @@ def validate_email(addr): return _validate_hostname(parts[1]) -def is_ip(val: celtypes.Value, version: celtypes.Value | None = 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.EvalError(msg) try: @@ -79,7 +80,7 @@ def is_ip(val: celtypes.Value, version: celtypes.Value | None = None) -> celpy.R def is_ip_prefix(val: celtypes.Value, *args) -> celpy.Result: - if not isinstance(val, celtypes.BytesType | celtypes.StringType): + if not isinstance(val, (celtypes.BytesType, celtypes.StringType)): msg = "invalid argument, expected string or bytes" raise celpy.EvalError(msg) version = None @@ -147,7 +148,7 @@ def is_nan(val: celtypes.Value) -> celpy.Result: return celtypes.BoolType(math.isnan(val)) -def is_inf(val: celtypes.Value, sign: None | celtypes.Value = None) -> celpy.Result: +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.EvalError(msg) @@ -172,7 +173,7 @@ def unique(val: celtypes.Value) -> celpy.Result: return celtypes.BoolType(len(val) == len(set(val))) -def make_extra_funcs(locale: str) -> dict[str, celpy.CELFunction]: +def make_extra_funcs(locale: str) -> typing.Dict[str, celpy.CELFunction]: string_fmt = string_format.StringFormat(locale) return { # Missing standard functions diff --git a/protovalidate/internal/string_format.py b/protovalidate/internal/string_format.py index 29b92b3..bed78f5 100644 --- a/protovalidate/internal/string_format.py +++ b/protovalidate/internal/string_format.py @@ -152,7 +152,7 @@ def format_string(self, arg: celtypes.Value) -> celpy.Result: return celtypes.StringType(arg) def format_value(self, arg: celtypes.Value) -> celpy.Result: - if isinstance(arg, celtypes.StringType | str): + if isinstance(arg, (celtypes.StringType, str)): return celtypes.StringType(quote(arg)) if isinstance(arg, celtypes.UintType): return celtypes.StringType(arg) diff --git a/protovalidate/validator.py b/protovalidate/validator.py index 90c122b..3b6f20d 100644 --- a/protovalidate/validator.py +++ b/protovalidate/validator.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import typing + from google.protobuf import message from buf.validate import expression_pb2 # type: ignore @@ -98,7 +100,7 @@ def __init__(self, msg: str, violations: expression_pb2.Violations): super().__init__(msg) self.violations = violations - def errors(self) -> list[expression_pb2.Violation]: + def errors(self) -> typing.List[expression_pb2.Violation]: """ Returns the validation errors as a simple Python list, rather than the Protobuf-specific collection type used by Violations. diff --git a/pyproject.toml b/pyproject.toml index f979220..673ab59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ description = "Protocol Buffer Validation for Python" readme = "README.md" license = { file = "LICENSE" } keywords = ["validate", "protobuf", "protocol buffer"] -requires-python = ">=3.10" +requires-python = ">=3.8" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: Apache Software License", @@ -26,11 +26,11 @@ Issues = "https://github.com/bufbuild/protovalidate-python/issues" source = "vcs" [tool.black] -target-version = ["py310"] +target-version = ["py38"] line-length = 120 [tool.ruff] -target-version = "py310" +target-version = "py38" line-length = 120 select = [ "A", diff --git a/tests/conformance/runner.py b/tests/conformance/runner.py index e704159..f5f41d2 100644 --- a/tests/conformance/runner.py +++ b/tests/conformance/runner.py @@ -49,7 +49,7 @@ from buf.validate.conformance.harness import harness_pb2 -def run_test_case(tc: typing.Any, result: harness_pb2.TestResult | None = None) -> harness_pb2.TestResult: +def run_test_case(tc: typing.Any, result: typing.Optional[harness_pb2.TestResult] = None) -> harness_pb2.TestResult: if result is None: result = harness_pb2.TestResult() # Run the validator @@ -67,7 +67,7 @@ def run_test_case(tc: typing.Any, result: harness_pb2.TestResult | None = None) def run_any_test_case( pool: descriptor_pool.DescriptorPool, tc: any_pb2.Any, - result: harness_pb2.TestResult | None = None, + result: typing.Optional[harness_pb2.TestResult] = None, ) -> harness_pb2.TestResult: type_name = tc.type_url.split("/")[-1] desc: descriptor.Descriptor = pool.FindMessageTypeByName(type_name)