Skip to content

Refactor typings #62

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
strategy:
max-parallel: 4
matrix:
python-version: [3.5, 3.6, 3.7, 3.8, 3.9]
python-version: [3.6, 3.7, 3.8, 3.9]
os: [windows-latest, ubuntu-18.04, macos-latest]
exclude:
# Python 3.5 is unable to properly
Expand Down Expand Up @@ -51,4 +51,5 @@ jobs:
run: |
pip install -e .[test]
flake8 immutables/ tests/
python -m unittest -v tests.suite
mypy immutables/
python -m pytest -v
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ __pycache__/
/.pytest_cache
/.coverage
/.mypy_cache
/.venv*
2 changes: 1 addition & 1 deletion MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
recursive-include tests *.py
recursive-include immutables *.py *.c *.h *.pyi
include LICENSE* NOTICE README.rst bench.png
include immutables/py.typed
include mypy.ini immutables/py.typed
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for adding mypy.ini in the distribution?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I did this out of habit. Most of the projects I work on do some sort of type checking during testing. That doesn't seem to be the case for this one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I think it is good idea to add such a test, otherwise the annotations will inevitably rot.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@elprans would you like me to add a test that runs mypy?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, that would work too.

2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ debug:
DEBUG_IMMUTABLES=1 $(PYTHON) setup.py build_ext --inplace

test:
$(PYTHON) -m unittest -v
$(PYTHON) -m pytest -v

rtest:
~/dev/venvs/36-debug/bin/python setup.py build_ext --inplace
Expand Down
25 changes: 20 additions & 5 deletions immutables/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
# flake8: noqa

try:
import sys

if sys.version_info >= (3, 5, 2):
from typing import TYPE_CHECKING
else:
from typing_extensions import TYPE_CHECKING

if TYPE_CHECKING:
from ._map import Map
except ImportError:
from .map import Map
else:
import collections.abc as _abc
_abc.Mapping.register(Map)
try:
from ._map import Map
except ImportError:
from .map import Map
else:
import collections.abc as _abc
_abc.Mapping.register(Map)

from ._protocols import MapKeys as MapKeys
from ._protocols import MapValues as MapValues
from ._protocols import MapItems as MapItems
from ._protocols import MapMutation as MapMutation

from ._version import __version__

Expand Down
125 changes: 51 additions & 74 deletions immutables/_map.pyi
Original file line number Diff line number Diff line change
@@ -1,96 +1,73 @@
from typing import Any
from typing import Dict
from typing import Generic
from typing import Hashable
from typing import Iterable
from typing import Iterator
from typing import Mapping
from typing import MutableMapping
from typing import NoReturn
from typing import overload
from typing import Optional
from typing import Tuple
from typing import Type
from typing import TypeVar
from typing import Union
from typing import overload


K = TypeVar('K', bound=Hashable)
V = TypeVar('V', bound=Any)
D = TypeVar('D', bound=Any)


class BitmapNode: ...


class MapKeys(Generic[K]):
def __init__(self, c: int, m: BitmapNode) -> None: ...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[K]: ...


class MapValues(Generic[V]):
def __init__(self, c: int, m: BitmapNode) -> None: ...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[V]: ...


class MapItems(Generic[K, V]):
def __init__(self, c: int, m: BitmapNode) -> None: ...
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[Tuple[K, V]]: ...
from ._protocols import IterableItems
from ._protocols import MapItems
from ._protocols import MapKeys
from ._protocols import MapMutation
from ._protocols import MapValues
from ._protocols import HT
from ._protocols import KT
from ._protocols import T
from ._protocols import VT_co


class Map(Mapping[K, V]):
class Map(Mapping[KT, VT_co]):
@overload
def __init__(self, **kw: V) -> None: ...
def __init__(self) -> None: ...
@overload
def __init__(self: Map[str, VT_co], **kw: VT_co) -> None: ...
@overload
def __init__(
self, __col: Union[IterableItems[KT, VT_co], Iterable[Tuple[KT, VT_co]]]
) -> None: ...
@overload
def __init__(
self, col: Union[Mapping[K, V], Iterable[Tuple[K, V]]], **kw: V
self: Map[Union[KT, str], VT_co],
__col: Union[IterableItems[KT, VT_co], Iterable[Tuple[KT, VT_co]]],
**kw: VT_co
) -> None: ...
def __reduce__(self) -> Tuple[Type[Map], Tuple[dict]]: ...
def __reduce__(self) -> Tuple[Type[Map[KT, VT_co]], Tuple[Dict[KT, VT_co]]]: ...
def __len__(self) -> int: ...
def __eq__(self, other: Any) -> bool: ...
@overload
def update(self, **kw: V) -> Map[str, V]: ...
@overload
def update(
self, col: Union[Mapping[K, V], Iterable[Tuple[K, V]]], **kw: V
) -> Map[K, V]: ...
def mutate(self) -> MapMutation[K, V]: ...
def set(self, key: K, val: V) -> Map[K, V]: ...
def delete(self, key: K) -> Map[K, V]: ...
def get(self, key: K, default: D = ...) -> Union[V, D]: ...
def __getitem__(self, key: K) -> V: ...
def __contains__(self, key: object) -> bool: ...
def __iter__(self) -> Iterator[K]: ...
def keys(self) -> MapKeys[K]: ...
def values(self) -> MapValues[V]: ...
def items(self) -> MapItems[K, V]: ...
def __hash__(self) -> int: ...
def __dump__(self) -> str: ...
def __class_getitem__(cls, item: Any) -> Type[Map]: ...


S = TypeVar('S', bound='MapMutation')


class MapMutation(MutableMapping[K, V]):
def __init__(self, count: int, root: BitmapNode) -> None: ...
def set(self, key: K, val: V) -> None: ...
def __enter__(self: S) -> S: ...
def __exit__(self, *exc: Any): ...
def __iter__(self) -> NoReturn: ...
def __delitem__(self, key: K) -> None: ...
def __setitem__(self, key: K, val: V) -> None: ...
def pop(self, __key: K, __default: D = ...) -> Union[V, D]: ...
def get(self, key: K, default: D = ...) -> Union[V, D]: ...
def __getitem__(self, key: K) -> V: ...
def __contains__(self, key: Any) -> bool: ...
self,
__col: Union[IterableItems[KT, VT_co], Iterable[Tuple[KT, VT_co]]]
) -> Map[KT, VT_co]: ...
@overload
def update(self, **kw: V) -> None: ...
def update(
self: Map[Union[HT, str], Any],
__col: Union[IterableItems[KT, VT_co], Iterable[Tuple[KT, VT_co]]],
**kw: VT_co # type: ignore[misc]
) -> Map[KT, VT_co]: ...
@overload
def update(
self, col: Union[Mapping[K, V], Iterable[Tuple[K, V]]], **kw: V
) -> None: ...
def finish(self) -> Map[K, V]: ...
def __len__(self) -> int: ...
def __eq__(self, other: Any) -> bool: ...
self: Map[Union[HT, str], Any],
**kw: VT_co # type: ignore[misc]
) -> Map[KT, VT_co]: ...
def mutate(self) -> MapMutation[KT, VT_co]: ...
def set(self, key: KT, val: VT_co) -> Map[KT, VT_co]: ... # type: ignore[misc]
def delete(self, key: KT) -> Map[KT, VT_co]: ...
@overload
def get(self, key: KT) -> Optional[VT_co]: ...
@overload
def get(self, key: KT, default: Union[VT_co, T]) -> Union[VT_co, T]: ...
def __getitem__(self, key: KT) -> VT_co: ...
def __contains__(self, key: Any) -> bool: ...
def __iter__(self) -> Iterator[KT]: ...
def keys(self) -> MapKeys[KT]: ... # type: ignore[override]
def values(self) -> MapValues[VT_co]: ... # type: ignore[override]
def items(self) -> MapItems[KT, VT_co]: ... # type: ignore[override]
def __hash__(self) -> int: ...
def __dump__(self) -> str: ...
def __class_getitem__(cls, item: Any) -> Type[Map[Any, Any]]: ...
85 changes: 85 additions & 0 deletions immutables/_protocols.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import sys
from typing import Any
from typing import Hashable
from typing import Iterable
from typing import Iterator
from typing import NoReturn
from typing import Optional
from typing import Tuple
from typing import TypeVar
from typing import Union
from typing import overload

if sys.version_info >= (3, 8):
from typing import Protocol
from typing import TYPE_CHECKING
else:
from typing_extensions import Protocol
from typing_extensions import TYPE_CHECKING

if TYPE_CHECKING:
from ._map import Map

HT = TypeVar('HT', bound=Hashable)
KT = TypeVar('KT', bound=Hashable)
KT_co = TypeVar('KT_co', covariant=True)
MM = TypeVar('MM', bound='MapMutation[Any, Any]')
T = TypeVar('T')
VT = TypeVar('VT')
VT_co = TypeVar('VT_co', covariant=True)


class MapKeys(Protocol[KT_co]):
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[KT_co]: ...


class MapValues(Protocol[VT_co]):
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[VT_co]: ...


class MapItems(Protocol[KT_co, VT_co]):
def __len__(self) -> int: ...
def __iter__(self) -> Iterator[Tuple[KT_co, VT_co]]: ...


class IterableItems(Protocol[KT_co, VT_co]):
def items(self) -> Iterable[Tuple[KT_co, VT_co]]: ...


class MapMutation(Protocol[KT, VT]):
def set(self, key: KT, val: VT) -> None: ...
def __enter__(self: MM) -> MM: ...
def __exit__(self, *exc: Any) -> bool: ...
def __iter__(self) -> NoReturn: ...
def __delitem__(self, key: KT) -> None: ...
def __setitem__(self, key: KT, val: VT) -> None: ...
@overload
def pop(self, __key: KT) -> VT: ...
@overload
def pop(self, __key: KT, __default: T) -> Union[VT, T]: ...
@overload
def get(self, key: KT) -> Optional[VT]: ...
@overload
def get(self, key: KT, default: Union[VT, T]) -> Union[VT, T]: ...
def __getitem__(self, key: KT) -> VT: ...
def __contains__(self, key: object) -> bool: ...

@overload
def update(
self,
__col: Union[IterableItems[KT, VT], Iterable[Tuple[KT, VT]]]
) -> None: ...

@overload
def update(
self: 'MapMutation[Union[HT, str], Any]',
__col: Union[IterableItems[KT, VT], Iterable[Tuple[KT, VT]]],
**kw: VT
) -> None: ...
@overload
def update(self: 'MapMutation[Union[HT, str], Any]', **kw: VT) -> None: ...
def finish(self) -> 'Map[KT, VT]': ...
def __len__(self) -> int: ...
def __eq__(self, other: Any) -> bool: ...
9 changes: 9 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[mypy]
incremental = True
strict = True

[mypy-immutables.map]
ignore_errors = True

[mypy-immutables._testutils]
ignore_errors = True
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
# (example breakage: https://gitlab.com/pycqa/flake8/issues/427)
'flake8~=3.8.4',
'pycodestyle~=2.6.0',
'mypy>=0.910',
'pytest~=6.2.4',
]

EXTRA_DEPENDENCIES = {
Expand Down Expand Up @@ -86,5 +88,6 @@
provides=['immutables'],
include_package_data=True,
ext_modules=ext_modules,
install_requires=['typing-extensions>=3.7.4.3;python_version<"3.8"'],
extras_require=EXTRA_DEPENDENCIES,
)
13 changes: 13 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# We need the mypy pytest plugin to do the test collection for our
# typing tests.

# mypy demands that its test-data be present for mypy.test.config to be
# imported, so thwart that check. mypy PR #10919 fixes this.
import unittest.mock
with unittest.mock.patch('os.path.isdir') as isdir:
isdir.return_value = True
import mypy.test.config # noqa

pytest_plugins = [
'mypy.test.data',
]
Loading