From a4e150e98704bce0cf61b238cee1c113d8597230 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Sun, 22 Sep 2024 07:40:59 +0100 Subject: [PATCH] Allow using GMT in the HTML last updated format --- CHANGES.rst | 4 ++++ doc/conf.py | 1 + doc/usage/configuration.rst | 8 ++++++++ sphinx/builders/html/__init__.py | 7 ++++++- sphinx/util/i18n.py | 22 +++++++++++++++++++--- tests/test_util/test_util_i18n.py | 19 +++++++++++++++++++ 6 files changed, 57 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 67434b30629..3d5d3913326 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -48,6 +48,10 @@ Features added * #12743: Add :option:`sphinx-build --exception-on-warning`, to raise an exception when warnings are emitted during the build. Patch by Adam Turner and Jeremy Maitin-Shepard. +* #12907: Add :confval:`html_last_updated_time_zone` to allow using + GMT (universal time) instead of local time for the date-time + supplied to :confval:`html_last_updated_fmt`. + Patch by Adam Turner. Bugs fixed ---------- diff --git a/doc/conf.py b/doc/conf.py index 2de3f34aa2e..a28b6549821 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -53,6 +53,7 @@ html_use_opensearch = 'https://www.sphinx-doc.org/en/master' html_baseurl = 'https://www.sphinx-doc.org/en/master/' html_favicon = '_static/favicon.svg' +html_last_updated_time_zone = 'GMT' htmlhelp_basename = 'Sphinxdoc' diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 213c879ea88..be6edee7d7c 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -1717,6 +1717,14 @@ and also make use of these options. The empty string is equivalent to :code-py:`'%b %d, %Y'` (or a locale-dependent equivalent). +.. confval:: html_last_updated_time_zone + :type: :code-py:`'local' | 'GMT'` + :default: :code-py:`'local'` + + Choose GMT (+00:00) or the system's local time zone + for the time supplied to :confval:`html_last_updated_fmt`. + This is most useful when the format used includes the time. + .. confval:: html_permalinks :type: :code-py:`bool` :default: :code-py:`True` diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index 5ef3c1cea6f..57ba0fcadf9 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -454,7 +454,10 @@ def prepare_writing(self, docnames: set[str]) -> None: last_updated: str | None if (lu_fmt := self.config.html_last_updated_fmt) is not None: lu_fmt = lu_fmt or _('%b %d, %Y') - last_updated = format_date(lu_fmt, language=self.config.language) + local_time = self.config.html_last_updated_time_zone == 'local' + last_updated = format_date( + lu_fmt, language=self.config.language, local_time=local_time + ) else: last_updated = None @@ -1323,6 +1326,8 @@ def setup(app: Sphinx) -> ExtensionMetadata: app.add_config_value('html_static_path', [], 'html') app.add_config_value('html_extra_path', [], 'html') app.add_config_value('html_last_updated_fmt', None, 'html', str) + app.add_config_value('html_last_updated_time_zone', 'local', 'html', + ENUM('GMT', 'local')) app.add_config_value('html_sidebars', {}, 'html') app.add_config_value('html_additional_pages', {}, 'html') app.add_config_value('html_domain_indices', True, 'html', types={set, list}) diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py index 2f2d500d908..e788b7ab376 100644 --- a/sphinx/util/i18n.py +++ b/sphinx/util/i18n.py @@ -4,6 +4,7 @@ import os import re +import sys from datetime import datetime, timezone from os import path from typing import TYPE_CHECKING, NamedTuple @@ -59,6 +60,11 @@ def __call__( # NoQA: E704 Formatter: TypeAlias = DateFormatter | TimeFormatter | DatetimeFormatter +if sys.version_info[:2] >= (3, 11): + from datetime import UTC +else: + UTC = timezone.utc + logger = logging.getLogger(__name__) @@ -223,16 +229,26 @@ def babel_format_date(date: datetime, format: str, locale: str, def format_date( - format: str, *, date: datetime | None = None, language: str, + format: str, + *, + date: datetime | None = None, + language: str, + local_time: bool = False, ) -> str: if date is None: # If time is not specified, try to use $SOURCE_DATE_EPOCH variable # See https://wiki.debian.org/ReproducibleBuilds/TimestampsProposal source_date_epoch = os.getenv('SOURCE_DATE_EPOCH') if source_date_epoch is not None: - date = datetime.fromtimestamp(float(source_date_epoch), tz=timezone.utc) + date = datetime.fromtimestamp(float(source_date_epoch), tz=UTC) else: - date = datetime.now(tz=timezone.utc).astimezone() + date = datetime.now(tz=UTC) + + if local_time: + # > If called with tz=None, the system local time zone + # > is assumed for the target time zone. + # https://docs.python.org/dev/library/datetime.html#datetime.datetime.astimezone + date = date.astimezone(tz=None) result = [] tokens = date_format_re.split(format) diff --git a/tests/test_util/test_util_i18n.py b/tests/test_util/test_util_i18n.py index 5f0f3816540..5bfa4adab63 100644 --- a/tests/test_util/test_util_i18n.py +++ b/tests/test_util/test_util_i18n.py @@ -2,6 +2,7 @@ import datetime import os +import time import babel import pytest @@ -93,6 +94,24 @@ def test_format_date(): assert i18n.format_date(format, date=datet, language='en') == '+0000' +def test_format_date_timezone(): + dt = datetime.datetime(2016, 8, 7, 5, 11, 17, 0, tzinfo=datetime.timezone.utc) + if time.localtime(dt.timestamp()).tm_gmtoff == 0: + raise pytest.skip('Local time zone is GMT') # NoQA: EM101 + + fmt = '%Y-%m-%d %H:%M:%S' + + iso_gmt = dt.isoformat(' ').split('+')[0] + fd_gmt = i18n.format_date(fmt, date=dt, language='en', local_time=False) + assert fd_gmt == '2016-08-07 05:11:17' + assert fd_gmt == iso_gmt + + iso_local = dt.astimezone().isoformat(' ').split('+')[0] + fd_local = i18n.format_date(fmt, date=dt, language='en', local_time=True) + assert fd_local == iso_local + assert fd_local != fd_gmt + + @pytest.mark.sphinx('html', testroot='root') def test_get_filename_for_language(app): get_filename = i18n.get_image_filename_for_language