mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-09-01 23:47:15 +00:00
Chore: enable tryceratops (TRY002/004/201/203/401) ruff rules
Only the 5 default-subset codes; the rest of tryceratops is opt-in. - 9 TRY201 (raise e -> raise) autofixed, preserving the traceback identically while dropping the redundant exception name - 26 TRY401 (redundant exception object passed to logger.exception, which already logs it) fixed by removing the duplicate from the message; three sites still needed the exception object for something else (re-raising, or a separate logger.error call) and kept their binding - 6 TRY002 (raise bare Exception): 2 production sites (documents/ matching.py, paperless_mail/preprocessor.py) got dedicated exception classes, with their tests narrowed to match instead of asserting a blind Exception; the other 4 are deliberate generic failures in test doubles/fixtures, suppressed with noqa
This commit is contained in:
@@ -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
|
||||
|
||||
+10
-10
@@ -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}",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
+11
-14
@@ -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
|
||||
|
||||
|
||||
@@ -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},
|
||||
)
|
||||
|
||||
|
||||
@@ -70,6 +70,6 @@ def send_webhook(
|
||||
logger.error(
|
||||
f"Failed attempt sending webhook to {url}: {e}",
|
||||
)
|
||||
raise e
|
||||
raise
|
||||
finally:
|
||||
transport.close()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user