Input and output¶
validobj.validation.parse_input()
takes an input to be processed and
andoutput specification. The useful values for these options and the
transformations that result are described below.
Supported input¶
Validobj is tested for input that can be processed from JSON. This includes:
- Integers and floats
- Strings
- Booleans
- None
- Lists
- Mappings with string keys (although in practice any hashable key should work)
Other “scalar” types, such as datetimes processed from YAML should work fine. However these have no tests and no effort is made to avoid corner cases for more general inputs.
Supported output¶
The above is concerted into a wider set of Python objects with additional restrictions on the type, supported by the typing module.
Simple verbatim input¶
All of the above input is supported verbatim
>>> validobj.parse_input({'a': 4, 'b': [1,2,"tres", None]}, dict)
{'a': 4, 'b': [1, 2, 'tres', None]}
Following typing
, type(None)
can be simply written as None
.
>>> validobj.parse_input(None, None)
Collections¶
Lists can be automatically converted to tuples, sets or frozensets.
>>> validobj.parse_input([1,2,3], frozenset)
frozenset({1, 2, 3})
as well as typed version of the above:
>>> import typing
>>> validobj.parse_input([1,2,3], typing.FrozenSet[int])
frozenset({1, 2, 3})
>>> validobj.parse_input([1,2,'x'], typing.FrozenSet[int])
Traceback (most recent call last):
...
validobj.errors.WrongTypeError: Expecting value of type 'int', not str.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
...
validobj.errors.WrongListItemError: Cannot process list item 3.
The types of the elements of a tuple can be specified either for each element or made homogeneous:
>>> validobj.parse_input([1,2,'x'], typing.Tuple[int, int, str])
(1, 2, 'x')
>>> validobj.parse_input([1,2,3], typing.Tuple[int, ...])
(1, 2, 3)
>>> validobj.parse_input([1,2,'x'], typing.Tuple[int, int])
Traceback (most recent call last):
...
validobj.errors.ValidationError: Expecting value of length 2, not 3
>>> validobj.parse_input([1,2,3, 'x'], typing.Tuple[int, ...])
Traceback (most recent call last):
...
validobj.errors.WrongTypeError: Expecting value of type 'int', not str.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
...
validobj.errors.WrongListItemError: Cannot process list item 4.
Unions¶
typing.Union
and typing.Optional
are supported:
>>> validobj.parse_input("Hello Zah", typing.Union[str, int] )
'Hello Zah'
>>> validobj.parse_input([None, 6], typing.Tuple[typing.Optional[str], int])
(None, 6)
If a given input can be coerced into more than one of the member of the union, then the order matters:
>>> validobj.parse_input([1,2,3], typing.Union[tuple, set])
(1, 2, 3)
>>> validobj.parse_input([1,2,3], typing.Union[set, tuple])
{1, 2, 3}
From Python 3.10, union types can be specified using the X | Y
syntax.
>>> validobj.parse_input([1,2,3], tuple | set)
(1, 2, 3)
Literals¶
typing.Literal
is supported with recent enough versions of the typing module:
>>> validobj.parse_input(5, typing.Literal[1, 2, typing.Literal[5]])
5
Annotaded¶
typing.Annotated
is used to enable custom processing
of types. Other annotation metadata is ignored.
>>> validobj.parse_input(5, typing.Annotated[int, "bogus"])
5
Typed mappings¶
typing.TypedDict
is supported for Python versions older than 3.9,
including with nesting of types.
>>> class Config(typing.TypedDict):
... a: str
... b: typing.Optional[typing.List[int]]
...
>>> validobj.parse_input({"a": "Hello", "b": [1,2,3]}, Config)
{'a': 'Hello', 'b': [1, 2, 3]}
>>> validobj.parse_input({"a": "Hello", "b": [1,2,"three"]}, Config)
...
WrongFieldError: Cannot process field 'b' of value into the corresponding field of 'Config'
typing.Mapping
can be used to restrict types of keys and values, for arbitrary keys;
>>> validobj.parse_input({'key': 'value', 'quantity': 5}, typing.Mapping[str, typing.Union[str, int]])
{'key': 'value', 'quantity': 5}
>>> validobj.parse_input({'key': 'value', 'quantity': 5}, typing.Mapping[str, str])
Traceback (most recent call last):
...
validobj.errors.WrongTypeError: Expecting value of type 'str', not int.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
...
validobj.errors.WrongFieldError: Cannot process value for key 'quantity'
Enums¶
Strings can be automatically converted to valid enum.Enum
elements:
>>> import enum
>>> class Colors(enum.Enum):
... RED = enum.auto()
... GREEN = enum.auto()
... BLUE = enum.auto()
...
>>> validobj.parse_input('RED', Colors)
<Colors.RED: 1>
>>> validobj.parse_input('NORED', Colors)
Traceback (most recent call last):
...
validobj.errors.NotAnEnumItemError: 'NORED' is not a valid member of 'Colors'. Alternatives to invalid value 'NORED' include:
- RED
All valid values are:
- RED
- GREEN
- BLUE
Additionally lists of strings can be turned into instances of
enum.Flag
:
>>> class Permissions(enum.Flag):
... READ = enum.auto()
... WRITE = enum.auto()
... EXECUTE = enum.auto()
...
>>> validobj.parse_input('READ', Permissions)
<Permissions.READ: 1>
>>> validobj.parse_input(['READ', 'EXECUTE'], Permissions)
<Permissions.EXECUTE|READ: 5>
>>> validobj.parse_input([], Permissions)
<Permissions.0: 0>
Dataclasses¶
The dataclasses
module is supported and input is parsed based on the
type annotations:
>>> import dataclasses
>>> @dataclasses.dataclass
... class FileMeta:
... description: str = ""
... keywords: typing.List[str] = dataclasses.field(default_factory=list)
... author: str = ""
>>> @dataclasses.dataclass
... class File:
... location: str
... meta: FileMeta = dataclasses.field(default_factory=FileMeta)
... storage_class: dataclasses.InitVar[str] = "local"
>>> validobj.parse_input({'location': 'https://example.com/file', 'storage_class': 'remote'}, File)
File(location='https://example.com/file', meta=FileMeta(description='', keywords=[], author=''))
Fields with defaults (or default factories) are inferred. Fields that are
themselves dataclasses are processed recursively. Init-only variables using
dataclasses.InitVar
are supported, with the types checked.
Rich tracebacks are produced in case of validation error:
>>> validobj.parse_input({'location': 'https://example.com/file', 'meta':{'keywords': [1, 'x', 'xx']}}, File)
Traceback (most recent call last):
...
validobj.errors.WrongTypeError: Expecting value of type 'str', not int.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
...
validobj.errors.WrongListItemError: Cannot process list item 1.
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
...
validobj.errors.WrongFieldError: Cannot process field 'keywords' of value into the corresponding field of 'FileMeta'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
...
validobj.errors.WrongFieldError: Cannot process field 'meta' of value into the corresponding field of 'File'