Skip to content

Commit

Permalink
Fix multi-line copyright when SOURCE_DATE_EPOCH is set (#11524)
Browse files Browse the repository at this point in the history
Co-authored-by: Bénédikt Tran <[email protected]>
  • Loading branch information
AA-Turner and picnixz authored Jul 27, 2023
1 parent fe08cec commit 8452300
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 24 deletions.
3 changes: 3 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ Features added
Bugs fixed
----------

* #11514: Fix ``SOURCE_DATE_EPOCH`` in multi-line copyright footer.
Patch by Bénédikt Tran.

Testing
-------

Expand Down
53 changes: 44 additions & 9 deletions sphinx/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from __future__ import annotations

import re
import time
import traceback
import types
from os import getenv, path
Expand All @@ -11,7 +11,6 @@
from sphinx.errors import ConfigError, ExtensionError
from sphinx.locale import _, __
from sphinx.util import logging
from sphinx.util.i18n import format_date
from sphinx.util.osutil import fs_encoding
from sphinx.util.tags import Tags
from sphinx.util.typing import NoneType
Expand All @@ -22,14 +21,15 @@
from sphinx.util.osutil import _chdir as chdir

if TYPE_CHECKING:
from collections.abc import Sequence

from sphinx.application import Sphinx
from sphinx.environment import BuildEnvironment

logger = logging.getLogger(__name__)

CONFIG_FILENAME = 'conf.py'
UNSERIALIZABLE_TYPES = (type, types.ModuleType, types.FunctionType)
copyright_year_re = re.compile(r'^((\d{4}-)?)(\d{4})(?=[ ,])')


class ConfigValue(NamedTuple):
Expand Down Expand Up @@ -417,17 +417,52 @@ def init_numfig_format(app: Sphinx, config: Config) -> None:
config.numfig_format = numfig_format # type: ignore


def correct_copyright_year(app: Sphinx, config: Config) -> None:
def correct_copyright_year(_app: Sphinx, config: Config) -> None:
"""Correct values of copyright year that are not coherent with
the SOURCE_DATE_EPOCH environment variable (if set)
See https://reproducible-builds.org/specs/source-date-epoch/
"""
if getenv('SOURCE_DATE_EPOCH') is not None:
for k in ('copyright', 'epub_copyright'):
if k in config:
replace = r'\g<1>%s' % format_date('%Y', language='en')
config[k] = copyright_year_re.sub(replace, config[k])
if (source_date_epoch := getenv('SOURCE_DATE_EPOCH')) is None:
return

source_date_epoch_year = str(time.gmtime(int(source_date_epoch)).tm_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)
else:
items = (_substitute_copyright_year(x, source_date_epoch_year) 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 year in a single copyright line.
Legal formats are:
* ``YYYY,``
* ``YYYY ``
* ``YYYY-YYYY,``
* ``YYYY-YYYY ``
The final year in the string is replaced with ``replace_year``.
"""
if not copyright_line[:4].isdigit():
return copyright_line

if copyright_line[4] in ' ,':
return replace_year + copyright_line[4:]

if copyright_line[4] != '-':
return copyright_line

if copyright_line[5:9].isdigit() and copyright_line[9] in ' ,':
return copyright_line[:5] + replace_year + copyright_line[9:]

return copyright_line


def check_confval_types(app: Sphinx | None, config: Config) -> None:
Expand Down
75 changes: 60 additions & 15 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Test the sphinx.config.Config class."""

import time
from unittest import mock

import pytest
Expand Down Expand Up @@ -444,23 +445,67 @@ def test_conf_py_nitpick_ignore_list(tempdir):
assert cfg.nitpick_ignore_regex == []


@pytest.fixture(params=[
# test with SOURCE_DATE_EPOCH unset: no modification
None,
# test with SOURCE_DATE_EPOCH set: copyright year should be updated
1293840000,
1293839999,
])
def source_date_year(request, monkeypatch):
sde = request.param
with monkeypatch.context() as m:
if sde:
m.setenv('SOURCE_DATE_EPOCH', sde)
yield time.gmtime(sde).tm_year
else:
m.delenv('SOURCE_DATE_EPOCH', raising=False)
yield None


@pytest.mark.sphinx(testroot='copyright-multiline')
def test_multi_line_copyright(app, status, warning):
def test_multi_line_copyright(source_date_year, app, monkeypatch):
app.builder.build_all()

content = (app.outdir / 'index.html').read_text(encoding='utf-8')

assert ' &#169; Copyright 2006-2009, Alice.<br/>' in content
assert ' &#169; Copyright 2010-2013, Bob.<br/>' in content
assert ' &#169; Copyright 2014-2017, Charlie.<br/>' in content
assert ' &#169; Copyright 2018-2021, David.<br/>' in content
assert ' &#169; Copyright 2022-2025, Eve.' in content

lines = (
' &#169; Copyright 2006-2009, Alice.<br/>\n \n'
' &#169; Copyright 2010-2013, Bob.<br/>\n \n'
' &#169; Copyright 2014-2017, Charlie.<br/>\n \n'
' &#169; Copyright 2018-2021, David.<br/>\n \n'
' &#169; Copyright 2022-2025, Eve.\n \n'
)
assert lines in content
if source_date_year is None:
# check the copyright footer line by line (empty lines ignored)
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 ' &#169; Copyright 2022-2025, Eve.' in content

# check the raw copyright footer block (empty lines included)
assert (
' &#169; Copyright 2006-2009, Alice.<br/>\n'
' \n'
' &#169; Copyright 2010-2013, Bob.<br/>\n'
' \n'
' &#169; Copyright 2014-2017, Charlie.<br/>\n'
' \n'
' &#169; Copyright 2018-2021, David.<br/>\n'
' \n'
' &#169; Copyright 2022-2025, Eve.'
) in content
else:
# check the copyright footer line by line (empty lines ignored)
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

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

0 comments on commit 8452300

Please sign in to comment.