From e08300134252a113c086e8313861a535b9eee39c Mon Sep 17 00:00:00 2001 From: James Addison Date: Sun, 7 Jul 2024 15:00:22 +0100 Subject: [PATCH 01/21] Configuration: add two preconditions for copyright-notice year-substitution: 1. The ``SOURCE_DATE_EPOCH`` year must be greater than the local system-clock year. 2. The year(s) to be substituted must match the local system-clock year. --- sphinx/config.py | 22 +++++++++-- tests/roots/test-copyright-multiline/conf.py | 4 +- tests/roots/test-correct-year/conf.py | 5 ++- tests/test_config/test_config.py | 40 +++++++++++--------- tests/test_config/test_correct_year.py | 17 +++++++-- 5 files changed, 62 insertions(+), 26 deletions(-) diff --git a/sphinx/config.py b/sphinx/config.py index 43781499588..7f50602c2d3 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -626,7 +626,7 @@ def correct_copyright_year(_app: Sphinx, config: Config) -> None: def _substitute_copyright_year(copyright_line: str, replace_year: str) -> str: - """Replace the year in a single copyright line. + """Replace the current local calendar year when it appears in a single copyright line. Legal formats are: @@ -638,17 +638,33 @@ def _substitute_copyright_year(copyright_line: str, replace_year: str) -> str: The final year in the string is replaced with ``replace_year``. """ + localtime_year = str(time.localtime().tm_year) + + # The current year _is_ the replacement year, so replacement would be a no-op + if localtime_year == replace_year: + return copyright_line + + # Do not replace the current year in a copyright notice with an earlier year + if int(replace_year) < int(localtime_year): + return copyright_line + if len(copyright_line) < 4 or not copyright_line[:4].isdigit(): return copyright_line if copyright_line[4:5] in {'', ' ', ','}: - return replace_year + copyright_line[4:] + if copyright_line[0:4] == localtime_year: + return replace_year + copyright_line[4:] + else: + return copyright_line if copyright_line[4] != '-': return copyright_line if copyright_line[5:9].isdigit() and copyright_line[9:10] in {'', ' ', ','}: - return copyright_line[:5] + replace_year + copyright_line[9:] + if copyright_line[5:9] == localtime_year: + return copyright_line[:5] + replace_year + copyright_line[9:] + else: + return copyright_line return copyright_line diff --git a/tests/roots/test-copyright-multiline/conf.py b/tests/roots/test-copyright-multiline/conf.py index a2b7b680100..4cc2833bf8f 100644 --- a/tests/roots/test-copyright-multiline/conf.py +++ b/tests/roots/test-copyright-multiline/conf.py @@ -1,9 +1,11 @@ +from datetime import datetime + copyright = ( '2006', '2006-2009, Alice', '2010-2013, Bob', '2014-2017, Charlie', '2018-2021, David', - '2022-2025, Eve', + f'2022-{datetime.today().year}, Eve', ) html_theme = 'basic' diff --git a/tests/roots/test-correct-year/conf.py b/tests/roots/test-correct-year/conf.py index 814c08b55e0..421fab3d505 100644 --- a/tests/roots/test-correct-year/conf.py +++ b/tests/roots/test-correct-year/conf.py @@ -1 +1,4 @@ -copyright = '2006-2009, Author' +from datetime import datetime + +# Dynamically evaluated copyright notice, as found in many Sphinx projects +copyright = f'2006-{datetime.today().year}, Author' diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py index 322daa3a495..f180ac6761c 100644 --- a/tests/test_config/test_config.py +++ b/tests/test_config/test_config.py @@ -31,6 +31,9 @@ CircularDict = dict[str, Union[int, 'CircularDict']] +_LOCALTIME_YEAR = str(time.localtime().tm_year) + + def check_is_serializable(subject: object, *, circular: bool) -> None: assert is_serializable(subject) @@ -712,14 +715,14 @@ def test_multi_line_copyright(source_date_year, app, monkeypatch): content = (app.outdir / 'index.html').read_text(encoding='utf-8') - if source_date_year is None: + if source_date_year is None or int(source_date_year) < int(_LOCALTIME_YEAR): # check the copyright footer line by line (empty lines ignored) assert ' © Copyright 2006.
\n' in content assert ' © Copyright 2006-2009, Alice.
\n' in content assert ' © Copyright 2010-2013, Bob.
\n' in content assert ' © Copyright 2014-2017, Charlie.
\n' in content assert ' © Copyright 2018-2021, David.
\n' in content - assert ' © Copyright 2022-2025, Eve.' in content + assert f' © Copyright 2022-{_LOCALTIME_YEAR}, Eve.' in content # check the raw copyright footer block (empty lines included) assert ( @@ -733,45 +736,48 @@ def test_multi_line_copyright(source_date_year, app, monkeypatch): ' \n' ' © Copyright 2018-2021, David.
\n' ' \n' - ' © Copyright 2022-2025, Eve.' + f' © Copyright 2022-{_LOCALTIME_YEAR}, Eve.' ) in content else: # check the copyright footer line by line (empty lines ignored) - assert f' © Copyright {source_date_year}.
\n' in content - assert f' © Copyright 2006-{source_date_year}, Alice.
\n' in content - assert f' © Copyright 2010-{source_date_year}, Bob.
\n' in content - assert f' © Copyright 2014-{source_date_year}, Charlie.
\n' in content - assert f' © Copyright 2018-{source_date_year}, David.
\n' in content + assert ' © Copyright 2006.
\n' in content + assert ' © Copyright 2006-2009, Alice.
\n' in content + assert ' © Copyright 2010-2013, Bob.
\n' in content + assert ' © Copyright 2014-2017, Charlie.
\n' in content + assert ' © Copyright 2018-2021, David.
\n' in content assert f' © Copyright 2022-{source_date_year}, Eve.' in content # check the raw copyright footer block (empty lines included) assert ( - f' © Copyright {source_date_year}.
\n' + f' © Copyright 2006.
\n' f' \n' - f' © Copyright 2006-{source_date_year}, Alice.
\n' + f' © Copyright 2006-2009, Alice.
\n' f' \n' - f' © Copyright 2010-{source_date_year}, Bob.
\n' + f' © Copyright 2010-2013, Bob.
\n' f' \n' - f' © Copyright 2014-{source_date_year}, Charlie.
\n' + f' © Copyright 2014-2017, Charlie.
\n' f' \n' - f' © Copyright 2018-{source_date_year}, David.
\n' + f' © Copyright 2018-2021, David.
\n' f' \n' f' © Copyright 2022-{source_date_year}, Eve.' ) in content @pytest.mark.parametrize(('conf_copyright', 'expected_copyright'), [ - ('1970', '{current_year}'), + ('1970', '1970'), + (_LOCALTIME_YEAR, '{current_year}'), + ('1970-1990', '1970-1990'), # https://github.com/sphinx-doc/sphinx/issues/11913 - ('1970-1990', '1970-{current_year}'), - ('1970-1990 Alice', '1970-{current_year} Alice'), + (f'1970-{_LOCALTIME_YEAR}', '1970-{current_year}'), + ('1970-1990 Alice', '1970-1990 Alice'), + (f'1970-{_LOCALTIME_YEAR} Alice', '1970-{current_year} Alice'), ]) def test_correct_copyright_year(conf_copyright, expected_copyright, source_date_year): config = Config({}, {'copyright': conf_copyright}) correct_copyright_year(_app=None, config=config) actual_copyright = config['copyright'] - if source_date_year is None: + if source_date_year is None or int(source_date_year) < int(_LOCALTIME_YEAR): expected_copyright = conf_copyright else: expected_copyright = expected_copyright.format(current_year=source_date_year) diff --git a/tests/test_config/test_correct_year.py b/tests/test_config/test_correct_year.py index 4ef77a6f5ff..6159c884885 100644 --- a/tests/test_config/test_correct_year.py +++ b/tests/test_config/test_correct_year.py @@ -1,14 +1,23 @@ """Test copyright year adjustment""" +from datetime import datetime, timedelta + import pytest +_LOCALTIME_YEAR = datetime.now().year # NoQA: DTZ005 +_FUTURE_MOMENT = datetime.now() + timedelta(days=400) # NoQA: DTZ005 +_FUTURE_TIMESTAMP = int(_FUTURE_MOMENT.timestamp()) +_FUTURE_YEAR = _FUTURE_MOMENT.year + @pytest.fixture( params=[ # test with SOURCE_DATE_EPOCH unset: no modification - (None, '2006-2009'), - # test with SOURCE_DATE_EPOCH set: copyright year should be updated - ('1293840000', '2006-2011'), - ('1293839999', '2006-2010'), + (None, f'2006-{_LOCALTIME_YEAR}'), + # test with past SOURCE_DATE_EPOCH set: copyright year should _not_ be updated + ('1293840000', f'2006-{_LOCALTIME_YEAR}'), + ('1293839999', f'2006-{_LOCALTIME_YEAR}'), + # test with +1yr SOURCE_DATE_EPOCH set: copyright year should be updated + (f'{_FUTURE_TIMESTAMP}', f'2006-{_FUTURE_YEAR}'), ], ) From 7cc52fd8b58496406df0eab5560813b97f859d36 Mon Sep 17 00:00:00 2001 From: James Addison Date: Sun, 7 Jul 2024 15:12:02 +0100 Subject: [PATCH 02/21] Configuration: allow copyright-notice year-substitution feature to be disabled. --- sphinx/config.py | 4 ++++ tests/test_config/test_correct_year.py | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/sphinx/config.py b/sphinx/config.py index 7f50602c2d3..d9e5d430e0e 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -266,6 +266,7 @@ class Config: 'smartquotes_excludes': _Opt( {'languages': ['ja'], 'builders': ['man', 'text']}, 'env', ()), 'option_emphasise_placeholders': _Opt(False, 'env', ()), + 'copyright_build_year_substitution': _Opt(True, 'env', ()), } def __init__(self, config: dict[str, Any] | None = None, @@ -610,6 +611,9 @@ def correct_copyright_year(_app: Sphinx, config: Config) -> None: See https://reproducible-builds.org/specs/source-date-epoch/ """ + if not config['copyright_build_year_substitution']: + return + if (source_date_epoch := getenv('SOURCE_DATE_EPOCH')) is None: return diff --git a/tests/test_config/test_correct_year.py b/tests/test_config/test_correct_year.py index 6159c884885..e2007370740 100644 --- a/tests/test_config/test_correct_year.py +++ b/tests/test_config/test_correct_year.py @@ -36,3 +36,11 @@ def test_correct_year(expect_date, app): app.build() content = (app.outdir / 'index.html').read_text(encoding='utf8') assert expect_date in content + + +@pytest.mark.sphinx('html', testroot='correct-year', + confoverrides={'copyright_build_year_substitution': False}) +def test_build_year_substitution_disabled(expect_date, app): + app.build() + content = (app.outdir / 'index.html').read_text(encoding='utf8') + assert f'2006-{_LOCALTIME_YEAR}' in content From f6d737fb3a7451410e0dfda243f1da3f3bbc5b24 Mon Sep 17 00:00:00 2001 From: James Addison Date: Sun, 7 Jul 2024 15:14:25 +0100 Subject: [PATCH 03/21] Tests: refactor: attempt to improve readability by extracting copyright-as-configured const variable. --- tests/test_config/test_correct_year.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/test_config/test_correct_year.py b/tests/test_config/test_correct_year.py index e2007370740..321fc9d6550 100644 --- a/tests/test_config/test_correct_year.py +++ b/tests/test_config/test_correct_year.py @@ -3,21 +3,23 @@ import pytest -_LOCALTIME_YEAR = datetime.now().year # NoQA: DTZ005 +_LOCALTIME_YEAR = str(datetime.now().year) # NoQA: DTZ005 +_ORIG_CONF_COPYRIGHT = f'2006-{_LOCALTIME_YEAR}' + _FUTURE_MOMENT = datetime.now() + timedelta(days=400) # NoQA: DTZ005 _FUTURE_TIMESTAMP = int(_FUTURE_MOMENT.timestamp()) -_FUTURE_YEAR = _FUTURE_MOMENT.year +_FUTURE_YEAR = str(_FUTURE_MOMENT.year) @pytest.fixture( params=[ # test with SOURCE_DATE_EPOCH unset: no modification - (None, f'2006-{_LOCALTIME_YEAR}'), + (None, _ORIG_CONF_COPYRIGHT), # test with past SOURCE_DATE_EPOCH set: copyright year should _not_ be updated - ('1293840000', f'2006-{_LOCALTIME_YEAR}'), - ('1293839999', f'2006-{_LOCALTIME_YEAR}'), + ('1293840000', _ORIG_CONF_COPYRIGHT), + ('1293839999', _ORIG_CONF_COPYRIGHT), # test with +1yr SOURCE_DATE_EPOCH set: copyright year should be updated - (f'{_FUTURE_TIMESTAMP}', f'2006-{_FUTURE_YEAR}'), + (f'{_FUTURE_TIMESTAMP}', _ORIG_CONF_COPYRIGHT.replace(_LOCALTIME_YEAR, _FUTURE_YEAR)), ], ) @@ -43,4 +45,4 @@ def test_correct_year(expect_date, app): def test_build_year_substitution_disabled(expect_date, app): app.build() content = (app.outdir / 'index.html').read_text(encoding='utf8') - assert f'2006-{_LOCALTIME_YEAR}' in content + assert _ORIG_CONF_COPYRIGHT in content From f8002ee91d30bc6cea1606102926c64afe6c1fd2 Mon Sep 17 00:00:00 2001 From: James Addison Date: Sun, 7 Jul 2024 15:19:23 +0100 Subject: [PATCH 04/21] Add CHANGES.rst entry. --- CHANGES.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c36233573a3..75ef7a3b4f2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -89,6 +89,9 @@ Bugs fixed * #12494: Fix invalid genindex.html file produced with translated docs (regression in 7.1.0). Patch by Nicolas Peugnet. +* #12451: Add further preconditions and a ``copyright_build_year_substitution`` + configuration setting check before copyright notice year substitution occurs. + Patch by James Addison. Testing ------- From 8656fec3cf4ccbf95292436212d44fbb610e408f Mon Sep 17 00:00:00 2001 From: James Addison Date: Sun, 7 Jul 2024 19:52:22 +0100 Subject: [PATCH 05/21] Precondition fixup: replacement (from build date / `SOURCE_DATE_EPOCH`) must be earlier than the current year. --- sphinx/config.py | 4 ++-- tests/test_config/test_config.py | 13 ++++++++----- tests/test_config/test_correct_year.py | 10 +++++----- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/sphinx/config.py b/sphinx/config.py index d9e5d430e0e..4e77f222190 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -648,8 +648,8 @@ def _substitute_copyright_year(copyright_line: str, replace_year: str) -> str: if localtime_year == replace_year: return copyright_line - # Do not replace the current year in a copyright notice with an earlier year - if int(replace_year) < int(localtime_year): + # Do not replace the current year in a copyright notice with a future year + if int(replace_year) > int(localtime_year): return copyright_line if len(copyright_line) < 4 or not copyright_line[:4].isdigit(): diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py index f180ac6761c..d81d3663506 100644 --- a/tests/test_config/test_config.py +++ b/tests/test_config/test_config.py @@ -715,7 +715,7 @@ def test_multi_line_copyright(source_date_year, app, monkeypatch): content = (app.outdir / 'index.html').read_text(encoding='utf-8') - if source_date_year is None or int(source_date_year) < int(_LOCALTIME_YEAR): + if source_date_year is None: # check the copyright footer line by line (empty lines ignored) assert ' © Copyright 2006.
\n' in content assert ' © Copyright 2006-2009, Alice.
\n' in content @@ -739,13 +739,15 @@ def test_multi_line_copyright(source_date_year, app, monkeypatch): f' © Copyright 2022-{_LOCALTIME_YEAR}, Eve.' ) in content else: + expected_year = min(int(source_date_year), int(_LOCALTIME_YEAR)) + # check the copyright footer line by line (empty lines ignored) assert ' © Copyright 2006.
\n' in content assert ' © Copyright 2006-2009, Alice.
\n' in content assert ' © Copyright 2010-2013, Bob.
\n' in content assert ' © Copyright 2014-2017, Charlie.
\n' in content assert ' © Copyright 2018-2021, David.
\n' in content - assert f' © Copyright 2022-{source_date_year}, Eve.' in content + assert f' © Copyright 2022-{expected_year}, Eve.' in content # check the raw copyright footer block (empty lines included) assert ( @@ -759,7 +761,7 @@ def test_multi_line_copyright(source_date_year, app, monkeypatch): f' \n' f' © Copyright 2018-2021, David.
\n' f' \n' - f' © Copyright 2022-{source_date_year}, Eve.' + f' © Copyright 2022-{expected_year}, Eve.' ) in content @@ -777,10 +779,11 @@ def test_correct_copyright_year(conf_copyright, expected_copyright, source_date_ correct_copyright_year(_app=None, config=config) actual_copyright = config['copyright'] - if source_date_year is None or int(source_date_year) < int(_LOCALTIME_YEAR): + if source_date_year is None: expected_copyright = conf_copyright else: - expected_copyright = expected_copyright.format(current_year=source_date_year) + expected_year = min(int(source_date_year), int(_LOCALTIME_YEAR)) + expected_copyright = expected_copyright.format(current_year=expected_year) assert actual_copyright == expected_copyright diff --git a/tests/test_config/test_correct_year.py b/tests/test_config/test_correct_year.py index 321fc9d6550..f1eb05f465c 100644 --- a/tests/test_config/test_correct_year.py +++ b/tests/test_config/test_correct_year.py @@ -15,11 +15,11 @@ params=[ # test with SOURCE_DATE_EPOCH unset: no modification (None, _ORIG_CONF_COPYRIGHT), - # test with past SOURCE_DATE_EPOCH set: copyright year should _not_ be updated - ('1293840000', _ORIG_CONF_COPYRIGHT), - ('1293839999', _ORIG_CONF_COPYRIGHT), - # test with +1yr SOURCE_DATE_EPOCH set: copyright year should be updated - (f'{_FUTURE_TIMESTAMP}', _ORIG_CONF_COPYRIGHT.replace(_LOCALTIME_YEAR, _FUTURE_YEAR)), + # test with past SOURCE_DATE_EPOCH set: copyright year should be updated + ('1293840000', '2006-2011'), + ('1293839999', '2006-2010'), + # test with +1yr SOURCE_DATE_EPOCH set: copyright year should _not_ be updated + (f'{_FUTURE_TIMESTAMP}', _ORIG_CONF_COPYRIGHT), ], ) From 92b8d7b600acecc7dbc50d1e3f2b68530fab1062 Mon Sep 17 00:00:00 2001 From: James Addison Date: Sun, 7 Jul 2024 21:27:50 +0100 Subject: [PATCH 06/21] Refactor / cleanup: consolidate constants, and reduce inconsistent typecasting. --- tests/test_config/test_config.py | 11 ++++++----- tests/test_config/test_correct_year.py | 9 +++------ 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py index d81d3663506..dc396c124b7 100644 --- a/tests/test_config/test_config.py +++ b/tests/test_config/test_config.py @@ -31,7 +31,7 @@ CircularDict = dict[str, Union[int, 'CircularDict']] -_LOCALTIME_YEAR = str(time.localtime().tm_year) +_LOCALTIME_YEAR = time.localtime().tm_year def check_is_serializable(subject: object, *, circular: bool) -> None: @@ -739,7 +739,7 @@ def test_multi_line_copyright(source_date_year, app, monkeypatch): f' © Copyright 2022-{_LOCALTIME_YEAR}, Eve.' ) in content else: - expected_year = min(int(source_date_year), int(_LOCALTIME_YEAR)) + expected_year = min(source_date_year, _LOCALTIME_YEAR) # check the copyright footer line by line (empty lines ignored) assert ' © Copyright 2006.
\n' in content @@ -766,13 +766,14 @@ def test_multi_line_copyright(source_date_year, app, monkeypatch): @pytest.mark.parametrize(('conf_copyright', 'expected_copyright'), [ + # static copyright notices ('1970', '1970'), - (_LOCALTIME_YEAR, '{current_year}'), ('1970-1990', '1970-1990'), + ('1970-1990 Alice', '1970-1990 Alice'), # https://github.com/sphinx-doc/sphinx/issues/11913 (f'1970-{_LOCALTIME_YEAR}', '1970-{current_year}'), - ('1970-1990 Alice', '1970-1990 Alice'), (f'1970-{_LOCALTIME_YEAR} Alice', '1970-{current_year} Alice'), + (f'{_LOCALTIME_YEAR}', '{current_year}'), ]) def test_correct_copyright_year(conf_copyright, expected_copyright, source_date_year): config = Config({}, {'copyright': conf_copyright}) @@ -782,7 +783,7 @@ def test_correct_copyright_year(conf_copyright, expected_copyright, source_date_ if source_date_year is None: expected_copyright = conf_copyright else: - expected_year = min(int(source_date_year), int(_LOCALTIME_YEAR)) + expected_year = min(source_date_year, _LOCALTIME_YEAR) expected_copyright = expected_copyright.format(current_year=expected_year) assert actual_copyright == expected_copyright diff --git a/tests/test_config/test_correct_year.py b/tests/test_config/test_correct_year.py index f1eb05f465c..710dfb0650c 100644 --- a/tests/test_config/test_correct_year.py +++ b/tests/test_config/test_correct_year.py @@ -3,12 +3,9 @@ import pytest -_LOCALTIME_YEAR = str(datetime.now().year) # NoQA: DTZ005 -_ORIG_CONF_COPYRIGHT = f'2006-{_LOCALTIME_YEAR}' - +_ORIG_CONF_COPYRIGHT = f'2006-{datetime.now().year}' # NoQA: DTZ005 _FUTURE_MOMENT = datetime.now() + timedelta(days=400) # NoQA: DTZ005 -_FUTURE_TIMESTAMP = int(_FUTURE_MOMENT.timestamp()) -_FUTURE_YEAR = str(_FUTURE_MOMENT.year) +_FUTURE_TIMESTAMP = str(int(_FUTURE_MOMENT.timestamp())) @pytest.fixture( @@ -19,7 +16,7 @@ ('1293840000', '2006-2011'), ('1293839999', '2006-2010'), # test with +1yr SOURCE_DATE_EPOCH set: copyright year should _not_ be updated - (f'{_FUTURE_TIMESTAMP}', _ORIG_CONF_COPYRIGHT), + (_FUTURE_TIMESTAMP, _ORIG_CONF_COPYRIGHT), ], ) From fb7b208be7b573f3a675345a2b7c210cfcee5fcc Mon Sep 17 00:00:00 2001 From: James Addison Date: Sun, 7 Jul 2024 21:52:41 +0100 Subject: [PATCH 07/21] Documentation: add description for ``copyright_build_year_substitution`` confval. --- doc/usage/configuration.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index b283c2fb7d0..fe93d27f383 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -808,6 +808,26 @@ General configuration .. versionadded:: 5.1 +.. confval:: copyright_build_year_substitution + + Default is ``True``. + When the `SOURCE_DATE_EPOCH`__ environment variable is set to a timestamp + in the past, this setting enables substitution of the current year by the + indicated build year. + + .. attention:: + + Copyright year substitution is employed to support bit-for-bit identical + project builds despite :confval:`copyright` configuration that dynamically + refers to the current year. + + For projects that require precise and controlled copyright notices, it is + recommended to configure copyright notices using static string values and + to disable this setting. + + __ https://reproducible-builds.org/specs/source-date-epoch/ + + .. versionadded:: 7.4 .. _intl-options: Options for internationalization From c43c9e11e456126e36e4fcc28232eaf239787e73 Mon Sep 17 00:00:00 2001 From: James Addison Date: Sun, 7 Jul 2024 22:01:38 +0100 Subject: [PATCH 08/21] Documentation: rephrasing for ``copyright_build_year_substitution`` confval. --- doc/usage/configuration.rst | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index fe93d27f383..5b8b8e9bdd6 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -811,18 +811,17 @@ General configuration .. confval:: copyright_build_year_substitution Default is ``True``. - When the `SOURCE_DATE_EPOCH`__ environment variable is set to a timestamp - in the past, this setting enables substitution of the current year by the - indicated build year. + Enables automatic substitution of the `SOURCE_DATE_EPOCH`__ build year in + project copyright notices that mention the current (local-time) year. .. attention:: Copyright year substitution is employed to support bit-for-bit identical - project builds despite :confval:`copyright` configuration that dynamically - refers to the current year. + project builds given the potential for :confval:`copyright` notices to + dynamically include the current year. For projects that require precise and controlled copyright notices, it is - recommended to configure copyright notices using static string values and + recommended to configure copyright notices using static strings, and then to disable this setting. __ https://reproducible-builds.org/specs/source-date-epoch/ From bc9c4c3462e674628fefb78c2f730c1b9a6f43a6 Mon Sep 17 00:00:00 2001 From: James Addison Date: Mon, 8 Jul 2024 22:04:18 +0100 Subject: [PATCH 09/21] Revert "Documentation: rephrasing for ``copyright_build_year_substitution`` confval." This reverts commit c43c9e11e456126e36e4fcc28232eaf239787e73. --- doc/usage/configuration.rst | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 5b8b8e9bdd6..fe93d27f383 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -811,17 +811,18 @@ General configuration .. confval:: copyright_build_year_substitution Default is ``True``. - Enables automatic substitution of the `SOURCE_DATE_EPOCH`__ build year in - project copyright notices that mention the current (local-time) year. + When the `SOURCE_DATE_EPOCH`__ environment variable is set to a timestamp + in the past, this setting enables substitution of the current year by the + indicated build year. .. attention:: Copyright year substitution is employed to support bit-for-bit identical - project builds given the potential for :confval:`copyright` notices to - dynamically include the current year. + project builds despite :confval:`copyright` configuration that dynamically + refers to the current year. For projects that require precise and controlled copyright notices, it is - recommended to configure copyright notices using static strings, and then + recommended to configure copyright notices using static string values and to disable this setting. __ https://reproducible-builds.org/specs/source-date-epoch/ From 6de8b76b7858a7a8dd89684c0d014812c38d1275 Mon Sep 17 00:00:00 2001 From: James Addison Date: Mon, 8 Jul 2024 22:04:21 +0100 Subject: [PATCH 10/21] Revert "Documentation: add description for ``copyright_build_year_substitution`` confval." This reverts commit fb7b208be7b573f3a675345a2b7c210cfcee5fcc. --- doc/usage/configuration.rst | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index fe93d27f383..b283c2fb7d0 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -808,26 +808,6 @@ General configuration .. versionadded:: 5.1 -.. confval:: copyright_build_year_substitution - - Default is ``True``. - When the `SOURCE_DATE_EPOCH`__ environment variable is set to a timestamp - in the past, this setting enables substitution of the current year by the - indicated build year. - - .. attention:: - - Copyright year substitution is employed to support bit-for-bit identical - project builds despite :confval:`copyright` configuration that dynamically - refers to the current year. - - For projects that require precise and controlled copyright notices, it is - recommended to configure copyright notices using static string values and - to disable this setting. - - __ https://reproducible-builds.org/specs/source-date-epoch/ - - .. versionadded:: 7.4 .. _intl-options: Options for internationalization From 49d8b79071654f48a4b48b519385e4ee47541100 Mon Sep 17 00:00:00 2001 From: James Addison Date: Mon, 8 Jul 2024 22:04:22 +0100 Subject: [PATCH 11/21] Revert "Refactor / cleanup: consolidate constants, and reduce inconsistent typecasting." This reverts commit 92b8d7b600acecc7dbc50d1e3f2b68530fab1062. --- tests/test_config/test_config.py | 11 +++++------ tests/test_config/test_correct_year.py | 9 ++++++--- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py index dc396c124b7..d81d3663506 100644 --- a/tests/test_config/test_config.py +++ b/tests/test_config/test_config.py @@ -31,7 +31,7 @@ CircularDict = dict[str, Union[int, 'CircularDict']] -_LOCALTIME_YEAR = time.localtime().tm_year +_LOCALTIME_YEAR = str(time.localtime().tm_year) def check_is_serializable(subject: object, *, circular: bool) -> None: @@ -739,7 +739,7 @@ def test_multi_line_copyright(source_date_year, app, monkeypatch): f' © Copyright 2022-{_LOCALTIME_YEAR}, Eve.' ) in content else: - expected_year = min(source_date_year, _LOCALTIME_YEAR) + expected_year = min(int(source_date_year), int(_LOCALTIME_YEAR)) # check the copyright footer line by line (empty lines ignored) assert ' © Copyright 2006.
\n' in content @@ -766,14 +766,13 @@ def test_multi_line_copyright(source_date_year, app, monkeypatch): @pytest.mark.parametrize(('conf_copyright', 'expected_copyright'), [ - # static copyright notices ('1970', '1970'), + (_LOCALTIME_YEAR, '{current_year}'), ('1970-1990', '1970-1990'), - ('1970-1990 Alice', '1970-1990 Alice'), # https://github.com/sphinx-doc/sphinx/issues/11913 (f'1970-{_LOCALTIME_YEAR}', '1970-{current_year}'), + ('1970-1990 Alice', '1970-1990 Alice'), (f'1970-{_LOCALTIME_YEAR} Alice', '1970-{current_year} Alice'), - (f'{_LOCALTIME_YEAR}', '{current_year}'), ]) def test_correct_copyright_year(conf_copyright, expected_copyright, source_date_year): config = Config({}, {'copyright': conf_copyright}) @@ -783,7 +782,7 @@ def test_correct_copyright_year(conf_copyright, expected_copyright, source_date_ if source_date_year is None: expected_copyright = conf_copyright else: - expected_year = min(source_date_year, _LOCALTIME_YEAR) + expected_year = min(int(source_date_year), int(_LOCALTIME_YEAR)) expected_copyright = expected_copyright.format(current_year=expected_year) assert actual_copyright == expected_copyright diff --git a/tests/test_config/test_correct_year.py b/tests/test_config/test_correct_year.py index 710dfb0650c..f1eb05f465c 100644 --- a/tests/test_config/test_correct_year.py +++ b/tests/test_config/test_correct_year.py @@ -3,9 +3,12 @@ import pytest -_ORIG_CONF_COPYRIGHT = f'2006-{datetime.now().year}' # NoQA: DTZ005 +_LOCALTIME_YEAR = str(datetime.now().year) # NoQA: DTZ005 +_ORIG_CONF_COPYRIGHT = f'2006-{_LOCALTIME_YEAR}' + _FUTURE_MOMENT = datetime.now() + timedelta(days=400) # NoQA: DTZ005 -_FUTURE_TIMESTAMP = str(int(_FUTURE_MOMENT.timestamp())) +_FUTURE_TIMESTAMP = int(_FUTURE_MOMENT.timestamp()) +_FUTURE_YEAR = str(_FUTURE_MOMENT.year) @pytest.fixture( @@ -16,7 +19,7 @@ ('1293840000', '2006-2011'), ('1293839999', '2006-2010'), # test with +1yr SOURCE_DATE_EPOCH set: copyright year should _not_ be updated - (_FUTURE_TIMESTAMP, _ORIG_CONF_COPYRIGHT), + (f'{_FUTURE_TIMESTAMP}', _ORIG_CONF_COPYRIGHT), ], ) From eac77d96734a021c17bf05f225ec77b7a2a671cf Mon Sep 17 00:00:00 2001 From: James Addison Date: Mon, 8 Jul 2024 22:04:48 +0100 Subject: [PATCH 12/21] Revert "Configuration: allow copyright-notice year-substitution feature to be disabled." This reverts commit 7cc52fd8b58496406df0eab5560813b97f859d36. Conflicts: tests/test_config/test_correct_year.py --- sphinx/config.py | 4 ---- tests/test_config/test_correct_year.py | 8 -------- 2 files changed, 12 deletions(-) diff --git a/sphinx/config.py b/sphinx/config.py index 4e77f222190..3bd3c120335 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -266,7 +266,6 @@ class Config: 'smartquotes_excludes': _Opt( {'languages': ['ja'], 'builders': ['man', 'text']}, 'env', ()), 'option_emphasise_placeholders': _Opt(False, 'env', ()), - 'copyright_build_year_substitution': _Opt(True, 'env', ()), } def __init__(self, config: dict[str, Any] | None = None, @@ -611,9 +610,6 @@ def correct_copyright_year(_app: Sphinx, config: Config) -> None: See https://reproducible-builds.org/specs/source-date-epoch/ """ - if not config['copyright_build_year_substitution']: - return - if (source_date_epoch := getenv('SOURCE_DATE_EPOCH')) is None: return diff --git a/tests/test_config/test_correct_year.py b/tests/test_config/test_correct_year.py index f1eb05f465c..1deea399681 100644 --- a/tests/test_config/test_correct_year.py +++ b/tests/test_config/test_correct_year.py @@ -38,11 +38,3 @@ def test_correct_year(expect_date, app): app.build() content = (app.outdir / 'index.html').read_text(encoding='utf8') assert expect_date in content - - -@pytest.mark.sphinx('html', testroot='correct-year', - confoverrides={'copyright_build_year_substitution': False}) -def test_build_year_substitution_disabled(expect_date, app): - app.build() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - assert _ORIG_CONF_COPYRIGHT in content From dea8d4f47675a49a3e9cbbaa48408a6257abf186 Mon Sep 17 00:00:00 2001 From: James Addison Date: Mon, 8 Jul 2024 22:07:27 +0100 Subject: [PATCH 13/21] Edit CHANGES.rst entry. --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 75ef7a3b4f2..b5c914ba04d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -89,8 +89,8 @@ Bugs fixed * #12494: Fix invalid genindex.html file produced with translated docs (regression in 7.1.0). Patch by Nicolas Peugnet. -* #12451: Add further preconditions and a ``copyright_build_year_substitution`` - configuration setting check before copyright notice year substitution occurs. +* #12451: Add further preconditions before copyright notice year substitution + occurs. Patch by James Addison. Testing From 5cde676b84ee1f52b4885daa71c4cc2c9563d9a5 Mon Sep 17 00:00:00 2001 From: James Addison Date: Sat, 28 Sep 2024 22:20:54 +0200 Subject: [PATCH 14/21] Lint fixup: add ruff-suggested linespace between module docstring and imports --- tests/test_config/test_correct_year.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_config/test_correct_year.py b/tests/test_config/test_correct_year.py index e97bc69ffa6..c18d7905986 100644 --- a/tests/test_config/test_correct_year.py +++ b/tests/test_config/test_correct_year.py @@ -1,4 +1,5 @@ """Test copyright year adjustment""" + from datetime import datetime, timedelta import pytest From 56303a7b7d9e230bd85cd0be3844420a82bd18fc Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 3 Oct 2024 20:15:09 +0100 Subject: [PATCH 15/21] Pull static checks out of loop function --- sphinx/config.py | 54 ++++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/sphinx/config.py b/sphinx/config.py index fbfa5a484bb..36f11ce7841 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -625,23 +625,35 @@ 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: - """Replace the current local calendar year when it appears in a single copyright line. +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: @@ -653,33 +665,21 @@ def _substitute_copyright_year(copyright_line: str, replace_year: str) -> str: The final year in the string is replaced with ``replace_year``. """ - localtime_year = str(time.localtime().tm_year) - - # The current year _is_ the replacement year, so replacement would be a no-op - if localtime_year == replace_year: - return copyright_line - - # Do not replace the current year in a copyright notice with a future year - if int(replace_year) > int(localtime_year): - return copyright_line - if len(copyright_line) < 4 or not copyright_line[:4].isdigit(): return copyright_line - if copyright_line[4:5] in {'', ' ', ','}: - if copyright_line[0:4] == localtime_year: - return replace_year + copyright_line[4:] - else: - return copyright_line + 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] == localtime_year: - return copyright_line[:5] + replace_year + copyright_line[9:] - else: - return copyright_line + 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 From 839dc5f5329eb19dfb73c4efa6f5454d143be186 Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 3 Oct 2024 20:43:52 +0100 Subject: [PATCH 16/21] Make test_correct_year less dynamic --- tests/roots/test-correct-year/conf.py | 5 +---- tests/test_config/test_correct_year.py | 28 ++++++++++++-------------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/tests/roots/test-correct-year/conf.py b/tests/roots/test-correct-year/conf.py index 421fab3d505..814c08b55e0 100644 --- a/tests/roots/test-correct-year/conf.py +++ b/tests/roots/test-correct-year/conf.py @@ -1,4 +1 @@ -from datetime import datetime - -# Dynamically evaluated copyright notice, as found in many Sphinx projects -copyright = f'2006-{datetime.today().year}, Author' +copyright = '2006-2009, Author' diff --git a/tests/test_config/test_correct_year.py b/tests/test_config/test_correct_year.py index c18d7905986..c7cb2b8d7ec 100644 --- a/tests/test_config/test_correct_year.py +++ b/tests/test_config/test_correct_year.py @@ -1,31 +1,31 @@ """Test copyright year adjustment""" -from datetime import datetime, timedelta +import time import pytest -_LOCALTIME_YEAR = str(datetime.now().year) # NoQA: DTZ005 -_ORIG_CONF_COPYRIGHT = f'2006-{_LOCALTIME_YEAR}' - -_FUTURE_MOMENT = datetime.now() + timedelta(days=400) # NoQA: DTZ005 -_FUTURE_TIMESTAMP = int(_FUTURE_MOMENT.timestamp()) -_FUTURE_YEAR = str(_FUTURE_MOMENT.year) +_LT = time.localtime() +_LOCALTIME_2009 = type(_LT)((2009, *_LT[1:], _LT.tm_zone, _LT.tm_gmtoff)) @pytest.fixture( params=[ # test with SOURCE_DATE_EPOCH unset: no modification - (None, _ORIG_CONF_COPYRIGHT), + (None, '2006-2009'), # test with past SOURCE_DATE_EPOCH set: copyright year should be updated - ('1293840000', '2006-2011'), - ('1293839999', '2006-2010'), - # test with +1yr SOURCE_DATE_EPOCH set: copyright year should _not_ be updated - (f'{_FUTURE_TIMESTAMP}', _ORIG_CONF_COPYRIGHT), + ('1199145600', '2006-2008'), + ('1199145599', '2006-2007'), + # test with future SOURCE_DATE_EPOCH set: copyright year should not be updated + ('1293840000', '2006-2009'), + ('1293839999', '2006-2009'), + ], ) def expect_date(request, monkeypatch): + sde, expect = request.param with monkeypatch.context() as m: + m.setattr(time, 'localtime', lambda *a: _LOCALTIME_2009) if sde: m.setenv('SOURCE_DATE_EPOCH', sde) else: @@ -35,6 +35,4 @@ def expect_date(request, monkeypatch): @pytest.mark.sphinx('html', testroot='correct-year') def test_correct_year(expect_date, app): - app.build() - content = (app.outdir / 'index.html').read_text(encoding='utf8') - assert expect_date in content + assert expect_date in app.config.copyright From 683267e06ec01170c1415c11e23d583cf4a612ba Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+aa-turner@users.noreply.github.com> Date: Thu, 3 Oct 2024 22:07:16 +0100 Subject: [PATCH 17/21] post-merge --- tests/roots/test-copyright-multiline/conf.py | 4 +- .../test_build_html_copyright.py | 39 +++++++++++-------- tests/test_config/test_correct_year.py | 29 +++++++------- 3 files changed, 38 insertions(+), 34 deletions(-) diff --git a/tests/roots/test-copyright-multiline/conf.py b/tests/roots/test-copyright-multiline/conf.py index 4cc2833bf8f..a2b7b680100 100644 --- a/tests/roots/test-copyright-multiline/conf.py +++ b/tests/roots/test-copyright-multiline/conf.py @@ -1,11 +1,9 @@ -from datetime import datetime - copyright = ( '2006', '2006-2009, Alice', '2010-2013, Bob', '2014-2017, Charlie', '2018-2021, David', - f'2022-{datetime.today().year}, Eve', + '2022-2025, Eve', ) html_theme = 'basic' diff --git a/tests/test_builders/test_build_html_copyright.py b/tests/test_builders/test_build_html_copyright.py index 2ddc50532aa..8e017ede4d7 100644 --- a/tests/test_builders/test_build_html_copyright.py +++ b/tests/test_builders/test_build_html_copyright.py @@ -2,16 +2,21 @@ 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}.
\n' in content + assert ' © Copyright 2006.
\n' in content assert f' © Copyright 2006-{source_date_year}, Alice.
\n' in content - assert f' © Copyright 2010-{source_date_year}, Bob.
\n' in content - assert f' © Copyright 2014-{source_date_year}, Charlie.
\n' in content - assert f' © Copyright 2018-{source_date_year}, David.
\n' in content - assert f' © Copyright 2022-{source_date_year}, Eve.' in content + assert ' © Copyright 2010-2013, Bob.
\n' in content + assert ' © Copyright 2014-2017, Charlie.
\n' in content + assert ' © Copyright 2018-2021, David.
\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}.
\n' - f' \n' + ' © Copyright 2006.
\n' + ' \n' f' © Copyright 2006-{source_date_year}, Alice.
\n' - f' \n' - f' © Copyright 2010-{source_date_year}, Bob.
\n' - f' \n' - f' © Copyright 2014-{source_date_year}, Charlie.
\n' - f' \n' - f' © Copyright 2018-{source_date_year}, David.
\n' - f' \n' - f' © Copyright 2022-{source_date_year}, Eve.' + ' \n' + ' © Copyright 2010-2013, Bob.
\n' + ' \n' + ' © Copyright 2014-2017, Charlie.
\n' + ' \n' + ' © Copyright 2018-2021, David.
\n' + ' \n' + ' © Copyright 2022-2025, Eve.' ) in content diff --git a/tests/test_config/test_correct_year.py b/tests/test_config/test_correct_year.py index 90fa4539775..33e6c8e4891 100644 --- a/tests/test_config/test_correct_year.py +++ b/tests/test_config/test_correct_year.py @@ -40,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 <= '2009': assert cfg.copyright == f'2006-{expect_date}, Alice' else: assert cfg.copyright == copyright_date @@ -52,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 <= '2009': assert cfg.copyright == f'2006-{expect_date} Alice' else: assert cfg.copyright == copyright_date @@ -64,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 <= '2009': assert cfg.copyright == f'2006-{expect_date}' else: assert cfg.copyright == copyright_date @@ -76,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 <= '2009': assert cfg.copyright == f'{expect_date}, Alice' else: assert cfg.copyright == copyright_date @@ -88,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 <= '2009': assert cfg.copyright == f'{expect_date} Alice' else: assert cfg.copyright == copyright_date @@ -100,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 <= '2009': assert cfg.copyright == f'{expect_date}' else: assert cfg.copyright == copyright_date @@ -109,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', @@ -119,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 <= '2009': 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 @@ -145,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 <= '2009': assert cfg.copyright == ( f'{expect_date}', f'{expect_date} Alice', @@ -167,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 <= '2009': assert app.config.copyright == f'2006-{expect_date}, Alice' else: assert app.config.copyright == copyright_date From dfe92ff89b4e0655773b45e07e3b93cc497ddd1f Mon Sep 17 00:00:00 2001 From: James Addison Date: Fri, 4 Oct 2024 00:38:45 +0100 Subject: [PATCH 18/21] Rephrase CHANGES.rst entry Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com> --- CHANGES.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index da2472b728c..a8ce6ea2cd3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -110,9 +110,9 @@ 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: Add further preconditions before copyright notice year substitution - occurs. - 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. + Patch by James Addison and Adam Turner. Testing ------- From bf5a8051163e4ab5b105c0270bf8642af5ed5f8e Mon Sep 17 00:00:00 2001 From: James Addison Date: Fri, 4 Oct 2024 00:42:43 +0100 Subject: [PATCH 19/21] Rephrase CHANGES.rst entry Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com> --- CHANGES.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index a8ce6ea2cd3..bc46d34638f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -111,7 +111,8 @@ Bugs fixed 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. + ``SOURCE_DATE_EPOCH`` entries that match the current system clock year, and + disallow substitution of future years. Patch by James Addison and Adam Turner. Testing From 899f76bff18cf51971f11c5315e101af3aa26088 Mon Sep 17 00:00:00 2001 From: James Addison Date: Fri, 4 Oct 2024 00:47:46 +0100 Subject: [PATCH 20/21] Fixup: restore missing word in CHANGES.rst entry --- CHANGES.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index bc46d34638f..55fb2cf8f6c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -111,8 +111,8 @@ Bugs fixed 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`` entries that match the current system clock year, and - disallow substitution of future years. + ``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 From 312201dfc7c1f408fc75de9b60b8b29f90d69f69 Mon Sep 17 00:00:00 2001 From: James Addison Date: Fri, 4 Oct 2024 01:04:29 +0100 Subject: [PATCH 21/21] Tests: refactor to reference `LOCALTIME_2009` variable --- tests/test_config/test_correct_year.py | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/test_config/test_correct_year.py b/tests/test_config/test_correct_year.py index 33e6c8e4891..7aafe5a66f5 100644 --- a/tests/test_config/test_correct_year.py +++ b/tests/test_config/test_correct_year.py @@ -14,13 +14,13 @@ @pytest.fixture( params=[ # test with SOURCE_DATE_EPOCH unset: no modification - (None, ''), + (None, None), # test with post-2009 SOURCE_DATE_EPOCH set: copyright year should not be updated - ('1293840000', '2011'), - ('1293839999', '2010'), + ('1293840000', 2011), + ('1293839999', 2010), # test with pre-2009 SOURCE_DATE_EPOCH set: copyright year should be updated - ('1199145600', '2008'), - ('1199145599', '2007'), + ('1199145600', 2008), + ('1199145599', 2007), ], ) def expect_date(request, monkeypatch): @@ -40,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 and expect_date <= '2009': + if expect_date and expect_date <= LOCALTIME_2009.tm_year: assert cfg.copyright == f'2006-{expect_date}, Alice' else: assert cfg.copyright == copyright_date @@ -52,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 and expect_date <= '2009': + if expect_date and expect_date <= LOCALTIME_2009.tm_year: assert cfg.copyright == f'2006-{expect_date} Alice' else: assert cfg.copyright == copyright_date @@ -64,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 and expect_date <= '2009': + if expect_date and expect_date <= LOCALTIME_2009.tm_year: assert cfg.copyright == f'2006-{expect_date}' else: assert cfg.copyright == copyright_date @@ -76,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 and expect_date <= '2009': + if expect_date and expect_date <= LOCALTIME_2009.tm_year: assert cfg.copyright == f'{expect_date}, Alice' else: assert cfg.copyright == copyright_date @@ -88,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 and expect_date <= '2009': + if expect_date and expect_date <= LOCALTIME_2009.tm_year: assert cfg.copyright == f'{expect_date} Alice' else: assert cfg.copyright == copyright_date @@ -100,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 and expect_date <= '2009': + if expect_date and expect_date <= LOCALTIME_2009.tm_year: assert cfg.copyright == f'{expect_date}' else: assert cfg.copyright == copyright_date @@ -119,7 +119,7 @@ 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 and expect_date <= '2009': + if expect_date and expect_date <= LOCALTIME_2009.tm_year: assert cfg.copyright == ( f'{expect_date}', f'2006-{expect_date}, Alice', @@ -146,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 and expect_date <= '2009': + if expect_date and expect_date <= LOCALTIME_2009.tm_year: assert cfg.copyright == ( f'{expect_date}', f'{expect_date} Alice', @@ -168,7 +168,7 @@ def test_correct_year_app(expect_date, tmp_path, make_app): srcdir=tmp_path, confoverrides={'copyright': copyright_date}, ) - if expect_date and expect_date <= '2009': + 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