Skip to content

Mypy plugin for typed decorators #88

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 4 commits into from
Jun 15, 2019
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: 4 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ install:
script:
- poetry run flake8 returns tests docs
- poetry run mypy returns tests/**/*.py
- poetry run pytest
- poetry run pytest tests
# Temporary work-around of
# https://github.com/mkurnikov/pytest-mypy-plugins/issues/2
- poetry run pytest -p no:cov -o addopts="" --mypy-ini-file=setup.cfg typesafety
- poetry run doc8 -q docs
- poetry check
- poetry run pip check
Expand Down
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ We also use `wemake_python_styleguide` to enforce the code quality.
To run all tests:

```bash
pytest
pytest tests
pytest -p no:cov -o addopts="" --mypy-ini-file=setup.cfg typesafety
```

To run linting:
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ Make your functions return something meaningful, typed, and safe!
pip install returns
```

You might also need to [configure](https://returns.readthedocs.io/en/latest/pages/container.html#type-safety)
`mypy` correctly and install our plugin:

```cfg
[mypy]
plugins =
returns.contrib.mypy.decorator_plugin
```

Make sure you know how to get started, [check out our docs](https://returns.readthedocs.io/en/latest/)!


Expand Down
8 changes: 6 additions & 2 deletions docs/pages/container.rst
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ since we are using ``__slots__`` for better performance and strictness.

Well, nothing is **really** immutable in python, but you were warned.

.. _type-safety:

Type safety
-----------
Expand All @@ -266,8 +267,11 @@ compatible ``.pyi`` files together with the source code.
In this case these types will be available to users
when they install our application.

However, this is still good old ``python`` type system,
and it has its drawbacks.
We also ship custom ``mypy`` plugins to overcome some existing problems,
please make sure to use them,
since they increase your developer experience and type-safety:

- ``decorator_plugin`` to solve untyped `decorator issue <https://github.com/python/mypy/issues/3157>`_

You can have a look at the suggested ``mypy``
`configuration <https://github.com/dry-python/returns/blob/master/setup.cfg>`_
Expand Down
6 changes: 3 additions & 3 deletions docs/pages/io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,9 @@ with ``@impure`` for better readability and clearness:
Limitations
~~~~~~~~~~~

There's one limitation in typing
that we are facing right now
due to `mypy issue <https://github.com/python/mypy/issues/3157>`_.
Typing will only work correctly
if :ref:`decorator_plugin <type-safety>` is used.
This happens due to `mypy issue <https://github.com/python/mypy/issues/3157>`_.


FAQ
Expand Down
26 changes: 3 additions & 23 deletions docs/pages/result.rst
Original file line number Diff line number Diff line change
Expand Up @@ -209,29 +209,9 @@ Supports both async and regular functions.
Limitations
~~~~~~~~~~~

There's one limitation in typing
that we are facing right now
due to `mypy issue <https://github.com/python/mypy/issues/3157>`_:

.. code:: python

from returns.result import safe

@safe
def function(param: int) -> int:
return param

reveal_type(function)
# Actual => def (*Any, **Any) -> builtins.int
# Expected => def (int) -> builtins.int

This effect can be reduced
with the help of `Design by Contract <https://en.wikipedia.org/wiki/Design_by_contract>`_
with these implementations:

- https://github.com/deadpixi/contracts
- https://github.com/orsinium/deal
- https://github.com/Parquery/icontract
Typing will only work correctly
if :ref:`decorator_plugin <type-safety>` is used.
This happens due to `mypy issue <https://github.com/python/mypy/issues/3157>`_.


API Reference
Expand Down
70 changes: 69 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ pytest = "^4.6"
pytest-cov = "^2.7"
pytest-randomly = "^3.0"
pytest-asyncio = "^0.10.0"
pytest-mypy-plugins = "^0.3.0"

sphinx = "^2.1"
sphinx-autodoc-typehints = "^1.6"
Expand Down
1 change: 1 addition & 0 deletions returns/contrib/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
1 change: 1 addition & 0 deletions returns/contrib/mypy/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-
75 changes: 75 additions & 0 deletions returns/contrib/mypy/decorator_plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-

"""
Custom mypy plugin to solve the temporary problem with untyped decorators.

This problem appears when we try to change the return type of the function.
However, currently it is impossible due to this bug:
https://github.com/python/mypy/issues/3157

This plugin is a temporary solution to the problem.
It should be later replaced with the official way of doing things.

``mypy`` API docs are here:
https://mypy.readthedocs.io/en/latest/extending_mypy.html

We use ``pytest-mypy-plugins`` to test that it works correctly, see:
https://github.com/mkurnikov/pytest-mypy-plugins
"""

from typing import Callable, Optional, Type

from mypy.plugin import FunctionContext, Plugin
from mypy.types import CallableType

#: Set of full names of our decorators.
_TYPED_DECORATORS = {
'returns.result.safe',
'returns.io.impure',
}


def _change_decorator_function_type(
decorated: CallableType,
decorator: CallableType,
) -> CallableType:
"""Replaces revealed argument types by mypy with types from decorated."""
decorator.arg_types = decorated.arg_types
decorator.arg_kinds = decorated.arg_kinds
decorator.arg_names = decorated.arg_names
return decorator


def _analyze_decorator(function_ctx: FunctionContext):
"""Tells us what to do when one of the typed decorators is called."""
if not isinstance(function_ctx.arg_types[0][0], CallableType):
return function_ctx.default_return_type
if not isinstance(function_ctx.default_return_type, CallableType):
return function_ctx.default_return_type
return _change_decorator_function_type(
function_ctx.arg_types[0][0],
function_ctx.default_return_type,
)


class _TypedDecoratorPlugin(Plugin):
def get_function_hook( # type: ignore
self, fullname: str,
) -> Optional[Callable[[FunctionContext], Type]]:
"""
One of the specified ``mypy`` callbacks.

Runs on each function call in the source code.
We are only interested in a particular subset of all functions.
So, we return a function handler for them.

Otherwise, we return ``None``.
"""
if fullname in _TYPED_DECORATORS:
return _analyze_decorator
return None


def plugin(version: str) -> Type[Plugin]:
"""Plugin's public API and entrypoint."""
return _TypedDecoratorPlugin
8 changes: 8 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ universal = 1

[coverage:run]
branch = True
omit =
# We test mypy plugins with `pytest-mypy-plugins`,
# which does not work with coverage:
returns/contrib/mypy/*


[flake8]
Expand Down Expand Up @@ -76,6 +80,10 @@ line_length = 79
# The mypy configurations: http://bit.ly/2zEl9WI
python_version = 3.6

# Plugins, includes custom:
plugins =
returns.contrib.mypy.decorator_plugin

# We have disabled this checks due to some problems with `mypy` type
# system, it does not look like it will be fixed soon.
# disallow_any_explicit = True
Expand Down
4 changes: 2 additions & 2 deletions tests/test_io/test_io_functions/test_impure.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ def test_impure():
@pytest.mark.asyncio
async def test_impure_async():
"""Ensures that impure returns IO container for async."""
impure_result = await impure(_fake_impure_coroutine)(None)
impure_result = await impure(_fake_impure_coroutine)(1)
assert isinstance(impure_result, IO)
assert impure_result == IO(None)
assert impure_result == IO(1)
Loading