Skip to content

Commit

Permalink
Merge pull request #30 from mdrachuk/no-hypothesis
Browse files Browse the repository at this point in the history
Clean up requirements
  • Loading branch information
mdrachuk committed Aug 13, 2019
2 parents 96804a0 + 9fb157c commit 00e2c26
Show file tree
Hide file tree
Showing 27 changed files with 294 additions and 206 deletions.
2 changes: 0 additions & 2 deletions .pr-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,10 @@ steps:
displayName: 'Install dependencies'
- script: |
pip install pytest pytest-cov
pytest tests --cov=serious --cov-report=html
displayName: 'Test'
- script: |
pip install coveralls
coveralls
displayName: 'Save code coverage'
env:
Expand Down
2 changes: 0 additions & 2 deletions .release-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,10 @@ steps:
displayName: 'Install dependencies'
- script: |
pip install pytest pytest-cov
pytest tests --cov=serious --cov-report=html
displayName: 'Test'
- script: |
pip install coveralls
coveralls
displayName: 'Save code coverage'
env:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Supported Python](https://img.shields.io/pypi/pyversions/serious)][pypi]
[![Documentation](https://img.shields.io/readthedocs/serious)][docs]

Define models in dataclasses: serialization, validation, and more.
A dataclass model toolkit: serialization, validation, and more.

[Documentation][docs]

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Test Coverage](https://img.shields.io/coveralls/github/mdrachuk/serious/master)](https://coveralls.io/github/mdrachuk/serious)
[![Supported Python](https://img.shields.io/pypi/pyversions/serious)](https://pypi.org/project/serious/)

Define models in dataclasses: serialization, validation, and more.
A dataclass model toolkit: serialization, validation, and more.


# Get It Now
Expand Down
30 changes: 1 addition & 29 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,35 +1,7 @@
atomicwrites==1.3.0
attrs==19.1.0
bleach==3.1.0
certifi==2019.3.9
chardet==3.0.4
Click==7.0
coverage==4.5.3
docutils==0.14
hypothesis==4.17.0
idna==2.8
Jinja2==2.10.1
livereload==2.6.1
Markdown==3.1.1
MarkupSafe==1.1.1
coveralls==1.8.2
mkdocs==1.0.4
more-itertools==7.0.0
mypy==0.701
mypy-extensions==0.4.1
pkginfo==1.5.0.1
pluggy==0.9.0
py==1.8.0
Pygments==2.3.1
pytest==4.4.1
pytest-cov==2.6.1
PyYAML==5.1.1
readme-renderer==24.0
requests==2.21.0
requests-toolbelt==0.9.1
six==1.12.0
tornado==6.0.3
tqdm==4.31.1
twine==1.13.0
typed-ast==1.3.1
urllib3==1.24.2
webencodings==0.5.1
83 changes: 56 additions & 27 deletions serious/descriptors.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
from __future__ import annotations

__all__ = ['TypeDescriptor', 'describe', 'DescTypes', 'scan_types']
__doc__ = """Descriptors of types used by serious.
Descriptors are simplifying work with types, enriching them with more contextual information.
This allows to make decisions, like picking a serializer, easier.
They unwrap the generic aliases, get generic parameters from parent classes, simplify optional,
dataclass checks and more.
The data is carried by `TypeDescriptor`s which are created by a call to `serious.descriptors.describe(cls)`.
"""
from collections import ChainMap
from dataclasses import dataclass, fields, is_dataclass
from typing import Type, Any, TypeVar, get_type_hints, Dict, Mapping, List, Union, Iterable
Expand All @@ -24,6 +35,9 @@ def cls(self): # Python fails when providing cls as a keyword parameter to data

@property
def fields(self) -> Mapping[str, TypeDescriptor]:
"""A mapping of all dataclass field names to their corresponding Type Descriptors.
Returns an empty mapping if the object is not a dataclass."""
if not is_dataclass(self.cls):
return {}
types = get_type_hints(self.cls) # type: Dict[str, Type]
Expand All @@ -35,40 +49,56 @@ def describe(self, type_: Type) -> TypeDescriptor:


def describe(type_: Type, generic_params: GenericParams = None) -> TypeDescriptor:
"""Creates a TypeDescriptor for the provided type.
Optionally generic params can be designated as a mapping of TypeVar to parameter Type or indexes in Dict/List/etc.
"""
generic_params = generic_params if generic_params is not None else {}
param = generic_params.get(type_, None)
if param is not None:
return param
return _unwrap_generic(type_, generic_params)
return _describe_generic(type_, generic_params)


_any_type_desc = TypeDescriptor(Any, FrozenDict()) # type: ignore
_ellipses_type_desc = TypeDescriptor(Ellipsis, FrozenDict()) # type: ignore
_generic_params = {
_generic_params: Dict[Type, Dict[int, TypeDescriptor]] = {
list: {0: _any_type_desc},
set: {0: _any_type_desc},
frozenset: {0: _any_type_desc},
tuple: {0: _any_type_desc, 1: _ellipses_type_desc},
tuple: {0: _any_type_desc, 1: TypeDescriptor(Ellipsis, FrozenDict())}, # type: ignore
dict: {0: _any_type_desc, 1: _any_type_desc},
} # type: Dict[Type, Dict[int, TypeDescriptor]]
}


def _get_default_generic_params(cls: Type, params: GenericParams) -> GenericParams:
"""Returns mapping of default generic params for the provided cls.
Examples:
- `dict` -> {0: <TypeDescriptor cls=Any>, 1: <TypeDescriptor cls=Any>};
- `list` -> {0: <TypeDescriptor cls=Any>};
- `tuple` -> {0: <TypeDescriptor cls=Any>, 1: <TypeDescriptor cls=Ellipses>}.
"""
for generic, default_params in _generic_params.items():
if issubclass(cls, generic):
return default_params
return params


def _unwrap_generic(cls: Type, generic_params: GenericParams) -> TypeDescriptor:
def _describe_generic(cls: Type, generic_params: GenericParams) -> TypeDescriptor:
"""Creates a TypeDescriptor for Python _GenericAlias, unwrapping it to its origin/
Examples:
- Tuple[str] -> <TypeDescriptor cls=tuple params={0: <TypeDescriptor cls=str>}>
- Optional[int] -> <TypeDescriptor cls=int is_optional=True>
"""
params: GenericParams = {}
is_optional = _is_optional(cls)
if is_optional:
cls = cls.__args__[0]
if hasattr(cls, '__orig_bases__') and is_dataclass(cls):
params = dict(ChainMap(*(_unwrap_generic(base, generic_params).parameters for base in cls.__orig_bases__)))
params = dict(ChainMap(*(_describe_generic(base, generic_params).parameters for base in cls.__orig_bases__)))
return TypeDescriptor(
_cls=cls,
cls,
parameters=FrozenDict(params),
is_optional=is_optional,
is_dataclass=True
Expand All @@ -81,15 +111,15 @@ def _unwrap_generic(cls: Type, generic_params: GenericParams) -> TypeDescriptor:
describe_ = lambda arg: describe(Any if type(arg) is TypeVar else arg, generic_params)
params = dict(enumerate(map(describe_, cls.__args__)))
return TypeDescriptor(
_cls=cls.__origin__,
cls.__origin__,
parameters=FrozenDict(params),
is_optional=is_optional,
is_dataclass=origin_is_dc
)
if isinstance(cls, type) and len(params) == 0:
params = _get_default_generic_params(cls, params)
return TypeDescriptor(
_cls=cls,
cls,
parameters=FrozenDict(params),
is_optional=is_optional,
is_dataclass=is_dataclass(cls)
Expand All @@ -101,25 +131,22 @@ def _collect_type_vars(alias: Any, generic_params: GenericParams) -> GenericPara
(describe(arg, generic_params) for arg in alias.__args__)))


class DescriptorTypes:
class DescTypes:
types: FrozenList[Type]

def __init__(self, types: Iterable[Type]):
super().__setattr__('types', FrozenList(types))

@classmethod
def scan(cls, desc: TypeDescriptor, _known_descriptors: List[TypeDescriptor] = None) -> 'DescriptorTypes':
if _known_descriptors is None:
_known_descriptors = [desc]
elif desc in _known_descriptors:
return _empty_descriptor_types
else:
_known_descriptors.append(desc)
dts = [] # type: List[DescriptorTypes]
def scan(cls, desc: TypeDescriptor, *, known: List[TypeDescriptor]) -> 'DescTypes':
if desc in known:
return _empty_desc_types
known.append(desc)
dts = [] # type: List[DescTypes]
for param in desc.parameters.values():
dts.append(cls.scan(param, _known_descriptors))
for field_desc in desc.fields.values():
dts.append(cls.scan(field_desc, _known_descriptors))
dts.append(cls.scan(param, known=known))
for child_desc in desc.fields.values():
dts.append(cls.scan(child_desc, known=known))
types = [type_ for dt in dts for type_ in dt.types]
types.append(desc.cls)
return cls(types)
Expand All @@ -131,16 +158,18 @@ def __contains__(self, item):
return item in self.types


_empty_descriptor_types = DescriptorTypes([])
_empty_desc_types = DescTypes({})


def scan_types(desc: TypeDescriptor) -> DescTypes:
"""Create a DescTypes object for the provided descriptor.
def scan_types(desc: TypeDescriptor) -> DescriptorTypes:
return DescriptorTypes.scan(desc)
DescTypes allow checks of the descriptor tree."""
return DescTypes.scan(desc, known=[])


def _is_optional(cls: Type) -> bool:
"""Returns True if the provided type is Optional."""
return hasattr(cls, '__origin__') \
and cls.__origin__ == Union \
return getattr(cls, '__origin__', None) == Union \
and len(cls.__args__) == 2 \
and cls.__args__[1] == type(None)
8 changes: 4 additions & 4 deletions serious/dict/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from typing import TypeVar, Type, Generic, List, Collection, Dict, Iterable, Any, Mapping, Union

from serious.descriptors import describe
from serious.preconditions import _check_is_instance
from serious.preconditions import check_is_instance
from serious.serialization import FieldSerializer, SeriousModel, field_serializers
from serious.utils import class_path

Expand Down Expand Up @@ -53,14 +53,14 @@ def load_many(self, items: Iterable[Dict[str, Any]]) -> List[T]:
return [self._from_dict(each) for each in items]

def dump(self, o: T) -> Dict[str, Any]:
_check_is_instance(o, self.cls)
return self._serializer.dump(o)
return self._dump(o)

def dump_many(self, items: Collection[T]) -> List[Dict[str, Any]]:
return [self._dump(o) for o in items]

def _dump(self, o) -> Dict[str, Any]:
return self._serializer.dump(_check_is_instance(o, self.cls))
check_is_instance(o, self.cls)
return self._serializer.dump(o)

def _from_dict(self, data: Mapping):
return self._serializer.load(data)
Expand Down

0 comments on commit 00e2c26

Please sign in to comment.