Skip to content
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

Mypy fails to determine function's type in icontract.ensure() #243

Open
claudio-ebel opened this issue May 9, 2022 · 6 comments
Open

Mypy fails to determine function's type in icontract.ensure() #243

claudio-ebel opened this issue May 9, 2022 · 6 comments

Comments

@claudio-ebel
Copy link

Bug Report
When a type-annotated function is (re-)used in its own postcondition icontract.ensure decorator, mypy is not able to determine its type. Since mypy does successfully infers the types of an annotated function before the function's actual definition, this bug is part of the icontract library and not mypy.

To Reproduce

  1. Create a file “bug.py” with this content:
    # ––– file: bug.py –––
    from icontract import ensure
    
    
    @ensure(lambda a, b, result: result == myadd(b, a), "Commutativity violated!")
    def myadd(a: int, b: int) -> int:
        return a + b
  2. Use mypy on it
    $ mypy --config-file= bug.py 
    bug.py:5: error: Cannot determine type of "myadd"
    Found 1 error in 1 file (checked 1 source file)

Expected Behavior
In general, mypy does not have a problem with using an annotated function before its definition. To see this,

  1. Create a file “no_bug.py” with this content:
    # ––– file: no_bug.py –––
    
    
    def use_before() -> int:
        return myadd(1, 2)  # used before def; like in bug.py
    
    
    def myadd(a: int, b: int) -> int:
        return a + b
  2. Use mypy on it
    $ mypy --config-file= no_bug.py 
    Success: no issues found in 1 source file

Failed solution attempts
Using a forward declaration of myadd did not help:

  1. Create a file “forward_decl.py” with this content:
    # ––– file forward_decl.py ––
    from icontract import ensure
    from typing import Callable, cast
    
    MyaddType = Callable[[int, int], int]
    
    
    @ensure(lambda a, b, result: result == cast(MyaddType, myadd)(b, a),
            "Commutativity violated!")
    def myadd(a: int, b: int) -> int:
        return a + b
  2. Use mypy on it
    $ mypy --config-file= forward_decl.py
    forward_decl.py:8: error: Cannot determine type of "myadd"
    Found 1 error in 1 file (checked 1 source file)

An alternative use of a cast of myadd's return type did not help either:

  1. Create a file “return_type.py” with this content:
    # ––– file: return_type.py –––
    from icontract import ensure
    from typing import cast
    
    
    @ensure(lambda a, b, result: result == cast(bool, myadd(b, a)),
            "Commutativity violated!")
    def myadd(a: int, b: int) -> int:
        return a + b
  2. Use mypy on it
    $ mypy --config-file= return_type.py
    return_type.py:6: error: Cannot determine type of "myadd"
    Found 1 error in 1 file (checked 1 source file)

Your Environment

  • Mypy version used: mypy 0.950 (compiled: yes)
  • Mypy command-line flags: --config-file=
  • icontract version used: icontract==2.6.1
  • Python version used: Python 3.10.4
@mristin
Copy link
Collaborator

mristin commented May 9, 2022

Thanks for the issue, @claudio-ebel ! I'm busy till this Thu, but afterwards I should be able to find some time to have a look at it.

@claudio-ebel
Copy link
Author

Wow, thank you @mristin for your super fast reply! Please take your time, I've even found a trick to bypass this issue! Just introduce an annotated helper function forwarding the arguments:

# ––– file: trick.py –––
from icontract import ensure


def _myadd(a: int, b: int) -> int:
    """helper function to circumvent icontract issue"""
    return myadd(a, b)


@ensure(lambda a, b, result: result == _myadd(b, a), "Commutativity violated!")
def myadd(a: int, b: int) -> int:
    return a + b

resulting to no issue:

$ mypy --config-file= trick.py
Success: no issues found in 1 source file

@mristin
Copy link
Collaborator

mristin commented May 19, 2022

@claudio-ebel please apologize again for the delay. I'm having some tough time at work.

@mristin
Copy link
Collaborator

mristin commented May 22, 2022

@claudio-ebel Now this is a coincidence :-) -- they are just about to fix this issue in mypy: python/mypy#8645

Let's wait for a couple of days for the next mypy release. You'll need Python 3.10 for this feature to work, though.

@claudio-ebel
Copy link
Author

@mristin absolutely no problem and thank you very much for linking that issue!

Sadly, even with the new mypy version, the problem is not solved yet:

$ python --version
Python 3.10.4
$ mypy --version
mypy 0.960 (compiled: yes)
$ pip freeze | grep icontract
icontract==2.6.1
$ mypy --config-file= bug.py 
bug.py:5: error: Cannot determine type of "myadd"
Found 1 error in 1 file (checked 1 source file)

I hope that the work of the mypy team still eases this bugfix. Please take your time, this issue should not add to your tough time at work!

@mristin
Copy link
Collaborator

mristin commented Jun 5, 2022

@claudio-ebel I had another look at the issue. Finally I understood what's going on -- I totally missed the point in the original comments.

Let me document it here, in case somebody else needs to look it up in the future. The issue is not with ParamSpec and inference on the callable, and has no relation to python/mypy#8645. Instead, it is an issue with myadd not being defined when it is first called in the lambda. Mypy can not make the link that the decorated function is also used in its decorator.

I suppose this is one of the cases where it is appropriate to write # type: ignore:

from icontract import ensure


@ensure(
    lambda a, b, result: 
    result == myadd(b, a),   # type: ignore
    "Commutativity violated!"
)
def myadd(a: int, b: int) -> int:
    return a + b

I'll file an issue with mypy; perhaps there are other people relying on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants