diff --git a/.github/workflows/slack.yml b/.github/workflows/slack.yml
index 8818ff1eb38..9a9c24b1835 100644
--- a/.github/workflows/slack.yml
+++ b/.github/workflows/slack.yml
@@ -6,13 +6,11 @@ on:
branches: [main]
jobs:
on-failure:
- runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
- steps:
- - name: Post to Slack
- uses: slackapi/slack-github-action@v1.24.0
- with:
- channel-id: 'C4K6M7P5E'
- slack-message: "A workflow run failed\n*Repo:* `${{ github.event.repository.full_name }}` (${{ github.event.repository.html_url }})\n*Workflow:* ${{ github.event.workflow.name }} (${{ github.event.workflow.html_url }})\n*Branch:* `${{ github.event.workflow_run.head_branch }}`\n*Commit:* `${{ github.event.workflow_run.head_commit.id }}`\n*Run:* ${{ github.event.workflow_run.html_url }}\n*Conclusion:* ${{ github.event.workflow_run.conclusion }}"
- env:
- SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
+ name: Post to Slack
+ uses: hypothesis/workflows/.github/workflows/slack.yml@main
+ with:
+ payload: |
+ channel: "C4K6M7P5E"
+ markdown_text: "Failed `${{ github.event.workflow_run.path }}` run (attempt ${{ github.event.workflow_run.run_attempt }}) on `${{ github.event.workflow_run.head_branch }}` in `${{ github.event.workflow_run.repository.full_name }}`: ${{ github.event.workflow_run.html_url }}"
+ secrets: inherit
diff --git a/Makefile b/Makefile
index bbad09a179b..278977b82ec 100644
--- a/Makefile
+++ b/Makefile
@@ -8,7 +8,6 @@ $(call help,make help,print this help message)
$(call help,make services,start the services that the app needs)
services: args?=up -d --wait
services: python
- @docker network create dbs 2>/dev/null || true
@docker compose $(args)
.PHONY: db
diff --git a/h/_version.py b/h/_version.py
index c158d99eca3..9e11ca0538a 100644
--- a/h/_version.py
+++ b/h/_version.py
@@ -13,7 +13,7 @@
def fetch_git_ref():
- return subprocess.check_output( # noqa: S603
+ return subprocess.check_output(
["git", "rev-parse", "--short", "HEAD"], # noqa: S607
stderr=DEVNULL,
).strip()
@@ -27,10 +27,10 @@ def fetch_git_date(ref):
def fetch_git_dirty():
# Ensure git index is up-to-date first. This usually isn't necessary, but
# can be needed inside a docker container where the index is out of date.
- subprocess.call(["git", "update-index", "-q", "--refresh"]) # noqa: S603, S607
- dirty_tree = bool(subprocess.call(["git", "diff-files", "--quiet"])) # noqa: S603, S607
+ subprocess.call(["git", "update-index", "-q", "--refresh"]) # noqa: S607
+ dirty_tree = bool(subprocess.call(["git", "diff-files", "--quiet"])) # noqa: S607
dirty_index = bool(
- subprocess.call(["git", "diff-index", "--quiet", "--cached", "HEAD"]) # noqa: S603, S607
+ subprocess.call(["git", "diff-index", "--quiet", "--cached", "HEAD"]) # noqa: S607
)
return dirty_tree or dirty_index
diff --git a/h/activity/bucketing.py b/h/activity/bucketing.py
index 832e10a13c5..4775dfb2e6b 100644
--- a/h/activity/bucketing.py
+++ b/h/activity/bucketing.py
@@ -12,7 +12,7 @@
_ = i18n.TranslationStringFactory(__package__)
-class DocumentBucket:
+class DocumentBucket: # noqa: PLW1641
def __init__(self, document, annotations=None):
self.annotations = []
self.tags = set()
diff --git a/h/cli/commands/create_annotations.py b/h/cli/commands/create_annotations.py
index 70211c46230..8da9f4f95ed 100644
--- a/h/cli/commands/create_annotations.py
+++ b/h/cli/commands/create_annotations.py
@@ -8,7 +8,7 @@
@click.pass_context
@click.option("--number", default=100)
def create_annotations(ctx, number):
- from tests.common import factories
+ from tests.common import factories # noqa: PLC0415
request = ctx.obj["bootstrap"]()
db = request.db
diff --git a/h/db/__init__.py b/h/db/__init__.py
index 4aa4bf8a2f7..f86238054f3 100644
--- a/h/db/__init__.py
+++ b/h/db/__init__.py
@@ -122,7 +122,7 @@ def close_the_sqlalchemy_session(_request):
def _maybe_create_default_organization(engine, authority): # pragma: no cover
- from h.services.organization import OrganizationService
+ from h.services.organization import OrganizationService # noqa: PLC0415
session = Session(bind=engine)
default_org = OrganizationService(session).get_default(authority)
@@ -134,8 +134,8 @@ def _maybe_create_default_organization(engine, authority): # pragma: no cover
def _maybe_create_world_group(engine, authority, default_org): # pragma: no cover
- from h import models
- from h.models.group import ReadableBy, WriteableBy
+ from h import models # noqa: PLC0415
+ from h.models.group import ReadableBy, WriteableBy # noqa: PLC0415
session = Session(bind=engine)
world_group = session.query(models.Group).filter_by(pubid="__world__").one_or_none()
diff --git a/h/db/types.py b/h/db/types.py
index f78e645b39b..76c8e7fa87f 100644
--- a/h/db/types.py
+++ b/h/db/types.py
@@ -1,4 +1,4 @@
-"""Custom SQLAlchemy types for use with the Annotations API database.""" # noqa: A005
+"""Custom SQLAlchemy types for use with the Annotations API database."""
import base64
import binascii
diff --git a/h/form.py b/h/form.py
index fa3e1063c0b..efa1921382c 100644
--- a/h/form.py
+++ b/h/form.py
@@ -49,7 +49,7 @@ def __call__(self, template_name, **kwargs):
context = self._system.copy()
context.update(kwargs)
- return Markup(template.render(context))
+ return Markup(f"{template.render(context)}") # noqa: S704
def create_environment(base):
diff --git a/h/jinja_extensions/filters.py b/h/jinja_extensions/filters.py
index 68360e4e6fb..4bdb8723d00 100644
--- a/h/jinja_extensions/filters.py
+++ b/h/jinja_extensions/filters.py
@@ -37,4 +37,4 @@ def to_json(value):
.replace("'", "\\u0027")
)
- return Markup(result)
+ return Markup(result) # noqa: S704
diff --git a/h/jinja_extensions/svg_icon.py b/h/jinja_extensions/svg_icon.py
index 179d5f9e5a2..26f2c7bdd7e 100644
--- a/h/jinja_extensions/svg_icon.py
+++ b/h/jinja_extensions/svg_icon.py
@@ -48,4 +48,4 @@ def svg_icon(name, css_class=""):
if title_el is not None:
root.remove(title_el)
- return Markup(ElementTree.tostring(root).decode())
+ return Markup(ElementTree.tostring(root).decode()) # noqa: S704
diff --git a/h/migrations/versions/58bb601c390f_fill_in_missing_denormalized_document_title.py b/h/migrations/versions/58bb601c390f_fill_in_missing_denormalized_document_title.py
index f2742585c17..6968d0b24ee 100644
--- a/h/migrations/versions/58bb601c390f_fill_in_missing_denormalized_document_title.py
+++ b/h/migrations/versions/58bb601c390f_fill_in_missing_denormalized_document_title.py
@@ -71,10 +71,12 @@ def downgrade():
def _document_title(document):
- for meta in document.meta_titles: # noqa: RET503
+ for meta in document.meta_titles:
if meta.value:
return meta.value[0]
+ return None
+
def _fetch_windows(session, chunksize=100):
updated = (
diff --git a/h/migrations/versions/9f5e274b202c_update_all_document_web_uris.py b/h/migrations/versions/9f5e274b202c_update_all_document_web_uris.py
index 24d792ee0cb..1a4dd215c91 100644
--- a/h/migrations/versions/9f5e274b202c_update_all_document_web_uris.py
+++ b/h/migrations/versions/9f5e274b202c_update_all_document_web_uris.py
@@ -39,7 +39,7 @@ class Document(Base):
def updated_web_uri(self):
def first_http_url(type_=None):
- for document_uri in self.document_uris: # noqa: RET503
+ for document_uri in self.document_uris:
uri = document_uri.uri
if type_ is not None and document_uri.type != type_:
continue
@@ -47,6 +47,8 @@ def first_http_url(type_=None):
continue
return document_uri.uri
+ return None
+
return (
first_http_url(type_="self-claim")
or first_http_url(type_="rel-canonical")
diff --git a/h/migrations/versions/a44ef07b085a_fill_in_missing_denormalized_document_web_uri.py b/h/migrations/versions/a44ef07b085a_fill_in_missing_denormalized_document_web_uri.py
index e240c5770a6..948ffd31939 100644
--- a/h/migrations/versions/a44ef07b085a_fill_in_missing_denormalized_document_web_uri.py
+++ b/h/migrations/versions/a44ef07b085a_fill_in_missing_denormalized_document_web_uri.py
@@ -68,11 +68,13 @@ def downgrade():
def _document_web_uri(document):
- for docuri in document.document_uris: # noqa: RET503
+ for docuri in document.document_uris:
uri = urlparse(docuri.uri)
if uri.scheme in ["http", "https"]:
return docuri.uri
+ return None
+
def _fetch_windows(session, chunksize=100):
updated = (
diff --git a/h/models/document/_document.py b/h/models/document/_document.py
index 6d94fa07e96..75302954c72 100644
--- a/h/models/document/_document.py
+++ b/h/models/document/_document.py
@@ -56,7 +56,7 @@ def first_http_url(type_=None):
If no type is given just return this document's first http(s)
URL, or None.
"""
- for document_uri in self.document_uris: # noqa: RET503
+ for document_uri in self.document_uris:
uri = document_uri.uri
if type_ is not None and document_uri.type != type_:
continue
@@ -64,6 +64,8 @@ def first_http_url(type_=None):
continue
return document_uri.uri
+ return None
+
self.web_uri = (
first_http_url(type_="self-claim")
or first_http_url(type_="rel-canonical")
@@ -155,7 +157,7 @@ def merge_documents(session, documents, updated=None):
# the purpose of centralized model access in the service.
# It's pending to move the rest of this functions to a DocumentService that uses DI to get this service.
# In the meantime we are doing this import here to avoid a circular dependency.
- from h.services.annotation_write import AnnotationWriteService
+ from h.services.annotation_write import AnnotationWriteService # noqa: PLC0415
AnnotationWriteService.change_document(session, duplicate_ids, master)
session.query(Document).filter(Document.id.in_(duplicate_ids)).delete(
diff --git a/h/models/token.py b/h/models/token.py
index 8021771d532..3ef9000c24d 100644
--- a/h/models/token.py
+++ b/h/models/token.py
@@ -1,4 +1,4 @@
-import datetime # noqa: A005
+import datetime
from typing import TYPE_CHECKING
import sqlalchemy
diff --git a/h/models/user.py b/h/models/user.py
index c74d32544d8..ec113094aff 100644
--- a/h/models/user.py
+++ b/h/models/user.py
@@ -69,7 +69,7 @@ def in_(self, other):
return _normalise_username(self.__clause_element__()).in_(usernames)
-class UserIDComparator(Comparator):
+class UserIDComparator(Comparator): # noqa: PLW1641
"""
Custom comparator for :py:attr:`~h.models.user.User.userid`.
diff --git a/h/paginator.py b/h/paginator.py
index a1de99412da..4c889d589c2 100644
--- a/h/paginator.py
+++ b/h/paginator.py
@@ -6,7 +6,7 @@
def paginate(request, total, page_size=PAGE_SIZE):
first = 1
- page_max = int(math.ceil(total / page_size))
+ page_max = int(math.ceil(total / page_size)) # noqa: RUF046
page_max = max(1, page_max) # There's always at least one page.
try:
diff --git a/h/presenters/annotation_html.py b/h/presenters/annotation_html.py
index 0309fa9f5a5..4be2bcb2157 100644
--- a/h/presenters/annotation_html.py
+++ b/h/presenters/annotation_html.py
@@ -28,7 +28,7 @@ def text_rendered(self):
care of all necessary escaping.
"""
if self.annotation.text_rendered:
- return Markup(self.annotation.text_rendered)
+ return Markup(self.annotation.text_rendered) # noqa: S704
return Markup("")
@property
diff --git a/h/presenters/document_html.py b/h/presenters/document_html.py
index be91aedc051..d8769c9ef50 100644
--- a/h/presenters/document_html.py
+++ b/h/presenters/document_html.py
@@ -226,4 +226,4 @@ def truncate(content, length=55):
host_or_filename=markupsafe.escape(host_or_filename),
)
- return markupsafe.Markup(link)
+ return markupsafe.Markup(f"{link}") # noqa: S704
diff --git a/h/pshell.py b/h/pshell.py
index e37be57dbb3..69daccafd55 100644
--- a/h/pshell.py
+++ b/h/pshell.py
@@ -8,7 +8,7 @@
def setup(env):
- from tests.common import factories
+ from tests.common import factories # noqa: PLC0415
request = env["request"]
diff --git a/h/security/encryption.py b/h/security/encryption.py
index d5a77c89845..2ba67667edc 100644
--- a/h/security/encryption.py
+++ b/h/security/encryption.py
@@ -40,7 +40,7 @@ def derive_key(key_material: str | bytes, salt: bytes, info: bytes) -> bytes:
key_material = key_material.encode()
return cast(
- bytes,
+ "bytes",
HKDF(
master=key_material,
key_len=64,
diff --git a/h/services/email.py b/h/services/email.py
index e6aaa75bc14..726a71c041c 100644
--- a/h/services/email.py
+++ b/h/services/email.py
@@ -1,5 +1,3 @@
-# noqa: A005
-
import smtplib
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
diff --git a/h/services/flag.py b/h/services/flag.py
index 3626a7fd249..713aab2673a 100644
--- a/h/services/flag.py
+++ b/h/services/flag.py
@@ -128,7 +128,7 @@ def flag_counts(self, annotation_ids):
flag_counts = {f.annotation_id: f.flag_count for f in query}
missing_ids = set(annotation_ids) - set(flag_counts.keys())
- flag_counts.update({id_: 0 for id_ in missing_ids})
+ flag_counts.update(dict.fromkeys(missing_ids, 0))
# Prime the cache for `flag_count()`
self._flag_count_cache.update(flag_counts)
diff --git a/h/services/html.py b/h/services/html.py
index 30e8ecebef6..bd11eb43d80 100644
--- a/h/services/html.py
+++ b/h/services/html.py
@@ -1,5 +1,3 @@
-# noqa: A005
-
from html.parser import HTMLParser
diff --git a/h/services/http.py b/h/services/http.py
index 7fe916b3a7d..4620f0e35f2 100644
--- a/h/services/http.py
+++ b/h/services/http.py
@@ -1,4 +1,4 @@
-from requests import RequestException, Response, Session # noqa: A005
+from requests import RequestException, Response, Session
from h.services.exceptions import ExternalRequestError
diff --git a/h/streamer/websocket.py b/h/streamer/websocket.py
index 479aa70f6c8..2bfb3dd9469 100644
--- a/h/streamer/websocket.py
+++ b/h/streamer/websocket.py
@@ -29,7 +29,7 @@ def reply(self, payload, ok=True): # noqa: FBT002
"""
reply_to = self.payload.get("id")
# Short-circuit if message is missing an ID or has a non-numeric ID.
- if not isinstance(reply_to, (int, float)): # noqa: UP038
+ if not isinstance(reply_to, (int, float)):
return
data = copy.deepcopy(payload)
data["ok"] = ok
diff --git a/h/tasks/email.py b/h/tasks/email.py
index f4bbf25aefa..3e537478bbd 100644
--- a/h/tasks/email.py
+++ b/h/tasks/email.py
@@ -2,7 +2,7 @@
A module for sending email.
This module defines a Celery task for sending emails in a worker process.
-""" # noqa: A005
+"""
from typing import Any
diff --git a/h/util/datetime.py b/h/util/datetime.py
index 0e788f57c50..2ff247c8f1a 100644
--- a/h/util/datetime.py
+++ b/h/util/datetime.py
@@ -1,4 +1,4 @@
-"""Shared utility functions for manipulating dates and times.""" # noqa: A005
+"""Shared utility functions for manipulating dates and times."""
def utc_iso8601(datetime):
diff --git a/h/views/accounts.py b/h/views/accounts.py
index 86d6c165c0f..e6633411193 100644
--- a/h/views/accounts.py
+++ b/h/views/accounts.py
@@ -310,8 +310,8 @@ def _reset_password(self, user, password):
svc.update_password(user, password)
self.request.session.flash(
- Markup(
- _(
+ _(
+ Markup(
"Your password has been reset. You can now log in with "
"your new password."
)
@@ -346,8 +346,8 @@ def get_when_not_logged_in(self):
activation = models.Activation.get_by_code(self.request.db, code)
if activation is None:
self.request.session.flash(
- Markup(
- _(
+ _(
+ Markup(
"We didn't recognize that activation link. "
"Have you already activated your account? "
"If so, try logging in using the username "
@@ -365,8 +365,8 @@ def get_when_not_logged_in(self):
user.activate()
self.request.session.flash(
- Markup(
- _(
+ _(
+ Markup(
"Your account has been activated! "
"You can now log in using the password you provided."
),
@@ -394,18 +394,18 @@ def get_when_logged_in(self):
# The user is already logged in to the account (so the account
# must already be activated).
self.request.session.flash(
- Markup(_("Your account has been activated and you're logged in.")),
+ _(Markup("Your account has been activated and you're logged in.")),
"success",
)
else:
self.request.session.flash(
- Markup(
- _(
+ _(
+ Markup(
"You're already logged in to a different account. "
'Log out and open the activation link '
"again."
- ).format(url=self.request.route_url("logout"))
- ),
+ )
+ ).format(url=self.request.route_url("logout")),
"error",
)
diff --git a/h/views/activity.py b/h/views/activity.py
index ba6089471e5..88e6037906f 100644
--- a/h/views/activity.py
+++ b/h/views/activity.py
@@ -171,11 +171,9 @@ def user_annotation_count(aggregation, userid):
result["more_info"] = "more_info" in self.request.params
if not result.get("q"):
- result["zero_message"] = Markup(
- _("The group “{name}” has not made any annotations yet.").format(
- name=Markup.escape(self.group.name)
- )
- )
+ result["zero_message"] = _(
+ Markup("The group “{name}” has not made any annotations yet.")
+ ).format(name=Markup.escape(self.group.name))
result["show_leave_button"] = self.request.user in self.group.members
diff --git a/h/views/admin/email.py b/h/views/admin/email.py
index 587650028c4..e13b267d7bb 100644
--- a/h/views/admin/email.py
+++ b/h/views/admin/email.py
@@ -1,4 +1,4 @@
-from dataclasses import asdict # noqa: A005
+from dataclasses import asdict
from pyramid.httpexceptions import HTTPSeeOther
from pyramid.view import view_config
diff --git a/h/views/admin/groups.py b/h/views/admin/groups.py
index 480ac9f62f4..e378d3ccc6e 100644
--- a/h/views/admin/groups.py
+++ b/h/views/admin/groups.py
@@ -116,7 +116,7 @@ def on_success(appstruct):
self.group_members_svc.add_members(group, member_userids)
self.request.session.flash(
- Markup(f'Created new group "{group.name}"'),
+ Markup('Created new group "{}"').format(group.name),
queue="success",
)
diff --git a/h/views/admin/organizations.py b/h/views/admin/organizations.py
index 5bafa5fe16c..6834a768502 100644
--- a/h/views/admin/organizations.py
+++ b/h/views/admin/organizations.py
@@ -60,7 +60,7 @@ def on_success(appstruct):
self.request.db.add(organization)
self.request.session.flash(
- Markup(_(f"Created new organization {name}")), # noqa: INT001
+ _(Markup("Created new organization {}")).format(name),
"success",
)
diff --git a/h/views/admin/users.py b/h/views/admin/users.py
index 3d66f117773..b23b0424f8c 100644
--- a/h/views/admin/users.py
+++ b/h/views/admin/users.py
@@ -88,7 +88,7 @@ def users_activate(request):
user.activate()
request.session.flash(
- Markup(_(f"User {user.username} has been activated!")), # noqa: INT001
+ _(Markup("User {} has been activated!")).format(user.username),
"success",
)
@@ -163,7 +163,7 @@ def users_delete(request):
@view_config(context=UserNotFoundError)
def user_not_found(exc, request):
- request.session.flash(Markup(_(str(exc))), "error")
+ request.session.flash(Markup("{}").format(_(str(exc))), "error")
return httpexceptions.HTTPFound(location=request.route_path("admin.users"))
diff --git a/h/views/api/profile.py b/h/views/api/profile.py
index a6488433db7..691f5666929 100644
--- a/h/views/api/profile.py
+++ b/h/views/api/profile.py
@@ -1,4 +1,4 @@
-from pyramid.httpexceptions import HTTPBadRequest # noqa: A005
+from pyramid.httpexceptions import HTTPBadRequest
from h import session as h_session
from h.presenters import GroupsJSONPresenter
diff --git a/requirements/checkformatting.txt b/requirements/checkformatting.txt
index 885e8ff4d13..a28fd992bcc 100644
--- a/requirements/checkformatting.txt
+++ b/requirements/checkformatting.txt
@@ -22,7 +22,7 @@ pyproject-hooks==1.0.0
# via
# build
# pip-tools
-ruff==0.9.9
+ruff==0.13.0
# via -r requirements/checkformatting.in
wheel==0.42.0
# via pip-tools
diff --git a/requirements/format.txt b/requirements/format.txt
index d0941b999d7..d6721925597 100644
--- a/requirements/format.txt
+++ b/requirements/format.txt
@@ -22,7 +22,7 @@ pyproject-hooks==1.0.0
# via
# build
# pip-tools
-ruff==0.9.9
+ruff==0.13.0
# via -r requirements/format.in
wheel==0.42.0
# via pip-tools
diff --git a/requirements/lint.txt b/requirements/lint.txt
index b166395f28f..61808a5db3a 100644
--- a/requirements/lint.txt
+++ b/requirements/lint.txt
@@ -461,7 +461,7 @@ rpds-py==0.22.3
# -r requirements/tests.txt
# jsonschema
# referencing
-ruff==0.9.9
+ruff==0.13.0
# via -r requirements/lint.in
sentry-sdk==2.22.0
# via
diff --git a/tests/common/factories/token.py b/tests/common/factories/token.py
index ff7d24107c8..a4bb7201429 100644
--- a/tests/common/factories/token.py
+++ b/tests/common/factories/token.py
@@ -1,4 +1,4 @@
-from datetime import datetime, timedelta # noqa: A005
+from datetime import datetime, timedelta
import factory
diff --git a/tests/conftest.py b/tests/conftest.py
index 83686614746..c28d6aee964 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -12,7 +12,7 @@
@pytest.fixture
def matchers():
- from tests import matchers
+ from tests import matchers # noqa: PLC0415
return matchers
diff --git a/tests/functional/api/groups/create_test.py b/tests/functional/api/groups/create_test.py
index d06614e79ba..efbee37a739 100644
--- a/tests/functional/api/groups/create_test.py
+++ b/tests/functional/api/groups/create_test.py
@@ -158,5 +158,5 @@ def user_with_token(db_session, factories):
@pytest.fixture
def token_auth_header(user_with_token):
- user, token = user_with_token
+ _, token = user_with_token
return {"Authorization": f"Bearer {token.value}"}
diff --git a/tests/functional/api/groups/read_test.py b/tests/functional/api/groups/read_test.py
index 8d133396129..55e7ddf896e 100644
--- a/tests/functional/api/groups/read_test.py
+++ b/tests/functional/api/groups/read_test.py
@@ -176,5 +176,5 @@ def user_with_token(db_session, factories):
@pytest.fixture
def token_auth_header(user_with_token):
- user, token = user_with_token
+ _, token = user_with_token
return {"Authorization": f"Bearer {token.value}"}
diff --git a/tests/functional/api/groups/update_test.py b/tests/functional/api/groups/update_test.py
index 0df83ef52d6..3add48bf363 100644
--- a/tests/functional/api/groups/update_test.py
+++ b/tests/functional/api/groups/update_test.py
@@ -285,7 +285,7 @@ def user_with_token(db_session, factories, first_party_user):
@pytest.fixture
def token_auth_header(user_with_token):
- user, token = user_with_token
+ _, token = user_with_token
return {"Authorization": f"Bearer {token.value}"}
diff --git a/tests/functional/h/views/admin/permissions_test.py b/tests/functional/h/views/admin/permissions_test.py
index e0f2247477a..a6349267d79 100644
--- a/tests/functional/h/views/admin/permissions_test.py
+++ b/tests/functional/h/views/admin/permissions_test.py
@@ -66,7 +66,7 @@ def test_group_end_points_not_accessible_by_regular_user(
group,
method,
url_template,
- _, # noqa: PT019
+ _,
):
url = url_template.format(pubid=group.pubid)
diff --git a/tests/matchers.py b/tests/matchers.py
index 169a533b675..06f32488a8e 100644
--- a/tests/matchers.py
+++ b/tests/matchers.py
@@ -28,7 +28,7 @@
from h.models.helpers import repr_
-class Matcher:
+class Matcher: # noqa: PLW1641
"""Base class for matcher classes below."""
repr_attrs = ()
@@ -44,7 +44,7 @@ def __repr__(self):
return repr_(self, self.repr_attrs)
-class InstanceOf(Matcher):
+class InstanceOf(Matcher): # noqa: PLW1641
"""Matches any instance of the given class with the given attrs.
As with Python's builtin isinstance() `class_` can be either a single class
@@ -91,6 +91,9 @@ def __eq__(self, other):
return other.location == self.location
+ def __hash__(self):
+ return hash(self.location)
+
class StringStartingWith(Matcher):
repr_attrs = ("starting_with",)
@@ -100,3 +103,6 @@ def __init__(self, starting_with):
def __eq__(self, other):
return other.startswith(self.starting_with)
+
+ def __hash__(self):
+ return hash(self.starting_with)
diff --git a/tests/unit/h/activity/bucketing_test.py b/tests/unit/h/activity/bucketing_test.py
index 72c55cbf6df..7777bf65e5d 100644
--- a/tests/unit/h/activity/bucketing_test.py
+++ b/tests/unit/h/activity/bucketing_test.py
@@ -13,7 +13,7 @@
FIFTH_NOVEMBER_1969 = datetime.datetime(year=1969, month=11, day=5) # noqa: DTZ001
-class timeframe_with:
+class timeframe_with: # noqa: PLW1641
def __init__(self, label, document_buckets):
self.label = label
self.document_buckets = document_buckets
diff --git a/tests/unit/h/form_test.py b/tests/unit/h/form_test.py
index 82ca0d564ee..b00932b5069 100644
--- a/tests/unit/h/form_test.py
+++ b/tests/unit/h/form_test.py
@@ -338,7 +338,7 @@ def on_success_spec(appstruct):
mock.sentinel.on_failure,
)
- class PostItemsMatcher:
+ class PostItemsMatcher: # noqa: PLW1641
"""Matches any iterable equal to request.POST.items()."""
def __eq__(self, other):
diff --git a/tests/unit/h/jinja2_extensions/svg_icon_test.py b/tests/unit/h/jinja2_extensions/svg_icon_test.py
index 961e6bb748d..5469642c43b 100644
--- a/tests/unit/h/jinja2_extensions/svg_icon_test.py
+++ b/tests/unit/h/jinja2_extensions/svg_icon_test.py
@@ -32,7 +32,7 @@ def test_it_sets_css_class(self, icon_file, css_class, expected):
result = svg_icon("test_icon_name", css_class=css_class)
- assert result == Markup(f'')
+ assert result == Markup(f'') # noqa: S704
@pytest.fixture
def icon_file(self, tmpdir):
diff --git a/tests/unit/h/models/group_test.py b/tests/unit/h/models/group_test.py
index 8fa3b3c876b..c8d4a9e2a14 100644
--- a/tests/unit/h/models/group_test.py
+++ b/tests/unit/h/models/group_test.py
@@ -236,7 +236,7 @@ def test_members_is_readonly(factories):
new_members = (factories.User.build(),)
with pytest.raises(
- AttributeError, match="^property 'members' of 'Group' object has no setter$"
+ AttributeError, match=r"^property 'members' of 'Group' object has no setter$"
):
group.members = new_members
@@ -246,7 +246,7 @@ def test_members_is_immutable(factories):
new_member = factories.User.build()
with pytest.raises(
- AttributeError, match="^'tuple' object has no attribute 'append'$"
+ AttributeError, match=r"^'tuple' object has no attribute 'append'$"
):
group.members.append(new_member)
diff --git a/tests/unit/h/models/user_test.py b/tests/unit/h/models/user_test.py
index 6fb57cce311..964373807c6 100644
--- a/tests/unit/h/models/user_test.py
+++ b/tests/unit/h/models/user_test.py
@@ -328,7 +328,7 @@ def test_it_is_readonly(self, factories):
new_groups = (factories.Group.build(),)
with pytest.raises(
- AttributeError, match="^property 'groups' of 'User' object has no setter$"
+ AttributeError, match=r"^property 'groups' of 'User' object has no setter$"
):
user.groups = new_groups
@@ -337,7 +337,7 @@ def test_it_is_immutable(self, factories):
new_group = factories.Group.build()
with pytest.raises(
- AttributeError, match="^'tuple' object has no attribute 'append'$"
+ AttributeError, match=r"^'tuple' object has no attribute 'append'$"
):
user.groups.append(new_group)
diff --git a/tests/unit/h/schemas/annotation_test.py b/tests/unit/h/schemas/annotation_test.py
index 2bf98198bbf..2c8757a37c8 100644
--- a/tests/unit/h/schemas/annotation_test.py
+++ b/tests/unit/h/schemas/annotation_test.py
@@ -835,15 +835,15 @@ def test_it_accepts_minimal_data(self, validate):
)
def test_it_raises_for_invalid_source_url(self, validate):
- with pytest.raises(ValidationError, match="not-a-url.*does not match"):
+ with pytest.raises(ValidationError, match=r"not-a-url.*does not match"):
validate({"not-a-url": {"url": "https://foobar.org"}})
def test_it_raises_for_invalid_dest_url(self, validate):
- with pytest.raises(ValidationError, match="also-not-a-url.*does not match"):
+ with pytest.raises(ValidationError, match=r"also-not-a-url.*does not match"):
validate({"https://example.com": {"url": "also-not-a-url"}})
def test_it_raises_if_new_url_missing(self, validate):
- with pytest.raises(ValidationError, match="url.*required"):
+ with pytest.raises(ValidationError, match=r"url.*required"):
validate({"https://example.com": {}})
@pytest.fixture
diff --git a/tests/unit/h/schemas/api/group_test.py b/tests/unit/h/schemas/api/group_test.py
index 30ed815010f..3d4260dd69f 100644
--- a/tests/unit/h/schemas/api/group_test.py
+++ b/tests/unit/h/schemas/api/group_test.py
@@ -238,7 +238,7 @@ def schema(self):
class TestCreateGroupAPISchema:
def test_it_raises_if_name_missing(self, schema):
- with pytest.raises(ValidationError, match="'name' is a required property.*"):
+ with pytest.raises(ValidationError, match=r"'name' is a required property.*"):
schema.validate({})
@pytest.fixture
diff --git a/tests/unit/h/schemas/api/user_test.py b/tests/unit/h/schemas/api/user_test.py
index 0aff8acbb78..8b48f06c883 100644
--- a/tests/unit/h/schemas/api/user_test.py
+++ b/tests/unit/h/schemas/api/user_test.py
@@ -133,20 +133,20 @@ def test_it_raises_when_email_missing_and_identities_empty(self, schema, payload
def test_it_raises_when_identities_not_an_array(self, schema, payload):
payload["identities"] = "dragnabit"
- with pytest.raises(ValidationError, match=".*identities.*is not of type.*"):
+ with pytest.raises(ValidationError, match=r".*identities.*is not of type.*"):
schema.validate(payload)
def test_it_raises_when_identities_items_not_objects(self, schema, payload):
payload["identities"] = ["flerp", "flop"]
- with pytest.raises(ValidationError, match=".*identities.*is not of type.*"):
+ with pytest.raises(ValidationError, match=r".*identities.*is not of type.*"):
schema.validate(payload)
def test_it_raises_when_provider_missing_in_identity(self, schema, payload):
payload["identities"] = [{"foo": "bar", "provider_unique_id": "flop"}]
with pytest.raises(
- ValidationError, match=".*provider'.*is a required property.*"
+ ValidationError, match=r".*provider'.*is a required property.*"
):
schema.validate(payload)
@@ -156,7 +156,7 @@ def test_it_raises_when_provider_unique_id_missing_in_identity(
payload["identities"] = [{"foo": "bar", "provider": "flop"}]
with pytest.raises(
- ValidationError, match=".*provider_unique_id'.*is a required property.*"
+ ValidationError, match=r".*provider_unique_id'.*is a required property.*"
):
schema.validate(payload)
@@ -164,7 +164,7 @@ def test_it_raises_if_identity_provider_is_not_a_string(self, schema, payload):
payload["identities"] = [{"provider_unique_id": "bar", "provider": 75}]
with pytest.raises(
- ValidationError, match=".*provider:.*is not of type.*string.*"
+ ValidationError, match=r".*provider:.*is not of type.*string.*"
):
schema.validate(payload)
@@ -174,7 +174,7 @@ def test_it_raises_if_identity_provider_unique_id_is_not_a_string(
payload["identities"] = [{"provider_unique_id": [], "provider": "hithere"}]
with pytest.raises(
- ValidationError, match=".*provider_unique_id:.*is not of type.*string.*"
+ ValidationError, match=r".*provider_unique_id:.*is not of type.*string.*"
):
schema.validate(payload)
diff --git a/tests/unit/h/schemas/forms/admin/group_test.py b/tests/unit/h/schemas/forms/admin/group_test.py
index 9eaa1cd2b4f..481db69b8ef 100644
--- a/tests/unit/h/schemas/forms/admin/group_test.py
+++ b/tests/unit/h/schemas/forms/admin/group_test.py
@@ -34,27 +34,27 @@ def test_it_raises_if_name_too_short(self, group_data, bound_schema):
too_short_name = "a" * (GROUP_NAME_MIN_LENGTH - 1)
group_data["name"] = too_short_name
- with pytest.raises(colander.Invalid, match=".*name.*"):
+ with pytest.raises(colander.Invalid, match=r".*name.*"):
bound_schema.deserialize(group_data)
def test_it_raises_if_name_too_long(self, group_data, bound_schema):
too_long_name = "a" * (GROUP_NAME_MAX_LENGTH + 1)
group_data["name"] = too_long_name
- with pytest.raises(colander.Invalid, match=".*name.*"):
+ with pytest.raises(colander.Invalid, match=r".*name.*"):
bound_schema.deserialize(group_data)
def test_it_raises_if_description_too_long(self, group_data, bound_schema):
too_long_description = "a" * (GROUP_DESCRIPTION_MAX_LENGTH + 1)
group_data["description"] = too_long_description
- with pytest.raises(colander.Invalid, match=".*description.*"):
+ with pytest.raises(colander.Invalid, match=r".*description.*"):
bound_schema.deserialize(group_data)
def test_it_raises_if_group_type_invalid(self, group_data, bound_schema):
group_data["group_type"] = "foobarbazding"
- with pytest.raises(colander.Invalid, match=".*group_type.*"):
+ with pytest.raises(colander.Invalid, match=r".*group_type.*"):
bound_schema.deserialize(group_data)
@pytest.mark.parametrize("required_field", ("name", "group_type", "creator"))
@@ -79,7 +79,7 @@ def test_it_allows_when_optional_field_missing(
)
def test_it_raises_if_origin_invalid(self, group_data, bound_schema, invalid_scope):
group_data["scopes"] = [invalid_scope]
- with pytest.raises(colander.Invalid, match="scope.*must be a complete URL"):
+ with pytest.raises(colander.Invalid, match=r"scope.*must be a complete URL"):
bound_schema.deserialize(group_data)
def test_it_allows_no_scopes(self, group_data, bound_schema):
@@ -120,7 +120,7 @@ def test_it_raises_if_member_invalid(self, group_data, bound_schema, user_servic
user_service.fetch.return_value = None
group_data["members"] = ["valid_user", "invalid_user"]
- with pytest.raises(colander.Invalid, match="members.1"):
+ with pytest.raises(colander.Invalid, match=r"members\.1"):
bound_schema.deserialize(group_data)
def test_it_passes_through_the_authority_when_checking_users(
diff --git a/tests/unit/h/security/predicates_test.py b/tests/unit/h/security/predicates_test.py
index 642c27c9211..6385057eee2 100644
--- a/tests/unit/h/security/predicates_test.py
+++ b/tests/unit/h/security/predicates_test.py
@@ -1036,7 +1036,7 @@ def test_it_crashes_if_new_roles_is_not_set(self, identity):
with pytest.raises(
AssertionError,
- match="^new_roles must be set before checking permissions$",
+ match=r"^new_roles must be set before checking permissions$",
):
predicates.group_member_edit(identity, context)
diff --git a/tests/unit/h/services/group_create_test.py b/tests/unit/h/services/group_create_test.py
index c1f95ed4b75..f539ac99952 100644
--- a/tests/unit/h/services/group_create_test.py
+++ b/tests/unit/h/services/group_create_test.py
@@ -544,7 +544,7 @@ def creator(factories):
return factories.User(username="group_creator")
-class GroupScopeWithOrigin(Matcher):
+class GroupScopeWithOrigin(Matcher): # noqa: PLW1641
"""Matches any GroupScope with the given origin."""
repr_attrs = ("origin",)
diff --git a/tests/unit/h/services/user_unique_test.py b/tests/unit/h/services/user_unique_test.py
index 6b8544996b4..90fe97a0620 100644
--- a/tests/unit/h/services/user_unique_test.py
+++ b/tests/unit/h/services/user_unique_test.py
@@ -45,7 +45,7 @@ def test_it_raises_if_identities_uniqueness_violated(self, svc, pyramid_request)
with pytest.raises(
DuplicateUserError,
- match=".*provider 'provider_a' and unique id '123' already exists",
+ match=r".*provider 'provider_a' and unique id '123' already exists",
):
svc.ensure_unique(
{"identities": [dupe_identity]},
@@ -59,7 +59,7 @@ def test_it_raises_if_identities_uniqueness_violated_at_different_authority(
dupe_identity = {"provider": "provider_a", "provider_unique_id": "123"}
with pytest.raises(
DuplicateUserError,
- match=".*provider 'provider_a' and unique id '123' already exists",
+ match=r".*provider 'provider_a' and unique id '123' already exists",
):
svc.ensure_unique({"identities": [dupe_identity]}, authority="foo.com")
@@ -128,7 +128,7 @@ def test_it_combines_error_messages(self, svc, user, pyramid_request):
"provider": user.identities[0].provider,
"provider_unique_id": user.identities[0].provider_unique_id,
}
- with pytest.raises(DuplicateUserError, match=".*email.*username.*provider"):
+ with pytest.raises(DuplicateUserError, match=r".*email.*username.*provider"):
svc.ensure_unique(
{
"email": user.email,
diff --git a/tests/unit/h/services/user_update_test.py b/tests/unit/h/services/user_update_test.py
index 66cdced6443..a3dd6d8f808 100644
--- a/tests/unit/h/services/user_update_test.py
+++ b/tests/unit/h/services/user_update_test.py
@@ -51,7 +51,7 @@ def test_it_raises_ValidationError_if_email_fails_model_validation(
user = factories.User()
with pytest.raises(
- ValidationError, match="email must be less than.*characters long"
+ ValidationError, match=r"email must be less than.*characters long"
):
svc.update(user, email="o" * 150)
@@ -61,7 +61,7 @@ def test_it_raises_ValidationError_if_username_fails_model_validation(
user = factories.User()
with pytest.raises(
- ValidationError, match="username must be between.*characters long"
+ ValidationError, match=r"username must be between.*characters long"
):
svc.update(user, username="lo")
diff --git a/tests/unit/h/streamer/tweens_test.py b/tests/unit/h/streamer/tweens_test.py
index 0a560691280..a3687616d4c 100644
--- a/tests/unit/h/streamer/tweens_test.py
+++ b/tests/unit/h/streamer/tweens_test.py
@@ -23,7 +23,7 @@ def test_it_closes_the_db_session_if_an_exception_is_raised(
):
handler.side_effect = RuntimeError("test_error")
- with pytest.raises(RuntimeError, match="^test_error$"):
+ with pytest.raises(RuntimeError, match=r"^test_error$"):
close_db_session_tween(pyramid_request)
pyramid_request.db.close.assert_called_once_with()
diff --git a/tests/unit/h/traversal/group_membership_test.py b/tests/unit/h/traversal/group_membership_test.py
index 1b1427963ae..dd8150e1bed 100644
--- a/tests/unit/h/traversal/group_membership_test.py
+++ b/tests/unit/h/traversal/group_membership_test.py
@@ -78,7 +78,7 @@ def test_when_no_matching_group(
pyramid_request.method = request_method
group_service.fetch.return_value = None
- with pytest.raises(HTTPNotFound, match="Group not found: sentinel.pubid"):
+ with pytest.raises(HTTPNotFound, match=r"Group not found: sentinel\.pubid"):
group_membership_api_factory(pyramid_request)
@pytest.mark.parametrize("request_method", ["GET", "POST", "PATCH", "DELETE"])
@@ -86,7 +86,7 @@ def test_when_no_matching_user(self, user_service, pyramid_request, request_meth
pyramid_request.method = request_method
user_service.fetch.return_value = None
- with pytest.raises(HTTPNotFound, match="User not found: sentinel.userid"):
+ with pytest.raises(HTTPNotFound, match=r"User not found: sentinel\.userid"):
group_membership_api_factory(pyramid_request)
@pytest.mark.parametrize("request_method", ["GET", "POST", "PATCH", "DELETE"])
@@ -94,7 +94,7 @@ def test_when_invalid_userid(self, user_service, pyramid_request, request_method
pyramid_request.method = request_method
user_service.fetch.side_effect = InvalidUserId(sentinel.userid)
- with pytest.raises(HTTPNotFound, match="User not found: sentinel.userid"):
+ with pytest.raises(HTTPNotFound, match=r"User not found: sentinel\.userid"):
group_membership_api_factory(pyramid_request)
@pytest.mark.parametrize("request_method", ["GET", "PATCH", "DELETE"])
diff --git a/tests/unit/h/views/accounts_test.py b/tests/unit/h/views/accounts_test.py
index 350e35fd450..b46cda0a174 100644
--- a/tests/unit/h/views/accounts_test.py
+++ b/tests/unit/h/views/accounts_test.py
@@ -1607,7 +1607,7 @@ def login(patch):
return patch("h.views.accounts.login")
-class PostItemsMatcher:
+class PostItemsMatcher: # noqa: PLW1641
def __init__(self, items):
self.items = items
diff --git a/tests/unit/h/views/api/group_annotations_test.py b/tests/unit/h/views/api/group_annotations_test.py
index 261ca97b2c3..0407d52e22d 100644
--- a/tests/unit/h/views/api/group_annotations_test.py
+++ b/tests/unit/h/views/api/group_annotations_test.py
@@ -125,7 +125,7 @@ def test_it_filters_by_moderation_status(
def test_when_no_context_group(self, context, pyramid_request):
context.group = None
- with pytest.raises(AssertionError, match="^Group is required$"):
+ with pytest.raises(AssertionError, match=r"^Group is required$"):
list_annotations(context, pyramid_request)
def test_when_pagination_params_invalid(
diff --git a/tests/unit/h/views/api/group_members_test.py b/tests/unit/h/views/api/group_members_test.py
index 73cf686197f..382bfd71f69 100644
--- a/tests/unit/h/views/api/group_members_test.py
+++ b/tests/unit/h/views/api/group_members_test.py
@@ -204,7 +204,7 @@ def test_it_when_a_conflicting_membership_already_exists(
"test_error_message"
)
- with pytest.raises(HTTPConflict, match="^test_error_message$"):
+ with pytest.raises(HTTPConflict, match=r"^test_error_message$"):
views.add_member(context, pyramid_request)
def test_it_errors_if_the_request_isnt_valid_JSON(
diff --git a/tests/unit/h/views/api/groups_test.py b/tests/unit/h/views/api/groups_test.py
index 6c7c5550d3f..1f082f3361f 100644
--- a/tests/unit/h/views/api/groups_test.py
+++ b/tests/unit/h/views/api/groups_test.py
@@ -256,7 +256,7 @@ def test_it_with_duplicate_groupid(
group_service.fetch.return_value = sentinel.duplicate_group
with pytest.raises(
- HTTPConflict, match="group with groupid 'sentinel.groupid' already exists"
+ HTTPConflict, match=r"group with groupid 'sentinel\.groupid' already exists"
):
views.create(pyramid_request)
@@ -402,7 +402,7 @@ def test_it_with_duplicate_groupid(
group_service.fetch.return_value = sentinel.duplicate_group
with pytest.raises(
- HTTPConflict, match="group with groupid 'sentinel.groupid' already exists"
+ HTTPConflict, match=r"group with groupid 'sentinel\.groupid' already exists"
):
views.update(context, pyramid_request)
diff --git a/tests/unit/h/views/helpers_test.py b/tests/unit/h/views/helpers_test.py
index ec6fa82cc40..d39320a472e 100644
--- a/tests/unit/h/views/helpers_test.py
+++ b/tests/unit/h/views/helpers_test.py
@@ -32,7 +32,7 @@ def test_login(
helpers.remember.assert_called_once_with(pyramid_request, user.userid)
assert headers == sentinel.headers
- class LoginEventMatcher(Matcher):
+ class LoginEventMatcher(Matcher): # noqa: PLW1641
repr_attrs = ("request", "user")
def __init__(self, request, user):
diff --git a/tests/unit/h/views/oidc_test.py b/tests/unit/h/views/oidc_test.py
index fdeffb453e4..ced7527c83c 100644
--- a/tests/unit/h/views/oidc_test.py
+++ b/tests/unit/h/views/oidc_test.py
@@ -87,7 +87,7 @@ def test_connect_or_login(
def test_connect_or_login_with_unexpected_route_name(self, pyramid_request):
pyramid_request.matched_route.name = "unexpected"
- with pytest.raises(UnexpectedRouteError, match="^unexpected$"):
+ with pytest.raises(UnexpectedRouteError, match=r"^unexpected$"):
OIDCConnectAndLoginViews(pyramid_request).connect_or_login()
def test_notfound(self, pyramid_request):
@@ -151,7 +151,7 @@ def test_redirect_when_theres_no_state_in_the_session(self, pyramid_request, vie
def test_redirect_with_unexpected_route_name(self, pyramid_request, views):
pyramid_request.matched_route.name = "unexpected"
- with pytest.raises(UnexpectedRouteError, match="^unexpected$"):
+ with pytest.raises(UnexpectedRouteError, match=r"^unexpected$"):
views.redirect()
@pytest.mark.usefixtures("with_both_connect_and_login_actions")