mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Fix multi-line copyright when `SOURCE_DATE_EPOCH
` is set (#11524)
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
This commit is contained in:
parent
fe08cec019
commit
8452300d54
3
CHANGES
3
CHANGES
@ -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
|
||||
-------
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
import types
|
||||
from os import getenv, path
|
||||
@ -11,7 +11,6 @@ from typing import TYPE_CHECKING, Any, Callable, Generator, Iterator, NamedTuple
|
||||
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
|
||||
@ -22,6 +21,8 @@ except ImportError:
|
||||
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
|
||||
|
||||
@ -29,7 +30,6 @@ 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):
|
||||
@ -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:
|
||||
|
@ -1,5 +1,6 @@
|
||||
"""Test the sphinx.config.Config class."""
|
||||
|
||||
import time
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
@ -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 ' © Copyright 2006-2009, Alice.<br/>' in content
|
||||
assert ' © Copyright 2010-2013, Bob.<br/>' in content
|
||||
assert ' © Copyright 2014-2017, Charlie.<br/>' in content
|
||||
assert ' © Copyright 2018-2021, David.<br/>' in content
|
||||
assert ' © Copyright 2022-2025, Eve.' in content
|
||||
if source_date_year is None:
|
||||
# check the copyright footer line by line (empty lines ignored)
|
||||
assert ' © Copyright 2006-2009, Alice.<br/>\n' 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
|
||||
|
||||
lines = (
|
||||
' © Copyright 2006-2009, Alice.<br/>\n \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.\n \n'
|
||||
)
|
||||
assert lines in content
|
||||
# check the raw copyright footer block (empty lines included)
|
||||
assert (
|
||||
' © Copyright 2006-2009, Alice.<br/>\n'
|
||||
' \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
|
||||
else:
|
||||
# check the copyright footer line by line (empty lines ignored)
|
||||
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
|
||||
|
||||
# check the raw copyright footer block (empty lines included)
|
||||
assert (
|
||||
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.'
|
||||
) in content
|
||||
|
Loading…
Reference in New Issue
Block a user