mirror of
https://github.com/kennethreitz/pydantic.git
synced 2026-06-05 23:00:18 +00:00
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:
@@ -0,0 +1 @@
|
||||
Fix for `__modify_schema__` when it conflicted with `field_class_to_schema*`
|
||||
Vendored
+48
-53
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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})
|
||||
|
||||
Reference in New Issue
Block a user