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

requirements: Add django-stubs. #18777

Merged
merged 5 commits into from Oct 5, 2022
Merged

requirements: Add django-stubs. #18777

merged 5 commits into from Oct 5, 2022

Conversation

PIG208
Copy link
Member

@PIG208 PIG208 commented Jun 10, 2021

#11560

@andersk
Copy link
Member

andersk commented Jun 10, 2021

update-locked-requirements needs to be run in an Ubuntu 18.04 environment (not Ubuntu 20.04).

And this is clearly going to be a backwards-incompatible provision update, so PROVISION_VERSION will need a major bump.

@PIG208
Copy link
Member Author

PIG208 commented Jun 10, 2021

Oops, this is accidentally closed by an empty push.

@PIG208 PIG208 reopened this Jun 10, 2021
@zulipbot zulipbot added size: M and removed size: XS labels Jun 10, 2021
@timabbott
Copy link
Sponsor Member

There's 2K errors, but if you look at them, a huge portion are copies of a single confusion. E.g. we frequently access the .realm_id attribute of Django model objects with a UserProfile object; that attribute should be being implicitly generated by the ORM integration.

My guess is the problem is you haven't installed the plugin: https://github.com/typeddjango/django-stubs#installation

which I suspect exists to implicitly add those fields.

@timabbott
Copy link
Sponsor Member

timabbott commented Jun 10, 2021

I'm pushing back to this PR with two commits: the requirements change tweaked to also configure the mypy plugin, which has 1999 errors. And then there's a new commit that brings that down to 1946 (fixes 53 errors) by adding some missing type declarations in test_auth_backends.py, and passes mypy in master (so we happily could merge it right now). Those missing type annotations were a type-checking bug -- the base class accesses values/functions that it expects the subclass to define, but didn't tell the type system about that intent. So we shouldn't think of 2K errors as scary or intimidating; the nature of mypy is that something incorrectly typed that is used in many places results in large numbers of copies of a given error.

A good strategy for this project will be to debug with the full branch, generate commits that fix type annotations issues in a way that's correct (especially those that cause 50 errors with the Django stubs included!), and then incrementally merge those as we can. At the same time, we'll accumulate some commits that only work with the Django stubs package, e.g. because they use types that it adds or would be a regressions without the package. One needs to switch branches and provisioning to run the different check modes, but Zulip's provision caching makes that only take a few seconds.

A few large clusters I notice when skimming:

  • Roughly 800 of the errors seem to be in zerver/models.py, resulting from the fact that we write # type: bool for our BooleanField attributes, which was the best approximation for typing Django model field attributes without the mypy plugin, but is wrong with it (the plugin will correct the type). So quite possibly half the errors with this branch will be fixed by just deleting those type: bool type variable annotations; that commit will need to merge late, though, since doing that deletion would be a regression without the plugin.
  • In zerver/middleware.py and various other places, we monkey-patch extra fields onto HttpRequest objects. What we should be doing is declaring a ZulipHttpRequest type extending the Django HttpRequest in zerver/lib/types.py and using that type.
  • In the markdown processor / message send path, we monkey-patch extra fields onto Message objects. What we should be doing is having those functions return a RenderedMessage type that explicitly declares those extra fields.

I think this is actually really promising!

@PIG208
Copy link
Member Author

PIG208 commented Jun 11, 2021

update 1946 -> 1892
I have refactored the render_markdown function and reduces approximately 50 errors caused by the untyped additional properties added to the message object. It has passed mypy on master. However, an additional type ignore needs to be added to rendered_message: RenderedMessage = message in do_render_markdown as we can't avoid an explicit type cast when trying to add properties that don't exist on Message before. (But this will causes error on master because the type ignore is "unused")

PIG208 added a commit to PIG208/zulip that referenced this pull request Jun 15, 2021
This is a preparation for adding django stubs.

Related: zulip#18777
@PIG208
Copy link
Member Author

PIG208 commented Jun 15, 2021

update 1907 (rebased to master) -> 1889
I have refactored the functions related to render_incoming_message to make sure that the additional properties brought by render_markdown are now referred via rendered_message of type RenderedMessage instead of the message object. This effectively eliminated "Message" has no attribute error for those attributes related to message rendering.

@PIG208 PIG208 marked this pull request as ready for review June 15, 2021 21:47
@PIG208
Copy link
Member Author

PIG208 commented Jun 15, 2021

Because this PR is going to be huge and it is unlikely to be completed without running into merge conflicts, perhaps we can start reviewing & merging part of it on a commit-by-commit basis.

zerver/lib/message.py Outdated Show resolved Hide resolved
zerver/lib/message.py Outdated Show resolved Hide resolved
zerver/lib/message.py Outdated Show resolved Hide resolved
@timabbott
Copy link
Sponsor Member

Because this PR is going to be huge and it is unlikely to be completed without running into merge conflicts, perhaps we can start reviewing & merging part of it on a commit-by-commit basis.

Yeah, I completely agree. And really, we can keep this PR as mostly the commits that we'll merge at the end, and submit commits that work in master as their own PRs. I've merged 56e6bd7164131d198a528bc450364971be665c87 as a first step in that direction.

PIG208 added a commit to PIG208/zulip that referenced this pull request Jun 19, 2021
This adds a new class called MessageRenderingResult to contain the
additional properties we added to the Message object (like alert_words)
as well as the rendered content to ensure typesafe reference. No
behavioral change is made except changes in typing.

This is a preparatory change for adding django-stubs to the backend.

Related: zulip#18777
@andersk
Copy link
Member

andersk commented Sep 26, 2022

I’ve reordered and squashed some of the commits to preserve bisectability. I think this is now ready.

@@ -295,6 +295,13 @@ class HostRequestMock(HttpRequest):
"""A mock request object where get_host() works. Useful for testing
routes that use Zulip's subdomains feature"""

# The base class HttpRequest has GET and POST been immutable QueryDict, which makes
# it incompatible for HostRequestMock to define them as being mutable. Since the test
# cases rely on the mutability of these fields, we override it with type ignore.
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine for now but I'd be curious how many tests change these vs. declaring a new HostRequestMock for new values; it seems possible we could either try to purge the modification pattern or make a MutableHostRequestMock used by a small number of tests that plan to do that.

Copy link
Member Author

@PIG208 PIG208 Sep 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main issue here is that HostRequestMock itself relies on this mutability of POST to initialize it in the first place. POST is mutated all the time via post_data.

For GET, HostRequestMock does not modify it in __init__, but we do have one or two test cases modifying its value, which is not as often.

@@ -15,6 +15,9 @@
# See https://zulip.readthedocs.io/en/latest/subsystems/settings.html for more information
#
########################################################################
import django_stubs_ext

django_stubs_ext.monkeypatch()
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a comment documenting what this does?

https://pypi.org/project/django-stubs-ext/ explains it allows runtime monkey-patching... but it'd be helpful to have the context on why Zulip needs that behavior.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I know, we only really need this for models.Lookup. Added comments on this.

@@ -4447,7 +4447,7 @@ class RealmAuditLog(AbstractRealmAuditLog):
null=True,
on_delete=CASCADE,
)
event_last_message_id: Optional[int] = models.IntegerField(null=True)
event_last_message_id = models.IntegerField(null=True)
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this hunk belong in the previous commit?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

Copy link
Member

@andersk andersk Sep 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had actually moved that out of the previous commit intentionally, as the previous commit doesn’t type check with this hunk.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. event_last_message_id = assert_is_not_none(log_entry.event_last_message_id) fails.

@timabbott
Copy link
Sponsor Member

I didn't full read models: Implicitly type model fields with django-stubs, which at least needs a commit message tweak; and also I guess the last commit should close the original issue? But I didn't see any significant impediments to merging this (requested a few comment tweaks above).

@PIG208 PIG208 force-pushed the django-stubs branch 2 times, most recently from 43c5171 to 4a7ee75 Compare September 27, 2022 18:21
@PIG208
Copy link
Member Author

PIG208 commented Sep 27, 2022

For reviewing purposes, this is the script that generates most of dc1aa60.

#!/usr/bin/bash

# This removes all the type annnotations for model fields

set -ex

TARGETS=$(find . -name models.py)
sed -i "s|:.*\( = models.*\)|\1|g" $TARGETS

@PIG208
Copy link
Member Author

PIG208 commented Sep 27, 2022

I chose to make requirements: Add django-stubs and configure plugin. close the original issue, since the last two commits are only follow-ups for idiomatic improvements.

@PIG208 PIG208 force-pushed the django-stubs branch 2 times, most recently from a1feb5b to 9e2c021 Compare October 1, 2022 15:30
We no longer need to annotate the type of objects returned
from queries since django-stubs plugin infers that already.

Signed-off-by: Zixuan James Li <p359101898@gmail.com>
Previously, we type the model fields with explicit type annotations
manually with the approximate types. This was because the lack of types
for Django.

django-stubs provides more specific types for all these fields that
incompatible with our previous approximate annotations. So now we can
remove the inline type annotations and rely on the types defined in the
stubs. This allows mypy to infer the types of the model fields for us.

Signed-off-by: Zixuan James Li <p359101898@gmail.com>
@PIG208

This comment was marked as resolved.

@zulipbot zulipbot added the integration review Added by maintainers when a PR may be ready for integration. label Oct 5, 2022
PIG208 and others added 3 commits October 5, 2022 15:41
Note that django_stubs_ext is required to be placed within common.in
because we need the monkeypatched types in runtime; django-stubs
itself is for type checking only.

In the future, we would like to pin to a release instead of a git
revision, but several patches we've contributed upstream have not
appeared in a release yet.

We also remove the type annotation for RealmAuditLog.event_last_message_id
here instead of earlier because type checking fails otherwise.

Fixes zulip#11560.
This saves us from using a conditional import.

Signed-off-by: Zixuan James Li <p359101898@gmail.com>
This saves us from using a conditional import.

Signed-off-by: Zixuan James Li <p359101898@gmail.com>
@timabbott
Copy link
Sponsor Member

This looks good; did some small edits to the comments and I've tagged this to merge once CI finishes.

Huge thanks for doing this project! It's been a long slog and I think very meaningful for the codebase.

@timabbott timabbott enabled auto-merge (rebase) October 5, 2022 22:44
@timabbott timabbott merged commit 7fd8d77 into zulip:main Oct 5, 2022
@timabbott
Copy link
Sponsor Member

Can you post in #backend an announcement about this having been integrated? And do an edit pass on our mypy documentation to reference that we now do this, perhaps with examples/links that may be helpful.

@PIG208 PIG208 deleted the django-stubs branch October 6, 2022 02:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
integration review Added by maintainers when a PR may be ready for integration. size: XL
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants