diff --git a/pyproject.toml b/pyproject.toml index aaf196f12..f283c4124 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -281,6 +281,11 @@ extend-select = [ "T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20 "TC", # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc "TID", # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid + "TRY002", # https://docs.astral.sh/ruff/rules/#tryceratops-try + "TRY004", # https://docs.astral.sh/ruff/rules/#tryceratops-try + "TRY201", # https://docs.astral.sh/ruff/rules/#tryceratops-try + "TRY203", # https://docs.astral.sh/ruff/rules/#tryceratops-try + "TRY401", # https://docs.astral.sh/ruff/rules/#tryceratops-try "UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w "YTT", # https://docs.astral.sh/ruff/rules/#flake8-2020-ytt diff --git a/src/documents/bulk_edit.py b/src/documents/bulk_edit.py index 4053ee8cb..fe3b09259 100644 --- a/src/documents/bulk_edit.py +++ b/src/documents/bulk_edit.py @@ -507,8 +507,8 @@ def rotate( logger.info( f"Queued new rotated version for document {pair.root_doc.id} by {degrees} degrees", ) - except Exception as e: - logger.exception(f"Error rotating document {pair.root_doc.id}: {e}") + except Exception: + logger.exception(f"Error rotating document {pair.root_doc.id}") return "OK" @@ -554,9 +554,9 @@ def merge( affected_docs.append(doc.id) if handoff_asn is None and doc.archive_serial_number is not None: handoff_asn = doc.archive_serial_number - except Exception as e: + except Exception: logger.exception( - f"Error merging document {doc.id}, it will not be included in the merge: {e}", + f"Error merging document {doc.id}, it will not be included in the merge", ) if len(affected_docs) == 0: logger.warning("No documents were merged") @@ -805,8 +805,8 @@ def split( else: group(consume_tasks).delay() - except Exception as e: - logger.exception(f"Error splitting document {doc.id}: {e}") + except Exception: + logger.exception(f"Error splitting document {doc.id}") return "OK" @@ -858,8 +858,8 @@ def delete_pages( logger.info( f"Queued new version for document {pair.root_doc.id} after deleting pages {pages}", ) - except Exception as e: - logger.exception(f"Error deleting pages from document {pair.root_doc.id}: {e}") + except Exception: + logger.exception(f"Error deleting pages from document {pair.root_doc.id}") return "OK" @@ -986,7 +986,7 @@ def edit_pdf( group(consume_tasks).delay() except Exception as e: - logger.exception(f"Error editing document {pair.root_doc.id}: {e}") + logger.exception(f"Error editing document {pair.root_doc.id}") raise ValueError( f"An error occurred while editing the document: {e}", ) from e @@ -1097,7 +1097,7 @@ def remove_password( except Exception as e: logger.exception( - f"Error removing password from document {pair.root_doc.id}: {e}", + f"Error removing password from document {pair.root_doc.id}", ) raise ValueError( f"An error occurred while removing the password: {e}", diff --git a/src/documents/classifier.py b/src/documents/classifier.py index a5c272062..ba49f96a3 100644 --- a/src/documents/classifier.py +++ b/src/documents/classifier.py @@ -72,8 +72,8 @@ def load_classifier(*, raise_exception: bool = False) -> DocumentClassifier | No Path(settings.MODEL_FILE).unlink() classifier = None if raise_exception: - raise e - except ClassifierModelCorruptError as e: + raise + except ClassifierModelCorruptError: # there's something wrong with the model file. logger.exception( "Unrecoverable error while loading document " @@ -82,17 +82,17 @@ def load_classifier(*, raise_exception: bool = False) -> DocumentClassifier | No Path(settings.MODEL_FILE).unlink() classifier = None if raise_exception: - raise e - except OSError as e: + raise + except OSError: logger.exception("IO error while loading document classification model") classifier = None if raise_exception: - raise e - except Exception as e: # pragma: no cover + raise + except Exception: # pragma: no cover logger.exception("Unknown error while loading document classification model") classifier = None if raise_exception: - raise e + raise return classifier diff --git a/src/documents/matching.py b/src/documents/matching.py index 65f52d567..68c520320 100644 --- a/src/documents/matching.py +++ b/src/documents/matching.py @@ -30,6 +30,10 @@ if TYPE_CHECKING: logger = logging.getLogger("paperless.matching") +class UnsupportedWorkflowTriggerTypeError(Exception): + pass + + def log_reason( matching_model: MatchingModel | WorkflowTrigger, document: Document, @@ -691,7 +695,9 @@ def document_matches_workflow( ) else: # New trigger types need to be explicitly checked above - raise Exception(f"Trigger type {trigger_type} not yet supported") + raise UnsupportedWorkflowTriggerTypeError( + f"Trigger type {trigger_type} not yet supported", + ) if trigger_matched: logger.info(f"Document matched {trigger} from {workflow}") diff --git a/src/documents/plugins/date_parsing/__init__.py b/src/documents/plugins/date_parsing/__init__.py index a96f440a4..6f1a21152 100644 --- a/src/documents/plugins/date_parsing/__init__.py +++ b/src/documents/plugins/date_parsing/__init__.py @@ -43,8 +43,8 @@ def _discover_parser_class() -> type[DateParserPluginBase]: valid_plugins.append(ep) else: logger.warning(f"Plugin {ep.name} does not subclass DateParser.") - except Exception as e: - logger.exception(f"Unable to load date parser plugin {ep.name}: {e}") + except Exception: + logger.exception(f"Unable to load date parser plugin {ep.name}") if not valid_plugins: return RegexDateParserPlugin diff --git a/src/documents/plugins/date_parsing/base.py b/src/documents/plugins/date_parsing/base.py index fd45ceec8..280b69500 100644 --- a/src/documents/plugins/date_parsing/base.py +++ b/src/documents/plugins/date_parsing/base.py @@ -91,8 +91,8 @@ class DateParserPluginBase(ABC): }, locales=self.config.languages, ) - except Exception as e: - logger.exception(f"Error while parsing date string '{date_string}': {e}") + except Exception: + logger.exception(f"Error while parsing date string '{date_string}'") return None def _filter_date( diff --git a/src/documents/regex.py b/src/documents/regex.py index 0c5f4aac6..9fc013514 100644 --- a/src/documents/regex.py +++ b/src/documents/regex.py @@ -59,11 +59,10 @@ def safe_regex_match(pattern: str, text: str, *, flags: int = 0): try: validate_regex_pattern(pattern) compiled = regex.compile(pattern, flags=flags) - except (regex.error, ValueError) as exc: + except (regex.error, ValueError): logger.exception( - "Error while processing regular expression %s: %s", + "Error while processing regular expression %s", textwrap.shorten(pattern, width=80, placeholder="…"), - exc, ) return None @@ -86,11 +85,10 @@ def safe_regex_sub(pattern: str, repl: str, text: str, *, flags: int = 0) -> str try: validate_regex_pattern(pattern) compiled = regex.compile(pattern, flags=flags) - except (regex.error, ValueError) as exc: + except (regex.error, ValueError): logger.exception( - "Error while processing regular expression %s: %s", + "Error while processing regular expression %s", textwrap.shorten(pattern, width=80, placeholder="…"), - exc, ) return None diff --git a/src/documents/serialisers.py b/src/documents/serialisers.py index 1fa0fb632..5cfe3741d 100644 --- a/src/documents/serialisers.py +++ b/src/documents/serialisers.py @@ -730,7 +730,7 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer): self.instance.clean() except ValidationError as e: logger.debug("Tag parent validation failed: %s", e) - raise e + raise finally: self.instance.tn_parent = original_parent else: @@ -740,7 +740,7 @@ class TagSerializer(MatchingModelSerializer, OwnedObjectSerializer): temp.clean() except ValidationError as e: logger.debug("Tag parent validation failed: %s", e) - raise e + raise return super().validate(attrs) @@ -1860,8 +1860,8 @@ class BulkEditSerializer( if isinstance(custom_fields, dict): try: ids = [int(i[0]) for i in custom_fields.items()] - except Exception as e: - logger.exception(f"Error validating custom fields: {e}") + except Exception: + logger.exception("Error validating custom fields") raise serializers.ValidationError( f"{name} must be a list of integers or a dict of id:value pairs, see the log for details", ) diff --git a/src/documents/tasks.py b/src/documents/tasks.py index 1ebf95bb7..821fe661e 100644 --- a/src/documents/tasks.py +++ b/src/documents/tasks.py @@ -261,7 +261,7 @@ def consume_file( ) except Exception as e: - logger.exception(f"{plugin_name} failed: {e}") + logger.exception(f"{plugin_name} failed") status_mgr.send_progress( ProgressStatusOptions.FAILED, f"{e}", @@ -495,8 +495,8 @@ def empty_trash(doc_ids=None) -> None: content_type=ContentType.objects.get_for_model(Document), object_id__in=deleted_document_ids, ).delete() - except Exception as e: # pragma: no cover - logger.exception(f"Error while emptying trash: {e}") + except Exception: # pragma: no cover + logger.exception("Error while emptying trash") finally: models.signals.post_delete.disconnect( cleanup_document_deletion, @@ -832,9 +832,8 @@ def build_share_link_bundle(bundle_id: int) -> None: logger.info("Built share link bundle %s", bundle.pk) except Exception as exc: logger.exception( - "Failed to build share link bundle %s: %s", + "Failed to build share link bundle %s", bundle_id, - exc, ) bundle.status = ShareLinkBundle.Status.FAILED bundle.last_error = { diff --git a/src/documents/templating/workflows.py b/src/documents/templating/workflows.py index f1c94913a..390d72a8b 100644 --- a/src/documents/templating/workflows.py +++ b/src/documents/templating/workflows.py @@ -138,9 +138,9 @@ def parse_w_workflow_placeholders( # We're good! return rendered_template - except UndefinedError as e: + except UndefinedError: # The undefined class logs this already for us - raise e + raise except TemplateSyntaxError as e: logger.warning(f"Template syntax error in title generation: {e}") except SecurityError as e: @@ -150,5 +150,5 @@ def parse_w_workflow_placeholders( logger.warning( f"Invalid title format '{text}', workflow not applied: {e}", ) - raise e + raise return None diff --git a/src/documents/tests/date_parsing/test_date_parsing.py b/src/documents/tests/date_parsing/test_date_parsing.py index f43303d22..dcf71ef36 100644 --- a/src/documents/tests/date_parsing/test_date_parsing.py +++ b/src/documents/tests/date_parsing/test_date_parsing.py @@ -296,7 +296,7 @@ class TestRegexDateParser: # simulate parse failure for malformed input if "99/99/9999" in date_string or "bad date" in date_string: - raise Exception("parse failed for malformed date") + raise Exception("parse failed for malformed date") # noqa: TRY002 - simulates a generic parser failure return None diff --git a/src/documents/tests/test_consumer.py b/src/documents/tests/test_consumer.py index 166978e94..a3d72d7c0 100644 --- a/src/documents/tests/test_consumer.py +++ b/src/documents/tests/test_consumer.py @@ -137,7 +137,7 @@ class FaultyParser(_BaseNewStyleParser): class FaultyGenericExceptionParser(_BaseNewStyleParser): def parse(self, document_path, mime_type, *, produce_archive: bool = True) -> None: - raise Exception("Generic exception.") + raise Exception("Generic exception.") # noqa: TRY002 - deliberately not a ParseError def fake_magic_from_file(file, *, mime=False): # NOSONAR diff --git a/src/documents/tests/test_workflows.py b/src/documents/tests/test_workflows.py index 8c7bb0114..9bf04e0e4 100644 --- a/src/documents/tests/test_workflows.py +++ b/src/documents/tests/test_workflows.py @@ -44,6 +44,7 @@ from documents import tasks from documents.data_models import ConsumableDocument from documents.data_models import DocumentMetadataOverrides from documents.data_models import DocumentSource +from documents.matching import UnsupportedWorkflowTriggerTypeError from documents.matching import document_matches_workflow from documents.matching import existing_document_matches_workflow from documents.matching import prefilter_documents_by_workflowtrigger @@ -2851,7 +2852,13 @@ class TestWorkflows( doc = Document.objects.create( title="test", ) - self.assertRaises(Exception, document_matches_workflow, doc, w, 99) # noqa: B017 - raises a bare Exception for unsupported trigger types + self.assertRaises( + UnsupportedWorkflowTriggerTypeError, + document_matches_workflow, + doc, + w, + 99, + ) def test_removal_action_document_updated_workflow(self) -> None: """ diff --git a/src/documents/views.py b/src/documents/views.py index cd1f1fd4f..211ee2ce5 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -1580,19 +1580,16 @@ class DocumentViewSet( except ValueError as exc: logger.exception( "Invalid AI configuration while generating suggestions for " - "document %s: %s", + "document %s", doc.pk, - exc, ) raise ValidationError( {"ai": [_("Invalid AI configuration.")]}, ) from exc - except LLMTimeoutError as exc: + except LLMTimeoutError: logger.exception( - "AI backend timed out while generating suggestions for " - "document %s: %s", + "AI backend timed out while generating suggestions for document %s", doc.pk, - exc, ) return Response( {"ai": [_("AI backend request timed out.")]}, @@ -5183,11 +5180,11 @@ class SystemStatusView(PassUserMixin): f"{m.app}.{m.name}" for m in MigrationRecorder.Migration.objects.all().order_by("id") ] - except Exception as e: # pragma: no cover + except Exception: # pragma: no cover applied_migrations = [] db_status = "ERROR" logger.exception( - f"System status detected a possible problem while connecting to the database: {e}", + "System status detected a possible problem while connecting to the database", ) db_error = "Error connecting to database, check logs for more detail." @@ -5203,10 +5200,10 @@ class SystemStatusView(PassUserMixin): try: client.ping() redis_status = "OK" - except Exception as e: + except Exception: redis_status = "ERROR" logger.exception( - f"System status detected a possible problem while connecting to redis: {e}", + "System status detected a possible problem while connecting to redis", ) redis_error = "Error connecting to redis, check logs for more detail." @@ -5236,10 +5233,10 @@ class SystemStatusView(PassUserMixin): else: celery_active = "WARNING" celery_error = "Celery worker responded unexpectedly." - except Exception as e: + except Exception: celery_active = "ERROR" logger.exception( - f"System status detected a possible problem while connecting to celery: {e}", + "System status detected a possible problem while connecting to celery", ) celery_error = "Error connecting to celery, check logs for more detail." @@ -5256,11 +5253,11 @@ class SystemStatusView(PassUserMixin): index_last_modified = ( make_aware(datetime.fromtimestamp(max(mtimes))) if mtimes else None ) - except Exception as e: + except Exception: index_status = "ERROR" index_error = "Error opening index, check logs for more detail." logger.exception( - f"System status detected a possible problem while opening the index: {e}", + "System status detected a possible problem while opening the index", ) index_last_modified = None diff --git a/src/documents/workflows/actions.py b/src/documents/workflows/actions.py index 84a833d85..3394418e3 100644 --- a/src/documents/workflows/actions.py +++ b/src/documents/workflows/actions.py @@ -179,9 +179,9 @@ def execute_email_action( f"Sent {n_messages} notification email(s) to {action.email.to}", extra={"group": logging_group}, ) - except Exception as e: + except Exception: logger.exception( - f"Error occurred sending notification email: {e}", + "Error occurred sending notification email", extra={"group": logging_group}, ) @@ -265,9 +265,9 @@ def execute_webhook_action( f"Webhook to {action.webhook.url} queued", extra={"group": logging_group}, ) - except Exception as e: + except Exception: logger.exception( - f"Error occurred sending webhook: {e}", + "Error occurred sending webhook", extra={"group": logging_group}, ) diff --git a/src/documents/workflows/webhooks.py b/src/documents/workflows/webhooks.py index e2eff558d..6b6a3cf3a 100644 --- a/src/documents/workflows/webhooks.py +++ b/src/documents/workflows/webhooks.py @@ -70,6 +70,6 @@ def send_webhook( logger.error( f"Failed attempt sending webhook to {url}: {e}", ) - raise e + raise finally: transport.close() diff --git a/src/paperless/parsers/remote.py b/src/paperless/parsers/remote.py index 35c4e9cb6..6821dea6e 100644 --- a/src/paperless/parsers/remote.py +++ b/src/paperless/parsers/remote.py @@ -505,7 +505,7 @@ class RemoteDocumentParser: return result.content except Exception as e: - logger.exception("Azure AI Vision parsing failed: %s", e) + logger.exception("Azure AI Vision parsing failed") raise ParseError(f"Azure AI Vision parsing failed: {e}") from e finally: diff --git a/src/paperless_ai/chat.py b/src/paperless_ai/chat.py index 03e64af36..d756b713d 100644 --- a/src/paperless_ai/chat.py +++ b/src/paperless_ai/chat.py @@ -103,8 +103,8 @@ def stream_chat_with_documents( documents, output_language=output_language, ) - except Exception as e: - logger.exception("Failed to stream document chat response: %s", e) + except Exception: + logger.exception("Failed to stream document chat response") yield CHAT_ERROR_MESSAGE diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index 0af5eb9c7..89df490c3 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -723,9 +723,9 @@ class MailAccountHandler(LoggingMixin): f"Rule {rule}: Stopping processing rules due to stop_processing flag", ) break - except Exception as e: + except Exception: self.log.exception( - f"Rule {rule}: Error while processing rule: {e}", + f"Rule {rule}: Error while processing rule", ) except MailError: raise @@ -874,9 +874,9 @@ class MailAccountHandler(LoggingMixin): total_processed_files += processed_files mails_processed += 1 - except Exception as e: + except Exception: self.log.exception( - f"Rule {rule}: Error while processing mail {message.uid}: {e}", + f"Rule {rule}: Error while processing mail {message.uid}", ) self.log.debug(f"Rule {rule}: Processed {mails_processed} matching mail(s)") diff --git a/src/paperless_mail/preprocessor.py b/src/paperless_mail/preprocessor.py index 6afbbb4f8..514c03504 100644 --- a/src/paperless_mail/preprocessor.py +++ b/src/paperless_mail/preprocessor.py @@ -11,6 +11,10 @@ from imap_tools import MailMessage from documents.loggers import LoggingMixin +class MailDecryptionError(Exception): + pass + + class MailMessagePreprocessor(abc.ABC): """ Defines the interface for preprocessors that alter messages before they are handled in MailAccountHandler @@ -69,7 +73,7 @@ class MailMessageDecryptor(MailMessagePreprocessor, LoggingMixin): f"Message decryption failed with status message " f"{decrypted_raw_message.status}", ) - raise Exception( + raise MailDecryptionError( f"Decryption failed: {decrypted_raw_message.status}, {decrypted_raw_message.stderr}", ) self.log.debug("Message decrypted successfully.") diff --git a/src/paperless_mail/tests/test_mail.py b/src/paperless_mail/tests/test_mail.py index f0183c1f5..e57cb6ba3 100644 --- a/src/paperless_mail/tests/test_mail.py +++ b/src/paperless_mail/tests/test_mail.py @@ -214,7 +214,7 @@ class BogusMailBox(AbstractContextManager): ) self.messages = list(filter(lambda m: m.uid not in uid_list, self.messages)) else: - raise Exception + raise Exception # noqa: TRY002 - test double simulating a generic mailbox failure def fake_magic_from_buffer(buffer, *, mime=False): diff --git a/src/paperless_mail/tests/test_preprocessor.py b/src/paperless_mail/tests/test_preprocessor.py index 581abb875..d5cef448d 100644 --- a/src/paperless_mail/tests/test_preprocessor.py +++ b/src/paperless_mail/tests/test_preprocessor.py @@ -14,6 +14,7 @@ from imap_tools import MailMessage from paperless_mail.mail import MailAccountHandler from paperless_mail.models import MailRule +from paperless_mail.preprocessor import MailDecryptionError from paperless_mail.preprocessor import MailMessageDecryptor from paperless_mail.tests.factories import MailAccountFactory from paperless_mail.tests.test_mail import TestMail @@ -82,7 +83,9 @@ class MessageEncryptor: armor=True, ) if not encrypted_data.ok: - raise Exception(f"Encryption failed: {encrypted_data.stderr}") + raise Exception( # noqa: TRY002 - test fixture setup, not production code + f"Encryption failed: {encrypted_data.stderr}", + ) encrypted_email_content = encrypted_data.data new_email = MIMEMultipart("encrypted", protocol="application/pgp-encrypted") @@ -184,7 +187,11 @@ class TestMailMessageGpgDecryptor(TestMail): EMAIL_GNUPG_HOME=empty_gpg_home, ): message_decryptor = MailMessageDecryptor() - self.assertRaises(Exception, message_decryptor.run, encrypted_message) # noqa: B017 - raises a bare Exception on decryption failure + self.assertRaises( + MailDecryptionError, + message_decryptor.run, + encrypted_message, + ) finally: # Clean up the temporary GPG home used only by this test try: