From bd2f9adc7b38ba28b93bf00fdbd931843a48300c Mon Sep 17 00:00:00 2001 From: DS/Charlie <82801887+ds-cbo@users.noreply.github.com> Date: Mon, 13 Feb 2023 10:41:18 +0100 Subject: [PATCH] add typing information (#475) --- .github/workflows/tests.yml | 7 --- setup.py | 3 ++ tox.ini | 2 +- voluptuous/error.py | 26 +++++----- voluptuous/humanize.py | 11 +++-- voluptuous/py.typed | 0 voluptuous/schema_builder.py | 49 +++++++++++-------- voluptuous/tests/tests.py | 27 ++++++----- voluptuous/util.py | 33 +++++++------ voluptuous/validators.py | 94 ++++++++++++++++++++++-------------- 10 files changed, 147 insertions(+), 105 deletions(-) create mode 100644 voluptuous/py.typed diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9b9413f..7d28ec4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 @@ -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 diff --git a/setup.py b/setup.py index be6b6b3..cd0254f 100644 --- a/setup.py +++ b/setup.py @@ -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=[ diff --git a/tox.ini b/tox.ini index e09e222..25d2eb8 100644 --- a/tox.ini +++ b/tox.ini @@ -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) diff --git a/voluptuous/error.py b/voluptuous/error.py index 97f37d2..35d392e 100644 --- a/voluptuous/error.py +++ b/voluptuous/error.py @@ -1,3 +1,5 @@ +import typing + class Error(Exception): """Base validation exception.""" @@ -17,17 +19,17 @@ 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) @@ -35,36 +37,36 @@ def __str__(self): 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) diff --git a/voluptuous/humanize.py b/voluptuous/humanize.py index 91ab201..734f367 100644 --- a/voluptuous/humanize.py +++ b/voluptuous/humanize.py @@ -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] @@ -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. @@ -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: diff --git a/voluptuous/py.typed b/voluptuous/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/voluptuous/schema_builder.py b/voluptuous/schema_builder.py index 0cb207f..e7e8d35 100644 --- a/voluptuous/schema_builder.py +++ b/voluptuous/schema_builder.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import collections import inspect import re @@ -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 @@ -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: @@ -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') @@ -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. @@ -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. @@ -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: @@ -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 @@ -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() @@ -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") @@ -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) @@ -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 @@ -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): @@ -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) @@ -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 @@ -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) @@ -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) @@ -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__ @@ -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: @@ -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: diff --git a/voluptuous/tests/tests.py b/voluptuous/tests/tests.py index 58512df..ef4580b 100644 --- a/voluptuous/tests/tests.py +++ b/voluptuous/tests/tests.py @@ -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, @@ -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(): diff --git a/voluptuous/util.py b/voluptuous/util.py index f57b8d7..58b9195 100644 --- a/voluptuous/util.py +++ b/voluptuous/util.py @@ -1,13 +1,16 @@ import sys -from voluptuous.error import LiteralInvalid, TypeInvalid, Invalid -from voluptuous.schema_builder import Schema, default_factory, raises -from voluptuous import validators +# F401: "imported but unused" +from voluptuous.error import LiteralInvalid, TypeInvalid, Invalid # noqa: F401 +from voluptuous.schema_builder import Schema, default_factory, raises # noqa: F401 +from voluptuous import validators # noqa: F401 +from voluptuous.schema_builder import DefaultFactory # noqa: F401 +import typing __author__ = 'tusharmakkar08' -def _fix_str(v): +def _fix_str(v: str) -> str: if sys.version_info[0] == 2 and isinstance(v, unicode): # noqa: F821 s = v else: @@ -15,7 +18,7 @@ def _fix_str(v): return s -def Lower(v): +def Lower(v: str) -> str: """Transform a string to lower case. >>> s = Schema(Lower) @@ -25,7 +28,7 @@ def Lower(v): return _fix_str(v).lower() -def Upper(v): +def Upper(v: str) -> str: """Transform a string to upper case. >>> s = Schema(Upper) @@ -35,7 +38,7 @@ def Upper(v): return _fix_str(v).upper() -def Capitalize(v): +def Capitalize(v: str) -> str: """Capitalise a string. >>> s = Schema(Capitalize) @@ -45,7 +48,7 @@ def Capitalize(v): return _fix_str(v).capitalize() -def Title(v): +def Title(v: str) -> str: """Title case a string. >>> s = Schema(Title) @@ -55,7 +58,7 @@ def Title(v): return _fix_str(v).title() -def Strip(v): +def Strip(v: str) -> str: """Strip whitespace from a string. >>> s = Schema(Strip) @@ -76,7 +79,7 @@ class DefaultTo(object): [] """ - def __init__(self, default_value, msg=None): + def __init__(self, default_value, msg: typing.Optional[str] = None) -> None: self.default_value = default_factory(default_value) self.msg = msg @@ -99,7 +102,7 @@ class SetTo(object): 42 """ - def __init__(self, value): + def __init__(self, value) -> None: self.value = default_factory(value) def __call__(self, v): @@ -121,7 +124,7 @@ class Set(object): ... s([set([1, 2]), set([3, 4])]) """ - def __init__(self, msg=None): + def __init__(self, msg: typing.Optional[str] = None) -> None: self.msg = msg def __call__(self, v): @@ -137,10 +140,10 @@ def __repr__(self): class Literal(object): - def __init__(self, lit): + def __init__(self, lit) -> None: self.lit = lit - def __call__(self, value, msg=None): + def __call__(self, value, msg: typing.Optional[str] = None): if self.lit != value: raise LiteralInvalid( msg or '%s not match for %s' % (value, self.lit) @@ -155,7 +158,7 @@ def __repr__(self): return repr(self.lit) -def u(x): +def u(x: str) -> str: if sys.version_info < (3,): return unicode(x) # noqa: F821 else: diff --git a/voluptuous/validators.py b/voluptuous/validators.py index 35a80e6..776adb8 100644 --- a/voluptuous/validators.py +++ b/voluptuous/validators.py @@ -1,20 +1,25 @@ +from voluptuous.error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, + AnyInvalid, AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, + RangeInvalid, PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, + DateInvalid, InInvalid, TypeInvalid, NotInInvalid, ContainsInvalid, NotEnoughValid, + TooManyValid) + +# F401: flake8 complains about 'raises' not being used, but it is used in doctests +from voluptuous.schema_builder import Schema, raises, message, Schemable # noqa: F401 import os import re import datetime import sys from functools import wraps from decimal import Decimal, InvalidOperation +import typing + +Enum: typing.Union[type, None] try: from enum import Enum except ImportError: Enum = None -from voluptuous.schema_builder import Schema, raises, message -from voluptuous.error import (MultipleInvalid, CoerceInvalid, TrueInvalid, FalseInvalid, BooleanInvalid, Invalid, - AnyInvalid, AllInvalid, MatchInvalid, UrlInvalid, EmailInvalid, FileInvalid, DirInvalid, - RangeInvalid, PathInvalid, ExactSequenceInvalid, LengthInvalid, DatetimeInvalid, - DateInvalid, InInvalid, TypeInvalid, NotInInvalid, ContainsInvalid, NotEnoughValid, - TooManyValid) if sys.version_info >= (3,): import urllib.parse as urlparse @@ -53,7 +58,7 @@ __author__ = 'tusharmakkar08' -def truth(f): +def truth(f: typing.Callable) -> typing.Callable: """Convenience decorator to convert truth functions into validators. >>> @truth @@ -97,7 +102,7 @@ class Coerce(object): ... validate('foo') """ - def __init__(self, type, msg=None): + def __init__(self, type: type, msg: typing.Optional[str] = None) -> None: self.type = type self.msg = msg self.type_name = type.__name__ @@ -203,13 +208,13 @@ class _WithSubValidators(object): sub-validators are compiled by the parent `Schema`. """ - def __init__(self, *validators, **kwargs): + def __init__(self, *validators, msg=None, required=False, discriminant=None, **kwargs) -> None: self.validators = validators - self.msg = kwargs.pop('msg', None) - self.required = kwargs.pop('required', False) - self.discriminant = kwargs.pop('discriminant', None) + self.msg = msg + self.required = required + self.discriminant = discriminant - def __voluptuous_compile__(self, schema): + def __voluptuous_compile__(self, schema: Schema) -> typing.Callable: self._compiled = [] old_required = schema.required self.schema = schema @@ -219,7 +224,7 @@ def __voluptuous_compile__(self, schema): schema.required = old_required return self._run - def _run(self, path, value): + def _run(self, path: typing.List[str], value): if self.discriminant is not None: self._compiled = [ self.schema._compile(v) @@ -238,6 +243,9 @@ def __repr__(self): self.msg ) + def _exec(self, funcs: typing.Iterable, v, path: typing.Optional[typing.List[str]] = None): + raise NotImplementedError() + class Any(_WithSubValidators): """Use the first validated value. @@ -379,7 +387,7 @@ class Match(object): '0x123ef4' """ - def __init__(self, pattern, msg=None): + def __init__(self, pattern: typing.Union[re.Pattern, str], msg: typing.Optional[str] = None) -> None: if isinstance(pattern, basestring): pattern = re.compile(pattern) self.pattern = pattern @@ -407,7 +415,7 @@ class Replace(object): 'I say goodbye' """ - def __init__(self, pattern, substitution, msg=None): + def __init__(self, pattern: typing.Union[re.Pattern, str], substitution: str, msg: typing.Optional[str] = None) -> None: if isinstance(pattern, basestring): pattern = re.compile(pattern) self.pattern = pattern @@ -423,7 +431,7 @@ def __repr__(self): self.msg) -def _url_validation(v): +def _url_validation(v: str) -> urlparse.ParseResult: parsed = urlparse.urlparse(v) if not parsed.scheme or not parsed.netloc: raise UrlInvalid("must have a URL scheme and host") @@ -556,7 +564,7 @@ def PathExists(v): raise PathInvalid("Not a Path") -def Maybe(validator, msg=None): +def Maybe(validator: typing.Callable, msg: typing.Optional[str] = None): """Validate that the object matches given validator or is None. :raises Invalid: If the value does not match the given validator and is not @@ -572,6 +580,9 @@ def Maybe(validator, msg=None): return Any(None, validator, msg=msg) +NullableNumber = typing.Union[int, float, None] + + class Range(object): """Limit a value to a range. @@ -593,8 +604,9 @@ class Range(object): ... Schema(Range(max=10, max_included=False))(20) """ - def __init__(self, min=None, max=None, min_included=True, - max_included=True, msg=None): + def __init__(self, min: NullableNumber = None, max: NullableNumber = None, + min_included: bool = True, max_included: bool = True, + msg: typing.Optional[str] = None) -> None: self.min = min self.max = max self.min_included = min_included @@ -649,7 +661,8 @@ class Clamp(object): 0 """ - def __init__(self, min=None, max=None, msg=None): + def __init__(self, min: NullableNumber = None, max: NullableNumber = None, + msg: typing.Optional[str] = None) -> None: self.min = min self.max = max self.msg = msg @@ -674,7 +687,8 @@ def __repr__(self): class Length(object): """The length of a value must be in a certain range.""" - def __init__(self, min=None, max=None, msg=None): + def __init__(self, min: NullableNumber = None, max: NullableNumber = None, + msg: typing.Optional[str] = None) -> None: self.min = min self.max = max self.msg = msg @@ -703,7 +717,7 @@ class Datetime(object): DEFAULT_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ' - def __init__(self, format=None, msg=None): + def __init__(self, format: typing.Optional[str] = None, msg: typing.Optional[str] = None) -> None: self.format = format or self.DEFAULT_FORMAT self.msg = msg @@ -741,7 +755,7 @@ def __repr__(self): class In(object): """Validate that a value is in a collection.""" - def __init__(self, container, msg=None): + def __init__(self, container: typing.Iterable, msg: typing.Optional[str] = None) -> None: self.container = container self.msg = msg @@ -762,7 +776,7 @@ def __repr__(self): class NotIn(object): """Validate that a value is not in a collection.""" - def __init__(self, container, msg=None): + def __init__(self, container: typing.Iterable, msg: typing.Optional[str] = None) -> None: self.container = container self.msg = msg @@ -790,7 +804,7 @@ class Contains(object): ... s([3, 2]) """ - def __init__(self, item, msg=None): + def __init__(self, item, msg: typing.Optional[str] = None) -> None: self.item = item self.msg = msg @@ -823,9 +837,9 @@ class ExactSequence(object): ('hourly_report', 10, [], []) """ - def __init__(self, validators, **kwargs): + def __init__(self, validators: typing.Iterable[Schemable], msg: typing.Optional[str] = None, **kwargs) -> None: self.validators = validators - self.msg = kwargs.pop('msg', None) + self.msg = msg self._schemas = [Schema(val, **kwargs) for val in validators] def __call__(self, v): @@ -868,7 +882,7 @@ class Unique(object): ... s('aabbc') """ - def __init__(self, msg=None): + def __init__(self, msg: typing.Optional[str] = None) -> None: self.msg = msg def __call__(self, v): @@ -904,7 +918,7 @@ class Equal(object): ... s('foo') """ - def __init__(self, target, msg=None): + def __init__(self, target, msg: typing.Optional[str] = None) -> None: self.target = target self.msg = msg @@ -932,7 +946,8 @@ class Unordered(object): [1, 'foo'] """ - def __init__(self, validators, msg=None, **kwargs): + def __init__(self, validators: typing.Iterable[Schemable], + msg: typing.Optional[str] = None, **kwargs) -> None: self.validators = validators self.msg = msg self._schemas = [Schema(val, **kwargs) for val in validators] @@ -989,7 +1004,8 @@ class Number(object): Decimal('1234.01') """ - def __init__(self, precision=None, scale=None, msg=None, yield_decimal=False): + def __init__(self, precision: typing.Optional[int] = None, scale: typing.Optional[int] = None, + msg: typing.Optional[str] = None, yield_decimal: bool = False) -> None: self.precision = precision self.scale = scale self.msg = msg @@ -1021,7 +1037,7 @@ def __call__(self, v): def __repr__(self): return ('Number(precision=%s, scale=%s, msg=%s)' % (self.precision, self.scale, self.msg)) - def _get_precision_scale(self, number): + def _get_precision_scale(self, number) -> typing.Tuple[int, int, Decimal]: """ :param number: :return: tuple(precision, scale, decimal_number) @@ -1031,7 +1047,13 @@ def _get_precision_scale(self, number): except InvalidOperation: raise Invalid(self.msg or 'Value must be a number enclosed with string') - return (len(decimal_num.as_tuple().digits), -(decimal_num.as_tuple().exponent), decimal_num) + exp = decimal_num.as_tuple().exponent + if isinstance(exp, int): + return (len(decimal_num.as_tuple().digits), -exp, decimal_num) + else: + # TODO: handle infinity and NaN + # raise Invalid(self.msg or 'Value has no precision') + raise TypeError("infinity and NaN have no precision") class SomeOf(_WithSubValidators): @@ -1058,7 +1080,9 @@ class SomeOf(_WithSubValidators): ... validate(6.2) """ - def __init__(self, validators, min_valid=None, max_valid=None, **kwargs): + def __init__(self, validators: typing.List[Schemable], + min_valid: typing.Optional[int] = None, max_valid: typing.Optional[int] = None, + **kwargs) -> None: assert min_valid is not None or max_valid is not None, \ 'when using "%s" you should specify at least one of min_valid and max_valid' % (type(self).__name__,) self.min_valid = min_valid or 0