Skip to content

Commit 3212405

Browse files
authored
Support nullable keyword in properties. Closes #99 (#163)
1 parent cc8d6fa commit 3212405

File tree

6 files changed

+173
-93
lines changed

6 files changed

+173
-93
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## 0.5.4 - Unreleased
99
### Additions
1010
- Added support for octet-stream content type (#116)
11+
- Support for [nullable](https://swagger.io/docs/specification/data-models/data-types/#null) (#99)
1112
- Union properties defined using oneOf (#98)
1213

14+
1315
## 0.5.3 - 2020-08-13
1416
### Security
1517
- All values that become file/directory names are sanitized to address path traversal vulnerabilities (CVE-2020-15141)

end_to_end_tests/fastapi_app/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from datetime import date, datetime
44
from enum import Enum
55
from pathlib import Path
6-
from typing import Any, Dict, List, Union
6+
from typing import Dict, List, Union
77

88
from fastapi import APIRouter, Body, FastAPI, File, Header, Query, UploadFile
99
from pydantic import BaseModel

openapi_python_client/parser/openapi.py

+1
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ def build(*, schemas: Dict[str, Union[oai.Reference, oai.Schema]]) -> Schemas:
273273
required=True,
274274
default=data.default,
275275
values=EnumProperty.values_from_list(data.enum),
276+
nullable=data.nullable,
276277
)
277278
continue
278279
s = Model.from_data(data=data, name=name)

openapi_python_client/parser/properties.py

+32-17
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class Property:
2828

2929
name: str
3030
required: bool
31+
nullable: bool
3132
default: Optional[Any]
3233

3334
template: ClassVar[Optional[str]] = None
@@ -45,8 +46,13 @@ def _validate_default(self, default: Any) -> Any:
4546
raise ValidationError
4647

4748
def get_type_string(self, no_optional: bool = False) -> str:
48-
""" Get a string representation of type that should be used when declaring this property """
49-
if self.required or no_optional:
49+
"""
50+
Get a string representation of type that should be used when declaring this property
51+
52+
Args:
53+
no_optional: Do not include Optional even if the value is optional (needed for isinstance checks)
54+
"""
55+
if no_optional or (self.required and not self.nullable):
5056
return self._type_string
5157
return f"Optional[{self._type_string}]"
5258

@@ -213,7 +219,7 @@ class ListProperty(Property, Generic[InnerProp]):
213219

214220
def get_type_string(self, no_optional: bool = False) -> str:
215221
""" Get a string representation of type that should be used when declaring this property """
216-
if self.required or no_optional:
222+
if no_optional or (self.required and not self.nullable):
217223
return f"List[{self.inner_property.get_type_string()}]"
218224
return f"Optional[List[{self.inner_property.get_type_string()}]]"
219225

@@ -254,7 +260,7 @@ def get_type_string(self, no_optional: bool = False) -> str:
254260
""" Get a string representation of type that should be used when declaring this property """
255261
inner_types = [p.get_type_string() for p in self.inner_properties]
256262
inner_prop_string = ", ".join(inner_types)
257-
if self.required or no_optional:
263+
if no_optional or (self.required and not self.nullable):
258264
return f"Union[{inner_prop_string}]"
259265
return f"Optional[Union[{inner_prop_string}]]"
260266

@@ -321,7 +327,7 @@ def get_enum(name: str) -> Optional[EnumProperty]:
321327
def get_type_string(self, no_optional: bool = False) -> str:
322328
""" Get a string representation of type that should be used when declaring this property """
323329

324-
if self.required or no_optional:
330+
if no_optional or (self.required and not self.nullable):
325331
return self.reference.class_name
326332
return f"Optional[{self.reference.class_name}]"
327333

@@ -376,7 +382,7 @@ def template(self) -> str: # type: ignore
376382

377383
def get_type_string(self, no_optional: bool = False) -> str:
378384
""" Get a string representation of type that should be used when declaring this property """
379-
if self.required or no_optional:
385+
if no_optional or (self.required and not self.nullable):
380386
return self.reference.class_name
381387
return f"Optional[{self.reference.class_name}]"
382388

@@ -438,13 +444,15 @@ def _string_based_property(
438444
""" Construct a Property from the type "string" """
439445
string_format = data.schema_format
440446
if string_format == "date-time":
441-
return DateTimeProperty(name=name, required=required, default=data.default)
447+
return DateTimeProperty(name=name, required=required, default=data.default, nullable=data.nullable,)
442448
elif string_format == "date":
443-
return DateProperty(name=name, required=required, default=data.default)
449+
return DateProperty(name=name, required=required, default=data.default, nullable=data.nullable,)
444450
elif string_format == "binary":
445-
return FileProperty(name=name, required=required, default=data.default)
451+
return FileProperty(name=name, required=required, default=data.default, nullable=data.nullable,)
446452
else:
447-
return StringProperty(name=name, default=data.default, required=required, pattern=data.pattern)
453+
return StringProperty(
454+
name=name, default=data.default, required=required, pattern=data.pattern, nullable=data.nullable,
455+
)
448456

449457

450458
def _property_from_data(
@@ -453,14 +461,17 @@ def _property_from_data(
453461
""" Generate a Property from the OpenAPI dictionary representation of it """
454462
name = utils.remove_string_escapes(name)
455463
if isinstance(data, oai.Reference):
456-
return RefProperty(name=name, required=required, reference=Reference.from_ref(data.ref), default=None)
464+
return RefProperty(
465+
name=name, required=required, reference=Reference.from_ref(data.ref), default=None, nullable=False,
466+
)
457467
if data.enum:
458468
return EnumProperty(
459469
name=name,
460470
required=required,
461471
values=EnumProperty.values_from_list(data.enum),
462472
title=data.title or name,
463473
default=data.default,
474+
nullable=data.nullable,
464475
)
465476
if data.anyOf or data.oneOf:
466477
sub_properties: List[Property] = []
@@ -469,26 +480,30 @@ def _property_from_data(
469480
if isinstance(sub_prop, PropertyError):
470481
return PropertyError(detail=f"Invalid property in union {name}", data=sub_prop_data)
471482
sub_properties.append(sub_prop)
472-
return UnionProperty(name=name, required=required, default=data.default, inner_properties=sub_properties)
483+
return UnionProperty(
484+
name=name, required=required, default=data.default, inner_properties=sub_properties, nullable=data.nullable,
485+
)
473486
if not data.type:
474487
return PropertyError(data=data, detail="Schemas must either have one of enum, anyOf, or type defined.")
475488
if data.type == "string":
476489
return _string_based_property(name=name, required=required, data=data)
477490
elif data.type == "number":
478-
return FloatProperty(name=name, default=data.default, required=required)
491+
return FloatProperty(name=name, default=data.default, required=required, nullable=data.nullable,)
479492
elif data.type == "integer":
480-
return IntProperty(name=name, default=data.default, required=required)
493+
return IntProperty(name=name, default=data.default, required=required, nullable=data.nullable,)
481494
elif data.type == "boolean":
482-
return BooleanProperty(name=name, required=required, default=data.default)
495+
return BooleanProperty(name=name, required=required, default=data.default, nullable=data.nullable,)
483496
elif data.type == "array":
484497
if data.items is None:
485498
return PropertyError(data=data, detail="type array must have items defined")
486499
inner_prop = property_from_data(name=f"{name}_item", required=True, data=data.items)
487500
if isinstance(inner_prop, PropertyError):
488501
return PropertyError(data=inner_prop.data, detail=f"invalid data in items of array {name}")
489-
return ListProperty(name=name, required=required, default=data.default, inner_property=inner_prop,)
502+
return ListProperty(
503+
name=name, required=required, default=data.default, inner_property=inner_prop, nullable=data.nullable,
504+
)
490505
elif data.type == "object":
491-
return DictProperty(name=name, required=required, default=data.default)
506+
return DictProperty(name=name, required=required, default=data.default, nullable=data.nullable,)
492507
return PropertyError(data=data, detail=f"unknown type {data.type}")
493508

494509

openapi_python_client/schema/schema.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ class Schema(BaseModel):
406406
Other than the JSON Schema subset fields, the following fields MAY be used for further schema documentation:
407407
"""
408408

409-
nullable: Optional[bool] = None
409+
nullable: bool = False
410410
"""
411411
A `true` value adds `"null"` to the allowed type specified by the `type` keyword,
412412
only if `type` is explicitly defined within the same Schema Object.

0 commit comments

Comments
 (0)