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

redo type annotations #1995

Merged
merged 2 commits into from
Jan 10, 2021
Merged

redo type annotations #1995

merged 2 commits into from
Jan 10, 2021

Conversation

davidism
Copy link
Member

@davidism davidism commented Jan 10, 2021

Rewrite the type annotations by hand. The previous annotations generated from MonkeyType were too inaccurate, and trying to modify them after the fact was frustrating, as fixing one part would cause errors in others. Wrote a RedBaron script to remove all existing annotations.

Tried to enable as many Mypy checks as possible, but had to make some compromises in the interest of time. Mypy is nice overall, but it has a lot of corner cases or unimplemented features that Werkzeug's code seems to trigger quite a lot. This can be addressed by future contributors.

  • allow_redefinition should be removed.
  • disallow_untyped_defs and disallow_incomplete_defs should be enabled (although can be disabled for tests).
  • warn_return_any should be enabled.
  • no_implicit_reexport should be enabled.
  • other things related to untyped code and Any should be enabled
  • remove as many type: ignore comments and cast() calls as possible. Ignore comments can hide other issues on the same line.
  • It would be interesting to see how other typing tools such as pyright and pyre handle the current annotations.

I tried to modify as little code as possible (besides the annotations). Occasionally I had to rewrite an instance or none check, or rename variables to avoid redefinition. There were a few places that typing seemed to catch a legitimate bug (such as calling dict.get and getting None). All tests still pass.

I removed the werkzeug.types module, as it didn't end up being necessary. I imported typing as t to make the annotations shorter (t.Optional instead of typing.Optional). That also avoids importing a ton of names from typing into every module. Typeshed provides a wsgiref.types stub that came in handy for annotating WSGI environs and callables. Flake8 (through PyFlakes) doesn't understand @t.overload, it only understands @typing.overload (PyCQA/pyflakes#561), so in some files there's both import typing and import typing as t.

Inheriting from typing.Generic makes instantiation many times slower until Python 3.9. This is unacceptable for Werkzeug where I estimate ~10 are created per request. I wanted the data structures to be generic, so that code is typed in a stub pyi file instead. Unfortunately, that disables all type checking on the py file and only consider the pyi file (python/mypy#5028), so there was no type checking done on the datastructures.py code itself. I considered this acceptable, as I already know Werkzeug's code is passing tests, and I'd rather be able to do MultiDict[str, str] in user code instead.

Much of the complexity came from Werkzeug allowing both str and bytes in many places, even where it no longer makes much sense since Python 2 compatibility is gone. Additionally, there are some odd code paths that don't appear to be used anywhere. Should look at deprecating these types of things in a future release.

@davidism davidism added this to the 2.0.0 milestone Jan 10, 2021
@davidism davidism merged commit ae5c781 into master Jan 10, 2021
@davidism davidism deleted the typing-imports branch January 10, 2021 17:26
@graingert
Copy link

@davidism it looks like you could backport the 3.9 typing.Generic into a compat.py and use it like:

if TYPE_CHECKING:
    from typing import Generic
else:
    from .compat import Generic

@davidism
Copy link
Member Author

davidism commented Jan 10, 2021

Thanks, but I'm not interested in maintaining compat files anymore. See: removal of Python 2. All that does is mean I have to pull in code that's not about Werkzeug, then keep track of removing it years later, and make sure it keeps working with Mypy and other tools in the mean time.

src/werkzeug/_internal.py Show resolved Hide resolved
src/werkzeug/_internal.py Show resolved Hide resolved
src/werkzeug/wrappers/response.py Show resolved Hide resolved
src/werkzeug/wrappers/base_request.py Show resolved Hide resolved
bjgill added a commit to bjgill/werkzeug that referenced this pull request Jan 26, 2021
In pallets#1995, typing was added to large parts of this project. This will be really useful to me.

One of the problems I have with the latest release is `abort`. PyCharm doesn't know that `abort` aborts. So it will complain about possibly uninitialised variables if I write code like this:

```
if foo:
    bar = 1
else:
    abort(404)

print(bar)
```

In master, `abort` now returns type `None`: `def abort(status: t.Union[int, "Response"], *args, **kwargs) -> None:`

This type annotation means that the function returns `None`. This is not the case - I think `abort` always raises an exception.

Instead, what we want to do is change the return type to [`t.NoReturn`](https://www.python.org/dev/peps/pep-0484/#the-noreturn-type). This expresses the correct meaning, which is that the `abort` function never returns. In turn, this will help PyCharm and other analysers understand code that uses `abort` better.
@bjgill bjgill mentioned this pull request Jan 26, 2021
4 tasks
bjgill added a commit to bjgill/werkzeug that referenced this pull request Jan 26, 2021
In pallets#1995, typing was added to large parts of this project. This will be really useful to me.

One of the problems I have with the latest release is `abort`. PyCharm doesn't know that `abort` aborts. So it will complain about possibly uninitialised variables if I write code like this:

```
if foo:
    bar = 1
else:
    abort(404)

print(bar)
```

In master, `abort` now returns type `None`: `def abort(status: t.Union[int, "Response"], *args, **kwargs) -> None:`

This type annotation means that the function returns `None`. This is not the case - I think `abort` always raises an exception.

Instead, what we want to do is change the return type to [`t.NoReturn`](https://www.python.org/dev/peps/pep-0484/#the-noreturn-type). This expresses the correct meaning, which is that the `abort` function never returns. In turn, this will help PyCharm and other analysers understand code that uses `abort` better.
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Jan 30, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants