From 38b068311c9bc5b6f17887f679b5a0ba542ec99f Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 14 Jun 2019 22:58:16 +0300 Subject: [PATCH 1/4] WIP: first working prototype --- returns/mypy_plugins/__init__.py | 1 + returns/mypy_plugins/decorator.py | 40 +++++++++++++++++++++++++++++++ setup.cfg | 4 ++++ test.py | 8 +++++++ 4 files changed, 53 insertions(+) create mode 100644 returns/mypy_plugins/__init__.py create mode 100644 returns/mypy_plugins/decorator.py create mode 100644 test.py diff --git a/returns/mypy_plugins/__init__.py b/returns/mypy_plugins/__init__.py new file mode 100644 index 000000000..40a96afc6 --- /dev/null +++ b/returns/mypy_plugins/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/returns/mypy_plugins/decorator.py b/returns/mypy_plugins/decorator.py new file mode 100644 index 000000000..f759b6f78 --- /dev/null +++ b/returns/mypy_plugins/decorator.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +from typing import Callable, Optional, Type + +from mypy.types import CallableType +from mypy.plugin import Plugin, FunctionContext + +TYPED_DECORATORS = { + 'returns.result.safe', +} + + +def _change_decorator_function_type( + decorated: CallableType, + decorator: CallableType, +) -> CallableType: + 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) -> Type: + return _change_decorator_function_type( + function_ctx.arg_types[0][0], + function_ctx.default_return_type, + ) + + +class TypedDecoratorPlugin(Plugin): + def get_function_hook( + self, fullname: str, + ) -> Optional[Callable[[FunctionContext], Type]]: + if fullname in TYPED_DECORATORS: + return _analyze_decorator + return None + + +def plugin(version: str): + return TypedDecoratorPlugin diff --git a/setup.cfg b/setup.cfg index adae7f4aa..ccde5f4b0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -76,6 +76,10 @@ line_length = 79 # The mypy configurations: http://bit.ly/2zEl9WI python_version = 3.6 +# Plugins, includes custom: +plugins = + returns.mypy_plugins.decorator + # 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 diff --git a/test.py b/test.py new file mode 100644 index 000000000..cca1d4940 --- /dev/null +++ b/test.py @@ -0,0 +1,8 @@ +from returns.result import safe + +@safe +def test() -> int: + return 1 + +reveal_type(test) +reveal_type(test()) From 2783cacc55455766414f37d8cc9f4a6099a51898 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 14 Jun 2019 23:17:59 +0300 Subject: [PATCH 2/4] Also type @impure --- returns/mypy_plugins/decorator.py | 1 + test.py | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/returns/mypy_plugins/decorator.py b/returns/mypy_plugins/decorator.py index f759b6f78..71e668d53 100644 --- a/returns/mypy_plugins/decorator.py +++ b/returns/mypy_plugins/decorator.py @@ -7,6 +7,7 @@ TYPED_DECORATORS = { 'returns.result.safe', + 'returns.io.impure', } diff --git a/test.py b/test.py index cca1d4940..f6802d417 100644 --- a/test.py +++ b/test.py @@ -1,8 +1,14 @@ from returns.result import safe +from returns.io import impure @safe def test() -> int: return 1 +@impure +def same(arg: str) -> str: + return arg + + reveal_type(test) -reveal_type(test()) +reveal_type(same) From b84538c7472bb9e054b804b5ed278e16425a21cb Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 15 Jun 2019 11:58:01 +0300 Subject: [PATCH 3/4] Working mypy plugin --- .travis.yml | 5 +- CONTRIBUTING.md | 3 +- README.md | 9 +++ docs/pages/container.rst | 8 +- docs/pages/io.rst | 6 +- docs/pages/result.rst | 26 +------ poetry.lock | 70 ++++++++++++++++- pyproject.toml | 1 + returns/{mypy_plugins => contrib}/__init__.py | 0 returns/contrib/mypy/__init__.py | 1 + returns/contrib/mypy/decorator_plugin.py | 75 +++++++++++++++++++ returns/mypy_plugins/decorator.py | 41 ---------- setup.cfg | 6 +- test.py | 14 ---- .../test_io/test_io_functions/test_impure.py | 4 +- typesafety/test_io_types/impure.test | 68 +++++++++++++++++ typesafety/test_result_types/safe.test | 68 +++++++++++++++++ 17 files changed, 316 insertions(+), 89 deletions(-) rename returns/{mypy_plugins => contrib}/__init__.py (100%) create mode 100644 returns/contrib/mypy/__init__.py create mode 100644 returns/contrib/mypy/decorator_plugin.py delete mode 100644 returns/mypy_plugins/decorator.py delete mode 100644 test.py create mode 100644 typesafety/test_io_types/impure.test create mode 100644 typesafety/test_result_types/safe.test diff --git a/.travis.yml b/.travis.yml index 6a9772416..64f1d7024 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 516735d4d..fae8a4cd2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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: diff --git a/README.md b/README.md index bd82f9975..00af18daa 100644 --- a/README.md +++ b/README.md @@ -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/)! diff --git a/docs/pages/container.rst b/docs/pages/container.rst index 26b54319c..a75fb1fb5 100644 --- a/docs/pages/container.rst +++ b/docs/pages/container.rst @@ -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 ----------- @@ -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 `_ You can have a look at the suggested ``mypy`` `configuration `_ diff --git a/docs/pages/io.rst b/docs/pages/io.rst index 0dc0585d3..1ae2ef52b 100644 --- a/docs/pages/io.rst +++ b/docs/pages/io.rst @@ -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 `_. +Typing will only work correctly +if :ref:`decorator_plugin ` is used. +This happens due to `mypy issue `_. FAQ diff --git a/docs/pages/result.rst b/docs/pages/result.rst index f84a3bd6a..15ba368c2 100644 --- a/docs/pages/result.rst +++ b/docs/pages/result.rst @@ -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 `_: - -.. 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 `_ -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 ` is used. +This happens due to `mypy issue `_. API Reference diff --git a/poetry.lock b/poetry.lock index 85a417b6b..597ea15d7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -56,6 +56,17 @@ colorama = ">=0.3.9" six = ">=1.10.0" stevedore = ">=1.20.0" +[[package]] +category = "dev" +description = "Easily capture stdout/stderr of the current process and subprocesses" +name = "capturer" +optional = false +python-versions = "*" +version = "2.4" + +[package.dependencies] +humanfriendly = ">=2.1" + [[package]] category = "dev" description = "Python package for providing Mozilla's CA Bundle." @@ -97,6 +108,22 @@ optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, <4" version = "4.5.3" +[[package]] +category = "dev" +description = "A backport of the dataclasses module for Python 3.6" +name = "dataclasses" +optional = false +python-versions = "*" +version = "0.6" + +[[package]] +category = "dev" +description = "Better living through Python with decorators" +name = "decorator" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +version = "4.4.0" + [[package]] category = "dev" description = "Style checker for Sphinx (or other) RST documentation" @@ -417,6 +444,17 @@ version = "2.1.11" [package.dependencies] gitdb2 = ">=2.0.0" +[[package]] +category = "dev" +description = "Human friendly output for text interfaces using Python" +name = "humanfriendly" +optional = false +python-versions = "*" +version = "4.18" + +[package.dependencies] +pyreadline = "*" + [[package]] category = "dev" description = "Internationalized Domain Names in Applications (IDNA)" @@ -622,6 +660,15 @@ optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" version = "2.4.0" +[[package]] +category = "dev" +description = "A python implmementation of GNU readline." +marker = "sys_platform == \"win32\"" +name = "pyreadline" +optional = false +python-versions = "*" +version = "2.1" + [[package]] category = "dev" description = "pytest: simple powerful testing with Python" @@ -668,6 +715,21 @@ version = "2.7.1" coverage = ">=4.4" pytest = ">=3.6" +[[package]] +category = "dev" +description = "pytest plugin for writing tests for mypy plugins" +name = "pytest-mypy-plugins" +optional = false +python-versions = "*" +version = "0.3.0" + +[package.dependencies] +capturer = "*" +dataclasses = "*" +decorator = "*" +mypy = "*" +pytest = "*" + [[package]] category = "dev" description = "Pytest plugin to randomly order tests and control random.seed." @@ -973,7 +1035,7 @@ python-versions = ">=2.7" version = "0.5.1" [metadata] -content-hash = "ec5ae0bef8205036f2b20697d9ef859f252d86f6d460af10fb3e7c8012e30403" +content-hash = "b3727185186422f200b4484a59b12fa767cbc574b1991e2ad35e859ffa501417" python-versions = "^3.6" [metadata.hashes] @@ -983,11 +1045,14 @@ atomicwrites = ["03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b attrs = ["10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", "ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"] babel = ["af92e6106cb7c55286b25b38ad7695f8b4efb36a90ba483d7f7a6628c46158ab", "e86135ae101e31e2c8ec20a4e0c5220f4eed12487d5cf3f78be7e98d3a57fc28"] bandit = ["d31a7b0819fe95d591106ba2d6c35568a513aba24db537ca71984781312a8e95", "e50fb4ed4ee8a98b8329385e48e606fded0999a2cb3e2acb6e7213c962ff0de1"] +capturer = ["090142a58f3f85def3a7dd55d9024d0d1a86d1a88aaf9317c0f146244994a615", "2c9d507516e5e86c442ff0c45b08cd0810d314e62bbea7e96ba0f473389d17dc"] certifi = ["59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", "b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"] chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"] colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"] coverage = ["0c5fe441b9cfdab64719f24e9684502a59432df7570521563d7b1aff27ac755f", "2b412abc4c7d6e019ce7c27cbc229783035eef6d5401695dccba80f481be4eb3", "3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", "39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", "3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", "42692db854d13c6c5e9541b6ffe0fe921fe16c9c446358d642ccae1462582d3b", "465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", "48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", "4ec30ade438d1711562f3786bea33a9da6107414aed60a5daa974d50a8c2c351", "5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", "5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", "6899797ac384b239ce1926f3cb86ffc19996f6fa3a1efbb23cb49e0c12d8c18c", "68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", "6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", "7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", "7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", "839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", "8e679d1bde5e2de4a909efb071f14b472a678b788904440779d2c449c0355b27", "8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", "932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", "93f965415cc51604f571e491f280cff0f5be35895b4eb5e55b47ae90c02a497b", "988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", "998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", "9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", "9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", "a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", "a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", "a9abc8c480e103dc05d9b332c6cc9fb1586330356fc14f1aa9c0ca5745097d19", "aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", "bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", "bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", "c22ab9f96cbaff05c6a84e20ec856383d27eae09e511d3e6ac4479489195861d", "c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", "c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", "c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", "ca58eba39c68010d7e87a823f22a081b5290e3e3c64714aac3c91481d8b34d22", "df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", "f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", "f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", "f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", "fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a"] +dataclasses = ["454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f", "6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84"] +decorator = ["86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", "f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6"] doc8 = ["2df89f9c1a5abfb98ab55d0175fed633cae0cf45025b8b1e0ee5ea772be28543", "d12f08aa77a4a65eb28752f4bc78f41f611f9412c4155e2b03f1f5d4a45efe04"] docutils = ["02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", "51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", "7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"] dparse = ["00a5fdfa900629e5159bf3600d44905b333f4059a3366f28e0dbd13eeab17b19", "cef95156fa0adedaf042cd42f9990974bec76f25dfeca4dc01f381a243d5aa5b"] @@ -1016,6 +1081,7 @@ flake8-quotes = ["fd9127ad8bbcf3b546fa7871a5266fd8623ce765ebe3d5aa5eabb80c01212b flake8-string-format = ["68ea72a1a5b75e7018cae44d14f32473c798cf73d75cbaed86c6a9a907b770b2", "774d56103d9242ed968897455ef49b7d6de272000cfa83de5814273a868832f1"] gitdb2 = ["83361131a1836661a155172932a13c08bda2db3674e4caa32368aa6eb02f38c2", "e3a0141c5f2a3f635c7209d56c496ebe1ad35da82fe4d3ec4aaa36278d70648a"] gitpython = ["563221e5a44369c6b79172f455584c9ebbb122a13368cc82cb4b5addff788f82", "8237dc5bfd6f1366abeee5624111b9d6879393d84745a507de0fda86043b65a8"] +humanfriendly = ["23057b10ad6f782e7bc3a20e3cb6768ab919f619bbdc0dd75691121bbde5591d", "33ee8ceb63f1db61cce8b5c800c531e1a61023ac5488ccde2ba574a85be00a85"] idna = ["c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", "ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"] imagesize = ["3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", "f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"] importlib-metadata = ["6dfd58dfe281e8d240937776065dd3624ad5469c835248219bd16cf2e12dbeb7", "cb6ee23b46173539939964df59d3d72c3e0c1b5d54b84f1d8a7e912fe43612db"] @@ -1038,9 +1104,11 @@ pydocstyle = ["2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8" pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"] pygments = ["71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127", "881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297"] pyparsing = ["1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", "9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03"] +pyreadline = ["4530592fc2e85b25b1a9f79664433da09237c1a270e4d78ea5aa3a2c7229e2d1", "65540c21bfe14405a3a77e4c085ecfce88724743a4ead47c66b84defcf82c32e", "9ce5fa65b8992dfa373bddc5b6e0864ead8f291c94fbfec05fbd5c836162e67b"] pytest = ["4a784f1d4f2ef198fe9b7aef793e9fa1a3b2f84e822d9b3a64a181293a572d45", "926855726d8ae8371803f7b2e6ec0a69953d9c6311fa7c3b6c1b929ff92d27da"] pytest-asyncio = ["9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf", "d734718e25cfc32d2bf78d346e99d33724deeba774cc4afdf491530c6184b63b"] pytest-cov = ["2b097cde81a302e1047331b48cadacf23577e431b61e9c6f49a1170bbe3d3da6", "e00ea4fdde970725482f1f35630d12f074e121a23801aabf2ae154ec6bdd343a"] +pytest-mypy-plugins = ["308577ec9279ac90b3bb25f5349cd52ea7d6b8fa841e1652ccd070a29e12945b", "ac3ea236287261dc19d177bf5efc8c853f8576f5bcf16e40ef31691d7d2e1c39"] pytest-randomly = ["07b642e177c5921386fa36ee3995a4bda918a1d20b3806a4f397a02788f88331", "f5ac7c6e30a32c1aa5ae911e00b5ffd6a0df38eb818f6b539e1e7b89995724ed"] pytz = ["303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", "d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"] pyyaml = ["57acc1d8533cbe51f6662a55434f0dbecfa2b9eaf115bede8f6fd00115a0c0d3", "588c94b3d16b76cfed8e0be54932e5729cc185caffaa5a451e7ad2f7ed8b4043", "68c8dd247f29f9a0d09375c9c6b8fdc64b60810ebf07ba4cdd64ceee3a58c7b7", "70d9818f1c9cd5c48bb87804f2efc8692f1023dac7f1a1a5c61d454043c1d265", "86a93cccd50f8c125286e637328ff4eef108400dd7089b46a7be3445eecfa391", "a0f329125a926876f647c9fa0ef32801587a12328b4a3c741270464e3e4fa778", "a3c252ab0fa1bb0d5a3f6449a4826732f3eb6c0270925548cac342bc9b22c225", "b4bb4d3f5e232425e25dda21c070ce05168a786ac9eda43768ab7f3ac2770955", "cd0618c5ba5bda5f4039b9398bb7fb6a317bb8298218c3de25c47c4740e4b95e", "ceacb9e5f8474dcf45b940578591c7f3d960e82f926c707788a570b51ba59190", "fe6a88094b64132c4bb3b631412e90032e8cfe9745a58370462240b8cb7553cd"] diff --git a/pyproject.toml b/pyproject.toml index a67e3a2f9..18a389c90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ sphinx-typlog-theme = "^0.7.1" doc8 = "^0.8.0" m2r = "^0.2.1" tomlkit = "^0.5.3" +pytest-mypy-plugins = "^0.3.0" [build-system] requires = ["poetry>=0.12"] diff --git a/returns/mypy_plugins/__init__.py b/returns/contrib/__init__.py similarity index 100% rename from returns/mypy_plugins/__init__.py rename to returns/contrib/__init__.py diff --git a/returns/contrib/mypy/__init__.py b/returns/contrib/mypy/__init__.py new file mode 100644 index 000000000..40a96afc6 --- /dev/null +++ b/returns/contrib/mypy/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/returns/contrib/mypy/decorator_plugin.py b/returns/contrib/mypy/decorator_plugin.py new file mode 100644 index 000000000..cc271b2fd --- /dev/null +++ b/returns/contrib/mypy/decorator_plugin.py @@ -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 diff --git a/returns/mypy_plugins/decorator.py b/returns/mypy_plugins/decorator.py deleted file mode 100644 index 71e668d53..000000000 --- a/returns/mypy_plugins/decorator.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- - -from typing import Callable, Optional, Type - -from mypy.types import CallableType -from mypy.plugin import Plugin, FunctionContext - -TYPED_DECORATORS = { - 'returns.result.safe', - 'returns.io.impure', -} - - -def _change_decorator_function_type( - decorated: CallableType, - decorator: CallableType, -) -> CallableType: - 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) -> Type: - return _change_decorator_function_type( - function_ctx.arg_types[0][0], - function_ctx.default_return_type, - ) - - -class TypedDecoratorPlugin(Plugin): - def get_function_hook( - self, fullname: str, - ) -> Optional[Callable[[FunctionContext], Type]]: - if fullname in TYPED_DECORATORS: - return _analyze_decorator - return None - - -def plugin(version: str): - return TypedDecoratorPlugin diff --git a/setup.cfg b/setup.cfg index ccde5f4b0..a4a0e700d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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] @@ -78,7 +82,7 @@ python_version = 3.6 # Plugins, includes custom: plugins = - returns.mypy_plugins.decorator + 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. diff --git a/test.py b/test.py deleted file mode 100644 index f6802d417..000000000 --- a/test.py +++ /dev/null @@ -1,14 +0,0 @@ -from returns.result import safe -from returns.io import impure - -@safe -def test() -> int: - return 1 - -@impure -def same(arg: str) -> str: - return arg - - -reveal_type(test) -reveal_type(same) diff --git a/tests/test_io/test_io_functions/test_impure.py b/tests/test_io/test_io_functions/test_impure.py index eb43d0bcb..8b13209f7 100644 --- a/tests/test_io/test_io_functions/test_impure.py +++ b/tests/test_io/test_io_functions/test_impure.py @@ -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) diff --git a/typesafety/test_io_types/impure.test b/typesafety/test_io_types/impure.test new file mode 100644 index 000000000..ac18b8c8e --- /dev/null +++ b/typesafety/test_io_types/impure.test @@ -0,0 +1,68 @@ +[CASE impure_decorator_no_params] +[disable_cache] +from returns.io import impure + +@impure +def test() -> int: + return 1 + +reveal_type(test) # E: Revealed type is 'def () -> returns.io.IO[builtins.int*]' +[/CASE] + + +[CASE impure_composition_no_params] +[disable_cache] +from returns.io import impure + +def test() -> int: + return 1 + +reveal_type(impure(test)) # E: Revealed type is 'def () -> returns.io.IO[builtins.int*]' +[/CASE] + + +[CASE impure_decorator_with_args] +[disable_cache] +from returns.io import impure + +@impure +def test(first: int, second: str = None, *, kw: bool = True) -> int: + return 1 + +reveal_type(test) # E: Revealed type is 'def (first: builtins.int, second: Union[builtins.str, None] =, *, kw: builtins.bool =) -> returns.io.IO[builtins.int*]' +[/CASE] + + +[CASE impure_composition_with_args] +[disable_cache] +from returns.io import impure + +def test(first: int, second: str = None, *, kw: bool = True) -> int: + return 1 + +reveal_type(impure(test)) # E: Revealed type is 'def (first: builtins.int, second: Union[builtins.str, None] =, *, kw: builtins.bool =) -> returns.io.IO[builtins.int*]' +[/CASE] + + +[CASE impure_decorator_with_args_kwargs] +[disable_cache] +from returns.io import impure + +@impure +def test(*args, **kwargs) -> int: + return 1 + +reveal_type(test) # E: Revealed type is 'def (*args: Any, **kwargs: Any) -> returns.io.IO[builtins.int*]' +[/CASE] + + +[CASE impure_decorator_with_typed_args_kwargs] +[disable_cache] +from returns.io import impure + +@impure +def test(*args: int, **kwargs: str) -> int: + return 1 + +reveal_type(test) # E: Revealed type is 'def (*args: builtins.int, **kwargs: builtins.str) -> returns.io.IO[builtins.int*]' +[/CASE] diff --git a/typesafety/test_result_types/safe.test b/typesafety/test_result_types/safe.test new file mode 100644 index 000000000..843333a2b --- /dev/null +++ b/typesafety/test_result_types/safe.test @@ -0,0 +1,68 @@ +[CASE safe_decorator_no_params] +[disable_cache] +from returns.result import safe + +@safe +def test() -> int: + return 1 + +reveal_type(test) # E: Revealed type is 'def () -> returns.result.Result[builtins.int*, builtins.Exception]' +[/CASE] + + +[CASE safe_composition_no_params] +[disable_cache] +from returns.result import safe + +def test() -> int: + return 1 + +reveal_type(safe(test)) # E: Revealed type is 'def () -> returns.result.Result[builtins.int*, builtins.Exception]' +[/CASE] + + +[CASE safe_decorator_with_args] +[disable_cache] +from returns.result import safe + +@safe +def test(first: int, second: str = None, *, kw: bool = True) -> int: + return 1 + +reveal_type(test) # E: Revealed type is 'def (first: builtins.int, second: Union[builtins.str, None] =, *, kw: builtins.bool =) -> returns.result.Result[builtins.int*, builtins.Exception]' +[/CASE] + + +[CASE safe_composition_with_args] +[disable_cache] +from returns.result import safe + +def test(first: int, second: str = None, *, kw: bool = True) -> int: + return 1 + +reveal_type(safe(test)) # E: Revealed type is 'def (first: builtins.int, second: Union[builtins.str, None] =, *, kw: builtins.bool =) -> returns.result.Result[builtins.int*, builtins.Exception]' +[/CASE] + + +[CASE safe_decorator_with_args_kwargs] +[disable_cache] +from returns.result import safe + +@safe +def test(*args, **kwargs) -> int: + return 1 + +reveal_type(test) # E: Revealed type is 'def (*args: Any, **kwargs: Any) -> returns.result.Result[builtins.int*, builtins.Exception]' +[/CASE] + + +[CASE safe_decorator_with_typed_args_kwargs] +[disable_cache] +from returns.result import safe + +@safe +def test(*args: int, **kwargs: str) -> int: + return 1 + +reveal_type(test) # E: Revealed type is 'def (*args: builtins.int, **kwargs: builtins.str) -> returns.result.Result[builtins.int*, builtins.Exception]' +[/CASE] From 5d8ecef84bedb41c2547615082596346048d66d4 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 15 Jun 2019 11:58:31 +0300 Subject: [PATCH 4/4] Sorts deps --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 18a389c90..998c5c680 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" @@ -55,7 +56,6 @@ sphinx-typlog-theme = "^0.7.1" doc8 = "^0.8.0" m2r = "^0.2.1" tomlkit = "^0.5.3" -pytest-mypy-plugins = "^0.3.0" [build-system] requires = ["poetry>=0.12"]