Skip to content

Commit de4af25

Browse files
committed
Config: native SOURCE_DATE_EPOCH pattern-replacement support
1 parent c4929d0 commit de4af25

File tree

5 files changed

+34
-11
lines changed

5 files changed

+34
-11
lines changed

CHANGES.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ Bugs fixed
2525

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

2932
Testing
3033
-------

sphinx/builders/gettext.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import os.path
99
import time
1010
from collections import defaultdict
11-
from os import getenv, walk
11+
from os import walk
1212
from pathlib import Path
1313
from typing import TYPE_CHECKING
1414
from uuid import uuid4
@@ -21,6 +21,7 @@
2121
from sphinx.errors import ThemeError
2222
from sphinx.locale import __
2323
from sphinx.util import logging
24+
from sphinx.util._timestamps import _get_publication_time
2425
from sphinx.util.display import status_iterator
2526
from sphinx.util.i18n import docname_to_domain
2627
from sphinx.util.index_entries import split_index_msg
@@ -200,11 +201,7 @@ def write_doc(self, docname: str, doctree: nodes.document) -> None:
200201

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

210207

sphinx/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from sphinx.errors import ConfigError, ExtensionError
1616
from sphinx.locale import _, __
1717
from sphinx.util import logging
18+
from sphinx.util._timestamps import _get_publication_time
1819

1920
if TYPE_CHECKING:
2021
import os
@@ -700,7 +701,8 @@ def init_numfig_format(app: Sphinx, config: Config) -> None:
700701

701702
def evaluate_copyright_placeholders(_app: Sphinx, config: Config) -> None:
702703
"""Replace copyright year placeholders (%Y) with the current year."""
703-
replace_yr = str(time.localtime().tm_year)
704+
publication_time = _get_publication_time()
705+
replace_yr = str(publication_time.tm_year)
704706
for k in ('copyright', 'epub_copyright'):
705707
if k in config:
706708
value: str | Sequence[str] = config[k]

sphinx/util/_timestamps.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import time
4+
from os import getenv
45

56

67
def _format_rfc3339_microseconds(timestamp: int, /) -> str:
@@ -11,3 +12,20 @@ def _format_rfc3339_microseconds(timestamp: int, /) -> str:
1112
seconds, fraction = divmod(timestamp, 10**6)
1213
time_tuple = time.gmtime(seconds)
1314
return time.strftime('%Y-%m-%d %H:%M:%S', time_tuple) + f'.{fraction // 1_000}'
15+
16+
17+
def _get_publication_time() -> time.struct_time:
18+
"""Return the publication time to use for the current build.
19+
20+
If set, use the timestamp from SOURCE_DATE_EPOCH
21+
https://reproducible-builds.org/specs/source-date-epoch/
22+
23+
Publication time cannot be projected into the future (beyond the local system
24+
clock time).
25+
"""
26+
time.tzset()
27+
system_time = time.localtime()
28+
if (source_date_epoch := getenv('SOURCE_DATE_EPOCH')) is not None:
29+
if (rebuild_time := time.localtime(float(source_date_epoch))) < system_time:
30+
return rebuild_time
31+
return system_time

tests/test_config/test_copyright.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ def expect_date(
4343
) -> Iterator[int | None]:
4444
sde, expect = request.param
4545
with monkeypatch.context() as m:
46-
m.setattr(time, 'localtime', lambda *a: LOCALTIME_2009)
46+
lt_orig = time.localtime
47+
m.setattr(time, 'localtime', lambda *a: lt_orig(*a) if a else LOCALTIME_2009)
4748
if sde:
4849
m.setenv('SOURCE_DATE_EPOCH', sde)
4950
else:
@@ -129,7 +130,6 @@ def test_correct_year_placeholder(expect_date: int | None) -> None:
129130
cfg = Config({'copyright': copyright_date}, {})
130131
assert cfg.copyright == copyright_date
131132
evaluate_copyright_placeholders(None, cfg) # type: ignore[arg-type]
132-
correct_copyright_year(None, cfg) # type: ignore[arg-type]
133133
if expect_date and expect_date <= LOCALTIME_2009.tm_year:
134134
assert cfg.copyright == f'2006-{expect_date}, Alice'
135135
else:
@@ -203,11 +203,12 @@ def test_correct_year_multi_line_all_formats_placeholder(
203203
# other format codes are left as-is
204204
'2006-%y, Eve',
205205
'%Y-%m-%d %H:%M:S %z, Francis',
206+
# non-ascii range patterns are supported
207+
'2000–%Y Guinevere',
206208
)
207209
cfg = Config({'copyright': copyright_dates}, {})
208210
assert cfg.copyright == copyright_dates
209211
evaluate_copyright_placeholders(None, cfg) # type: ignore[arg-type]
210-
correct_copyright_year(None, cfg) # type: ignore[arg-type]
211212
if expect_date and expect_date <= LOCALTIME_2009.tm_year:
212213
assert cfg.copyright == (
213214
f'{expect_date}',
@@ -217,7 +218,8 @@ def test_correct_year_multi_line_all_formats_placeholder(
217218
f'2006-{expect_date} Charlie',
218219
f'2006-{expect_date}, David',
219220
'2006-%y, Eve',
220-
'2009-%m-%d %H:%M:S %z, Francis',
221+
f'{expect_date}-%m-%d %H:%M:S %z, Francis',
222+
f'2000–{expect_date} Guinevere',
221223
)
222224
else:
223225
assert cfg.copyright == (
@@ -229,6 +231,7 @@ def test_correct_year_multi_line_all_formats_placeholder(
229231
'2006-2009, David',
230232
'2006-%y, Eve',
231233
'2009-%m-%d %H:%M:S %z, Francis',
234+
'2000–2009 Guinevere',
232235
)
233236

234237

0 commit comments

Comments
 (0)