diff --git a/sphinx/config.py b/sphinx/config.py
index 0fd2102e732..3096cef0359 100644
--- a/sphinx/config.py
+++ b/sphinx/config.py
@@ -648,6 +648,7 @@ def _substitute_copyright_year(copyright_line: str, replace_year: str) -> str:
* ``YYYY``
* ``YYYY,``
* ``YYYY ``
+ * ``YYYY-YYYY``
* ``YYYY-YYYY,``
* ``YYYY-YYYY ``
diff --git a/tests/roots/test-copyright-multiline/index.rst b/tests/roots/test-copyright-multiline/index.rst
index aa32ae60f4a..e69de29bb2d 100644
--- a/tests/roots/test-copyright-multiline/index.rst
+++ b/tests/roots/test-copyright-multiline/index.rst
@@ -1,3 +0,0 @@
-========================
-test-copyright-multiline
-========================
diff --git a/tests/roots/test-correct-year/conf.py b/tests/roots/test-correct-year/conf.py
deleted file mode 100644
index 814c08b55e0..00000000000
--- a/tests/roots/test-correct-year/conf.py
+++ /dev/null
@@ -1 +0,0 @@
-copyright = '2006-2009, Author'
diff --git a/tests/roots/test-correct-year/index.rst b/tests/roots/test-correct-year/index.rst
deleted file mode 100644
index 938dfd50310..00000000000
--- a/tests/roots/test-correct-year/index.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-=================
-test-correct-year
-=================
-
diff --git a/tests/test_builders/test_build_html_copyright.py b/tests/test_builders/test_build_html_copyright.py
new file mode 100644
index 00000000000..2ddc50532aa
--- /dev/null
+++ b/tests/test_builders/test_build_html_copyright.py
@@ -0,0 +1,76 @@
+import time
+
+import pytest
+
+
+@pytest.fixture(
+ params=[
+ 1293840000, # 2011-01-01 00:00:00
+ 1293839999, # 2010-12-31 23:59:59
+ ]
+)
+def source_date_year(request, monkeypatch):
+ source_date_epoch = request.param
+ with monkeypatch.context() as m:
+ m.setenv('SOURCE_DATE_EPOCH', str(source_date_epoch))
+ yield time.gmtime(source_date_epoch).tm_year
+
+
+@pytest.mark.sphinx('html', testroot='copyright-multiline')
+def test_html_multi_line_copyright(app):
+ app.build(force_all=True)
+
+ content = (app.outdir / 'index.html').read_text(encoding='utf-8')
+
+ # 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
+
+ # check the raw copyright footer block (empty lines included)
+ assert (
+ ' © Copyright 2006.
\n'
+ ' \n'
+ ' © Copyright 2006-2009, Alice.
\n'
+ ' \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
+
+
+@pytest.mark.sphinx('html', testroot='copyright-multiline')
+def test_html_multi_line_copyright_sde(source_date_year, app):
+ app.build(force_all=True)
+
+ 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 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
+
+ # check the raw copyright footer block (empty lines included)
+ assert (
+ f' © Copyright {source_date_year}.
\n'
+ f' \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.'
+ ) in content
diff --git a/tests/test_config/test_config.py b/tests/test_config/test_config.py
index 7f748f11e70..f1d6c12a0fd 100644
--- a/tests/test_config/test_config.py
+++ b/tests/test_config/test_config.py
@@ -3,7 +3,6 @@
from __future__ import annotations
import pickle
-import time
from collections import Counter
from pathlib import Path
from typing import TYPE_CHECKING, Any
@@ -18,7 +17,6 @@
Config,
_Opt,
check_confval_types,
- correct_copyright_year,
is_serializable,
)
from sphinx.deprecation import RemovedInSphinx90Warning
@@ -741,101 +739,6 @@ def test_conf_py_nitpick_ignore_list(tmp_path):
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', str(sde))
- yield time.gmtime(sde).tm_year
- else:
- m.delenv('SOURCE_DATE_EPOCH', raising=False)
- yield None
-
-
-@pytest.mark.sphinx('html', testroot='copyright-multiline')
-def test_multi_line_copyright(source_date_year, app):
- app.build(force_all=True)
-
- content = (app.outdir / 'index.html').read_text(encoding='utf-8')
-
- 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
- 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 (
- ' © Copyright 2006.
\n'
- ' \n'
- ' © Copyright 2006-2009, Alice.
\n'
- ' \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
- 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 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' \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.'
- ) in content
-
-
-@pytest.mark.parametrize(
- ('conf_copyright', 'expected_copyright'),
- [
- ('1970', '{current_year}'),
- # https://github.com/sphinx-doc/sphinx/issues/11913
- ('1970-1990', '1970-{current_year}'),
- ('1970-1990 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:
- expected_copyright = conf_copyright
- else:
- expected_copyright = expected_copyright.format(current_year=source_date_year)
- assert actual_copyright == expected_copyright
-
-
def test_gettext_compact_command_line_true():
config = Config({}, {'gettext_compact': '1'})
config.add('gettext_compact', True, '', {bool, str})
diff --git a/tests/test_config/test_correct_year.py b/tests/test_config/test_correct_year.py
index 815383dfaf4..4d0c70f32e4 100644
--- a/tests/test_config/test_correct_year.py
+++ b/tests/test_config/test_correct_year.py
@@ -1,20 +1,31 @@
"""Test copyright year adjustment"""
+import time
+
import pytest
+from sphinx.config import Config, correct_copyright_year
+
+LT = time.localtime()
+LT_NEW = (2009, *LT[1:], LT.tm_zone, LT.tm_gmtoff)
+LOCALTIME_2009 = type(LT)(LT_NEW)
+
@pytest.fixture(
params=[
# test with SOURCE_DATE_EPOCH unset: no modification
- (None, '2006-2009'),
+ (None, ''),
# test with SOURCE_DATE_EPOCH set: copyright year should be updated
- ('1293840000', '2006-2011'),
- ('1293839999', '2006-2010'),
+ ('1293840000', '2011'),
+ ('1293839999', '2010'),
+ ('1199145600', '2008'),
+ ('1199145599', '2007'),
],
)
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:
@@ -22,8 +33,140 @@ def expect_date(request, monkeypatch):
yield expect
-@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
+def test_correct_year(expect_date):
+ # test that copyright is substituted
+ copyright_date = '2006-2009, Alice'
+ cfg = Config({'copyright': copyright_date}, {})
+ assert cfg.copyright == copyright_date
+ correct_copyright_year(None, cfg) # type: ignore[arg-type]
+ if expect_date:
+ assert cfg.copyright == f'2006-{expect_date}, Alice'
+ else:
+ assert cfg.copyright == copyright_date
+
+
+def test_correct_year_space(expect_date):
+ # test that copyright is substituted
+ copyright_date = '2006-2009 Alice'
+ cfg = Config({'copyright': copyright_date}, {})
+ assert cfg.copyright == copyright_date
+ correct_copyright_year(None, cfg) # type: ignore[arg-type]
+ if expect_date:
+ assert cfg.copyright == f'2006-{expect_date} Alice'
+ else:
+ assert cfg.copyright == copyright_date
+
+
+def test_correct_year_no_author(expect_date):
+ # test that copyright is substituted
+ copyright_date = '2006-2009'
+ cfg = Config({'copyright': copyright_date}, {})
+ assert cfg.copyright == copyright_date
+ correct_copyright_year(None, cfg) # type: ignore[arg-type]
+ if expect_date:
+ assert cfg.copyright == f'2006-{expect_date}'
+ else:
+ assert cfg.copyright == copyright_date
+
+
+def test_correct_year_single(expect_date):
+ # test that copyright is substituted
+ copyright_date = '2009, Alice'
+ cfg = Config({'copyright': copyright_date}, {})
+ assert cfg.copyright == copyright_date
+ correct_copyright_year(None, cfg) # type: ignore[arg-type]
+ if expect_date:
+ assert cfg.copyright == f'{expect_date}, Alice'
+ else:
+ assert cfg.copyright == copyright_date
+
+
+def test_correct_year_single_space(expect_date):
+ # test that copyright is substituted
+ copyright_date = '2009 Alice'
+ cfg = Config({'copyright': copyright_date}, {})
+ assert cfg.copyright == copyright_date
+ correct_copyright_year(None, cfg) # type: ignore[arg-type]
+ if expect_date:
+ assert cfg.copyright == f'{expect_date} Alice'
+ else:
+ assert cfg.copyright == copyright_date
+
+
+def test_correct_year_single_no_author(expect_date):
+ # test that copyright is substituted
+ copyright_date = '2009'
+ cfg = Config({'copyright': copyright_date}, {})
+ assert cfg.copyright == copyright_date
+ correct_copyright_year(None, cfg) # type: ignore[arg-type]
+ if expect_date:
+ assert cfg.copyright == f'{expect_date}'
+ else:
+ assert cfg.copyright == copyright_date
+
+
+def test_correct_year_multi_line(expect_date):
+ # test that copyright is substituted
+ copyright_dates = (
+ '2006',
+ '2006-2009, Alice',
+ '2010-2013, Bob',
+ '2014-2017, Charlie',
+ '2018-2021, David',
+ '2022-2025, Eve',
+ )
+ cfg = Config({'copyright': copyright_dates}, {})
+ assert cfg.copyright == copyright_dates
+ correct_copyright_year(None, cfg) # type: ignore[arg-type]
+ if expect_date:
+ 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',
+ )
+ else:
+ assert cfg.copyright == copyright_dates
+
+
+def test_correct_year_multi_line_all_formats(expect_date):
+ # test that copyright is substituted
+ copyright_dates = (
+ '2009',
+ '2009 Alice',
+ '2009, Bob',
+ '2006-2009',
+ '2006-2009 Charlie',
+ '2006-2009, David',
+ )
+ cfg = Config({'copyright': copyright_dates}, {})
+ assert cfg.copyright == copyright_dates
+ correct_copyright_year(None, cfg) # type: ignore[arg-type]
+ if expect_date:
+ assert cfg.copyright == (
+ f'{expect_date}',
+ f'{expect_date} Alice',
+ f'{expect_date}, Bob',
+ f'2006-{expect_date}',
+ f'2006-{expect_date} Charlie',
+ f'2006-{expect_date}, David',
+ )
+ else:
+ assert cfg.copyright == copyright_dates
+
+
+def test_correct_year_app(expect_date, tmp_path, make_app):
+ # integration test
+ copyright_date = '2006-2009, Alice'
+ (tmp_path / 'conf.py').touch()
+ app = make_app(
+ 'dummy',
+ srcdir=tmp_path,
+ confoverrides={'copyright': copyright_date},
+ )
+ if expect_date:
+ assert app.config.copyright == f'2006-{expect_date}, Alice'
+ else:
+ assert app.config.copyright == copyright_date