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! 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/pyproject.toml b/pyproject.toml index ef948da30..43aab74b7 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 3c60c2daf..a30059a93 100644 --- a/tests/test_parser/test_properties/test_init.py +++ b/tests/test_parser/test_properties/test_init.py @@ -688,7 +688,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 @@ -705,7 +705,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 @@ -907,7 +907,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()] @@ -1171,7 +1171,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 4b93f2c42..3c8c2ecea 100644 --- a/tests/test_schema/test_schema.py +++ b/tests/test_schema/test_schema.py @@ -25,3 +25,39 @@ 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