From 9085810188c38f9a9ced91e82c978905cf7c5822 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 20:34:10 -0700 Subject: [PATCH 01/20] Clarify Makefile variable --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 75677e6..4738511 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ LICENSE_IGNORE := LICENSE_HEADER_VERSION := 59c69fa4ddbd56c887cb178a03257cd3908ce518 # Set to use a different compiler. For example, `GO=go1.18rc1 make test`. GO ?= go -ARGS ?= --strict --expected_failures=nonconforming.yaml +CONFORMANCE_ARGS ?= --strict --expected_failures=nonconforming.yaml .PHONY: help help: ## Describe useful make targets @@ -56,7 +56,7 @@ test: generate install ## Run all unit tests .PHONY: conformance conformance: $(BIN)/protovalidate-conformance install - $(BIN)/protovalidate-conformance $(ARGS) pipenv -- run python3 -m tests.conformance.runner + $(BIN)/protovalidate-conformance $(CONFORMANCE_ARGS) pipenv -- run python3 -m tests.conformance.runner .PHONY: install install: From 8aad3f47d33463c43e3a2f65213f7bf9a33c8051 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 20:41:10 -0700 Subject: [PATCH 02/20] Allow Makefile users to swap Python interpreter --- Makefile | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 4738511..625ee9d 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,8 @@ LICENSE_IGNORE := LICENSE_HEADER_VERSION := 59c69fa4ddbd56c887cb178a03257cd3908ce518 # Set to use a different compiler. For example, `GO=go1.18rc1 make test`. GO ?= go +# Set to use a different Python interpreter. For example, `PYTHON=python make test`. +PYTHON ?= python3 CONFORMANCE_ARGS ?= --strict --expected_failures=nonconforming.yaml .PHONY: help @@ -47,8 +49,8 @@ format: generate-license .PHONY: format-python format-python: install ## Format all code according to isort and black - python3 -m isort protovalidate tests - python3 -m black protovalidate tests + $(PYTHON) -m isort protovalidate tests + $(PYTHON) -m black protovalidate tests .PHONY: test test: generate install ## Run all unit tests @@ -56,13 +58,13 @@ test: generate install ## Run all unit tests .PHONY: conformance conformance: $(BIN)/protovalidate-conformance install - $(BIN)/protovalidate-conformance $(CONFORMANCE_ARGS) pipenv -- run python3 -m tests.conformance.runner + $(BIN)/protovalidate-conformance $(CONFORMANCE_ARGS) pipenv -- run $(PYTHON) -m tests.conformance.runner .PHONY: install install: - python3 -m pip install --upgrade pip + $(PYTHON) -m pip install --upgrade pip pip install pipenv ruff mypy types-protobuf black isort - pipenv --python python3 install + pipenv --python $(PYTHON) install .PHONY: checkgenerate checkgenerate: generate From a8de0897cf7ce67dd774134cd8d959740210ce53 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 20:46:49 -0700 Subject: [PATCH 03/20] Explicitly order code generation Recipe dependencies are run in dependency order, but otherwise don't have any guaranteed ordering. To make sure that `make -j 4 generate` retains license headers, we need to explicitly invoke `buf generate`, then add license headers. (Recursive make is generally evil, but this simple use case is innocuous enough.) --- Makefile | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index 625ee9d..c3b1b0e 100644 --- a/Makefile +++ b/Makefile @@ -29,28 +29,17 @@ clean: ## Delete intermediate build artifacts git clean -Xdf .PHONY: generate -generate: generate-proto generate-license ## Regenerate code and license headers - -.PHONY: generate-license -generate-license: $(BIN)/license-header format-python ## Format code and regenerate license headers - $(BIN)/license-header \ - --license-type apache \ - --copyright-holder "Buf Technologies, Inc." \ - --year-range "$(COPYRIGHT_YEARS)" $(LICENSE_IGNORE) - -.PHONY: generate-proto -generate-proto: $(BIN)/buf ## Regenerate code from proto files +generate: $(BIN)/buf ## Regenerate code and license headers rm -rf gen $(BIN)/buf generate buf.build/bufbuild/protovalidate $(BIN)/buf generate buf.build/bufbuild/protovalidate-testing + $(MAKE) generate-license -.PHONY: format ## Format all code -format: generate-license - -.PHONY: format-python -format-python: install ## Format all code according to isort and black +.PHONY: format +format: install ## Format code $(PYTHON) -m isort protovalidate tests $(PYTHON) -m black protovalidate tests + $(MAKE) generate-license .PHONY: test test: generate install ## Run all unit tests @@ -71,6 +60,13 @@ checkgenerate: generate @# Used in CI to verify that `make generate` doesn't produce a diff. test -z "$$(git status --porcelain | tee /dev/stderr)" +.PHONY: generate-license +generate-license: $(BIN)/license-header + $(BIN)/license-header \ + --license-type apache \ + --copyright-holder "Buf Technologies, Inc." \ + --year-range "$(COPYRIGHT_YEARS)" $(LICENSE_IGNORE) + $(BIN): @mkdir -p $(BIN) From bce0ff84b9ab367abbeb561a31ef71d336bedd2a Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 20:58:21 -0700 Subject: [PATCH 04/20] Add comments for make help Add the comments so that user-facing Make targets are included in `make help`. --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index c3b1b0e..75cf0bb 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ help: ## Describe useful make targets @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-15s %s\n", $$1, $$2}' .PHONY: all -all: test conformance +all: test conformance ## Run unit and conformance tests (default) .PHONY: clean clean: ## Delete intermediate build artifacts @@ -46,11 +46,11 @@ test: generate install ## Run all unit tests pipenv run pytest .PHONY: conformance -conformance: $(BIN)/protovalidate-conformance install +conformance: $(BIN)/protovalidate-conformance install ## Run conformance tests $(BIN)/protovalidate-conformance $(CONFORMANCE_ARGS) pipenv -- run $(PYTHON) -m tests.conformance.runner .PHONY: install -install: +install: ## Install dependencies $(PYTHON) -m pip install --upgrade pip pip install pipenv ruff mypy types-protobuf black isort pipenv --python $(PYTHON) install From e3f3b7a72577e91b0935ed28b72b1ddf05c0f254 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 21:05:04 -0700 Subject: [PATCH 05/20] Small pyproject.toml cleanups --- pyproject.toml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 93d1727..a8dd875 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,9 +4,10 @@ build-backend = "hatchling.build" [project] name = "protovalidate" -description = "Protocol Buffer Validation for python" +description = "Protocol Buffer Validation for Python" readme = "README.md" license = { file = "LICENSE" } +keywords = ["validate", "protobuf", "protocol buffer"] requires-python = ">=3.7" classifiers = [ "Programming Language :: Python :: 3", @@ -15,9 +16,12 @@ classifiers = [ ] dynamic = ["version"] dependencies = ["protobuf", "cel-python"] + +[project.urls] +Homepage = "https://github.com/bufbuild/protovalidate-python" +Source = "https://github.com/bufbuild/protovalidate-python" +Issues = "https://github.com/bufbuild/protovalidate-python/issues" + [tool.hatch.version] source = "vcs" -[project.urls] -"Homepage" = "https://github.com/bufbuild/protovalidate-python" -"Bug Tracker" = "https://github.com/bufbuild/protovalidate-python/issues" From 3a43be4ca43ac944e3c0cf841d8dd7157aab1cac Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 21:09:33 -0700 Subject: [PATCH 06/20] Configure 120-character line length Especially with type hints, modern Python most commonly uses 120-char lines. This is also the default when using Hatch to create new projects. --- protovalidate/internal/constraints.py | 95 +++++++------------------ protovalidate/internal/extra_func.py | 7 +- protovalidate/internal/string_format.py | 20 ++---- pyproject.toml | 7 ++ tests/conformance/runner.py | 4 +- tests/validate_test.py | 8 +-- 6 files changed, 39 insertions(+), 102 deletions(-) diff --git a/protovalidate/internal/constraints.py b/protovalidate/internal/constraints.py index f25f18f..11ea926 100644 --- a/protovalidate/internal/constraints.py +++ b/protovalidate/internal/constraints.py @@ -97,18 +97,14 @@ def _MsgToCel(msg: message.Message) -> dict[str, celtypes.Value]: } -def _ScalarFieldValueToCel( - val: typing.Any, field: descriptor.FieldDescriptor -) -> celtypes.Value: +def _ScalarFieldValueToCel(val: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: ctor = _TYPE_TO_CTOR.get(field.type) if ctor is None: raise CompilationError("unknown field type") return ctor(val) -def _FieldValueToCel( - val: typing.Any, field: descriptor.FieldDescriptor -) -> celtypes.Value: +def _FieldValueToCel(val: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: if field.message_type is not None and field.message_type.GetOptions().map_entry: return _MapFieldValueToCel(val, field) @@ -158,26 +154,20 @@ def _IsEmptyField(msg: message.Message, field: descriptor.FieldDescriptor) -> bo raise ValueError("unknown field type") -def _RepeatedFieldToCel( - msg: message.Message, field: descriptor.FieldDescriptor -) -> celtypes.Value: +def _RepeatedFieldToCel(msg: message.Message, field: descriptor.FieldDescriptor) -> celtypes.Value: if field.message_type is not None and field.message_type.GetOptions().map_entry: return _MapFieldToCel(msg, field) return _RepeatedFieldValueToCel(getattr(msg, field.name), field) -def _RepeatedFieldValueToCel( - val: typing.Any, field: descriptor.FieldDescriptor -) -> celtypes.Value: +def _RepeatedFieldValueToCel(val: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: result = celtypes.ListType() for item in val: result.append(_ScalarFieldValueToCel(item, field)) return result -def _MapFieldValueToCel( - map: typing.Any, field: descriptor.FieldDescriptor -) -> celtypes.Value: +def _MapFieldValueToCel(map: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: result = celtypes.MapType() key_field = field.message_type.fields[0] val_field = field.message_type.fields[1] @@ -186,15 +176,11 @@ def _MapFieldValueToCel( return result -def _MapFieldToCel( - msg: message.Message, field: descriptor.FieldDescriptor -) -> celtypes.Value: +def _MapFieldToCel(msg: message.Message, field: descriptor.FieldDescriptor) -> celtypes.Value: return _MapFieldValueToCel(getattr(msg, field.name), field) -def _FieldToCel( - msg: message.Message, field: descriptor.FieldDescriptor -) -> celtypes.Value: +def _FieldToCel(msg: message.Message, field: descriptor.FieldDescriptor) -> celtypes.Value: if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: return _RepeatedFieldToCel(msg, field) elif field.message_type is not None and not msg.HasField(field.name): @@ -206,9 +192,7 @@ def _FieldToCel( class ConstraintContext: """The state associated with a single constraint evaluation.""" - def __init__( - self, fail_fast: bool = False, violations: expression_pb2.Violations = None - ): + def __init__(self, fail_fast: bool = False, violations: expression_pb2.Violations = None): self._fail_fast = fail_fast if violations is None: violations = expression_pb2.Violations() @@ -263,9 +247,7 @@ def validate(self, ctx: ConstraintContext, message: message.Message): class CelConstraintRules(ConstraintRules): """A constraint that has rules written in CEL.""" - _runners: list[ - typing.Tuple[celpy.Runner, expression_pb2.Constraint | private_pb2.Constraint] - ] + _runners: list[typing.Tuple[celpy.Runner, expression_pb2.Constraint | private_pb2.Constraint]] _rules_cel: celtypes.Value = None def __init__(self, rules: message.Message | None): @@ -273,13 +255,9 @@ def __init__(self, rules: message.Message | None): if rules is not None: self._rules_cel = _MsgToCel(rules) - def _validate_cel( - self, ctx: ConstraintContext, field_name: str, activation: dict[str, typing.Any] - ): + def _validate_cel(self, ctx: ConstraintContext, field_name: str, activation: dict[str, typing.Any]): activation["rules"] = self._rules_cel - activation["now"] = celtypes.TimestampType( - datetime.datetime.now(tz=datetime.timezone.utc) - ) + activation["now"] = celtypes.TimestampType(datetime.datetime.now(tz=datetime.timezone.utc)) for runner, constraint in self._runners: result = runner.evaluate(activation) if isinstance(result, celtypes.BoolType): @@ -309,16 +287,11 @@ def validate(self, ctx: ConstraintContext, message: message.Message): self._validate_cel(ctx, "", {"this": _MsgToCel(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: str | None = None): if field.type != expected and ( - field.type != descriptor.FieldDescriptor.TYPE_MESSAGE - or field.message_type.full_name != wrapper_name + field.type != descriptor.FieldDescriptor.TYPE_MESSAGE or field.message_type.full_name != wrapper_name ): - raise CompilationError( - f"field {field.name} has type {field.type} but expected {expected}" - ) + raise CompilationError(f"field {field.name} has type {field.type} but expected {expected}") class FieldConstraintRules(CelConstraintRules): @@ -373,15 +346,11 @@ def validate(self, ctx: ConstraintContext, message: message.Message): return val = getattr(message, self._field.name) self._validate_value(ctx, self._field.name, val) - self._validate_cel( - ctx, self._field.name, {"this": _FieldValueToCel(val, self._field)} - ) + self._validate_cel(ctx, self._field.name, {"this": _FieldValueToCel(val, self._field)}) def validate_item(self, ctx: ConstraintContext, field_path: str, val: typing.Any): self._validate_value(ctx, field_path, val) - self._validate_cel( - ctx, field_path, {"this": _ScalarFieldValueToCel(val, self._field)} - ) + self._validate_cel(ctx, field_path, {"this": _ScalarFieldValueToCel(val, self._field)}) def _validate_value(self, ctx: ConstraintContext, field_path: str, val: typing.Any): pass @@ -406,9 +375,7 @@ def __init__( if fieldLvl.any.not_in: self._not_in = fieldLvl.any.not_in - def _validate_value( - self, ctx: ConstraintContext, field_path: str, value: any_pb2.Any - ): + def _validate_value(self, ctx: ConstraintContext, field_path: str, value: any_pb2.Any): if len(self._in) > 0: if value.type_url not in self._in: ctx.add( @@ -526,9 +493,7 @@ class OneofConstraintRules(ConstraintRules): required = True - def __init__( - self, oneof: descriptor.OneofDescriptor, rules: validate_pb2.OneofConstraints - ): + def __init__(self, oneof: descriptor.OneofDescriptor, rules: validate_pb2.OneofConstraints): self._oneof = oneof if not rules.required: self.required = False @@ -567,9 +532,7 @@ def get(self, descriptor: descriptor.Descriptor) -> list[ConstraintRules]: raise result return result - def _new_message_constraint( - self, rules: validate_pb2.message - ) -> MessageConstraintRules: + def _new_message_constraint(self, rules: validate_pb2.message) -> MessageConstraintRules: result = MessageConstraintRules(rules) for cel in rules.cel: result.add_rule(self._env, self._funcs, cel) @@ -599,9 +562,7 @@ def _new_scalar_field_constraint( result = EnumConstraintRules(self._env, self._funcs, field, fieldLvl) return result elif type_case == "bool": - check_field_type( - field, descriptor.FieldDescriptor.TYPE_BOOL, "google.protobuf.BoolValue" - ) + check_field_type(field, descriptor.FieldDescriptor.TYPE_BOOL, "google.protobuf.BoolValue") result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) return result elif type_case == "bytes": @@ -693,9 +654,7 @@ def _new_scalar_field_constraint( result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) return result elif type_case == "any": - check_field_type( - field, descriptor.FieldDescriptor.TYPE_MESSAGE, "google.protobuf.Any" - ) + check_field_type(field, descriptor.FieldDescriptor.TYPE_MESSAGE, "google.protobuf.Any") result = AnyConstraintRules(self._env, self._funcs, field, fieldLvl) return result @@ -714,12 +673,8 @@ def _new_field_constraint( value_rules = None if rules.map.HasField("values"): value_field = field.message_type.fields_by_name["value"] - value_rules = self._new_scalar_field_constraint( - value_field, rules.map.values - ) - return MapConstraintRules( - self._env, self._funcs, field, rules, key_rules, value_rules - ) + value_rules = self._new_scalar_field_constraint(value_field, rules.map.values) + return MapConstraintRules(self._env, self._funcs, field, rules, key_rules, value_rules) item_rule = None if rules.repeated.HasField("items"): item_rule = self._new_scalar_field_constraint(field, rules.repeated.items) @@ -737,9 +692,7 @@ def _new_constraints(self, desc: descriptor.Descriptor) -> list[ConstraintRules] for oneof in desc.oneofs: if validate_pb2.oneof in oneof.GetOptions().Extensions: - if constraint := OneofConstraintRules( - oneof, oneof.GetOptions().Extensions[validate_pb2.oneof] - ): + if constraint := OneofConstraintRules(oneof, oneof.GetOptions().Extensions[validate_pb2.oneof]): result.append(constraint) for field in desc.fields: diff --git a/protovalidate/internal/extra_func.py b/protovalidate/internal/extra_func.py index 2f16636..26224cd 100644 --- a/protovalidate/internal/extra_func.py +++ b/protovalidate/internal/extra_func.py @@ -38,12 +38,7 @@ def _validateHostName(host): if part[0] == "-" or part[-1] == "-": return False for r in part: - if ( - (r < "A" or r > "Z") - and (r < "a" or r > "z") - and (r < "0" or r > "9") - and r != "-" - ): + if (r < "A" or r > "Z") and (r < "a" or r > "z") and (r < "0" or r > "9") and r != "-": return False return True diff --git a/protovalidate/internal/string_format.py b/protovalidate/internal/string_format.py index 42c4fe9..0dcad72 100644 --- a/protovalidate/internal/string_format.py +++ b/protovalidate/internal/string_format.py @@ -42,13 +42,9 @@ def __init__(self, locale: str): def format(self, fmt: celtypes.Value, args: celtypes.Value) -> celpy.Result: if not isinstance(fmt, celtypes.StringType): - return celpy.native_to_cel( - celpy.new_error("format() requires a string as the first argument") - ) + return celpy.native_to_cel(celpy.new_error("format() requires a string as the first argument")) if not isinstance(args, celtypes.ListType): - return celpy.native_to_cel( - celpy.new_error("format() requires a list as the second argument") - ) + return celpy.native_to_cel(celpy.new_error("format() requires a list as the second argument")) # printf style formatting i = 0 j = 0 @@ -64,9 +60,7 @@ def format(self, fmt: celtypes.Value, args: celtypes.Value) -> celpy.Result: i += 2 continue if j >= len(args): - return celpy.CELEvalError( - "format() not enough arguments for format string" - ) + return celpy.CELEvalError("format() not enough arguments for format string") arg = args[j] j += 1 i += 1 @@ -98,9 +92,7 @@ def format(self, fmt: celtypes.Value, args: celtypes.Value) -> celpy.Result: elif fmt[i] == "b": result += self.format_bin(arg) else: - return celpy.CELEvalError( - "format() unknown format specifier: " + fmt[i] - ) + return celpy.CELEvalError("format() unknown format specifier: " + fmt[i]) i += 1 if j < len(args): return celpy.CELEvalError("format() too many arguments for format string") @@ -132,9 +124,7 @@ def format_hex(self, arg: celtypes.Value) -> celpy.Result: return celtypes.StringType(arg.hex()) if isinstance(arg, celtypes.StringType): return celtypes.StringType(arg.encode("utf-8").hex()) - return celpy.CELEvalError( - "format_hex() requires an integer, string, or binary argument" - ) + return celpy.CELEvalError("format_hex() requires an integer, string, or binary argument") def format_oct(self, arg: celtypes.Value) -> celpy.Result: if isinstance(arg, celtypes.IntType): diff --git a/pyproject.toml b/pyproject.toml index a8dd875..077d2ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,3 +25,10 @@ Issues = "https://github.com/bufbuild/protovalidate-python/issues" [tool.hatch.version] source = "vcs" +[tool.black] +target-version = ["py37"] +line-length = 120 + +[tool.ruff] +target-version = "py37" +line-length = 120 diff --git a/tests/conformance/runner.py b/tests/conformance/runner.py index 6ca48be..01f4e41 100644 --- a/tests/conformance/runner.py +++ b/tests/conformance/runner.py @@ -45,9 +45,7 @@ import protovalidate -def run_test_case( - tc: typing.Any, result: harness_pb2.TestResult | None = None -) -> harness_pb2.TestResult: +def run_test_case(tc: typing.Any, result: harness_pb2.TestResult | None = None) -> harness_pb2.TestResult: if result is None: result = harness_pb2.TestResult() # Run the validator diff --git a/tests/validate_test.py b/tests/validate_test.py index d3f7ce0..71d357a 100644 --- a/tests/validate_test.py +++ b/tests/validate_test.py @@ -14,13 +14,7 @@ import unittest -from buf.validate.conformance.cases import ( - maps_pb2, - numbers_pb2, - oneofs_pb2, - repeated_pb2, - wkt_timestamp_pb2, -) +from buf.validate.conformance.cases import maps_pb2, numbers_pb2, oneofs_pb2, repeated_pb2, wkt_timestamp_pb2 import protovalidate From 6b7d72fe1bb67bb34744c63ec8598c02b84e3bcc Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 21:52:36 -0700 Subject: [PATCH 07/20] Don't install lint tools system-wide Change the Makefile and Pipfile to install lint and formatting tools in the virtual environment, rather than dropping them into the host system. Along the way, tighten the lint settings, add a Make target for linting, and unify the target for unit and integration tests. --- Makefile | 23 +- Pipfile | 7 +- Pipfile.lock | 386 +++++++++++++++++++---------- protovalidate/internal/__init__.py | 1 - tests/__init__.py | 1 - tests/conformance/__init__.py | 1 - tests/conformance/runner.py | 34 +-- tests/runner_test.py | 7 +- 8 files changed, 295 insertions(+), 165 deletions(-) diff --git a/Makefile b/Makefile index 75cf0bb..9273944 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ help: ## Describe useful make targets @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-15s %s\n", $$1, $$2}' .PHONY: all -all: test conformance ## Run unit and conformance tests (default) +all: test lint ## Run all tests and lint (default) .PHONY: clean clean: ## Delete intermediate build artifacts @@ -37,23 +37,26 @@ generate: $(BIN)/buf ## Regenerate code and license headers .PHONY: format format: install ## Format code - $(PYTHON) -m isort protovalidate tests - $(PYTHON) -m black protovalidate tests $(MAKE) generate-license + pipenv run black protovalidate tests + pipenv run ruff --fix protovalidate tests .PHONY: test -test: generate install ## Run all unit tests +test: $(BIN)/protovalidate-conformance generate install ## Run unit and conformance tests pipenv run pytest - -.PHONY: conformance -conformance: $(BIN)/protovalidate-conformance install ## Run conformance tests $(BIN)/protovalidate-conformance $(CONFORMANCE_ARGS) pipenv -- run $(PYTHON) -m tests.conformance.runner +.PHONY: lint +lint: install ## Lint code + pipenv run black --check --diff protovalidate tests + pipenv run mypy protovalidate + pipenv run ruff protovalidate tests + pipenv verify + .PHONY: install install: ## Install dependencies - $(PYTHON) -m pip install --upgrade pip - pip install pipenv ruff mypy types-protobuf black isort - pipenv --python $(PYTHON) install + $(PYTHON) -m pip install --upgrade pip pipenv + pipenv --python $(PYTHON) sync .PHONY: checkgenerate checkgenerate: generate diff --git a/Pipfile b/Pipfile index 0f988cf..de36392 100644 --- a/Pipfile +++ b/Pipfile @@ -6,10 +6,13 @@ name = "pypi" [packages] cel-python = "*" protobuf = "*" -pytest = "*" -validate-email = "*" [dev-packages] +pytest = "*" +mypy = "*" +ruff = "*" +types-protobuf = "*" +black = "*" [requires] python_version = "3.11" diff --git a/Pipfile.lock b/Pipfile.lock index 8ce22e7..2d3a31a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "b3a40595ed674476cb0172e403a1095a20dc91700d287f71913ec878538f33f1" + "sha256": "88b036cd2aa1e619c58537ce9fb4e89bfadbb0d4c5b549e96094bc11e6e91a5a" }, "pipfile-spec": 6, "requires": { @@ -42,84 +42,84 @@ }, "charset-normalizer": { "hashes": [ - "sha256:04afa6387e2b282cf78ff3dbce20f0cc071c12dc8f685bd40960cc68644cfea6", - "sha256:04eefcee095f58eaabe6dc3cc2262f3bcd776d2c67005880894f447b3f2cb9c1", - "sha256:0be65ccf618c1e7ac9b849c315cc2e8a8751d9cfdaa43027d4f6624bd587ab7e", - "sha256:0c95f12b74681e9ae127728f7e5409cbbef9cd914d5896ef238cc779b8152373", - "sha256:0ca564606d2caafb0abe6d1b5311c2649e8071eb241b2d64e75a0d0065107e62", - "sha256:10c93628d7497c81686e8e5e557aafa78f230cd9e77dd0c40032ef90c18f2230", - "sha256:11d117e6c63e8f495412d37e7dc2e2fff09c34b2d09dbe2bee3c6229577818be", - "sha256:11d3bcb7be35e7b1bba2c23beedac81ee893ac9871d0ba79effc7fc01167db6c", - "sha256:12a2b561af122e3d94cdb97fe6fb2bb2b82cef0cdca131646fdb940a1eda04f0", - "sha256:12d1a39aa6b8c6f6248bb54550efcc1c38ce0d8096a146638fd4738e42284448", - "sha256:1435ae15108b1cb6fffbcea2af3d468683b7afed0169ad718451f8db5d1aff6f", - "sha256:1c60b9c202d00052183c9be85e5eaf18a4ada0a47d188a83c8f5c5b23252f649", - "sha256:1e8fcdd8f672a1c4fc8d0bd3a2b576b152d2a349782d1eb0f6b8e52e9954731d", - "sha256:20064ead0717cf9a73a6d1e779b23d149b53daf971169289ed2ed43a71e8d3b0", - "sha256:21fa558996782fc226b529fdd2ed7866c2c6ec91cee82735c98a197fae39f706", - "sha256:22908891a380d50738e1f978667536f6c6b526a2064156203d418f4856d6e86a", - "sha256:3160a0fd9754aab7d47f95a6b63ab355388d890163eb03b2d2b87ab0a30cfa59", - "sha256:322102cdf1ab682ecc7d9b1c5eed4ec59657a65e1c146a0da342b78f4112db23", - "sha256:34e0a2f9c370eb95597aae63bf85eb5e96826d81e3dcf88b8886012906f509b5", - "sha256:3573d376454d956553c356df45bb824262c397c6e26ce43e8203c4c540ee0acb", - "sha256:3747443b6a904001473370d7810aa19c3a180ccd52a7157aacc264a5ac79265e", - "sha256:38e812a197bf8e71a59fe55b757a84c1f946d0ac114acafaafaf21667a7e169e", - "sha256:3a06f32c9634a8705f4ca9946d667609f52cf130d5548881401f1eb2c39b1e2c", - "sha256:3a5fc78f9e3f501a1614a98f7c54d3969f3ad9bba8ba3d9b438c3bc5d047dd28", - "sha256:3d9098b479e78c85080c98e1e35ff40b4a31d8953102bb0fd7d1b6f8a2111a3d", - "sha256:3dc5b6a8ecfdc5748a7e429782598e4f17ef378e3e272eeb1340ea57c9109f41", - "sha256:4155b51ae05ed47199dc5b2a4e62abccb274cee6b01da5b895099b61b1982974", - "sha256:49919f8400b5e49e961f320c735388ee686a62327e773fa5b3ce6721f7e785ce", - "sha256:53d0a3fa5f8af98a1e261de6a3943ca631c526635eb5817a87a59d9a57ebf48f", - "sha256:5f008525e02908b20e04707a4f704cd286d94718f48bb33edddc7d7b584dddc1", - "sha256:628c985afb2c7d27a4800bfb609e03985aaecb42f955049957814e0491d4006d", - "sha256:65ed923f84a6844de5fd29726b888e58c62820e0769b76565480e1fdc3d062f8", - "sha256:6734e606355834f13445b6adc38b53c0fd45f1a56a9ba06c2058f86893ae8017", - "sha256:6baf0baf0d5d265fa7944feb9f7451cc316bfe30e8df1a61b1bb08577c554f31", - "sha256:6f4f4668e1831850ebcc2fd0b1cd11721947b6dc7c00bf1c6bd3c929ae14f2c7", - "sha256:6f5c2e7bc8a4bf7c426599765b1bd33217ec84023033672c1e9a8b35eaeaaaf8", - "sha256:6f6c7a8a57e9405cad7485f4c9d3172ae486cfef1344b5ddd8e5239582d7355e", - "sha256:7381c66e0561c5757ffe616af869b916c8b4e42b367ab29fedc98481d1e74e14", - "sha256:73dc03a6a7e30b7edc5b01b601e53e7fc924b04e1835e8e407c12c037e81adbd", - "sha256:74db0052d985cf37fa111828d0dd230776ac99c740e1a758ad99094be4f1803d", - "sha256:75f2568b4189dda1c567339b48cba4ac7384accb9c2a7ed655cd86b04055c795", - "sha256:78cacd03e79d009d95635e7d6ff12c21eb89b894c354bd2b2ed0b4763373693b", - "sha256:80d1543d58bd3d6c271b66abf454d437a438dff01c3e62fdbcd68f2a11310d4b", - "sha256:830d2948a5ec37c386d3170c483063798d7879037492540f10a475e3fd6f244b", - "sha256:891cf9b48776b5c61c700b55a598621fdb7b1e301a550365571e9624f270c203", - "sha256:8f25e17ab3039b05f762b0a55ae0b3632b2e073d9c8fc88e89aca31a6198e88f", - "sha256:9a3267620866c9d17b959a84dd0bd2d45719b817245e49371ead79ed4f710d19", - "sha256:a04f86f41a8916fe45ac5024ec477f41f886b3c435da2d4e3d2709b22ab02af1", - "sha256:aaf53a6cebad0eae578f062c7d462155eada9c172bd8c4d250b8c1d8eb7f916a", - "sha256:abc1185d79f47c0a7aaf7e2412a0eb2c03b724581139193d2d82b3ad8cbb00ac", - "sha256:ac0aa6cd53ab9a31d397f8303f92c42f534693528fafbdb997c82bae6e477ad9", - "sha256:ac3775e3311661d4adace3697a52ac0bab17edd166087d493b52d4f4f553f9f0", - "sha256:b06f0d3bf045158d2fb8837c5785fe9ff9b8c93358be64461a1089f5da983137", - "sha256:b116502087ce8a6b7a5f1814568ccbd0e9f6cfd99948aa59b0e241dc57cf739f", - "sha256:b82fab78e0b1329e183a65260581de4375f619167478dddab510c6c6fb04d9b6", - "sha256:bd7163182133c0c7701b25e604cf1611c0d87712e56e88e7ee5d72deab3e76b5", - "sha256:c36bcbc0d5174a80d6cccf43a0ecaca44e81d25be4b7f90f0ed7bcfbb5a00909", - "sha256:c3af8e0f07399d3176b179f2e2634c3ce9c1301379a6b8c9c9aeecd481da494f", - "sha256:c84132a54c750fda57729d1e2599bb598f5fa0344085dbde5003ba429a4798c0", - "sha256:cb7b2ab0188829593b9de646545175547a70d9a6e2b63bf2cd87a0a391599324", - "sha256:cca4def576f47a09a943666b8f829606bcb17e2bc2d5911a46c8f8da45f56755", - "sha256:cf6511efa4801b9b38dc5546d7547d5b5c6ef4b081c60b23e4d941d0eba9cbeb", - "sha256:d16fd5252f883eb074ca55cb622bc0bee49b979ae4e8639fff6ca3ff44f9f854", - "sha256:d2686f91611f9e17f4548dbf050e75b079bbc2a82be565832bc8ea9047b61c8c", - "sha256:d7fc3fca01da18fbabe4625d64bb612b533533ed10045a2ac3dd194bfa656b60", - "sha256:dd5653e67b149503c68c4018bf07e42eeed6b4e956b24c00ccdf93ac79cdff84", - "sha256:de5695a6f1d8340b12a5d6d4484290ee74d61e467c39ff03b39e30df62cf83a0", - "sha256:e0ac8959c929593fee38da1c2b64ee9778733cdf03c482c9ff1d508b6b593b2b", - "sha256:e1b25e3ad6c909f398df8921780d6a3d120d8c09466720226fc621605b6f92b1", - "sha256:e633940f28c1e913615fd624fcdd72fdba807bf53ea6925d6a588e84e1151531", - "sha256:e89df2958e5159b811af9ff0f92614dabf4ff617c03a4c1c6ff53bf1c399e0e1", - "sha256:ea9f9c6034ea2d93d9147818f17c2a0860d41b71c38b9ce4d55f21b6f9165a11", - "sha256:f645caaf0008bacf349875a974220f1f1da349c5dbe7c4ec93048cdc785a3326", - "sha256:f8303414c7b03f794347ad062c0516cee0e15f7a612abd0ce1e25caf6ceb47df", - "sha256:fca62a8301b605b954ad2e9c3666f9d97f63872aa4efcae5492baca2056b74ab" + "sha256:04e57ab9fbf9607b77f7d057974694b4f6b142da9ed4a199859d9d4d5c63fe96", + "sha256:09393e1b2a9461950b1c9a45d5fd251dc7c6f228acab64da1c9c0165d9c7765c", + "sha256:0b87549028f680ca955556e3bd57013ab47474c3124dc069faa0b6545b6c9710", + "sha256:1000fba1057b92a65daec275aec30586c3de2401ccdcd41f8a5c1e2c87078706", + "sha256:1249cbbf3d3b04902ff081ffbb33ce3377fa6e4c7356f759f3cd076cc138d020", + "sha256:1920d4ff15ce893210c1f0c0e9d19bfbecb7983c76b33f046c13a8ffbd570252", + "sha256:193cbc708ea3aca45e7221ae58f0fd63f933753a9bfb498a3b474878f12caaad", + "sha256:1a100c6d595a7f316f1b6f01d20815d916e75ff98c27a01ae817439ea7726329", + "sha256:1f30b48dd7fa1474554b0b0f3fdfdd4c13b5c737a3c6284d3cdc424ec0ffff3a", + "sha256:203f0c8871d5a7987be20c72442488a0b8cfd0f43b7973771640fc593f56321f", + "sha256:246de67b99b6851627d945db38147d1b209a899311b1305dd84916f2b88526c6", + "sha256:2dee8e57f052ef5353cf608e0b4c871aee320dd1b87d351c28764fc0ca55f9f4", + "sha256:2efb1bd13885392adfda4614c33d3b68dee4921fd0ac1d3988f8cbb7d589e72a", + "sha256:2f4ac36d8e2b4cc1aa71df3dd84ff8efbe3bfb97ac41242fbcfc053c67434f46", + "sha256:3170c9399da12c9dc66366e9d14da8bf7147e1e9d9ea566067bbce7bb74bd9c2", + "sha256:3b1613dd5aee995ec6d4c69f00378bbd07614702a315a2cf6c1d21461fe17c23", + "sha256:3bb3d25a8e6c0aedd251753a79ae98a093c7e7b471faa3aa9a93a81431987ace", + "sha256:3bb7fda7260735efe66d5107fb7e6af6a7c04c7fce9b2514e04b7a74b06bf5dd", + "sha256:41b25eaa7d15909cf3ac4c96088c1f266a9a93ec44f87f1d13d4a0e86c81b982", + "sha256:45de3f87179c1823e6d9e32156fb14c1927fcc9aba21433f088fdfb555b77c10", + "sha256:46fb8c61d794b78ec7134a715a3e564aafc8f6b5e338417cb19fe9f57a5a9bf2", + "sha256:48021783bdf96e3d6de03a6e39a1171ed5bd7e8bb93fc84cc649d11490f87cea", + "sha256:4957669ef390f0e6719db3613ab3a7631e68424604a7b448f079bee145da6e09", + "sha256:5e86d77b090dbddbe78867a0275cb4df08ea195e660f1f7f13435a4649e954e5", + "sha256:6339d047dab2780cc6220f46306628e04d9750f02f983ddb37439ca47ced7149", + "sha256:681eb3d7e02e3c3655d1b16059fbfb605ac464c834a0c629048a30fad2b27489", + "sha256:6c409c0deba34f147f77efaa67b8e4bb83d2f11c8806405f76397ae5b8c0d1c9", + "sha256:7095f6fbfaa55defb6b733cfeb14efaae7a29f0b59d8cf213be4e7ca0b857b80", + "sha256:70c610f6cbe4b9fce272c407dd9d07e33e6bf7b4aa1b7ffb6f6ded8e634e3592", + "sha256:72814c01533f51d68702802d74f77ea026b5ec52793c791e2da806a3844a46c3", + "sha256:7a4826ad2bd6b07ca615c74ab91f32f6c96d08f6fcc3902ceeedaec8cdc3bcd6", + "sha256:7c70087bfee18a42b4040bb9ec1ca15a08242cf5867c58726530bdf3945672ed", + "sha256:855eafa5d5a2034b4621c74925d89c5efef61418570e5ef9b37717d9c796419c", + "sha256:8700f06d0ce6f128de3ccdbc1acaea1ee264d2caa9ca05daaf492fde7c2a7200", + "sha256:89f1b185a01fe560bc8ae5f619e924407efca2191b56ce749ec84982fc59a32a", + "sha256:8b2c760cfc7042b27ebdb4a43a4453bd829a5742503599144d54a032c5dc7e9e", + "sha256:8c2f5e83493748286002f9369f3e6607c565a6a90425a3a1fef5ae32a36d749d", + "sha256:8e098148dd37b4ce3baca71fb394c81dc5d9c7728c95df695d2dca218edf40e6", + "sha256:94aea8eff76ee6d1cdacb07dd2123a68283cb5569e0250feab1240058f53b623", + "sha256:95eb302ff792e12aba9a8b8f8474ab229a83c103d74a750ec0bd1c1eea32e669", + "sha256:9bd9b3b31adcb054116447ea22caa61a285d92e94d710aa5ec97992ff5eb7cf3", + "sha256:9e608aafdb55eb9f255034709e20d5a83b6d60c054df0802fa9c9883d0a937aa", + "sha256:a103b3a7069b62f5d4890ae1b8f0597618f628b286b03d4bc9195230b154bfa9", + "sha256:a386ebe437176aab38c041de1260cd3ea459c6ce5263594399880bbc398225b2", + "sha256:a38856a971c602f98472050165cea2cdc97709240373041b69030be15047691f", + "sha256:a401b4598e5d3f4a9a811f3daf42ee2291790c7f9d74b18d75d6e21dda98a1a1", + "sha256:a7647ebdfb9682b7bb97e2a5e7cb6ae735b1c25008a70b906aecca294ee96cf4", + "sha256:aaf63899c94de41fe3cf934601b0f7ccb6b428c6e4eeb80da72c58eab077b19a", + "sha256:b0dac0ff919ba34d4df1b6131f59ce95b08b9065233446be7e459f95554c0dc8", + "sha256:baacc6aee0b2ef6f3d308e197b5d7a81c0e70b06beae1f1fcacffdbd124fe0e3", + "sha256:bf420121d4c8dce6b889f0e8e4ec0ca34b7f40186203f06a946fa0276ba54029", + "sha256:c04a46716adde8d927adb9457bbe39cf473e1e2c2f5d0a16ceb837e5d841ad4f", + "sha256:c0b21078a4b56965e2b12f247467b234734491897e99c1d51cee628da9786959", + "sha256:c1c76a1743432b4b60ab3358c937a3fe1341c828ae6194108a94c69028247f22", + "sha256:c4983bf937209c57240cff65906b18bb35e64ae872da6a0db937d7b4af845dd7", + "sha256:c4fb39a81950ec280984b3a44f5bd12819953dc5fa3a7e6fa7a80db5ee853952", + "sha256:c57921cda3a80d0f2b8aec7e25c8aa14479ea92b5b51b6876d975d925a2ea346", + "sha256:c8063cf17b19661471ecbdb3df1c84f24ad2e389e326ccaf89e3fb2484d8dd7e", + "sha256:ccd16eb18a849fd8dcb23e23380e2f0a354e8daa0c984b8a732d9cfaba3a776d", + "sha256:cd6dbe0238f7743d0efe563ab46294f54f9bc8f4b9bcf57c3c666cc5bc9d1299", + "sha256:d62e51710986674142526ab9f78663ca2b0726066ae26b78b22e0f5e571238dd", + "sha256:db901e2ac34c931d73054d9797383d0f8009991e723dab15109740a63e7f902a", + "sha256:e03b8895a6990c9ab2cdcd0f2fe44088ca1c65ae592b8f795c3294af00a461c3", + "sha256:e1c8a2f4c69e08e89632defbfabec2feb8a8d99edc9f89ce33c4b9e36ab63037", + "sha256:e4b749b9cc6ee664a3300bb3a273c1ca8068c46be705b6c31cf5d276f8628a94", + "sha256:e6a5bf2cba5ae1bb80b154ed68a3cfa2fa00fde979a7f50d6598d3e17d9ac20c", + "sha256:e857a2232ba53ae940d3456f7533ce6ca98b81917d47adc3c7fd55dad8fab858", + "sha256:ee4006268ed33370957f55bf2e6f4d263eaf4dc3cfc473d1d90baff6ed36ce4a", + "sha256:eef9df1eefada2c09a5e7a40991b9fc6ac6ef20b1372abd48d2794a316dc0449", + "sha256:f058f6963fd82eb143c692cecdc89e075fa0828db2e5b291070485390b2f1c9c", + "sha256:f25c229a6ba38a35ae6e25ca1264621cc25d4d38dca2942a7fce0b67a4efe918", + "sha256:f2a1d0fd4242bd8643ce6f98927cf9c04540af6efa92323e9d3124f57727bfc1", + "sha256:f7560358a6811e52e9c4d142d497f1a6e10103d3a6881f18d04dbce3729c0e2c", + "sha256:f779d3ad205f108d14e99bb3859aa7dd8e9c68874617c72354d7ecaec2a054ac", + "sha256:f87f746ee241d30d6ed93969de31e5ffd09a2961a051e60ae6bddde9ec3583aa" ], - "markers": "python_version >= '3.7'", - "version": "==3.1.0" + "markers": "python_full_version >= '3.7.0'", + "version": "==3.2.0" }, "idna": { "hashes": [ @@ -129,14 +129,6 @@ "markers": "python_version >= '3.5'", "version": "==3.4" }, - "iniconfig": { - "hashes": [ - "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", - "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.0" - }, "jmespath": { "hashes": [ "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980", @@ -152,48 +144,24 @@ ], "version": "==0.12.0" }, - "packaging": { - "hashes": [ - "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", - "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" - ], - "markers": "python_version >= '3.7'", - "version": "==23.1" - }, - "pluggy": { - "hashes": [ - "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", - "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3" - ], - "markers": "python_version >= '3.7'", - "version": "==1.2.0" - }, "protobuf": { "hashes": [ - "sha256:0149053336a466e3e0b040e54d0b615fc71de86da66791c592cc3c8d18150bf8", - "sha256:08fe19d267608d438aa37019236db02b306e33f6b9902c3163838b8e75970223", - "sha256:29660574cd769f2324a57fb78127cda59327eb6664381ecfe1c69731b83e8288", - "sha256:2991f5e7690dab569f8f81702e6700e7364cc3b5e572725098215d3da5ccc6ac", - "sha256:3b01a5274ac920feb75d0b372d901524f7e3ad39c63b1a2d55043f3887afe0c1", - "sha256:3bcbeb2bf4bb61fe960dd6e005801a23a43578200ea8ceb726d1f6bd0e562ba1", - "sha256:447b9786ac8e50ae72cae7a2eec5c5df6a9dbf9aa6f908f1b8bda6032644ea62", - "sha256:514b6bbd54a41ca50c86dd5ad6488afe9505901b3557c5e0f7823a0cf67106fb", - "sha256:5cb9e41188737f321f4fce9a4337bf40a5414b8d03227e1d9fbc59bc3a216e35", - "sha256:7a92beb30600332a52cdadbedb40d33fd7c8a0d7f549c440347bc606fb3fe34b", - "sha256:84ea0bd90c2fdd70ddd9f3d3fc0197cc24ecec1345856c2b5ba70e4d99815359", - "sha256:aca6e86a08c5c5962f55eac9b5bd6fce6ed98645d77e8bfc2b952ecd4a8e4f6a", - "sha256:cc14358a8742c4e06b1bfe4be1afbdf5c9f6bd094dff3e14edb78a1513893ff5" - ], - "index": "pypi", - "version": "==4.23.3" - }, - "pytest": { - "hashes": [ - "sha256:3799fa815351fea3a5e96ac7e503a96fa51cc9942c3753cda7651b93c1cfa362", - "sha256:434afafd78b1d78ed0addf160ad2b77a30d35d4bdf8af234fe621919d9ed15e3" + "sha256:0a5759f5696895de8cc913f084e27fd4125e8fb0914bb729a17816a33819f474", + "sha256:351cc90f7d10839c480aeb9b870a211e322bf05f6ab3f55fcb2f51331f80a7d2", + "sha256:5fea3c64d41ea5ecf5697b83e41d09b9589e6f20b677ab3c48e5f242d9b7897b", + "sha256:6dd9b9940e3f17077e820b75851126615ee38643c2c5332aa7a359988820c720", + "sha256:7b19b6266d92ca6a2a87effa88ecc4af73ebc5cfde194dc737cf8ef23a9a3b12", + "sha256:8547bf44fe8cec3c69e3042f5c4fb3e36eb2a7a013bb0a44c018fc1e427aafbd", + "sha256:9053df6df8e5a76c84339ee4a9f5a2661ceee4a0dab019e8663c50ba324208b0", + "sha256:c3e0939433c40796ca4cfc0fac08af50b00eb66a40bbbc5dee711998fb0bbc1e", + "sha256:ccd9430c0719dce806b93f89c91de7977304729e55377f872a92465d548329a9", + "sha256:e1c915778d8ced71e26fcf43c0866d7499891bca14c4368448a82edc61fdbc70", + "sha256:e9d0be5bf34b275b9f87ba7407796556abeeba635455d036c7351f7c183ef8ff", + "sha256:effeac51ab79332d44fba74660d40ae79985901ac21bca408f8dc335a81aa597", + "sha256:fee88269a090ada09ca63551bf2f573eb2424035bcf2cb1b121895b01a46594a" ], "index": "pypi", - "version": "==7.3.1" + "version": "==4.23.4" }, "python-dateutil": { "hashes": [ @@ -272,14 +240,174 @@ ], "markers": "python_version >= '3.7'", "version": "==2.0.3" + } + }, + "develop": { + "black": { + "hashes": [ + "sha256:064101748afa12ad2291c2b91c960be28b817c0c7eaa35bec09cc63aa56493c5", + "sha256:0945e13506be58bf7db93ee5853243eb368ace1c08a24c65ce108986eac65915", + "sha256:11c410f71b876f961d1de77b9699ad19f939094c3a677323f43d7a29855fe326", + "sha256:1c7b8d606e728a41ea1ccbd7264677e494e87cf630e399262ced92d4a8dac940", + "sha256:1d06691f1eb8de91cd1b322f21e3bfc9efe0c7ca1f0e1eb1db44ea367dff656b", + "sha256:3238f2aacf827d18d26db07524e44741233ae09a584273aa059066d644ca7b30", + "sha256:32daa9783106c28815d05b724238e30718f34155653d4d6e125dc7daec8e260c", + "sha256:35d1381d7a22cc5b2be2f72c7dfdae4072a3336060635718cc7e1ede24221d6c", + "sha256:3a150542a204124ed00683f0db1f5cf1c2aaaa9cc3495b7a3b5976fb136090ab", + "sha256:48f9d345675bb7fbc3dd85821b12487e1b9a75242028adad0333ce36ed2a6d27", + "sha256:50cb33cac881766a5cd9913e10ff75b1e8eb71babf4c7104f2e9c52da1fb7de2", + "sha256:562bd3a70495facf56814293149e51aa1be9931567474993c7942ff7d3533961", + "sha256:67de8d0c209eb5b330cce2469503de11bca4085880d62f1628bd9972cc3366b9", + "sha256:6b39abdfb402002b8a7d030ccc85cf5afff64ee90fa4c5aebc531e3ad0175ddb", + "sha256:6f3c333ea1dd6771b2d3777482429864f8e258899f6ff05826c3a4fcc5ce3f70", + "sha256:714290490c18fb0126baa0fca0a54ee795f7502b44177e1ce7624ba1c00f2331", + "sha256:7c3eb7cea23904399866c55826b31c1f55bbcd3890ce22ff70466b907b6775c2", + "sha256:92c543f6854c28a3c7f39f4d9b7694f9a6eb9d3c5e2ece488c327b6e7ea9b266", + "sha256:a6f6886c9869d4daae2d1715ce34a19bbc4b95006d20ed785ca00fa03cba312d", + "sha256:a8a968125d0a6a404842fa1bf0b349a568634f856aa08ffaff40ae0dfa52e7c6", + "sha256:c7ab5790333c448903c4b721b59c0d80b11fe5e9803d8703e84dcb8da56fec1b", + "sha256:e114420bf26b90d4b9daa597351337762b63039752bdf72bf361364c1aa05925", + "sha256:e198cf27888ad6f4ff331ca1c48ffc038848ea9f031a3b40ba36aced7e22f2c8", + "sha256:ec751418022185b0c1bb7d7736e6933d40bbb14c14a0abcf9123d1b159f98dd4", + "sha256:f0bd2f4a58d6666500542b26354978218a9babcdc972722f4bf90779524515f3" + ], + "index": "pypi", + "version": "==23.3.0" + }, + "click": { + "hashes": [ + "sha256:2739815aaa5d2c986a88f1e9230c55e17f0caad3d958a5e13ad0797c166db9e3", + "sha256:b97d0c74955da062a7d4ef92fadb583806a585b2ea81958a81bd72726cbb8e37" + ], + "markers": "python_version >= '3.7'", + "version": "==8.1.4" + }, + "iniconfig": { + "hashes": [ + "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", + "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374" + ], + "markers": "python_version >= '3.7'", + "version": "==2.0.0" }, - "validate-email": { + "mypy": { "hashes": [ - "sha256:784719dc5f780be319cdd185dc85dd93afebdb6ebb943811bc4c7c5f9c72aeaf" + "sha256:01fd2e9f85622d981fd9063bfaef1aed6e336eaacca00892cd2d82801ab7c042", + "sha256:0dde1d180cd84f0624c5dcaaa89c89775550a675aff96b5848de78fb11adabcd", + "sha256:141dedfdbfe8a04142881ff30ce6e6653c9685b354876b12e4fe6c78598b45e2", + "sha256:16f0db5b641ba159eff72cff08edc3875f2b62b2fa2bc24f68c1e7a4e8232d01", + "sha256:190b6bab0302cec4e9e6767d3eb66085aef2a1cc98fe04936d8a42ed2ba77bb7", + "sha256:2460a58faeea905aeb1b9b36f5065f2dc9a9c6e4c992a6499a2360c6c74ceca3", + "sha256:34a9239d5b3502c17f07fd7c0b2ae6b7dd7d7f6af35fbb5072c6208e76295816", + "sha256:43b592511672017f5b1a483527fd2684347fdffc041c9ef53428c8dc530f79a3", + "sha256:43d24f6437925ce50139a310a64b2ab048cb2d3694c84c71c3f2a1626d8101dc", + "sha256:45d32cec14e7b97af848bddd97d85ea4f0db4d5a149ed9676caa4eb2f7402bb4", + "sha256:470c969bb3f9a9efcedbadcd19a74ffb34a25f8e6b0e02dae7c0e71f8372f97b", + "sha256:566e72b0cd6598503e48ea610e0052d1b8168e60a46e0bfd34b3acf2d57f96a8", + "sha256:5703097c4936bbb9e9bce41478c8d08edd2865e177dc4c52be759f81ee4dd26c", + "sha256:7549fbf655e5825d787bbc9ecf6028731973f78088fbca3a1f4145c39ef09462", + "sha256:8207b7105829eca6f3d774f64a904190bb2231de91b8b186d21ffd98005f14a7", + "sha256:8c4d8e89aa7de683e2056a581ce63c46a0c41e31bd2b6d34144e2c80f5ea53dc", + "sha256:98324ec3ecf12296e6422939e54763faedbfcc502ea4a4c38502082711867258", + "sha256:9bbcd9ab8ea1f2e1c8031c21445b511442cc45c89951e49bbf852cbb70755b1b", + "sha256:9d40652cc4fe33871ad3338581dca3297ff5f2213d0df345bcfbde5162abf0c9", + "sha256:a2746d69a8196698146a3dbe29104f9eb6a2a4d8a27878d92169a6c0b74435b6", + "sha256:ae704dcfaa180ff7c4cfbad23e74321a2b774f92ca77fd94ce1049175a21c97f", + "sha256:bfdca17c36ae01a21274a3c387a63aa1aafe72bff976522886869ef131b937f1", + "sha256:c482e1246726616088532b5e964e39765b6d1520791348e6c9dc3af25b233828", + "sha256:ca637024ca67ab24a7fd6f65d280572c3794665eaf5edcc7e90a866544076878", + "sha256:e02d700ec8d9b1859790c0475df4e4092c7bf3272a4fd2c9f33d87fac4427b8f", + "sha256:e5952d2d18b79f7dc25e62e014fe5a23eb1a3d2bc66318df8988a01b1a037c5b" ], "index": "pypi", - "version": "==1.3" + "version": "==1.4.1" + }, + "mypy-extensions": { + "hashes": [ + "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", + "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782" + ], + "markers": "python_version >= '3.5'", + "version": "==1.0.0" + }, + "packaging": { + "hashes": [ + "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", + "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f" + ], + "markers": "python_version >= '3.7'", + "version": "==23.1" + }, + "pathspec": { + "hashes": [ + "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687", + "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293" + ], + "markers": "python_version >= '3.7'", + "version": "==0.11.1" + }, + "platformdirs": { + "hashes": [ + "sha256:cec7b889196b9144d088e4c57d9ceef7374f6c39694ad1577a0aab50d27ea28c", + "sha256:f87ca4fcff7d2b0f81c6a748a77973d7af0f4d526f98f308477c3c436c74d528" + ], + "markers": "python_version >= '3.7'", + "version": "==3.8.1" + }, + "pluggy": { + "hashes": [ + "sha256:c2fd55a7d7a3863cba1a013e4e2414658b1d07b6bc57b3919e0c63c9abb99849", + "sha256:d12f0c4b579b15f5e054301bb226ee85eeeba08ffec228092f8defbaa3a4c4b3" + ], + "markers": "python_version >= '3.7'", + "version": "==1.2.0" + }, + "pytest": { + "hashes": [ + "sha256:78bf16451a2eb8c7a2ea98e32dc119fd2aa758f1d5d66dbf0a59d69a3969df32", + "sha256:b4bf8c45bd59934ed84001ad51e11b4ee40d40a1229d2c79f9c592b0a3f6bd8a" + ], + "index": "pypi", + "version": "==7.4.0" + }, + "ruff": { + "hashes": [ + "sha256:14a7b2f00f149c5a295f188a643ac25226ff8a4d08f7a62b1d4b0a1dc9f9b85c", + "sha256:2d4444c60f2e705c14cd802b55cd2b561d25bf4311702c463a002392d3116b22", + "sha256:2dab13cdedbf3af6d4427c07f47143746b6b95d9e4a254ac369a0edb9280a0d2", + "sha256:323b674c98078be9aaded5b8b51c0d9c424486566fb6ec18439b496ce79e5998", + "sha256:3250b24333ef419b7a232080d9724ccc4d2da1dbbe4ce85c4caa2290d83200f8", + "sha256:3a43fbe026ca1a2a8c45aa0d600a0116bec4dfa6f8bf0c3b871ecda51ef2b5dd", + "sha256:3e60605e07482183ba1c1b7237eca827bd6cbd3535fe8a4ede28cbe2a323cb97", + "sha256:468bfb0a7567443cec3d03cf408d6f562b52f30c3c29df19927f1e0e13a40cd7", + "sha256:479864a3ccd8a6a20a37a6e7577bdc2406868ee80b1e65605478ad3b8eb2ba0b", + "sha256:6fe81732f788894a00f6ade1fe69e996cc9e485b7c35b0f53fb00284397284b2", + "sha256:734165ea8feb81b0d53e3bf523adc2413fdb76f1264cde99555161dd5a725522", + "sha256:74e4b206cb24f2e98a615f87dbe0bde18105217cbcc8eb785bb05a644855ba50", + "sha256:7baa97c3d7186e5ed4d5d4f6834d759a27e56cf7d5874b98c507335f0ad5aadb", + "sha256:88d0f2afb2e0c26ac1120e7061ddda2a566196ec4007bd66d558f13b374b9efc", + "sha256:a9879f59f763cc5628aa01c31ad256a0f4dc61a29355c7315b83c2a5aac932b5", + "sha256:f32ec416c24542ca2f9cc8c8b65b84560530d338aaf247a4a78e74b99cd476b4", + "sha256:f612e0a14b3d145d90eb6ead990064e22f6f27281d847237560b4e10bf2251f3" + ], + "index": "pypi", + "version": "==0.0.277" + }, + "types-protobuf": { + "hashes": [ + "sha256:7bd5ea122a057b11a82b785d9de464932a1e9175fe977a4128adef11d7f35547", + "sha256:c926104f69ea62103846681b35b690d8d100ecf86c6cdda16c850a1313a272e4" + ], + "index": "pypi", + "version": "==4.23.0.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36", + "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2" + ], + "markers": "python_version >= '3.7'", + "version": "==4.7.1" } - }, - "develop": {} + } } diff --git a/protovalidate/internal/__init__.py b/protovalidate/internal/__init__.py index b71b700..29a76bb 100644 --- a/protovalidate/internal/__init__.py +++ b/protovalidate/internal/__init__.py @@ -11,4 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - diff --git a/tests/__init__.py b/tests/__init__.py index b71b700..29a76bb 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -11,4 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - diff --git a/tests/conformance/__init__.py b/tests/conformance/__init__.py index b71b700..29a76bb 100644 --- a/tests/conformance/__init__.py +++ b/tests/conformance/__init__.py @@ -11,4 +11,3 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - diff --git a/tests/conformance/runner.py b/tests/conformance/runner.py index 01f4e41..c3e6389 100644 --- a/tests/conformance/runner.py +++ b/tests/conformance/runner.py @@ -21,24 +21,24 @@ # in the TestConformanceRequest, once the Python protobuf library no longer # segfaults when using a dynamic descriptor pool. from buf.validate.conformance.cases import ( - bool_pb2, - bytes_pb2, - enums_pb2, - filename_with_dash_pb2, - kitchen_sink_pb2, - maps_pb2, - messages_pb2, - numbers_pb2, - oneofs_pb2, - repeated_pb2, - strings_pb2, - wkt_any_pb2, - wkt_duration_pb2, - wkt_nested_pb2, - wkt_timestamp_pb2, - wkt_wrappers_pb2, + bool_pb2, # noqa: F401 + bytes_pb2, # noqa: F401 + enums_pb2, # noqa: F401 + filename_with_dash_pb2, # noqa: F401 + kitchen_sink_pb2, # noqa: F401 + maps_pb2, # noqa: F401 + messages_pb2, # noqa: F401 + numbers_pb2, # noqa: F401 + oneofs_pb2, # noqa: F401 + repeated_pb2, # noqa: F401 + strings_pb2, # noqa: F401 + wkt_any_pb2, # noqa: F401 + wkt_duration_pb2, # noqa: F401 + wkt_nested_pb2, # noqa: F401 + wkt_timestamp_pb2, # noqa: F401 + wkt_wrappers_pb2, # noqa: F401 ) -from buf.validate.conformance.cases.custom_constraints import custom_constraints_pb2 +from buf.validate.conformance.cases.custom_constraints import custom_constraints_pb2 # noqa: F401 from buf.validate.conformance.harness import harness_pb2 from google.protobuf import any_pb2, descriptor, descriptor_pool, message_factory diff --git a/tests/runner_test.py b/tests/runner_test.py index 9b190d9..ee0000a 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -14,11 +14,10 @@ import unittest -from buf.validate.conformance.cases import oneofs_pb2 +from buf.validate.conformance.cases import oneofs_pb2 # noqa: F401 from buf.validate.conformance.harness import results_pb2 -from google.protobuf import descriptor_pool, message_factory +from google.protobuf import descriptor_pool -import protovalidate from tests.conformance import runner @@ -33,7 +32,7 @@ def test_oneof(self): # for fd in suite.fdset.file: # pool.Add(fd) for result in suite.cases: - actual = runner.run_any_test_case(pool, result.input) + runner.run_any_test_case(pool, result.input) if __name__ == "__main__": From 72c2af7b27b5944e8d0c0f25209202a1b3764e8a Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 22:06:44 -0700 Subject: [PATCH 08/20] Update CONTRIBUTING.md Python developers often don't have Go or make installed, so they may not be able to use the Makefile without some additional guidance. --- .github/CONTRIBUTING.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8f496ad..627d951 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -11,14 +11,29 @@ We pledge to maintain a welcoming and inclusive community. Please read our [Code of Conduct][code-of-conduct] before participating. ## How Can I Contribute? -Clone the repository and install dependencies: + +First, clone the repository: + ``` git clone git@github.com:bufbuild/protovalidate-python.git cd protovalidate-python -make install -pipenv shell ``` +Then, make any changes you'd like. We use a Makefile to test and lint our code, +so you'll need a few non-Python tools: + +* GNU Make (to use the Makefile): part of the `build-essential` package on + Debian-derived Linux distributions (including Ubuntu), and part of + `xcode-select --install` on Macs. +* Go (for the conformance test runner): often available in your system package + manager (`apt`, `dnf`, `brew`, etc.), but most reliable when [installed + directly from upstream](https://go.dev/doc/install). + +With Go and GNU Make installed, you can verify that your changes pass tests and +lint checks by running `make`. If your Python 3 interpreter isn't available as +`python3`, try `PYTHON=python make`. For a list of other useful commands, run +`make help`. + ### Reporting Bugs Bugs are tracked as GitHub issues. If you discover a problem From c89f45315223eee2ffe9f0acbbd09208f92b6eae Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 22:30:32 -0700 Subject: [PATCH 09/20] Fix formatting of __init__.py files Our license header tool and black don't agree about whether comment-only __init__.py files should have a trailing newline or now. Refactor the Makefile to stay DRY, but allow black to own the final formatting decisions. --- Makefile | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 9273944..acd20f0 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,10 @@ GO ?= go # Set to use a different Python interpreter. For example, `PYTHON=python make test`. PYTHON ?= python3 CONFORMANCE_ARGS ?= --strict --expected_failures=nonconforming.yaml +LICENSE_HEADER := $(BIN)/license-header \ + --license-type apache \ + --copyright-holder "Buf Technologies, Inc." \ + --year-range "$(COPYRIGHT_YEARS)" .PHONY: help help: ## Describe useful make targets @@ -33,11 +37,11 @@ generate: $(BIN)/buf ## Regenerate code and license headers rm -rf gen $(BIN)/buf generate buf.build/bufbuild/protovalidate $(BIN)/buf generate buf.build/bufbuild/protovalidate-testing - $(MAKE) generate-license + $(LICENSE_HEADER) --ignore __init__.py .PHONY: format format: install ## Format code - $(MAKE) generate-license + $(LICENSE_HEADER) pipenv run black protovalidate tests pipenv run ruff --fix protovalidate tests @@ -63,13 +67,6 @@ checkgenerate: generate @# Used in CI to verify that `make generate` doesn't produce a diff. test -z "$$(git status --porcelain | tee /dev/stderr)" -.PHONY: generate-license -generate-license: $(BIN)/license-header - $(BIN)/license-header \ - --license-type apache \ - --copyright-holder "Buf Technologies, Inc." \ - --year-range "$(COPYRIGHT_YEARS)" $(LICENSE_IGNORE) - $(BIN): @mkdir -p $(BIN) From d194601a40472c0f138ebe0beb7539c8a65b6097 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 22:34:14 -0700 Subject: [PATCH 10/20] Consider first-party code in import sort Manually use isort to resort imports, considering the `protovalidate` and `buf` modules to be first-party code. This is a pre-factoring: later in this stack of commits, we'll automate this sort with ruff. --- protovalidate/internal/constraints.py | 5 ++--- protovalidate/validator.py | 2 +- tests/conformance/runner.py | 6 +++--- tests/runner_test.py | 4 ++-- tests/validate_test.py | 3 +-- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/protovalidate/internal/constraints.py b/protovalidate/internal/constraints.py index 11ea926..48f5835 100644 --- a/protovalidate/internal/constraints.py +++ b/protovalidate/internal/constraints.py @@ -16,12 +16,11 @@ import typing import celpy # type: ignore -from buf.validate import expression_pb2 # type: ignore -from buf.validate import validate_pb2 # type: ignore -from buf.validate.priv import private_pb2 # type: ignore from celpy import celtypes # type: ignore from google.protobuf import any_pb2, descriptor, message +from buf.validate import expression_pb2, validate_pb2 # type: ignore +from buf.validate.priv import private_pb2 # type: ignore from protovalidate.internal import string_format diff --git a/protovalidate/validator.py b/protovalidate/validator.py index 2349a9a..85f5af0 100644 --- a/protovalidate/validator.py +++ b/protovalidate/validator.py @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from buf.validate import expression_pb2 # type: ignore from google.protobuf import message +from buf.validate import expression_pb2 # type: ignore from protovalidate.internal import constraints as _constraints from protovalidate.internal import extra_func diff --git a/tests/conformance/runner.py b/tests/conformance/runner.py index c3e6389..2a6163e 100644 --- a/tests/conformance/runner.py +++ b/tests/conformance/runner.py @@ -16,6 +16,9 @@ import typing import celpy # type: ignore +from google.protobuf import any_pb2, descriptor, descriptor_pool, message_factory + +import protovalidate # TODO(afuller): Use dynamic descriptor pool based on the FileDescriptorSet # in the TestConformanceRequest, once the Python protobuf library no longer @@ -40,9 +43,6 @@ ) from buf.validate.conformance.cases.custom_constraints import custom_constraints_pb2 # noqa: F401 from buf.validate.conformance.harness import harness_pb2 -from google.protobuf import any_pb2, descriptor, descriptor_pool, message_factory - -import protovalidate def run_test_case(tc: typing.Any, result: harness_pb2.TestResult | None = None) -> harness_pb2.TestResult: diff --git a/tests/runner_test.py b/tests/runner_test.py index ee0000a..e66834d 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -14,10 +14,10 @@ import unittest -from buf.validate.conformance.cases import oneofs_pb2 # noqa: F401 -from buf.validate.conformance.harness import results_pb2 from google.protobuf import descriptor_pool +from buf.validate.conformance.cases import oneofs_pb2 # noqa: F401 +from buf.validate.conformance.harness import results_pb2 from tests.conformance import runner diff --git a/tests/validate_test.py b/tests/validate_test.py index 71d357a..c4ae1e7 100644 --- a/tests/validate_test.py +++ b/tests/validate_test.py @@ -14,9 +14,8 @@ import unittest -from buf.validate.conformance.cases import maps_pb2, numbers_pb2, oneofs_pb2, repeated_pb2, wkt_timestamp_pb2 - import protovalidate +from buf.validate.conformance.cases import maps_pb2, numbers_pb2, oneofs_pb2, repeated_pb2, wkt_timestamp_pb2 # Test basic validation From 427e3f2c0786d3699ea6b73e268e757669e54a1f Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 22:41:10 -0700 Subject: [PATCH 11/20] Use variables in exceptions Fixes EM101 (https://beta.ruff.rs/docs/rules/raw-string-in-exception/). --- protovalidate/internal/constraints.py | 6 ++++-- protovalidate/internal/extra_func.py | 15 ++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/protovalidate/internal/constraints.py b/protovalidate/internal/constraints.py index 48f5835..fca8f60 100644 --- a/protovalidate/internal/constraints.py +++ b/protovalidate/internal/constraints.py @@ -99,7 +99,8 @@ def _MsgToCel(msg: message.Message) -> dict[str, celtypes.Value]: def _ScalarFieldValueToCel(val: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: ctor = _TYPE_TO_CTOR.get(field.type) if ctor is None: - raise CompilationError("unknown field type") + msg = "unknown field type" + raise CompilationError(msg) return ctor(val) @@ -290,7 +291,8 @@ def check_field_type(field: descriptor.FieldDescriptor, expected: int, wrapper_n if field.type != expected and ( field.type != descriptor.FieldDescriptor.TYPE_MESSAGE or field.message_type.full_name != wrapper_name ): - raise CompilationError(f"field {field.name} has type {field.type} but expected {expected}") + msg = f"field {field.name} has type {field.type} but expected {expected}" + raise CompilationError(msg) class FieldConstraintRules(CelConstraintRules): diff --git a/protovalidate/internal/extra_func.py b/protovalidate/internal/extra_func.py index 26224cd..f7baf9a 100644 --- a/protovalidate/internal/extra_func.py +++ b/protovalidate/internal/extra_func.py @@ -60,7 +60,8 @@ def _validateEmail(addr): def is_ip(val: celtypes.Value, version: celtypes.Value | None = None) -> celpy.Result: if not isinstance(val, (celtypes.BytesType, celtypes.StringType)): - raise celpy.EvalError("invalid argument, expected string or bytes") + msg = "invalid argument, expected string or bytes" + raise celpy.EvalError(msg) try: if version is None: ip_address(val) @@ -69,7 +70,8 @@ def is_ip(val: celtypes.Value, version: celtypes.Value | None = None) -> celpy.R elif version == 6: IPv6Address(val) else: - raise celpy.EvalError("invalid argument, expected 4 or 6") + msg = "invalid argument, expected 4 or 6" + raise celpy.EvalError(msg) return celtypes.BoolType(True) except ValueError: return celtypes.BoolType(False) @@ -77,7 +79,8 @@ def is_ip(val: celtypes.Value, version: celtypes.Value | None = None) -> celpy.R def is_email(string: celtypes.Value) -> celpy.Result: if not isinstance(string, celtypes.StringType): - raise celpy.EvalError("invalid argument, expected string") + msg = "invalid argument, expected string" + raise celpy.EvalError(msg) return celtypes.BoolType(_validateEmail(string)) @@ -97,13 +100,15 @@ def is_uri_ref(string: celtypes.Value) -> celpy.Result: def is_hostname(string: celtypes.Value) -> celpy.Result: if not isinstance(string, celtypes.StringType): - raise celpy.EvalError("invalid argument, expected string") + msg = "invalid argument, expected string" + raise celpy.EvalError(msg) return celtypes.BoolType(_validateHostName(string)) def unique(val: celtypes.Value) -> celpy.Result: if not isinstance(val, celtypes.ListType): - raise celpy.EvalError("invalid argument, expected list") + msg = "invalid argument, expected list" + raise celpy.EvalError(msg) return celtypes.BoolType(len(val) == len(set(val))) From f520bd95229c5c3248865f6549bdb3eb9fc8e894 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 22:56:37 -0700 Subject: [PATCH 12/20] Rename functions to snake_case Python style is to use `snake_case` for function and method names. Later in this stack of commits, we'll enforce this with `ruff`. --- protovalidate/internal/constraints.py | 54 +++++++++++++-------------- protovalidate/internal/extra_func.py | 10 ++--- tests/validate_test.py | 10 ++--- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/protovalidate/internal/constraints.py b/protovalidate/internal/constraints.py index fca8f60..5152389 100644 --- a/protovalidate/internal/constraints.py +++ b/protovalidate/internal/constraints.py @@ -44,7 +44,7 @@ def make_timestamp(msg: message.Message) -> celtypes.TimestampType: def unwrap(msg: message.Message) -> celtypes.Value: - return _FieldToCel(msg, msg.DESCRIPTOR.fields_by_name["value"]) + return _field_to_cel(msg, msg.DESCRIPTOR.fields_by_name["value"]) _MSG_TYPE_URL_TO_CTOR = { @@ -62,7 +62,7 @@ def unwrap(msg: message.Message) -> celtypes.Value: } -def _MsgToCel(msg: message.Message) -> dict[str, celtypes.Value]: +def _msg_to_cel(msg: message.Message) -> dict[str, celtypes.Value]: ctor = _MSG_TYPE_URL_TO_CTOR.get(msg.DESCRIPTOR.full_name) if ctor is not None: return ctor(msg) @@ -71,12 +71,12 @@ def _MsgToCel(msg: message.Message) -> dict[str, celtypes.Value]: for field in msg.DESCRIPTOR.fields: if field.containing_oneof is not None and not msg.HasField(field.name): continue - result[field.name] = _FieldToCel(msg, field) + result[field.name] = _field_to_cel(msg, field) return result _TYPE_TO_CTOR = { - descriptor.FieldDescriptor.TYPE_MESSAGE: _MsgToCel, + descriptor.FieldDescriptor.TYPE_MESSAGE: _msg_to_cel, descriptor.FieldDescriptor.TYPE_ENUM: celtypes.IntType, descriptor.FieldDescriptor.TYPE_BOOL: celtypes.BoolType, descriptor.FieldDescriptor.TYPE_BYTES: celtypes.BytesType, @@ -96,7 +96,7 @@ def _MsgToCel(msg: message.Message) -> dict[str, celtypes.Value]: } -def _ScalarFieldValueToCel(val: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: +def _scalar_field_value_to_cel(val: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: ctor = _TYPE_TO_CTOR.get(field.type) if ctor is None: msg = "unknown field type" @@ -104,15 +104,15 @@ def _ScalarFieldValueToCel(val: typing.Any, field: descriptor.FieldDescriptor) - return ctor(val) -def _FieldValueToCel(val: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: +def _field_value_to_cel(val: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: if field.message_type is not None and field.message_type.GetOptions().map_entry: - return _MapFieldValueToCel(val, field) - return _RepeatedFieldValueToCel(val, field) - return _ScalarFieldValueToCel(val, field) + return _map_field_value_to_cel(val, field) + return _repeated_field_value_to_cel(val, field) + return _scalar_field_value_to_cel(val, field) -def _IsEmptyField(msg: message.Message, field: descriptor.FieldDescriptor) -> bool: +def _is_empty_field(msg: message.Message, field: descriptor.FieldDescriptor) -> bool: if field.containing_oneof is not None and not msg.HasField(field.name): return True if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: @@ -154,39 +154,39 @@ def _IsEmptyField(msg: message.Message, field: descriptor.FieldDescriptor) -> bo raise ValueError("unknown field type") -def _RepeatedFieldToCel(msg: message.Message, field: descriptor.FieldDescriptor) -> celtypes.Value: +def _repeated_field_to_cel(msg: message.Message, field: descriptor.FieldDescriptor) -> celtypes.Value: if field.message_type is not None and field.message_type.GetOptions().map_entry: - return _MapFieldToCel(msg, field) - return _RepeatedFieldValueToCel(getattr(msg, field.name), field) + return _map_field_to_cel(msg, field) + return _repeated_field_value_to_cel(getattr(msg, field.name), field) -def _RepeatedFieldValueToCel(val: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: +def _repeated_field_value_to_cel(val: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: result = celtypes.ListType() for item in val: - result.append(_ScalarFieldValueToCel(item, field)) + result.append(_scalar_field_value_to_cel(item, field)) return result -def _MapFieldValueToCel(map: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: +def _map_field_value_to_cel(map: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: result = celtypes.MapType() key_field = field.message_type.fields[0] val_field = field.message_type.fields[1] for key, val in map.items(): - result[_FieldValueToCel(key, key_field)] = _FieldValueToCel(val, val_field) + result[_field_value_to_cel(key, key_field)] = _field_value_to_cel(val, val_field) return result -def _MapFieldToCel(msg: message.Message, field: descriptor.FieldDescriptor) -> celtypes.Value: - return _MapFieldValueToCel(getattr(msg, field.name), field) +def _map_field_to_cel(msg: message.Message, field: descriptor.FieldDescriptor) -> celtypes.Value: + return _map_field_value_to_cel(getattr(msg, field.name), field) -def _FieldToCel(msg: message.Message, field: descriptor.FieldDescriptor) -> celtypes.Value: +def _field_to_cel(msg: message.Message, field: descriptor.FieldDescriptor) -> celtypes.Value: if field.label == descriptor.FieldDescriptor.LABEL_REPEATED: - return _RepeatedFieldToCel(msg, field) + return _repeated_field_to_cel(msg, field) elif field.message_type is not None and not msg.HasField(field.name): return None else: - return _ScalarFieldValueToCel(getattr(msg, field.name), field) + return _scalar_field_value_to_cel(getattr(msg, field.name), field) class ConstraintContext: @@ -253,7 +253,7 @@ class CelConstraintRules(ConstraintRules): def __init__(self, rules: message.Message | None): self._runners = [] if rules is not None: - self._rules_cel = _MsgToCel(rules) + self._rules_cel = _msg_to_cel(rules) def _validate_cel(self, ctx: ConstraintContext, field_name: str, activation: dict[str, typing.Any]): activation["rules"] = self._rules_cel @@ -284,7 +284,7 @@ class MessageConstraintRules(CelConstraintRules): """Message-level rules.""" def validate(self, ctx: ConstraintContext, message: message.Message): - self._validate_cel(ctx, "", {"this": _MsgToCel(message)}) + self._validate_cel(ctx, "", {"this": _msg_to_cel(message)}) def check_field_type(field: descriptor.FieldDescriptor, expected: int, wrapper_name: str | None = None): @@ -328,7 +328,7 @@ def __init__( self.add_rule(env, funcs, cel) def validate(self, ctx: ConstraintContext, message: message.Message): - if _IsEmptyField(message, self._field): + if _is_empty_field(message, self._field): if self._required: ctx.add( self._field.name, @@ -347,11 +347,11 @@ def validate(self, ctx: ConstraintContext, message: message.Message): return val = getattr(message, self._field.name) self._validate_value(ctx, self._field.name, val) - self._validate_cel(ctx, self._field.name, {"this": _FieldValueToCel(val, self._field)}) + self._validate_cel(ctx, self._field.name, {"this": _field_value_to_cel(val, self._field)}) def validate_item(self, ctx: ConstraintContext, field_path: str, val: typing.Any): self._validate_value(ctx, field_path, val) - self._validate_cel(ctx, field_path, {"this": _ScalarFieldValueToCel(val, self._field)}) + self._validate_cel(ctx, field_path, {"this": _scalar_field_value_to_cel(val, self._field)}) def _validate_value(self, ctx: ConstraintContext, field_path: str, val: typing.Any): pass diff --git a/protovalidate/internal/extra_func.py b/protovalidate/internal/extra_func.py index f7baf9a..9902c6c 100644 --- a/protovalidate/internal/extra_func.py +++ b/protovalidate/internal/extra_func.py @@ -21,7 +21,7 @@ from protovalidate.internal import string_format -def _validateHostName(host): +def _validate_hostname(host): if not host: return False if len(host) > 253: @@ -43,7 +43,7 @@ def _validateHostName(host): return True -def _validateEmail(addr): +def validate_email(addr): if "<" in addr and ">" in addr: addr = addr.split("<")[1].split(">")[0] @@ -55,7 +55,7 @@ def _validateEmail(addr): return False if len(parts[0]) > 64: return False - return _validateHostName(parts[1]) + return _validate_hostname(parts[1]) def is_ip(val: celtypes.Value, version: celtypes.Value | None = None) -> celpy.Result: @@ -81,7 +81,7 @@ def is_email(string: celtypes.Value) -> celpy.Result: if not isinstance(string, celtypes.StringType): msg = "invalid argument, expected string" raise celpy.EvalError(msg) - return celtypes.BoolType(_validateEmail(string)) + return celtypes.BoolType(validate_email(string)) def is_uri(string: celtypes.Value) -> celpy.Result: @@ -102,7 +102,7 @@ def is_hostname(string: celtypes.Value) -> celpy.Result: if not isinstance(string, celtypes.StringType): msg = "invalid argument, expected string" raise celpy.EvalError(msg) - return celtypes.BoolType(_validateHostName(string)) + return celtypes.BoolType(_validate_hostname(string)) def unique(val: celtypes.Value) -> celpy.Result: diff --git a/tests/validate_test.py b/tests/validate_test.py index c4ae1e7..d56a6be 100644 --- a/tests/validate_test.py +++ b/tests/validate_test.py @@ -20,12 +20,12 @@ # Test basic validation class TestValidate(unittest.TestCase): - def test_SFixed64ExLTGT(self): + def test_sfixed64(self): msg = numbers_pb2.SFixed64ExLTGT(val=11) violations = protovalidate.validate(msg) self.assertEqual(len(violations.violations), 0) - def test_Oneofs(self): + def test_oneofs(self): msg1 = oneofs_pb2.Oneof() msg1.y = 123 violations = protovalidate.validate(msg1) @@ -36,18 +36,18 @@ def test_Oneofs(self): violations = protovalidate.validate(msg2) self.assertEqual(len(violations.violations), 0) - def test_Repeated(self): + def test_repeated(self): msg = repeated_pb2.RepeatedEmbedSkip() msg.val.add(val=-1) violations = protovalidate.validate(msg) self.assertEqual(len(violations.violations), 0) - def test_Maps(self): + def test_maps(self): msg = maps_pb2.MapMinMax() violations = protovalidate.validate(msg) self.assertEqual(len(violations.violations), 1) - def test_Timestamp(self): + def test_timestamp(self): msg = wkt_timestamp_pb2.TimestampGTNow() violations = protovalidate.validate(msg) self.assertEqual(len(violations.violations), 0) From 6afc87a5e7e33a6c5515fe95fa5a1e66a424f5c6 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 23:20:04 -0700 Subject: [PATCH 13/20] Fix lint errors in constraint.py Fix errors in constraint.py (bypassing spurious ones). Later in this stack of commits, ruff will require these fixes. --- protovalidate/internal/constraints.py | 135 +++++++++++++------------- 1 file changed, 68 insertions(+), 67 deletions(-) diff --git a/protovalidate/internal/constraints.py b/protovalidate/internal/constraints.py index 5152389..157fd82 100644 --- a/protovalidate/internal/constraints.py +++ b/protovalidate/internal/constraints.py @@ -151,7 +151,8 @@ def _is_empty_field(msg: message.Message, field: descriptor.FieldDescriptor) -> return getattr(msg, field.name) == 0 if field.type == descriptor.FieldDescriptor.TYPE_ENUM: return getattr(msg, field.name) == 0 - raise ValueError("unknown field type") + exception_msg = "unknown field type" + raise ValueError(exception_msg) def _repeated_field_to_cel(msg: message.Message, field: descriptor.FieldDescriptor) -> celtypes.Value: @@ -167,11 +168,11 @@ def _repeated_field_value_to_cel(val: typing.Any, field: descriptor.FieldDescrip return result -def _map_field_value_to_cel(map: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: +def _map_field_value_to_cel(mapping: typing.Any, field: descriptor.FieldDescriptor) -> celtypes.Value: result = celtypes.MapType() key_field = field.message_type.fields[0] val_field = field.message_type.fields[1] - for key, val in map.items(): + for key, val in mapping.items(): result[_field_value_to_cel(key, key_field)] = _field_value_to_cel(val, val_field) return result @@ -192,7 +193,7 @@ def _field_to_cel(msg: message.Message, field: descriptor.FieldDescriptor) -> ce class ConstraintContext: """The state associated with a single constraint evaluation.""" - def __init__(self, fail_fast: bool = False, violations: expression_pb2.Violations = None): + def __init__(self, fail_fast: bool = False, violations: expression_pb2.Violations = None): # noqa: FBT001, FBT002 self._fail_fast = fail_fast if violations is None: violations = expression_pb2.Violations() @@ -239,7 +240,7 @@ def sub_context(self): class ConstraintRules: """The constrains associated with a single 'rules' message.""" - def validate(self, ctx: ConstraintContext, message: message.Message): + def validate(self, ctx: ConstraintContext, message: message.Message): # noqa: ARG002 """Validate the message against the rules in this constraint.""" ctx.add("", "unimplemented", "Unimplemented") @@ -306,25 +307,25 @@ def __init__( env: celpy.Environment, funcs: dict[str, celpy.CELFunction], field: descriptor.FieldDescriptor, - fieldLvl: validate_pb2.FieldConstraints, + field_level: validate_pb2.FieldConstraints, ): - type_case = fieldLvl.WhichOneof("type") - super().__init__(None if type_case is None else getattr(fieldLvl, type_case)) + type_case = field_level.WhichOneof("type") + super().__init__(None if type_case is None else getattr(field_level, type_case)) self._field = field - if fieldLvl.ignore_empty: + if field_level.ignore_empty: self._ignore_empty = True - if fieldLvl.required: + if field_level.required: self._required = True - type_case = fieldLvl.WhichOneof("type") + type_case = field_level.WhichOneof("type") if type_case is not None: - rules = getattr(fieldLvl, type_case) + rules = getattr(field_level, type_case) # For each set field in the message, look for the private constraint # extension. for field, _ in rules.ListFields(): if private_pb2.field in field.GetOptions().Extensions: for cel in field.GetOptions().Extensions[private_pb2.field].cel: self.add_rule(env, funcs, cel) - for cel in fieldLvl.cel: + for cel in field_level.cel: self.add_rule(env, funcs, cel) def validate(self, ctx: ConstraintContext, message: message.Message): @@ -360,21 +361,21 @@ def _validate_value(self, ctx: ConstraintContext, field_path: str, val: typing.A class AnyConstraintRules(FieldConstraintRules): """Rules for an Any field.""" - _in: typing.List[str] = [] - _not_in: typing.List[str] = [] + _in: typing.List[str] = [] # noqa: RUF012 + _not_in: typing.List[str] = [] # noqa: RUF012 def __init__( self, env: celpy.Environment, funcs: dict[str, celpy.CELFunction], field: descriptor.FieldDescriptor, - fieldLvl: validate_pb2.FieldConstraints, + field_level: validate_pb2.FieldConstraints, ): - super().__init__(env, funcs, field, fieldLvl) - if getattr(fieldLvl.any, "in"): - self._in = getattr(fieldLvl.any, "in") - if fieldLvl.any.not_in: - self._not_in = fieldLvl.any.not_in + super().__init__(env, funcs, field, field_level) + if getattr(field_level.any, "in"): + self._in = getattr(field_level.any, "in") + if field_level.any.not_in: + self._not_in = field_level.any.not_in def _validate_value(self, ctx: ConstraintContext, field_path: str, value: any_pb2.Any): if len(self._in) > 0: @@ -402,10 +403,10 @@ def __init__( env: celpy.Environment, funcs: dict[str, celpy.CELFunction], field: descriptor.FieldDescriptor, - fieldLvl: validate_pb2.FieldConstraints, + field_level: validate_pb2.FieldConstraints, ): - super().__init__(env, funcs, field, fieldLvl) - if fieldLvl.enum.defined_only: + super().__init__(env, funcs, field, field_level) + if field_level.enum.defined_only: self._defined_only = True def validate(self, ctx: ConstraintContext, message: message.Message): @@ -432,10 +433,10 @@ def __init__( env: celpy.Environment, funcs: dict[str, celpy.CELFunction], field: descriptor.FieldDescriptor, - fieldLvl: validate_pb2.FieldConstraints, + field_level: validate_pb2.FieldConstraints, item_rules: FieldConstraintRules | None, ): - super().__init__(env, funcs, field, fieldLvl) + super().__init__(env, funcs, field, field_level) if item_rules is not None: self._item_rules = item_rules @@ -466,11 +467,11 @@ def __init__( env: celpy.Environment, funcs: dict[str, celpy.CELFunction], field: descriptor.FieldDescriptor, - fieldLvl: validate_pb2.FieldConstraints, + field_level: validate_pb2.FieldConstraints, key_rules: FieldConstraintRules | None, value_rules: FieldConstraintRules | None, ): - super().__init__(env, funcs, field, fieldLvl) + super().__init__(env, funcs, field, field_level) if key_rules is not None: self._key_rules = key_rules if value_rules is not None: @@ -481,12 +482,12 @@ def validate(self, ctx: ConstraintContext, message: message.Message): if ctx.done: return value = getattr(message, self._field.name) - for key, value in value.items(): - key_field_path = make_key_path(self._field.name, key) + for k, v in value.items(): + key_field_path = make_key_path(self._field.name, k) if self._key_rules is not None: - self._key_rules.validate_item(ctx, key_field_path, key) + self._key_rules.validate_item(ctx, key_field_path, k) if self._value_rules is not None: - self._value_rules.validate_item(ctx, key_field_path, value) + self._value_rules.validate_item(ctx, key_field_path, v) class OneofConstraintRules(ConstraintRules): @@ -542,29 +543,29 @@ def _new_message_constraint(self, rules: validate_pb2.message) -> MessageConstra def _new_scalar_field_constraint( self, field: descriptor.FieldDescriptor, - fieldLvl: validate_pb2.field, + field_level: validate_pb2.field, ): - if fieldLvl.skipped: + if field_level.skipped: return None - type_case = fieldLvl.WhichOneof("type") + type_case = field_level.WhichOneof("type") if type_case is None: - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "duration": check_field_type(field, 0, "google.protobuf.Duration") - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "timestamp": check_field_type(field, 0, "google.protobuf.Timestamp") - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "enum": check_field_type(field, descriptor.FieldDescriptor.TYPE_ENUM) - result = EnumConstraintRules(self._env, self._funcs, field, fieldLvl) + result = EnumConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "bool": check_field_type(field, descriptor.FieldDescriptor.TYPE_BOOL, "google.protobuf.BoolValue") - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "bytes": check_field_type( @@ -572,15 +573,15 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_BYTES, "google.protobuf.BytesValue", ) - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "fixed32": check_field_type(field, descriptor.FieldDescriptor.TYPE_FIXED32) - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "fixed64": check_field_type(field, descriptor.FieldDescriptor.TYPE_FIXED64) - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "float": check_field_type( @@ -588,7 +589,7 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_FLOAT, "google.protobuf.FloatValue", ) - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "double": check_field_type( @@ -596,7 +597,7 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_DOUBLE, "google.protobuf.DoubleValue", ) - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "int32": check_field_type( @@ -604,7 +605,7 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_INT32, "google.protobuf.Int32Value", ) - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "int64": check_field_type( @@ -612,23 +613,23 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_INT64, "google.protobuf.Int64Value", ) - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "sfixed32": check_field_type(field, descriptor.FieldDescriptor.TYPE_SFIXED32) - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "sfixed64": check_field_type(field, descriptor.FieldDescriptor.TYPE_SFIXED64) - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "sint32": check_field_type(field, descriptor.FieldDescriptor.TYPE_SINT32) - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "sint64": check_field_type(field, descriptor.FieldDescriptor.TYPE_SINT64) - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "uint32": check_field_type( @@ -636,7 +637,7 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_UINT32, "google.protobuf.UInt32Value", ) - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "uint64": check_field_type( @@ -644,7 +645,7 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_UINT64, "google.protobuf.UInt64Value", ) - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "string": check_field_type( @@ -652,11 +653,11 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_STRING, "google.protobuf.StringValue", ) - result = FieldConstraintRules(self._env, self._funcs, field, fieldLvl) + result = FieldConstraintRules(self._env, self._funcs, field, field_level) return result elif type_case == "any": check_field_type(field, descriptor.FieldDescriptor.TYPE_MESSAGE, "google.protobuf.Any") - result = AnyConstraintRules(self._env, self._funcs, field, fieldLvl) + result = AnyConstraintRules(self._env, self._funcs, field, field_level) return result def _new_field_constraint( @@ -685,10 +686,10 @@ def _new_constraints(self, desc: descriptor.Descriptor) -> list[ConstraintRules] result: list[ConstraintRules] = [] constraint: ConstraintRules | None = None if validate_pb2.message in desc.GetOptions().Extensions: - msgLvl = desc.GetOptions().Extensions[validate_pb2.message] - if msgLvl.disabled: + message_level = desc.GetOptions().Extensions[validate_pb2.message] + if message_level.disabled: return [] - if constraint := self._new_message_constraint(msgLvl): + if constraint := self._new_message_constraint(message_level): result.append(constraint) for oneof in desc.oneofs: @@ -698,11 +699,11 @@ def _new_constraints(self, desc: descriptor.Descriptor) -> list[ConstraintRules] for field in desc.fields: if validate_pb2.field in field.GetOptions().Extensions: - fieldLvl = field.GetOptions().Extensions[validate_pb2.field] - if fieldLvl.skipped: + field_level = field.GetOptions().Extensions[validate_pb2.field] + if field_level.skipped: continue - result.append(self._new_field_constraint(field, fieldLvl)) - if fieldLvl.repeated.items.skipped: + result.append(self._new_field_constraint(field, field_level)) + if field_level.repeated.items.skipped: continue if field.message_type is None: continue @@ -760,12 +761,12 @@ def validate(self, ctx: ConstraintContext, message: message.Message): constraints = self._factory.get(self._value_field.message_type) if constraints is None: return - for key, val in val.items(): + for k, v in val.items(): sub_ctx = ctx.sub_context() for constraint in constraints: - constraint.validate(sub_ctx, val) + constraint.validate(sub_ctx, v) if sub_ctx.has_errors(): - sub_ctx.add_path_prefix(f"{self._field.name}[{key}]") + sub_ctx.add_path_prefix(f"{self._field.name}[{k}]") ctx.add_errors(sub_ctx) @@ -785,10 +786,10 @@ def validate(self, ctx: ConstraintContext, message: message.Message): constraints = self._factory.get(self._field.message_type) if constraints is None: return - for idx, val in enumerate(val): + for idx, item in enumerate(val): sub_ctx = ctx.sub_context() for constraint in constraints: - constraint.validate(sub_ctx, val) + constraint.validate(sub_ctx, item) if sub_ctx.has_errors(): sub_ctx.add_path_prefix(f"{self._field.name}[{idx}]") ctx.add_errors(sub_ctx) From 226245941a0ca7cb3aeb16fbae9a2b13c3d6ce71 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 23:27:57 -0700 Subject: [PATCH 14/20] Fix lint in string_format.py Bypass some spurious warnings in string_format.py. --- protovalidate/internal/string_format.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/protovalidate/internal/string_format.py b/protovalidate/internal/string_format.py index 0dcad72..bed78f5 100644 --- a/protovalidate/internal/string_format.py +++ b/protovalidate/internal/string_format.py @@ -40,7 +40,7 @@ class StringFormat: def __init__(self, locale: str): self.locale = locale - def format(self, fmt: celtypes.Value, args: celtypes.Value) -> celpy.Result: + def format(self, fmt: celtypes.Value, args: celtypes.Value) -> celpy.Result: # noqa: A003 if not isinstance(fmt, celtypes.StringType): return celpy.native_to_cel(celpy.new_error("format() requires a string as the first argument")) if not isinstance(args, celtypes.ListType): @@ -169,5 +169,5 @@ def format_list(self, arg: celtypes.ListType) -> celpy.Result: _default_format = StringFormat("en_US") -format = _default_format.format +format = _default_format.format # noqa: A001 format_value = _default_format.format_value From c05f1050eb03fdb93d01b2ed18fc1ba07585f48c Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 23:28:18 -0700 Subject: [PATCH 15/20] Bypass lint errors in validator.py Bypass spurious lint errors in validator.py. --- protovalidate/validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protovalidate/validator.py b/protovalidate/validator.py index 85f5af0..d4eca96 100644 --- a/protovalidate/validator.py +++ b/protovalidate/validator.py @@ -39,7 +39,7 @@ def __init__(self): def validate( self, message: message.Message, - fail_fast: bool = False, + fail_fast: bool = False, # noqa: FBT001, FBT002 result: expression_pb2.Violations = None, ) -> expression_pb2.Violations: """ From 15ec11e40ea26c0a7ed3e1decaabfba9731b81c0 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 23:30:52 -0700 Subject: [PATCH 16/20] Tighten ruff configuration Configure ruff to catch a wider variety of unidiomatic and error-prone patterns. --- pyproject.toml | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 077d2ef..c2b327c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,3 +32,53 @@ line-length = 120 [tool.ruff] target-version = "py37" line-length = 120 +select = [ + "A", + "ARG", + "B", + "C", + "DTZ", + "E", + "EM", + "F", + "FBT", + "I", + "ICN", + "ISC", + "N", + "PLC", + "PLE", + "PLR", + "PLW", + "Q", + "RUF", + "S", + "T", + "TID", + "UP", + "W", + "YTT", +] +ignore = [ + # Allow boolean positional values in function calls, like `dict.get(..., True)`. + "FBT003", + # Ignore complexity + "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", + # Ignore magic values - in this library, most are obvious in context. + "PLR2004", +] +unfixable = [ + # Don't autofix unused imports. + "F401", +] + +[tool.ruff.isort] +known-first-party = ["protovalidate", "buf"] + +[tool.ruff.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ruff.per-file-ignores] +# Tests can use magic values, assertions, and relative imports. +"tests/**/*" = ["PLR2004", "S101", "TID252"] + From 91776b6042ed908fff5f0a05e3aa536bcaea3e73 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Fri, 7 Jul 2023 23:35:28 -0700 Subject: [PATCH 17/20] Use idiomatic pytest style There's no reason to use the wordy xUnit-style `unittest`: we're using `pytest`, which supports a more Pythonic approach. --- tests/runner_test.py | 27 +++++++------------ tests/validate_test.py | 60 ++++++++++++++++++++---------------------- 2 files changed, 37 insertions(+), 50 deletions(-) diff --git a/tests/runner_test.py b/tests/runner_test.py index e66834d..588a1e9 100644 --- a/tests/runner_test.py +++ b/tests/runner_test.py @@ -12,8 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - from google.protobuf import descriptor_pool from buf.validate.conformance.cases import oneofs_pb2 # noqa: F401 @@ -21,19 +19,12 @@ from tests.conformance import runner -class RunnerTest(unittest.TestCase): - def test_oneof(self): - results = results_pb2.ResultSet() - # load the results from oneof.binproto - with open("tests/oneof.binproto", "rb") as f: - results.ParseFromString(f.read()) - for suite in results.suites: - pool = descriptor_pool.Default() - # for fd in suite.fdset.file: - # pool.Add(fd) - for result in suite.cases: - runner.run_any_test_case(pool, result.input) - - -if __name__ == "__main__": - unittest.main() +def test_oneof(): + results = results_pb2.ResultSet() + # load the results from oneof.binproto + with open("tests/oneof.binproto", "rb") as f: + results.ParseFromString(f.read()) + for suite in results.suites: + pool = descriptor_pool.Default() + for result in suite.cases: + runner.run_any_test_case(pool, result.input) diff --git a/tests/validate_test.py b/tests/validate_test.py index d56a6be..9ab89d6 100644 --- a/tests/validate_test.py +++ b/tests/validate_test.py @@ -12,46 +12,42 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - import protovalidate from buf.validate.conformance.cases import maps_pb2, numbers_pb2, oneofs_pb2, repeated_pb2, wkt_timestamp_pb2 -# Test basic validation -class TestValidate(unittest.TestCase): - def test_sfixed64(self): - msg = numbers_pb2.SFixed64ExLTGT(val=11) - violations = protovalidate.validate(msg) - self.assertEqual(len(violations.violations), 0) +def test_sfixed64(): + msg = numbers_pb2.SFixed64ExLTGT(val=11) + violations = protovalidate.validate(msg) + assert len(violations.violations) == 0 + + +def test_oneofs(): + msg1 = oneofs_pb2.Oneof() + msg1.y = 123 + violations = protovalidate.validate(msg1) + assert len(violations.violations) == 0 - def test_oneofs(self): - msg1 = oneofs_pb2.Oneof() - msg1.y = 123 - violations = protovalidate.validate(msg1) - self.assertEqual(len(violations.violations), 0) + msg2 = oneofs_pb2.Oneof() + msg2.z.val = True + violations = protovalidate.validate(msg2) + assert len(violations.violations) == 0 - msg2 = oneofs_pb2.Oneof() - msg2.z.val = True - violations = protovalidate.validate(msg2) - self.assertEqual(len(violations.violations), 0) - def test_repeated(self): - msg = repeated_pb2.RepeatedEmbedSkip() - msg.val.add(val=-1) - violations = protovalidate.validate(msg) - self.assertEqual(len(violations.violations), 0) +def test_repeated(): + msg = repeated_pb2.RepeatedEmbedSkip() + msg.val.add(val=-1) + violations = protovalidate.validate(msg) + assert len(violations.violations) == 0 - def test_maps(self): - msg = maps_pb2.MapMinMax() - violations = protovalidate.validate(msg) - self.assertEqual(len(violations.violations), 1) - def test_timestamp(self): - msg = wkt_timestamp_pb2.TimestampGTNow() - violations = protovalidate.validate(msg) - self.assertEqual(len(violations.violations), 0) +def test_maps(): + msg = maps_pb2.MapMinMax() + violations = protovalidate.validate(msg) + assert len(violations.violations) == 1 -if __name__ == "__main__": - unittest.main() +def test_timestamp(): + msg = wkt_timestamp_pb2.TimestampGTNow() + violations = protovalidate.validate(msg) + assert len(violations.violations) == 0 From 97169f83c04ba4014c344153f87d6fbfb65fc8ad Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Sat, 8 Jul 2023 02:51:42 -0700 Subject: [PATCH 18/20] Use Makefile in CI Use the Makefile in CI, rather than scripting directly in the YAML files. Also, ensure that the Go compiler is up-to-date in CI. --- .github/workflows/ci.yaml | 18 +++++++++--------- .github/workflows/conformance.yaml | 4 +++- Makefile | 7 +++++-- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b0dd3b8..565c24d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,17 +22,17 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 1 - - name: Set up Python ${{ matrix.python-version }} + - name: Install Go + uses: actions/setup-go@v4 + - name: Install Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: Check generated - run: make checkgenerate - name: Execute tests run: make test - - name: Check mypy - run: mypy protovalidate - - name: Check ruff - uses: chartboost/ruff-action@v1 - with: - src: "./protovalidate" + - name: Lint + run: make lint + - name: Format + run: make format + - name: Check generated + run: make checkgenerate diff --git a/.github/workflows/conformance.yaml b/.github/workflows/conformance.yaml index b6586a1..8ef9b69 100644 --- a/.github/workflows/conformance.yaml +++ b/.github/workflows/conformance.yaml @@ -22,7 +22,9 @@ jobs: uses: actions/checkout@v3 with: fetch-depth: 1 - - name: Set up Python ${{ matrix.python-version }} + - name: Install Go + uses: actions/setup-go@v4 + - name: Install Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} diff --git a/Makefile b/Makefile index acd20f0..543b587 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ help: ## Describe useful make targets @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "%-15s %s\n", $$1, $$2}' .PHONY: all -all: test lint ## Run all tests and lint (default) +all: test conformance lint ## Run all tests and lint (default) .PHONY: clean clean: ## Delete intermediate build artifacts @@ -46,8 +46,11 @@ format: install ## Format code pipenv run ruff --fix protovalidate tests .PHONY: test -test: $(BIN)/protovalidate-conformance generate install ## Run unit and conformance tests +test: $(BIN)/protovalidate-conformance generate install ## Run unit tests pipenv run pytest + +.PHONY: conformance +conformance: $(BIN)/protovalidate-conformance generate install ## Run conformance tests $(BIN)/protovalidate-conformance $(CONFORMANCE_ARGS) pipenv -- run $(PYTHON) -m tests.conformance.runner .PHONY: lint From 65296428ad597acdfef74806da29bbc3ceacbde4 Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Sat, 8 Jul 2023 03:10:51 -0700 Subject: [PATCH 19/20] Fix license-header dependency --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 543b587..72ce3ef 100644 --- a/Makefile +++ b/Makefile @@ -33,14 +33,14 @@ clean: ## Delete intermediate build artifacts git clean -Xdf .PHONY: generate -generate: $(BIN)/buf ## Regenerate code and license headers +generate: $(BIN)/buf $(BIN)/license-header ## Regenerate code and license headers rm -rf gen $(BIN)/buf generate buf.build/bufbuild/protovalidate $(BIN)/buf generate buf.build/bufbuild/protovalidate-testing $(LICENSE_HEADER) --ignore __init__.py .PHONY: format -format: install ## Format code +format: install $(BIN)/license-header ## Format code $(LICENSE_HEADER) pipenv run black protovalidate tests pipenv run ruff --fix protovalidate tests From 88eb7ec8ef3f8261b7622c02a479d7c9aab7a4ab Mon Sep 17 00:00:00 2001 From: Akshay Shah Date: Sat, 8 Jul 2023 03:19:36 -0700 Subject: [PATCH 20/20] Install development dependencies too --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 72ce3ef..26c0f65 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,7 @@ lint: install ## Lint code .PHONY: install install: ## Install dependencies $(PYTHON) -m pip install --upgrade pip pipenv - pipenv --python $(PYTHON) sync + pipenv --python $(PYTHON) sync --dev .PHONY: checkgenerate checkgenerate: generate