From 8e8f8eecc25f053e49c08199152f89c7e4fc1dc9 Mon Sep 17 00:00:00 2001 From: Dylan Anthony Date: Tue, 18 Aug 2020 20:05:53 -0400 Subject: [PATCH] Support nullable keyword in properties. Closes #99 --- CHANGELOG.md | 1 + end_to_end_tests/fastapi_app/__init__.py | 2 +- openapi_python_client/parser/openapi.py | 1 + openapi_python_client/parser/properties.py | 49 +++-- openapi_python_client/schema/schema.py | 2 +- tests/test_openapi_parser/test_properties.py | 210 ++++++++++++------- 6 files changed, 172 insertions(+), 93 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 620795695..dc8bed479 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## 0.5.4 - Unreleased ### Additions - Added support for octet-stream content type (#116) +- Support for [nullable](https://swagger.io/docs/specification/data-models/data-types/#null) (#99) ## 0.5.3 - 2020-08-13 diff --git a/end_to_end_tests/fastapi_app/__init__.py b/end_to_end_tests/fastapi_app/__init__.py index 46df53ea7..fb0901fc5 100644 --- a/end_to_end_tests/fastapi_app/__init__.py +++ b/end_to_end_tests/fastapi_app/__init__.py @@ -3,7 +3,7 @@ from datetime import date, datetime from enum import Enum from pathlib import Path -from typing import Any, Dict, List, Union +from typing import Dict, List, Union from fastapi import APIRouter, Body, FastAPI, File, Header, Query, UploadFile from pydantic import BaseModel diff --git a/openapi_python_client/parser/openapi.py b/openapi_python_client/parser/openapi.py index d9d13f226..1e9de52bb 100644 --- a/openapi_python_client/parser/openapi.py +++ b/openapi_python_client/parser/openapi.py @@ -273,6 +273,7 @@ def build(*, schemas: Dict[str, Union[oai.Reference, oai.Schema]]) -> Schemas: required=True, default=data.default, values=EnumProperty.values_from_list(data.enum), + nullable=data.nullable, ) continue s = Model.from_data(data=data, name=name) diff --git a/openapi_python_client/parser/properties.py b/openapi_python_client/parser/properties.py index fb2d2d6cc..a4acbd772 100644 --- a/openapi_python_client/parser/properties.py +++ b/openapi_python_client/parser/properties.py @@ -27,6 +27,7 @@ class Property: name: str required: bool + nullable: bool default: Optional[Any] template: ClassVar[Optional[str]] = None @@ -44,8 +45,13 @@ def _validate_default(self, default: Any) -> Any: raise ValidationError def get_type_string(self, no_optional: bool = False) -> str: - """ Get a string representation of type that should be used when declaring this property """ - if self.required or no_optional: + """ + Get a string representation of type that should be used when declaring this property + + Args: + no_optional: Do not include Optional even if the value is optional (needed for isinstance checks) + """ + if no_optional or (self.required and not self.nullable): return self._type_string return f"Optional[{self._type_string}]" @@ -212,7 +218,7 @@ class ListProperty(Property, Generic[InnerProp]): def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - if self.required or no_optional: + if no_optional or (self.required and not self.nullable): return f"List[{self.inner_property.get_type_string()}]" return f"Optional[List[{self.inner_property.get_type_string()}]]" @@ -253,7 +259,7 @@ def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ inner_types = [p.get_type_string() for p in self.inner_properties] inner_prop_string = ", ".join(inner_types) - if self.required or no_optional: + if no_optional or (self.required and not self.nullable): return f"Union[{inner_prop_string}]" return f"Optional[Union[{inner_prop_string}]]" @@ -320,7 +326,7 @@ def get_enum(name: str) -> Optional[EnumProperty]: def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - if self.required or no_optional: + if no_optional or (self.required and not self.nullable): return self.reference.class_name return f"Optional[{self.reference.class_name}]" @@ -375,7 +381,7 @@ def template(self) -> str: # type: ignore def get_type_string(self, no_optional: bool = False) -> str: """ Get a string representation of type that should be used when declaring this property """ - if self.required or no_optional: + if no_optional or (self.required and not self.nullable): return self.reference.class_name return f"Optional[{self.reference.class_name}]" @@ -437,13 +443,15 @@ def _string_based_property( """ Construct a Property from the type "string" """ string_format = data.schema_format if string_format == "date-time": - return DateTimeProperty(name=name, required=required, default=data.default) + return DateTimeProperty(name=name, required=required, default=data.default, nullable=data.nullable,) elif string_format == "date": - return DateProperty(name=name, required=required, default=data.default) + return DateProperty(name=name, required=required, default=data.default, nullable=data.nullable,) elif string_format == "binary": - return FileProperty(name=name, required=required, default=data.default) + return FileProperty(name=name, required=required, default=data.default, nullable=data.nullable,) else: - return StringProperty(name=name, default=data.default, required=required, pattern=data.pattern) + return StringProperty( + name=name, default=data.default, required=required, pattern=data.pattern, nullable=data.nullable, + ) def _property_from_data( @@ -452,7 +460,9 @@ def _property_from_data( """ Generate a Property from the OpenAPI dictionary representation of it """ name = utils.remove_string_escapes(name) if isinstance(data, oai.Reference): - return RefProperty(name=name, required=required, reference=Reference.from_ref(data.ref), default=None) + return RefProperty( + name=name, required=required, reference=Reference.from_ref(data.ref), default=None, nullable=False, + ) if data.enum: return EnumProperty( name=name, @@ -460,6 +470,7 @@ def _property_from_data( values=EnumProperty.values_from_list(data.enum), title=data.title or name, default=data.default, + nullable=data.nullable, ) if data.anyOf: sub_properties: List[Property] = [] @@ -468,26 +479,30 @@ def _property_from_data( if isinstance(sub_prop, PropertyError): return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data) sub_properties.append(sub_prop) - return UnionProperty(name=name, required=required, default=data.default, inner_properties=sub_properties) + return UnionProperty( + name=name, required=required, default=data.default, inner_properties=sub_properties, nullable=data.nullable, + ) if not data.type: return PropertyError(data=data, detail="Schemas must either have one of enum, anyOf, or type defined.") if data.type == "string": return _string_based_property(name=name, required=required, data=data) elif data.type == "number": - return FloatProperty(name=name, default=data.default, required=required) + return FloatProperty(name=name, default=data.default, required=required, nullable=data.nullable,) elif data.type == "integer": - return IntProperty(name=name, default=data.default, required=required) + return IntProperty(name=name, default=data.default, required=required, nullable=data.nullable,) elif data.type == "boolean": - return BooleanProperty(name=name, required=required, default=data.default) + return BooleanProperty(name=name, required=required, default=data.default, nullable=data.nullable,) elif data.type == "array": if data.items is None: return PropertyError(data=data, detail="type array must have items defined") inner_prop = property_from_data(name=f"{name}_item", required=True, data=data.items) if isinstance(inner_prop, PropertyError): return PropertyError(data=inner_prop.data, detail=f"invalid data in items of array {name}") - return ListProperty(name=name, required=required, default=data.default, inner_property=inner_prop,) + return ListProperty( + name=name, required=required, default=data.default, inner_property=inner_prop, nullable=data.nullable, + ) elif data.type == "object": - return DictProperty(name=name, required=required, default=data.default) + return DictProperty(name=name, required=required, default=data.default, nullable=data.nullable,) return PropertyError(data=data, detail=f"unknown type {data.type}") diff --git a/openapi_python_client/schema/schema.py b/openapi_python_client/schema/schema.py index 962eba72b..98033f7ae 100644 --- a/openapi_python_client/schema/schema.py +++ b/openapi_python_client/schema/schema.py @@ -406,7 +406,7 @@ class Schema(BaseModel): Other than the JSON Schema subset fields, the following fields MAY be used for further schema documentation: """ - nullable: Optional[bool] = None + nullable: bool = False """ A `true` value adds `"null"` to the allowed type specified by the `type` keyword, only if `type` is explicitly defined within the same Schema Object. diff --git a/tests/test_openapi_parser/test_properties.py b/tests/test_openapi_parser/test_properties.py index d6833adf6..ae3104bb2 100644 --- a/tests/test_openapi_parser/test_properties.py +++ b/tests/test_openapi_parser/test_properties.py @@ -12,28 +12,33 @@ def test___post_init(self, mocker): validate_default = mocker.patch(f"{MODULE_NAME}.Property._validate_default") - Property(name="a name", required=True, default=None) + Property(name="a name", required=True, default=None, nullable=False) validate_default.assert_not_called() - Property(name="a name", required=True, default="the default value") + Property(name="a name", required=True, default="the default value", nullable=False) validate_default.assert_called_with(default="the default value") def test_get_type_string(self): from openapi_python_client.parser.properties import Property - p = Property(name="test", required=True, default=None) + p = Property(name="test", required=True, default=None, nullable=False) p._type_string = "TestType" assert p.get_type_string() == "TestType" p.required = False assert p.get_type_string() == "Optional[TestType]" + assert p.get_type_string(True) == "TestType" + + p.required = False + p.nullable = True + assert p.get_type_string() == "Optional[TestType]" def test_to_string(self, mocker): from openapi_python_client.parser.properties import Property name = mocker.MagicMock() snake_case = mocker.patch("openapi_python_client.utils.snake_case") - p = Property(name=name, required=True, default=None) + p = Property(name=name, required=True, default=None, nullable=False) get_type_string = mocker.patch.object(p, "get_type_string") assert p.to_string() == f"{snake_case(name)}: {get_type_string()}" @@ -48,7 +53,7 @@ def test_get_imports(self, mocker): name = mocker.MagicMock() mocker.patch("openapi_python_client.utils.snake_case") - p = Property(name=name, required=True, default=None) + p = Property(name=name, required=True, default=None, nullable=False) assert p.get_imports(prefix="") == set() p.required = False @@ -58,20 +63,20 @@ def test__validate_default(self): from openapi_python_client.parser.properties import Property # should be okay if default isn't specified - p = Property(name="a name", required=True, default=None) + p = Property(name="a name", required=True, default=None, nullable=False) with pytest.raises(ValidationError): p._validate_default("a default value") with pytest.raises(ValidationError): - Property(name="a name", required=True, default="") + Property(name="a name", required=True, default="", nullable=False) class TestStringProperty: def test_get_type_string(self): from openapi_python_client.parser.properties import StringProperty - p = StringProperty(name="test", required=True, default=None) + p = StringProperty(name="test", required=True, default=None, nullable=False) assert p.get_type_string() == "str" p.required = False @@ -80,7 +85,7 @@ def test_get_type_string(self): def test__validate_default(self): from openapi_python_client.parser.properties import StringProperty - p = StringProperty(name="a name", required=True, default="the default value") + p = StringProperty(name="a name", required=True, default="the default value", nullable=False) assert p.default == '"the default value"' @@ -90,7 +95,7 @@ def test_get_imports(self, mocker): name = mocker.MagicMock() mocker.patch("openapi_python_client.utils.snake_case") - p = DateTimeProperty(name=name, required=True, default=None) + p = DateTimeProperty(name=name, required=True, default=None, nullable=False) assert p.get_imports(prefix="") == { "import datetime", "from typing import cast", @@ -107,9 +112,9 @@ def test__validate_default(self): from openapi_python_client.parser.properties import DateTimeProperty with pytest.raises(ValidationError): - DateTimeProperty(name="a name", required=True, default="not a datetime") + DateTimeProperty(name="a name", required=True, default="not a datetime", nullable=False) - p = DateTimeProperty(name="a name", required=True, default="2017-07-21T17:32:28Z") + p = DateTimeProperty(name="a name", required=True, default="2017-07-21T17:32:28Z", nullable=False) assert p.default == "datetime.datetime(2017, 7, 21, 17, 32, 28, tzinfo=datetime.timezone.utc)" @@ -119,7 +124,7 @@ def test_get_imports(self, mocker): name = mocker.MagicMock() mocker.patch("openapi_python_client.utils.snake_case") - p = DateProperty(name=name, required=True, default=None) + p = DateProperty(name=name, required=True, default=None, nullable=False) assert p.get_imports(prefix="") == { "import datetime", "from typing import cast", @@ -136,9 +141,9 @@ def test__validate_default(self): from openapi_python_client.parser.properties import DateProperty with pytest.raises(ValidationError): - DateProperty(name="a name", required=True, default="not a date") + DateProperty(name="a name", required=True, default="not a date", nullable=False) - p = DateProperty(name="a name", required=True, default="1010-10-10") + p = DateProperty(name="a name", required=True, default="1010-10-10", nullable=False) assert p.default == "datetime.date(1010, 10, 10)" @@ -149,7 +154,7 @@ def test_get_imports(self, mocker): name = mocker.MagicMock() mocker.patch("openapi_python_client.utils.snake_case") prefix = "blah" - p = FileProperty(name=name, required=True, default=None) + p = FileProperty(name=name, required=True, default=None, nullable=False) assert p.get_imports(prefix=prefix) == {f"from {prefix}.types import File", "from dataclasses import astuple"} p.required = False @@ -163,10 +168,10 @@ def test__validate_default(self): from openapi_python_client.parser.properties import FileProperty # should be okay if default isn't specified - FileProperty(name="a name", required=True, default=None) + FileProperty(name="a name", required=True, default=None, nullable=False) with pytest.raises(ValidationError): - FileProperty(name="a name", required=True, default="") + FileProperty(name="a name", required=True, default="", nullable=False) class TestFloatProperty: @@ -174,13 +179,13 @@ def test__validate_default(self): from openapi_python_client.parser.properties import FloatProperty # should be okay if default isn't specified - FloatProperty(name="a name", required=True, default=None) + FloatProperty(name="a name", required=True, default=None, nullable=False) - p = FloatProperty(name="a name", required=True, default="123.123") + p = FloatProperty(name="a name", required=True, default="123.123", nullable=False) assert p.default == 123.123 with pytest.raises(ValidationError): - FloatProperty(name="a name", required=True, default="not a float") + FloatProperty(name="a name", required=True, default="not a float", nullable=False) class TestIntProperty: @@ -188,13 +193,13 @@ def test__validate_default(self): from openapi_python_client.parser.properties import IntProperty # should be okay if default isn't specified - IntProperty(name="a name", required=True, default=None) + IntProperty(name="a name", required=True, default=None, nullable=False) - p = IntProperty(name="a name", required=True, default="123") + p = IntProperty(name="a name", required=True, default="123", nullable=False) assert p.default == 123 with pytest.raises(ValidationError): - IntProperty(name="a name", required=True, default="not an int") + IntProperty(name="a name", required=True, default="not an int", nullable=False) class TestBooleanProperty: @@ -202,9 +207,9 @@ def test__validate_default(self): from openapi_python_client.parser.properties import BooleanProperty # should be okay if default isn't specified - BooleanProperty(name="a name", required=True, default=None) + BooleanProperty(name="a name", required=True, default=None, nullable=False) - p = BooleanProperty(name="a name", required=True, default="Literally anything will work") + p = BooleanProperty(name="a name", required=True, default="Literally anything will work", nullable=False) assert p.default == True @@ -215,13 +220,13 @@ def test_get_type_string(self, mocker): inner_property = mocker.MagicMock() inner_type_string = mocker.MagicMock() inner_property.get_type_string.return_value = inner_type_string - p = ListProperty(name="test", required=True, default=None, inner_property=inner_property) + p = ListProperty(name="test", required=True, default=None, inner_property=inner_property, nullable=False) assert p.get_type_string() == f"List[{inner_type_string}]" p.required = False assert p.get_type_string() == f"Optional[List[{inner_type_string}]]" - p = ListProperty(name="test", required=True, default=[], inner_property=inner_property) + p = ListProperty(name="test", required=True, default=[], inner_property=inner_property, nullable=False) assert p.default == f"field(default_factory=lambda: cast(List[{inner_type_string}], []))" def test_get_type_imports(self, mocker): @@ -231,7 +236,7 @@ def test_get_type_imports(self, mocker): inner_import = mocker.MagicMock() inner_property.get_imports.return_value = {inner_import} prefix = mocker.MagicMock() - p = ListProperty(name="test", required=True, default=None, inner_property=inner_property) + p = ListProperty(name="test", required=True, default=None, inner_property=inner_property, nullable=False) assert p.get_imports(prefix=prefix) == { inner_import, @@ -261,11 +266,11 @@ def test__validate_default(self, mocker): inner_property.get_type_string.return_value = inner_type_string inner_property._validate_default.return_value = "y" - p = ListProperty(name="a name", required=True, default=["x"], inner_property=inner_property) + p = ListProperty(name="a name", required=True, default=["x"], inner_property=inner_property, nullable=False) assert p.default == f"field(default_factory=lambda: cast(List[{inner_type_string}], ['y']))" with pytest.raises(ValidationError): - ListProperty(name="a name", required=True, default="x", inner_property=inner_property) + ListProperty(name="a name", required=True, default="x", inner_property=inner_property, nullable=False) def test__validate_default_enum_items(self, mocker): from openapi_python_client.parser.properties import ListProperty, RefProperty @@ -274,7 +279,9 @@ def test__validate_default_enum_items(self, mocker): inner_enum_property.get_type_string.return_value = "AnEnum" inner_enum_property._validate_default.return_value = "AnEnum.val1" - p = ListProperty(name="a name", required=True, default=["val1"], inner_property=inner_enum_property) + p = ListProperty( + name="a name", required=True, default=["val1"], inner_property=inner_enum_property, nullable=False + ) assert p.default == "field(default_factory=lambda: cast(List[AnEnum], [AnEnum.val1]))" @@ -287,7 +294,11 @@ def test_get_type_string(self, mocker): inner_property_2 = mocker.MagicMock() inner_property_2.get_type_string.return_value = "inner_type_string_2" p = UnionProperty( - name="test", required=True, default=None, inner_properties=[inner_property_1, inner_property_2] + name="test", + required=True, + default=None, + inner_properties=[inner_property_1, inner_property_2], + nullable=False, ) assert p.get_type_string() == "Union[inner_type_string_1, inner_type_string_2]" @@ -305,7 +316,11 @@ def test_get_type_imports(self, mocker): inner_property_2.get_imports.return_value = {inner_import_2} prefix = mocker.MagicMock() p = UnionProperty( - name="test", required=True, default=None, inner_properties=[inner_property_1, inner_property_2] + name="test", + required=True, + default=None, + inner_properties=[inner_property_1, inner_property_2], + nullable=False, ) assert p.get_imports(prefix=prefix) == { @@ -331,7 +346,11 @@ def test__validate_default(self, mocker): inner_property_2.get_type_string.return_value = "inner_type_string_2" inner_property_2._validate_default.return_value = "the default value" p = UnionProperty( - name="test", required=True, default="a value", inner_properties=[inner_property_1, inner_property_2] + name="test", + required=True, + default="a value", + inner_properties=[inner_property_1, inner_property_2], + nullable=False, ) assert p.default == "the default value" @@ -340,7 +359,11 @@ def test__validate_default(self, mocker): with pytest.raises(ValidationError): UnionProperty( - name="test", required=True, default="a value", inner_properties=[inner_property_1, inner_property_2] + name="test", + required=True, + default="a value", + inner_properties=[inner_property_1, inner_property_2], + nullable=False, ) @@ -361,7 +384,7 @@ def test___post_init__(self, mocker): values = {"FIRST": "first", "SECOND": "second"} enum_property = properties.EnumProperty( - name=name, required=True, default="second", values=values, title="a_title", + name=name, required=True, default="second", values=values, title="a_title", nullable=False ) assert enum_property.default == "Deduped.SECOND" @@ -372,7 +395,9 @@ def test___post_init__(self, mocker): # Test encountering exactly the same Enum again assert ( - properties.EnumProperty(name=name, required=True, default="second", values=values, title="a_title",) + properties.EnumProperty( + name=name, required=True, default="second", values=values, title="a_title", nullable=False + ) == enum_property ) assert properties._existing_enums == {"MyTestEnum": fake_dup_enum, "Deduped": enum_property} @@ -382,7 +407,7 @@ def test___post_init__(self, mocker): from_ref.reset_mock() from_ref.side_effect = [fake_reference] enum_property = properties.EnumProperty( - name=name, required=True, default="second", values=values, title="a_title", + name=name, required=True, default="second", values=values, title="a_title", nullable=False ) assert enum_property.default == "MyTestEnum.SECOND" assert enum_property.python_name == snake_case(name) @@ -398,7 +423,9 @@ def test_get_type_string(self, mocker): from openapi_python_client.parser import properties - enum_property = properties.EnumProperty(name="test", required=True, default=None, values={}, title="a_title") + enum_property = properties.EnumProperty( + name="test", required=True, default=None, values={}, title="a_title", nullable=False + ) assert enum_property.get_type_string() == "MyTestEnum" enum_property.required = False @@ -412,7 +439,9 @@ def test_get_imports(self, mocker): from openapi_python_client.parser import properties - enum_property = properties.EnumProperty(name="test", required=True, default=None, values={}, title="a_title") + enum_property = properties.EnumProperty( + name="test", required=True, default=None, values={}, title="a_title", nullable=False + ) assert enum_property.get_imports(prefix=prefix) == { f"from {prefix}.{fake_reference.module_name} import {fake_reference.class_name}" @@ -468,13 +497,13 @@ def test__validate_default(self, mocker): from openapi_python_client.parser import properties enum_property = properties.EnumProperty( - name="test", required=True, default="test", values={"TEST": "test"}, title="a_title" + name="test", required=True, default="test", values={"TEST": "test"}, title="a_title", nullable=False ) assert enum_property.default == "MyTestEnum.TEST" with pytest.raises(ValidationError): properties.EnumProperty( - name="test", required=True, default="bad_val", values={"TEST": "test"}, title="a_title" + name="test", required=True, default="bad_val", values={"TEST": "test"}, title="a_title", nullable=False ) properties._existing_enums = {} @@ -485,7 +514,11 @@ def test_template(self, mocker): from openapi_python_client.parser.properties import RefProperty ref_property = RefProperty( - name="test", required=True, default=None, reference=mocker.MagicMock(class_name="MyRefClass") + name="test", + required=True, + default=None, + reference=mocker.MagicMock(class_name="MyRefClass"), + nullable=False, ) assert ref_property.template == "ref_property.pyi" @@ -498,7 +531,11 @@ def test_get_type_string(self, mocker): from openapi_python_client.parser.properties import RefProperty ref_property = RefProperty( - name="test", required=True, default=None, reference=mocker.MagicMock(class_name="MyRefClass") + name="test", + required=True, + default=None, + reference=mocker.MagicMock(class_name="MyRefClass"), + nullable=False, ) assert ref_property.get_type_string() == "MyRefClass" @@ -512,7 +549,7 @@ def test_get_imports(self, mocker): from openapi_python_client.parser.properties import RefProperty - p = RefProperty(name="test", required=True, default=None, reference=fake_reference) + p = RefProperty(name="test", required=True, default=None, reference=fake_reference, nullable=False) assert p.get_imports(prefix=prefix) == { f"from {prefix}.{fake_reference.module_name} import {fake_reference.class_name}", @@ -532,12 +569,12 @@ def test__validate_default(self, mocker): from openapi_python_client.parser.properties import RefProperty with pytest.raises(ValidationError): - RefProperty(name="a name", required=True, default="", reference=mocker.MagicMock()) + RefProperty(name="a name", required=True, default="", reference=mocker.MagicMock(), nullable=False) enum_property = mocker.MagicMock() enum_property._validate_default.return_value = "val1" mocker.patch(f"{MODULE_NAME}.EnumProperty.get_enum", return_value=enum_property) - p = RefProperty(name="a name", required=True, default="", reference=mocker.MagicMock()) + p = RefProperty(name="a name", required=True, default="", reference=mocker.MagicMock(), nullable=False) assert p.default == "val1" @@ -548,7 +585,7 @@ def test_get_imports(self, mocker): name = mocker.MagicMock() mocker.patch("openapi_python_client.utils.snake_case") prefix = mocker.MagicMock() - p = DictProperty(name=name, required=True, default=None) + p = DictProperty(name=name, required=True, default=None, nullable=False) assert p.get_imports(prefix=prefix) == { "from typing import Dict", } @@ -570,10 +607,10 @@ def test_get_imports(self, mocker): def test__validate_default(self): from openapi_python_client.parser.properties import DictProperty - DictProperty(name="a name", required=True, default={"key": "value"}) + DictProperty(name="a name", required=True, default={"key": "value"}, nullable=False) with pytest.raises(ValidationError): - DictProperty(name="a name", required=True, default="not a dict") + DictProperty(name="a name", required=True, default="not a dict", nullable=False) class TestPropertyFromData: @@ -590,7 +627,12 @@ def test_property_from_data_enum(self, mocker): EnumProperty.values_from_list.assert_called_once_with(data.enum) EnumProperty.assert_called_once_with( - name=name, required=required, values=EnumProperty.values_from_list(), default=data.default, title=name + name=name, + required=required, + values=EnumProperty.values_from_list(), + default=data.default, + title=name, + nullable=data.nullable, ) assert p == EnumProperty() @@ -601,7 +643,12 @@ def test_property_from_data_enum(self, mocker): name=name, required=required, data=data, ) EnumProperty.assert_called_once_with( - name=name, required=required, values=EnumProperty.values_from_list(), default=data.default, title=data.title + name=name, + required=required, + values=EnumProperty.values_from_list(), + default=data.default, + title=data.title, + nullable=data.nullable, ) def test_property_from_data_ref(self, mocker): @@ -617,7 +664,9 @@ def test_property_from_data_ref(self, mocker): p = property_from_data(name=name, required=required, data=data) from_ref.assert_called_once_with(data.ref) - RefProperty.assert_called_once_with(name=name, required=required, reference=from_ref(), default=None) + RefProperty.assert_called_once_with( + name=name, required=required, reference=from_ref(), default=None, nullable=False + ) assert p == RefProperty() def test_property_from_data_string(self, mocker): @@ -654,17 +703,18 @@ def test_property_from_data_simple_types(self, mocker, openapi_type, python_type p = property_from_data(name=name, required=required, data=data) - clazz.assert_called_once_with(name=name, required=required, default=None) + clazz.assert_called_once_with(name=name, required=required, default=None, nullable=False) assert p == clazz() # Test optional values clazz.reset_mock() data.default = mocker.MagicMock() + data.nullable = mocker.MagicMock() property_from_data( name=name, required=required, data=data, ) - clazz.assert_called_once_with(name=name, required=required, default=data.default) + clazz.assert_called_once_with(name=name, required=required, default=data.default, nullable=data.nullable) def test_property_from_data_array(self, mocker): name = mocker.MagicMock() @@ -678,9 +728,9 @@ def test_property_from_data_array(self, mocker): p = property_from_data(name=name, required=required, data=data) - FloatProperty.assert_called_once_with(name=name, required=True, default="0.0") + FloatProperty.assert_called_once_with(name=name, required=True, default="0.0", nullable=False) ListProperty.assert_called_once_with( - name=name, required=required, default=None, inner_property=FloatProperty.return_value + name=name, required=required, default=None, inner_property=FloatProperty.return_value, nullable=False ) assert p == ListProperty.return_value @@ -720,13 +770,14 @@ def test_property_from_data_union(self, mocker): p = property_from_data(name=name, required=required, data=data) - FloatProperty.assert_called_once_with(name=name, required=required, default="0.0") - IntProperty.assert_called_once_with(name=name, required=required, default="0") + FloatProperty.assert_called_once_with(name=name, required=required, default="0.0", nullable=False) + IntProperty.assert_called_once_with(name=name, required=required, default="0", nullable=False) UnionProperty.assert_called_once_with( name=name, required=required, default=None, inner_properties=[FloatProperty.return_value, IntProperty.return_value], + nullable=False, ) assert p == UnionProperty.return_value @@ -779,14 +830,16 @@ class TestStringBasedProperty: def test__string_based_property_no_format(self, mocker): name = mocker.MagicMock() required = mocker.MagicMock() - data = oai.Schema.construct(type="string") + data = oai.Schema.construct(type="string", nullable=mocker.MagicMock()) StringProperty = mocker.patch(f"{MODULE_NAME}.StringProperty") from openapi_python_client.parser.properties import _string_based_property p = _string_based_property(name=name, required=required, data=data) - StringProperty.assert_called_once_with(name=name, required=required, pattern=None, default=None) + StringProperty.assert_called_once_with( + name=name, required=required, pattern=None, default=None, nullable=data.nullable + ) assert p == StringProperty.return_value # Test optional values @@ -797,19 +850,21 @@ def test__string_based_property_no_format(self, mocker): _string_based_property( name=name, required=required, data=data, ) - StringProperty.assert_called_once_with(name=name, required=required, pattern=data.pattern, default=data.default) + StringProperty.assert_called_once_with( + name=name, required=required, pattern=data.pattern, default=data.default, nullable=data.nullable + ) def test__string_based_property_datetime_format(self, mocker): name = mocker.MagicMock() required = mocker.MagicMock() - data = oai.Schema.construct(type="string", schema_format="date-time") + data = oai.Schema.construct(type="string", schema_format="date-time", nullable=mocker.MagicMock()) DateTimeProperty = mocker.patch(f"{MODULE_NAME}.DateTimeProperty") from openapi_python_client.parser.properties import _string_based_property p = _string_based_property(name=name, required=required, data=data) - DateTimeProperty.assert_called_once_with(name=name, required=required, default=None) + DateTimeProperty.assert_called_once_with(name=name, required=required, default=None, nullable=data.nullable) assert p == DateTimeProperty.return_value # Test optional values @@ -819,18 +874,20 @@ def test__string_based_property_datetime_format(self, mocker): _string_based_property( name=name, required=required, data=data, ) - DateTimeProperty.assert_called_once_with(name=name, required=required, default=data.default) + DateTimeProperty.assert_called_once_with( + name=name, required=required, default=data.default, nullable=data.nullable + ) def test__string_based_property_date_format(self, mocker): name = mocker.MagicMock() required = mocker.MagicMock() - data = oai.Schema.construct(type="string", schema_format="date") + data = oai.Schema.construct(type="string", schema_format="date", nullable=mocker.MagicMock()) DateProperty = mocker.patch(f"{MODULE_NAME}.DateProperty") from openapi_python_client.parser.properties import _string_based_property p = _string_based_property(name=name, required=required, data=data) - DateProperty.assert_called_once_with(name=name, required=required, default=None) + DateProperty.assert_called_once_with(name=name, required=required, default=None, nullable=data.nullable) assert p == DateProperty.return_value # Test optional values @@ -840,18 +897,18 @@ def test__string_based_property_date_format(self, mocker): _string_based_property( name=name, required=required, data=data, ) - DateProperty.assert_called_once_with(name=name, required=required, default=data.default) + DateProperty.assert_called_once_with(name=name, required=required, default=data.default, nullable=data.nullable) def test__string_based_property_binary_format(self, mocker): name = mocker.MagicMock() required = mocker.MagicMock() - data = oai.Schema.construct(type="string", schema_format="binary") + data = oai.Schema.construct(type="string", schema_format="binary", nullable=mocker.MagicMock()) FileProperty = mocker.patch(f"{MODULE_NAME}.FileProperty") from openapi_python_client.parser.properties import _string_based_property p = _string_based_property(name=name, required=required, data=data) - FileProperty.assert_called_once_with(name=name, required=required, default=None) + FileProperty.assert_called_once_with(name=name, required=required, default=None, nullable=data.nullable) assert p == FileProperty.return_value # Test optional values @@ -861,19 +918,22 @@ def test__string_based_property_binary_format(self, mocker): _string_based_property( name=name, required=required, data=data, ) - FileProperty.assert_called_once_with(name=name, required=required, default=data.default) + FileProperty.assert_called_once_with(name=name, required=required, default=data.default, nullable=data.nullable) def test__string_based_property_unsupported_format(self, mocker): name = mocker.MagicMock() required = mocker.MagicMock() data = oai.Schema.construct(type="string", schema_format=mocker.MagicMock()) + data.nullable = mocker.MagicMock() StringProperty = mocker.patch(f"{MODULE_NAME}.StringProperty") from openapi_python_client.parser.properties import _string_based_property p = _string_based_property(name=name, required=required, data=data) - StringProperty.assert_called_once_with(name=name, required=required, pattern=None, default=None) + StringProperty.assert_called_once_with( + name=name, required=required, pattern=None, default=None, nullable=data.nullable + ) assert p == StringProperty.return_value # Test optional values @@ -884,4 +944,6 @@ def test__string_based_property_unsupported_format(self, mocker): _string_based_property( name=name, required=required, data=data, ) - StringProperty.assert_called_once_with(name=name, required=required, pattern=data.pattern, default=data.default) + StringProperty.assert_called_once_with( + name=name, required=required, pattern=data.pattern, default=data.default, nullable=data.nullable + )