diff --git a/Makefile b/Makefile index 828ac40d..ffdbd866 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,7 @@ format: install $(BIN)/buf $(BIN)/license-header ## Format code .PHONY: test test: generate install gettestdata ## Run unit tests - uv run -- python -m unittest + uv run -- pytest .PHONY: conformance conformance: $(BIN)/protovalidate-conformance generate install ## Run conformance tests diff --git a/pyproject.toml b/pyproject.toml index a7b7f9a3..45d3cc4e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ Issues = "https://github.com/bufbuild/protovalidate-python/issues" dev = [ "google-re2-stubs>=0.1.1", "mypy>=1.17.1", + "pytest>=8.4.1", "ruff>=0.12.0", "types-protobuf>=5.29.1.20250315", ] @@ -101,8 +102,12 @@ known-first-party = ["protovalidate", "buf"] ban-relative-imports = "all" [tool.ruff.lint.per-file-ignores] -# Tests can use magic values, assertions, and relative imports. -"tests/**/*" = ["PLR2004", "S101", "TID252"] +# Tests can use assertions. +"test/**/*" = ["S101"] + +[tool.pytest.ini_options] +# restrict testpaths to speed up test discovery +testpaths = ["test"] [tool.mypy] mypy_path = "gen" diff --git a/test/test_format.py b/test/test_format.py index 3da9fd9a..092a698e 100644 --- a/test/test_format.py +++ b/test/test_format.py @@ -12,12 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest -from collections.abc import MutableMapping +from collections.abc import Iterable, MutableMapping from itertools import chain from typing import Any, Optional import celpy +import pytest from celpy import celtypes from google.protobuf import text_format @@ -82,65 +82,53 @@ def get_eval_error_message(test: simple_pb2.SimpleTest) -> Optional[str]: return None -class TestFormat(unittest.TestCase): - @classmethod - def setUpClass(cls): - # The test data from the cel-spec conformance tests - cel_test_data = load_test_data(f"test/testdata/string_ext_{CEL_SPEC_VERSION}.textproto") - # Our supplemental tests of functionality not in the cel conformance file, but defined in the spec. - supplemental_test_data = load_test_data("test/testdata/string_ext_supplemental.textproto") - - # Combine the test data from both files into one - sections = cel_test_data.section - sections.extend(supplemental_test_data.section) - - # Find the format tests which test successful formatting - cls._format_tests = chain.from_iterable(x.test for x in sections if x.name == "format") - # Find the format error tests which test errors during formatting - cls._format_error_tests = chain.from_iterable(x.test for x in sections if x.name == "format_errors") - - cls._env = celpy.Environment(runner_class=InterpretedRunner) - - def test_format_successes(self): - """ - Tests success scenarios for string.format - """ - for test in self._format_tests: - if test.name in skipped_tests: - continue - ast = self._env.compile(test.expr) - prog = self._env.program(ast, functions=extra_func.make_extra_funcs()) - - bindings = build_variables(test.bindings) - with self.subTest(test.name): - try: - result = prog.evaluate(bindings) - expected = get_expected_result(test) - if expected is not None: - self.assertEqual(result, expected) - else: - self.fail(f"[{test.name}]: expected a success result to be defined") - except celpy.CELEvalError as e: - self.fail(e) - - def test_format_errors(self): - """ - Tests error scenarios for string.format - """ - for test in self._format_error_tests: - if test.name in skipped_error_tests: - continue - ast = self._env.compile(test.expr) - prog = self._env.program(ast, functions=extra_func.make_extra_funcs()) - - bindings = build_variables(test.bindings) - with self.subTest(test.name): - try: - prog.evaluate(bindings) - self.fail(f"[{test.name}]: expected an error to be raised during evaluation") - except celpy.CELEvalError as e: - msg = get_eval_error_message(test) - if msg is not None: - self.assertEqual(str(e), msg) - else: - self.fail(f"[{test.name}]: expected an eval error to be defined") +# The test data from the cel-spec conformance tests +cel_test_data = load_test_data(f"test/testdata/string_ext_{CEL_SPEC_VERSION}.textproto") +# Our supplemental tests of functionality not in the cel conformance file, but defined in the spec. +supplemental_test_data = load_test_data("test/testdata/string_ext_supplemental.textproto") + +# Combine the test data from both files into one +sections = cel_test_data.section +sections.extend(supplemental_test_data.section) + +# Find the format tests which test successful formatting +_format_tests: Iterable[simple_pb2.SimpleTest] = chain.from_iterable(x.test for x in sections if x.name == "format") +# Find the format error tests which test errors during formatting +_format_error_tests: Iterable[simple_pb2.SimpleTest] = chain.from_iterable( + x.test for x in sections if x.name == "format_errors" +) + +env = celpy.Environment(runner_class=InterpretedRunner) + + +@pytest.mark.parametrize("format_test", _format_tests) +def test_format_successes(format_test): + """Tests success scenarios for string.format""" + if format_test.name in skipped_tests: + pytest.skip(f"skipped test: {format_test.name}") + ast = env.compile(format_test.expr) + prog = env.program(ast, functions=extra_func.make_extra_funcs()) + + bindings = build_variables(format_test.bindings) + result = prog.evaluate(bindings) + expected = get_expected_result(format_test) + assert expected is not None, f"[{format_test.name}]: expected a success result to be defined" + assert result == expected + + +@pytest.mark.parametrize("format_error_test", _format_error_tests) +def test_format_errors(format_error_test): + """Tests error scenarios for string.format""" + if format_error_test.name in skipped_error_tests: + pytest.skip(f"skipped test: {format_error_test.name}") + ast = env.compile(format_error_test.expr) + prog = env.program(ast, functions=extra_func.make_extra_funcs()) + + bindings = build_variables(format_error_test.bindings) + try: + prog.evaluate(bindings) + pytest.fail(f"[{format_error_test.name}]: expected an error to be raised during evaluation") + except celpy.CELEvalError as e: + msg = get_eval_error_message(format_error_test) + assert msg is not None, f"[{format_error_test.name}]: expected an eval error to be defined" + assert str(e) == msg diff --git a/test/test_matches.py b/test/test_matches.py index 6c0b3641..69356c17 100644 --- a/test/test_matches.py +++ b/test/test_matches.py @@ -12,18 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - import celpy from celpy import celtypes from protovalidate.internal.extra_func import cel_matches -class TestCollectViolations(unittest.TestCase): - def test_function_matches_re2(self): - empty_string = celtypes.StringType("") - # \z is valid re2 syntax for end of text - self.assertTrue(cel_matches(empty_string, "^\\z")) - # \Z is invalid re2 syntax - self.assertIsInstance(cel_matches(empty_string, "^\\Z"), celpy.CELEvalError) +def test_function_matches_re2(): + empty_string = celtypes.StringType("") + # \z is valid re2 syntax for end of text + assert cel_matches(empty_string, "^\\z") + # \Z is invalid re2 syntax + assert isinstance(cel_matches(empty_string, "^\\Z"), celpy.CELEvalError) diff --git a/test/test_validate.py b/test/test_validate.py index c0589692..6ffe9f4a 100644 --- a/test/test_validate.py +++ b/test/test_validate.py @@ -12,249 +12,235 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - +import pytest from google.protobuf import message import protovalidate from gen.tests.example.v1 import validations_pb2 from protovalidate.internal import rules +validators: list[protovalidate.Validator] = [ + protovalidate, # global module singleton + protovalidate.Validator(), # via constructor +] -def get_default_validator(): - """Returns a default validator created in all available ways - This allows testing for validators created via: - - module-level singleton - - instantiated class - """ - return [ - ("module singleton", protovalidate), - ("instantiated class", protovalidate.Validator()), - ] +@pytest.mark.parametrize("validator", validators) +def test_ninf(validator): + msg = validations_pb2.DoubleFinite() + msg.val = float("-inf") + expected_violation = rules.Violation() + expected_violation.proto.message = "value must be finite" + expected_violation.proto.rule_id = "double.finite" + expected_violation.field_value = msg.val + expected_violation.rule_value = True -class TestCollectViolations(unittest.TestCase): - """Test class for testing message validations. + check_invalid(validator, msg, [expected_violation]) - A validator can be created via various ways: - - a module-level singleton, which returns a default validator - - instantiating the Validator class - In addition, the API for validating a message allows for two approaches: - - via a call to `validate`, which will raise a ValidationError if validation fails - - via a call to `collect_violations`, which will not raise an error and instead return a list of violations. +@pytest.mark.parametrize("validator", validators) +def test_map_key(validator): + msg = validations_pb2.MapKeys() + msg.val[1] = "a" - Unless otherwise noted, each test in this class tests against a validator created via all 3 methods and tests - validation using both approaches. - """ + expected_violation = rules.Violation() + expected_violation.proto.message = "value must be less than 0" + expected_violation.proto.rule_id = "sint64.lt" + expected_violation.proto.for_key = True + expected_violation.field_value = 1 + expected_violation.rule_value = 0 + + check_invalid(validator, msg, [expected_violation]) + + +@pytest.mark.parametrize("validator", validators) +def test_sfixed64_valid(validator): + msg = validations_pb2.SFixed64ExLTGT(val=11) + + check_valid(validator, msg) + + +@pytest.mark.parametrize("validator", validators) +def test_oneofs(validator): + msg = validations_pb2.Oneof() + msg.y = 123 + + check_valid(validator, msg) + + +@pytest.mark.parametrize("validator", validators) +def test_protovalidate_oneof_valid(validator): + msg = validations_pb2.ProtovalidateOneof() + msg.a = "A" + + check_valid(validator, msg) + + +@pytest.mark.parametrize("validator", validators) +def test_protovalidate_oneof_violation(validator): + msg = validations_pb2.ProtovalidateOneof() + msg.a = "A" + msg.b = "B" + + expected_violation = rules.Violation() + expected_violation.proto.message = "only one of a, b can be set" + expected_violation.proto.rule_id = "message.oneof" + + check_invalid(validator, msg, [expected_violation]) - def test_ninf(self): - msg = validations_pb2.DoubleFinite() - msg.val = float("-inf") - expected_violation = rules.Violation() - expected_violation.proto.message = "value must be finite" - expected_violation.proto.rule_id = "double.finite" - expected_violation.field_value = msg.val - expected_violation.rule_value = True +@pytest.mark.parametrize("validator", validators) +def test_protovalidate_oneof_required_violation(validator): + msg = validations_pb2.ProtovalidateOneofRequired() - self._run_invalid_tests(msg, [expected_violation]) + expected_violation = rules.Violation() + expected_violation.proto.message = "one of a, b must be set" + expected_violation.proto.rule_id = "message.oneof" - def test_map_key(self): - msg = validations_pb2.MapKeys() - msg.val[1] = "a" + check_invalid(validator, msg, [expected_violation]) - expected_violation = rules.Violation() - expected_violation.proto.message = "value must be less than 0" - expected_violation.proto.rule_id = "sint64.lt" - expected_violation.proto.for_key = True - expected_violation.field_value = 1 - expected_violation.rule_value = 0 - self._run_invalid_tests(msg, [expected_violation]) +@pytest.mark.parametrize("validator", validators) +def test_protovalidate_oneof_unknown_field_name(validator): + """Tests that a compilation error is thrown when specifying a oneof rule with an invalid field name""" + msg = validations_pb2.ProtovalidateOneofUnknownFieldName() - def test_sfixed64_valid(self): - msg = validations_pb2.SFixed64ExLTGT(val=11) + check_compilation_errors( + validator, msg, 'field "xxx" not found in message tests.example.v1.ProtovalidateOneofUnknownFieldName' + ) - self._run_valid_tests(msg) - def test_oneofs(self): - msg = validations_pb2.Oneof() - msg.y = 123 +@pytest.mark.parametrize("validator", validators) +def test_repeated(validator): + msg = validations_pb2.RepeatedEmbedSkip() + msg.val.add(val=-1) - self._run_valid_tests(msg) + check_valid(validator, msg) - def test_protovalidate_oneof_valid(self): - msg = validations_pb2.ProtovalidateOneof() - msg.a = "A" - self._run_valid_tests(msg) +@pytest.mark.parametrize("validator", validators) +def test_maps(validator): + msg = validations_pb2.MapMinMax() - def test_protovalidate_oneof_violation(self): - msg = validations_pb2.ProtovalidateOneof() - msg.a = "A" - msg.b = "B" + expected_violation = rules.Violation() + expected_violation.proto.message = "map must be at least 2 entries" + expected_violation.proto.rule_id = "map.min_pairs" + expected_violation.field_value = {} + expected_violation.rule_value = 2 - expected_violation = rules.Violation() - expected_violation.proto.message = "only one of a, b can be set" - expected_violation.proto.rule_id = "message.oneof" + check_invalid(validator, msg, [expected_violation]) - self._run_invalid_tests(msg, [expected_violation]) - def test_protovalidate_oneof_required_violation(self): - msg = validations_pb2.ProtovalidateOneofRequired() +@pytest.mark.parametrize("validator", validators) +def test_timestamp(validator): + msg = validations_pb2.TimestampGTNow() - expected_violation = rules.Violation() - expected_violation.proto.message = "one of a, b must be set" - expected_violation.proto.rule_id = "message.oneof" + check_valid(validator, msg) - self._run_invalid_tests(msg, [expected_violation]) - def test_protovalidate_oneof_unknown_field_name(self): - """Tests that a compilation error is thrown when specifying a oneof rule with an invalid field name""" - msg = validations_pb2.ProtovalidateOneofUnknownFieldName() +@pytest.mark.parametrize("validator", validators) +def test_multiple_validations(validator): + """Test that a message with multiple violations correctly returns all of them.""" + msg = validations_pb2.MultipleValidations() + msg.title = "bar" + msg.name = "blah" - self._run_compilation_error_tests( - msg, 'field "xxx" not found in message tests.example.v1.ProtovalidateOneofUnknownFieldName' - ) + expected_violation1 = rules.Violation() + expected_violation1.proto.message = "value does not have prefix `foo`" + expected_violation1.proto.rule_id = "string.prefix" + expected_violation1.field_value = msg.title + expected_violation1.rule_value = "foo" - def test_repeated(self): - msg = validations_pb2.RepeatedEmbedSkip() - msg.val.add(val=-1) + expected_violation2 = rules.Violation() + expected_violation2.proto.message = "value length must be at least 5 characters" + expected_violation2.proto.rule_id = "string.min_len" + expected_violation2.field_value = msg.name + expected_violation2.rule_value = 5 - self._run_valid_tests(msg) - - def test_maps(self): - msg = validations_pb2.MapMinMax() - - expected_violation = rules.Violation() - expected_violation.proto.message = "map must be at least 2 entries" - expected_violation.proto.rule_id = "map.min_pairs" - expected_violation.field_value = {} - expected_violation.rule_value = 2 + check_invalid(validator, msg, [expected_violation1, expected_violation2]) - self._run_invalid_tests(msg, [expected_violation]) - - def test_timestamp(self): - msg = validations_pb2.TimestampGTNow() - - self._run_valid_tests(msg) - - def test_multiple_validations(self): - """Test that a message with multiple violations correctly returns all of them.""" - msg = validations_pb2.MultipleValidations() - msg.title = "bar" - msg.name = "blah" - - expected_violation1 = rules.Violation() - expected_violation1.proto.message = "value does not have prefix `foo`" - expected_violation1.proto.rule_id = "string.prefix" - expected_violation1.field_value = msg.title - expected_violation1.rule_value = "foo" - - expected_violation2 = rules.Violation() - expected_violation2.proto.message = "value length must be at least 5 characters" - expected_violation2.proto.rule_id = "string.min_len" - expected_violation2.field_value = msg.name - expected_violation2.rule_value = 5 - - self._run_invalid_tests(msg, [expected_violation1, expected_violation2]) - - def test_concatenated_values(self): - msg = validations_pb2.ConcatenatedValues( - bar=["a", "b", "c"], - baz=["d", "e", "f"], - ) - - self._run_valid_tests(msg) - - def test_fail_fast(self): - """Test that fail fast correctly fails on first violation""" - msg = validations_pb2.MultipleValidations() - msg.title = "bar" - msg.name = "blah" - - expected_violation = rules.Violation() - expected_violation.proto.message = "value does not have prefix `foo`" - expected_violation.proto.rule_id = "string.prefix" - expected_violation.field_value = msg.title - expected_violation.rule_value = "foo" - - validator = protovalidate.Validator() - - # Test validate - with self.assertRaises(protovalidate.ValidationError) as cm: - validator.validate(msg, fail_fast=True) - e = cm.exception - self.assertEqual(str(e), f"invalid {msg.DESCRIPTOR.name}") - self._compare_violations(e.violations, [expected_violation]) - - # Test collect_violations - violations = validator.collect_violations(msg, fail_fast=True) - self._compare_violations(violations, [expected_violation]) - - def _run_valid_tests(self, msg: message.Message): - """A helper function for testing successful validation on a given message - - The tests are run using validators created via all possible methods and - validation is done via a call to `validate` as well as a call to `collect_violations`. - """ - for label, v in get_default_validator(): - with self.subTest(label=label): - # Test validate - try: - v.validate(msg) - except Exception: - self.fail(f"[{label}]: unexpected validation failure") - - # Test collect_violations - violations = v.collect_violations(msg) - self.assertEqual(len(violations), 0) - - def _run_invalid_tests(self, msg: message.Message, expected: list[rules.Violation]): - """A helper function for testing unsuccessful validation on a given message - - The tests are run using validators created via all possible methods and - validation is done via a call to `validate` as well as a call to `collect_violations`. - """ - for label, v in get_default_validator(): - with self.subTest(label=label): - # Test validate - with self.assertRaises(protovalidate.ValidationError) as cm: - v.validate(msg) - e = cm.exception - self.assertEqual(str(e), f"invalid {msg.DESCRIPTOR.name}") - self._compare_violations(e.violations, expected) - - # Test collect_violations - violations = v.collect_violations(msg) - self._compare_violations(violations, expected) - - def _run_compilation_error_tests(self, msg: message.Message, expected: str): - """A helper function for testing compilation errors when validating. - - The tests are run using validators created via all possible methods and - validation is done via a call to `validate` as well as a call to `collect_violations`. - """ - for label, v in get_default_validator(): - with self.subTest(label=label): - # Test validate - with self.assertRaises(protovalidate.CompilationError) as vce: - v.validate(msg) - self.assertEqual(str(vce.exception), expected) - - # Test collect_violations - with self.assertRaises(protovalidate.CompilationError) as cvce: - v.collect_violations(msg) - self.assertEqual(str(cvce.exception), expected) - - def _compare_violations(self, actual: list[rules.Violation], expected: list[rules.Violation]) -> None: - """Compares two lists of violations. The violations are expected to be in the expected order also.""" - self.assertEqual(len(actual), len(expected)) - for a, e in zip(actual, expected): - self.assertEqual(a.proto.message, e.proto.message) - self.assertEqual(a.proto.rule_id, e.proto.rule_id) - self.assertEqual(a.proto.for_key, e.proto.for_key) - self.assertEqual(a.field_value, e.field_value) - self.assertEqual(a.rule_value, e.rule_value) + +@pytest.mark.parametrize("validator", validators) +def test_concatenated_values(validator): + msg = validations_pb2.ConcatenatedValues( + bar=["a", "b", "c"], + baz=["d", "e", "f"], + ) + + check_valid(validator, msg) + + +@pytest.mark.parametrize("validator", validators) +def test_fail_fast(validator): + """Test that fail fast correctly fails on first violation""" + msg = validations_pb2.MultipleValidations() + msg.title = "bar" + msg.name = "blah" + + expected_violation = rules.Violation() + expected_violation.proto.message = "value does not have prefix `foo`" + expected_violation.proto.rule_id = "string.prefix" + expected_violation.field_value = msg.title + expected_violation.rule_value = "foo" + + # Test validate + with pytest.raises(protovalidate.ValidationError) as cm: + validator.validate(msg, fail_fast=True) + e = cm.value + assert str(e) == f"invalid {msg.DESCRIPTOR.name}" + _compare_violations(e.violations, [expected_violation]) # ty: ignore + + # Test collect_violations + violations = validator.collect_violations(msg, fail_fast=True) + _compare_violations(violations, [expected_violation]) + + +def check_valid(validator: protovalidate.Validator, msg: message.Message): + # Test validate + validator.validate(msg) + + # Test collect_violations + violations = validator.collect_violations(msg) + assert len(violations) == 0 + + +def check_invalid(validator: protovalidate.Validator, msg: message.Message, expected: list[rules.Violation]): + # Test validate + with pytest.raises(protovalidate.ValidationError) as exc_info: + validator.validate(msg) + e = exc_info.value + assert str(e) == f"invalid {msg.DESCRIPTOR.name}" + _compare_violations(e.violations, expected) # ty: ignore + + # Test collect_violations + violations = validator.collect_violations(msg) + _compare_violations(violations, expected) + + +def check_compilation_errors(validator: protovalidate.Validator, msg: message.Message, expected: str): + """A helper function for testing compilation errors when validating. + + The tests are run using validators created via all possible methods and + validation is done via a call to `validate` as well as a call to `collect_violations`. + """ + # Test validate + with pytest.raises(protovalidate.CompilationError) as vce: + validator.validate(msg) + assert str(vce.value) == expected + + # Test collect_violations + with pytest.raises(protovalidate.CompilationError) as cvce: + validator.collect_violations(msg) + assert str(cvce.value) == expected + + +def _compare_violations(actual: list[rules.Violation], expected: list[rules.Violation]): + """Compares two lists of violations. The violations are expected to be in the expected order also.""" + assert len(actual) == len(expected) + for a, e in zip(actual, expected): + assert a.proto.message == e.proto.message + assert a.proto.rule_id == e.proto.rule_id + assert a.proto.for_key == e.proto.for_key + assert a.field_value == e.field_value + assert a.rule_value == e.rule_value diff --git a/uv.lock b/uv.lock index 70816d10..7884f8cd 100644 --- a/uv.lock +++ b/uv.lock @@ -24,6 +24,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/28/08871462a0347b3e707658a8308be6f979167488a2196f93b402c2ea7170/cel_python-0.2.0-py3-none-any.whl", hash = "sha256:478ff73def7b39d51e6982f95d937a57c2b088c491c578fe5cecdbd79f476f60", size = 71337, upload-time = "2025-02-14T11:42:19.996Z" }, ] +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + [[package]] name = "google-re2" version = "1.1.20250805" @@ -97,6 +118,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/86/2134e8ff65a22e30883bfc38ac34c5c76fd88cc9cef02ff3561f05db2b68/google_re2_stubs-0.1.1-py3-none-any.whl", hash = "sha256:a82eb6c3accd20879d711cd38151583d8a154fcca755f43a5595143a484c8118", size = 3676, upload-time = "2024-10-21T15:22:43.08Z" }, ] +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + [[package]] name = "jmespath" version = "1.0.1" @@ -175,6 +205,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + [[package]] name = "pathspec" version = "0.12.1" @@ -184,6 +223,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + [[package]] name = "protobuf" version = "6.32.0" @@ -213,6 +261,7 @@ dependencies = [ dev = [ { name = "google-re2-stubs" }, { name = "mypy" }, + { name = "pytest" }, { name = "ruff" }, { name = "types-protobuf" }, ] @@ -230,10 +279,38 @@ requires-dist = [ dev = [ { name = "google-re2-stubs", specifier = ">=0.1.1" }, { name = "mypy", specifier = ">=1.17.1" }, + { name = "pytest", specifier = ">=8.4.1" }, { name = "ruff", specifier = ">=0.12.0" }, { name = "types-protobuf", specifier = ">=5.29.1.20250315" }, ] +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, +] + +[[package]] +name = "pytest" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/08/ba/45911d754e8eba3d5a841a5ce61a65a685ff1798421ac054f85aa8747dfb/pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c", size = 1517714, upload-time = "2025-06-18T05:48:06.109Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/29/16/c8a903f4c4dffe7a12843191437d7cd8e32751d5de349d45d3fe69544e87/pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7", size = 365474, upload-time = "2025-06-18T05:48:03.955Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0"