Skip to content

Commit b20d240

Browse files
mikkelamdbanty
andauthored
add backward compatibility for exclusiveMinimum and exclusiveMaximum (#1092)
This PR ensures backward compatibility in the schema validator for the exclusiveMinimum and exclusiveMaximum properties, handling both the boolean format from OpenAPI v3.0 and the numeric format from v3.1. Documentation for this can be found here: https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0 under the `Tweak exclusiveMinimum and exclusiveMaximum` section Changes Made: - Updated the Schema class to handle both boolean (v3.0) and numeric (v3.1) formats for exclusiveMinimum and exclusiveMaximum. - Added a handle_exclusive_min_max method to ensure that the schema is correctly validated and converted based on the OpenAPI version. - Added tests to ensure the correct behavior for both formats of exclusiveMinimum and exclusiveMaximum. --------- Co-authored-by: Dylan Anthony <[email protected]> Co-authored-by: Dylan Anthony <[email protected]>
1 parent cf181c2 commit b20d240

File tree

7 files changed

+79
-8
lines changed

7 files changed

+79
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
default: patch
3+
---
4+
5+
# Allow OpenAPI 3.1-style `exclusiveMinimum` and `exclusiveMaximum`
6+
7+
Fixed by PR #1092. Thanks @mikkelam!

Diff for: openapi_python_client/schema/openapi_schema_pydantic/schema.py

+29-2
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ class Schema(BaseModel):
2323
title: Optional[str] = None
2424
multipleOf: Optional[float] = Field(default=None, gt=0.0)
2525
maximum: Optional[float] = None
26-
exclusiveMaximum: Optional[bool] = None
26+
exclusiveMaximum: Optional[Union[bool, float]] = None
2727
minimum: Optional[float] = None
28-
exclusiveMinimum: Optional[bool] = None
28+
exclusiveMinimum: Optional[Union[bool, float]] = None
2929
maxLength: Optional[int] = Field(default=None, ge=0)
3030
minLength: Optional[int] = Field(default=None, ge=0)
3131
pattern: Optional[str] = None
@@ -160,6 +160,33 @@ class Schema(BaseModel):
160160
},
161161
)
162162

163+
@model_validator(mode="after")
164+
def handle_exclusive_min_max(self) -> "Schema":
165+
"""
166+
Convert exclusiveMinimum/exclusiveMaximum between OpenAPI v3.0 (bool) and v3.1 (numeric).
167+
"""
168+
# Handle exclusiveMinimum
169+
if isinstance(self.exclusiveMinimum, bool) and self.minimum is not None:
170+
if self.exclusiveMinimum:
171+
self.exclusiveMinimum = self.minimum
172+
self.minimum = None
173+
else:
174+
self.exclusiveMinimum = None
175+
elif isinstance(self.exclusiveMinimum, float):
176+
self.minimum = None
177+
178+
# Handle exclusiveMaximum
179+
if isinstance(self.exclusiveMaximum, bool) and self.maximum is not None:
180+
if self.exclusiveMaximum:
181+
self.exclusiveMaximum = self.maximum
182+
self.maximum = None
183+
else:
184+
self.exclusiveMaximum = None
185+
elif isinstance(self.exclusiveMaximum, float):
186+
self.maximum = None
187+
188+
return self
189+
163190
@model_validator(mode="after")
164191
def handle_nullable(self) -> "Schema":
165192
"""Convert the old 3.0 `nullable` property into the new 3.1 style"""

Diff for: pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ ignore = ["E501", "PLR0913"]
6363

6464
[tool.ruff.lint.per-file-ignores]
6565
"openapi_python_client/cli.py" = ["B008"]
66+
"tests/*" = ["PLR2004"]
6667

6768
[tool.coverage.run]
6869
omit = ["openapi_python_client/templates/*"]

Diff for: tests/test_cli.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def test_bad_config():
2020

2121
result = runner.invoke(app, ["generate", f"--config={config_path}", f"--path={path}"])
2222

23-
assert result.exit_code == 2 # noqa: PLR2004
23+
assert result.exit_code == 2
2424
assert "Unable to parse config" in result.stdout
2525

2626

Diff for: tests/test_parser/test_openapi.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,7 @@ def test__add_parameters_query_optionality(self, config):
350350
endpoint=endpoint, data=data, schemas=Schemas(), parameters=Parameters(), config=config
351351
)
352352

353-
assert len(endpoint.query_parameters) == 2, "Not all query params were added" # noqa: PLR2004
353+
assert len(endpoint.query_parameters) == 2, "Not all query params were added"
354354
for param in endpoint.query_parameters:
355355
if param.name == "required":
356356
assert param.required

Diff for: tests/test_parser/test_properties/test_init.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ def test_property_from_data_union(self, config):
688688
)[0]
689689

690690
assert isinstance(response, UnionProperty)
691-
assert len(response.inner_properties) == 2 # noqa: PLR2004
691+
assert len(response.inner_properties) == 2
692692

693693
def test_property_from_data_list_of_types(self, config):
694694
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):
705705
)[0]
706706

707707
assert isinstance(response, UnionProperty)
708-
assert len(response.inner_properties) == 2 # noqa: PLR2004
708+
assert len(response.inner_properties) == 2
709709

710710
def test_property_from_data_union_of_one_element(self, model_property_factory, config):
711711
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):
907907
call("#/components/schemas/first"),
908908
]
909909
)
910-
assert update_schemas_with_data.call_count == 3 # noqa: PLR2004
910+
assert update_schemas_with_data.call_count == 3
911911
assert result.errors == [PropertyError()]
912912

913913

@@ -1171,7 +1171,7 @@ def test_retries_failing_parameters_while_making_progress(self, mocker, config):
11711171
call("#/components/parameters/first"),
11721172
]
11731173
)
1174-
assert update_parameters_with_data.call_count == 3 # noqa: PLR2004
1174+
assert update_parameters_with_data.call_count == 3
11751175
assert result.errors == [ParameterError()]
11761176

11771177

Diff for: tests/test_schema/test_schema.py

+36
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,39 @@ def test_nullable_with_any_of():
2525
def test_nullable_with_one_of():
2626
schema = Schema.model_validate_json('{"oneOf": [{"type": "string"}], "nullable": true}')
2727
assert schema.oneOf == [Schema(type=DataType.STRING), Schema(type=DataType.NULL)]
28+
29+
30+
def test_exclusive_minimum_as_boolean():
31+
schema = Schema.model_validate_json('{"minimum": 10, "exclusiveMinimum": true}')
32+
assert schema.exclusiveMinimum == 10
33+
assert schema.minimum is None
34+
35+
36+
def test_exclusive_maximum_as_boolean():
37+
schema = Schema.model_validate_json('{"maximum": 100, "exclusiveMaximum": true}')
38+
assert schema.exclusiveMaximum == 100
39+
assert schema.maximum is None
40+
41+
42+
def test_exclusive_minimum_as_number():
43+
schema = Schema.model_validate_json('{"exclusiveMinimum": 5}')
44+
assert schema.exclusiveMinimum == 5
45+
assert schema.minimum is None
46+
47+
48+
def test_exclusive_maximum_as_number():
49+
schema = Schema.model_validate_json('{"exclusiveMaximum": 50}')
50+
assert schema.exclusiveMaximum == 50
51+
assert schema.maximum is None
52+
53+
54+
def test_exclusive_minimum_as_false_boolean():
55+
schema = Schema.model_validate_json('{"minimum": 10, "exclusiveMinimum": false}')
56+
assert schema.exclusiveMinimum is None
57+
assert schema.minimum == 10
58+
59+
60+
def test_exclusive_maximum_as_false_boolean():
61+
schema = Schema.model_validate_json('{"maximum": 100, "exclusiveMaximum": false}')
62+
assert schema.exclusiveMaximum is None
63+
assert schema.maximum == 100

0 commit comments

Comments
 (0)