From 7466bd07563ad577b5a786655430a5c637afa872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20K=C3=B6tter?= Date: Wed, 28 Aug 2024 15:20:36 +0200 Subject: [PATCH] model - using regex_engine python-re instead of the default rust-regex rust re is not posix compatible https://github.com/rust-lang/regex/issues/592 --- aiopenapi3/model.py | 13 +++++++--- tests/conftest.py | 5 ++++ tests/fixtures/schema-regex-engine.yaml | 20 +++++++++++++++ tests/schema_test.py | 33 +++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/schema-regex-engine.yaml diff --git a/aiopenapi3/model.py b/aiopenapi3/model.py index ee12d5eb..2ee482ac 100644 --- a/aiopenapi3/model.py +++ b/aiopenapi3/model.py @@ -77,6 +77,10 @@ def class_from_schema(s, _type): import pydantic_core +class ConfiguredRootModel(RootModel): + model_config = dict(regex_engine="python-re") + + @dataclasses.dataclass class _ClassInfo: @dataclasses.dataclass @@ -226,18 +230,20 @@ def model(self) -> Union[Type[BaseModel], Type[None]]: return m @classmethod - def collapse(cls, type_name, items) -> Type[BaseModel]: + def collapse(cls, type_name, items: List["_ClassInfo"]) -> Type[BaseModel]: r: List[Union[Type[BaseModel], Type[None]]] r = [i.model() for i in items] if len(r) > 1: ru: object = Union[tuple(r)] - m: Type[RootModel] = pydantic.create_model(type_name, __base__=(RootModel[ru],), __module__=me.__name__) + m: Type[RootModel] = pydantic.create_model( + type_name, __base__=(ConfiguredRootModel[ru],), __module__=me.__name__ + ) elif len(r) == 1: m: Type[BaseModel] = cast(Type[BaseModel], r[0]) if not (inspect.isclass(m) and issubclass(m, pydantic.BaseModel)): - m = pydantic.create_model(type_name, __base__=(RootModel[m],), __module__=me.__name__) + m = pydantic.create_model(type_name, __base__=(ConfiguredRootModel[m],), __module__=me.__name__) else: # == 0 assert len(r), r return m @@ -476,6 +482,7 @@ def createConfigDict(schema: "SchemaType"): return ConfigDict( extra=extra_, arbitrary_types_allowed=arbitrary_types_allowed_, + regex_engine="python-re", # defer_build=True, # validate_assignment=True ) diff --git a/tests/conftest.py b/tests/conftest.py index c17e1e6c..33296f76 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -397,6 +397,11 @@ def with_schema_string_pattern(openapi_version): yield _get_parsed_yaml("schema-string-pattern.yaml", openapi_version) +@pytest.fixture +def with_schema_regex_engine(openapi_version): + yield _get_parsed_yaml("schema-regex-engine.yaml", openapi_version) + + @pytest.fixture def with_schema_type_list(): yield _get_parsed_yaml("schema-type-list.yaml") diff --git a/tests/fixtures/schema-regex-engine.yaml b/tests/fixtures/schema-regex-engine.yaml new file mode 100644 index 00000000..f9e9c5e0 --- /dev/null +++ b/tests/fixtures/schema-regex-engine.yaml @@ -0,0 +1,20 @@ +openapi: "3.1.0" +info: + version: 1.0.0 + title: | + string pattern test with pattern invalid with pydantic-core/rust regex + https://github.com/pydantic/pydantic-core/issues/1374 + +components: + schemas: + Root: + type: string + pattern: ^Passphrase:[ ^[ !#-~]+$ + + Object: + type: object + additionalProperties: False + properties: + v: + type: string + pattern: ^Passphrase:[ ^[ !#-~]+$ diff --git a/tests/schema_test.py b/tests/schema_test.py index 5d7f2594..c7fd8f44 100644 --- a/tests/schema_test.py +++ b/tests/schema_test.py @@ -4,6 +4,8 @@ import uuid from unittest.mock import MagicMock, patch +from pydantic.fields import FieldInfo + if sys.version_info >= (3, 9): from pathlib import Path else: @@ -104,6 +106,37 @@ def test_schema_string_pattern(with_schema_string_pattern): GUID.model_validate(str(uuid.uuid4()).replace("-", "@")) +@pytest.mark.skipif(sys.version_info < (3, 9), reason="typing") +def test_schema_regex_engine(with_schema_regex_engine): + api = OpenAPI("/", with_schema_regex_engine) + Root = api.components.schemas["Root"].get_type() + + Root.model_validate("Passphrase: test!") + + with pytest.raises(ValidationError): + Root.model_validate("P_ssphrase:") + + Object = api.components.schemas["Object"].get_type() + Object.model_validate({"v": "Passphrase: test!"}) + + with pytest.raises(ValidationError): + Object.model_validate({"v": "P_ssphrase:"}) + + import pydantic_core._pydantic_core + + with pytest.raises(pydantic_core._pydantic_core.SchemaError, match="error: unclosed character class$"): + annotations = typing.get_args(Root.model_fields["root"].annotation) + assert len(annotations) == 2 and annotations[0] == str and isinstance(annotations[1], FieldInfo), annotations + metadata = annotations[1].metadata + assert len(metadata) == 1, metadata + pattern = metadata[0].pattern + assert isinstance(pattern, str), pattern + from typing import Annotated + + C = Annotated[str, pydantic.Field(pattern=pattern)] + pydantic.create_model("C", __base__=(pydantic.RootModel[C],)) + + def test_schema_type_list(with_schema_type_list): api = OpenAPI("/", with_schema_type_list) _Any = api.components.schemas["Any"]