Skip to content
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

copyright: Apply substitutions only to current-year entries, and disallow future year insertion #12516

Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e083001
Configuration: add two preconditions for copyright-notice year-substi…
jayaddison Jul 7, 2024
7cc52fd
Configuration: allow copyright-notice year-substitution feature to be…
jayaddison Jul 7, 2024
f6d737f
Tests: refactor: attempt to improve readability by extracting copyrig…
jayaddison Jul 7, 2024
f8002ee
Add CHANGES.rst entry.
jayaddison Jul 7, 2024
8656fec
Precondition fixup: replacement (from build date / `SOURCE_DATE_EPOCH…
jayaddison Jul 7, 2024
92b8d7b
Refactor / cleanup: consolidate constants, and reduce inconsistent ty…
jayaddison Jul 7, 2024
fb7b208
Documentation: add description for ``copyright_build_year_substitutio…
jayaddison Jul 7, 2024
c43c9e1
Documentation: rephrasing for ``copyright_build_year_substitution`` c…
jayaddison Jul 7, 2024
cd65435
Merge branch 'master' into issue-12451/reproducible-copyright-substit…
jayaddison Jul 7, 2024
bc9c4c3
Revert "Documentation: rephrasing for ``copyright_build_year_substitu…
jayaddison Jul 8, 2024
6de8b76
Revert "Documentation: add description for ``copyright_build_year_sub…
jayaddison Jul 8, 2024
49d8b79
Revert "Refactor / cleanup: consolidate constants, and reduce inconsi…
jayaddison Jul 8, 2024
eac77d9
Revert "Configuration: allow copyright-notice year-substitution featu…
jayaddison Jul 8, 2024
dea8d4f
Edit CHANGES.rst entry.
jayaddison Jul 8, 2024
7ff5865
Merge branch 'master' into issue-12451/reproducible-copyright-substit…
jayaddison Jul 8, 2024
33ea51a
Merge branch 'master' into issue-12451/reproducible-copyright-substit…
jayaddison Sep 28, 2024
5cde676
Lint fixup: add ruff-suggested linespace between module docstring and…
jayaddison Sep 28, 2024
3cf86cc
Merge branch 'master' into issue-12451/reproducible-copyright-substit…
AA-Turner Oct 3, 2024
56303a7
Pull static checks out of loop function
AA-Turner Oct 3, 2024
839dc5f
Make test_correct_year less dynamic
AA-Turner Oct 3, 2024
e8258a9
Merge branch 'master' into issue-12451/reproducible-copyright-substit…
AA-Turner Oct 3, 2024
683267e
post-merge
AA-Turner Oct 3, 2024
dfe92ff
Rephrase CHANGES.rst entry
jayaddison Oct 3, 2024
bf5a805
Rephrase CHANGES.rst entry
jayaddison Oct 3, 2024
899f76b
Fixup: restore missing word in CHANGES.rst entry
jayaddison Oct 3, 2024
312201d
Tests: refactor to reference `LOCALTIME_2009` variable
jayaddison Oct 4, 2024
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 @@ -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
-------
Expand Down
19 changes: 19 additions & 0 deletions doc/usage/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,25 @@ General configuration

.. versionadded:: 5.1

.. confval:: copyright_build_year_substitution
jayaddison marked this conversation as resolved.
Show resolved Hide resolved

Default is ``True``.
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 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 strings, and then
to disable this setting.

__ https://reproducible-builds.org/specs/source-date-epoch/

.. versionadded:: 7.4
.. _intl-options:

Options for internationalization
Expand Down
26 changes: 23 additions & 3 deletions sphinx/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand All @@ -626,7 +630,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:

Expand All @@ -638,17 +642,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 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 {'', ' ', ','}:
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

Expand Down
4 changes: 3 additions & 1 deletion tests/roots/test-copyright-multiline/conf.py
Original file line number Diff line number Diff line change
@@ -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'
5 changes: 4 additions & 1 deletion tests/roots/test-correct-year/conf.py
Original file line number Diff line number Diff line change
@@ -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'
46 changes: 28 additions & 18 deletions tests/test_config/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
CircularDict = dict[str, Union[int, 'CircularDict']]


_LOCALTIME_YEAR = time.localtime().tm_year


def check_is_serializable(subject: object, *, circular: bool) -> None:
assert is_serializable(subject)

Expand Down Expand Up @@ -719,7 +722,7 @@ def test_multi_line_copyright(source_date_year, app, monkeypatch):
assert ' &#169; Copyright 2010-2013, Bob.<br/>\n' in content
assert ' &#169; Copyright 2014-2017, Charlie.<br/>\n' in content
assert ' &#169; Copyright 2018-2021, David.<br/>\n' in content
assert ' &#169; Copyright 2022-2025, Eve.' in content
assert f' &#169; Copyright 2022-{_LOCALTIME_YEAR}, Eve.' in content

# check the raw copyright footer block (empty lines included)
assert (
Expand All @@ -733,38 +736,44 @@ def test_multi_line_copyright(source_date_year, app, monkeypatch):
' \n'
' &#169; Copyright 2018-2021, David.<br/>\n'
' \n'
' &#169; Copyright 2022-2025, Eve.'
f' &#169; Copyright 2022-{_LOCALTIME_YEAR}, Eve.'
) in content
else:
expected_year = min(source_date_year, _LOCALTIME_YEAR)

# check the copyright footer line by line (empty lines ignored)
assert f' &#169; Copyright {source_date_year}.<br/>\n' in content
assert f' &#169; Copyright 2006-{source_date_year}, Alice.<br/>\n' in content
assert f' &#169; Copyright 2010-{source_date_year}, Bob.<br/>\n' in content
assert f' &#169; Copyright 2014-{source_date_year}, Charlie.<br/>\n' in content
assert f' &#169; Copyright 2018-{source_date_year}, David.<br/>\n' in content
assert f' &#169; Copyright 2022-{source_date_year}, Eve.' in content
assert ' &#169; Copyright 2006.<br/>\n' in content
assert ' &#169; Copyright 2006-2009, Alice.<br/>\n' in content
assert ' &#169; Copyright 2010-2013, Bob.<br/>\n' in content
assert ' &#169; Copyright 2014-2017, Charlie.<br/>\n' in content
assert ' &#169; Copyright 2018-2021, David.<br/>\n' in content
assert f' &#169; Copyright 2022-{expected_year}, Eve.' in content

# check the raw copyright footer block (empty lines included)
assert (
f' &#169; Copyright {source_date_year}.<br/>\n'
f' &#169; Copyright 2006.<br/>\n'
f' \n'
f' &#169; Copyright 2006-{source_date_year}, Alice.<br/>\n'
f' &#169; Copyright 2006-2009, Alice.<br/>\n'
f' \n'
f' &#169; Copyright 2010-{source_date_year}, Bob.<br/>\n'
f' &#169; Copyright 2010-2013, Bob.<br/>\n'
f' \n'
f' &#169; Copyright 2014-{source_date_year}, Charlie.<br/>\n'
f' &#169; Copyright 2014-2017, Charlie.<br/>\n'
f' \n'
f' &#169; Copyright 2018-{source_date_year}, David.<br/>\n'
f' &#169; Copyright 2018-2021, David.<br/>\n'
f' \n'
f' &#169; Copyright 2022-{source_date_year}, Eve.'
f' &#169; Copyright 2022-{expected_year}, Eve.'
) in content


@pytest.mark.parametrize(('conf_copyright', 'expected_copyright'), [
('1970', '{current_year}'),
# static copyright notices
('1970', '1970'),
('1970-1990', '1970-1990'),
('1970-1990 Alice', '1970-1990 Alice'),
# 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}'),
(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})
Expand All @@ -774,7 +783,8 @@ def test_correct_copyright_year(conf_copyright, expected_copyright, source_date_
if source_date_year is None:
expected_copyright = conf_copyright
else:
expected_copyright = expected_copyright.format(current_year=source_date_year)
expected_year = min(source_date_year, _LOCALTIME_YEAR)
expected_copyright = expected_copyright.format(current_year=expected_year)
assert actual_copyright == expected_copyright


Expand Down
20 changes: 18 additions & 2 deletions tests/test_config/test_correct_year.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
"""Test copyright year adjustment"""
from datetime import datetime, timedelta

import pytest

_ORIG_CONF_COPYRIGHT = f'2006-{datetime.now().year}' # NoQA: DTZ005
_FUTURE_MOMENT = datetime.now() + timedelta(days=400) # NoQA: DTZ005
_FUTURE_TIMESTAMP = str(int(_FUTURE_MOMENT.timestamp()))


@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
(None, _ORIG_CONF_COPYRIGHT),
# 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
(_FUTURE_TIMESTAMP, _ORIG_CONF_COPYRIGHT),
],

)
Expand All @@ -27,3 +35,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 _ORIG_CONF_COPYRIGHT in content