From fdef4a99a7854a1393873a0b7378a2a50d611c73 Mon Sep 17 00:00:00 2001 From: stumpylog <797416+stumpylog@users.noreply.github.com> Date: Thu, 27 Aug 2026 11:00:12 -0700 Subject: [PATCH] Chore: enable flake8-datetimez (DTZ) ruff rules Full category (10/10 codes are all default in ruff 0.16). Of 54 hits, 46 were in test fixture code constructing naive datetimes for comparison/input purposes only - added DTZ to the existing per-file-ignores for */tests/*.py alongside E501/SIM117. The 8 production hits: - documents/consumer.py, documents/views.py (index_last_modified): suppressed with noqa - timezone.make_aware() requires a naive datetime, so wrapping fromtimestamp() in tz= would break it - documents/double_sided.py (x2): switched to datetime.now(tz=UTC).timestamp() - behavior-identical since .timestamp() returns the same epoch value regardless of the attached tz, but now explicit - documents/views.py (x2, upload temp file mtime): suppressed with noqa - mktime() requires a local time tuple, so an aware/UTC now() would introduce a timezone-offset bug - documents/workflows/ai.py (AI suggested date parsing): suppressed with noqa - only .date() is used, time/tz is discarded - paperless_mail/mail.py (IMAP fetch date filter): switched date.today() to timezone.localdate(), which respects settings.TIME_ZONE instead of the system clock - a real correctness improvement when they differ - paperless_mail/views.py (placeholder name string): switched datetime.datetime.now() to timezone.now(), matching the app's existing aware-datetime convention --- pyproject.toml | 2 ++ src/documents/consumer.py | 2 +- src/documents/double_sided.py | 6 ++++-- src/documents/views.py | 8 +++++--- src/documents/workflows/ai.py | 2 +- src/paperless_mail/mail.py | 3 +-- src/paperless_mail/views.py | 3 +-- 7 files changed, 15 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f283c4124..dbabcdd7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -220,6 +220,7 @@ extend-select = [ "COM", # https://docs.astral.sh/ruff/rules/#flake8-commas-com "D419", # https://docs.astral.sh/ruff/rules/#pydocstyle-d "DJ", # https://docs.astral.sh/ruff/rules/#flake8-django-dj + "DTZ", # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz "EXE", # https://docs.astral.sh/ruff/rules/#flake8-executable-exe "FA", # https://docs.astral.sh/ruff/rules/#flake8-future-annotations-fa "FBT", # https://docs.astral.sh/ruff/rules/#flake8-boolean-trap-fbt @@ -304,6 +305,7 @@ per-file-ignores."*/migrations/*.py" = [ ] # Testing per-file-ignores."*/tests/*.py" = [ + "DTZ", "E501", "SIM117", ] diff --git a/src/documents/consumer.py b/src/documents/consumer.py index fc13c959f..a653a2f5c 100644 --- a/src/documents/consumer.py +++ b/src/documents/consumer.py @@ -856,7 +856,7 @@ class ConsumerPlugin( else: stats = Path(self.input_doc.original_file).stat() create_date = timezone.make_aware( - datetime.datetime.fromtimestamp(stats.st_mtime), + datetime.datetime.fromtimestamp(stats.st_mtime), # noqa: DTZ006 - make_aware() requires a naive datetime ) self.log.debug(f"Creation date from st_mtime: {create_date}") diff --git a/src/documents/double_sided.py b/src/documents/double_sided.py index 3c3ec4723..b4b62cfe6 100644 --- a/src/documents/double_sided.py +++ b/src/documents/double_sided.py @@ -78,7 +78,9 @@ class CollatePlugin(NoCleanupPluginMixin, NoSetupPluginMixin, ConsumeTaskPlugin) stats = staging.stat() # if the file is older than the timeout, we don't consider # it valid - if (dt.datetime.now().timestamp() - stats.st_mtime) > TIMEOUT_SECONDS: + if ( + dt.datetime.now(tz=dt.UTC).timestamp() - stats.st_mtime + ) > TIMEOUT_SECONDS: logger.warning("Outdated double sided staging file exists, deleting it") staging.unlink() else: @@ -134,7 +136,7 @@ class CollatePlugin(NoCleanupPluginMixin, NoSetupPluginMixin, ConsumeTaskPlugin) shutil.move(pdf_file, staging) # update access to modification time so we know if the file # is outdated when another file gets uploaded - timestamp = dt.datetime.now().timestamp() + timestamp = dt.datetime.now(tz=dt.UTC).timestamp() os.utime(staging, (timestamp, timestamp)) logger.info( "Got scan with odd numbered pages of double-sided scan, moved it to %s", diff --git a/src/documents/views.py b/src/documents/views.py index 211ee2ce5..aa94e7b50 100644 --- a/src/documents/views.py +++ b/src/documents/views.py @@ -2062,7 +2062,7 @@ class DocumentViewSet( doc_name, doc_data = serializer.validated_data.get("document") version_label = serializer.validated_data.get("version_label") - t = int(mktime(datetime.now().timetuple())) + t = int(mktime(datetime.now().timetuple())) # noqa: DTZ005 - mktime() requires a local time tuple settings.SCRATCH_DIR.mkdir(parents=True, exist_ok=True) @@ -3332,7 +3332,7 @@ class PostDocumentView(GenericAPIView[Any]): cf = serializer.validated_data.get("custom_fields") from_webui = serializer.validated_data.get("from_webui") - t = int(mktime(datetime.now().timetuple())) + t = int(mktime(datetime.now().timetuple())) # noqa: DTZ005 - mktime() requires a local time tuple settings.SCRATCH_DIR.mkdir(parents=True, exist_ok=True) @@ -5251,7 +5251,9 @@ class SystemStatusView(PassUserMixin): index_dir = settings.INDEX_DIR mtimes = [p.stat().st_mtime for p in index_dir.iterdir() if p.is_file()] index_last_modified = ( - make_aware(datetime.fromtimestamp(max(mtimes))) if mtimes else None + make_aware(datetime.fromtimestamp(max(mtimes))) # noqa: DTZ006 - make_aware() requires a naive datetime + if mtimes + else None ) except Exception: index_status = "ERROR" diff --git a/src/documents/workflows/ai.py b/src/documents/workflows/ai.py index 23ffbd71c..8122173c8 100644 --- a/src/documents/workflows/ai.py +++ b/src/documents/workflows/ai.py @@ -47,7 +47,7 @@ def resolve_date(dates: list[str]) -> date | None: """ for value in dates: try: - return datetime.strptime(value, "%Y-%m-%d").date() + return datetime.strptime(value, "%Y-%m-%d").date() # noqa: DTZ007 - only the calendar date is used, time/tz is discarded except (TypeError, ValueError): logger.debug("Ignoring unparsable suggested date %s", value) return None diff --git a/src/paperless_mail/mail.py b/src/paperless_mail/mail.py index 89df490c3..2a86f1973 100644 --- a/src/paperless_mail/mail.py +++ b/src/paperless_mail/mail.py @@ -7,7 +7,6 @@ import ssl import tempfile import traceback import unicodedata -from datetime import date from datetime import timedelta from fnmatch import fnmatch from pathlib import Path @@ -406,7 +405,7 @@ def make_criterias(rule: MailRule, *, supports_gmail_labels: bool): Returns criteria to be applied to MailBox.fetch for the given rule. """ - maximum_age = date.today() - timedelta(days=rule.maximum_age) + maximum_age = timezone.localdate() - timedelta(days=rule.maximum_age) criterias = {} if rule.maximum_age > 0: criterias["date_gte"] = maximum_age diff --git a/src/paperless_mail/views.py b/src/paperless_mail/views.py index 3faff5da5..bf4f6de3c 100644 --- a/src/paperless_mail/views.py +++ b/src/paperless_mail/views.py @@ -1,4 +1,3 @@ -import datetime import logging from datetime import timedelta from http import HTTPStatus @@ -87,7 +86,7 @@ class MailAccountViewSet(PassUserMixin, ModelViewSet[MailAccount]): @action(methods=["post"], detail=False) def test(self, request): logger = logging.getLogger("paperless_mail") - request.data["name"] = datetime.datetime.now().isoformat() + request.data["name"] = timezone.now().isoformat() serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) existing_account = None