Skip to content

Commit

Permalink
add typing information (#475)
Browse files Browse the repository at this point in the history
  • Loading branch information
ds-cbo authored Feb 13, 2023
1 parent 1105b31 commit bd2f9ad
Show file tree
Hide file tree
Showing 10 changed files with 147 additions and 105 deletions.
7 changes: 0 additions & 7 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ jobs:
- { python-version: "3.9", session: "py39" }
- { python-version: "3.8", session: "py38" }
- { python-version: "3.7", session: "py37" }
- { python-version: "3.6", session: "py36" }
- { python-version: "2.7", session: "py27" }

steps:
- name: Check out the repository
Expand All @@ -31,11 +29,6 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Install tox-setuptools-version
if: ${{ matrix.session != 'py27' }}
run: |
pip install tox-setuptools-version
- name: Run tox
run: |
pip install tox
Expand Down
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
license='BSD-3-Clause',
platforms=['any'],
packages=['voluptuous'],
package_data={
'voluptuous': ['py.typed'],
},
author='Alec Thomas',
author_email='alec@swapoff.org',
classifiers=[
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = flake8,py27,py36,py37,py38,py39,py310
envlist = flake8,py37,py38,py39,py310

[flake8]
; E501: line too long (X > 79 characters)
Expand Down
26 changes: 14 additions & 12 deletions voluptuous/error.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import typing


class Error(Exception):
"""Base validation exception."""
Expand All @@ -17,54 +19,54 @@ class Invalid(Error):
"""

def __init__(self, message, path=None, error_message=None, error_type=None):
def __init__(self, message: str, path: typing.Optional[typing.List[str]] = None, error_message: typing.Optional[str] = None, error_type: typing.Optional[str] = None) -> None:
Error.__init__(self, message)
self.path = path or []
self.error_message = error_message or message
self.error_type = error_type

@property
def msg(self):
def msg(self) -> str:
return self.args[0]

def __str__(self):
def __str__(self) -> str:
path = ' @ data[%s]' % ']['.join(map(repr, self.path)) \
if self.path else ''
output = Exception.__str__(self)
if self.error_type:
output += ' for ' + self.error_type
return output + path

def prepend(self, path):
def prepend(self, path: typing.List[str]) -> None:
self.path = path + self.path


class MultipleInvalid(Invalid):
def __init__(self, errors=None):
def __init__(self, errors: typing.Optional[typing.List[Invalid]] = None) -> None:
self.errors = errors[:] if errors else []

def __repr__(self):
def __repr__(self) -> str:
return 'MultipleInvalid(%r)' % self.errors

@property
def msg(self):
def msg(self) -> str:
return self.errors[0].msg

@property
def path(self):
def path(self) -> typing.List[str]:
return self.errors[0].path

@property
def error_message(self):
def error_message(self) -> str:
return self.errors[0].error_message

def add(self, error):
def add(self, error: Invalid) -> None:
self.errors.append(error)

def __str__(self):
def __str__(self) -> str:
return str(self.errors[0])

def prepend(self, path):
def prepend(self, path: typing.List[str]) -> None:
for error in self.errors:
error.prepend(path)

Expand Down
11 changes: 8 additions & 3 deletions voluptuous/humanize.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
from voluptuous import Invalid, MultipleInvalid
from voluptuous.error import Error
from voluptuous.schema_builder import Schema
import typing


MAX_VALIDATION_ERROR_ITEM_LENGTH = 500


def _nested_getitem(data, path):
IndexT = typing.TypeVar("IndexT")


def _nested_getitem(data: typing.Dict[IndexT, typing.Any], path: typing.List[IndexT]) -> typing.Optional[typing.Any]:
for item_index in path:
try:
data = data[item_index]
Expand All @@ -16,7 +21,7 @@ def _nested_getitem(data, path):
return data


def humanize_error(data, validation_error, max_sub_error_length=MAX_VALIDATION_ERROR_ITEM_LENGTH):
def humanize_error(data, validation_error: Invalid, max_sub_error_length: int = MAX_VALIDATION_ERROR_ITEM_LENGTH) -> str:
""" Provide a more helpful + complete validation error message than that provided automatically
Invalid and MultipleInvalid do not include the offending value in error messages,
and MultipleInvalid.__str__ only provides the first error.
Expand All @@ -33,7 +38,7 @@ def humanize_error(data, validation_error, max_sub_error_length=MAX_VALIDATION_E
return '%s. Got %s' % (validation_error, offending_item_summary)


def validate_with_humanized_errors(data, schema, max_sub_error_length=MAX_VALIDATION_ERROR_ITEM_LENGTH):
def validate_with_humanized_errors(data, schema: Schema, max_sub_error_length: int = MAX_VALIDATION_ERROR_ITEM_LENGTH) -> typing.Any:
try:
return schema(data)
except (Invalid, MultipleInvalid) as e:
Expand Down
Empty file added voluptuous/py.typed
Empty file.
49 changes: 30 additions & 19 deletions voluptuous/schema_builder.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import collections
import inspect
import re
Expand All @@ -7,6 +9,9 @@

import itertools
from voluptuous import error as er
from collections.abc import Generator
import typing
from voluptuous.error import Error

if sys.version_info >= (3,):
long = int
Expand Down Expand Up @@ -127,18 +132,21 @@ def __repr__(self):
UNDEFINED = Undefined()


def Self():
def Self() -> None:
raise er.SchemaError('"Self" should never be called')


def default_factory(value):
DefaultFactory = typing.Union[Undefined, typing.Callable[[], typing.Any]]


def default_factory(value) -> DefaultFactory:
if value is UNDEFINED or callable(value):
return value
return lambda: value


@contextmanager
def raises(exc, msg=None, regex=None):
def raises(exc, msg: typing.Optional[str] = None, regex: typing.Optional[re.Pattern] = None) -> Generator[None, None, None]:
try:
yield
except exc as e:
Expand All @@ -148,7 +156,7 @@ def raises(exc, msg=None, regex=None):
assert re.search(regex, str(e)), '%r does not match %r' % (str(e), regex)


def Extra(_):
def Extra(_) -> None:
"""Allow keys in the data that are not present in the schema."""
raise er.SchemaError('"Extra" should never be called')

Expand All @@ -157,6 +165,8 @@ def Extra(_):
# deprecated object, so we just leave an alias here instead.
extra = Extra

Schemable = typing.Union[dict, list, type, typing.Callable]


class Schema(object):
"""A validation schema.
Expand Down Expand Up @@ -186,7 +196,7 @@ class Schema(object):
PREVENT_EXTRA: 'PREVENT_EXTRA',
}

def __init__(self, schema, required=False, extra=PREVENT_EXTRA):
def __init__(self, schema: Schemable, required: bool = False, extra: int = PREVENT_EXTRA) -> None:
"""Create a new Schema.
:param schema: Validation schema. See :module:`voluptuous` for details.
Expand All @@ -207,7 +217,7 @@ def __init__(self, schema, required=False, extra=PREVENT_EXTRA):
self._compiled = self._compile(schema)

@classmethod
def infer(cls, data, **kwargs):
def infer(cls, data, **kwargs) -> Schema:
"""Create a Schema from concrete data (e.g. an API response).
For example, this will take a dict like:
Expand Down Expand Up @@ -723,7 +733,7 @@ def validate_set(path, data):

return validate_set

def extend(self, schema, required=None, extra=None):
def extend(self, schema: dict, required: typing.Optional[bool] = None, extra: typing.Optional[int] = None) -> Schema:
"""Create a new `Schema` by merging this and the provided `schema`.
Neither this `Schema` nor the provided `schema` are modified. The
Expand All @@ -738,6 +748,7 @@ def extend(self, schema, required=None, extra=None):
"""

assert type(self.schema) == dict and type(schema) == dict, 'Both schemas must be dictionary-based'
assert isinstance(self.schema, dict)

result = self.schema.copy()

Expand Down Expand Up @@ -936,7 +947,7 @@ class Msg(object):
... assert isinstance(e.errors[0], er.RangeInvalid)
"""

def __init__(self, schema, msg, cls=None):
def __init__(self, schema: dict, msg: str, cls: typing.Optional[typing.Type[Error]] = None) -> None:
if cls and not issubclass(cls, er.Invalid):
raise er.SchemaError("Msg can only use subclases of"
" Invalid as custom class")
Expand All @@ -961,7 +972,7 @@ def __repr__(self):
class Object(dict):
"""Indicate that we should work with attributes, not keys."""

def __init__(self, schema, cls=UNDEFINED):
def __init__(self, schema, cls: object = UNDEFINED) -> None:
self.cls = cls
super(Object, self).__init__(schema)

Expand All @@ -977,7 +988,7 @@ def __repr__(self):
class Marker(object):
"""Mark nodes for special treatment."""

def __init__(self, schema_, msg=None, description=None):
def __init__(self, schema_: dict, msg: typing.Optional[str] = None, description: typing.Optional[str] = None) -> None:
self.schema = schema_
self._schema = Schema(schema_)
self.msg = msg
Expand Down Expand Up @@ -1009,7 +1020,7 @@ def __eq__(self, other):
return self.schema == other

def __ne__(self, other):
return not(self.schema == other)
return not (self.schema == other)


class Optional(Marker):
Expand All @@ -1035,7 +1046,7 @@ class Optional(Marker):
{'key2': 'value'}
"""

def __init__(self, schema, msg=None, default=UNDEFINED, description=None):
def __init__(self, schema: dict, msg: typing.Optional[str] = None, default=UNDEFINED, description: typing.Optional[str] = None) -> None:
super(Optional, self).__init__(schema, msg=msg,
description=description)
self.default = default_factory(default)
Expand Down Expand Up @@ -1077,7 +1088,7 @@ class Exclusive(Optional):
... 'social': {'social_network': 'barfoo', 'token': 'tEMp'}})
"""

def __init__(self, schema, group_of_exclusion, msg=None, description=None):
def __init__(self, schema: dict, group_of_exclusion: str, msg: typing.Optional[str] = None, description: typing.Optional[str] = None) -> None:
super(Exclusive, self).__init__(schema, msg=msg,
description=description)
self.group_of_exclusion = group_of_exclusion
Expand Down Expand Up @@ -1125,8 +1136,8 @@ class Inclusive(Optional):
True
"""

def __init__(self, schema, group_of_inclusion,
msg=None, description=None, default=UNDEFINED):
def __init__(self, schema: dict, group_of_inclusion: str,
msg: typing.Optional[str] = None, description: typing.Optional[str] = None, default=UNDEFINED) -> None:
super(Inclusive, self).__init__(schema, msg=msg,
default=default,
description=description)
Expand All @@ -1148,7 +1159,7 @@ class Required(Marker):
{'key': []}
"""

def __init__(self, schema, msg=None, default=UNDEFINED, description=None):
def __init__(self, schema: dict, msg: typing.Optional[str] = None, default=UNDEFINED, description: typing.Optional[str] = None) -> None:
super(Required, self).__init__(schema, msg=msg,
description=description)
self.default = default_factory(default)
Expand All @@ -1169,7 +1180,7 @@ class Remove(Marker):
[1, 2, 3, 5, '7']
"""

def __call__(self, v):
def __call__(self, v: object):
super(Remove, self).__call__(v)
return self.__class__

Expand All @@ -1180,7 +1191,7 @@ def __hash__(self):
return object.__hash__(self)


def message(default=None, cls=None):
def message(default: typing.Optional[str] = None, cls: typing.Optional[typing.Type[Error]] = None) -> typing.Callable:
"""Convenience decorator to allow functions to provide a message.
Set a default message:
Expand Down Expand Up @@ -1251,7 +1262,7 @@ def _merge_args_with_kwargs(args_dict, kwargs_dict):
return ret


def validate(*a, **kw):
def validate(*a, **kw) -> typing.Callable:
"""Decorator for validating arguments of a function against a given schema.
Set restrictions for arguments:
Expand Down
27 changes: 14 additions & 13 deletions voluptuous/tests/tests.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,5 @@
import collections
import copy

try:
from enum import Enum
except ImportError:
Enum = None
import os
import sys

import pytest
from voluptuous.util import Capitalize, Lower, Strip, Title, Upper, u
from voluptuous.humanize import humanize_error
from voluptuous import (ALLOW_EXTRA, PREVENT_EXTRA, All, Any, Clamp, Coerce,
Contains, Date, Datetime, Email, Equal, ExactSequence,
Exclusive, Extra, FqdnUrl, In, Inclusive, Invalid,
Expand All @@ -17,8 +8,18 @@
Optional, PathExists, Range, Remove, Replace, Required,
Schema, Self, SomeOf, TooManyValid, TypeInvalid, Union,
Unordered, Url, raises, validate)
from voluptuous.humanize import humanize_error
from voluptuous.util import Capitalize, Lower, Strip, Title, Upper, u
import pytest
import sys
import os
import collections
import copy
import typing

Enum: typing.Union[type, None]
try:
from enum import Enum
except ImportError:
Enum = None


def test_new_required_test():
Expand Down
Loading

0 comments on commit bd2f9ad

Please sign in to comment.