mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Restrict substitution of `copyright` years (#12516)
Only substitute copyright notice years with values from ``SOURCE_DATE_EPOCH`` for entries that match the current system clock year, and disallow substitution of future years. Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com>
This commit is contained in:
@@ -110,6 +110,10 @@ Bugs fixed
|
||||
* #12916: Restore support for custom templates named with the legacy ``_t``
|
||||
suffix during ``apidoc`` RST rendering (regression in 7.4.0).
|
||||
Patch by James Addison.
|
||||
* #12451: Only substitute copyright notice years with values from
|
||||
``SOURCE_DATE_EPOCH`` for entries that match the current system clock year,
|
||||
and disallow substitution of future years.
|
||||
Patch by James Addison and Adam Turner.
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
@@ -625,22 +625,34 @@ def correct_copyright_year(_app: Sphinx, config: Config) -> None:
|
||||
|
||||
See https://reproducible-builds.org/specs/source-date-epoch/
|
||||
"""
|
||||
if (source_date_epoch := getenv('SOURCE_DATE_EPOCH')) is None:
|
||||
if source_date_epoch := int(getenv('SOURCE_DATE_EPOCH', '0')):
|
||||
source_date_epoch_year = time.gmtime(source_date_epoch).tm_year
|
||||
else:
|
||||
return
|
||||
|
||||
source_date_epoch_year = str(time.gmtime(int(source_date_epoch)).tm_year)
|
||||
# If the current year is the replacement year, there's no work to do.
|
||||
# We also skip replacement years that are in the future.
|
||||
current_year = time.localtime().tm_year
|
||||
if current_year <= source_date_epoch_year:
|
||||
return
|
||||
|
||||
current_yr = str(current_year)
|
||||
replace_yr = str(source_date_epoch_year)
|
||||
for k in ('copyright', 'epub_copyright'):
|
||||
if k in config:
|
||||
value: str | Sequence[str] = config[k]
|
||||
if isinstance(value, str):
|
||||
config[k] = _substitute_copyright_year(value, source_date_epoch_year)
|
||||
config[k] = _substitute_copyright_year(value, current_yr, replace_yr)
|
||||
else:
|
||||
items = (_substitute_copyright_year(x, source_date_epoch_year) for x in value)
|
||||
items = (
|
||||
_substitute_copyright_year(x, current_yr, replace_yr) for x in value
|
||||
)
|
||||
config[k] = type(value)(items) # type: ignore[call-arg]
|
||||
|
||||
|
||||
def _substitute_copyright_year(copyright_line: str, replace_year: str) -> str:
|
||||
def _substitute_copyright_year(
|
||||
copyright_line: str, current_year: str, replace_year: str
|
||||
) -> str:
|
||||
"""Replace the year in a single copyright line.
|
||||
|
||||
Legal formats are:
|
||||
@@ -657,13 +669,17 @@ def _substitute_copyright_year(copyright_line: str, replace_year: str) -> str:
|
||||
if len(copyright_line) < 4 or not copyright_line[:4].isdigit():
|
||||
return copyright_line
|
||||
|
||||
if copyright_line[4:5] in {'', ' ', ','}:
|
||||
if copyright_line[:4] == current_year and copyright_line[4:5] in {'', ' ', ','}:
|
||||
return replace_year + copyright_line[4:]
|
||||
|
||||
if copyright_line[4] != '-':
|
||||
if copyright_line[4:5] != '-':
|
||||
return copyright_line
|
||||
|
||||
if copyright_line[5:9].isdigit() and copyright_line[9:10] in {'', ' ', ','}:
|
||||
if (
|
||||
copyright_line[5:9].isdigit()
|
||||
and copyright_line[5:9] == current_year
|
||||
and copyright_line[9:10] in {'', ' ', ','}
|
||||
):
|
||||
return copyright_line[:5] + replace_year + copyright_line[9:]
|
||||
|
||||
return copyright_line
|
||||
|
||||
@@ -2,16 +2,21 @@ import time
|
||||
|
||||
import pytest
|
||||
|
||||
LT = time.localtime()
|
||||
LT_NEW = (2009, *LT[1:], LT.tm_zone, LT.tm_gmtoff)
|
||||
LOCALTIME_2009 = type(LT)(LT_NEW)
|
||||
|
||||
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
1293840000, # 2011-01-01 00:00:00
|
||||
1293839999, # 2010-12-31 23:59:59
|
||||
1199145600, # 2008-01-01 00:00:00
|
||||
1199145599, # 2007-12-31 23:59:59
|
||||
]
|
||||
)
|
||||
def source_date_year(request, monkeypatch):
|
||||
source_date_epoch = request.param
|
||||
with monkeypatch.context() as m:
|
||||
m.setattr(time, 'localtime', lambda *a: LOCALTIME_2009)
|
||||
m.setenv('SOURCE_DATE_EPOCH', str(source_date_epoch))
|
||||
yield time.gmtime(source_date_epoch).tm_year
|
||||
|
||||
@@ -53,24 +58,24 @@ def test_html_multi_line_copyright_sde(source_date_year, app):
|
||||
content = (app.outdir / 'index.html').read_text(encoding='utf-8')
|
||||
|
||||
# check the copyright footer line by line (empty lines ignored)
|
||||
assert f' © Copyright {source_date_year}.<br/>\n' in content
|
||||
assert ' © Copyright 2006.<br/>\n' in content
|
||||
assert f' © Copyright 2006-{source_date_year}, Alice.<br/>\n' in content
|
||||
assert f' © Copyright 2010-{source_date_year}, Bob.<br/>\n' in content
|
||||
assert f' © Copyright 2014-{source_date_year}, Charlie.<br/>\n' in content
|
||||
assert f' © Copyright 2018-{source_date_year}, David.<br/>\n' in content
|
||||
assert f' © Copyright 2022-{source_date_year}, Eve.' in content
|
||||
assert ' © Copyright 2010-2013, Bob.<br/>\n' in content
|
||||
assert ' © Copyright 2014-2017, Charlie.<br/>\n' in content
|
||||
assert ' © Copyright 2018-2021, David.<br/>\n' in content
|
||||
assert ' © Copyright 2022-2025, Eve.' in content
|
||||
|
||||
# check the raw copyright footer block (empty lines included)
|
||||
assert (
|
||||
f' © Copyright {source_date_year}.<br/>\n'
|
||||
f' \n'
|
||||
' © Copyright 2006.<br/>\n'
|
||||
' \n'
|
||||
f' © Copyright 2006-{source_date_year}, Alice.<br/>\n'
|
||||
f' \n'
|
||||
f' © Copyright 2010-{source_date_year}, Bob.<br/>\n'
|
||||
f' \n'
|
||||
f' © Copyright 2014-{source_date_year}, Charlie.<br/>\n'
|
||||
f' \n'
|
||||
f' © Copyright 2018-{source_date_year}, David.<br/>\n'
|
||||
f' \n'
|
||||
f' © Copyright 2022-{source_date_year}, Eve.'
|
||||
' \n'
|
||||
' © Copyright 2010-2013, Bob.<br/>\n'
|
||||
' \n'
|
||||
' © Copyright 2014-2017, Charlie.<br/>\n'
|
||||
' \n'
|
||||
' © Copyright 2018-2021, David.<br/>\n'
|
||||
' \n'
|
||||
' © Copyright 2022-2025, Eve.'
|
||||
) in content
|
||||
|
||||
@@ -14,12 +14,13 @@ LOCALTIME_2009 = type(LT)(LT_NEW)
|
||||
@pytest.fixture(
|
||||
params=[
|
||||
# test with SOURCE_DATE_EPOCH unset: no modification
|
||||
(None, ''),
|
||||
# test with SOURCE_DATE_EPOCH set: copyright year should be updated
|
||||
('1293840000', '2011'),
|
||||
('1293839999', '2010'),
|
||||
('1199145600', '2008'),
|
||||
('1199145599', '2007'),
|
||||
(None, None),
|
||||
# test with post-2009 SOURCE_DATE_EPOCH set: copyright year should not be updated
|
||||
('1293840000', 2011),
|
||||
('1293839999', 2010),
|
||||
# test with pre-2009 SOURCE_DATE_EPOCH set: copyright year should be updated
|
||||
('1199145600', 2008),
|
||||
('1199145599', 2007),
|
||||
],
|
||||
)
|
||||
def expect_date(request, monkeypatch):
|
||||
@@ -39,7 +40,7 @@ def test_correct_year(expect_date):
|
||||
cfg = Config({'copyright': copyright_date}, {})
|
||||
assert cfg.copyright == copyright_date
|
||||
correct_copyright_year(None, cfg) # type: ignore[arg-type]
|
||||
if expect_date:
|
||||
if expect_date and expect_date <= LOCALTIME_2009.tm_year:
|
||||
assert cfg.copyright == f'2006-{expect_date}, Alice'
|
||||
else:
|
||||
assert cfg.copyright == copyright_date
|
||||
@@ -51,7 +52,7 @@ def test_correct_year_space(expect_date):
|
||||
cfg = Config({'copyright': copyright_date}, {})
|
||||
assert cfg.copyright == copyright_date
|
||||
correct_copyright_year(None, cfg) # type: ignore[arg-type]
|
||||
if expect_date:
|
||||
if expect_date and expect_date <= LOCALTIME_2009.tm_year:
|
||||
assert cfg.copyright == f'2006-{expect_date} Alice'
|
||||
else:
|
||||
assert cfg.copyright == copyright_date
|
||||
@@ -63,7 +64,7 @@ def test_correct_year_no_author(expect_date):
|
||||
cfg = Config({'copyright': copyright_date}, {})
|
||||
assert cfg.copyright == copyright_date
|
||||
correct_copyright_year(None, cfg) # type: ignore[arg-type]
|
||||
if expect_date:
|
||||
if expect_date and expect_date <= LOCALTIME_2009.tm_year:
|
||||
assert cfg.copyright == f'2006-{expect_date}'
|
||||
else:
|
||||
assert cfg.copyright == copyright_date
|
||||
@@ -75,7 +76,7 @@ def test_correct_year_single(expect_date):
|
||||
cfg = Config({'copyright': copyright_date}, {})
|
||||
assert cfg.copyright == copyright_date
|
||||
correct_copyright_year(None, cfg) # type: ignore[arg-type]
|
||||
if expect_date:
|
||||
if expect_date and expect_date <= LOCALTIME_2009.tm_year:
|
||||
assert cfg.copyright == f'{expect_date}, Alice'
|
||||
else:
|
||||
assert cfg.copyright == copyright_date
|
||||
@@ -87,7 +88,7 @@ def test_correct_year_single_space(expect_date):
|
||||
cfg = Config({'copyright': copyright_date}, {})
|
||||
assert cfg.copyright == copyright_date
|
||||
correct_copyright_year(None, cfg) # type: ignore[arg-type]
|
||||
if expect_date:
|
||||
if expect_date and expect_date <= LOCALTIME_2009.tm_year:
|
||||
assert cfg.copyright == f'{expect_date} Alice'
|
||||
else:
|
||||
assert cfg.copyright == copyright_date
|
||||
@@ -99,7 +100,7 @@ def test_correct_year_single_no_author(expect_date):
|
||||
cfg = Config({'copyright': copyright_date}, {})
|
||||
assert cfg.copyright == copyright_date
|
||||
correct_copyright_year(None, cfg) # type: ignore[arg-type]
|
||||
if expect_date:
|
||||
if expect_date and expect_date <= LOCALTIME_2009.tm_year:
|
||||
assert cfg.copyright == f'{expect_date}'
|
||||
else:
|
||||
assert cfg.copyright == copyright_date
|
||||
@@ -108,7 +109,7 @@ def test_correct_year_single_no_author(expect_date):
|
||||
def test_correct_year_multi_line(expect_date):
|
||||
# test that copyright is substituted
|
||||
copyright_dates = (
|
||||
'2006',
|
||||
'2009',
|
||||
'2006-2009, Alice',
|
||||
'2010-2013, Bob',
|
||||
'2014-2017, Charlie',
|
||||
@@ -118,14 +119,15 @@ def test_correct_year_multi_line(expect_date):
|
||||
cfg = Config({'copyright': copyright_dates}, {})
|
||||
assert cfg.copyright == copyright_dates
|
||||
correct_copyright_year(None, cfg) # type: ignore[arg-type]
|
||||
if expect_date:
|
||||
if expect_date and expect_date <= LOCALTIME_2009.tm_year:
|
||||
assert cfg.copyright == (
|
||||
f'{expect_date}',
|
||||
f'2006-{expect_date}, Alice',
|
||||
f'2010-{expect_date}, Bob',
|
||||
f'2014-{expect_date}, Charlie',
|
||||
f'2018-{expect_date}, David',
|
||||
f'2022-{expect_date}, Eve',
|
||||
# post 2009-dates aren't substituted
|
||||
'2010-2013, Bob',
|
||||
'2014-2017, Charlie',
|
||||
'2018-2021, David',
|
||||
'2022-2025, Eve',
|
||||
)
|
||||
else:
|
||||
assert cfg.copyright == copyright_dates
|
||||
@@ -144,7 +146,7 @@ def test_correct_year_multi_line_all_formats(expect_date):
|
||||
cfg = Config({'copyright': copyright_dates}, {})
|
||||
assert cfg.copyright == copyright_dates
|
||||
correct_copyright_year(None, cfg) # type: ignore[arg-type]
|
||||
if expect_date:
|
||||
if expect_date and expect_date <= LOCALTIME_2009.tm_year:
|
||||
assert cfg.copyright == (
|
||||
f'{expect_date}',
|
||||
f'{expect_date} Alice',
|
||||
@@ -166,7 +168,7 @@ def test_correct_year_app(expect_date, tmp_path, make_app):
|
||||
srcdir=tmp_path,
|
||||
confoverrides={'copyright': copyright_date},
|
||||
)
|
||||
if expect_date:
|
||||
if expect_date and expect_date <= LOCALTIME_2009.tm_year:
|
||||
assert app.config.copyright == f'2006-{expect_date}, Alice'
|
||||
else:
|
||||
assert app.config.copyright == copyright_date
|
||||
|
||||
Reference in New Issue
Block a user