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
|
Bugs fixed
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
* #11514: Fix ``SOURCE_DATE_EPOCH`` in multi-line copyright footer.
|
||||||
|
Patch by Bénédikt Tran.
|
||||||
|
|
||||||
Testing
|
Testing
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
import types
|
import types
|
||||||
from os import getenv, path
|
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.errors import ConfigError, ExtensionError
|
||||||
from sphinx.locale import _, __
|
from sphinx.locale import _, __
|
||||||
from sphinx.util import logging
|
from sphinx.util import logging
|
||||||
from sphinx.util.i18n import format_date
|
|
||||||
from sphinx.util.osutil import fs_encoding
|
from sphinx.util.osutil import fs_encoding
|
||||||
from sphinx.util.tags import Tags
|
from sphinx.util.tags import Tags
|
||||||
from sphinx.util.typing import NoneType
|
from sphinx.util.typing import NoneType
|
||||||
@ -22,6 +21,8 @@ except ImportError:
|
|||||||
from sphinx.util.osutil import _chdir as chdir
|
from sphinx.util.osutil import _chdir as chdir
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
from sphinx.application import Sphinx
|
from sphinx.application import Sphinx
|
||||||
from sphinx.environment import BuildEnvironment
|
from sphinx.environment import BuildEnvironment
|
||||||
|
|
||||||
@ -29,7 +30,6 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
CONFIG_FILENAME = 'conf.py'
|
CONFIG_FILENAME = 'conf.py'
|
||||||
UNSERIALIZABLE_TYPES = (type, types.ModuleType, types.FunctionType)
|
UNSERIALIZABLE_TYPES = (type, types.ModuleType, types.FunctionType)
|
||||||
copyright_year_re = re.compile(r'^((\d{4}-)?)(\d{4})(?=[ ,])')
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigValue(NamedTuple):
|
class ConfigValue(NamedTuple):
|
||||||
@ -417,17 +417,52 @@ def init_numfig_format(app: Sphinx, config: Config) -> None:
|
|||||||
config.numfig_format = numfig_format # type: ignore
|
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
|
"""Correct values of copyright year that are not coherent with
|
||||||
the SOURCE_DATE_EPOCH environment variable (if set)
|
the SOURCE_DATE_EPOCH environment variable (if set)
|
||||||
|
|
||||||
See https://reproducible-builds.org/specs/source-date-epoch/
|
See https://reproducible-builds.org/specs/source-date-epoch/
|
||||||
"""
|
"""
|
||||||
if getenv('SOURCE_DATE_EPOCH') is not None:
|
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'):
|
for k in ('copyright', 'epub_copyright'):
|
||||||
if k in config:
|
if k in config:
|
||||||
replace = r'\g<1>%s' % format_date('%Y', language='en')
|
value: str | Sequence[str] = config[k]
|
||||||
config[k] = copyright_year_re.sub(replace, 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:
|
def check_confval_types(app: Sphinx | None, config: Config) -> None:
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
"""Test the sphinx.config.Config class."""
|
"""Test the sphinx.config.Config class."""
|
||||||
|
|
||||||
|
import time
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -444,23 +445,67 @@ def test_conf_py_nitpick_ignore_list(tempdir):
|
|||||||
assert cfg.nitpick_ignore_regex == []
|
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')
|
@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()
|
app.builder.build_all()
|
||||||
|
|
||||||
content = (app.outdir / 'index.html').read_text(encoding='utf-8')
|
content = (app.outdir / 'index.html').read_text(encoding='utf-8')
|
||||||
|
|
||||||
assert ' © Copyright 2006-2009, Alice.<br/>' in content
|
if source_date_year is None:
|
||||||
assert ' © Copyright 2010-2013, Bob.<br/>' in content
|
# check the copyright footer line by line (empty lines ignored)
|
||||||
assert ' © Copyright 2014-2017, Charlie.<br/>' in content
|
assert ' © Copyright 2006-2009, Alice.<br/>\n' in content
|
||||||
assert ' © Copyright 2018-2021, David.<br/>' 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
|
assert ' © Copyright 2022-2025, Eve.' in content
|
||||||
|
|
||||||
lines = (
|
# check the raw copyright footer block (empty lines included)
|
||||||
' © Copyright 2006-2009, Alice.<br/>\n \n'
|
assert (
|
||||||
' © Copyright 2010-2013, Bob.<br/>\n \n'
|
' © Copyright 2006-2009, Alice.<br/>\n'
|
||||||
' © Copyright 2014-2017, Charlie.<br/>\n \n'
|
' \n'
|
||||||
' © Copyright 2018-2021, David.<br/>\n \n'
|
' © Copyright 2010-2013, Bob.<br/>\n'
|
||||||
' © Copyright 2022-2025, Eve.\n \n'
|
' \n'
|
||||||
)
|
' © Copyright 2014-2017, Charlie.<br/>\n'
|
||||||
assert lines in content
|
' \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