From 205335dec56a5a9bed02ef2164598ff7818a1714 Mon Sep 17 00:00:00 2001 From: mikkelam Date: Sat, 10 Aug 2024 11:17:05 +0200 Subject: [PATCH 1/3] fix: add backward compatibility for exclusiveMinimum and exclusiveMaximum in schema validator --- .../schema/openapi_schema_pydantic/schema.py | 31 +++++++++++++++++-- tests/test_schema/test_schema.py | 31 +++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/openapi_python_client/schema/openapi_schema_pydantic/schema.py b/openapi_python_client/schema/openapi_schema_pydantic/schema.py index e2201c6e7..54828fe48 100644 --- a/openapi_python_client/schema/openapi_schema_pydantic/schema.py +++ b/openapi_python_client/schema/openapi_schema_pydantic/schema.py @@ -23,9 +23,9 @@ class Schema(BaseModel): title: Optional[str] = None multipleOf: Optional[float] = Field(default=None, gt=0.0) maximum: Optional[float] = None - exclusiveMaximum: Optional[bool] = None + exclusiveMaximum: Optional[Union[bool, float]] = None minimum: Optional[float] = None - exclusiveMinimum: Optional[bool] = None + exclusiveMinimum: Optional[Union[bool, float]] = None maxLength: Optional[int] = Field(default=None, ge=0) minLength: Optional[int] = Field(default=None, ge=0) pattern: Optional[str] = None @@ -160,6 +160,33 @@ class Schema(BaseModel): }, ) + @model_validator(mode="after") + def handle_exclusive_min_max(self) -> "Schema": + """ + Convert exclusiveMinimum/exclusiveMaximum between OpenAPI v3.0 (bool) and v3.1 (numeric). + """ + # Handle exclusiveMinimum + if isinstance(self.exclusiveMinimum, bool) and self.minimum is not None: + if self.exclusiveMinimum: + self.exclusiveMinimum = self.minimum + self.minimum = None + else: + self.exclusiveMinimum = None + elif isinstance(self.exclusiveMinimum, float): + self.minimum = None + + # Handle exclusiveMaximum + if isinstance(self.exclusiveMaximum, bool) and self.maximum is not None: + if self.exclusiveMaximum: + self.exclusiveMaximum = self.maximum + self.maximum = None + else: + self.exclusiveMaximum = None + elif isinstance(self.exclusiveMaximum, float): + self.maximum = None + + return self + @model_validator(mode="after") def handle_nullable(self) -> "Schema": """Convert the old 3.0 `nullable` property into the new 3.1 style""" diff --git a/tests/test_schema/test_schema.py b/tests/test_schema/test_schema.py index 4b93f2c42..70359d5c4 100644 --- a/tests/test_schema/test_schema.py +++ b/tests/test_schema/test_schema.py @@ -25,3 +25,34 @@ def test_nullable_with_any_of(): def test_nullable_with_one_of(): schema = Schema.model_validate_json('{"oneOf": [{"type": "string"}], "nullable": true}') assert schema.oneOf == [Schema(type=DataType.STRING), Schema(type=DataType.NULL)] + + +def test_exclusive_minimum_as_boolean(): + schema = Schema.model_validate_json('{"minimum": 10, "exclusiveMinimum": true}') + assert schema.exclusiveMinimum == 10 + assert schema.minimum is None + +def test_exclusive_maximum_as_boolean(): + schema = Schema.model_validate_json('{"maximum": 100, "exclusiveMaximum": true}') + assert schema.exclusiveMaximum == 100 + assert schema.maximum is None + +def test_exclusive_minimum_as_number(): + schema = Schema.model_validate_json('{"exclusiveMinimum": 5}') + assert schema.exclusiveMinimum == 5 + assert schema.minimum is None + +def test_exclusive_maximum_as_number(): + schema = Schema.model_validate_json('{"exclusiveMaximum": 50}') + assert schema.exclusiveMaximum == 50 + assert schema.maximum is None + +def test_exclusive_minimum_as_false_boolean(): + schema = Schema.model_validate_json('{"minimum": 10, "exclusiveMinimum": false}') + assert schema.exclusiveMinimum is None + assert schema.minimum == 10 + +def test_exclusive_maximum_as_false_boolean(): + schema = Schema.model_validate_json('{"maximum": 100, "exclusiveMaximum": false}') + assert schema.exclusiveMaximum is None + assert schema.maximum == 100 From 50e1d0fd455700709d77f73596e95c13409cbe76 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sat, 24 Aug 2024 23:57:59 -0600 Subject: [PATCH 2/3] Format and lint --- pyproject.toml | 1 + tests/test_cli.py | 2 +- tests/test_parser/test_openapi.py | 2 +- tests/test_parser/test_properties/test_init.py | 8 ++++---- tests/test_schema/test_schema.py | 5 +++++ 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c585ff365..4867b971a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ ignore = ["E501", "PLR0913"] [tool.ruff.lint.per-file-ignores] "openapi_python_client/cli.py" = ["B008"] +"tests/*" = ["PLR2004"] [tool.coverage.run] omit = ["openapi_python_client/templates/*"] diff --git a/tests/test_cli.py b/tests/test_cli.py index bb73cb48c..f5f3e0ea8 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -20,7 +20,7 @@ def test_bad_config(): result = runner.invoke(app, ["generate", f"--config={config_path}", f"--path={path}"]) - assert result.exit_code == 2 # noqa: PLR2004 + assert result.exit_code == 2 assert "Unable to parse config" in result.stdout diff --git a/tests/test_parser/test_openapi.py b/tests/test_parser/test_openapi.py index 75eea1b47..ce4f9d5e6 100644 --- a/tests/test_parser/test_openapi.py +++ b/tests/test_parser/test_openapi.py @@ -350,7 +350,7 @@ def test__add_parameters_query_optionality(self, config): endpoint=endpoint, data=data, schemas=Schemas(), parameters=Parameters(), config=config ) - assert len(endpoint.query_parameters) == 2, "Not all query params were added" # noqa: PLR2004 + assert len(endpoint.query_parameters) == 2, "Not all query params were added" for param in endpoint.query_parameters: if param.name == "required": assert param.required diff --git a/tests/test_parser/test_properties/test_init.py b/tests/test_parser/test_properties/test_init.py index 3290dcd39..45aee8d00 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -687,7 +687,7 @@ def test_property_from_data_union(self, config): )[0] assert isinstance(response, UnionProperty) - assert len(response.inner_properties) == 2 # noqa: PLR2004 + assert len(response.inner_properties) == 2 def test_property_from_data_list_of_types(self, config): from openapi_python_client.parser.properties import Schemas, property_from_data @@ -704,7 +704,7 @@ def test_property_from_data_list_of_types(self, config): )[0] assert isinstance(response, UnionProperty) - assert len(response.inner_properties) == 2 # noqa: PLR2004 + assert len(response.inner_properties) == 2 def test_property_from_data_union_of_one_element(self, model_property_factory, config): from openapi_python_client.parser.properties import Schemas, property_from_data @@ -906,7 +906,7 @@ def test_retries_failing_properties_while_making_progress(self, mocker, config): call("#/components/schemas/first"), ] ) - assert update_schemas_with_data.call_count == 3 # noqa: PLR2004 + assert update_schemas_with_data.call_count == 3 assert result.errors == [PropertyError()] @@ -1147,7 +1147,7 @@ def test_retries_failing_parameters_while_making_progress(self, mocker, config): call("#/components/parameters/first"), ] ) - assert update_parameters_with_data.call_count == 3 # noqa: PLR2004 + assert update_parameters_with_data.call_count == 3 assert result.errors == [ParameterError()] diff --git a/tests/test_schema/test_schema.py b/tests/test_schema/test_schema.py index 70359d5c4..3c8c2ecea 100644 --- a/tests/test_schema/test_schema.py +++ b/tests/test_schema/test_schema.py @@ -32,26 +32,31 @@ def test_exclusive_minimum_as_boolean(): assert schema.exclusiveMinimum == 10 assert schema.minimum is None + def test_exclusive_maximum_as_boolean(): schema = Schema.model_validate_json('{"maximum": 100, "exclusiveMaximum": true}') assert schema.exclusiveMaximum == 100 assert schema.maximum is None + def test_exclusive_minimum_as_number(): schema = Schema.model_validate_json('{"exclusiveMinimum": 5}') assert schema.exclusiveMinimum == 5 assert schema.minimum is None + def test_exclusive_maximum_as_number(): schema = Schema.model_validate_json('{"exclusiveMaximum": 50}') assert schema.exclusiveMaximum == 50 assert schema.maximum is None + def test_exclusive_minimum_as_false_boolean(): schema = Schema.model_validate_json('{"minimum": 10, "exclusiveMinimum": false}') assert schema.exclusiveMinimum is None assert schema.minimum == 10 + def test_exclusive_maximum_as_false_boolean(): schema = Schema.model_validate_json('{"maximum": 100, "exclusiveMaximum": false}') assert schema.exclusiveMaximum is None From 07b0a7d45cdc7705bd2627204360e2a603b03b59 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Sun, 25 Aug 2024 00:00:16 -0600 Subject: [PATCH 3/3] add changeset --- ...patibility_for_exclusiveminimum_and_exclusivemaximum.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/add_backward_compatibility_for_exclusiveminimum_and_exclusivemaximum.md diff --git a/.changeset/add_backward_compatibility_for_exclusiveminimum_and_exclusivemaximum.md b/.changeset/add_backward_compatibility_for_exclusiveminimum_and_exclusivemaximum.md new file mode 100644 index 000000000..5fae1f660 --- /dev/null +++ b/.changeset/add_backward_compatibility_for_exclusiveminimum_and_exclusivemaximum.md @@ -0,0 +1,7 @@ +--- +default: patch +--- + +# Allow OpenAPI 3.1-style `exclusiveMinimum` and `exclusiveMaximum` + +Fixed by PR #1092. Thanks @mikkelam!