Skip to content
This repository has been archived by the owner on Mar 24, 2024. It is now read-only.

Commit

Permalink
Merge pull request #209 from quantmind/ls-docs
Browse files Browse the repository at this point in the history
required is True by default
  • Loading branch information
lsbardel committed Mar 3, 2020
2 parents 1dd3781 + 2929714 commit d535034
Show file tree
Hide file tree
Showing 8 changed files with 193 additions and 22 deletions.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ Contents
:maxdepth: 2

reference
validation
queries
env
glossary
Expand Down
33 changes: 33 additions & 0 deletions docs/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,31 @@ Reference
Data
====

DataView
--------

.. module:: openapi.data.view

.. autoclass:: DataView
:members:

TypeInfo
--------

.. module:: openapi.utils

.. autoclass:: TypingInfo
:members:


Data Fields
===========

.. module:: openapi.data.fields

.. autofunction:: data_field


String field
------------
.. autofunction:: str_field
Expand Down Expand Up @@ -70,6 +83,26 @@ JSON field
Data Validation
===============

.. module:: openapi.data.validate

Validate
-----------------------

The entry function to validate input data and return a python representation.
The function accept as input a valid type annotation or a :class:`.TypingInfo` object.

.. autofunction:: validate


Validate Schema
-----------------------

Same as the :func:`.validate` but returns the validation schema object rather than
simple data types (this is mainly different for dataclasses)

.. autofunction:: validated_schema


Dataclass from db table
-----------------------
.. module:: openapi.data.db
Expand Down
61 changes: 61 additions & 0 deletions docs/validation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
.. _aio-openapi-validation:


===========
Validation
===========

Validation is an important component of the library and it is designed to validate
data to and from JSON serializable objects.

To validate a simple list of integers

.. code-block:: python
from typing import List
from openapi.data.validate import validate
validate(List[int], [5,2,4,8])
# ValidatedData(data=[5, 2, 4, 8], errors={})
validate(List[int], [5,2,"5",8])
# ValidatedData(data=None, errors='not valid type')
The main object for validation are python dataclasses:


.. code-block:: python
from dataclasses import dataclass
from typing import Union
@dataclass
class Foo:
text: str
param: Union[str, int]
done: bool = False
validate(Foo, {})
# ValidatedData(data=None, errors={'text': 'required', 'param': 'required'})
validate(Foo, dict(text=1))
# ValidatedData(data=None, errors={'text': 'not valid type', 'param': 'required'})
validate(Foo, dict(text="ciao", param=3))
# ValidatedData(data={'text': 'ciao', 'param': 3, 'done': False}, errors={})
Validated Schema
================

Use the :func:`.validated_schema` to validate input data and return an instance of the
validation schema. This differes from :func:`.validate` only when dataclasses are involved

.. code-block:: python
from openapi.data.validate import validated_schema
validated_schema(Foo, dict(text="ciao", param=3))
# Foo(text='ciao', param=3, done=False)
81 changes: 65 additions & 16 deletions openapi/data/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,16 @@ def __init__(self, errors) -> None:
self.errors = errors


def validated_schema(schema, data, *, strict: bool = True):
data = validate(schema, data, strict=strict, raise_on_errors=True)
return schema(**data)
def validated_schema(schema, data, *, strict: bool = True) -> Any:
"""Validate data with a given schema and return a valid representation of the data
as a schema instance
:param schema: a typing annotation or a :class:`.TypingInfo` object
:param data: a data object to validate against the schema
:param strict: if `True` validation is strict, i.e. missing required parameters
will cause validation to fails
"""
return validate(schema, data, strict=strict, raise_on_errors=True, as_schema=True)


def validate(
Expand All @@ -31,25 +38,49 @@ def validate(
strict: bool = True,
multiple: bool = False,
raise_on_errors: bool = False,
as_schema: bool = False,
) -> Any:
"""Validate a dictionary of data with a given dataclass
"""Validate data with a given schema
:param schema: a typing annotation or a :class:`.TypingInfo` object
:param data: a data object to validate against the schema
:param strict: if `True` validation is strict, i.e. missing required parameters
will cause validation to fails
:param multiple: allow parameters to have multiple values
:param raise_on_errors: when `True` failure of validation will result in a
`ValidationErrors` error, otherwise a :class:`.ValidatedData` object
is returned.
:param as_schema: return the schema object rather than simple data type
(dataclass rather than dict for example)
"""
type_info = TypingInfo.get(schema)
try:
if type_info.container is list:
vdata = validate_list(
type_info.element, data, strict=strict, multiple=multiple,
type_info.element,
data,
strict=strict,
multiple=multiple,
as_schema=as_schema,
)
elif type_info.container is dict:
vdata = validate_dict(
type_info.element, data, strict=strict, multiple=multiple,
type_info.element,
data,
strict=strict,
multiple=multiple,
as_schema=as_schema,
)
elif type_info.is_dataclass:
vdata = validate_dataclass(
type_info.element, data, strict=strict, multiple=multiple,
type_info.element,
data,
strict=strict,
multiple=multiple,
as_schema=as_schema,
)
elif type_info.is_union:
vdata = validate_union(type_info.element, data)
vdata = validate_union(type_info.element, data, as_schema=as_schema)
else:
vdata = validate_simple(type_info.element, data)
except ValidationErrors as e:
Expand All @@ -66,23 +97,35 @@ def validate_simple(schema: type, data: Any) -> Any:
raise ValidationErrors(NOT_VALID_TYPE)


def validate_union(schema: Tuple[TypingInfo, ...], data: Any) -> Any:
def validate_union(
schema: Tuple[TypingInfo, ...], data: Any, as_schema: bool = False
) -> Any:
for type_info in schema:
try:
return validate(type_info, data, raise_on_errors=True)
return validate(type_info, data, raise_on_errors=True, as_schema=as_schema)
except ValidationErrors:
continue
raise ValidationErrors(NOT_VALID_TYPE)


def validate_list(
schema: type, data: list, *, strict: bool = True, multiple: bool = False,
schema: type,
data: list,
*,
strict: bool = True,
multiple: bool = False,
as_schema: bool = False,
) -> ValidatedData:
validated = []
if isinstance(data, list):
for d in data:
v = validate(
schema, d, strict=strict, multiple=multiple, raise_on_errors=True
schema,
d,
strict=strict,
multiple=multiple,
raise_on_errors=True,
as_schema=as_schema,
)
validated.append(v)
return validated
Expand All @@ -96,14 +139,19 @@ def validate_dict(
*,
strict: bool = True,
multiple: bool = False,
raise_on_errors: bool = True,
as_schema: bool = False,
) -> ValidatedData:
if isinstance(data, dict):
validated = ValidatedData(data={}, errors={})
for name, d in data.items():
try:
validated.data[name] = validate(
schema, d, strict=strict, multiple=multiple, raise_on_errors=True
schema,
d,
strict=strict,
multiple=multiple,
raise_on_errors=True,
as_schema=as_schema,
)
except ValidationErrors as exc:
validated.errors[name] = exc.errors
Expand All @@ -120,13 +168,14 @@ def validate_dataclass(
*,
strict: bool = True,
multiple: bool = False,
as_schema: bool = False,
) -> ValidatedData:
errors: Dict = {}
cleaned: Dict = {}
data = MultiDict(data)
for field in fields(schema):
try:
required = field.metadata.get(REQUIRED)
required = field.metadata.get(REQUIRED, True)
default = get_default(field)
if strict and default is not None and data.get(field.name) is None:
data[field.name] = default
Expand Down Expand Up @@ -166,7 +215,7 @@ def validate_dataclass(

if errors:
raise ValidationErrors(errors=errors)
return cleaned
return schema(**cleaned) if as_schema else cleaned


def collect_value(field: Field, name: str, value: Any) -> Any:
Expand Down
10 changes: 10 additions & 0 deletions openapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,23 +57,33 @@ def get_origin(value: Any) -> Any:


class TypingInfo(NamedTuple):
"""Information about a type annotation"""

element: ElementType
container: Optional[type] = None

@property
def is_dataclass(self) -> bool:
"""True if :attr:`.element` is a dataclass"""
return not self.container and is_dataclass(self.element)

@property
def is_union(self) -> bool:
"""True if :attr:`.element` is a union of typing info"""
return isinstance(self.element, tuple)

@property
def is_complex(self) -> bool:
"""True if :attr:`.element` is either a dataclass or a union"""
return self.container or self.is_union

@classmethod
def get(cls, value: Any) -> Optional["TypingInfo"]:
"""Create a :class:`.TypingInfo` from a typing annotation or
another typing info
:param value: typing annotation
"""
if value is None or isinstance(value, cls):
return value
origin = get_origin(value)
Expand Down
9 changes: 4 additions & 5 deletions openapi/ws/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
import hashlib
import logging
import time
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Dict

from aiohttp import web

from .. import json
from ..data import fields
from ..data.validate import ValidationErrors, validated_schema
from ..utils import compact
from .channels import Channels
Expand All @@ -18,9 +17,9 @@

@dataclass
class RpcProtocol:
id: str = fields.data_field(required=True)
method: str = fields.data_field(required=True)
payload: Dict = None
id: str
method: str
payload: Dict = field(default_factory=dict)


class ProtocolError(RuntimeError):
Expand Down
13 changes: 12 additions & 1 deletion tests/data/test_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

from openapi.data.fields import ListValidator, NumberValidator
from openapi.data.validate import ValidationErrors, validate, validated_schema
from tests.example.models import Moon, Permission, Role, TaskAdd
from tests.example.models import Moon, Permission, Role, TaskAdd, Foo


def test_validated_schema():
Expand Down Expand Up @@ -79,3 +79,14 @@ def test_validate_union_nested():
assert d.data == 3
d = validate(schema, dict(foo=3, bla="ciao"))
assert d.data == dict(foo=3, bla="ciao")


def test_foo():
assert validate(Foo, {}).errors
assert validate(Foo, dict(text="ciao")).errors
assert validate(Foo, dict(text="ciao"), strict=False).data == dict(text="ciao")
valid = dict(text="ciao", param=3)
assert validate(Foo, valid).data == dict(text="ciao", param=3, done=False)
d = validated_schema(List[Foo], [valid])
assert len(d) == 1
assert isinstance(d[0], Foo)
7 changes: 7 additions & 0 deletions tests/example/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,10 @@ class Moon:
description="Comma separated list of names",
post_process=lambda values: [v.strip() for v in values.split(",")],
)


@dataclass
class Foo:
text: str
param: Union[str, int]
done: bool = False

0 comments on commit d535034

Please sign in to comment.