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, Next Steps #19389

Closed
3 tasks
troygiorshev opened this issue Jun 26, 2020 · 16 comments
Closed
3 tasks

MyPy, Next Steps #19389

troygiorshev opened this issue Jun 26, 2020 · 16 comments

Comments

@troygiorshev
Copy link
Contributor

troygiorshev commented Jun 26, 2020

MyPy was added in #18210, allowing for static type checking of type hints in our testing framework. However, there is still some work to be done before we can take advantage of these new capabilities. 🚀

Right now, lint-python.sh runs MyPy with the --ignore-imports flag. This is done primarily to ignore the lack of type hinting for zmq, but it hides many errors in our own code too.

To begin to see what's needed, first run mypy test/functional. You'll see two sets of errors. One complains about Skipping analyzing 'zmq'. This one can be ignored for now by changing the identified import zmq lines to import zmq # type: ignore.

The other states that it Cannot find implementation or library stub for module named 'data'. Remember that for a second.

Now go into test/functional/test_framework and run mypy mininode.py. Observe two more Cannot find implementation or library stub for module errors, now for test_framework.messages and test_framework.util.

What's happening here is that MyPy is failing to handle importing our test_framework package. We'll need to do a few things to fix this.

Resources

Important Notes

Our minimum supported python version is 3.5, so any changes must be compatible. In particular this means that we must use type comments, as opposed to type hinting, in variables that aren't function parameters. (i.e. foo # type: int as opposed to foo: int). This will complicate the use of MonkeyType, as it doesn't support type comments. It may be useful to make a scripted diff between type comments and type hints.

Useful skills:

MyPy experience

Want to work on this issue?

For guidance on contributing, please read CONTRIBUTING.md before opening your pull request.

@fanquake
Copy link
Member

Just a note for anyone looking into this. Make sure that whatever changes you plan on making are compatible with Python 3.5, as that is our minimum supported version of Python.

@ysangkok
Copy link

Why would you use stub files for in-tree modules? Why not add the type annotations in the implementation file?

@troygiorshev
Copy link
Contributor Author

Just a note for anyone looking into this. Make sure that whatever changes you plan on making are compatible with Python 3.5, as that is our minimum supported version of Python.

This is a very important point, I've updated the issue to reflect it. In particular this means that we must use type comments, as opposed to type hinting, in variables that aren't function parameters. (i.e. foo # type: int as opposed to foo: int). This will complicate the use of MonkeyType, as it doesn't support type comments. It may be easy to make a scripted diff between type comments and type hints.

Why would you use stub files for in-tree modules? Why not add the type annotations in the implementation file?

afaik we will need to do both, and this is simply a limitation of MyPy's design. I'm more than happy to be proven wrong!

@scorbajio
Copy link

I am open to take over this issue if there is no one actively working on it.

@JeremyRubin
Copy link
Contributor

I have some of the library parts mypy typed (I think messages.py and script.py), if anyone wants them I can share.

@scorbajio
Copy link

I generated the stub files using stubgen, but they do not seem to be python 3.5 compatible. I cannot think of a better solution than to write a script to change non-function-parameter type hints (i.e. foo: int) into type comments (i.e. foo # type: int). I think this is what @troygiorshev means by "scripted diff", right?

@troygiorshev
Copy link
Contributor Author

troygiorshev commented Jul 16, 2020

@ghorbanian Thanks for giving this a look! Yup, that's exactly what I mean by "scripted diff". I'll go check out #19532.

@emc99
Copy link

emc99 commented Mar 9, 2021

I am keen on helping out with this issue. Where do I start? Should I go away and learn C++ and Python first?

@NelsonFrancisco
Copy link

I just want to point out two things because of my experience:

  1. I don't see any mention in this repo about a Python virtualenv to facilitate development. Is it too far fetched or a script/tutorial would be relevant?

  2. Maybe it's because it is outdated, so an update would be relevant:

    Now go into test/functional/test_framework and run mypy mininode.py. Observe two more Cannot find implementation or library stub for module errors, now for test_framework.messages and test_framework.util.

    mininode.py does not exist in that folder.

@fanquake
Copy link
Member

Since the release of PyZMQ 21.0, the library now contains mypy stubs:

mypy type stubs, which should improve static analysis of pyzmq, especially for dynamically defined attributes such as zmq constants. These are new! Let us know if you find any issues.

This should negate the need to ignore zmq imports.

@fspv
Copy link
Contributor

fspv commented Nov 3, 2021

I created a PR to remove the most of remaining type: ignore comments #23420

Merging it will leave us with just a few suppressed places:

spv@laptop bitcoin $ grep -EIR 'type:.*ignore' . 2>/dev/null | grep -v venv
./contrib/devtools/security-check.py:import lief #type:ignore
./contrib/devtools/symbol-check.py:import lief #type:ignore
./test/functional/test_runner.py:if os.name != "nt" or sys.getwindowsversion() >= (10, 0, 14393):  # type:ignore
./test/functional/test_runner.py:        kernel32 = ctypes.windll.kernel32  # type: ignore
./test/functional/combine_logs.py:        import jinja2  # type:ignore

@maflcko
Copy link
Member

maflcko commented Nov 3, 2021

Anything left to do here that is a good first issue? If yes, the issue text should be updated or a new issue be filed (and this one closed).

@fspv
Copy link
Contributor

fspv commented Nov 3, 2021

These 2 Windows-specific lines are easy to fix

./test/functional/test_runner.py:if os.name != "nt" or sys.getwindowsversion() >= (10, 0, 14393):  # type:ignore
./test/functional/test_runner.py:        kernel32 = ctypes.windll.kernel32  # type: ignore

https://mypy.readthedocs.io/en/stable/common_issues.html#python-version-and-system-platform-checks

After that it can be closed, I believe, as only untyped libs (lief and jinja2) are left.

UPD: for the ignored imports the description suggests generating stub files, so the issue can't be closed. But needs to be updated anyway.

@maflcko
Copy link
Member

maflcko commented Nov 3, 2021

These 2 Windows-specific lines are easy to fix

If someone wants to fix those, seems fine to just create a pull. Though, I think there is no risk if they are left unfixed. It is not like color formatting in the test framework output is any critical feature.

UPD: for the ignored imports the description suggests generating stub files

I am not sure if we want to maintain stub files for third party python packages that we use. Is the effort going to be worth the additional checks? jinja2 is only used to print logs, a feature that is know to be working and unlikely to break. lief I am not sure. An alternative would be to just wait for the upstream project to add the annotations and do nothing here?

@fspv
Copy link
Contributor

fspv commented Nov 3, 2021

Though, I think there is no risk if they are left unfixed.

Agree, no risks at all. Just a low-hanging fruit for those looking for the "good first issue".

I am not sure if we want to maintain stub files

Agree with this as well. The description should be updated to reflect this though.

@maflcko
Copy link
Member

maflcko commented Nov 3, 2021

Ok, I am just going ahead and closing this. Maybe a new issue can be started if people want to discuss whether we want to maintain stub files for 3rd party packages.

@maflcko maflcko closed this as completed Nov 3, 2021
fanquake added a commit to bitcoin-core/gui that referenced this issue Nov 11, 2021
…corator

467fe57 test: Correct MyPy typing for subtest decorator (Pavel Safronov)

Pull request description:

  This is the part of the effort to make python typing correct bitcoin/bitcoin#19389

  The typing of the `subtest` decorator within `p2p_segwit.py` test file was incorrect.

  Since `subtest` function is defined as a member of the class, it expects `self` as a first argument, and it is not provided. Hence the typing errors (that are currently suppressed by `type: ignore`).

  ```
  (venv) vagrant@ubuntu-focal:/vagrant/test/functional$ mypy p2p_segwit.py
  p2p_segwit.py:298: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:327: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:358: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:447: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:519: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:561: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:659: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:670: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:737: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:826: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:866: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:941: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:977: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1052: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1089: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1136: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1220: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1312: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1406: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1440: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1543: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1729: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1782: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1881: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1983: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:2027: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  Found 26 errors in 1 file (checked 1 source file)
  ```

  However, the tests are passing, because there is no `self` argument passed when it is called as a decorator.

  There is also suppressed pylint error `# noqa: N805` pointing to the same issue.
  ```
  N805 first argument of a method should be named 'self'
  ```

  So the solution is to move the `subtest` definition outside the class, so the `self` argument is no longer required.

  After doing so, both mypy and unittests are successfully passing:

  ```
  (venv) vagrant@ubuntu-focal:/vagrant/test/functional$ mypy p2p_segwit.py
  Success: no issues found in 1 source file
  ```

  ```
  (venv) vagrant@ubuntu-focal:/vagrant/test/functional$ ./test_runner.py p2p_segwit
  Temporary test directory at /tmp/test_runner__🏃_20211103_011449
  Running Unit Tests for Test Framework Modules
  ..........
  ----------------------------------------------------------------------
  Ran 10 tests in 0.546s

  OK
  Remaining jobs: [p2p_segwit.py]
  1/1 - p2p_segwit.py passed, Duration: 81 s

  TEST          | STATUS    | DURATION

  p2p_segwit.py | ✓ Passed  | 81 s

  ALL           | ✓ Passed  | 81 s (accumulated)
  Runtime: 81 s
  ```
  ```

ACKs for top commit:
  fanquake:
    ACK 467fe57

Tree-SHA512: e4c3e2d284f47a6bfbf4af22d4021123cdd9c2ea16ec90a91b466ad1a5af615bb4e15959e6cf56c788701d7e7cbda91a8ffc4347965095c3384eae3d28f261af
sidhujag pushed a commit to syscoin/syscoin that referenced this issue Nov 11, 2021
467fe57 test: Correct MyPy typing for subtest decorator (Pavel Safronov)

Pull request description:

  This is the part of the effort to make python typing correct bitcoin#19389

  The typing of the `subtest` decorator within `p2p_segwit.py` test file was incorrect.

  Since `subtest` function is defined as a member of the class, it expects `self` as a first argument, and it is not provided. Hence the typing errors (that are currently suppressed by `type: ignore`).

  ```
  (venv) vagrant@ubuntu-focal:/vagrant/test/functional$ mypy p2p_segwit.py
  p2p_segwit.py:298: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:327: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:358: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:447: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:519: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:561: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:659: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:670: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:737: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:826: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:866: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:941: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:977: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1052: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1089: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1136: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1220: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1312: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1406: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1440: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1543: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1729: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1782: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1881: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:1983: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  p2p_segwit.py:2027: error: Argument 1 to "subtest" has incompatible type "Callable[[SegWitTest], Any]"; expected "SegWitTest"
  Found 26 errors in 1 file (checked 1 source file)
  ```

  However, the tests are passing, because there is no `self` argument passed when it is called as a decorator.

  There is also suppressed pylint error `# noqa: N805` pointing to the same issue.
  ```
  N805 first argument of a method should be named 'self'
  ```

  So the solution is to move the `subtest` definition outside the class, so the `self` argument is no longer required.

  After doing so, both mypy and unittests are successfully passing:

  ```
  (venv) vagrant@ubuntu-focal:/vagrant/test/functional$ mypy p2p_segwit.py
  Success: no issues found in 1 source file
  ```

  ```
  (venv) vagrant@ubuntu-focal:/vagrant/test/functional$ ./test_runner.py p2p_segwit
  Temporary test directory at /tmp/test_runner__🏃_20211103_011449
  Running Unit Tests for Test Framework Modules
  ..........
  ----------------------------------------------------------------------
  Ran 10 tests in 0.546s

  OK
  Remaining jobs: [p2p_segwit.py]
  1/1 - p2p_segwit.py passed, Duration: 81 s

  TEST          | STATUS    | DURATION

  p2p_segwit.py | ✓ Passed  | 81 s

  ALL           | ✓ Passed  | 81 s (accumulated)
  Runtime: 81 s
  ```
  ```

ACKs for top commit:
  fanquake:
    ACK 467fe57

Tree-SHA512: e4c3e2d284f47a6bfbf4af22d4021123cdd9c2ea16ec90a91b466ad1a5af615bb4e15959e6cf56c788701d7e7cbda91a8ffc4347965095c3384eae3d28f261af
@bitcoin bitcoin locked and limited conversation to collaborators Nov 3, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

9 participants