diff --git a/src/validr/__init__.py b/src/validr/__init__.py index b0c4ef7..026732f 100644 --- a/src/validr/__init__.py +++ b/src/validr/__init__.py @@ -1,9 +1,18 @@ """A simple, fast, extensible python library for data validation.""" -from .exception import Invalid, ModelInvalid, SchemaError, ValidrError, mark_index, mark_key +from .model import ImmutableInstanceError, asdict, fields, modelclass +from .schema import Builder, Compiler, Schema, T from .validator import ( - create_re_validator, create_enum_validator, builtin_validators, validator) -from .schema import Schema, Compiler, T, Builder -from .model import modelclass, fields, asdict, ImmutableInstanceError + Invalid, + ModelInvalid, + SchemaError, + ValidrError, + builtin_validators, + create_enum_validator, + create_re_validator, +) +from .validator import py_mark_index as mark_index +from .validator import py_mark_key as mark_key +from .validator import validator __all__ = ( 'ValidrError', 'Invalid', 'ModelInvalid', 'SchemaError', diff --git a/src/validr/_exception_c.pyx b/src/validr/_exception_c.pyx deleted file mode 100644 index e2ae48d..0000000 --- a/src/validr/_exception_c.pyx +++ /dev/null @@ -1,176 +0,0 @@ -_NOT_SET = object() - - -cdef _shorten(str text, int length): - if len(text) > length: - return text[:length] + '..' - return text - - -cdef _format_value(value): - if isinstance(value, str): - return repr(_shorten(value, 75)) - else: - return _shorten(str(value), 75) - - -cdef _format_error(args, str position, str value_clause=None): - cdef str msg = str(args[0]) if args else 'invalid' - if position: - msg = '%s: %s' % (position, msg) - if value_clause: - msg = '%s, %s' % (msg, value_clause) - return msg - - -class ValidrError(ValueError): - """Base exception of validr""" - - def __init__(self, *args, value=_NOT_SET, **kwargs): - super().__init__(*args, **kwargs) - self._value = value - # marks item: (is_key, index_or_key) - self.marks = [] - - def mark_index(self, int index=-1): - self.marks.append((False, index)) - return self - - def mark_key(self, str key): - self.marks.append((True, key)) - return self - - @property - def has_value(self): - """Check has value set""" - return self._value is not _NOT_SET - - def set_value(self, value): - """Set value if not set""" - if self._value is _NOT_SET: - self._value = value - - @property - def value(self): - """The invalid value""" - if self._value is _NOT_SET: - return None - return self._value - - @property - def field(self): - """First level index or key, usually it's the field""" - if not self.marks: - return None - __, index_or_key = self.marks[-1] - return index_or_key - - @property - def position(self): - """A string which represent the position of invalid. - - For example: - - { - "tags": ["ok", "invalid"], # tags[1] - "user": { - "name": "invalid", # user.name - "age": 500 # user.age - } - } - """ - cdef str text = '' - cdef bint is_key - for is_key, index_or_key in reversed(self.marks): - if is_key: - text = '%s.%s' % (text, index_or_key) - else: - if index_or_key == -1: - text = '%s[]' % text - else: - text = '%s[%d]' % (text, index_or_key) - if text and text[0] == '.': - text = text[1:] - return text - - @property - def message(self): - """Error message""" - if self.args: - return self.args[0] - else: - return None - - def __str__(self): - return _format_error(self.args, self.position) - - -class Invalid(ValidrError): - """Data invalid""" - def __str__(self): - cdef str value_clause = None - if self.has_value: - value_clause = 'value=%s' % _format_value(self.value) - return _format_error(self.args, self.position, value_clause) - - -class ModelInvalid(Invalid): - """Model data invalid""" - def __init__(self, errors): - if not errors: - raise ValueError('errors is required') - self.errors = errors - message = errors[0].message or 'invalid' - message += ' ...total {} errors'.format(len(errors)) - super().__init__(message) - - def __str__(self): - error_line_s = [] - for ex in self.errors: - error_line_s.append('{} is {}'.format(ex.position, ex.message)) - return '; '.join(error_line_s) - - -class SchemaError(ValidrError): - """Schema error""" - def __str__(self): - cdef str value_clause = None - if self.has_value: - value_clause = 'schema=%s' % self.value.repr(prefix=False, desc=False) - return _format_error(self.args, self.position, value_clause) - - -cdef class _mark_index: - """Add current index to Invalid/SchemaError""" - - cdef int index - - def __init__(self, index=-1): - """index = -1 means the position is uncertainty""" - self.index = index - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - if exc_type is not None and issubclass(exc_type, ValidrError): - exc_val.mark_index(self.index) - -class mark_index(_mark_index): pass - -cdef class _mark_key: - """Add current key to Invalid/SchemaError""" - - cdef str key - - def __init__(self, key): - self.key = key - - def __enter__(self): - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - if exc_type is not None and issubclass(exc_type, ValidrError): - exc_val.mark_key(self.key) - -class mark_key(_mark_key): pass diff --git a/src/validr/_validator_c.pyx b/src/validr/_validator_c.pyx index 809aa10..5666267 100644 --- a/src/validr/_validator_c.pyx +++ b/src/validr/_validator_c.pyx @@ -9,12 +9,190 @@ from copy import copy from functools import partial from urllib.parse import urlparse, urlunparse -from .exception import Invalid, SchemaError, mark_key, mark_index from ._vendor import durationpy from ._vendor.email_validator import validate_email, EmailNotValidError from ._vendor.fqdn import FQDN +_NOT_SET = object() + + +cdef _shorten(str text, int length): + if len(text) > length: + return text[:length] + '..' + return text + + +cdef _format_value(value): + if isinstance(value, str): + return repr(_shorten(value, 75)) + else: + return _shorten(str(value), 75) + + +cdef _format_error(args, str position, str value_clause=None): + cdef str msg = str(args[0]) if args else 'invalid' + if position: + msg = '%s: %s' % (position, msg) + if value_clause: + msg = '%s, %s' % (msg, value_clause) + return msg + + +class ValidrError(ValueError): + """Base exception of validr""" + + def __init__(self, *args, value=_NOT_SET, **kwargs): + super().__init__(*args, **kwargs) + self._value = value + # marks item: (is_key, index_or_key) + self.marks = [] + + def mark_index(self, int index=-1): + self.marks.append((False, index)) + return self + + def mark_key(self, str key): + self.marks.append((True, key)) + return self + + @property + def has_value(self): + """Check has value set""" + return self._value is not _NOT_SET + + def set_value(self, value): + """Set value if not set""" + if self._value is _NOT_SET: + self._value = value + + @property + def value(self): + """The invalid value""" + if self._value is _NOT_SET: + return None + return self._value + + @property + def field(self): + """First level index or key, usually it's the field""" + if not self.marks: + return None + __, index_or_key = self.marks[-1] + return index_or_key + + @property + def position(self): + """A string which represent the position of invalid. + + For example: + + { + "tags": ["ok", "invalid"], # tags[1] + "user": { + "name": "invalid", # user.name + "age": 500 # user.age + } + } + """ + cdef str text = '' + cdef bint is_key + for is_key, index_or_key in reversed(self.marks): + if is_key: + text = '%s.%s' % (text, index_or_key) + else: + if index_or_key == -1: + text = '%s[]' % text + else: + text = '%s[%d]' % (text, index_or_key) + if text and text[0] == '.': + text = text[1:] + return text + + @property + def message(self): + """Error message""" + if self.args: + return self.args[0] + else: + return None + + def __str__(self): + return _format_error(self.args, self.position) + + +class Invalid(ValidrError): + """Data invalid""" + def __str__(self): + cdef str value_clause = None + if self.has_value: + value_clause = 'value=%s' % _format_value(self.value) + return _format_error(self.args, self.position, value_clause) + + +class ModelInvalid(Invalid): + """Model data invalid""" + def __init__(self, errors): + if not errors: + raise ValueError('errors is required') + self.errors = errors + message = errors[0].message or 'invalid' + message += ' ...total {} errors'.format(len(errors)) + super().__init__(message) + + def __str__(self): + error_line_s = [] + for ex in self.errors: + error_line_s.append('{} is {}'.format(ex.position, ex.message)) + return '; '.join(error_line_s) + + +class SchemaError(ValidrError): + """Schema error""" + def __str__(self): + cdef str value_clause = None + if self.has_value: + value_clause = 'schema=%s' % self.value.repr(prefix=False, desc=False) + return _format_error(self.args, self.position, value_clause) + + +cdef class mark_index: + """Add current index to Invalid/SchemaError""" + + cdef int index + + def __init__(self, index=-1): + """index = -1 means the position is uncertainty""" + self.index = index + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is not None and issubclass(exc_type, ValidrError): + exc_val.mark_index(self.index) + + +cdef class mark_key: + """Add current key to Invalid/SchemaError""" + + cdef str key + + def __init__(self, key): + self.key = key + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is not None and issubclass(exc_type, ValidrError): + exc_val.mark_key(self.key) + + +class py_mark_index(mark_index): pass +class py_mark_key(mark_key): pass + + cdef bint is_dict(obj): # use isinstance(obj, Mapping) is slow, # hasattr check can speed up about 30% diff --git a/src/validr/exception.py b/src/validr/exception.py deleted file mode 100644 index f6309db..0000000 --- a/src/validr/exception.py +++ /dev/null @@ -1,4 +0,0 @@ -try: - from ._exception_c import * # noqa: F401,F403 -except ImportError: - from ._exception_py import * # noqa: F401,F403 diff --git a/src/validr/model.py b/src/validr/model.py index 913b757..55e2a38 100644 --- a/src/validr/model.py +++ b/src/validr/model.py @@ -1,11 +1,13 @@ """ -Model class is a convenient way to use schema, it's inspired by data class but works differently. +Model class is a convenient way to use schema, it's inspired by data class +but works differently. """ -from .exception import Invalid, ModelInvalid, mark_key from .schema import Compiler, Schema, T +from .validator import Invalid, ModelInvalid from .validator import py_get_dict_value as get_dict_value from .validator import py_get_object_value as get_object_value from .validator import py_is_dict as is_dict +from .validator import py_mark_key as mark_key class ImmutableInstanceError(AttributeError): diff --git a/src/validr/model.pyi b/src/validr/model.pyi index 4a5a6c9..6b2e32e 100644 --- a/src/validr/model.pyi +++ b/src/validr/model.pyi @@ -1,7 +1,12 @@ import typing + from .schema import Compiler +class ImmutableInstanceError(AttributeError): + ... + + M = typing.TypeVar('M') diff --git a/src/validr/schema.py b/src/validr/schema.py index 0722b34..7a44144 100644 --- a/src/validr/schema.py +++ b/src/validr/schema.py @@ -28,19 +28,29 @@ T(schema) -> T T.__schema__ -> Schema """ -import json import copy import enum import inspect +import json from pyparsing import ( - Group, Keyword, Optional, StringEnd, StringStart, Suppress, - ZeroOrMore, quotedString, removeQuotes, replaceWith, - pyparsing_common, ParseBaseException, + Group, + Keyword, + Optional, + ParseBaseException, + StringEnd, + StringStart, + Suppress, + ZeroOrMore, + pyparsing_common, + quotedString, + removeQuotes, + replaceWith, ) -from .validator import builtin_validators -from .exception import SchemaError, mark_index, mark_key +from .validator import SchemaError, builtin_validators +from .validator import py_mark_index as mark_index +from .validator import py_mark_key as mark_key def _make_keyword(kwd_str, kwd_value): diff --git a/src/validr/schema.pyi b/src/validr/schema.pyi index 926abd6..b5d2d22 100644 --- a/src/validr/schema.pyi +++ b/src/validr/schema.pyi @@ -1,10 +1,26 @@ -import typing +from typing import Callable, Iterable, Union + + +class Schema: + def repr(self, *, prefix=True, desc=True) -> str: + ... + + def copy(self) -> "Schema": + ... class Builder: - def __getitem__(self, keys: typing.Iterable[str]) -> "Builder": ... + def __getitem__(self, keys: Iterable[str]) -> "Builder": ... def __getattr__(self, name: str) -> "Builder": ... def __call__(self, *args, **kwargs) -> "Builder": ... T: Builder + + +class Compiler: + def __init__(self, validators: dict = None): + ... + + def compile(self, schema: Union[Schema, Builder]) -> Callable: + ... diff --git a/src/validr/validator.pyi b/src/validr/validator.pyi new file mode 100644 index 0000000..01f7026 --- /dev/null +++ b/src/validr/validator.pyi @@ -0,0 +1,74 @@ +from typing import Callable, Dict, List + + +class ValidrError(ValueError): + + @property + def value(self): ... + + @property + def field(self) -> str: ... + + @property + def position(self) -> str: ... + + @property + def message(self) -> str: ... + + +class Invalid(ValidrError): + ... + + +class ModelInvalid(Invalid): + ... + + +class SchemaError(ValidrError): + ... + + +def validator(string: bool = None, *, accept=None, output=None): + ... + + +builtin_validators: Dict[str, Callable] + + +def create_enum_validator( + name: str, + items: List, + string: bool = True, +) -> Callable: + ... + + +def create_re_validator( + name: str, + r: str, + maxlen: int = None, + strip: bool = False, +) -> Callable: + ... + + +class py_mark_index(): + def __init__(self, index: int = -1): + ... + + def __enter__(self): + ... + + def __exit__(self, exc_type, exc_val, exc_tb): + ... + + +class py_mark_key(): + def __init__(self, key: str): + ... + + def __enter__(self): + ... + + def __exit__(self, exc_type, exc_val, exc_tb): + ... diff --git a/tests/smoke.py b/tests/smoke.py index c69a65a..8540246 100644 --- a/tests/smoke.py +++ b/tests/smoke.py @@ -1,5 +1,7 @@ import os -from validr import T, modelclass, asdict +from platform import python_implementation + +from validr import T, asdict, modelclass @modelclass @@ -29,13 +31,15 @@ def check_pure_python(): def check_c_python(): - import validr._validator_py # noqa: F401 import validr._validator_c # noqa: F401 + import validr._validator_py # noqa: F401 def check(): check_feature() - if os.getenv('VALIDR_SETUP_MODE') == 'py': + is_cpython = python_implementation() == 'CPython' + is_mode_py = os.getenv('VALIDR_SETUP_MODE') == 'py' + if is_mode_py or not is_cpython: check_pure_python() else: check_c_python()