Skip to content

Config: native 'SOURCE_DATE_EPOCH' pattern-replacement support #13538

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ Bugs fixed

* #13369: Correctly parse and cross-reference unpacked type annotations.
Patch by Alicia Garcia-Raboso.
* #13526: Improve ``SOURCE_DATE_EPOCH`` support during ``%Y`` pattern
substition in :confval:`copyright` (and :confval:`project_copyright`).
Patch by James Addison.

Testing
-------
9 changes: 3 additions & 6 deletions sphinx/builders/gettext.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import os.path
import time
from collections import defaultdict
from os import getenv, walk
from os import walk
from pathlib import Path
from typing import TYPE_CHECKING
from uuid import uuid4
Expand All @@ -21,6 +21,7 @@
from sphinx.errors import ThemeError
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util._timestamps import _get_publication_time
from sphinx.util.display import status_iterator
from sphinx.util.i18n import docname_to_domain
from sphinx.util.index_entries import split_index_msg
Expand Down Expand Up @@ -200,11 +201,7 @@ def write_doc(self, docname: str, doctree: nodes.document) -> None:

# If set, use the timestamp from SOURCE_DATE_EPOCH
# https://reproducible-builds.org/specs/source-date-epoch/
if (source_date_epoch := getenv('SOURCE_DATE_EPOCH')) is not None:
timestamp = time.gmtime(float(source_date_epoch))
else:
# determine timestamp once to remain unaffected by DST changes during build
timestamp = time.localtime()
timestamp = _get_publication_time()
ctime = time.strftime('%Y-%m-%d %H:%M%z', timestamp)


Expand Down
4 changes: 3 additions & 1 deletion sphinx/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from sphinx.errors import ConfigError, ExtensionError
from sphinx.locale import _, __
from sphinx.util import logging
from sphinx.util._timestamps import _get_publication_time

if TYPE_CHECKING:
import os
Expand Down Expand Up @@ -700,7 +701,8 @@ def init_numfig_format(app: Sphinx, config: Config) -> None:

def evaluate_copyright_placeholders(_app: Sphinx, config: Config) -> None:
"""Replace copyright year placeholders (%Y) with the current year."""
replace_yr = str(time.localtime().tm_year)
publication_time = _get_publication_time()
replace_yr = str(publication_time.tm_year)
for k in ('copyright', 'epub_copyright'):
if k in config:
value: str | Sequence[str] = config[k]
Expand Down
17 changes: 17 additions & 0 deletions sphinx/util/_timestamps.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import time
from os import getenv


def _format_rfc3339_microseconds(timestamp: int, /) -> str:
Expand All @@ -11,3 +12,19 @@ def _format_rfc3339_microseconds(timestamp: int, /) -> str:
seconds, fraction = divmod(timestamp, 10**6)
time_tuple = time.gmtime(seconds)
return time.strftime('%Y-%m-%d %H:%M:%S', time_tuple) + f'.{fraction // 1_000}'


def _get_publication_time() -> time.struct_time:
"""Return the publication time to use for the current build.

If set, use the timestamp from SOURCE_DATE_EPOCH
https://reproducible-builds.org/specs/source-date-epoch/

Publication time cannot be projected into the future (beyond the local system
clock time).
"""
system_time = time.localtime()
if (source_date_epoch := getenv('SOURCE_DATE_EPOCH')) is not None:
if (rebuild_time := time.localtime(float(source_date_epoch))) < system_time:
return rebuild_time
return system_time
11 changes: 7 additions & 4 deletions tests/test_config/test_copyright.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def expect_date(
) -> Iterator[int | None]:
sde, expect = request.param
with monkeypatch.context() as m:
m.setattr(time, 'localtime', lambda *a: LOCALTIME_2009)
lt_orig = time.localtime
m.setattr(time, 'localtime', lambda *a: lt_orig(*a) if a else LOCALTIME_2009)
if sde:
m.setenv('SOURCE_DATE_EPOCH', sde)
else:
Expand Down Expand Up @@ -129,7 +130,6 @@ def test_correct_year_placeholder(expect_date: int | None) -> None:
cfg = Config({'copyright': copyright_date}, {})
assert cfg.copyright == copyright_date
evaluate_copyright_placeholders(None, cfg) # type: ignore[arg-type]
correct_copyright_year(None, cfg) # type: ignore[arg-type]
if expect_date and expect_date <= LOCALTIME_2009.tm_year:
assert cfg.copyright == f'2006-{expect_date}, Alice'
else:
Expand Down Expand Up @@ -203,11 +203,12 @@ def test_correct_year_multi_line_all_formats_placeholder(
# other format codes are left as-is
'2006-%y, Eve',
'%Y-%m-%d %H:%M:S %z, Francis',
# non-ascii range patterns are supported
'2000–%Y Guinevere',
)
cfg = Config({'copyright': copyright_dates}, {})
assert cfg.copyright == copyright_dates
evaluate_copyright_placeholders(None, cfg) # type: ignore[arg-type]
correct_copyright_year(None, cfg) # type: ignore[arg-type]
if expect_date and expect_date <= LOCALTIME_2009.tm_year:
assert cfg.copyright == (
f'{expect_date}',
Expand All @@ -217,7 +218,8 @@ def test_correct_year_multi_line_all_formats_placeholder(
f'2006-{expect_date} Charlie',
f'2006-{expect_date}, David',
'2006-%y, Eve',
'2009-%m-%d %H:%M:S %z, Francis',
f'{expect_date}-%m-%d %H:%M:S %z, Francis',
f'2000–{expect_date} Guinevere',
)
else:
assert cfg.copyright == (
Expand All @@ -229,6 +231,7 @@ def test_correct_year_multi_line_all_formats_placeholder(
'2006-2009, David',
'2006-%y, Eve',
'2009-%m-%d %H:%M:S %z, Francis',
'2000–2009 Guinevere',
)


Expand Down
Loading