Skip to content

Support nullable keyword in properties. Closes #99 #163

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 19, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion end_to_end_tests/fastapi_app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions openapi_python_client/parser/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
49 changes: 32 additions & 17 deletions openapi_python_client/parser/properties.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Property:

name: str
required: bool
nullable: bool
default: Optional[Any]

template: ClassVar[Optional[str]] = None
Expand All @@ -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}]"

Expand Down Expand Up @@ -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()}]]"

Expand Down Expand Up @@ -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}]]"

Expand Down Expand Up @@ -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}]"

Expand Down Expand Up @@ -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}]"

Expand Down Expand Up @@ -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(
Expand All @@ -452,14 +460,17 @@ 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,
required=required,
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] = []
Expand All @@ -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}")


Expand Down
2 changes: 1 addition & 1 deletion openapi_python_client/schema/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading