From b6f3c8e5f587a17ff7e322a7e1d0fc0a637ed041 Mon Sep 17 00:00:00 2001 From: eviljeff Date: Fri, 4 Sep 2026 15:52:55 +0100 Subject: [PATCH] Apply auto-fixes to UPxxx formatting rules; update ignore list --- conftest.py | 4 +-- pyproject.toml | 9 +++--- settings_test.py | 1 - src/olympia/accounts/serializers.py | 2 +- src/olympia/accounts/tests/test_utils.py | 20 ++++--------- src/olympia/accounts/tests/test_views.py | 14 ++++----- src/olympia/accounts/verify.py | 11 ++----- src/olympia/accounts/views.py | 8 ++--- src/olympia/activity/admin.py | 2 +- src/olympia/activity/tests/test_utils.py | 20 +++++-------- src/olympia/activity/utils.py | 19 +++--------- src/olympia/addons/admin.py | 4 +-- src/olympia/addons/tests/test_tasks.py | 2 +- src/olympia/amo/celery.py | 20 ++++--------- src/olympia/amo/cron.py | 4 +-- src/olympia/amo/templatetags/jinja_helpers.py | 5 +--- src/olympia/amo/tests/__init__.py | 6 ++-- src/olympia/amo/tests/test_commands.py | 6 ++-- src/olympia/amo/tests/test_views.py | 5 +--- src/olympia/api/authentication.py | 8 ++--- src/olympia/api/jwt_auth.py | 8 ++--- src/olympia/api/models.py | 9 +++--- src/olympia/api/tests/test_authentication.py | 4 +-- src/olympia/api/urls.py | 2 +- src/olympia/blocklist/mlbf.py | 30 +++++++++---------- src/olympia/blocklist/tasks.py | 3 +- src/olympia/blocklist/tests/test_cron.py | 3 +- src/olympia/constants/categories.py | 5 +--- src/olympia/core/utils.py | 8 ++--- src/olympia/devhub/tasks.py | 23 ++++---------- src/olympia/devhub/utils.py | 16 +++------- .../commands/extract_content_strings.py | 5 ++-- .../commands/extract_host_permissions.py | 1 - src/olympia/files/tests/test_commands.py | 3 +- src/olympia/files/tests/test_models.py | 4 +-- src/olympia/landfill/serializers.py | 4 +-- src/olympia/lib/crypto/signing.py | 10 +++---- src/olympia/lib/crypto/tasks.py | 4 +-- src/olympia/lib/crypto/tests/test_signing.py | 2 +- src/olympia/reviewers/admin.py | 7 +++-- src/olympia/reviewers/models.py | 6 +--- .../reviewers/tests/test_assay_tags.py | 6 ++-- src/olympia/reviewers/tests/test_views.py | 7 ++--- src/olympia/scanners/admin.py | 18 +++++------ .../scanners/tests/test_templatetags.py | 4 +-- .../search/tests/test_search_ranking.py | 14 ++++----- src/olympia/signing/views.py | 15 +++------- src/olympia/translations/tests/test_models.py | 5 ++-- src/olympia/users/models.py | 6 +--- .../users/templatetags/jinja_helpers.py | 7 ++--- src/olympia/users/tests/test_helpers.py | 14 ++++----- src/olympia/users/tests/test_models.py | 2 +- src/olympia/users/tests/test_tasks.py | 2 +- src/olympia/versions/models.py | 6 ++-- src/olympia/versions/tests/test_tasks.py | 5 +--- src/olympia/versions/utils.py | 5 +--- src/olympia/versions/views.py | 6 ++-- src/olympia/zadmin/admin.py | 10 ++++--- 58 files changed, 159 insertions(+), 300 deletions(-) diff --git a/conftest.py b/conftest.py index d1c9f47b7290..18193490f6b6 100644 --- a/conftest.py +++ b/conftest.py @@ -80,7 +80,7 @@ def mock_basket(settings): ) responses.add( responses.POST, - settings.BASKET_URL + '/news/unsubscribe/{}/'.format(USER_TOKEN), + settings.BASKET_URL + f'/news/unsubscribe/{USER_TOKEN}/', json={'status': 'ok', 'token': USER_TOKEN}, ) @@ -165,7 +165,7 @@ def test_pre_setup(request, tmpdir, settings): # Randomize the cache key prefix to keep # tests isolated from each other. prefix = uuid.uuid4().hex - settings.CACHES['default']['KEY_PREFIX'] = 'amo:{0}:'.format(prefix) + settings.CACHES['default']['KEY_PREFIX'] = f'amo:{prefix}:' # Reset global django-waffle cache instance to make sure it's properly # using our new key prefix diff --git a/pyproject.toml b/pyproject.toml index ed9a77ffa201..f2325366ad12 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,12 +38,12 @@ line-length = 88 [tool.ruff.lint] ignore = [ + "DTZ", # flake8-datetimez (except DTZ003) - we only run in UTC so unnecessary + "ISC004", # implicit-string-concatenation-in-collection-literal - too many false positives # The following rules fail currently, and should probably eventually be addressed - "DTZ", # flake8-datetimez (except DTZ003) - "UP", # pyupgrade - # They are in most-error-occuring order + # They are in most-error-occuring order, descending + "UP031", # printf-string-formatting "RUF012", # mutable-class-default - "ISC004", # implicit-string-concatenation-in-collection-literal "SIM115", # open-file-with-context-handler "BLE001", # blind-except "RUF059", # unused-unpacked-variable @@ -54,6 +54,7 @@ ignore = [ "SIM118", # in-dict-keys "TRY201", # verbose-raise "INT003", # printf-in-get-text-func-call + "UP030", # format-literals "INT002", # format-in-get-text-func-call "INT001", # f-string-in-get-text-func-cal ] diff --git a/settings_test.py b/settings_test.py index d6b0f2cc0a5f..284a5b56bc12 100644 --- a/settings_test.py +++ b/settings_test.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # ruff: noqa: F405 from settings import * # noqa diff --git a/src/olympia/accounts/serializers.py b/src/olympia/accounts/serializers.py index 48602a7a07b9..52c03cb436d5 100644 --- a/src/olympia/accounts/serializers.py +++ b/src/olympia/accounts/serializers.py @@ -255,7 +255,7 @@ def validate_group(self, group): if count != 1: log.info( 'Super creation: looking for group with ' - 'permissions {} {} (count: {})'.format(group, rule, count) + f'permissions {group} {rule} (count: {count})' ) raise serializers.ValidationError( 'Could not find a permissions group with the exact rules needed.' diff --git a/src/olympia/accounts/tests/test_utils.py b/src/olympia/accounts/tests/test_utils.py index 797ee9349877..8991542b0cd0 100644 --- a/src/olympia/accounts/tests/test_utils.py +++ b/src/olympia/accounts/tests/test_utils.py @@ -30,9 +30,7 @@ def test_fxa_login_url_without_requiring_two_factor_auth(): ) url = urlparse(raw_url) - base = '{scheme}://{netloc}{path}'.format( - scheme=url.scheme, netloc=url.netloc, path=url.path - ) + base = f'{url.scheme}://{url.netloc}{url.path}' assert base == 'https://accounts.firefox.com/oauth/authorization' query = parse_qs(url.query) next_path = urlsafe_b64encode(path.encode('utf-8')).rstrip(b'=') @@ -58,9 +56,7 @@ def test_fxa_login_url_requiring_two_factor_auth(): ) url = urlparse(raw_url) - base = '{scheme}://{netloc}{path}'.format( - scheme=url.scheme, netloc=url.netloc, path=url.path - ) + base = f'{url.scheme}://{url.netloc}{url.path}' assert base == 'https://accounts.firefox.com/oauth/authorization' query = parse_qs(url.query) next_path = urlsafe_b64encode(path.encode('utf-8')).rstrip(b'=') @@ -89,9 +85,7 @@ def test_fxa_login_url_requiring_two_factor_auth_passing_token(): ) url = urlparse(raw_url) - base = '{scheme}://{netloc}{path}'.format( - scheme=url.scheme, netloc=url.netloc, path=url.path - ) + base = f'{url.scheme}://{url.netloc}{url.path}' assert base == 'https://accounts.firefox.com/oauth/authorization' query = parse_qs(url.query) next_path = urlsafe_b64encode(path.encode('utf-8')).rstrip(b'=') @@ -121,9 +115,7 @@ def test_fxa_login_url_requiring_two_factor_auth_passing_request(): ) url = urlparse(raw_url) - base = '{scheme}://{netloc}{path}'.format( - scheme=url.scheme, netloc=url.netloc, path=url.path - ) + base = f'{url.scheme}://{url.netloc}{url.path}' assert base == 'https://accounts.firefox.com/oauth/authorization' query = parse_qs(url.query) next_path = urlsafe_b64encode(path.encode('utf-8')).rstrip(b'=') @@ -153,9 +145,7 @@ def test_fxa_login_url_requiring_two_factor_auth_passing_login_hint(): ) url = urlparse(raw_url) - base = '{scheme}://{netloc}{path}'.format( - scheme=url.scheme, netloc=url.netloc, path=url.path - ) + base = f'{url.scheme}://{url.netloc}{url.path}' assert base == 'https://accounts.firefox.com/oauth/authorization' query = parse_qs(url.query) next_path = urlsafe_b64encode(path.encode('utf-8')).rstrip(b'=') diff --git a/src/olympia/accounts/tests/test_views.py b/src/olympia/accounts/tests/test_views.py index b571bd84595b..5c2b765a6c71 100644 --- a/src/olympia/accounts/tests/test_views.py +++ b/src/olympia/accounts/tests/test_views.py @@ -95,9 +95,7 @@ def test_redirect_url_is_correct(self): == 'max-age=0, no-cache, no-store, must-revalidate, private' ) url = urlparse(response['location']) - redirect = '{scheme}://{netloc}{path}'.format( - scheme=url.scheme, netloc=url.netloc, path=url.path - ) + redirect = f'{url.scheme}://{url.netloc}{url.path}' assert redirect == 'https://accounts.firefox.com/v1/authorization' assert parse_qs(url.query) == { 'access_type': ['offline'], @@ -606,9 +604,7 @@ def _test_should_redirect_for_two_factor_auth(self): # as prompt=none to avoid the need for the user to re-authenticate. assert response.status_code == 302 url = urlparse(response['Location']) - base = '{scheme}://{netloc}{path}'.format( - scheme=url.scheme, netloc=url.netloc, path=url.path - ) + base = f'{url.scheme}://{url.netloc}{url.path}' fxa_config = settings.FXA_CONFIG[settings.DEFAULT_FXA_CONFIG_NAME] assert base == '{host}{path}'.format( host=settings.FXA_OAUTH_HOST, path='/authorization' @@ -2416,9 +2412,9 @@ def test_basket_integration(self): 'sync': 'Y', 'optin': 'Y', 'source_url': ( - 'http://testserver/api/{api_version}/accounts/account/' - '{id}/notifications/' - ).format(id=self.user.id, api_version=api_settings.DEFAULT_VERSION), + f'http://testserver/api/{api_settings.DEFAULT_VERSION}/accounts/account/' + f'{self.user.id}/notifications/' + ), 'email': self.user.email, }, headers={'x-api-key': 'testkey'}, diff --git a/src/olympia/accounts/verify.py b/src/olympia/accounts/verify.py index 560525f70fb3..b53877341737 100644 --- a/src/olympia/accounts/verify.py +++ b/src/olympia/accounts/verify.py @@ -74,11 +74,8 @@ def get_fxa_token(*, code=None, refresh_token=None, config=None): raise IdentificationError(f'No access token returned for {log_identifier}') else: log.info( - 'Token returned non-200 status {status} {body} [{code_or_token}]'.format( - code_or_token=log_identifier, - status=response.status_code, - body=response.content, - ) + f'Token returned non-200 status {response.status_code} {response.content} ' + f'[{log_identifier}]' ) raise IdentificationError(f'Could not get access token for {log_identifier}') @@ -102,9 +99,7 @@ def get_fxa_profile(token): raise IdentificationError(f'Profile incomplete for {token}') else: log.info( - 'Profile returned non-200 status {status} {body}'.format( - status=response.status_code, body=response.content - ) + f'Profile returned non-200 status {response.status_code} {response.content}' ) raise IdentificationError(f'Could not find profile for {token}') diff --git a/src/olympia/accounts/views.py b/src/olympia/accounts/views.py index 6d50378c9402..60ccd2887bab 100644 --- a/src/olympia/accounts/views.py +++ b/src/olympia/accounts/views.py @@ -630,10 +630,10 @@ def post(self, request): request.session.save() log.info( - 'API user {api_user} created and logged in a user from ' - 'the super-create API: user_id: {user.pk}; ' - 'user_name: {user.username}; fxa_id: {user.fxa_id}; ' - 'group: {group}'.format(user=user, api_user=request.user, group=group) + f'API user {request.user} created and logged in a user from ' + f'the super-create API: user_id: {user.pk}; ' + f'user_name: {user.username}; fxa_id: {user.fxa_id}; ' + f'group: {group}' ) cookie = { diff --git a/src/olympia/activity/admin.py b/src/olympia/activity/admin.py index df3a306fb0b0..3661e44e4cc7 100644 --- a/src/olympia/activity/admin.py +++ b/src/olympia/activity/admin.py @@ -158,7 +158,7 @@ def has_add_permission(self, request): return False def get_form(self, request, obj=None, **kwargs): - form = super(ReviewActionReasonLogAdmin, self).get_form(request, obj, **kwargs) + form = super().get_form(request, obj, **kwargs) form.base_fields['reason'].widget.can_add_related = False form.base_fields['reason'].widget.can_change_related = False form.base_fields['reason'].empty_label = None diff --git a/src/olympia/activity/tests/test_utils.py b/src/olympia/activity/tests/test_utils.py index f9d1e350cff8..d967b852323f 100644 --- a/src/olympia/activity/tests/test_utils.py +++ b/src/olympia/activity/tests/test_utils.py @@ -342,10 +342,7 @@ def _check_email( ): subject = call[0][0] body = call[0][1] - assert subject == 'Mozilla Add-ons: {} {}'.format( - self.addon.name, - self.version.version, - ) + assert subject == f'Mozilla Add-ons: {self.addon.name} {self.version.version}' assert url in body assert ('receiving this email because %s' % reason_text) in body assert 'If we do not hear from you within' not in body @@ -684,16 +681,13 @@ def test_send_activity_mail(): assert mail.outbox[0].body == message assert mail.outbox[0].subject == subject uuid = latest_version.token.get(user=user).uuid.hex - reference_header = '<{addon}/{version}@{site}>'.format( - addon=latest_version.addon.id, - version=latest_version.id, - site=settings.INBOUND_EMAIL_DOMAIN, + reference_header = ( + f'<{latest_version.addon.id}/' + f'{latest_version.id}@{settings.INBOUND_EMAIL_DOMAIN}>' ) - message_id = '<{addon}/{version}/{action}@{site}>'.format( - addon=latest_version.addon.id, - version=latest_version.id, - action=action.id, - site=settings.INBOUND_EMAIL_DOMAIN, + message_id = ( + f'<{latest_version.addon.id}/' + f'{latest_version.id}/{action.id}@{settings.INBOUND_EMAIL_DOMAIN}>' ) assert mail.outbox[0].extra_headers['In-Reply-To'] == reference_header diff --git a/src/olympia/activity/utils.py b/src/olympia/activity/utils.py index 7d60f5f5bba5..cc6443619ae8 100644 --- a/src/olympia/activity/utils.py +++ b/src/olympia/activity/utils.py @@ -292,10 +292,7 @@ def notify_about_activity_log( # Not being localised because we don't know the recipients locale. with translation.override('en-US'): - subject = reviewer_subject = 'Mozilla Add-ons: {} {}'.format( - addon.name, - version.version, - ) + subject = reviewer_subject = f'Mozilla Add-ons: {addon.name} {version.version}' # Build and send the mail for authors. template = template_from_user(note.user, version) from_email = formataddr((sender_name, settings.ADDONS_EMAIL)) @@ -353,12 +350,8 @@ def send_activity_mail( subject, message, version, recipients, from_email, unique_id, perm_setting=None ): thread_id = f'{version.addon.id}/{version.id}' - reference_header = '<{thread}@{site}>'.format( - thread=thread_id, site=settings.INBOUND_EMAIL_DOMAIN - ) - message_id = '<{thread}/{message}@{site}>'.format( - thread=thread_id, message=unique_id, site=settings.INBOUND_EMAIL_DOMAIN - ) + reference_header = f'<{thread_id}@{settings.INBOUND_EMAIL_DOMAIN}>' + message_id = f'<{thread_id}/{unique_id}@{settings.INBOUND_EMAIL_DOMAIN}>' headers = { 'In-Reply-To': reference_header, 'References': reference_header, @@ -373,11 +366,7 @@ def send_activity_mail( token.update(use_count=0) else: log.info(f'Created token with UUID {token.uuid} for user: {recipient.id}.') - reply_to = '{}{}@{}'.format( - REPLY_TO_PREFIX, - token.uuid.hex, - settings.INBOUND_EMAIL_DOMAIN, - ) + reply_to = f'{REPLY_TO_PREFIX}{token.uuid.hex}@{settings.INBOUND_EMAIL_DOMAIN}' log.info( 'Sending activity email to %s for %s version %s' % (recipient, version.addon.pk, version.pk) diff --git a/src/olympia/addons/admin.py b/src/olympia/addons/admin.py index 00a16062f8bf..6433230a84bb 100644 --- a/src/olympia/addons/admin.py +++ b/src/olympia/addons/admin.py @@ -569,9 +569,7 @@ def discovery_addon(self, obj): from olympia.discovery.admin import DiscoveryAddon url = reverse( - 'admin:{}_{}_change'.format( - DiscoveryAddon._meta.app_label, DiscoveryAddon._meta.model_name - ), + f'admin:{DiscoveryAddon._meta.app_label}_{DiscoveryAddon._meta.model_name}_change', args=[obj.pk], ) return format_html('Discovery Addon', url) diff --git a/src/olympia/addons/tests/test_tasks.py b/src/olympia/addons/tests/test_tasks.py index e7143e7b0163..fe382d774df6 100644 --- a/src/olympia/addons/tests/test_tasks.py +++ b/src/olympia/addons/tests/test_tasks.py @@ -690,7 +690,7 @@ def setUp(self): self.addCleanup(patcher2.stop) self.download_file_contents_from_backup_storage_mock = patcher2.start() self.download_file_contents_from_backup_storage_mock.side_effect = lambda nme: ( - f'Content for {nme}'.encode('utf-8') + f'Content for {nme}'.encode() ) patcher3 = mock.patch('olympia.addons.tasks.backup_storage_enabled') self.addCleanup(patcher3.stop) diff --git a/src/olympia/amo/celery.py b/src/olympia/amo/celery.py index a0a9d20c261e..a98b856fecf9 100644 --- a/src/olympia/amo/celery.py +++ b/src/olympia/amo/celery.py @@ -123,9 +123,8 @@ def process_failure_signal( def start_task_timer(task_id, task, **kw): timer = TaskTimer() log.info( - 'starting task timer; id={id}; name={name}; current_dt={current_dt}'.format( - id=task_id, name=task.name, current_dt=timer.current_datetime - ) + f'starting task timer; id={task_id}; name={task.name}; ' + f'current_dt={timer.current_datetime}' ) # Cache start time for one hour. This will allow us to catch crazy long @@ -141,21 +140,14 @@ def track_task_run_time(task_id, task, **kw): start_time = cache.get(timer.cache_key(task_id)) if start_time is None: log.info( - 'could not track task run time; id={id}; name={name}; ' - 'current_dt={current_dt}'.format( - id=task_id, name=task.name, current_dt=timer.current_datetime - ) + f'could not track task run time; id={task_id}; name={task.name}; ' + f'current_dt={timer.current_datetime}' ) else: run_time = timer.current_epoch_ms - start_time log.info( - 'tracking task run time; id={id}; name={name}; ' - 'run_time={run_time}; current_dt={current_dt}'.format( - id=task_id, - name=task.name, - current_dt=timer.current_datetime, - run_time=run_time, - ) + f'tracking task run time; id={task_id}; name={task.name}; ' + f'run_time={run_time}; current_dt={timer.current_datetime}' ) statsd.timing(f'tasks.{task.name}', run_time) cache.delete(timer.cache_key(task_id)) diff --git a/src/olympia/amo/cron.py b/src/olympia/amo/cron.py index 428bbbd5b681..20e8dc6fcac2 100644 --- a/src/olympia/amo/cron.py +++ b/src/olympia/amo/cron.py @@ -54,9 +54,7 @@ def days_ago(days): stale_uploads = FileUpload.objects.filter(created__lte=two_weeks_ago).order_by('id') for file_upload in stale_uploads: log.info( - '[FileUpload:{uuid}] Removing file: {path}'.format( - uuid=file_upload.uuid, path=file_upload.file_path - ) + f'[FileUpload:{file_upload.uuid}] Removing file: {file_upload.file_path}' ) if file_upload.file_path: try: diff --git a/src/olympia/amo/templatetags/jinja_helpers.py b/src/olympia/amo/templatetags/jinja_helpers.py index b04e12372b29..cbc852de2ed8 100644 --- a/src/olympia/amo/templatetags/jinja_helpers.py +++ b/src/olympia/amo/templatetags/jinja_helpers.py @@ -65,10 +65,7 @@ def url(viewname, *args, **kwargs): """Helper for Django's ``reverse`` in templates.""" add_prefix = kwargs.pop('add_prefix', True) host = kwargs.pop('host', '') - url = '{}{}'.format( - host, - reverse(viewname, args=args, kwargs=kwargs, add_prefix=add_prefix), - ) + url = f'{host}{reverse(viewname, args=args, kwargs=kwargs, add_prefix=add_prefix)}' return url diff --git a/src/olympia/amo/tests/__init__.py b/src/olympia/amo/tests/__init__.py index 7356d78d4687..e3ed18ee442d 100644 --- a/src/olympia/amo/tests/__init__.py +++ b/src/olympia/amo/tests/__init__.py @@ -1266,7 +1266,7 @@ def prefix_indexes(config): Note that this is a pytest helper that is primarily used in conftest. """ if hasattr(config, 'slaveinput'): - prefix = 'test_{[slaveid]}'.format(config.slaveinput) + prefix = f'test_{config.slaveinput["slaveid"]}' else: prefix = 'test' @@ -1277,9 +1277,7 @@ def prefix_indexes(config): # unittest-based setup. for key, index in settings.ES_INDEXES.items(): if not index.startswith(prefix): - settings.ES_INDEXES[key] = '{prefix}_amo_{index}'.format( - prefix=prefix, index=index - ) + settings.ES_INDEXES[key] = f'{prefix}_amo_{index}' def reverse_ns(viewname, api_version=None, args=None, kwargs=None, **extra): diff --git a/src/olympia/amo/tests/test_commands.py b/src/olympia/amo/tests/test_commands.py index 0c5182d1b49d..a75e92020ddb 100644 --- a/src/olympia/amo/tests/test_commands.py +++ b/src/olympia/amo/tests/test_commands.py @@ -119,10 +119,8 @@ def test_raises_on_server_error(self): with ( mock.patch( - ( - 'olympia.amo.management.commands.generate_js_swagger_files.' - 'serve_swagger_ui_js' - ) + 'olympia.amo.management.commands.generate_js_swagger_files.' + 'serve_swagger_ui_js' ) as mock_view, pytest.raises(CommandError) as error_info, ): diff --git a/src/olympia/amo/tests/test_views.py b/src/olympia/amo/tests/test_views.py index ad66245c7bfc..54c78b7b0236 100644 --- a/src/olympia/amo/tests/test_views.py +++ b/src/olympia/amo/tests/test_views.py @@ -596,10 +596,7 @@ def test_version_json(self): assert result.get('Content-Type') == 'application/json' assert result.get('Access-Control-Allow-Origin') == '*' content = result.json() - assert content['python'] == '{}.{}'.format( - sys.version_info.major, - sys.version_info.minor, - ) + assert content['python'] == f'{sys.version_info.major}.{sys.version_info.minor}' assert content['django'] == f'{django.VERSION[0]}.{django.VERSION[1]}' assert 'addons-linter' in content assert '.' in content['addons-linter'] diff --git a/src/olympia/api/authentication.py b/src/olympia/api/authentication.py index a6fe322d7684..751b0f1609f4 100644 --- a/src/olympia/api/authentication.py +++ b/src/olympia/api/authentication.py @@ -46,9 +46,7 @@ def authenticate_header(self, request): header in a `401 Unauthenticated` response, or `None` if the authentication scheme should return `403 Permission Denied` responses. """ - return '{} realm="{}"'.format( - self.auth_header_prefix, self.www_authenticate_realm - ) + return f'{self.auth_header_prefix} realm="{self.www_authenticate_realm}"' def get_token_value(self, request): auth_header = get_authorization_header(request).split() @@ -163,9 +161,7 @@ def authenticate_header(self, request): header in a `401 Unauthenticated` response, or `None` if the authentication scheme should return `403 Permission Denied` responses. """ - return '{} realm="{}"'.format( - self.auth_header_prefix, self.www_authenticate_realm - ) + return f'{self.auth_header_prefix} realm="{self.www_authenticate_realm}"' def authenticate(self, request): """ diff --git a/src/olympia/api/jwt_auth.py b/src/olympia/api/jwt_auth.py index 08e7ec46fc29..e9a788a5e81f 100644 --- a/src/olympia/api/jwt_auth.py +++ b/src/olympia/api/jwt_auth.py @@ -100,14 +100,12 @@ def jwt_decode_handler(token, get_api_key=APIKey.get_jwt_key): except jwt.MissingRequiredClaimError as exc: log.info( 'Missing required claim during JWT authentication: ' - '{e.__class__.__name__}: {e}'.format(e=exc) + f'{exc.__class__.__name__}: {exc}' ) raise exceptions.AuthenticationFailed(detail=f'Invalid JWT: {exc}.') from exc except (jwt.exceptions.ImmatureSignatureError, jwt.InvalidIssuedAtError) as exc: log.info( - 'Invalid iat during JWT authentication: {e.__class__.__name__}: {e}'.format( - e=exc - ) + f'Invalid iat during JWT authentication: {exc.__class__.__name__}: {exc}' ) raise exceptions.AuthenticationFailed( detail='JWT iat (issued at time) is invalid. Make sure your ' @@ -116,7 +114,7 @@ def jwt_decode_handler(token, get_api_key=APIKey.get_jwt_key): except Exception as exc: log.warning( 'Unhandled exception during JWT authentication: ' - '{e.__class__.__name__}: {e}'.format(e=exc) + f'{exc.__class__.__name__}: {exc}' ) raise diff --git a/src/olympia/api/models.py b/src/olympia/api/models.py index cbff1e9bfa3c..934918f3e5a4 100644 --- a/src/olympia/api/models.py +++ b/src/olympia/api/models.py @@ -60,8 +60,9 @@ class Meta: ] def __str__(self): - return '<{cls} user={user}, type={type}, key={key} secret=...>'.format( - cls=self.__class__.__name__, key=self.key, type=self.type, user=self.user + return ( + f'<{self.__class__.__name__} user={self.user}, type={self.type}, ' + f'key={self.key} secret=...>' ) @classmethod @@ -116,9 +117,7 @@ def generate_secret(byte_length): """ if byte_length < 32: # at least 256 bit raise ValueError( - '{} is too short; secrets must be longer than 32 bytes'.format( - byte_length - ) + f'{byte_length} is too short; secrets must be longer than 32 bytes' ) return force_str(binascii.b2a_hex(os.urandom(byte_length))) diff --git a/src/olympia/api/tests/test_authentication.py b/src/olympia/api/tests/test_authentication.py index 8ff888baca99..4d4c84d29e52 100644 --- a/src/olympia/api/tests/test_authentication.py +++ b/src/olympia/api/tests/test_authentication.py @@ -319,9 +319,7 @@ def test_wrong_header_only_prefix(self): def test_wrong_header_too_many_spaces(self): request = self.factory.post( '/api/v4/whatever/', - HTTP_AUTHORIZATION='{} foo bar'.format( - SessionIDAuthentication.auth_header_prefix - ), + HTTP_AUTHORIZATION=f'{SessionIDAuthentication.auth_header_prefix} foo bar', ) with self.assertRaises(AuthenticationFailed) as exp: self.auth.authenticate(request) diff --git a/src/olympia/api/urls.py b/src/olympia/api/urls.py index 483222bafa6e..315ed3ea684f 100644 --- a/src/olympia/api/urls.py +++ b/src/olympia/api/urls.py @@ -15,7 +15,7 @@ def get_versioned_api_routes(version, url_patterns): - route_pattern = r'^{}/'.format(version) + route_pattern = rf'^{version}/' url_name = 'schema' routes = url_patterns diff --git a/src/olympia/blocklist/mlbf.py b/src/olympia/blocklist/mlbf.py index 44bde43885fc..dcdf8d476993 100644 --- a/src/olympia/blocklist/mlbf.py +++ b/src/olympia/blocklist/mlbf.py @@ -3,7 +3,7 @@ import secrets from collections import defaultdict from enum import Enum -from typing import Dict, List, Optional, Tuple +from typing import Optional from django.utils.functional import cached_property @@ -27,8 +27,8 @@ def get_base_replace_threshold(): def ordered_diff_lists( - previous: List[str], current: List[str] -) -> Tuple[List[str], List[str], int]: + previous: list[str], current: list[str] +) -> tuple[list[str], list[str], int]: current_set = set(current) previous_set = set(previous) # Use lists instead of sets to maintain order @@ -97,7 +97,7 @@ def _raw(self): """ return {self.data_type_key(key): self[key] for key in MLBFDataType} - def __getitem__(self, key: MLBFDataType) -> List[str]: + def __getitem__(self, key: MLBFDataType) -> list[str]: return getattr(self, f'{self.data_type_key(key)}_items') @cached_property @@ -105,15 +105,15 @@ def _cache_path(self): return self.storage.path('cache.json') @cached_property - def blocked_items(self) -> List[str]: + def blocked_items(self) -> list[str]: raise NotImplementedError @cached_property - def soft_blocked_items(self) -> List[str]: + def soft_blocked_items(self) -> list[str]: raise NotImplementedError @cached_property - def not_blocked_items(self) -> List[str]: + def not_blocked_items(self) -> list[str]: raise NotImplementedError @@ -124,15 +124,15 @@ def __init__(self, storage: SafeStorage): self._data = json.load(f) @cached_property - def blocked_items(self) -> List[str]: + def blocked_items(self) -> list[str]: return self._data.get(self.data_type_key(MLBFDataType.BLOCKED), []) @cached_property - def soft_blocked_items(self) -> List[str]: + def soft_blocked_items(self) -> list[str]: return self._data.get(self.data_type_key(MLBFDataType.SOFT_BLOCKED), []) @cached_property - def not_blocked_items(self) -> List[str]: + def not_blocked_items(self) -> list[str]: return self._data.get(self.data_type_key(MLBFDataType.NOT_BLOCKED), []) @@ -157,7 +157,7 @@ def _all_blocks(self): ) ) - def _format_blocks(self, versions: List[Tuple[str, str]]) -> List[str]: + def _format_blocks(self, versions: list[tuple[str, str]]) -> list[str]: unique_versions = set() deduped_versions = [] @@ -169,7 +169,7 @@ def _format_blocks(self, versions: List[Tuple[str, str]]) -> List[str]: return MLBF.hash_filter_inputs(deduped_versions) @cached_property - def blocked_items(self) -> List[str]: + def blocked_items(self) -> list[str]: return self._format_blocks( [ (version.block__guid, version.version__version) @@ -179,7 +179,7 @@ def blocked_items(self) -> List[str]: ) @cached_property - def soft_blocked_items(self) -> List[str]: + def soft_blocked_items(self) -> list[str]: return self._format_blocks( [ (version.block__guid, version.version__version) @@ -189,7 +189,7 @@ def soft_blocked_items(self) -> List[str]: ) @cached_property - def not_blocked_items(self) -> List[str]: + def not_blocked_items(self) -> list[str]: all_blocks_ids = [version.version_id for version in self._all_blocks] not_blocked_items = self._format_blocks( Version.unfiltered.exclude(id__in=all_blocks_ids) @@ -295,7 +295,7 @@ def generate_and_write_filter(self, block_type: BlockType): def generate_diffs( self, previous_mlbf: 'MLBF' = None - ) -> Dict[BlockType, Tuple[List[str], List[str], int]]: + ) -> dict[BlockType, tuple[list[str], list[str], int]]: return { block_type: ordered_diff_lists( [] if previous_mlbf is None else previous_mlbf.data[block_type], diff --git a/src/olympia/blocklist/tasks.py b/src/olympia/blocklist/tasks.py index e72274028477..dcc16d83b861 100644 --- a/src/olympia/blocklist/tasks.py +++ b/src/olympia/blocklist/tasks.py @@ -3,7 +3,6 @@ import os import re from datetime import datetime, timedelta -from typing import List from django.conf import settings from django.contrib.admin.models import CHANGE, LogEntry @@ -104,7 +103,7 @@ def monitor_remote_settings(): @task -def upload_filter(generation_time: str, actions: List[str] | None = None): +def upload_filter(generation_time: str, actions: list[str] | None = None): # Deserialize the actions from the string list # We have to do this because celery does not support enum arguments actions = [BlockListAction[action] for action in actions] diff --git a/src/olympia/blocklist/tests/test_cron.py b/src/olympia/blocklist/tests/test_cron.py index 82ecbe68f7f3..fb63ead3dd14 100644 --- a/src/olympia/blocklist/tests/test_cron.py +++ b/src/olympia/blocklist/tests/test_cron.py @@ -1,7 +1,6 @@ import json import uuid from datetime import datetime, timedelta -from typing import List from unittest import mock from django.conf import settings @@ -315,7 +314,7 @@ def test_upload_stash_unless_enough_changes(self, mock_get_base_replace_threshol def _test_upload_stash_and_filter( self, - expected_actions: List[BlockListAction], + expected_actions: list[BlockListAction], ): set_config(amo.config_keys.BLOCKLIST_BASE_REPLACE_THRESHOLD, 1) upload_mlbf_to_remote_settings() diff --git a/src/olympia/constants/categories.py b/src/olympia/constants/categories.py index 29e99bd8eade..4ee47705aa4a 100644 --- a/src/olympia/constants/categories.py +++ b/src/olympia/constants/categories.py @@ -34,10 +34,7 @@ def __str__(self): return str(self.name) def __repr__(self): - return '<{}: {}>'.format( - self.__class__.__name__, - force_bytes(self), - ) + return f'<{self.__class__.__name__}: {force_bytes(self)}>' def __eq__(self, other): return self.__class__ == other.__class__ and self.__dict__ == other.__dict__ diff --git a/src/olympia/core/utils.py b/src/olympia/core/utils.py index 6fddda3f85ba..e0c8728e7169 100644 --- a/src/olympia/core/utils.py +++ b/src/olympia/core/utils.py @@ -26,12 +26,8 @@ def get_version_json( contents.update(json.loads(f.read())) py_info = sys.version_info - contents['python'] = '{major}.{minor}'.format( - major=py_info.major, minor=py_info.minor - ) - contents['django'] = '{major}.{minor}'.format( - major=django.VERSION[0], minor=django.VERSION[1] - ) + contents['python'] = f'{py_info.major}.{py_info.minor}' + contents['django'] = f'{django.VERSION[0]}.{django.VERSION[1]}' if os.path.exists(pkg_json_path): with open(pkg_json_path) as f: diff --git a/src/olympia/devhub/tasks.py b/src/olympia/devhub/tasks.py index 7e050ac01144..c64c246384d7 100644 --- a/src/olympia/devhub/tasks.py +++ b/src/olympia/devhub/tasks.py @@ -92,11 +92,7 @@ def submit_file(*, addon_pk, upload_pk, client_info): addon=addon, upload=upload, channel=upload.channel, client_info=client_info ) else: - log.info( - 'Skipping version creation for {upload_uuid} that failed validation'.format( - upload_uuid=upload.uuid - ) - ) + log.info(f'Skipping version creation for {upload.uuid} that failed validation') @task @@ -295,9 +291,7 @@ def handle_upload_validation_result(results, upload_pk, is_mozilla_signed): # TODO: actually fix this so we can get stats. It seems that # the file maybe gets moved but it needs more investigation. log.warning( - 'Scaled upload stats were not tracked. File is missing: {}'.format( - upload.file_path - ) + f'Scaled upload stats were not tracked. File is missing: {upload.file_path}' ) return @@ -321,16 +315,9 @@ def handle_upload_validation_result(results, upload_pk, is_mozilla_signed): log.info( 'Time to process and save upload validation; ' - 'upload.pk={upload}; processing_time={delta}; ' - 'scaled_per_mb={scaled}; upload_size_in_mb={size_in_mb}; ' - 'created={created}; now={now}'.format( - delta=delta, - upload=upload.pk, - created=upload.created, - now=now, - scaled=scaled_delta, - size_in_mb=size_in_mb, - ) + f'upload.pk={upload.pk}; processing_time={delta}; ' + f'scaled_per_mb={scaled_delta}; upload_size_in_mb={size_in_mb}; ' + f'created={upload.created}; now={now}' ) diff --git a/src/olympia/devhub/utils.py b/src/olympia/devhub/utils.py index b04a200e02be..0992da399775 100644 --- a/src/olympia/devhub/utils.py +++ b/src/olympia/devhub/utils.py @@ -223,11 +223,7 @@ def __init__(self, file_, *, addon=None, theme_specific=False, final_task=None): ) raise except ValidationError as form_error: - log.info( - 'could not parse addon for upload {}: {}'.format( - file_.pk, form_error - ) - ) + log.info(f'could not parse addon for upload {file_.pk}: {form_error}') addon_data = None else: file_.update(version=addon_data.get('version')) @@ -333,16 +329,12 @@ def create_version_for_upload(*, addon, upload, channel, client_info=None): ).exists() if fileupload_exists or version_exists: log.info( - 'Skipping Version creation for {upload_uuid} that would ' - ' cause duplicate version'.format(upload_uuid=upload.uuid) + f'Skipping Version creation for {upload.uuid} that would ' + ' cause duplicate version' ) return None else: - log.info( - 'Creating version for {upload_uuid} that passed validation'.format( - upload_uuid=upload.uuid - ) - ) + log.info(f'Creating version for {upload.uuid} that passed validation') # Note: if we somehow managed to get here with an invalid add-on, # parse_addon() will raise ValidationError and the task will fail # loudly in sentry. diff --git a/src/olympia/discovery/management/commands/extract_content_strings.py b/src/olympia/discovery/management/commands/extract_content_strings.py index 47d5a3e77cb1..5e6f48ea36b7 100644 --- a/src/olympia/discovery/management/commands/extract_content_strings.py +++ b/src/olympia/discovery/management/commands/extract_content_strings.py @@ -58,9 +58,8 @@ def build_output_for_item(self, item): return ''.join(output) def build_output_for_single_value(self, value): - output = '{{# L10n: {} #}}\n{{% trans %}}{}{{% endtrans %}}\n'.format( - self.l10n_comment, - value, + output = ( + f'{{# L10n: {self.l10n_comment} #}}\n{{% trans %}}{value}{{% endtrans %}}\n' ) return output diff --git a/src/olympia/files/management/commands/extract_host_permissions.py b/src/olympia/files/management/commands/extract_host_permissions.py index 18daed29c651..16db3bbfc78f 100644 --- a/src/olympia/files/management/commands/extract_host_permissions.py +++ b/src/olympia/files/management/commands/extract_host_permissions.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from django.core.management.base import BaseCommand import olympia.core.logger diff --git a/src/olympia/files/tests/test_commands.py b/src/olympia/files/tests/test_commands.py index 28fa8fa95482..92a8b0f881ff 100644 --- a/src/olympia/files/tests/test_commands.py +++ b/src/olympia/files/tests/test_commands.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from unittest import mock from django.conf import settings @@ -31,7 +30,7 @@ def setUpTestData(cls): ) def setUp(self): - super(TestExtractHostPermissions, self).setUp() + super().setUp() self.addon = Addon.objects.create( guid='mv3@extension', type=amo.ADDON_EXTENSION, name='xxx' ) diff --git a/src/olympia/files/tests/test_models.py b/src/olympia/files/tests/test_models.py index 37aaf3952604..29c686a40ef3 100644 --- a/src/olympia/files/tests/test_models.py +++ b/src/olympia/files/tests/test_models.py @@ -1266,9 +1266,7 @@ def test_get_authenticated_download_url(self): relative_url = reverse( 'files.serve_file_upload', kwargs={'uuid': upload.uuid.hex} ) - expected_url = '{}?access_token={}'.format( - site_url + relative_url, access_token - ) + expected_url = f'{site_url + relative_url}?access_token={access_token}' with override_settings(EXTERNAL_SITE_URL=site_url): assert upload.get_authenticated_download_url() == expected_url diff --git a/src/olympia/landfill/serializers.py b/src/olympia/landfill/serializers.py index 97be5f08fa42..3991df195b01 100644 --- a/src/olympia/landfill/serializers.py +++ b/src/olympia/landfill/serializers.py @@ -68,8 +68,8 @@ def _create_addon_user(self): ) except Exception as e: log.info( - 'There was a problem creating the user: {}.' - ' Returning user from database'.format(e) + f'There was a problem creating the user: {e}.' + ' Returning user from database' ) return UserProfile.objects.get(username='uitest') diff --git a/src/olympia/lib/crypto/signing.py b/src/olympia/lib/crypto/signing.py index e56d366fc973..44c81688797c 100644 --- a/src/olympia/lib/crypto/signing.py +++ b/src/olympia/lib/crypto/signing.py @@ -181,18 +181,16 @@ def sign_file(file_obj): # though we didn't sign, it's not an error - we just don't need to do # anything in this case. log.info( - 'Not signing file {}: mozilla signed extension is already signed'.format( - file_obj.pk - ) + f'Not signing file {file_obj.pk}: mozilla signed extension is already ' + 'signed' ) return file_obj # We only sign files that are compatible with Firefox. if not supports_firefox(file_obj): raise SigningError( - 'Not signing version {}: not for a Firefox version we support'.format( - file_obj.version.pk - ) + f'Not signing version {file_obj.version.pk}: not for a Firefox version we ' + 'support' ) # Get the path before modifying it... We'll delete it after if signing was diff --git a/src/olympia/lib/crypto/tasks.py b/src/olympia/lib/crypto/tasks.py index b5d87d453460..bfca39397af8 100644 --- a/src/olympia/lib/crypto/tasks.py +++ b/src/olympia/lib/crypto/tasks.py @@ -128,9 +128,7 @@ def duplicate_addon_version(old_version, new_version_number, user): # We only sign files that have been reviewed if old_file_obj.status not in amo.APPROVED_STATUSES: log.info( - 'Not signing addon {}, version {} (no files)'.format( - old_version.addon, old_version - ) + f'Not signing addon {old_version.addon}, version {old_version} (no files)' ) return diff --git a/src/olympia/lib/crypto/tests/test_signing.py b/src/olympia/lib/crypto/tests/test_signing.py index 18768de6f18e..b28ceff5846b 100644 --- a/src/olympia/lib/crypto/tests/test_signing.py +++ b/src/olympia/lib/crypto/tests/test_signing.py @@ -637,7 +637,7 @@ def test_no_bump_bad_zipfile(self, mock_sign_file): assert len(mail.outbox) == 0 def test_dont_sign_dont_bump_sign_error(self, mock_sign_file): - mock_sign_file.side_effect = IOError() + mock_sign_file.side_effect = OSError() # IOError should be caught, this shouldn't raise. tasks.bump_and_resign_addons([self.addon.pk]) diff --git a/src/olympia/reviewers/admin.py b/src/olympia/reviewers/admin.py index 34bb7412b743..291795276dc7 100644 --- a/src/olympia/reviewers/admin.py +++ b/src/olympia/reviewers/admin.py @@ -106,7 +106,7 @@ def addons_sql_count_query(self, qs): # have to do it ourselves. For our purposes, just surrounding strings # by double-quotes is enough here. sql = sql_with_params[0] % tuple( - '"{}"'.format(p) if isinstance(p, (str,)) else p for p in sql_with_params[1] + f'"{p}"' if isinstance(p, (str,)) else p for p in sql_with_params[1] ) # Similarly we have no way to directly print a COUNT(*) query without # executing it, but we know we're only dealing with addons and we know @@ -174,8 +174,9 @@ def get_form(self, request, obj=None, **kwargs): 'growth_threshold_before_flagging': ( 'Actual growth threshold above which we would flag add-ons in that ' 'tier, as computed using the percentage defined above and the ' - 'current average growth of add-ons (currently {}) in that tier.' - ).format(obj.average_growth), + 'current average growth of add-ons ' + f'(currently {obj.average_growth}) in that tier.' + ), 'number_of_addons_that_would_be_flagged_for_growth': ( 'Number of add-ons that would be flagged for growth using the ' 'percentage defined above, if the task ran now with the current ' diff --git a/src/olympia/reviewers/models.py b/src/olympia/reviewers/models.py index 66800c64c769..697a9988bc86 100644 --- a/src/olympia/reviewers/models.py +++ b/src/olympia/reviewers/models.py @@ -771,11 +771,7 @@ class Meta: db_table = 'review_whiteboard' def __str__(self): - return '[{}] private: |{}| public: |{}|'.format( - self.addon.name, - self.private, - self.public, - ) + return f'[{self.addon.name}] private: |{self.private}| public: |{self.public}|' class ReviewActionReason(ModelBase): diff --git a/src/olympia/reviewers/tests/test_assay_tags.py b/src/olympia/reviewers/tests/test_assay_tags.py index a543e7f21885..7cb6d0b329a4 100644 --- a/src/olympia/reviewers/tests/test_assay_tags.py +++ b/src/olympia/reviewers/tests/test_assay_tags.py @@ -12,12 +12,10 @@ def setUp(self): def test_create_an_assay_url(self): assert assay.assay_url(self.addon_guid, self.version_string) == ( - '{}/{}/{}'.format(self.assay_url, self.addon_guid, self.version_string) + f'{self.assay_url}/{self.addon_guid}/{self.version_string}' ) def test_create_an_assay_url_with_file(self): assert assay.assay_url(self.addon_guid, self.version_string, self.filepath) == ( - '{}/{}/{}?path={}'.format( - self.assay_url, self.addon_guid, self.version_string, self.filepath - ) + f'{self.assay_url}/{self.addon_guid}/{self.version_string}?path={self.filepath}' ) diff --git a/src/olympia/reviewers/tests/test_views.py b/src/olympia/reviewers/tests/test_views.py index dcef0da875b9..e7b5b41ce21e 100644 --- a/src/olympia/reviewers/tests/test_views.py +++ b/src/olympia/reviewers/tests/test_views.py @@ -5245,10 +5245,9 @@ def test_block_multiple_versions(self): follow=True, ) - new_block_url = reverse( - 'admin:blocklist_blocklistsubmission_add' - ) + '?guids={}&v={}&v={}'.format( - self.addon.guid, old_version.pk, self.version.pk + new_block_url = ( + reverse('admin:blocklist_blocklistsubmission_add') + + f'?guids={self.addon.guid}&v={old_version.pk}&v={self.version.pk}' ) self.assertRedirects(response, new_block_url) diff --git a/src/olympia/scanners/admin.py b/src/olympia/scanners/admin.py index e30fe1b65fdf..d54aa0dc0828 100644 --- a/src/olympia/scanners/admin.py +++ b/src/olympia/scanners/admin.py @@ -564,9 +564,7 @@ def matched_results_link(self, obj): ) ResultModel = obj.results.model url = reverse( - 'admin:{}_{}_changelist'.format( - ResultModel._meta.app_label, ResultModel._meta.model_name - ) + f'admin:{ResultModel._meta.app_label}_{ResultModel._meta.model_name}_changelist' ) # The parameter name is called matched_rule or matched_rules depending # on the model, because one of them is a many to many and the other a @@ -896,17 +894,15 @@ def handle_run(self, request, pk, *args, **kwargs): messages.add_message( request, messages.INFO, - 'Scanner Query Rule {} has been successfully queued for ' - 'execution.'.format(rule.pk), + f'Scanner Query Rule {rule.pk} has been successfully queued for ' + 'execution.', ) except ImproperScannerQueryRuleStateError: messages.add_message( request, messages.ERROR, - 'Scanner Query Rule {} could not be queued for execution ' - 'because it was in "{}"" state.'.format( - rule.pk, rule.get_state_display() - ), + f'Scanner Query Rule {rule.pk} could not be queued for execution ' + f'because it was in "{rule.get_state_display()}"" state.', ) return redirect('admin:scanners_scannerqueryrule_changelist') @@ -934,8 +930,8 @@ def handle_abort(self, request, pk, *args, **kwargs): messages.add_message( request, messages.ERROR, - 'Scanner Query Rule {} could not be aborted because it was ' - 'in "{}" state'.format(rule.pk, rule.get_state_display()), + f'Scanner Query Rule {rule.pk} could not be aborted because it was ' + f'in "{rule.get_state_display()}" state', ) return redirect('admin:scanners_scannerqueryrule_changelist') diff --git a/src/olympia/scanners/tests/test_templatetags.py b/src/olympia/scanners/tests/test_templatetags.py index 3edd72f1a0ca..f049f8f35556 100644 --- a/src/olympia/scanners/tests/test_templatetags.py +++ b/src/olympia/scanners/tests/test_templatetags.py @@ -82,7 +82,7 @@ def test_format_scanners_data_complex(): {'extensionId': '@welp'}, ] expected_url = 'http://testserver/en-US/reviewers/review/@welp' - expected = """