From 6a25ae336d5bca775c0c25114649041980ef880a Mon Sep 17 00:00:00 2001 From: p1c2u Date: Thu, 26 Feb 2026 14:21:27 +0000 Subject: [PATCH] Add optional ecma-regex backend for strict OpenAPI pattern validation --- .github/workflows/python-tests.yml | 40 ++++++++ README.rst | 14 ++- docs/validation.rst | 18 +++- openapi_schema_validator/_format.py | 12 +++ openapi_schema_validator/_keywords.py | 33 ++++++ openapi_schema_validator/_regex.py | 44 ++++++++ openapi_schema_validator/validators.py | 9 +- poetry.lock | 133 ++++++++++++++++++++++++- pyproject.toml | 6 ++ tests/integration/test_validators.py | 45 ++++++++- 10 files changed, 347 insertions(+), 7 deletions(-) create mode 100644 openapi_schema_validator/_regex.py diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index d5d1956..f00d4f8 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -62,6 +62,46 @@ jobs: - name: Upload coverage uses: codecov/codecov-action@v5 + tests_no_extras: + name: "py3.14 no extras" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Set up Python 3.14 + uses: actions/setup-python@v6 + with: + python-version: "3.14" + + - name: Get full Python version + id: full-python-version + run: echo ::set-output name=version::$(python -c "import sys; print('-'.join(str(v) for v in sys.version_info))") + + - name: Set up poetry + uses: Gr1N/setup-poetry@v9 + + - name: Configure poetry + run: poetry config virtualenvs.in-project true + + - name: Set up cache + uses: actions/cache@v5 + id: cache + with: + path: .venv + key: venv-no-extras-${{ steps.full-python-version.outputs.version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Ensure cache is healthy + if: steps.cache.outputs.cache-hit == 'true' + run: timeout 10s poetry run pip --version || rm -rf .venv + + - name: Install dependencies + run: poetry install + + - name: Test fallback regex behavior + env: + PYTEST_ADDOPTS: "--color=yes" + run: poetry run pytest tests/integration/test_validators.py -k pattern + static_checks: name: "Static checks" runs-on: ubuntu-latest diff --git a/README.rst b/README.rst index 42d2af3..499c0cd 100644 --- a/README.rst +++ b/README.rst @@ -127,7 +127,6 @@ unresolved-metaschema fallback warnings. assert validator_for(schema) is OAS31Validator assert validator_for(schema32) is OAS32Validator - Binary Data Semantics ===================== @@ -189,6 +188,19 @@ Quick Reference - Fail - Same semantics as OAS 3.1 + +Regex Behavior +============== + +By default, ``pattern`` handling follows host Python regex behavior. +For ECMAScript-oriented regex validation and matching (via ``regress``), +install the optional extra: + +.. code-block:: console + + pip install "openapi-schema-validator[ecma-regex]" + + For more details read about `Validation `__. diff --git a/docs/validation.rst b/docs/validation.rst index 23fd1f1..9ee8722 100644 --- a/docs/validation.rst +++ b/docs/validation.rst @@ -138,7 +138,8 @@ Malformed schema values (for example an invalid regex in ``pattern``) raise If you instantiate a validator class directly and call ``.validate(...)``, schema checking is not performed automatically, matching ``jsonschema`` validator-class behavior. -For malformed regex patterns this may raise a lower-level regex error. +For malformed regex patterns this may raise a lower-level regex error +(default mode) or ``ValidationError`` from the validator (ECMAScript mode). Use ``.check_schema(schema)`` first when you need deterministic schema-validation errors with direct validator usage. @@ -245,6 +246,21 @@ Quick Reference - Fail - Same semantics as OAS 3.1 +Regex Behavior +-------------- + +Pattern validation follows one of two modes: + +- default installation: follows host Python regex behavior +- ``ecma-regex`` extra installed: uses ``regress`` for ECMAScript-oriented + regex validation and matching + +Install optional ECMAScript regex support with: + +.. code-block:: console + + pip install "openapi-schema-validator[ecma-regex]" + Example usage: .. code-block:: python diff --git a/openapi_schema_validator/_format.py b/openapi_schema_validator/_format.py index 92b4f61..a4544d1 100644 --- a/openapi_schema_validator/_format.py +++ b/openapi_schema_validator/_format.py @@ -5,6 +5,8 @@ from jsonschema._format import FormatChecker +from openapi_schema_validator._regex import is_valid_regex + def is_int32(instance: object) -> bool: # bool inherits from int, so ensure bools aren't reported as ints @@ -82,6 +84,12 @@ def is_password(instance: object) -> bool: return True +def is_regex(instance: object) -> bool: + if not isinstance(instance, str): + return True + return is_valid_regex(instance) + + oas30_format_checker = FormatChecker() oas30_format_checker.checks("int32")(is_int32) oas30_format_checker.checks("int64")(is_int64) @@ -90,6 +98,7 @@ def is_password(instance: object) -> bool: oas30_format_checker.checks("binary")(is_binary_pragmatic) oas30_format_checker.checks("byte", (binascii.Error, TypeError))(is_byte) oas30_format_checker.checks("password")(is_password) +oas30_format_checker.checks("regex")(is_regex) oas30_strict_format_checker = FormatChecker() oas30_strict_format_checker.checks("int32")(is_int32) @@ -101,6 +110,7 @@ def is_password(instance: object) -> bool: is_byte ) oas30_strict_format_checker.checks("password")(is_password) +oas30_strict_format_checker.checks("regex")(is_regex) oas31_format_checker = FormatChecker() oas31_format_checker.checks("int32")(is_int32) @@ -108,6 +118,7 @@ def is_password(instance: object) -> bool: oas31_format_checker.checks("float")(is_float) oas31_format_checker.checks("double")(is_double) oas31_format_checker.checks("password")(is_password) +oas31_format_checker.checks("regex")(is_regex) # OAS 3.2 uses the same format checks as OAS 3.1 oas32_format_checker = FormatChecker() @@ -116,3 +127,4 @@ def is_password(instance: object) -> bool: oas32_format_checker.checks("float")(is_float) oas32_format_checker.checks("double")(is_double) oas32_format_checker.checks("password")(is_password) +oas32_format_checker.checks("regex")(is_regex) diff --git a/openapi_schema_validator/_keywords.py b/openapi_schema_validator/_keywords.py index 144d4d5..7ebe467 100644 --- a/openapi_schema_validator/_keywords.py +++ b/openapi_schema_validator/_keywords.py @@ -6,12 +6,17 @@ from jsonschema._keywords import allOf as _allOf from jsonschema._keywords import anyOf as _anyOf from jsonschema._keywords import oneOf as _oneOf +from jsonschema._keywords import pattern as _pattern from jsonschema._utils import extras_msg from jsonschema._utils import find_additional_properties from jsonschema.exceptions import FormatError from jsonschema.exceptions import ValidationError from jsonschema.exceptions import _WrappedReferencingError +from openapi_schema_validator._regex import ECMARegexSyntaxError +from openapi_schema_validator._regex import has_ecma_regex +from openapi_schema_validator._regex import search as regex_search + def handle_discriminator( validator: Any, _: Any, instance: Any, schema: Mapping[str, Any] @@ -159,6 +164,34 @@ def strict_type( yield ValidationError(f"{instance!r} is not of type {data_repr}") +def pattern( + validator: Any, + patrn: str, + instance: Any, + schema: Mapping[str, Any], +) -> Iterator[ValidationError]: + if not has_ecma_regex(): + yield from cast( + Iterator[ValidationError], + _pattern(validator, patrn, instance, schema), + ) + return + + if not validator.is_type(instance, "string"): + return + + try: + matches = regex_search(patrn, instance) + except ECMARegexSyntaxError as exc: + yield ValidationError( + f"{patrn!r} is not a valid regular expression ({exc})" + ) + return + + if not matches: + yield ValidationError(f"{instance!r} does not match {patrn!r}") + + def format( validator: Any, format: str, diff --git a/openapi_schema_validator/_regex.py b/openapi_schema_validator/_regex.py new file mode 100644 index 0000000..df50e02 --- /dev/null +++ b/openapi_schema_validator/_regex.py @@ -0,0 +1,44 @@ +import re +from typing import Any + +_REGEX_CLASS: Any = None +_REGRESS_ERROR: type[Exception] = Exception + +try: + from regress import Regex as _REGEX_CLASS + from regress import RegressError as _REGRESS_ERROR +except ImportError: # pragma: no cover - optional dependency + pass + + +class ECMARegexSyntaxError(ValueError): + pass + + +def has_ecma_regex() -> bool: + return _REGEX_CLASS is not None + + +def is_valid_regex(pattern: str) -> bool: + if _REGEX_CLASS is None: + try: + re.compile(pattern) + except re.error: + return False + return True + + try: + _REGEX_CLASS(pattern) + except _REGRESS_ERROR: + return False + return True + + +def search(pattern: str, instance: str) -> bool: + if _REGEX_CLASS is None: + return re.search(pattern, instance) is not None + + try: + return _REGEX_CLASS(pattern).find(instance) is not None + except _REGRESS_ERROR as exc: + raise ECMARegexSyntaxError(str(exc)) from exc diff --git a/openapi_schema_validator/validators.py b/openapi_schema_validator/validators.py index 6d82006..e6be03b 100644 --- a/openapi_schema_validator/validators.py +++ b/openapi_schema_validator/validators.py @@ -62,7 +62,7 @@ def _oas30_id_of(schema: Any) -> str: "minimum": _legacy_keywords.minimum_draft3_draft4, "maxLength": _keywords.maxLength, "minLength": _keywords.minLength, - "pattern": _keywords.pattern, + "pattern": oas_keywords.pattern, "maxItems": _keywords.maxItems, "minItems": _keywords.minItems, "uniqueItems": _keywords.uniqueItems, @@ -118,6 +118,7 @@ def _build_oas31_validator() -> Any: "allOf": oas_keywords.allOf, "oneOf": oas_keywords.oneOf, "anyOf": oas_keywords.anyOf, + "pattern": oas_keywords.pattern, "description": oas_keywords.not_implemented, # fixed OAS fields "discriminator": oas_keywords.not_implemented, @@ -180,5 +181,11 @@ def _build_oas32_validator() -> Any: OAS31Validator = _build_oas31_validator() OAS32Validator = _build_oas32_validator() +# These validator classes are generated via jsonschema create/extend, so there +# is no simpler hook to inject registry-aware schema checking while preserving +# each class's FORMAT_CHECKER. Override check_schema on each class to keep +# OpenAPI metaschema resolution local and to apply optional ecma-regex +# behavior consistently across OAS 3.0/3.1/3.2. +OAS30Validator.check_schema = classmethod(check_openapi_schema) OAS31Validator.check_schema = classmethod(check_openapi_schema) OAS32Validator.check_schema = classmethod(check_openapi_schema) diff --git a/poetry.lock b/poetry.lock index c811dfd..6c5964b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1506,6 +1506,136 @@ attrs = ">=22.2.0" rpds-py = ">=0.7.0" typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} +[[package]] +name = "regress" +version = "2025.10.1" +description = "Python bindings to Rust's regress ECMA regular expressions library" +optional = true +python-versions = ">=3.10" +groups = ["main"] +markers = "extra == \"ecma-regex\"" +files = [ + {file = "regress-2025.10.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:590abc9fa10255dcf84b4469f08cad9787001c38600080a033cd7a71f51cae02"}, + {file = "regress-2025.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc9046f25971e9d6dc3b57028de8991d0d7c346efcb0c15acbfecbfb8e4c1813"}, + {file = "regress-2025.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81733f0e7f583d181bc9fc187b3d766489bcf7e0c85eff26f1e067c942e4e45a"}, + {file = "regress-2025.10.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d48ae257483ff7da43c15b8071b132a1e75e4be2242a44e2587b8492afae32e"}, + {file = "regress-2025.10.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30e70b966015beef7e9287feaf37ed9606744ceaa88be24b10824f5b4c354498"}, + {file = "regress-2025.10.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6c1c968ee4dca933e0cd6cea51d9c6494c3d82650ce826e7a5d007c9de720da2"}, + {file = "regress-2025.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:212fe8c4f823730d1578d0fe8b5edf21b393074b90a8ec704d6d6c1c96ffe7dc"}, + {file = "regress-2025.10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cb23c4ab28e75e35f033e4e392f01f6a9344a961e2f4ded56af5520b5841fe8d"}, + {file = "regress-2025.10.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fdcd839a87fae1e47bf376248589ebcb2e58d3a0fad66837d69856ae99ef3c93"}, + {file = "regress-2025.10.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1809ac971207bb2e098a46e08c2f241a259ac3bda48228f295a3c2cba49c6c0e"}, + {file = "regress-2025.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cbf1371ae2cfe1a2d6e1bb21f73a557138bc45347021f1701f196b484789639a"}, + {file = "regress-2025.10.1-cp310-cp310-win32.whl", hash = "sha256:8a9c00ede347a5a431e36d4fcf75e2ec9094feac2f6170f2ffbd5e32f7f7a4b4"}, + {file = "regress-2025.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:cba77808a756f1f117916c0afb0e79f01be1cb46ee51c77780e8afc59ddac76b"}, + {file = "regress-2025.10.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:02da99e41c6a97f6d2146d326541b4035ed2139c92b2f56ca7e464ceb84fe24f"}, + {file = "regress-2025.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5d961e81b169fce4f6c0e35f52bbfca9e20abf9674dd391c75708f0665ef4f6f"}, + {file = "regress-2025.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34b13e7785554997e18725b896d404ba992cee5b691768476f14b48de0c393a"}, + {file = "regress-2025.10.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15efcb8d568e712919cb56b78fffe08a09d5ed1844b43cccc3235c2da90d0e59"}, + {file = "regress-2025.10.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a2ddb0d1c0821b70dd50daa773c5f3fbb2155398c57809a2f54447958c9569f"}, + {file = "regress-2025.10.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c78c6cf679da47fbde85b75407fdfb4642477315ee94d6cdd62a0606941b83"}, + {file = "regress-2025.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707e2846e784ecef666a6892753cf5d8441e4cf02bcc7fa10fd21527429815fc"}, + {file = "regress-2025.10.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5c147e4d3799022bac9fc49fd042a51b7f746c41ed231fa6b496720dab0d2f9d"}, + {file = "regress-2025.10.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:148f15530807a63e24ca2dea3795546723d84c0b3a8e4152a24b58318f841b3c"}, + {file = "regress-2025.10.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9670d957d156fa90e5582c4337ca1757b643859780896c8d853b015cd01456dc"}, + {file = "regress-2025.10.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:af290cfb3b7d4b14319a88feab4e86f54a2c43bd8189be5a0dcd8f847f832847"}, + {file = "regress-2025.10.1-cp311-cp311-win32.whl", hash = "sha256:7343ef7eae795e1449308d6d05131195f5af31ab1b2b6b405ff9370b64c1e7e1"}, + {file = "regress-2025.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:8f42578d920fc878fe19ed8e2fbe38edd212c451bb2fc5e0084716d5acd26c4c"}, + {file = "regress-2025.10.1-cp311-cp311-win_arm64.whl", hash = "sha256:a6f0320f53e8fd8722211c0620fcd9bfecc0db05bf0d59385ee301f86b815671"}, + {file = "regress-2025.10.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:45695cd7ddc6a919863f243a09a9e737257f958c0d2af0e71e349c9d0f3048ad"}, + {file = "regress-2025.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:63503a8f601a10e5d9d72ea6efb415a2838a4766736775322578ce5fe18cb233"}, + {file = "regress-2025.10.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28941c80252119ef051ad67195fa8d155a0c8dc9ecb801786d94eeea6738e4c3"}, + {file = "regress-2025.10.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d26daee7d46905c8d4232f44c39ea788f10d39c166735177f603783b4ace4e8"}, + {file = "regress-2025.10.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f0599f9cb12722300b6d4fcd6bd9b2be5bf233bc567c3ca503d8e392de23798a"}, + {file = "regress-2025.10.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:46b92e1ec6092e6e989e4ad5f52f0a358e88355f70cf4dba9abce84f3cd513cf"}, + {file = "regress-2025.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ac0e197c7f1b5ffca42341518b6a03e2ea3cdd66516af9278492d2d2bfc9ce2"}, + {file = "regress-2025.10.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05e6c68021dff89fee7bcdab25e0819cff4c7f0761dc41f0fb609b0e9ebb6272"}, + {file = "regress-2025.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:39600db4e404155168d6f75c3dbb2ae03ef3e288b4a57c7a6562a1144bf07682"}, + {file = "regress-2025.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d48e91d14a366570685f76adaf9d108e3abc5522572e7e1d0d78c3cc3ebf0833"}, + {file = "regress-2025.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce4e38ada2a39159d8a2523084e2b5ed94b3f7f7b196ba6b99c2d6c4ff634a87"}, + {file = "regress-2025.10.1-cp312-cp312-win32.whl", hash = "sha256:c35c8cd42900d9195e5bf48d701dda28c204854fba7cc27d18f309745f57b8b5"}, + {file = "regress-2025.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:089b6c5f0965962e8046493e5a1222a24e88d6f235fa8fe2424ec869e6ff613c"}, + {file = "regress-2025.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:469b03b1deffb6d20ea4f95aa44ef0e0e5a9b47d56f709276b06a96338b570a0"}, + {file = "regress-2025.10.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:9100da34f69e18ad6d545f5d74f8ee729e42ce200a73752dd6d94f8a373d0e71"}, + {file = "regress-2025.10.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9e25096697b034848d8fc7910cdb38b7abc2bd2d7ade8767893359918ed4efd0"}, + {file = "regress-2025.10.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4c40a8c9f3e0119d2384a52b55dcc770461e1ead6ae7b41314999223116a15"}, + {file = "regress-2025.10.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25d8518285aa3ce67ddbede90a1a9ca6a5d23a1b8275dd5a9722af0c64b37b2a"}, + {file = "regress-2025.10.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8051fac3696730bb84d7675c62c7073792a0e105233d4f8e1055f2cab9b04fce"}, + {file = "regress-2025.10.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8e337ce3b150ca0ff599b1150b995ff6a2e32b5940e17ac3f30a29133960709e"}, + {file = "regress-2025.10.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:290a15652c3fafe387db02061d4ed9a100804ed5a162d069b5a0ac28b5df162a"}, + {file = "regress-2025.10.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b4bc2008b59e5124c1672d7fd9f203e5d4a4ff88ebaf4666e3281141e2d8db20"}, + {file = "regress-2025.10.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:56ef2f8ef9a102a7d42cbbc2f3c0c5e1b186bd8eaa78d564da566b0bf20653dc"}, + {file = "regress-2025.10.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0680ac0b0a0058acb55b64aa56956732cf56b0baa84ea95e3fa124ab16da58aa"}, + {file = "regress-2025.10.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:47c03bfd853651241fc11436fb14ef7c9b312bb9a9c2828aa5c93943e945f1ef"}, + {file = "regress-2025.10.1-cp313-cp313-win32.whl", hash = "sha256:3d12e6a834ed5f6d9dc7e86ea8fd77d37bb13900129930151d87c3261c3a799e"}, + {file = "regress-2025.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:ee3b7325ea849097d674020c72b2e6deb8f1018f085a7ecbd22831cd217b7f80"}, + {file = "regress-2025.10.1-cp313-cp313-win_arm64.whl", hash = "sha256:e5f35e04fe6382c236d60c98b3f0a4a22dea75398b99c9c9bf3fb9d386cd7ebb"}, + {file = "regress-2025.10.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:0ee12c4e1c4e6e3609f41dd58065bc241945e912772f7320238d6544f0745950"}, + {file = "regress-2025.10.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:389c2278848cbfed81753a04ce8ea6c037271179cb9ef4decac7d3c65ae3330b"}, + {file = "regress-2025.10.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf743b00cac20f8e8d271f275df7bc192ebb4faa7aee8fe98484df338786dec6"}, + {file = "regress-2025.10.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5541da1f581377bc20d2f77e017453aa8f2c2f4bfe7679dee00e139ec700abe8"}, + {file = "regress-2025.10.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a67216efa72bb27db170f637d67eb9acdc167da5d617163b057803de7aed1e6d"}, + {file = "regress-2025.10.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fb61e653a325aea4681cf7b96ba9bbabc1aeb3f3d8fe877a07800024907398b9"}, + {file = "regress-2025.10.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2a0a7da1d42fdff8068ab976a66beea572621514a130b37592489ae134a0e27"}, + {file = "regress-2025.10.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:337ef62f785dc6e05c4a09be7a902980cf4d4f15346f4eca9eaf02745485440c"}, + {file = "regress-2025.10.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:aaf5d102e05b109dde13363e705969b3ffdbbfeb880270187eed0871e75f0c8f"}, + {file = "regress-2025.10.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:adf331c8938e0d8705fc5d05d08fab09c11cb4bcdf8a64fa21902972c4cd38f4"}, + {file = "regress-2025.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:287f86b5c0bf3bc9c0abd45bf6745ba9c6a5624c3132b07631bac4403b45143f"}, + {file = "regress-2025.10.1-cp313-cp313t-win32.whl", hash = "sha256:877e05e7c570ee1e077e8b587cca8a318b7675f3c94c6c4e25d0d145abf7c0b6"}, + {file = "regress-2025.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:97307f87b128389d8b3f385c8e431fc318263281d1a1c0394606bc813aea05fc"}, + {file = "regress-2025.10.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:56123dbe783a2bab04d1a1850605c483d40f36196bc52d249aa245d06f866f78"}, + {file = "regress-2025.10.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8405a31f0a1475e1c9aa20c4d6e1465ef2f7259581c018a2e273083494ca9a61"}, + {file = "regress-2025.10.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12430b2c7263a7b359d30dd0f2b179426d489df30da78cd21023376eb2fe2682"}, + {file = "regress-2025.10.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7ecba827aff7f951db40be777c32608a1b16bbeb7f02fcc97a2e9fc6702641f"}, + {file = "regress-2025.10.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:293be370961c6887efb82e466a15523ff24a702d444f44917ce318b222ffd229"}, + {file = "regress-2025.10.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:783b9c50760aab988e4d60dcb7c54eba3fe730d80f9a877f1dc52d14263f86b1"}, + {file = "regress-2025.10.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ed3b4d0df960aeccb685f8de5001c19f426f1ab09fde715e5abdbf9c59b26a"}, + {file = "regress-2025.10.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:08e99c44e4c3860352400af96b25a4ebb673c16d53c6367153631ec77d5130b8"}, + {file = "regress-2025.10.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6b52096ecbf39f50756e51efa9286f47c598572f6b8bb2119f855de817f38b8e"}, + {file = "regress-2025.10.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:b99a73cfbdc5d99681aeb3aeaa2d88369023c96648cc785433d6a92c8d3a8394"}, + {file = "regress-2025.10.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:05d02b4d7179b85acf28d7329a488901d6baef5f5b337dcd52f53ea0ff980bc3"}, + {file = "regress-2025.10.1-cp314-cp314-win32.whl", hash = "sha256:e7cc153fabb47b6f8dfc2903186934e07aa57ee1debe9b3569ac4779b43708ae"}, + {file = "regress-2025.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:102c4627f026db8d361ab61155e0f1093176555d60ddb1cc4c9b6f5bbe255c1f"}, + {file = "regress-2025.10.1-cp314-cp314-win_arm64.whl", hash = "sha256:e5c441a6017a5a29bb38c573892d882485cc26937cb1ee12da8593723bf6c041"}, + {file = "regress-2025.10.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:77d63b338c2a4e56b4f05632d3fd94061ad47ee2b272b158b9c2e09545a4c6bb"}, + {file = "regress-2025.10.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:722c408a3bc92b4904005e68244c28fa6df943290df8d670faf349414c86aabb"}, + {file = "regress-2025.10.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3afe24e6474f5dbc448f865641c29bcbed4eb3b87ac9eb0e6755c4eff4f7111"}, + {file = "regress-2025.10.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9fdbbea49bf2fe65f7272b0316e1343aff1ccbb85b58fab325778c416d648ed9"}, + {file = "regress-2025.10.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d451a88292c5a93f57cf754c71b3aa8b570ed159f9fd481554948c467e6105d"}, + {file = "regress-2025.10.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a97649f21c875b7e95e10a59f0f487a518735f51a7aec7fc95fb2c3d9a3914d5"}, + {file = "regress-2025.10.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a542e38c8bb95f618674a5d2d248f1010547d7ff2e46a6cf4fa4b851459ba440"}, + {file = "regress-2025.10.1-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:933561ea90b2ac9a826e956a994b8a66635cd96467374281da992ceea8b0de4c"}, + {file = "regress-2025.10.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b6b5aa9f9408fdf260c73071282a28d29efbb4c30a4b95ab29863bea31987621"}, + {file = "regress-2025.10.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:afee501f000666afe18531132edcaf0dc0178dd591cccf5b9596563e7456c118"}, + {file = "regress-2025.10.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4d0bf23a6d996655ed88c822bb0123cc2e92a1df95079ce7408552c35ec05d47"}, + {file = "regress-2025.10.1-cp314-cp314t-win32.whl", hash = "sha256:adaa80c97927d623ff72b920bcc637568f124eabe84559c7927a91253ff55d5e"}, + {file = "regress-2025.10.1-cp314-cp314t-win_amd64.whl", hash = "sha256:6553c8ba57fa92ab3e9ef5c811d6214c80131bba06496bd5920e6e5a3d53ca8e"}, + {file = "regress-2025.10.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5254f758206ba45776aefbd3c4223898890209b4dd3a743f2dc5cd1ebc5e9fd"}, + {file = "regress-2025.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:966dcad04fe1821c7af9bf6dd33bbeca5c2649b7a3c5b2af57700e6d93335fd4"}, + {file = "regress-2025.10.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d725e10a99ff65b0ad83b8c20f05645d9c67dbc809e2bf3b1db230d3e15081b"}, + {file = "regress-2025.10.1-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3605ef64ab64658856ce8d6fda730dfb62c01002e2283bd6400df8e912d1b56a"}, + {file = "regress-2025.10.1-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:daece9f2fbaaadd23ef2cb31ffbaecd74f946938ff9b70d22c891c66d1435ad2"}, + {file = "regress-2025.10.1-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1460d95d39d956ba0fab8a7b614b7ee5486473b1b210f65d6a3043dc08462f38"}, + {file = "regress-2025.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac04243bf4ba86196b2491bdacd9450b339b3e5e97192aab82234baac1f0a74f"}, + {file = "regress-2025.10.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3962e4b980bfb844518beb1a3afce069674377ba99189fe339918fe7e7cbb7a"}, + {file = "regress-2025.10.1-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:f418c11a6bae820fad1334e0569336e2e7848cf4b81e503bfb038daf0e57ec12"}, + {file = "regress-2025.10.1-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:19b875513ebecc2b37a125d5794ce91fb4c203b06cb187e15d13b2a761df4621"}, + {file = "regress-2025.10.1-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:415dd30885dfdb57f281f7c284e4495e85a90aa66965e3f10cb1bd862f40c2e4"}, + {file = "regress-2025.10.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:faa057895de8e41301f9367b286bb3fd0cd80bc8523c8c80866fe746a33d13b6"}, + {file = "regress-2025.10.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:de16665fcc008db88729f52bab92e36d0c0534bcb3f334ff6d2c7574b16a3d6d"}, + {file = "regress-2025.10.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1624855927d72bb0f8281785fc110eb89702078abe1c42e2e6cbe872ec374277"}, + {file = "regress-2025.10.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0f63f4c3f2e701c832e54434ee10bd0bd2c850b0ae4029829d2dd0c9a340d83"}, + {file = "regress-2025.10.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ccfc11e2b632a21d82a56b52a70fc002b990fcf4f6b30661724732776bea6f1d"}, + {file = "regress-2025.10.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be96403b244e3d6925e225b4da5fbc4f5dd6f06b07f751f00047aa48fd2c7fe3"}, + {file = "regress-2025.10.1-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15a7c2777e8eeb153fe87327871669c3bd1aab4fe20c1fccf448616bc298350d"}, + {file = "regress-2025.10.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:905c4e364526f89b33db8e69468b15d9c294a56af4d533f2700e0e1a363fbae7"}, + {file = "regress-2025.10.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0d1f3c465f415521ca259209dd2587c9a35785b35e98d62d4a7fba3c2bf1cfc8"}, + {file = "regress-2025.10.1-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:701d1e4f2b30abfb39b27759a52e68cab8b76484b3d9b51b2d7f368807613f77"}, + {file = "regress-2025.10.1-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:189899161133e7c56e733a3fc939642611585c5599626352ee0849cd34d7f436"}, + {file = "regress-2025.10.1-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:4c559282471f4f0c9bf7b588985c4bec16de0a85e1a3e017dca39640e15118ce"}, + {file = "regress-2025.10.1.tar.gz", hash = "sha256:dcc0a8af0cdbc3d6e0d4725f113335d0a5ffbba86ae3ca18d2b5b352c5f2c8ed"}, +] + [[package]] name = "requests" version = "2.32.5" @@ -2068,8 +2198,9 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [extras] docs = [] +ecma-regex = ["regress"] [metadata] lock-version = "2.1" python-versions = "^3.10.0" -content-hash = "09c27eb4b3e29b9e0ec20db1cab6922cb73e91eeac1049b29e1b65a6a5ca8359" +content-hash = "dd73974c171fe06ed00a48c4b069c177885162945d22079c1d71cd7bc70e0a11" diff --git a/pyproject.toml b/pyproject.toml index acf37bb..1e88af6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,10 @@ ignore_missing_imports = true module = "rfc3339_validator" ignore_missing_imports = true +[[tool.mypy.overrides]] +module = "regress" +ignore_missing_imports = true + [tool.tbump] github_url = "https://github.com/python-openapi/openapi-schema-validator" @@ -84,9 +88,11 @@ jsonschema = "^4.19.1" rfc3339-validator = "*" # requred by jsonschema for date-time checker jsonschema-specifications = ">=2024.10.1" referencing = "^0.37.0" +regress = {version = ">=2025.10.1", optional = true} [tool.poetry.extras] docs = ["sphinx", "sphinx-immaterial"] +ecma-regex = ["regress"] [tool.poetry.group.dev.dependencies] black = ">=24.4,<27.0" diff --git a/tests/integration/test_validators.py b/tests/integration/test_validators.py index 461e881..b59f31e 100644 --- a/tests/integration/test_validators.py +++ b/tests/integration/test_validators.py @@ -37,6 +37,7 @@ from openapi_schema_validator._dialects import OAS32_BASE_DIALECT_ID from openapi_schema_validator._dialects import OAS32_BASE_DIALECT_METASCHEMA from openapi_schema_validator._dialects import register_openapi_dialect +from openapi_schema_validator._regex import has_ecma_regex class TestOAS30ValidatorFormatChecker: @@ -126,12 +127,19 @@ def test_string_invalid(self, validator_class, value): with pytest.raises(ValidationError): validator.validate(value) - def test_invalid_pattern_raises_regex_error(self, validator_class): + def test_invalid_pattern_raises_expected_error(self, validator_class): schema = {"type": "string", "pattern": "["} validator = validator_class(schema) - with pytest.raises(re.error): - validator.validate("foo") + if has_ecma_regex(): + with pytest.raises( + ValidationError, + match="is not a valid regular expression", + ): + validator.validate("foo") + else: + with pytest.raises(re.error): + validator.validate("foo") def test_invalid_pattern_rejected_by_validate_helper( self, validator_class @@ -141,6 +149,37 @@ def test_invalid_pattern_rejected_by_validate_helper( with pytest.raises(SchemaError, match="is not a 'regex'"): validate("foo", schema, cls=validator_class) + @pytest.mark.skipif( + not has_ecma_regex(), reason="requires optional ecma-regex extra" + ) + def test_z_escape_behaves_as_ecma_literal_escape(self, validator_class): + schema = {"type": "string", "pattern": r"^foo\z"} + validator = validator_class(schema) + + with pytest.raises(ValidationError, match="does not match"): + validator.validate("foo") + + result = validator.validate("fooz") + + assert result is None + + result = validate("fooz", schema, cls=validator_class) + + assert result is None + + @pytest.mark.skipif( + not has_ecma_regex(), reason="requires optional ecma-regex extra" + ) + def test_escaped_z_pattern_is_allowed_with_ecma_regex( + self, validator_class + ): + schema = {"type": "string", "pattern": r"^foo\\z$"} + validator = validator_class(schema) + + result = validator.validate(r"foo\z") + + assert result is None + def test_referencing(self, validator_class): name_schema = Resource.from_contents( {