Skip to content

Commit 49fb215

Browse files
Run mypy in Github continuous integration checks (#2324)
* run mypy in CI * more type hint fixing * Update requirements.txt * google.cloud import workaround python/mypy#10360 * Revert "google.cloud import workaround" This reverts commit e03dbc7. * mypy google.cloud workaround python/mypy#12985 * fix regular tests * remove --install-types flag * last mypy fix * type fixes for address_reasons * move email lists creation * update for newly merged changes * Fix regular tests * Changes suggested by @jrobbins * Catch invalid requests * small type hint * Change methods to properly override * revert to previous implementation * revert test * add back original validate request type method * remove unused import and rearrange methods * remove comment string casting; add test back * add ndb ignore * Update test_html_rendering.html * mypy fix * remove merge addition
1 parent 9a35f31 commit 49fb215

34 files changed

+149
-117
lines changed

.github/workflows/ci.yml

+3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ jobs:
4141
- name: lint
4242
run: npm run lint
4343

44+
- name: mypy
45+
run: mypy --ignore-missing-imports --exclude cs-env/ --exclude appengine_config.py .
46+
4447
- name: test
4548
run: npm test
4649

api/accounts_api.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import logging
1717

18-
from google.cloud import ndb
18+
from google.cloud import ndb # type: ignore
1919

2020
from framework import basehandlers
2121
from framework import permissions

api/comments_api.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# limitations under the License.
1515

1616
import logging
17-
from typing import Any, Optional
17+
from typing import Any
1818

1919
from framework import basehandlers
2020
from framework import permissions
@@ -24,7 +24,6 @@
2424

2525

2626
def comment_to_json_dict(comment: Activity) -> dict[str, Any]:
27-
2827
return {
2928
'comment_id': comment.key.id(),
3029
'feature_id': comment.feature_id,
@@ -56,8 +55,8 @@ def do_get(self, **kwargs) -> dict[str, list[dict[str, Any]]]:
5655
is_admin = permissions.can_admin_site(user)
5756

5857
# Filter deleted comments the user can't see.
59-
comments = filter(
60-
lambda c: self._should_show_comment(c, user.email(), is_admin), comments)
58+
comments = list(filter(
59+
lambda c: self._should_show_comment(c, user.email(), is_admin), comments))
6160

6261
dicts = [comment_to_json_dict(c) for c in comments]
6362
return {'comments': dicts}
@@ -110,7 +109,7 @@ def do_post(self, **kwargs) -> dict[str, str]:
110109

111110
def do_patch(self, **kwargs) -> dict[str, str]:
112111
comment_id = self.get_param('commentId', required=True)
113-
comment: Optional[Activity] = Activity.get_by_id(comment_id)
112+
comment: Activity = Activity.get_by_id(comment_id)
114113

115114
user = self.get_current_user(required=True)
116115
if not permissions.can_admin_site(user) and (

api/features_api.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
class FeaturesAPI(basehandlers.APIHandler):
2828
"""Features are the the main records that we track."""
2929

30-
def get_one_feature(self, feature_id: int) -> dict:
30+
def get_one_feature(self, feature_id: int) -> dict[str, Any]:
3131
features = core_models.Feature.get_by_ids([feature_id])
3232
if not features:
3333
self.abort(404, msg='Feature %r not found' % feature_id)
@@ -64,7 +64,7 @@ def do_search(self) -> dict[str, Any]:
6464
'features': features_on_page,
6565
}
6666

67-
def do_get(self, **kwargs) -> dict:
67+
def do_get(self, **kwargs) -> dict[str, Any]:
6868
"""Handle GET requests for a single feature or a search."""
6969
feature_id = kwargs.get('feature_id', None)
7070
if feature_id:

framework/basehandlers.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,14 @@
1818
import logging
1919
import os
2020
import re
21+
from typing import Optional
2122

2223
import flask
2324
import flask.views
2425
import werkzeug.exceptions
2526

2627
import google.appengine.api
27-
from google.cloud import ndb
28+
from google.cloud import ndb # type: ignore
2829

2930
import settings
3031
from framework import csp
@@ -268,8 +269,8 @@ def require_signed_in_and_xsrf_token(self):
268269

269270
class FlaskHandler(BaseHandler):
270271

271-
TEMPLATE_PATH = None # Subclasses should define this.
272-
HTTP_CACHE_TYPE = None # Subclasses can use 'public' or 'private'
272+
TEMPLATE_PATH: Optional[str] = None # Subclasses should define this.
273+
HTTP_CACHE_TYPE: Optional[str] = None # Subclasses can use 'public' or 'private'
273274
JSONIFY = False # Set to True for JSON feeds.
274275
IS_INTERNAL_HANDLER = False # Subclasses can skip XSRF check.
275276

@@ -362,7 +363,6 @@ def get(self, *args, **kwargs):
362363
location = self.request.url.replace('www.', '', 1)
363364
logging.info('Striping www and redirecting to %r', location)
364365
return self.redirect(location)
365-
366366
handler_data = self.get_template_data(*args, **kwargs)
367367
users.refresh_user_session()
368368

framework/basehandlers_test.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -528,19 +528,19 @@ def test_get_headers(self):
528528
actual)
529529

530530
def test_get_template_data__missing(self):
531-
"""Every subsclass should overide get_template_data()."""
531+
"""Every subclass should overide get_template_data()."""
532532
self.handler = basehandlers.FlaskHandler()
533533
with self.assertRaises(NotImplementedError):
534534
self.handler.get_template_data()
535535

536536
def test_get_template_path__missing(self):
537-
"""Subsclasses that don't define TEMPLATE_PATH trigger error."""
537+
"""Subclasses that don't define TEMPLATE_PATH trigger error."""
538538
self.handler = basehandlers.FlaskHandler()
539539
with self.assertRaises(ValueError):
540540
self.handler.get_template_path({})
541541

542542
def test_get_template_path__specified_in_class(self):
543-
"""Subsclasses can define TEMPLATE_PATH."""
543+
"""Subclasses can define TEMPLATE_PATH."""
544544
actual = self.handler.get_template_path({})
545545
self.assertEqual('test_template.html', actual)
546546

@@ -551,7 +551,7 @@ def test_get_template_path__specalized_by_template_data(self):
551551
self.assertEqual('special.html', actual)
552552

553553
def test_process_post_data__missing(self):
554-
"""Subsclasses that don't override process_post_data() give a 405."""
554+
"""Subclasses that don't override process_post_data() give a 405."""
555555
self.handler = basehandlers.FlaskHandler()
556556
with self.assertRaises(werkzeug.exceptions.MethodNotAllowed):
557557
self.handler.process_post_data()

framework/cloud_tasks_helpers.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
if not settings.UNIT_TEST_MODE:
2727
import grpc # See requirements.dev.txt.
2828
from google.api_core import retry
29-
from google.cloud import tasks
29+
from google.cloud import tasks # type: ignore
3030

3131

3232

framework/csp.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
import copy
1818
import logging
1919
import os
20-
import six
2120

2221
import flask
2322

@@ -101,7 +100,7 @@ def get_csp_header_key():
101100
def build_policy(policy):
102101
"""Builds the CSP policy string from the internal representation."""
103102
csp_directives = [
104-
k + ' ' + ' '.join(v) for k, v in six.iteritems(policy) if v is not None
103+
k + ' ' + ' '.join(v) for k, v in policy.items() if v is not None
105104
]
106105
if REPORT_URI:
107106
csp_directives.append('report-uri %s' % REPORT_URI)

framework/csp_test.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ def test_get_default_policy__strict(self):
5252
"""We can get the regular strict policy."""
5353
policy = csp.get_default_policy(nonce='12345')
5454
self.assertCountEqual(list(csp.DEFAULT_POLICY.keys()), list(policy.keys()))
55-
self.assertIn('strict-dynamic', policy['script-src'])
55+
self.assertIn('\'strict-dynamic\'', policy['script-src'])
5656
self.assertIn("'nonce-12345'", policy['script-src'])
5757

5858
@mock.patch('framework.csp.USE_NONCE_ONLY_POLICY', True)
59-
def test_get_default_policy__strict(self):
59+
def test_get_default_policy__strict_two(self):
6060
"""We can get the even stricter nonce-only policy."""
6161
policy = csp.get_default_policy(nonce='12345')
6262
self.assertCountEqual(list(csp.NONCE_ONLY_POLICY.keys()), list(policy.keys()))
@@ -71,7 +71,7 @@ def test_get_csp_header_key__enforced(self):
7171
csp.get_csp_header_key())
7272

7373
@mock.patch('framework.csp.REPORT_ONLY', True)
74-
def test_get_csp_header_key__enforced(self):
74+
def test_get_csp_header_key__enforced_two(self):
7575
"""We can get the header used when only reporting violations."""
7676
self.assertEqual(
7777
csp.HEADER_KEY_REPORT_ONLY,

framework/secrets.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import string
2121
import time
2222

23-
from google.cloud import ndb
23+
from google.cloud import ndb # type: ignore
2424

2525

2626
# For random key generation

internals/approval_defs.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
'main/third_party/blink/API_OWNERS?format=TEXT')
3636

3737
ApprovalFieldDef = collections.namedtuple(
38-
'ApprovalField',
38+
'ApprovalFieldDef',
3939
'name, description, field_id, rule, approvers')
4040

4141
# Note: This can be requested manually through the UI, but it is not
@@ -193,7 +193,8 @@ def set_vote(
193193
set_on=now, set_by=set_by_email)
194194
new_vote.put()
195195

196-
update_gate_approval_state(gate)
196+
if gate:
197+
update_gate_approval_state(gate)
197198

198199
def get_gate_by_type(feature_id: int, gate_type: int):
199200
"""Return a single gate based on the feature and gate type."""

internals/core_enums.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515

1616
import collections
17+
from typing import Optional
1718

1819

1920
WEBCOMPONENTS = 1
@@ -149,43 +150,43 @@
149150
GATE_SHIP = 4
150151

151152
# Prototype stage types for every feature type.
152-
STAGE_TYPES_PROTOTYPE = {
153+
STAGE_TYPES_PROTOTYPE: dict[int, Optional[int]] = {
153154
FEATURE_TYPE_INCUBATE_ID: STAGE_BLINK_PROTOTYPE,
154155
FEATURE_TYPE_EXISTING_ID: STAGE_FAST_PROTOTYPE,
155156
FEATURE_TYPE_CODE_CHANGE_ID: None,
156157
FEATURE_TYPE_DEPRECATION_ID: None
157158
}
158159
# Dev trial stage types for every feature type.
159-
STAGE_TYPES_DEV_TRIAL = {
160+
STAGE_TYPES_DEV_TRIAL: dict[int, Optional[int]] = {
160161
FEATURE_TYPE_INCUBATE_ID: STAGE_BLINK_DEV_TRIAL,
161162
FEATURE_TYPE_EXISTING_ID: STAGE_FAST_DEV_TRIAL,
162163
FEATURE_TYPE_CODE_CHANGE_ID: STAGE_PSA_DEV_TRIAL,
163164
FEATURE_TYPE_DEPRECATION_ID: STAGE_DEP_DEV_TRIAL
164165
}
165166
# Origin trial stage types for every feature type.
166-
STAGE_TYPES_ORIGIN_TRIAL = {
167+
STAGE_TYPES_ORIGIN_TRIAL: dict[int, Optional[int]] = {
167168
FEATURE_TYPE_INCUBATE_ID: STAGE_BLINK_ORIGIN_TRIAL,
168169
FEATURE_TYPE_EXISTING_ID: STAGE_FAST_ORIGIN_TRIAL,
169170
FEATURE_TYPE_CODE_CHANGE_ID: None,
170171
FEATURE_TYPE_DEPRECATION_ID: STAGE_DEP_DEPRECATION_TRIAL
171172
}
172173
# Origin trial extension stage types for every feature type.
173-
STAGE_TYPES_EXTEND_ORIGIN_TRIAL = {
174+
STAGE_TYPES_EXTEND_ORIGIN_TRIAL: dict[int, Optional[int]] = {
174175
FEATURE_TYPE_INCUBATE_ID: STAGE_BLINK_EXTEND_ORIGIN_TRIAL,
175176
FEATURE_TYPE_EXISTING_ID: STAGE_FAST_EXTEND_ORIGIN_TRIAL,
176177
FEATURE_TYPE_CODE_CHANGE_ID: None,
177178
FEATURE_TYPE_DEPRECATION_ID: STAGE_DEP_EXTEND_DEPRECATION_TRIAL
178179
}
179180
# Shipping stage types for every feature type.
180-
STAGE_TYPES_SHIPPING = {
181+
STAGE_TYPES_SHIPPING: dict[int, Optional[int]] = {
181182
FEATURE_TYPE_INCUBATE_ID: STAGE_BLINK_SHIPPING,
182183
FEATURE_TYPE_EXISTING_ID: STAGE_FAST_SHIPPING,
183184
FEATURE_TYPE_CODE_CHANGE_ID: STAGE_PSA_SHIPPING,
184185
FEATURE_TYPE_DEPRECATION_ID: STAGE_DEP_SHIPPING
185186
}
186187

187188
# Mapping of original field names to the new stage types the fields live on.
188-
STAGE_TYPES_BY_FIELD_MAPPING = {
189+
STAGE_TYPES_BY_FIELD_MAPPING: dict[str, dict[int, Optional[int]]] = {
189190
'finch_url': STAGE_TYPES_SHIPPING,
190191
'experiment_goals': STAGE_TYPES_ORIGIN_TRIAL,
191192
'experiment_risks': STAGE_TYPES_ORIGIN_TRIAL,

internals/core_models.py

+14-13
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import re
2323
from typing import Any, Optional
2424

25-
from google.cloud import ndb
25+
from google.cloud import ndb # type: ignore
2626

2727
from framework import rediscache
2828
from framework import users
@@ -205,7 +205,7 @@ def format_for_template(self, version=2) -> dict[str, Any]:
205205
self.migrate_views()
206206
logging.info('In format_for_template for %s',
207207
repr(self)[:settings.MAX_LOG_LINE])
208-
d = self.to_dict()
208+
d: dict[str, Any] = self.to_dict()
209209
is_released = self.impl_status_chrome in RELEASE_IMPL_STATES
210210
d['is_released'] = is_released
211211

@@ -494,7 +494,8 @@ def filter_unlisted(self, feature_list: list[dict]) -> list[dict]:
494494

495495
@classmethod
496496
def get_by_ids(
497-
self, feature_ids: list[int], update_cache: bool=False) -> list[dict]:
497+
self, feature_ids: list[int],
498+
update_cache: bool=False) -> list[dict[str, Any]]:
498499
"""Return a list of JSON dicts for the specified features.
499500
500501
Because the cache may rarely have stale data, this should only be
@@ -514,7 +515,7 @@ def get_by_ids(
514515
result_dict[feature_id] = feature
515516

516517
for future in futures:
517-
unformatted_feature = future.get_result()
518+
unformatted_feature: Optional[Feature] = future.get_result()
518519
if unformatted_feature and not unformatted_feature.deleted:
519520
feature = unformatted_feature.format_for_template()
520521
feature['updated_display'] = (
@@ -525,7 +526,7 @@ def get_by_ids(
525526
result_dict[unformatted_feature.key.integer_id()] = feature
526527

527528
result_list = [
528-
result_dict.get(feature_id) for feature_id in feature_ids
529+
result_dict[feature_id] for feature_id in feature_ids
529530
if feature_id in result_dict]
530531
return result_list
531532

@@ -632,7 +633,7 @@ def getSortingMilestone(feature):
632633

633634
@classmethod
634635
def get_in_milestone(self, milestone: int,
635-
show_unlisted: bool=False) -> dict[int, list[dict[str, Any]]]:
636+
show_unlisted: bool=False) -> dict[str, list[dict[str, Any]]]:
636637
"""Return {reason: [feaure_dict]} with all the reasons a feature can
637638
be part of a milestone.
638639
@@ -648,7 +649,7 @@ def get_in_milestone(self, milestone: int,
648649
if cached_features_by_type:
649650
features_by_type = cached_features_by_type
650651
else:
651-
all_features: dict[int, list[Feature]] = {}
652+
all_features: dict[str, list[Feature]] = {}
652653
all_features[IMPLEMENTATION_STATUS[ENABLED_BY_DEFAULT]] = []
653654
all_features[IMPLEMENTATION_STATUS[DEPRECATED]] = []
654655
all_features[IMPLEMENTATION_STATUS[REMOVED]] = []
@@ -782,10 +783,11 @@ def get_in_milestone(self, milestone: int,
782783

783784
def crbug_number(self) -> Optional[str]:
784785
if not self.bug_url:
785-
return
786+
return None
786787
m = re.search(r'[\/|?id=]([0-9]+)$', self.bug_url)
787788
if m:
788789
return m.group(1)
790+
return None
789791

790792
def new_crbug_url(self) -> str:
791793
url = 'https://bugs.chromium.org/p/chromium/issues/entry'
@@ -1132,7 +1134,7 @@ def __init__(self, *args, **kwargs):
11321134

11331135
@classmethod
11341136
def get_feature_entry(self, feature_id: int, update_cache: bool=False
1135-
) -> FeatureEntry:
1137+
) -> Optional[FeatureEntry]:
11361138
KEY = feature_cache_key(FeatureEntry.DEFAULT_CACHE_KEY, feature_id)
11371139
feature = rediscache.get(KEY)
11381140

@@ -1174,7 +1176,7 @@ def get_by_ids(self, entry_ids: list[int], update_cache: bool=False
11741176
procesing a POST to edit data. For editing use case, load the
11751177
data from NDB directly.
11761178
"""
1177-
result_dict = {}
1179+
result_dict: dict[int, int] = {}
11781180
futures = []
11791181

11801182
for fe_id in entry_ids:
@@ -1192,9 +1194,8 @@ def get_by_ids(self, entry_ids: list[int], update_cache: bool=False
11921194
rediscache.set(store_key, entry)
11931195
result_dict[entry.key.integer_id()] = entry
11941196

1195-
result_list = [
1196-
result_dict.get(fe_id) for fe_id in entry_ids
1197-
if fe_id in result_dict]
1197+
result_list = [result_dict[fe_id] for fe_id in entry_ids
1198+
if fe_id in result_dict]
11981199
return result_list
11991200

12001201
# Note: get_in_milestone will be in a new file legacy_queries.py.

internals/data_backup.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414
import logging
15+
from typing import Any
1516

1617
from googleapiclient.discovery import build
1718
from googleapiclient.discovery_cache.base import Cache
@@ -20,7 +21,7 @@
2021
from framework import basehandlers
2122

2223
class MemoryCache(Cache):
23-
_CACHE = {}
24+
_CACHE: dict[Any, Any] = {}
2425

2526
def get(self, url):
2627
return MemoryCache._CACHE.get(url)

0 commit comments

Comments
 (0)