From 7dfb8126c4b72f69931aa013b52b763fd20aa366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20I=C3=9Fbr=C3=BCcker?= Date: Sun, 4 Jan 2026 13:16:33 +0100 Subject: [PATCH] Remove python-dateutil dependency (#1265) --- .../tests/test_bookmarks_list_template.py | 11 +++--- bookmarks/tests/test_utils.py | 16 -------- bookmarks/utils.py | 37 ++++++++++++++++--- pyproject.toml | 1 - uv.lock | 14 ------- 5 files changed, 37 insertions(+), 42 deletions(-) diff --git a/bookmarks/tests/test_bookmarks_list_template.py b/bookmarks/tests/test_bookmarks_list_template.py index 17cfc95..04d2aa3 100644 --- a/bookmarks/tests/test_bookmarks_list_template.py +++ b/bookmarks/tests/test_bookmarks_list_template.py @@ -1,6 +1,5 @@ import datetime -from dateutil.relativedelta import relativedelta from django.contrib.auth.models import AnonymousUser from django.http import HttpResponse from django.template import RequestContext, Template @@ -285,7 +284,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin): self, date_display_setting: str, web_archive_url: str = "" ): bookmark = self.setup_bookmark() - bookmark.date_added = timezone.now() - relativedelta(days=8) + bookmark.date_added = timezone.now() - datetime.timedelta(days=8) bookmark.web_archive_snapshot_url = web_archive_url bookmark.save() user = self.get_or_create_test_user() @@ -554,7 +553,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin): def test_web_archive_link_target_should_be_blank_by_default(self): bookmark = self.setup_bookmark() - bookmark.date_added = timezone.now() - relativedelta(days=8) + bookmark.date_added = timezone.now() - datetime.timedelta(days=8) bookmark.web_archive_snapshot_url = "https://example.com" bookmark.save() @@ -570,7 +569,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin): profile.save() bookmark = self.setup_bookmark() - bookmark.date_added = timezone.now() - relativedelta(days=8) + bookmark.date_added = timezone.now() - datetime.timedelta(days=8) bookmark.web_archive_snapshot_url = "https://example.com" bookmark.save() @@ -998,7 +997,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin): profile.save() bookmark = self.setup_bookmark() - bookmark.date_added = timezone.now() - relativedelta(days=8) + bookmark.date_added = timezone.now() - datetime.timedelta(days=8) bookmark.web_archive_snapshot_url = ( "https://web.archive.org/web/20230531200136/https://example.com" ) @@ -1099,7 +1098,7 @@ class BookmarkListTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin): def test_no_actions_rendered_when_is_preview(self): bookmark = self.setup_bookmark() - bookmark.date_added = timezone.now() - relativedelta(days=8) + bookmark.date_added = timezone.now() - datetime.timedelta(days=8) bookmark.web_archive_snapshot_url = "https://example.com" bookmark.save() diff --git a/bookmarks/tests/test_utils.py b/bookmarks/tests/test_utils.py index ae67b98..867faf0 100644 --- a/bookmarks/tests/test_utils.py +++ b/bookmarks/tests/test_utils.py @@ -1,6 +1,5 @@ from unittest.mock import patch -from dateutil.relativedelta import relativedelta from django.test import TestCase from django.utils import timezone @@ -152,30 +151,15 @@ class UtilsTestCase(TestCase): def test_parse_timestamp_parses_millisecond_timestamps(self): now = timezone.now().replace(microsecond=0) - fifty_years_ago = now - relativedelta(year=50) - fifty_years_from_now = now + relativedelta(year=50) - self.verify_timestamp(now) - self.verify_timestamp(fifty_years_ago) - self.verify_timestamp(fifty_years_from_now) def test_parse_timestamp_parses_microsecond_timestamps(self): now = timezone.now().replace(microsecond=0) - fifty_years_ago = now - relativedelta(year=50) - fifty_years_from_now = now + relativedelta(year=50) - self.verify_timestamp(now, 1000) - self.verify_timestamp(fifty_years_ago, 1000) - self.verify_timestamp(fifty_years_from_now, 1000) def test_parse_timestamp_parses_nanosecond_timestamps(self): now = timezone.now().replace(microsecond=0) - fifty_years_ago = now - relativedelta(year=50) - fifty_years_from_now = now + relativedelta(year=50) - self.verify_timestamp(now, 1000000) - self.verify_timestamp(fifty_years_ago, 1000000) - self.verify_timestamp(fifty_years_from_now, 1000000) def test_parse_timestamp_fails_for_out_of_range_timestamp(self): now = timezone.now().replace(microsecond=0) diff --git a/bookmarks/utils.py b/bookmarks/utils.py index 3b1fb7f..08d42ad 100644 --- a/bookmarks/utils.py +++ b/bookmarks/utils.py @@ -3,8 +3,8 @@ import logging import re import unicodedata import urllib.parse +from dataclasses import dataclass -from dateutil.relativedelta import relativedelta from django.conf import settings from django.http import HttpResponseRedirect from django.template.defaultfilters import pluralize @@ -33,13 +33,40 @@ weekday_names = { } +@dataclass +class DateDelta: + years: int + months: int + weeks: int + + +def _calculate_date_delta( + now: datetime.datetime, value: datetime.datetime +) -> DateDelta: + """Calculate the difference between two datetimes in years, months, and weeks.""" + # Full calendar years + years = now.year - value.year + if (now.month, now.day) < (value.month, value.day): + years -= 1 + + # Full calendar months + months = (now.year - value.year) * 12 + (now.month - value.month) + if now.day < value.day: + months -= 1 + + # Weeks from total days + weeks = (now - value).days // 7 + + return DateDelta(years=max(0, years), months=max(0, months), weeks=max(0, weeks)) + + def humanize_absolute_date( value: datetime.datetime, now: datetime.datetime | None = None ): if not now: now = timezone.now() - delta = relativedelta(now, value) - yesterday = now - relativedelta(days=1) + delta = _calculate_date_delta(now, value) + yesterday = now - datetime.timedelta(days=1) is_older_than_a_week = delta.years > 0 or delta.months > 0 or delta.weeks > 0 @@ -58,7 +85,7 @@ def humanize_relative_date( ): if not now: now = timezone.now() - delta = relativedelta(now, value) + delta = _calculate_date_delta(now, value) if delta.years > 0: return f"{delta.years} year{pluralize(delta.years)} ago" @@ -67,7 +94,7 @@ def humanize_relative_date( elif delta.weeks > 0: return f"{delta.weeks} week{pluralize(delta.weeks)} ago" else: - yesterday = now - relativedelta(days=1) + yesterday = now - datetime.timedelta(days=1) if value.day == now.day: return "Today" elif value.day == yesterday.day: diff --git a/pyproject.toml b/pyproject.toml index 0112436..4c3db96 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,6 @@ dependencies = [ "huey>=2.5.5", "markdown>=3.10", "mozilla-django-oidc>=5.0.2", - "python-dateutil>=2.9.0.post0", "requests>=2.32.5", "supervisor>=4.3.0", "uwsgi>=2.0.31", diff --git a/uv.lock b/uv.lock index f9718e1..0c765e9 100644 --- a/uv.lock +++ b/uv.lock @@ -499,7 +499,6 @@ dependencies = [ { name = "huey" }, { name = "markdown" }, { name = "mozilla-django-oidc" }, - { name = "python-dateutil" }, { name = "requests" }, { name = "supervisor" }, { name = "uwsgi" }, @@ -534,7 +533,6 @@ requires-dist = [ { name = "huey", specifier = ">=2.5.5" }, { name = "markdown", specifier = ">=3.10" }, { name = "mozilla-django-oidc", specifier = ">=5.0.2" }, - { name = "python-dateutil", specifier = ">=2.9.0.post0" }, { name = "requests", specifier = ">=2.32.5" }, { name = "supervisor", specifier = ">=4.3.0" }, { name = "uwsgi", specifier = ">=2.0.31" }, @@ -760,18 +758,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - [[package]] name = "pyyaml" version = "6.0.3"