diff --git a/.gitignore b/.gitignore index d3a8e8b..30294fe 100644 --- a/.gitignore +++ b/.gitignore @@ -181,3 +181,4 @@ pip-selfcheck.json # End of https://www.gitignore.io/api/python,pycharm,linux,virtualenv README.rst +.mypy_cache diff --git a/.travis.yml b/.travis.yml index ea24fd0..61f97c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,4 +14,6 @@ script: - py.test tests --cov=./reobject - PYTHONPATH=`pwd` py.test examples/* -after_success: codecov +after_success: + - codecov + - mypy reobject || true diff --git a/reobject/models/manager.py b/reobject/models/manager.py index 7df4dd4..de1f901 100644 --- a/reobject/models/manager.py +++ b/reobject/models/manager.py @@ -11,7 +11,7 @@ class ManagerDescriptor(object): def __init__(self): self.manager = None - def __get__(self, instance, owner): + def __get__(self, instance, owner) -> 'Manager': if instance is not None: raise AttributeError( "Manager isn't accessible via %s instances" % owner.__name__ @@ -30,7 +30,7 @@ class RelatedManagerDescriptor(object): def __init__(self, model): self.model = model - def __get__(self, instance, owner): + def __get__(self, instance, owner)-> 'Manager': if instance is None: raise AttributeError( "RelatedManager isn't accessible via %s class" % owner.__name__ @@ -72,13 +72,13 @@ def __contains__(self, item): def store(self): return ModelStoreMapping.get(self.model.__name__) - def get_queryset(self): + def get_queryset(self) -> QuerySet: return QuerySet( self.store, model=self.model ) - def add(self, instance: object) -> object: + def add(self, instance: 'Model') -> 'Model': if not isinstance(instance, self.model): raise TypeError( '{model} instance expected, got {obj}'.format( @@ -165,7 +165,7 @@ def none(self) -> EmptyQuerySet: """ return EmptyQuerySet(model=self.model) - def random(self) -> object: + def random(self) -> 'Model': """ Returns a random model instance. """ diff --git a/reobject/models/store.py b/reobject/models/store.py index d238c79..6936fa2 100644 --- a/reobject/models/store.py +++ b/reobject/models/store.py @@ -1,4 +1,4 @@ -ModelStoreMapping = dict() +ModelStoreMapping: dict = dict() class Store(list): diff --git a/reobject/query/parser.py b/reobject/query/parser.py index 418cd7d..e3f9ff5 100644 --- a/reobject/query/parser.py +++ b/reobject/query/parser.py @@ -4,6 +4,7 @@ from reobject.utils import cmp +from ..types import LookupParams class _Q(object): verbs = ( @@ -25,7 +26,7 @@ class _Q(object): 'startswith' ) - def __init__(self, **kwargs): + def __init__(self, **kwargs: LookupParams) -> None: if kwargs: attr, self.value = list(kwargs.items())[0] @@ -47,13 +48,13 @@ def _comparator_func(self, obj): else: return value == self.value - def __and__(self, other): + def __and__(self, other: '_Q') -> '_Q': new = type(self)() new.comparator = lambda obj: self.comparator(obj) and \ other.comparator(obj) return new - def __or__(self, other): + def __or__(self, other: '_Q') -> '_Q': new = type(self)() new.comparator = lambda obj: self.comparator(obj) or \ other.comparator(obj) @@ -127,7 +128,7 @@ def apply_verb(self, value): class Q(object): - def __new__(cls, *args, **kwargs): + def __new__(cls, *args, **kwargs: LookupParams) -> _Q: return reduce( operator.and_, map(lambda k: _Q(**{k: kwargs[k]}), kwargs), @@ -135,7 +136,7 @@ def __new__(cls, *args, **kwargs): ) @classmethod - def from_Qs(cls, *args): + def from_Qs(cls, *args) -> _Q: return reduce( operator.and_, args, diff --git a/reobject/query/queryset.py b/reobject/query/queryset.py index 100aefd..c1f72ef 100644 --- a/reobject/query/queryset.py +++ b/reobject/query/queryset.py @@ -1,11 +1,14 @@ import random from collections import OrderedDict from itertools import chain +from typing import Tuple, Optional as Maybe, Dict, Any, Iterable from reobject.exceptions import DoesNotExist, MultipleObjectsReturned -from reobject.query import Q +from reobject.query.parser import Q from reobject.utils import cmp, flatmap +from ..types import LookupParams, Fields + class QuerySet(list): def __init__(self, *args, model, **kwargs): @@ -16,16 +19,16 @@ def __init__(self, *args, model, **kwargs): def _attrs(self): return self[0]._attrs if self.exists() else set() - def __or__(self, other): + def __or__(self, other: 'QuerySet') -> 'QuerySet': return type(self)( chain(self, other), model=self.model ).distinct('id') - def count(self): + def count(self) -> int: # type: ignore return len(self) - def delete(self): + def delete(self) -> Tuple[int, dict]: _len = self.count() _type = self.model.__name__ @@ -34,12 +37,12 @@ def delete(self): return _len, {_type: _len} - def distinct(self, *attrs): - if not attrs: - attrs = self._attrs - {'created', 'updated'} + def distinct(self, *fields: Fields) -> 'QuerySet': + if not fields: + fields = self._attrs - {'created', 'updated'} meta = [ - (cmp(*attrs)(obj), obj) + (cmp(*fields)(obj), obj) for obj in self.reverse() ] @@ -48,17 +51,19 @@ def distinct(self, *attrs): model=self.model ).reverse() - def earliest(self, field_name='created'): + def earliest(self, field_name: str = 'created') -> Maybe['Model']: try: obj = self.filter( **{field_name + '__isnone': False} - ).order_by(field_name)[0] + ).order_by( + field_name + )[0] except IndexError: return None else: return obj - def exclude(self, *args, **kwargs): + def exclude(self, *args: Tuple[Q, ...], **kwargs: LookupParams) -> 'QuerySet': q = ~(Q.from_Qs(*args) & Q(**kwargs)) return type(self)( @@ -66,10 +71,10 @@ def exclude(self, *args, **kwargs): model=self.model ) - def exists(self): + def exists(self) -> bool: return bool(self) - def filter(self, *args, **kwargs): + def filter(self, *args: Tuple[Q, ...], **kwargs: LookupParams) -> 'QuerySet': q = Q.from_Qs(*args) & Q(**kwargs) return type(self)( @@ -77,7 +82,7 @@ def filter(self, *args, **kwargs): model=self.model ) - def first(self): + def first(self) -> Maybe['Model']: # type: ignore try: obj = self[0] except IndexError: @@ -85,7 +90,7 @@ def first(self): else: return obj - def get(self, *args, **kwargs): + def get(self, *args: Tuple[Q, ...], **kwargs: LookupParams): result_set = self.filter(*args, **kwargs) if len(result_set) == 0: @@ -105,7 +110,7 @@ def get(self, *args, **kwargs): ) ) - def get_or_create(self, defaults=None, **kwargs): + def get_or_create(self, defaults=None, **kwargs: LookupParams): try: obj = self.get(**kwargs) except DoesNotExist: @@ -119,7 +124,7 @@ def get_or_create(self, defaults=None, **kwargs): else: return obj, False - def last(self): + def last(self) -> Maybe['Model']: # type: ignore try: obj = self[-1] except IndexError: @@ -127,17 +132,19 @@ def last(self): else: return obj - def latest(self, field_name='created'): + def latest(self, field_name: str = 'created') -> Maybe['Model']: # type: ignore try: obj = self.filter( **{field_name + '__isnone': False} - ).order_by(field_name)[-1] + ).order_by( + field_name + )[-1] except IndexError: return None else: return obj - def map(self, func): + def map(self, func) -> Iterable: if not callable(func): raise TypeError( 'Expected a callable, got {}'.format(type(func)) @@ -145,19 +152,19 @@ def map(self, func): return map(func, self) - def none(self): + def none(self) -> 'EmptyQuerySet': return EmptyQuerySet(model=self.model) - def order_by(self, *attrs): - if not attrs: + def order_by(self, *fields: Fields): + if not fields: raise AttributeError return type(self)( - sorted(self, key=cmp(*attrs)), + sorted(self, key=cmp(*fields)), model=self.model ) - def random(self): + def random(self) -> Maybe['Model']: # type: ignore try: obj = random.choice(self) except IndexError: @@ -165,13 +172,13 @@ def random(self): else: return obj - def reverse(self): + def reverse(self) -> 'QuerySet': return type(self)( reversed(self), model=self.model ) - def values(self, *fields): + def values(self, *fields: Fields) -> 'QuerySet': if not fields: fields = self._attrs @@ -183,7 +190,7 @@ def values(self, *fields): model=self.model ) - def values_list(self, *fields, flat=False): + def values_list(self, *fields: Fields, flat: bool=False) -> 'QuerySet': # TODO: Allow order_by on values_list if not fields: @@ -202,6 +209,6 @@ def values_list(self, *fields, flat=False): class EmptyQuerySet(QuerySet): - def __init__(self, model, *args, **kwargs): + def __init__(self, model: 'Model', *args, **kwargs) -> None: # type: ignore super(QuerySet, self).__init__(*args, **kwargs) self.model = model diff --git a/reobject/types.py b/reobject/types.py new file mode 100644 index 0000000..142d388 --- /dev/null +++ b/reobject/types.py @@ -0,0 +1,5 @@ +from typing import Dict, Any, Tuple, Union + +LookupParams = Dict[str, Any] +Fields = Union[Tuple[str, ...], str] + diff --git a/test-requirements.txt b/test-requirements.txt index 9cf8484..673415c 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,4 @@ pytest pytest-cov codecov +mypy