fix modify schema (#1103)

* modify schema, fix #1102

* remove DSN from schema table

* fix tests

* add outer_type_ to ModelField, fix coverage

* simplify ModelField._type_display

* remove debugs

* revert change to _type_display

* test fix
This commit is contained in:
Samuel Colvin
2019-12-21 15:46:11 +00:00
committed by GitHub
parent a1b6aa5f5a
commit 065ae2e4c4
9 changed files with 192 additions and 151 deletions
+1
View File
@@ -0,0 +1 @@
Fix for `__modify_schema__` when it conflicted with `field_class_to_schema*`
+48 -53
View File
@@ -6,6 +6,7 @@ Done like this rather than as a raw rst table to make future edits easier.
Please edit this file directly not .tmp_schema_mappings.html
"""
import json
import re
from pathlib import Path
@@ -48,35 +49,35 @@ table = [
[
'list',
'array',
'{"items": {}}',
{'items': {}},
'JSON Schema Core',
''
],
[
'tuple',
'array',
'{"items": {}}',
{'items': {}},
'JSON Schema Core',
''
],
[
'set',
'array',
'{"items": {}, {"uniqueItems": true}',
{'items': {}, 'uniqueItems': True},
'JSON Schema Validation',
''
],
[
'List[str]',
'array',
'{"items": {"type": "string"}}',
{'items': {'type': 'string'}},
'JSON Schema Validation',
'And equivalently for any other sub type, e.g. `List[int]`.'
],
[
'Tuple[str, int]',
'array',
'{"items": [{"type": "string"}, {"type": "integer"}]}',
{'items': [{'type': 'string'}, {'type': 'integer'}]},
'JSON Schema Validation',
(
'And equivalently for any other set of subtypes. Note: If using schemas for OpenAPI, '
@@ -87,7 +88,7 @@ table = [
[
'Dict[str, int]',
'object',
'{"additionalProperties": {"type": "integer"}}',
{'additionalProperties': {'type': 'integer'}},
'JSON Schema Validation',
(
'And equivalently for any other subfields for dicts. Have in mind that although you can use other types as '
@@ -98,7 +99,7 @@ table = [
[
'Union[str, int]',
'anyOf',
'{"anyOf": [{"type": "string"}, {"type": "integer"}]}',
{'anyOf': [{'type': 'string'}, {'type': 'integer'}]},
'JSON Schema Validation',
'And equivalently for any other subfields for unions.'
],
@@ -112,49 +113,42 @@ table = [
[
'SecretStr',
'string',
'{"writeOnly": true}',
{'writeOnly': True},
'JSON Schema Validation',
''
],
[
'SecretBytes',
'string',
'{"writeOnly": true}',
{'writeOnly': True},
'JSON Schema Validation',
''
],
[
'EmailStr',
'string',
'{"format": "email"}',
{'format': 'email'},
'JSON Schema Validation',
''
],
[
'NameEmail',
'string',
'{"format": "name-email"}',
{'format': 'name-email'},
'Pydantic standard "format" extension',
''
],
[
'AnyUrl',
'string',
'{"format": "uri"}',
{'format': 'uri'},
'JSON Schema Validation',
''
],
[
'DSN',
'string',
'{"format": "dsn"}',
'Pydantic standard "format" extension',
''
],
[
'bytes',
'string',
'{"format": "binary"}',
{'format': 'binary'},
'OpenAPI',
''
],
@@ -168,154 +162,154 @@ table = [
[
'UUID1',
'string',
'{"format": "uuid1"}',
{'format': 'uuid1'},
'Pydantic standard "format" extension',
''
],
[
'UUID3',
'string',
'{"format": "uuid3"}',
{'format': 'uuid3'},
'Pydantic standard "format" extension',
''
],
[
'UUID4',
'string',
'{"format": "uuid4"}',
{'format': 'uuid4'},
'Pydantic standard "format" extension',
''
],
[
'UUID5',
'string',
'{"format": "uuid5"}',
{'format': 'uuid5'},
'Pydantic standard "format" extension',
''
],
[
'UUID',
'string',
'{"format": "uuid"}',
{'format': 'uuid'},
'Pydantic standard "format" extension',
'Suggested in OpenAPI.'
],
[
'FilePath',
'string',
'{"format": "file-path"}',
{'format': 'file-path'},
'Pydantic standard "format" extension',
''
],
[
'DirectoryPath',
'string',
'{"format": "directory-path"}',
{'format': 'directory-path'},
'Pydantic standard "format" extension',
''
],
[
'Path',
'string',
'{"format": "path"}',
{'format': 'path'},
'Pydantic standard "format" extension',
''
],
[
'datetime',
'string',
'{"format": "date-time"}',
{'format': 'date-time'},
'JSON Schema Validation',
''
],
[
'date',
'string',
'{"format": "date"}',
{'format': 'date'},
'JSON Schema Validation',
''
],
[
'time',
'string',
'{"format": "time"}',
{'format': 'time'},
'JSON Schema Validation',
''
],
[
'timedelta',
'number',
'{"format": "time-delta"}',
{'format': 'time-delta'},
'Difference in seconds (a `float`), with Pydantic standard "format" extension',
'Suggested in JSON Schema repository\'s issues by maintainer.'
],
[
'Json',
'string',
'{"format": "json-string"}',
{'format': 'json-string'},
'Pydantic standard "format" extension',
''
],
[
'IPv4Address',
'string',
'{"format": "ipv4"}',
{'format': 'ipv4'},
'JSON Schema Validation',
''
],
[
'IPv6Address',
'string',
'{"format": "ipv6"}',
{'format': 'ipv6'},
'JSON Schema Validation',
''
],
[
'IPvAnyAddress',
'string',
'{"format": "ipvanyaddress"}',
{'format': 'ipvanyaddress'},
'Pydantic standard "format" extension',
'IPv4 or IPv6 address as used in `ipaddress` module',
],
[
'IPv4Interface',
'string',
'{"format": "ipv4interface"}',
{'format': 'ipv4interface'},
'Pydantic standard "format" extension',
'IPv4 interface as used in `ipaddress` module',
],
[
'IPv6Interface',
'string',
'{"format": "ipv6interface"}',
{'format': 'ipv6interface'},
'Pydantic standard "format" extension',
'IPv6 interface as used in `ipaddress` module',
],
[
'IPvAnyInterface',
'string',
'{"format": "ipvanyinterface"}',
{'format': 'ipvanyinterface'},
'Pydantic standard "format" extension',
'IPv4 or IPv6 interface as used in `ipaddress` module',
],
[
'IPv4Network',
'string',
'{"format": "ipv4network"}',
{'format': 'ipv4network'},
'Pydantic standard "format" extension',
'IPv4 network as used in `ipaddress` module',
],
[
'IPv6Network',
'string',
'{"format": "ipv6network"}',
{'format': 'ipv6network'},
'Pydantic standard "format" extension',
'IPv6 network as used in `ipaddress` module',
],
[
'IPvAnyNetwork',
'string',
'{"format": "ipvanynetwork"}',
{'format': 'ipvanynetwork'},
'Pydantic standard "format" extension',
'IPv4 or IPv6 network as used in `ipaddress` module',
],
@@ -346,7 +340,7 @@ table = [
[
'constr(regex=\'^text$\', min_length=2, max_length=10)',
'string',
'{"pattern": "^text$", "minLength": 2, "maxLength": 10}',
{'pattern': '^text$', 'minLength': 2, 'maxLength': 10},
'JSON Schema Validation',
'Any argument not passed to the function (not defined) will not be included in the schema.'
],
@@ -363,21 +357,21 @@ table = [
[
'conint(gt=1, ge=2, lt=6, le=5, multiple_of=2)',
'integer',
'{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1, "multipleOf": 2}',
{'maximum': 5, 'exclusiveMaximum': 6, 'minimum': 2, 'exclusiveMinimum': 1, 'multipleOf': 2},
'',
'Any argument not passed to the function (not defined) will not be included in the schema.'
],
[
'PositiveInt',
'integer',
'{"exclusiveMinimum": 0}',
{'exclusiveMinimum': 0},
'JSON Schema Validation',
''
],
[
'NegativeInt',
'integer',
'{"exclusiveMaximum": 0}',
{'exclusiveMaximum': 0},
'JSON Schema Validation',
''
],
@@ -394,21 +388,21 @@ table = [
[
'confloat(gt=1, ge=2, lt=6, le=5, multiple_of=2)',
'number',
'{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1, "multipleOf": 2}',
{'maximum': 5, 'exclusiveMaximum': 6, 'minimum': 2, 'exclusiveMinimum': 1, 'multipleOf': 2},
'JSON Schema Validation',
'Any argument not passed to the function (not defined) will not be included in the schema.'
],
[
'PositiveFloat',
'number',
'{"exclusiveMinimum": 0}',
{'exclusiveMinimum': 0},
'JSON Schema Validation',
''
],
[
'NegativeFloat',
'number',
'{"exclusiveMaximum": 0}',
{'exclusiveMaximum': 0},
'JSON Schema Validation',
''
],
@@ -425,7 +419,7 @@ table = [
[
'condecimal(gt=1, ge=2, lt=6, le=5, multiple_of=2)',
'number',
'{"maximum": 5, "exclusiveMaximum": 6, "minimum": 2, "exclusiveMinimum": 1, "multipleOf": 2}',
{'maximum': 5, 'exclusiveMaximum': 6, 'minimum': 2, 'exclusiveMinimum': 1, 'multipleOf': 2},
'JSON Schema Validation',
'Any argument not passed to the function (not defined) will not be included in the schema.'
],
@@ -439,7 +433,7 @@ table = [
[
'Color',
'string',
'{"format": "color"}',
{'format': 'color'},
'Pydantic standard "format" extension',
'',
],
@@ -461,7 +455,8 @@ def build_schema_mappings():
rows = []
for py_type, json_type, additional, defined_in, notes in table:
if additional and not isinstance(additional, str):
additional = json.dumps(additional)
cols = [
f'<code>{py_type}</code>',
f'<code>{json_type}</code>',
@@ -479,7 +474,7 @@ def build_schema_mappings():
heading = '\n'.join(f' <th>{h}</th>' for h in headings)
body = '\n</tr>\n<tr>\n'.join(rows)
text = f"""\
<!--
<!--
Generated from docs/build/schema_mapping.py, DO NOT EDIT THIS FILE DIRECTLY.
Instead edit docs/build/schema_mapping.py and run `make docs`.
-->
+5 -1
View File
@@ -10,7 +10,7 @@ eg. Color((0, 255, 255)).as_named() == 'cyan' because "cyan" comes after "aqua".
import math
import re
from colorsys import hls_to_rgb, rgb_to_hls
from typing import TYPE_CHECKING, Any, Optional, Tuple, Union, cast
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union, cast
from .errors import ColorError
from .utils import Representation, almost_equal_floats
@@ -75,6 +75,10 @@ class Color(Representation):
# if we've got here value must be a valid color
self._original = value
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update(type='string', format='color')
def original(self) -> ColorType:
"""
Original value passed to Color
+7
View File
@@ -186,6 +186,7 @@ SHAPE_NAME_LOOKUP = {
class ModelField(Representation):
__slots__ = (
'type_',
'outer_type_',
'sub_fields',
'key_field',
'validators',
@@ -222,6 +223,7 @@ class ModelField(Representation):
self.has_alias: bool = bool(alias)
self.alias: str = alias or name
self.type_: Any = type_
self.outer_type_: Any = type_
self.class_validators = class_validators or {}
self.default: Any = default
self.required: 'BoolUndefined' = required
@@ -298,6 +300,7 @@ class ModelField(Representation):
"""
if self.default is not None and self.type_ is None:
self.type_ = type(self.default)
self.outer_type_ = self.type_
if self.type_ is None:
raise errors_.ConfigError(f'unable to infer type for attribute "{self.name}"')
@@ -366,7 +369,10 @@ class ModelField(Representation):
types_.append(type_)
if len(types_) == 1:
# Optional[]
self.type_ = types_[0]
# this is the one case where the "outer type" isn't just the original type
self.outer_type_ = self.type_
# re-run to correctly interpret the new self.type_
self._type_analysis()
else:
@@ -647,6 +653,7 @@ class ModelField(Representation):
def _type_display(self) -> PyObjectStr:
t = display_as_type(self.type_)
# have to do this since display_as_type(self.outer_type_) is different (and wrong) on python 3.6
if self.shape == SHAPE_MAPPING:
t = f'Mapping[{display_as_type(self.key_field.type_)}, {t}]' # type: ignore
elif self.shape == SHAPE_TUPLE:
+21 -1
View File
@@ -141,7 +141,7 @@ class AnyUrl(str):
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
update_not_none(field_schema, minLength=cls.min_length, maxLength=cls.max_length)
update_not_none(field_schema, minLength=cls.min_length, maxLength=cls.max_length, format='uri')
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
@@ -264,6 +264,10 @@ def stricturl(
class EmailStr(str):
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update(type='string', format='email')
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
# included here and below so the error happens straight away
@@ -285,6 +289,10 @@ class NameEmail(Representation):
self.name = name
self.email = email
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update(type='string', format='name-email')
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
if email_validator is None:
@@ -302,6 +310,10 @@ class NameEmail(Representation):
class IPvAnyAddress(_BaseAddress):
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update(type='string', format='ipvanyaddress')
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield cls.validate
@@ -320,6 +332,10 @@ class IPvAnyAddress(_BaseAddress):
class IPvAnyInterface(_BaseAddress):
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update(type='string', format='ipvanyinterface')
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield cls.validate
@@ -338,6 +354,10 @@ class IPvAnyInterface(_BaseAddress):
class IPvAnyNetwork(_BaseNetwork): # type: ignore
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update(type='string', format='ipvanynetwork')
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield cls.validate
+41 -74
View File
@@ -25,7 +25,6 @@ from typing import (
from uuid import UUID
from .class_validators import ROOT_KEY
from .color import Color
from .fields import (
SHAPE_FROZENSET,
SHAPE_LIST,
@@ -39,23 +38,13 @@ from .fields import (
ModelField,
)
from .json import pydantic_encoder
from .networks import AnyUrl, EmailStr, IPvAnyAddress, IPvAnyInterface, IPvAnyNetwork, NameEmail
from .networks import AnyUrl, EmailStr
from .types import (
UUID1,
UUID3,
UUID4,
UUID5,
ConstrainedDecimal,
ConstrainedFloat,
ConstrainedInt,
ConstrainedList,
ConstrainedStr,
DirectoryPath,
FilePath,
Json,
SecretBytes,
SecretStr,
StrictBool,
conbytes,
condecimal,
confloat,
@@ -385,38 +374,34 @@ def field_type_schema(
"""
definitions = {}
nested_models: Set[str] = set()
f_schema: Dict[str, Any]
ref_prefix = ref_prefix or default_prefix
if field.shape in {SHAPE_LIST, SHAPE_TUPLE_ELLIPSIS, SHAPE_SEQUENCE, SHAPE_SET, SHAPE_FROZENSET}:
f_schema, f_definitions, f_nested_models = field_singleton_schema(
items_schema, f_definitions, f_nested_models = field_singleton_schema(
field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix, known_models=known_models
)
definitions.update(f_definitions)
nested_models.update(f_nested_models)
s: Dict[str, Any] = {'type': 'array', 'items': f_schema}
f_schema = {'type': 'array', 'items': items_schema}
if field.shape in {SHAPE_SET, SHAPE_FROZENSET}:
s['uniqueItems'] = True
if field.field_info.min_items is not None:
s['minItems'] = field.field_info.min_items
if field.field_info.max_items is not None:
s['maxItems'] = field.field_info.max_items
return s, definitions, nested_models
f_schema['uniqueItems'] = True
elif field.shape == SHAPE_MAPPING:
dict_schema: Dict[str, Any] = {'type': 'object'}
f_schema = {'type': 'object'}
key_field = cast(ModelField, field.key_field)
regex = getattr(key_field.type_, 'regex', None)
f_schema, f_definitions, f_nested_models = field_singleton_schema(
items_schema, f_definitions, f_nested_models = field_singleton_schema(
field, by_alias=by_alias, model_name_map=model_name_map, ref_prefix=ref_prefix, known_models=known_models
)
definitions.update(f_definitions)
nested_models.update(f_nested_models)
if regex:
# Dict keys have a regex pattern
# f_schema might be a schema or empty dict, add it either way
dict_schema['patternProperties'] = {regex.pattern: f_schema}
elif f_schema:
# items_schema might be a schema or empty dict, add it either way
f_schema['patternProperties'] = {regex.pattern: items_schema}
elif items_schema:
# The dict values are not simply Any, so they need a schema
dict_schema['additionalProperties'] = f_schema
return dict_schema, definitions, nested_models
f_schema['additionalProperties'] = items_schema
elif field.shape == SHAPE_TUPLE:
sub_schema = []
sub_fields = cast(List[ModelField], field.sub_fields)
@@ -429,7 +414,7 @@ def field_type_schema(
sub_schema.append(sf_schema)
if len(sub_schema) == 1:
sub_schema = sub_schema[0] # type: ignore
return {'type': 'array', 'items': sub_schema}, definitions, nested_models
f_schema = {'type': 'array', 'items': sub_schema}
else:
assert field.shape == SHAPE_SINGLETON, field.shape
f_schema, f_definitions, f_nested_models = field_singleton_schema(
@@ -442,7 +427,13 @@ def field_type_schema(
)
definitions.update(f_definitions)
nested_models.update(f_nested_models)
return f_schema, definitions, nested_models
# check field type to avoid repeated calls to the same __modify_schema__ method
if field.type_ != field.outer_type_:
modify_schema = getattr(field.outer_type_, '__modify_schema__', None)
if modify_schema:
modify_schema(f_schema)
return f_schema, definitions, nested_models
def model_process_schema(
@@ -573,55 +564,35 @@ def field_singleton_sub_fields_schema(
return {'anyOf': sub_field_schemas}, definitions, nested_models
# Order is important, subclasses of str must go before str, etc
field_class_to_schema_enum_enabled: Tuple[Tuple[Any, Dict[str, Any]], ...] = (
(EmailStr, {'type': 'string', 'format': 'email'}),
(AnyUrl, {'type': 'string', 'format': 'uri'}),
(SecretStr, {'type': 'string', 'writeOnly': True}),
(str, {'type': 'string'}),
(SecretBytes, {'type': 'string', 'writeOnly': True}),
(bytes, {'type': 'string', 'format': 'binary'}),
(StrictBool, {'type': 'boolean'}),
(bool, {'type': 'boolean'}),
(int, {'type': 'integer'}),
(float, {'type': 'number'}),
(Decimal, {'type': 'number'}),
(UUID1, {'type': 'string', 'format': 'uuid1'}),
(UUID3, {'type': 'string', 'format': 'uuid3'}),
(UUID4, {'type': 'string', 'format': 'uuid4'}),
(UUID5, {'type': 'string', 'format': 'uuid5'}),
(UUID, {'type': 'string', 'format': 'uuid'}),
(NameEmail, {'type': 'string', 'format': 'name-email'}),
(dict, {'type': 'object'}),
(list, {'type': 'array', 'items': {}}),
(tuple, {'type': 'array', 'items': {}}),
(set, {'type': 'array', 'items': {}, 'uniqueItems': True}),
(Color, {'type': 'string', 'format': 'color'}),
)
json_scheme = {'type': 'string', 'format': 'json-string'}
# Order is important, subclasses of Path must go before Path, etc
field_class_to_schema_enum_disabled = (
(FilePath, {'type': 'string', 'format': 'file-path'}),
(DirectoryPath, {'type': 'string', 'format': 'directory-path'}),
# Order is important, e.g. subclasses of str must go before str
# this is used only for standard library types, custom types should use __modify_schema__ instead
field_class_to_schema: Tuple[Tuple[Any, Dict[str, Any]], ...] = (
(Path, {'type': 'string', 'format': 'path'}),
(datetime, {'type': 'string', 'format': 'date-time'}),
(date, {'type': 'string', 'format': 'date'}),
(time, {'type': 'string', 'format': 'time'}),
(timedelta, {'type': 'number', 'format': 'time-delta'}),
(Json, json_scheme),
(IPv4Network, {'type': 'string', 'format': 'ipv4network'}),
(IPv6Network, {'type': 'string', 'format': 'ipv6network'}),
(IPvAnyNetwork, {'type': 'string', 'format': 'ipvanynetwork'}),
(IPv4Interface, {'type': 'string', 'format': 'ipv4interface'}),
(IPv6Interface, {'type': 'string', 'format': 'ipv6interface'}),
(IPvAnyInterface, {'type': 'string', 'format': 'ipvanyinterface'}),
(IPv4Address, {'type': 'string', 'format': 'ipv4'}),
(IPv6Address, {'type': 'string', 'format': 'ipv6'}),
(IPvAnyAddress, {'type': 'string', 'format': 'ipvanyaddress'}),
(str, {'type': 'string'}),
(bytes, {'type': 'string', 'format': 'binary'}),
(bool, {'type': 'boolean'}),
(int, {'type': 'integer'}),
(float, {'type': 'number'}),
(Decimal, {'type': 'number'}),
(UUID, {'type': 'string', 'format': 'uuid'}),
(dict, {'type': 'object'}),
(list, {'type': 'array', 'items': {}}),
(tuple, {'type': 'array', 'items': {}}),
(set, {'type': 'array', 'items': {}, 'uniqueItems': True}),
)
json_scheme = {'type': 'string', 'format': 'json-string'}
def field_singleton_schema( # noqa: C901 (ignore complexity)
field: ModelField,
@@ -652,10 +623,7 @@ def field_singleton_schema( # noqa: C901 (ignore complexity)
known_models=known_models,
)
if field.type_ is Any or type(field.type_) == TypeVar:
if field.parse_json:
return json_scheme, definitions, nested_models
else:
return {}, definitions, nested_models # no restrictions
return {}, definitions, nested_models # no restrictions
if is_callable_type(field.type_):
raise SkipField(f'Callable {field.name} was excluded from schema since JSON schema has no equivalent type.')
f_schema: Dict[str, Any] = {}
@@ -677,11 +645,12 @@ def field_singleton_schema( # noqa: C901 (ignore complexity)
literal_value = values[0]
field_type = type(literal_value)
f_schema['const'] = literal_value
if issubclass(field_type, Enum):
f_schema.update({'enum': [item.value for item in field_type]})
# Don't return immediately, to allow adding specific types
for type_, t_schema in field_class_to_schema_enum_enabled:
for type_, t_schema in field_class_to_schema:
if issubclass(field_type, type_):
f_schema.update(t_schema)
break
@@ -690,16 +659,13 @@ def field_singleton_schema( # noqa: C901 (ignore complexity)
if modify_schema:
modify_schema(f_schema)
# Return schema, with or without enum definitions
if f_schema:
return f_schema, definitions, nested_models
for type_, t_schema in field_class_to_schema_enum_disabled:
if issubclass(field_type, type_):
return t_schema, definitions, nested_models
# Handle dataclass-based models
if lenient_issubclass(getattr(field_type, '__pydantic_model__', None), BaseModel):
field_type = field_type.__pydantic_model__
if issubclass(field_type, BaseModel):
model_name = model_name_map[field_type]
if field_type not in known_models:
@@ -720,6 +686,7 @@ def field_singleton_schema( # noqa: C901 (ignore complexity)
return schema_ref, definitions, nested_models
else:
return {'allOf': [schema_ref]}, definitions, nested_models
raise ValueError(f'Value not declarable with JSON Schema, field: {field}')
+32 -10
View File
@@ -92,9 +92,7 @@ class ConstrainedBytes(bytes):
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
update_not_none(
field_schema, minLength=cls.min_length, maxLength=cls.max_length,
)
update_not_none(field_schema, minLength=cls.min_length, maxLength=cls.max_length)
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
@@ -129,9 +127,7 @@ class ConstrainedList(list): # type: ignore
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
update_not_none(
field_schema, minLength=cls.min_items, maxLength=cls.max_items,
)
update_not_none(field_schema, minItems=cls.min_items, maxItems=cls.max_items)
@classmethod
def list_length_validator(cls, v: 'List[T]') -> 'List[T]':
@@ -220,6 +216,10 @@ else:
StrictBool to allow for bools which are not type-coerced.
"""
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update(type='boolean')
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield cls.validate
@@ -453,20 +453,28 @@ def condecimal(
class UUID1(UUID):
_required_version = 1
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update(type='string', format=f'uuid{cls._required_version}')
class UUID3(UUID):
class UUID3(UUID1):
_required_version = 3
class UUID4(UUID):
class UUID4(UUID1):
_required_version = 4
class UUID5(UUID):
class UUID5(UUID1):
_required_version = 5
class FilePath(Path):
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update(format='file-path')
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield path_validator
@@ -482,6 +490,10 @@ class FilePath(Path):
class DirectoryPath(Path):
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update(format='directory-path')
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield path_validator
@@ -506,10 +518,16 @@ class JsonMeta(type):
class Json(metaclass=JsonMeta):
pass
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update(type='string', format='json-string')
class SecretStr:
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update(type='string', writeOnly=True)
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield str_validator
@@ -540,6 +558,10 @@ class SecretStr:
class SecretBytes:
@classmethod
def __modify_schema__(cls, field_schema: Dict[str, Any]) -> None:
field_schema.update(type='string', writeOnly=True)
@classmethod
def __get_validators__(cls) -> 'CallableGenerator':
yield bytes_validator
+3 -1
View File
@@ -2,7 +2,7 @@ import re
import sys
from decimal import Decimal
from enum import Enum
from typing import Any, Dict, List, Optional, Set, Tuple, Type, TypeVar, Union
from typing import Any, Dict, FrozenSet, List, Optional, Set, Tuple, Type, TypeVar, Union
import pytest
@@ -1222,8 +1222,10 @@ def test_field_str_shape():
(Union[List[int], Set[bytes]], 'Union[List[int], Set[bytes]]'),
(List[Tuple[int, int]], 'List[Tuple[int, int]]'),
(Dict[int, str], 'Mapping[int, str]'),
(FrozenSet[int], 'FrozenSet[int]'),
(Tuple[int, ...], 'Tuple[int, ...]'),
(Optional[List[int]], 'Optional[List[int]]'),
(dict, 'dict'),
],
)
def test_field_type_display(type_, expected):
+34 -11
View File
@@ -516,8 +516,7 @@ def test_str_constrained_types(field_type, expected_schema):
model_schema = Model.schema()
assert model_schema['properties']['a'] == expected_schema
base_schema = {'title': 'Model', 'type': 'object', 'properties': {'a': {}}, 'required': ['a']}
base_schema['properties']['a'] = expected_schema
base_schema = {'title': 'Model', 'type': 'object', 'properties': {'a': expected_schema}, 'required': ['a']}
assert model_schema == base_schema
@@ -670,12 +669,16 @@ def test_path_types(field_type, expected_schema):
def test_json_type():
class Model(BaseModel):
a: Json
b: Json[int]
model_schema = Model.schema()
assert model_schema == {
assert Model.schema() == {
'title': 'Model',
'type': 'object',
'properties': {'a': {'title': 'A', 'type': 'string', 'format': 'json-string'}},
'properties': {
'a': {'title': 'A', 'type': 'string', 'format': 'json-string'},
'b': {'title': 'B', 'type': 'integer'},
},
'required': ['b'],
}
@@ -1496,10 +1499,7 @@ def test_model_with_schema_extra_callable():
schema.pop('properties')
schema['type'] = 'override'
assert Model.schema() == {
'title': 'Model',
'type': 'override',
}
assert Model.schema() == {'title': 'Model', 'type': 'override'}
def test_model_with_extra_forbidden():
@@ -1613,8 +1613,9 @@ def test_real_vs_phony_constraints():
def test_conlist():
class Model(BaseModel):
foo: List[int] = Field(..., min_items=2, max_items=4)
bar: conlist(str, min_items=1, max_items=4) = None
assert Model(foo=[1, 2]).dict() == {'foo': [1, 2]}
assert Model(foo=[1, 2], bar=['spoon']).dict() == {'foo': [1, 2], 'bar': ['spoon']}
with pytest.raises(ValidationError, match='ensure this value has at least 2 items'):
Model(foo=[1])
@@ -1626,7 +1627,8 @@ def test_conlist():
'title': 'Model',
'type': 'object',
'properties': {
'foo': {'title': 'Foo', 'type': 'array', 'items': {'type': 'integer'}, 'minItems': 2, 'maxItems': 4}
'foo': {'title': 'Foo', 'type': 'array', 'items': {'type': 'integer'}, 'minItems': 2, 'maxItems': 4},
'bar': {'title': 'Bar', 'type': 'array', 'items': {'type': 'string'}, 'minItems': 1, 'maxItems': 4},
},
'required': ['foo'],
}
@@ -1706,6 +1708,27 @@ def test_schema_attributes():
}
def test_path_modify_schema():
class MyPath(Path):
@classmethod
def __modify_schema__(cls, schema):
schema.update(foobar=123)
class Model(BaseModel):
path1: Path
path2: MyPath
assert Model.schema() == {
'title': 'Model',
'type': 'object',
'properties': {
'path1': {'title': 'Path1', 'type': 'string', 'format': 'path'},
'path2': {'title': 'Path2', 'type': 'string', 'format': 'path', 'foobar': 123},
},
'required': ['path1', 'path2'],
}
def test_frozen_set():
class Model(BaseModel):
a: FrozenSet[int] = frozenset({1, 2, 3})