diff --git a/noxfile.py b/noxfile.py index 9599f07..a2b7cbb 100644 --- a/noxfile.py +++ b/noxfile.py @@ -72,6 +72,7 @@ def test(session: nox.Session, django_ver: str) -> None: "Australia/LHI", "Asia/Magadan", "Antarctica/McMurdo", + "Pacific/Auckland", "NZ-CHAT", "Etc/GMT-13", "Etc/GMT-14", diff --git a/pyproject.toml b/pyproject.toml index 80a0f02..1fd7786 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "djpress" -version = "0.30.0" +version = "0.30.1" description = "A blog application for Django sites, inspired by classic WordPress." readme = "README.md" requires-python = ">=3.10" @@ -112,7 +112,7 @@ omit = ["*/tests/*", "*/migrations/*"] exclude_lines = ["if TYPE_CHECKING:", "# pragma: no cover"] [tool.bumpver] -current_version = "0.30.0" +current_version = "0.30.1" version_pattern = "MAJOR.MINOR.PATCH[PYTAGNUM]" commit_message = "👍 bump version {old_version} -> {new_version}" commit = true diff --git a/src/djpress/__init__.py b/src/djpress/__init__.py index 19689b5..cd855af 100644 --- a/src/djpress/__init__.py +++ b/src/djpress/__init__.py @@ -1,3 +1,3 @@ """djpress module.""" -__version__ = "0.30.0" +__version__ = "0.30.1" diff --git a/src/djpress/management/commands/djpress_export.py b/src/djpress/management/commands/djpress_export.py index 46d1e66..ad0607f 100644 --- a/src/djpress/management/commands/djpress_export.py +++ b/src/djpress/management/commands/djpress_export.py @@ -8,6 +8,7 @@ from django.core.management.base import BaseCommand, CommandError, CommandParser from django.db.models import QuerySet +from django.utils import timezone from djpress.models import Media, Post @@ -180,8 +181,8 @@ def _export_media_library(self, target_dir: Path) -> int: "description": media.description, "media_type": media.media_type, "uploaded_by": media.uploaded_by.username if media.uploaded_by else None, - "uploaded_at": media.uploaded_at.isoformat() if media.uploaded_at else None, - "updated_at": media.updated_at.isoformat() if media.updated_at else None, + "uploaded_at": timezone.localtime(media.uploaded_at).isoformat() if media.uploaded_at else None, + "updated_at": timezone.localtime(media.updated_at).isoformat() if media.updated_at else None, "url": media.file.url, } @@ -215,8 +216,10 @@ def _create_directory_structure(self, output_dir: Path, *, include_media: bool = def _export_post(self, post: Post, output_dir: Path) -> None: """Export a single post to flat file format.""" - # Create filename based on date and slug - date_str = post.published_at.strftime("%Y-%m-%d") + # Create filename based on date and slug. Use `_date` (the frozen local publish date used + # for the post's canonical URL) rather than `published_at` (stored in UTC), so the exported + # filename's date matches the site's actual date-based routing. + date_str = post._date.strftime("%Y-%m-%d") # noqa: SLF001 filename = f"{date_str}-{post.slug}.md" filepath = output_dir / "content" / "posts" / filename @@ -250,8 +253,8 @@ def _generate_frontmatter(self, content: Post) -> dict[str, Any]: Frontmatter fields: - Common: - "title": content.title - - "date": content.published_at.isoformat() - - "lastmod": content.updated_at.isoformat() + - "date": content.published_at, converted to the local `TIME_ZONE`, isoformat() + - "lastmod": content.updated_at, converted to the local `TIME_ZONE`, isoformat() - "status": content.status - "slug": content.slug - "author": content.author.get_full_name() or content.author.username @@ -265,8 +268,8 @@ def _generate_frontmatter(self, content: Post) -> dict[str, Any]: """ frontmatter = { "title": content.title, - "date": content.published_at.isoformat(), - "lastmod": content.updated_at.isoformat(), + "date": timezone.localtime(content.published_at).isoformat(), + "lastmod": timezone.localtime(content.updated_at).isoformat(), "status": content.status, "slug": content.slug, "author": content.author.get_full_name() or content.author.username, diff --git a/src/djpress/models/post.py b/src/djpress/models/post.py index b9f3112..8efd3e4 100644 --- a/src/djpress/models/post.py +++ b/src/djpress/models/post.py @@ -671,7 +671,7 @@ def save(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003 # We only do this if the post is new or if the date has changed. This ensures the _date field doesn't change # unless the date field has been specifically changed. if self.pk is None: - self._date = self.published_at.date() + self._date = self.local_datetime.date() else: old = self.__class__.admin_objects.filter(pk=self.pk).only("published_at").first() if old is None or old.published_at != self.published_at: diff --git a/tests/test_management_commands_export.py b/tests/test_management_commands_export.py index 439c5c0..4d1ba81 100644 --- a/tests/test_management_commands_export.py +++ b/tests/test_management_commands_export.py @@ -1,5 +1,6 @@ """Tests for management commands.""" +import datetime import os import pytest import tempfile @@ -158,7 +159,9 @@ def test_basic_export_published_only(self, test_post1, test_page, draft_post): # Check file naming post_file = post_files[0] assert test_post1.slug in post_file.name - assert str(test_post1.published_at.year) in post_file.name + # The exported filename's date comes from `_date` (frozen local publish date), not + # `published_at` (stored in UTC). + assert str(test_post1._date.year) in post_file.name def test_export_posts_only(self, test_post1, test_page): """Test export with posts-only option.""" @@ -205,6 +208,32 @@ def test_frontmatter_generation_complete(self, post_with_categories_and_tags): assert len(frontmatter["categories"]) == 1 assert len(frontmatter["tags"]) == 2 + def test_frontmatter_dates_use_local_timezone_not_utc(self, user): + """`date`/`lastmod` in frontmatter must reflect `TIME_ZONE`, not the UTC + value Django stores internally and returns on fetch from the DB. + """ + with override_settings(TIME_ZONE="Pacific/Auckland"): # UTC+12/+13 + published_at = datetime.datetime(2026, 4, 20, 23, 0, tzinfo=datetime.timezone.utc) + post = Post.admin_objects.create( + title="Timezone Export Post", + slug="timezone-export-post", + content="Test content.", + author=user, + status="published", + post_type="post", + published_at=published_at, + ) + # Re-fetch, since the export command works off a queryset, not the + # in-memory instance that was just created. + post = Post.admin_objects.get(pk=post.pk) + + command = Command() + frontmatter = command._generate_frontmatter(post) + + assert frontmatter["date"] == timezone.localtime(post.published_at).isoformat() + assert frontmatter["date"].startswith("2026-04-21T11:00:00") + assert frontmatter["lastmod"] == timezone.localtime(post.updated_at).isoformat() + def test_frontmatter_generation_page_with_parent(self, child_page): """Test frontmatter generation for page with parent and menu order.""" command = Command() @@ -255,8 +284,9 @@ def test_export_post_filename_format(self, test_post1): assert len(post_files) == 1 filename = post_files[0].name - # Should be in format YYYY-MM-DD-slug.md - date_str = test_post1.published_at.strftime("%Y-%m-%d") + # Should be in format YYYY-MM-DD-slug.md. The filename is built from `_date`, the frozen + # local publish date - not `published_at`, which is stored in UTC. + date_str = test_post1._date.strftime("%Y-%m-%d") expected_filename = f"{date_str}-{test_post1.slug}.md" assert filename == expected_filename @@ -715,6 +745,31 @@ def test_export_media_metadata_success(self, test_media_file_1, user): assert "uploaded_at" in item assert "updated_at" in item + def test_export_media_metadata_dates_use_local_timezone_not_utc(self, test_media_file_1): + """`uploaded_at`/`updated_at` in the media metadata must reflect `TIME_ZONE`, not the UTC + value Django stores internally and returns on fetch from the DB. + """ + import json + + with override_settings(TIME_ZONE="Pacific/Auckland"): # UTC+12/+13 + uploaded_at = datetime.datetime(2026, 4, 20, 23, 0, tzinfo=datetime.timezone.utc) + test_media_file_1.uploaded_at = uploaded_at + test_media_file_1.save() + # Re-fetch, since the export command works off a queryset, not the in-memory instance. + media = Media.objects.get(pk=test_media_file_1.pk) + + with tempfile.TemporaryDirectory() as temp_dir: + call_command("djpress_export", output=temp_dir, no_zip=True) + + metadata_file = Path(temp_dir) / "static" / "metadata.json" + with metadata_file.open("r", encoding="utf-8") as f: + metadata = json.load(f) + + item = metadata[media.file.name] + assert item["uploaded_at"] == timezone.localtime(media.uploaded_at).isoformat() + assert item["uploaded_at"].startswith("2026-04-21T11:00:00") + assert item["updated_at"] == timezone.localtime(media.updated_at).isoformat() + def test_export_media_metadata_no_uploaded_by(self, test_media_file_1): """Test that media metadata handles null uploaded_by correctly.""" import json diff --git a/tests/test_models_post.py b/tests/test_models_post.py index 5560985..3116501 100644 --- a/tests/test_models_post.py +++ b/tests/test_models_post.py @@ -6,6 +6,8 @@ from django.core.exceptions import ValidationError from django.db.models import QuerySet +from django.forms.utils import from_current_timezone + from djpress.models import Category, Post from djpress.models.post import PUBLISHED_POSTS_CACHE_KEY from djpress.exceptions import PostNotFoundError, PageNotFoundError @@ -23,6 +25,74 @@ def test_post_model(test_post1, user, category1): assert str(test_post1) == "Test Post1" +@pytest.mark.django_db +def test_new_post_date_field_matches_local_date_via_admin_style_input(user, settings): + """Simulate how the admin actually populates `published_at`. + + The admin field for `published_at` is `forms.SplitDateTimeField` (Django's + `FORMFIELD_FOR_DBFIELD_DEFAULTS` swaps this in for every `DateTimeField`), + and both it and plain `DateTimeField` run parsed input through + `django.forms.utils.from_current_timezone()`. That attaches the *current* + timezone to the naive value via `timezone.make_aware()` - it does not + convert to UTC. So `published_at` on a freshly-bound, not-yet-saved + instance carries local tzinfo, not UTC, and `.date()` on it already + reflects the local calendar date. + """ + settings.TIME_ZONE = "Pacific/Auckland" # UTC+12/+13 + + # What the user actually typed into the split date/time widget: 21 April, 11am local. + typed_naive_datetime = datetime.datetime(2026, 4, 21, 11, 0) + published_at = from_current_timezone(typed_naive_datetime) + assert published_at.tzinfo is not None + assert str(published_at.tzinfo) == "Pacific/Auckland" + + post = Post.objects.create( + title="Timezone Test Post", + slug="timezone-test-post", + content="Test content.", + author=user, + status="published", + post_type="post", + published_at=published_at, + ) + + # No UTC/local mismatch: on the real (admin) creation path, `_date` is + # already the correct local date, because `published_at` was never UTC + # in the first place at the point `Post.save()` calls `.date()` on it. + assert post._date == datetime.date(2026, 4, 21) + assert post._date == post.local_datetime.date() + + +@pytest.mark.django_db +def test_new_post_date_field_matches_local_date_when_published_at_is_utc_aware(user, settings): + """`_date` must reflect the local calendar date even when `published_at` is + handed to `Post.save()` already UTC-aware, not just when it arrives via the + admin's `from_current_timezone()` conversion. + + This covers creation paths other than the admin form - e.g. the field's + own `default=timezone.now` firing when `published_at` is omitted, a bulk + import script, or a future API - where the aware datetime is UTC from the + start rather than carrying local tzinfo. + """ + settings.TIME_ZONE = "Pacific/Auckland" # UTC+12/+13 + + # 23:00 UTC on 20 April is already 21 April in Auckland. + published_at = datetime.datetime(2026, 4, 20, 23, 0, tzinfo=datetime.timezone.utc) + + post = Post.objects.create( + title="Imported Post", + slug="imported-post", + content="Test content.", + author=user, + status="published", + post_type="post", + published_at=published_at, + ) + + assert post.local_datetime.date() == datetime.date(2026, 4, 21) + assert post._date == datetime.date(2026, 4, 21) + + @pytest.mark.django_db def test_post_default_queryset(test_post1, test_post2, test_post3): """Make sure the default queryset returns only published posts.""" @@ -1144,12 +1214,14 @@ def test_post_parent_is_none(test_post1, test_page1): @pytest.mark.django_db def test_post_get_years(test_post1, test_post2, test_post3): + # `get_years` groups by `_date`, the frozen local publish date - not `published_at`, which is + # stored in UTC - so the expected values must come from `_date` too. # type should be a queryset assert isinstance(Post.post_objects.get_years(), QuerySet) # Queryset should have 1 item assert len(list(Post.post_objects.get_years())) == 1 # The item should be the year of the post - assert list(Post.post_objects.get_years())[0].year == test_post1.published_at.year + assert list(Post.post_objects.get_years())[0].year == test_post1._date.year test_post2.published_at = timezone.make_aware(timezone.datetime(2023, 1, 1, 12, 0, 0)) test_post2.save() @@ -1157,8 +1229,8 @@ def test_post_get_years(test_post1, test_post2, test_post3): # Queryset should have 2 items assert len(list(Post.post_objects.get_years())) == 2 # The items should be the years of the posts - assert list(Post.post_objects.get_years())[0].year == test_post2.published_at.year - assert list(Post.post_objects.get_years())[1].year == test_post1.published_at.year + assert list(Post.post_objects.get_years())[0].year == test_post2._date.year + assert list(Post.post_objects.get_years())[1].year == test_post1._date.year test_post3.published_at = timezone.make_aware(timezone.datetime(2022, 1, 1, 12, 0, 0)) test_post3.save() @@ -1166,9 +1238,9 @@ def test_post_get_years(test_post1, test_post2, test_post3): # Queryset should have 3 items assert len(list(Post.post_objects.get_years())) == 3 # The items should be the years of the posts - assert list(Post.post_objects.get_years())[0].year == test_post3.published_at.year - assert list(Post.post_objects.get_years())[1].year == test_post2.published_at.year - assert list(Post.post_objects.get_years())[2].year == test_post1.published_at.year + assert list(Post.post_objects.get_years())[0].year == test_post3._date.year + assert list(Post.post_objects.get_years())[1].year == test_post2._date.year + assert list(Post.post_objects.get_years())[2].year == test_post1._date.year # Change a post to draft status test_post1.status = "draft" @@ -1177,20 +1249,22 @@ def test_post_get_years(test_post1, test_post2, test_post3): # Queryset should have 2 items assert len(list(Post.post_objects.get_years())) == 2 # The items should be the years of the posts - assert list(Post.post_objects.get_years())[0].year == test_post3.published_at.year - assert list(Post.post_objects.get_years())[1].year == test_post2.published_at.year + assert list(Post.post_objects.get_years())[0].year == test_post3._date.year + assert list(Post.post_objects.get_years())[1].year == test_post2._date.year @pytest.mark.django_db def test_post_get_months(test_post1, test_post2, test_post3): - months = Post.post_objects.get_months(test_post1.published_at.year) + # `get_months` groups by `_date`, the frozen local publish date - not `published_at`, which is + # stored in UTC - so the expected values must come from `_date` too. + months = Post.post_objects.get_months(test_post1._date.year) # type should be a queryset assert isinstance(months, QuerySet) # Queryset should have 1 item - all three posts are in the same year and month assert len(months) == 1 # The item should be the month of the post - assert months[0].month == test_post1.published_at.month + assert months[0].month == test_post1._date.month # Set specific dates for each of the posts test_post1.published_at = timezone.make_aware(timezone.datetime(2022, 1, 1, 12, 0, 0)) @@ -1200,31 +1274,33 @@ def test_post_get_months(test_post1, test_post2, test_post3): test_post3.published_at = timezone.make_aware(timezone.datetime(2022, 3, 1, 12, 0, 0)) test_post3.save() - months = list(Post.post_objects.get_months(test_post1.published_at.year)) + months = list(Post.post_objects.get_months(test_post1._date.year)) # Queryset should have 3 items assert len(months) == 3 # The items should be the months of the posts - assert months[0].month == test_post1.published_at.month - assert months[1].month == test_post2.published_at.month - assert months[2].month == test_post3.published_at.month + assert months[0].month == test_post1._date.month + assert months[1].month == test_post2._date.month + assert months[2].month == test_post3._date.month # Change a post to draft status test_post1.status = "draft" test_post1.save() - months = list(Post.post_objects.get_months(test_post1.published_at.year)) + months = list(Post.post_objects.get_months(test_post1._date.year)) # Queryset should have 2 items assert len(months) == 2 # The items should be the months of the posts - assert months[0].month == test_post2.published_at.month - assert months[1].month == test_post3.published_at.month + assert months[0].month == test_post2._date.month + assert months[1].month == test_post3._date.month @pytest.mark.django_db def test_post_get_days(test_post1, test_post2, test_post3): - days = Post.post_objects.get_days(test_post1.published_at.year, test_post1.published_at.month) + # `get_days` groups by `_date`, the frozen local publish date - not `published_at`, which is + # stored in UTC - so the expected values must come from `_date` too. + days = Post.post_objects.get_days(test_post1._date.year, test_post1._date.month) # type should be a queryset assert isinstance(days, QuerySet) @@ -1239,49 +1315,53 @@ def test_post_get_days(test_post1, test_post2, test_post3): test_post3.published_at = timezone.make_aware(timezone.datetime(2022, 1, 3, 12, 0, 0)) test_post3.save() - days = list(Post.post_objects.get_days(test_post1.published_at.year, test_post1.published_at.month)) + days = list(Post.post_objects.get_days(test_post1._date.year, test_post1._date.month)) # Queryset should have 3 items assert len(days) == 3 # The items should be the days of the posts - assert days[0].day == test_post1.published_at.day - assert days[1].day == test_post2.published_at.day - assert days[2].day == test_post3.published_at.day + assert days[0].day == test_post1._date.day + assert days[1].day == test_post2._date.day + assert days[2].day == test_post3._date.day # Change a post to draft status test_post1.status = "draft" test_post1.save() - days = list(Post.post_objects.get_days(test_post1.published_at.year, test_post1.published_at.month)) + days = list(Post.post_objects.get_days(test_post1._date.year, test_post1._date.month)) # Queryset should have 2 items assert len(days) == 2 # The items should be the days of the posts - assert days[0].day == test_post2.published_at.day - assert days[1].day == test_post3.published_at.day + assert days[0].day == test_post2._date.day + assert days[1].day == test_post3._date.day @pytest.mark.django_db def test_get_year_last_modified(test_post1, test_post2, test_post3): + # `get_year_last_modified` filters on `_date`, the frozen local publish date - not `published_at`, + # which is stored in UTC - so the expected values must come from `_date` too. # Should match the modified date of the last post in the list - i.e. most recent post - assert Post.post_objects.get_year_last_modified(test_post1.published_at.year) == test_post3.updated_at + assert Post.post_objects.get_year_last_modified(test_post1._date.year) == test_post3.updated_at # Change test_post3 to draft and it should now match test_post2 test_post3.status = "draft" test_post3.save() - assert Post.post_objects.get_year_last_modified(test_post1.published_at.year) == test_post2.updated_at + assert Post.post_objects.get_year_last_modified(test_post1._date.year) == test_post2.updated_at # Changetest_post2 to future date and it should now match test_post1 test_post2.published_at = timezone.now() + timezone.timedelta(days=1) test_post2.save() - assert Post.post_objects.get_year_last_modified(test_post1.published_at.year) == test_post1.updated_at + assert Post.post_objects.get_year_last_modified(test_post1._date.year) == test_post1.updated_at @pytest.mark.django_db def test_get_month_last_modified(test_post1, test_post2, test_post3): + # `get_month_last_modified` filters on `_date`, the frozen local publish date - not `published_at`, + # which is stored in UTC - so the expected values must come from `_date` too. # Should match the modified date of the last post in the list - i.e. most recent post assert ( - Post.post_objects.get_month_last_modified(test_post1.published_at.year, test_post1.published_at.month) + Post.post_objects.get_month_last_modified(test_post1._date.year, test_post1._date.month) == test_post3.updated_at ) @@ -1289,7 +1369,7 @@ def test_get_month_last_modified(test_post1, test_post2, test_post3): test_post3.status = "draft" test_post3.save() assert ( - Post.post_objects.get_month_last_modified(test_post1.published_at.year, test_post1.published_at.month) + Post.post_objects.get_month_last_modified(test_post1._date.year, test_post1._date.month) == test_post2.updated_at ) @@ -1297,7 +1377,7 @@ def test_get_month_last_modified(test_post1, test_post2, test_post3): test_post2.published_at = timezone.now() + timezone.timedelta(days=1) test_post2.save() assert ( - Post.post_objects.get_month_last_modified(test_post1.published_at.year, test_post1.published_at.month) + Post.post_objects.get_month_last_modified(test_post1._date.year, test_post1._date.month) == test_post1.updated_at ) @@ -1305,41 +1385,23 @@ def test_get_month_last_modified(test_post1, test_post2, test_post3): @pytest.mark.django_db def test_get_day_last_modified(test_post1, test_post2, test_post3): # Should match the modified date of the last post in the list - i.e. most recent post - post_year = test_post1.published_at.year - post_month = test_post1.published_at.month - post_day = test_post1.published_at.day - - print(f"{post_year=}") - print(f"{post_month=}") - print(f"{post_day=}") - print(f"{test_post1._date=}") - print(f"{Post.post_objects.get_day_last_modified(post_year, post_month, post_day)=}") - assert ( - Post.post_objects.get_day_last_modified( - test_post1.published_at.year, test_post1.published_at.month, test_post1.published_at.day - ) - == test_post3.updated_at - ) + # `get_day_last_modified` filters on `_date`, the frozen local publish date - not `published_at`, + # which is stored in UTC - so the expected values must come from `_date` too. + post_year = test_post1._date.year + post_month = test_post1._date.month + post_day = test_post1._date.day + + assert Post.post_objects.get_day_last_modified(post_year, post_month, post_day) == test_post3.updated_at # Change test_post3 to draft and it should now match test_post2 test_post3.status = "draft" test_post3.save() - assert ( - Post.post_objects.get_day_last_modified( - test_post1.published_at.year, test_post1.published_at.month, test_post1.published_at.day - ) - == test_post2.updated_at - ) + assert Post.post_objects.get_day_last_modified(post_year, post_month, post_day) == test_post2.updated_at # Changetest_post2 to future date and it should now match test_post1 test_post2.published_at = timezone.now() + timezone.timedelta(days=1) test_post2.save() - assert ( - Post.post_objects.get_day_last_modified( - test_post1.published_at.year, test_post1.published_at.month, test_post1.published_at.day - ) - == test_post1.updated_at - ) + assert Post.post_objects.get_day_last_modified(post_year, post_month, post_day) == test_post1.updated_at @pytest.mark.django_db diff --git a/tests/test_sitemaps.py b/tests/test_sitemaps.py index 336bb38..48b2be0 100644 --- a/tests/test_sitemaps.py +++ b/tests/test_sitemaps.py @@ -63,17 +63,19 @@ def test_category_sitemap(category1, category2, test_post1, test_post2): def test_date_based_sitemap(test_post1, test_post2, test_post3): """Test the DateBasedSitemap class.""" + # DateBasedSitemap groups by `_date`, the frozen local publish date - not `published_at`, + # which is stored in UTC - so the expected values must come from `_date` too. expected_items = [ - {"year": test_post3.published_at.year, "latest_modified": test_post3.updated_at}, + {"year": test_post3._date.year, "latest_modified": test_post3.updated_at}, { - "year": test_post3.published_at.year, - "month": test_post3.published_at.month, + "year": test_post3._date.year, + "month": test_post3._date.month, "latest_modified": test_post3.updated_at, }, { - "year": test_post3.published_at.year, - "month": test_post3.published_at.month, - "day": test_post3.published_at.day, + "year": test_post3._date.year, + "month": test_post3._date.month, + "day": test_post3._date.day, "latest_modified": test_post3.updated_at, }, ] @@ -84,12 +86,10 @@ def test_date_based_sitemap(test_post1, test_post2, test_post3): assert sitemap.protocol == "https" assert sitemap.items() == expected_items assert sitemap.lastmod(expected_items[0]) == test_post3.updated_at - assert sitemap.location(expected_items[0]) == get_archives_url(test_post3.published_at.year) - assert sitemap.location(expected_items[1]) == get_archives_url( - test_post3.published_at.year, test_post3.published_at.month - ) + assert sitemap.location(expected_items[0]) == get_archives_url(test_post3._date.year) + assert sitemap.location(expected_items[1]) == get_archives_url(test_post3._date.year, test_post3._date.month) assert sitemap.location(expected_items[2]) == get_archives_url( - test_post3.published_at.year, test_post3.published_at.month, test_post3.published_at.day + test_post3._date.year, test_post3._date.month, test_post3._date.day ) diff --git a/tests/test_url_utils.py b/tests/test_url_utils.py index 048a32b..d6d549f 100644 --- a/tests/test_url_utils.py +++ b/tests/test_url_utils.py @@ -434,40 +434,40 @@ def test_get_post_url(settings, test_post1): url = get_post_url(test_post1) assert url == expected_url + # get_post_url builds the URL from `_date`, the frozen local publish date - not `published_at`, + # which is stored in UTC - so the expected values must come from `_date` too. settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}/{{ day }}" - expected_url = f"/{test_post1.published_at.strftime('%Y')}/{test_post1.published_at.strftime('%m')}/{test_post1.published_at.strftime('%d')}/{test_post1.slug}/" + expected_url = f"/{test_post1._date.strftime('%Y')}/{test_post1._date.strftime('%m')}/{test_post1._date.strftime('%d')}/{test_post1.slug}/" url = get_post_url(test_post1) assert url == expected_url settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{year}}/{{month}}/{{day}}" - expected_url = f"/{test_post1.published_at.strftime('%Y')}/{test_post1.published_at.strftime('%m')}/{test_post1.published_at.strftime('%d')}/{test_post1.slug}/" + expected_url = f"/{test_post1._date.strftime('%Y')}/{test_post1._date.strftime('%m')}/{test_post1._date.strftime('%d')}/{test_post1.slug}/" url = get_post_url(test_post1) assert url == expected_url settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{y e a r}}/{{m onth}}/{{day }}" - expected_url = f"/{test_post1.published_at.strftime('%Y')}/{test_post1.published_at.strftime('%m')}/{test_post1.published_at.strftime('%d')}/{test_post1.slug}/" + expected_url = f"/{test_post1._date.strftime('%Y')}/{test_post1._date.strftime('%m')}/{test_post1._date.strftime('%d')}/{test_post1.slug}/" url = get_post_url(test_post1) assert url == expected_url settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}" - expected_url = ( - f"/{test_post1.published_at.strftime('%Y')}/{test_post1.published_at.strftime('%m')}/{test_post1.slug}/" - ) + expected_url = f"/{test_post1._date.strftime('%Y')}/{test_post1._date.strftime('%m')}/{test_post1.slug}/" url = get_post_url(test_post1) assert url == expected_url settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}" - expected_url = f"/{test_post1.published_at.strftime('%Y')}/{test_post1.slug}/" + expected_url = f"/{test_post1._date.strftime('%Y')}/{test_post1.slug}/" url = get_post_url(test_post1) assert url == expected_url settings.DJPRESS_SETTINGS["POST_PREFIX"] = "post/{{ year }}/{{ month }}/{{ day }}" - expected_url = f"/post/{test_post1.published_at.strftime('%Y')}/{test_post1.published_at.strftime('%m')}/{test_post1.published_at.strftime('%d')}/{test_post1.slug}/" + expected_url = f"/post/{test_post1._date.strftime('%Y')}/{test_post1._date.strftime('%m')}/{test_post1._date.strftime('%d')}/{test_post1.slug}/" url = get_post_url(test_post1) assert url == expected_url settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}/{{ day }}/post" - expected_url = f"/{test_post1.published_at.strftime('%Y')}/{test_post1.published_at.strftime('%m')}/{test_post1.published_at.strftime('%d')}/post/{test_post1.slug}/" + expected_url = f"/{test_post1._date.strftime('%Y')}/{test_post1._date.strftime('%m')}/{test_post1._date.strftime('%d')}/post/{test_post1.slug}/" url = get_post_url(test_post1) assert url == expected_url diff --git a/tests/test_views.py b/tests/test_views.py index 6ecb32a..012036a 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -216,9 +216,11 @@ def test_tag_with_tag_enabled_false(client, settings, tag1): @pytest.mark.django_db def test_date_archives_year(client, settings, test_post1): + # The year archive filters on `_date`, the frozen local publish date - not `published_at`, + # which is stored in UTC - so the expected values must come from `_date` too. assert settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] == "test-url-archives" - url = get_archives_url(test_post1.published_at.year) - assert url == f"/test-url-archives/{test_post1.published_at.year}/" + url = get_archives_url(test_post1._date.year) + assert url == f"/test-url-archives/{test_post1._date.year}/" response = client.get(url) assert response.status_code == 200 assert test_post1.title.encode() in response.content @@ -227,8 +229,8 @@ def test_date_archives_year(client, settings, test_post1): assert "Test Post1" in response.content.decode() settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] = "" - url = get_archives_url(test_post1.published_at.year) - assert url == f"/{test_post1.published_at.year}/" + url = get_archives_url(test_post1._date.year) + assert url == f"/{test_post1._date.year}/" response = client.get(url) assert response.status_code == 200 @@ -247,7 +249,7 @@ def test_date_archives_year_invalid_year(client, settings): @pytest.mark.django_db def test_date_archives_year_no_posts(client, test_post1): - url = get_archives_url(test_post1.published_at.year - 1) + url = get_archives_url(test_post1._date.year - 1) response = client.get(url) assert response.status_code == 200 assert not test_post1.title.encode() in response.content @@ -258,9 +260,11 @@ def test_date_archives_year_no_posts(client, test_post1): @pytest.mark.django_db def test_date_archives_month(client, settings, test_post1): + # The month archive filters on `_date`, the frozen local publish date - not `published_at`, + # which is stored in UTC - so the expected values must come from `_date` too. assert settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] == "test-url-archives" - url = get_archives_url(test_post1.published_at.year, test_post1.published_at.month) - assert url == f"/test-url-archives/{test_post1.published_at.year}/{test_post1.published_at.month:02}/" + url = get_archives_url(test_post1._date.year, test_post1._date.month) + assert url == f"/test-url-archives/{test_post1._date.year}/{test_post1._date.month:02}/" response = client.get(url) assert response.status_code == 200 @@ -269,8 +273,8 @@ def test_date_archives_month(client, settings, test_post1): assert isinstance(response.context["posts"], Iterable) settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] = "" - url = get_archives_url(test_post1.published_at.year, test_post1.published_at.month) - assert url == f"/{test_post1.published_at.year}/{test_post1.published_at.month:02}/" + url = get_archives_url(test_post1._date.year, test_post1._date.month) + assert url == f"/{test_post1._date.year}/{test_post1._date.month:02}/" response = client.get(url) assert response.status_code == 200 @@ -290,7 +294,7 @@ def test_date_archives_month_invalid_month(client, settings): @pytest.mark.django_db def test_date_archives_month_no_posts(client, test_post1): - url = get_archives_url(test_post1.published_at.year - 1, test_post1.published_at.month) + url = get_archives_url(test_post1._date.year - 1, test_post1._date.month) response = client.get(url) assert response.status_code == 200 assert not test_post1.title.encode() in response.content @@ -302,12 +306,11 @@ def test_date_archives_month_no_posts(client, test_post1): @pytest.mark.django_db def test_date_archives_day(client, settings, test_post1): assert settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] == "test-url-archives" + # The day archive filters on `_date`, the frozen local publish date - not `published_at`, + # which is stored in UTC - so the expected values must come from `_date` too. url = get_archives_url(test_post1._date.year, test_post1._date.month, test_post1._date.day) assert url == f"/test-url-archives/{test_post1._date.year}/{test_post1._date.month:02}/{test_post1._date.day:02}/" - print(url) - print(test_post1._date) - response = client.get(url) assert response.status_code == 200 assert test_post1.title.encode() in response.content @@ -315,10 +318,8 @@ def test_date_archives_day(client, settings, test_post1): assert isinstance(response.context["posts"], Iterable) settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] = "" - url = get_archives_url(test_post1.published_at.year, test_post1.published_at.month, test_post1.published_at.day) - assert ( - url == f"/{test_post1.published_at.year}/{test_post1.published_at.month:02}/{test_post1.published_at.day:02}/" - ) + url = get_archives_url(test_post1._date.year, test_post1._date.month, test_post1._date.day) + assert url == f"/{test_post1._date.year}/{test_post1._date.month:02}/{test_post1._date.day:02}/" response = client.get(url) assert response.status_code == 200 @@ -328,10 +329,8 @@ def test_date_archives_day(client, settings, test_post1): assert settings.DJPRESS_SETTINGS["POST_PREFIX"] == "test-posts" settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}/{{ day }}" - url = get_archives_url(test_post1.published_at.year, test_post1.published_at.month, test_post1.published_at.day) - assert ( - url == f"/{test_post1.published_at.year}/{test_post1.published_at.month:02}/{test_post1.published_at.day:02}/" - ) + url = get_archives_url(test_post1._date.year, test_post1._date.month, test_post1._date.day) + assert url == f"/{test_post1._date.year}/{test_post1._date.month:02}/{test_post1._date.day:02}/" response = client.get(url) assert response.status_code == 200 @@ -354,10 +353,10 @@ def test_conflict_day_archives_and_single_post(client, settings, test_post1): """ settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] = "" settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}/{{ month }}" - url = get_archives_url(test_post1.published_at.year, test_post1.published_at.month, test_post1.published_at.day) - assert ( - url == f"/{test_post1.published_at.year}/{test_post1.published_at.month:02}/{test_post1.published_at.day:02}/" - ) + # The day archive filters on `_date`, the frozen local publish date - not `published_at`, + # which is stored in UTC - so the expected values must come from `_date` too. + url = get_archives_url(test_post1._date.year, test_post1._date.month, test_post1._date.day) + assert url == f"/{test_post1._date.year}/{test_post1._date.month:02}/{test_post1._date.day:02}/" response = client.get(url) assert response.status_code == 200 @@ -377,8 +376,10 @@ def test_conflict_month_archives_and_single_post(client, settings, test_post1): """ settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] = "" settings.DJPRESS_SETTINGS["POST_PREFIX"] = "{{ year }}" - url = get_archives_url(test_post1.published_at.year, test_post1.published_at.month) - assert url == f"/{test_post1.published_at.year}/{test_post1.published_at.month:02}/" + # The month archive filters on `_date`, the frozen local publish date - not `published_at`, + # which is stored in UTC - so the expected values must come from `_date` too. + url = get_archives_url(test_post1._date.year, test_post1._date.month) + assert url == f"/{test_post1._date.year}/{test_post1._date.month:02}/" response = client.get(url) assert response.status_code == 200 @@ -398,8 +399,10 @@ def test_conflict_year_archives_and_single_post(client, settings, test_post1): """ settings.DJPRESS_SETTINGS["ARCHIVE_PREFIX"] = "" settings.DJPRESS_SETTINGS["POST_PREFIX"] = "" - url = get_archives_url(test_post1.published_at.year) - assert url == f"/{test_post1.published_at.year}/" + # The year archive filters on `_date`, the frozen local publish date - not `published_at`, + # which is stored in UTC - so the expected values must come from `_date` too. + url = get_archives_url(test_post1._date.year) + assert url == f"/{test_post1._date.year}/" response = client.get(url) assert response.status_code == 200 @@ -408,7 +411,7 @@ def test_conflict_year_archives_and_single_post(client, settings, test_post1): assert isinstance(response.context["posts"], Iterable) # If the post slug is 2024, then the post will be returned. - test_post1.slug = str(test_post1.published_at.year) + test_post1.slug = str(test_post1._date.year) test_post1.save() response = client.get(url) assert response.status_code == 200 @@ -428,7 +431,7 @@ def test_date_archives_day_invalid_day(client, settings): @pytest.mark.django_db def test_date_archives_day_no_posts(client, test_post1): - url = get_archives_url(test_post1.published_at.year - 1, test_post1.published_at.month, test_post1.published_at.day) + url = get_archives_url(test_post1._date.year - 1, test_post1._date.month, test_post1._date.day) response = client.get(url) assert response.status_code == 200 assert not test_post1.title.encode() in response.content diff --git a/uv.lock b/uv.lock index 337d592..fa3f0de 100644 --- a/uv.lock +++ b/uv.lock @@ -387,9 +387,9 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "asgiref", marker = "python_full_version < '3.12'" }, - { name = "sqlparse", marker = "python_full_version < '3.12'" }, - { name = "tzdata", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/26/889449d521ae508b26de715954faecd8bcf3f740affb81b2d146a83b42a5/django-5.2.16.tar.gz", hash = "sha256:59ea02020c3136fce14bef0bbece21a10a4febef5eed1c51c22ae468efa22200", size = 10890894, upload-time = "2026-07-07T13:52:17.005Z" } wheels = [ @@ -404,9 +404,9 @@ resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ - { name = "asgiref", marker = "python_full_version >= '3.12'" }, - { name = "sqlparse", marker = "python_full_version >= '3.12'" }, - { name = "tzdata", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/89/55/664f24ff81c9ea19cb7dfc851afeae1f3c2390c7aee01d4ded68b5c1580d/django-6.0.7.tar.gz", hash = "sha256:2998503fc083124fb58037084bfa00de323c7c743f05f1b4284e77bff0ab8890", size = 10921299, upload-time = "2026-07-07T13:51:26.485Z" } wheels = [ @@ -429,7 +429,7 @@ wheels = [ [[package]] name = "djpress" -version = "0.30.0" +version = "0.30.1" source = { editable = "." } dependencies = [ { name = "django", version = "5.2.16", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, @@ -542,7 +542,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -659,7 +659,7 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "mdurl", marker = "python_full_version < '3.11'" }, + { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } wheels = [ @@ -675,7 +675,7 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "mdurl", marker = "python_full_version >= '3.11'" }, + { name = "mdurl" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ @@ -797,12 +797,12 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "jinja2", marker = "python_full_version < '3.11'" }, - { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "mdit-py-plugins", marker = "python_full_version < '3.11'" }, - { name = "pyyaml", marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" } }, + { name = "jinja2" }, + { name = "markdown-it-py", version = "3.0.0", source = { registry = "https://pypi.org/simple" } }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985, upload-time = "2025-02-12T10:53:03.833Z" } wheels = [ @@ -818,12 +818,12 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jinja2", marker = "python_full_version >= '3.11'" }, - { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "mdit-py-plugins", marker = "python_full_version >= '3.11'" }, - { name = "pyyaml", marker = "python_full_version >= '3.11'" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, + { name = "jinja2" }, + { name = "markdown-it-py", version = "4.2.0", source = { registry = "https://pypi.org/simple" } }, + { name = "mdit-py-plugins" }, + { name = "pyyaml" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/21/dc/603751677fff302f34396e206b610f556a59d7fe58b9a2145f54e96b48e8/myst_parser-5.1.0.tar.gz", hash = "sha256:ab69322dc6719dcc7f296479dbb70181b66df6ed315064f92dbc85c0e1bf2f02", size = 101182, upload-time = "2026-05-13T09:38:19.361Z" } @@ -1095,23 +1095,23 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version < '3.11'" }, - { name = "babel", marker = "python_full_version < '3.11'" }, - { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "imagesize", marker = "python_full_version < '3.11'" }, - { name = "jinja2", marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "requests", marker = "python_full_version < '3.11'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils", version = "0.21.2", source = { registry = "https://pypi.org/simple" } }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, + { name = "tomli" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611, upload-time = "2024-10-13T20:27:13.93Z" } wheels = [ @@ -1126,23 +1126,23 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version == '3.11.*'" }, - { name = "babel", marker = "python_full_version == '3.11.*'" }, - { name = "colorama", marker = "python_full_version == '3.11.*' and sys_platform == 'win32'" }, - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, - { name = "imagesize", marker = "python_full_version == '3.11.*'" }, - { name = "jinja2", marker = "python_full_version == '3.11.*'" }, - { name = "packaging", marker = "python_full_version == '3.11.*'" }, - { name = "pygments", marker = "python_full_version == '3.11.*'" }, - { name = "requests", marker = "python_full_version == '3.11.*'" }, - { name = "roman-numerals", marker = "python_full_version == '3.11.*'" }, - { name = "snowballstemmer", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version == '3.11.*'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version == '3.11.*'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/42/50/a8c6ccc36d5eacdfd7913ddccd15a9cee03ecafc5ee2bc40e1f168d85022/sphinx-9.0.4.tar.gz", hash = "sha256:594ef59d042972abbc581d8baa577404abe4e6c3b04ef61bd7fc2acbd51f3fa3", size = 8710502, upload-time = "2025-12-04T07:45:27.343Z" } wheels = [ @@ -1157,23 +1157,23 @@ resolution-markers = [ "python_full_version >= '3.12'", ] dependencies = [ - { name = "alabaster", marker = "python_full_version >= '3.12'" }, - { name = "babel", marker = "python_full_version >= '3.12'" }, - { name = "colorama", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, - { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "imagesize", marker = "python_full_version >= '3.12'" }, - { name = "jinja2", marker = "python_full_version >= '3.12'" }, - { name = "packaging", marker = "python_full_version >= '3.12'" }, - { name = "pygments", marker = "python_full_version >= '3.12'" }, - { name = "requests", marker = "python_full_version >= '3.12'" }, - { name = "roman-numerals", marker = "python_full_version >= '3.12'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.12'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.12'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils", version = "0.22.4", source = { registry = "https://pypi.org/simple" } }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/bd/f08eb0f4eed5c83f1ba2a3bd18f7745a2b1525fad70660a1c00224ec468a/sphinx-9.1.0.tar.gz", hash = "sha256:7741722357dd75f8190766926071fed3bdc211c74dd2d7d4df5404da95930ddb", size = 8718324, upload-time = "2025-12-31T15:09:27.646Z" } wheels = [ @@ -1188,12 +1188,12 @@ resolution-markers = [ "python_full_version < '3.11'", ] dependencies = [ - { name = "colorama", marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "starlette", marker = "python_full_version < '3.11'" }, - { name = "uvicorn", marker = "python_full_version < '3.11'" }, - { name = "watchfiles", marker = "python_full_version < '3.11'" }, - { name = "websockets", marker = "python_full_version < '3.11'" }, + { name = "colorama" }, + { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" } }, + { name = "starlette" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a5/2c/155e1de2c1ba96a72e5dba152c509a8b41e047ee5c2def9e9f0d812f8be7/sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1", size = 14023, upload-time = "2024-10-02T23:15:30.172Z" } wheels = [ @@ -1209,13 +1209,13 @@ resolution-markers = [ "python_full_version == '3.11.*'", ] dependencies = [ - { name = "colorama", marker = "python_full_version >= '3.11'" }, - { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" }, + { name = "colorama" }, + { name = "sphinx", version = "9.0.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, { name = "sphinx", version = "9.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, - { name = "starlette", marker = "python_full_version >= '3.11'" }, - { name = "uvicorn", marker = "python_full_version >= '3.11'" }, - { name = "watchfiles", marker = "python_full_version >= '3.11'" }, - { name = "websockets", marker = "python_full_version >= '3.11'" }, + { name = "starlette" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/3c/a59a3a453d4133777f7ed2e83c80b7dc817d43c74b74298ca0af869662ad/sphinx_autobuild-2025.8.25.tar.gz", hash = "sha256:9cf5aab32853c8c31af572e4fecdc09c997e2b8be5a07daf2a389e270e85b213", size = 15200, upload-time = "2025-08-25T18:44:55.436Z" } wheels = [