python: Convert more variable type annotations to Python 3.6 style.

Signed-off-by: Anders Kaseorg <anders@zulip.com>
This commit is contained in:
Anders Kaseorg 2020-05-08 15:10:17 -07:00 committed by Tim Abbott
parent 708c6f4f11
commit 8cdf2801f7
17 changed files with 122 additions and 113 deletions

View File

@ -1024,7 +1024,7 @@ def ad_hoc_queries() -> List[Dict[str, str]]:
@require_server_admin @require_server_admin
@has_request_variables @has_request_variables
def get_activity(request: HttpRequest) -> HttpResponse: def get_activity(request: HttpRequest) -> HttpResponse:
duration_content, realm_minutes = user_activity_intervals() # type: Tuple[mark_safe, Dict[str, float]] duration_content, realm_minutes = user_activity_intervals()
counts_content: str = realm_summary_table(realm_minutes) counts_content: str = realm_summary_table(realm_minutes)
data = [ data = [
('Counts', counts_content), ('Counts', counts_content),

View File

@ -102,9 +102,7 @@ automatically in Zulip's automated test suite. The code there will
look something like this: look something like this:
``` python ``` python
def render_message(client): def render_message(client: Client) -> None:
# type: (Client) -> None
# {code_example|start} # {code_example|start}
# Render a message # Render a message
request = { request = {

View File

@ -3,12 +3,12 @@
[mypy](http://mypy-lang.org/) is a compile-time static type checker [mypy](http://mypy-lang.org/) is a compile-time static type checker
for Python, allowing optional, gradual typing of Python code. Zulip for Python, allowing optional, gradual typing of Python code. Zulip
was fully annotated with mypy's Python 2 syntax in 2016, before our was fully annotated with mypy's Python 2 syntax in 2016, before our
migration to Python 3 in late 2017. In 2018, we migrated essentially migration to Python 3 in late 2017. In 2018 and 2020, we migrated
the entire codebase to the nice PEP-484 (Python 3 only) syntax for essentially the entire codebase to the nice PEP 484 (Python 3 only)
static types: and PEP 526 (Python 3.6) syntax for static types:
``` ```
user_dict = {} # type: Dict[str, UserProfile] user_dict: Dict[str, UserProfile] = {}
def get_user(email: str, realm: Realm) -> UserProfile: def get_user(email: str, realm: Realm) -> UserProfile:
... # Actual code of the function here ... # Actual code of the function here

View File

@ -173,9 +173,9 @@ boolean field, `mandatory_topics`, to the Realm model in
class Realm(models.Model): class Realm(models.Model):
# ... # ...
emails_restricted_to_domains = models.BooleanField(default=True) # type: bool emails_restricted_to_domains: bool = models.BooleanField(default=True)
invite_required = models.BooleanField(default=False) # type: bool invite_required: bool = models.BooleanField(default=False)
+ mandatory_topics = models.BooleanField(default=False) # type: bool + mandatory_topics: bool = models.BooleanField(default=False)
``` ```
The Realm model also contains an attribute, `property_types`, which The Realm model also contains an attribute, `property_types`, which
@ -405,11 +405,14 @@ annotation).
# zerver/views/realm.py # zerver/views/realm.py
def update_realm(request, user_profile, name=REQ(validator=check_string, default=None), def update_realm(
# ..., request: HttpRequest,
+ mandatory_topics=REQ(validator=check_bool, default=None), user_profile: UserProfile,
# ...): name: Optional[str] = REQ(validator=check_string, default=None),
+ # type: (HttpRequest, UserProfile, ..., Optional[bool], ... # ...
+ mandatory_topics: Optional[bool] = REQ(validator=check_bool, default=None),
# ...
):
# ... # ...
``` ```

View File

@ -68,8 +68,7 @@ views, as an introduction to view decorators.
```py ```py
@require_post @require_post
def accounts_register(request): def accounts_register(request: HttpRequest) -> HttpResponse:
# type: (HttpRequest) -> HttpResponse
``` ```
This decorator ensures that the request was a POST--here, we're This decorator ensures that the request was a POST--here, we're
@ -91,8 +90,7 @@ specific to Zulip.
```py ```py
@zulip_login_required @zulip_login_required
def home(request): def home(request: HttpRequest) -> HttpResponse:
# type: (HttpRequest) -> HttpResponse
``` ```
[login-required-link]: https://docs.djangoproject.com/en/1.8/topics/auth/default/#django.contrib.auth.decorators.login_required [login-required-link]: https://docs.djangoproject.com/en/1.8/topics/auth/default/#django.contrib.auth.decorators.login_required
@ -261,29 +259,15 @@ For example, in [zerver/views/realm.py](https://github.com/zulip/zulip/blob/mast
```py ```py
@require_realm_admin @require_realm_admin
@has_request_variables @has_request_variables
def update_realm(request, user_profile, name=REQ(validator=check_string, default=None), ...)): def update_realm(
# type: (HttpRequest, UserProfile, ...) -> HttpResponse request: HttpRequest, user_profile: UserProfile,
name: Optional[str]=REQ(validator=check_string, default=None),
# ...
):
realm = user_profile.realm realm = user_profile.realm
data = {} # type: Dict[str, Any] # ...
if name is not None and realm.name != name: do_set_realm_property(realm, k, v)
do_set_realm_name(realm, name) # ...
data['name'] = 'updated'
```
and in [zerver/lib/actions.py](https://github.com/zulip/zulip/blob/master/zerver/lib/actions.py):
```py
def do_set_realm_name(realm, name):
# type: (Realm, str) -> None
realm.name = name
realm.save(update_fields=['name'])
event = dict(
type="realm",
op="update",
property='name',
value=name,
)
send_event(realm, event, active_user_ids(realm))
``` ```
`realm.save()` actually saves the changes to the realm to the `realm.save()` actually saves the changes to the realm to the

View File

@ -53,7 +53,7 @@ with open('/var/lib/nagios_state/last_postgres_backup', 'w') as f:
f.write(now.isoformat()) f.write(now.isoformat())
f.write("\n") f.write("\n")
backups = {} # type: Dict[datetime, str] backups: Dict[datetime, str] = {}
lines = run(['env-wal-e', 'backup-list']).split("\n") lines = run(['env-wal-e', 'backup-list']).split("\n")
for line in lines[1:]: for line in lines[1:]:
if line: if line:

View File

@ -392,7 +392,7 @@ refactor them.
from zulip_bots.test_lib import StubBotTestCase from zulip_bots.test_lib import StubBotTestCase
class TestHelpBot(StubBotTestCase): class TestHelpBot(StubBotTestCase):
bot_name = "helloworld" # type: str bot_name: str = "helloworld"
def test_bot(self) -> None: def test_bot(self) -> None:
dialog = [ dialog = [

View File

@ -417,26 +417,26 @@ python_rules = RuleList(
}, },
'description': 'Comment-style function type annotation. Use Python3 style annotations instead.', 'description': 'Comment-style function type annotation. Use Python3 style annotations instead.',
}, },
{'pattern': r' = models[.].*null=True.*\) # type: (?!Optional)', {'pattern': r': *(?!Optional)[^ ].*= models[.].*null=True',
'include_only': {"zerver/models.py"}, 'include_only': {"zerver/models.py"},
'description': 'Model variable with null=true not annotated as Optional.', 'description': 'Model variable with null=true not annotated as Optional.',
'good_lines': ['desc = models.TextField(null=True) # type: Optional[Text]', 'good_lines': ['desc: Optional[Text] = models.TextField(null=True)',
'stream = models.ForeignKey(Stream, null=True, on_delete=CASCADE) # type: Optional[Stream]', 'stream: Optional[Stream] = models.ForeignKey(Stream, null=True, on_delete=CASCADE)',
'desc = models.TextField() # type: Text', 'desc: Text = models.TextField()',
'stream = models.ForeignKey(Stream, on_delete=CASCADE) # type: Stream'], 'stream: Stream = models.ForeignKey(Stream, on_delete=CASCADE)'],
'bad_lines': ['desc = models.CharField(null=True) # type: Text', 'bad_lines': ['desc: Text = models.CharField(null=True)',
'stream = models.ForeignKey(Stream, null=True, on_delete=CASCADE) # type: Stream'], 'stream: Stream = models.ForeignKey(Stream, null=True, on_delete=CASCADE)'],
}, },
{'pattern': r' = models[.].* # type: Optional', # Optional tag {'pattern': r': *Optional.*= models[.].*\)',
'exclude_pattern': 'null=True', 'exclude_pattern': 'null=True',
'include_only': {"zerver/models.py"}, 'include_only': {"zerver/models.py"},
'description': 'Model variable annotated with Optional but variable does not have null=true.', 'description': 'Model variable annotated with Optional but variable does not have null=true.',
'good_lines': ['desc = models.TextField(null=True) # type: Optional[Text]', 'good_lines': ['desc: Optional[Text] = models.TextField(null=True)',
'stream = models.ForeignKey(Stream, null=True, on_delete=CASCADE) # type: Optional[Stream]', 'stream: Optional[Stream] = models.ForeignKey(Stream, null=True, on_delete=CASCADE)',
'desc = models.TextField() # type: Text', 'desc: Text = models.TextField()',
'stream = models.ForeignKey(Stream, on_delete=CASCADE) # type: Stream'], 'stream: Stream = models.ForeignKey(Stream, on_delete=CASCADE)'],
'bad_lines': ['desc = models.TextField() # type: Optional[Text]', 'bad_lines': ['desc: Optional[Text] = models.TextField()',
'stream = models.ForeignKey(Stream, on_delete=CASCADE) # type: Optional[Stream]'], 'stream: Optional[Stream] = models.ForeignKey(Stream, on_delete=CASCADE)'],
}, },
{'pattern': r'[\s([]Text([^\s\w]|$)', {'pattern': r'[\s([]Text([^\s\w]|$)',
'exclude': { 'exclude': {

View File

@ -30,9 +30,9 @@ INACTIVE_ENTRY = (
FILE_TEMPLATE = ( FILE_TEMPLATE = (
"from typing import Any, Dict\n\n" "from typing import Any, Dict\n\n"
"EMOJI_NAME_MAPS = {" "EMOJI_NAME_MAPS: Dict[str, Dict[str, Any]] = {"
"%(emoji_entries)s\n" "%(emoji_entries)s\n"
"} # type: Dict[str, Dict[str, Any]]\n" "}\n"
) )
emoji_names: Set[str] = set() emoji_names: Set[str] = set()

View File

@ -10,7 +10,7 @@ from typing import Dict, Iterable, List
def alert_words_in_realm(realm: Realm) -> Dict[int, List[str]]: def alert_words_in_realm(realm: Realm) -> Dict[int, List[str]]:
user_ids_and_words = AlertWord.objects.filter( user_ids_and_words = AlertWord.objects.filter(
realm=realm, user_profile__is_active=True).values("user_profile_id", "word") realm=realm, user_profile__is_active=True).values("user_profile_id", "word")
user_ids_with_words = dict() # type: Dict[int, List[str]] user_ids_with_words: Dict[int, List[str]] = dict()
for id_and_word in user_ids_and_words: for id_and_word in user_ids_and_words:
user_ids_with_words.setdefault(id_and_word["user_profile_id"], []) user_ids_with_words.setdefault(id_and_word["user_profile_id"], [])
user_ids_with_words[id_and_word["user_profile_id"]].append(id_and_word["word"]) user_ids_with_words[id_and_word["user_profile_id"]].append(id_and_word["word"])

View File

@ -68,7 +68,7 @@ class JsonableError(Exception):
data_fields = ['widget_name'] data_fields = ['widget_name']
def __init__(self, widget_name: str) -> None: def __init__(self, widget_name: str) -> None:
self.widget_name = widget_name # type: str self.widget_name: str = widget_name
@staticmethod @staticmethod
def msg_format() -> str: def msg_format() -> str:

View File

@ -33,7 +33,10 @@ def move_missed_message_addresses_to_database(apps: StateApps, schema_editor: Da
redis_client.delete(key) redis_client.delete(key)
continue continue
user_profile_id, recipient_id, subject_b = result # type: (bytes, bytes, bytes) user_profile_id: bytes
recipient_id: bytes
subject_id: bytes
user_profile_id, recipient_id, subject_b = result
topic_name = subject_b.decode('utf-8') topic_name = subject_b.decode('utf-8')
# The data model for missed-message emails has changed in two # The data model for missed-message emails has changed in two

View File

@ -28,7 +28,7 @@ def move_back_to_user_profile(apps: StateApps, schema_editor: DatabaseSchemaEdit
UserProfile = apps.get_model('zerver', 'UserProfile') UserProfile = apps.get_model('zerver', 'UserProfile')
user_ids_and_words = AlertWord.objects.all().values("user_profile_id", "word") user_ids_and_words = AlertWord.objects.all().values("user_profile_id", "word")
user_ids_with_words = dict() # type: Dict[int, List[str]] user_ids_with_words: Dict[int, List[str]] = dict()
for id_and_word in user_ids_and_words: for id_and_word in user_ids_and_words:
user_ids_with_words.setdefault(id_and_word["user_profile_id"], []) user_ids_with_words.setdefault(id_and_word["user_profile_id"], [])

View File

@ -2894,10 +2894,10 @@ class AlertWord(models.Model):
# never move to another realm, so it's static, and having Realm # never move to another realm, so it's static, and having Realm
# here optimizes the main query on this table, which is fetching # here optimizes the main query on this table, which is fetching
# all the alert words in a realm. # all the alert words in a realm.
realm = models.ForeignKey(Realm, db_index=True, on_delete=CASCADE) # type: Realm realm: Realm = models.ForeignKey(Realm, db_index=True, on_delete=CASCADE)
user_profile = models.ForeignKey(UserProfile, on_delete=CASCADE) # type: UserProfile user_profile: UserProfile = models.ForeignKey(UserProfile, on_delete=CASCADE)
# Case-insensitive name for the alert word. # Case-insensitive name for the alert word.
word = models.TextField() # type: str word: str = models.TextField()
class Meta: class Meta:
unique_together = ("user_profile", "word") unique_together = ("user_profile", "word")

View File

@ -2229,11 +2229,13 @@ class GoogleAuthBackendTest(SocialAuthBase):
def test_redirect_to_next_url_for_log_into_subdomain(self) -> None: def test_redirect_to_next_url_for_log_into_subdomain(self) -> None:
def test_redirect_to_next_url(next: str='') -> HttpResponse: def test_redirect_to_next_url(next: str='') -> HttpResponse:
data = {'full_name': 'Hamlet', data: ExternalAuthDataDict = {
'full_name': 'Hamlet',
'email': self.example_email("hamlet"), 'email': self.example_email("hamlet"),
'subdomain': 'zulip', 'subdomain': 'zulip',
'is_signup': False, 'is_signup': False,
'redirect_to': next} # type: ExternalAuthDataDict 'redirect_to': next,
}
user_profile = self.example_user('hamlet') user_profile = self.example_user('hamlet')
with mock.patch( with mock.patch(
'zerver.views.auth.authenticate', 'zerver.views.auth.authenticate',
@ -2254,22 +2256,26 @@ class GoogleAuthBackendTest(SocialAuthBase):
self.assertEqual(res.url, 'http://zulip.testserver/#narrow/stream/7-test-here') self.assertEqual(res.url, 'http://zulip.testserver/#narrow/stream/7-test-here')
def test_log_into_subdomain_when_token_is_malformed(self) -> None: def test_log_into_subdomain_when_token_is_malformed(self) -> None:
data = {'full_name': 'Full Name', data: ExternalAuthDataDict = {
'full_name': 'Full Name',
'email': self.example_email("hamlet"), 'email': self.example_email("hamlet"),
'subdomain': 'zulip', 'subdomain': 'zulip',
'is_signup': False, 'is_signup': False,
'redirect_to': ''} # type: ExternalAuthDataDict 'redirect_to': '',
}
with mock.patch("logging.warning") as mock_warn: with mock.patch("logging.warning") as mock_warn:
result = self.get_log_into_subdomain(data, force_token='nonsense') result = self.get_log_into_subdomain(data, force_token='nonsense')
mock_warn.assert_called_once_with("log_into_subdomain: Malformed token given: %s", "nonsense") mock_warn.assert_called_once_with("log_into_subdomain: Malformed token given: %s", "nonsense")
self.assertEqual(result.status_code, 400) self.assertEqual(result.status_code, 400)
def test_log_into_subdomain_when_token_not_found(self) -> None: def test_log_into_subdomain_when_token_not_found(self) -> None:
data = {'full_name': 'Full Name', data: ExternalAuthDataDict = {
'full_name': 'Full Name',
'email': self.example_email("hamlet"), 'email': self.example_email("hamlet"),
'subdomain': 'zulip', 'subdomain': 'zulip',
'is_signup': False, 'is_signup': False,
'redirect_to': ''} # type: ExternalAuthDataDict 'redirect_to': '',
}
with mock.patch("logging.warning") as mock_warn: with mock.patch("logging.warning") as mock_warn:
token = generate_random_token(ExternalAuthResult.LOGIN_TOKEN_LENGTH) token = generate_random_token(ExternalAuthResult.LOGIN_TOKEN_LENGTH)
result = self.get_log_into_subdomain(data, force_token=token) result = self.get_log_into_subdomain(data, force_token=token)
@ -2283,11 +2289,13 @@ class GoogleAuthBackendTest(SocialAuthBase):
existing_user.email = 'whatever@zulip.com' existing_user.email = 'whatever@zulip.com'
existing_user.save() existing_user.save()
data = {'full_name': 'Full Name', data: ExternalAuthDataDict = {
'full_name': 'Full Name',
'email': 'existing@zulip.com', 'email': 'existing@zulip.com',
'subdomain': 'zulip', 'subdomain': 'zulip',
'is_signup': True, 'is_signup': True,
'redirect_to': ''} # type: ExternalAuthDataDict 'redirect_to': '',
}
result = self.get_log_into_subdomain(data) result = self.get_log_into_subdomain(data)
# Should simply get logged into the existing account: # Should simply get logged into the existing account:
@ -2295,11 +2303,13 @@ class GoogleAuthBackendTest(SocialAuthBase):
self.assert_logged_in_user_id(existing_user.id) self.assert_logged_in_user_id(existing_user.id)
def test_log_into_subdomain_when_is_signup_is_true_and_new_user(self) -> None: def test_log_into_subdomain_when_is_signup_is_true_and_new_user(self) -> None:
data = {'full_name': 'New User Name', data: ExternalAuthDataDict = {
'full_name': 'New User Name',
'email': 'new@zulip.com', 'email': 'new@zulip.com',
'subdomain': 'zulip', 'subdomain': 'zulip',
'is_signup': True, 'is_signup': True,
'redirect_to': ''} # type: ExternalAuthDataDict 'redirect_to': '',
}
result = self.get_log_into_subdomain(data) result = self.get_log_into_subdomain(data)
self.assertEqual(result.status_code, 302) self.assertEqual(result.status_code, 302)
confirmation = Confirmation.objects.all().first() confirmation = Confirmation.objects.all().first()
@ -2318,11 +2328,13 @@ class GoogleAuthBackendTest(SocialAuthBase):
self.assert_in_success_response(['id_full_name'], result) self.assert_in_success_response(['id_full_name'], result)
def test_log_into_subdomain_when_is_signup_is_false_and_new_user(self) -> None: def test_log_into_subdomain_when_is_signup_is_false_and_new_user(self) -> None:
data = {'full_name': 'New User Name', data: ExternalAuthDataDict = {
'full_name': 'New User Name',
'email': 'new@zulip.com', 'email': 'new@zulip.com',
'subdomain': 'zulip', 'subdomain': 'zulip',
'is_signup': False, 'is_signup': False,
'redirect_to': ''} # type: ExternalAuthDataDict 'redirect_to': '',
}
result = self.get_log_into_subdomain(data) result = self.get_log_into_subdomain(data)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
self.assert_in_response('No account found for', result) self.assert_in_response('No account found for', result)
@ -2346,11 +2358,13 @@ class GoogleAuthBackendTest(SocialAuthBase):
self.assert_in_success_response(['id_full_name'], result) self.assert_in_success_response(['id_full_name'], result)
def test_log_into_subdomain_when_using_invite_link(self) -> None: def test_log_into_subdomain_when_using_invite_link(self) -> None:
data = {'full_name': 'New User Name', data: ExternalAuthDataDict = {
'full_name': 'New User Name',
'email': 'new@zulip.com', 'email': 'new@zulip.com',
'subdomain': 'zulip', 'subdomain': 'zulip',
'is_signup': True, 'is_signup': True,
'redirect_to': ''} # type: ExternalAuthDataDict 'redirect_to': '',
}
realm = get_realm("zulip") realm = get_realm("zulip")
realm.invite_required = True realm.invite_required = True
@ -2406,11 +2420,13 @@ class GoogleAuthBackendTest(SocialAuthBase):
self.assertEqual(sorted(new_streams), stream_names) self.assertEqual(sorted(new_streams), stream_names)
def test_log_into_subdomain_when_email_is_none(self) -> None: def test_log_into_subdomain_when_email_is_none(self) -> None:
data = {'full_name': None, data: ExternalAuthDataDict = {
'full_name': None,
'email': None, 'email': None,
'subdomain': 'zulip', 'subdomain': 'zulip',
'is_signup': False, 'is_signup': False,
'redirect_to': ''} # type: ExternalAuthDataDict 'redirect_to': '',
}
with mock.patch('logging.warning') as mock_warn: with mock.patch('logging.warning') as mock_warn:
result = self.get_log_into_subdomain(data) result = self.get_log_into_subdomain(data)
@ -2418,9 +2434,11 @@ class GoogleAuthBackendTest(SocialAuthBase):
mock_warn.assert_called_once() mock_warn.assert_called_once()
def test_user_cannot_log_into_wrong_subdomain(self) -> None: def test_user_cannot_log_into_wrong_subdomain(self) -> None:
data = {'full_name': 'Full Name', data: ExternalAuthDataDict = {
'full_name': 'Full Name',
'email': self.example_email("hamlet"), 'email': self.example_email("hamlet"),
'subdomain': 'zephyr'} # type: ExternalAuthDataDict 'subdomain': 'zephyr',
}
result = self.get_log_into_subdomain(data) result = self.get_log_into_subdomain(data)
self.assert_json_error(result, "Invalid subdomain") self.assert_json_error(result, "Invalid subdomain")

View File

@ -677,7 +677,8 @@ class SlackImporter(ZulipTestCase):
realm: Dict[str, Any] = {'zerver_subscription': []} realm: Dict[str, Any] = {'zerver_subscription': []}
user_list: List[Dict[str, Any]] = [] user_list: List[Dict[str, Any]] = []
reactions = [{"name": "grinning", "users": ["U061A5N1G"], "count": 1}] reactions = [{"name": "grinning", "users": ["U061A5N1G"], "count": 1}]
attachments = uploads = [] # type: List[Dict[str, Any]] attachments: List[Dict[str, Any]] = []
uploads: List[Dict[str, Any]] = []
zerver_usermessage = [{'id': 3}, {'id': 5}, {'id': 6}, {'id': 9}] zerver_usermessage = [{'id': 3}, {'id': 5}, {'id': 6}, {'id': 9}]

View File

@ -47,7 +47,9 @@ def _transform_commits_list_to_common_format(commits: List[Dict[str, Any]]) -> L
def beanstalk_decoder(view_func: ViewFuncT) -> ViewFuncT: def beanstalk_decoder(view_func: ViewFuncT) -> ViewFuncT:
@wraps(view_func) @wraps(view_func)
def _wrapped_view_func(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse: def _wrapped_view_func(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
auth_type, encoded_value = request.META['HTTP_AUTHORIZATION'].split() # type: str, str auth_type: str
encoded_value: str
auth_type, encoded_value = request.META['HTTP_AUTHORIZATION'].split()
if auth_type.lower() == "basic": if auth_type.lower() == "basic":
email, api_key = base64.b64decode(encoded_value).decode('utf-8').split(":") email, api_key = base64.b64decode(encoded_value).decode('utf-8').split(":")
email = email.replace('%40', '@') email = email.replace('%40', '@')