Skip to content

Commit

Permalink
Fix Index.query and Index.scan typing issues
Browse files Browse the repository at this point in the history
  • Loading branch information
ikonst committed Jan 24, 2020
1 parent 6660ff2 commit 922534e
Show file tree
Hide file tree
Showing 6 changed files with 61 additions and 15 deletions.
7 changes: 7 additions & 0 deletions docs/release_notes.rst
@@ -1,6 +1,13 @@
Release Notes
=============

v4.3.1
----------

* Fix Index.query and Index.scan typing regressions introduced in 4.2.0, which were causing false errors
in type checkers


v4.3.0
----------

Expand Down
2 changes: 1 addition & 1 deletion pynamodb/__init__.py
Expand Up @@ -7,4 +7,4 @@
"""
__author__ = 'Jharrod LaFon'
__license__ = 'MIT'
__version__ = '4.3.0'
__version__ = '4.3.1'
19 changes: 10 additions & 9 deletions pynamodb/indexes.pyi
@@ -1,16 +1,17 @@
from typing import Any, Dict, List, Optional, Text, Type, TypeVar
from typing import Any, Dict, List, Optional, Text, TypeVar, Generic

from pynamodb.expressions.condition import Condition
from pynamodb.models import Model
from pynamodb.pagination import ResultIterator

_T = TypeVar('_T', bound='Index')
_M = TypeVar('_M', bound=Model)


class IndexMeta(type):
def __init__(cls, name, bases, attrs) -> None: ...


class Index(metaclass=IndexMeta):
class Index(Generic[_M], metaclass=IndexMeta):
Meta: Any
def __init__(self) -> None: ...
@classmethod
Expand All @@ -25,7 +26,7 @@ class Index(metaclass=IndexMeta):
) -> int: ...
@classmethod
def query(
cls: Type[_T],
cls,
hash_key,
range_key_condition: Optional[Condition] = ...,
filter_condition: Optional[Condition] = ...,
Expand All @@ -36,10 +37,10 @@ class Index(metaclass=IndexMeta):
attributes_to_get: Optional[Any] = ...,
page_size: Optional[int] = ...,
rate_limit: Optional[float] = ...,
) -> ResultIterator[_T]: ...
) -> ResultIterator[_M]: ...
@classmethod
def scan(
cls: Type[_T],
cls,
filter_condition: Optional[Condition] = ...,
segment: Optional[int] = ...,
total_segments: Optional[int] = ...,
Expand All @@ -49,10 +50,10 @@ class Index(metaclass=IndexMeta):
consistent_read: Optional[bool] = ...,
rate_limit: Optional[float] = ...,
attributes_to_get: Optional[List[str]] = ...,
) -> ResultIterator[_T]: ...
) -> ResultIterator[_M]: ...

class GlobalSecondaryIndex(Index): ...
class LocalSecondaryIndex(Index): ...
class GlobalSecondaryIndex(Index[_M]): ...
class LocalSecondaryIndex(Index[_M]): ...

class Projection(object):
projection_type: Any
Expand Down
1 change: 0 additions & 1 deletion pynamodb/models.pyi
@@ -1,4 +1,3 @@

from .attributes import Attribute
from .exceptions import DoesNotExist as DoesNotExist
from typing import Any, Dict, Generic, Iterable, Iterator, List, Optional, Sequence, Tuple, Type, TypeVar, Text, Union
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Expand Up @@ -7,5 +7,5 @@ python-dateutil==2.8.0

# only used in .travis.yml
coveralls
mypy==0.740;python_version>="3.7"
mypy==0.761;python_version>="3.7"
pytest-cov
45 changes: 42 additions & 3 deletions tests/test_mypy.py
Expand Up @@ -35,7 +35,7 @@ class MyModel(Model):
MyModel.query(12.3)
MyModel.query(b'123')
MyModel.query((1, 2, 3))
MyModel.query({'1': '2'}) # E: Argument 1 to "query" of "Model" has incompatible type "Dict[str, str]"; expected "Union[str, bytes, float, Tuple[Any, ...]]"
MyModel.query({'1': '2'}) # E: Argument 1 to "query" of "Model" has incompatible type "Dict[str, str]"; expected "Union[str, bytes, float, int, Tuple[Any, ...]]"
# test conditions
MyModel.query(123, range_key_condition=(MyModel.my_attr == 5), filter_condition=(MyModel.my_attr == 5))
Expand Down Expand Up @@ -150,10 +150,49 @@ class MyModel(Model):
reveal_type(MyModel.my_list) # E: Revealed type is 'pynamodb.attributes.ListAttribute[__main__.MyMap]'
reveal_type(MyModel().my_list) # E: Revealed type is 'builtins.list[__main__.MyMap*]'
reveal_type(MyModel.my_list[0]) # E: Revealed type is 'Any' # E: Value of type "ListAttribute[MyMap]" is not indexable
reveal_type(MyModel.my_list[0]) # E: Value of type "ListAttribute[MyMap]" is not indexable # E: Revealed type is 'Any'
reveal_type(MyModel().my_list[0].my_sub_attr) # E: Revealed type is 'builtins.str'
# Untyped lists are not well supported yet
reveal_type(MyModel.my_untyped_list[0]) # E: Revealed type is 'Any' # E: Cannot determine type of 'my_untyped_list'
reveal_type(MyModel.my_untyped_list[0]) # E: Value of type "ListAttribute[Any]" is not indexable # E: Revealed type is 'Any'
reveal_type(MyModel().my_untyped_list[0].my_sub_attr) # E: Revealed type is 'Any'
""")


def test_index_query_scan():
assert_mypy_output("""
from pynamodb.attributes import NumberAttribute
from pynamodb.models import Model
from pynamodb.indexes import GlobalSecondaryIndex
class UntypedIndex(GlobalSecondaryIndex):
bar = NumberAttribute(hash_key=True)
class TypedIndex(GlobalSecondaryIndex[MyModel]):
bar = NumberAttribute(hash_key=True)
class MyModel(Model):
foo = NumberAttribute(hash_key=True)
bar = NumberAttribute()
untyped_index = UntypedIndex()
typed_index = TypedIndex()
# Ensure old code keeps working
reveal_type(MyModel.untyped_index.query(123)) # E: Revealed type is 'pynamodb.pagination.ResultIterator[Any]'
my_untyped_model = next(MyModel.untyped_index.query(123))
reveal_type(my_untyped_model.bar) # E: Revealed type is 'Any'
# Allow users to specify which model their indices return
reveal_type(MyModel.typed_index.query(123)) # E: Revealed type is 'pynamodb.pagination.ResultIterator[__main__.MyModel*]'
my_model = next(MyModel.typed_index.query(123))
reveal_type(my_model.bar) # E: Revealed type is 'builtins.float'
# Ensure old code keeps working
my_untyped_model = next(MyModel.untyped_index.scan())
reveal_type(my_untyped_model.bar) # E: Revealed type is 'Any'
# Allow users to specify which model their indices return
my_model = next(MyModel.typed_index.scan())
reveal_type(my_model.bar) # E: Revealed type is 'builtins.float'
""")

0 comments on commit 922534e

Please sign in to comment.