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")