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

Support placeholders for years in copyright #12910

Merged
merged 11 commits into from
Oct 4, 2024
6 changes: 6 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ Features added
GMT (universal time) instead of local time for the date-time
supplied to :confval:`html_last_updated_fmt`.
Patch by Adam Turner.
* #12910: Copyright entries now support the ``'%Y'`` placeholder
to substitute the current year.
This is helpful for reducing the reliance on Python modules
such as :py:mod:`time` or :py:mod:`datetime` in :file:`conf.py`.
See :confval:`the docs <copyright>` for further detail.
jayaddison marked this conversation as resolved.
Show resolved Hide resolved
Patch by Adam Turner.

Bugs fixed
----------
Expand Down
3 changes: 1 addition & 2 deletions doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import os
import re
import time
from typing import TYPE_CHECKING

from sphinx import __display_version__
Expand All @@ -27,7 +26,7 @@
exclude_patterns = ['_build']

project = 'Sphinx'
copyright = f'2007-{time.strftime("%Y")}, the Sphinx developers'
copyright = '2007-%Y, the Sphinx developers'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I think we should support this templating for other projects, I have a strong preference for using constant/static copyright notices where possible (although I fully admit that adding supporting tooling to detect cases where they're out-of-date could be useful).

Given that, and the fact that we update most of the copyright notices throughout the codebase manually each year, I wonder whether we should do the same here.

Suggested change
copyright = '2007-%Y, the Sphinx developers'
copyright = '2007-2024, the Sphinx developers'

release = version = __display_version__
show_authors = True
nitpicky = True
Expand Down
12 changes: 12 additions & 0 deletions doc/usage/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -128,13 +128,24 @@ Project information
* :code-py:`copyright = 'YYYY-YYYY, Author Name'`
* :code-py:`copyright = 'YYYY-YYYY Author Name'`

If the string :code-py:`'%Y'` appears in a copyright line,
it will be replaced with the current four-digit year.
For example:

* :code-py:`copyright = '%Y'`
* :code-py:`copyright = '%Y, Author Name'`
* :code-py:`copyright = 'YYYY-%Y, Author Name'`

.. versionadded:: 3.5
The :code-py:`project_copyright` alias.

.. versionchanged:: 7.1
The value may now be a sequence of copyright statements in the above form,
which will be displayed each to their own line.

.. versionchanged:: 8.1
Copyright statements support the :code-py:`'%Y'` placeholder.

.. confval:: version
:type: :code-py:`str`
:default: :code-py:`''`
Expand Down Expand Up @@ -2528,6 +2539,7 @@ so the HTML options also apply where appropriate.
:default: The value of **copyright**

The copyright of the document.
See :confval:`copyright` for permitted formats.

.. confval:: epub_identifier
:type: :code-py:`str`
Expand Down
16 changes: 16 additions & 0 deletions sphinx/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,21 @@ def init_numfig_format(app: Sphinx, config: Config) -> None:
config.numfig_format = numfig_format


def evaluate_copyright_placeholders(_app: Sphinx, config: Config) -> None:
"""Replace copyright year placeholders (%Y) with the current year."""
replace_yr = str(time.localtime().tm_year)
for k in ('copyright', 'epub_copyright'):
if k in config:
value: str | Sequence[str] = config[k]
if isinstance(value, str):
if '%Y' in value:
config[k] = value.replace('%Y', replace_yr)
else:
if any('%Y' in line for line in value):
items = (line.replace('%Y', replace_yr) for line in value)
config[k] = type(value)(items) # type: ignore[call-arg]


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)
Expand Down Expand Up @@ -775,6 +790,7 @@ def setup(app: Sphinx) -> ExtensionMetadata:
app.connect('config-inited', convert_source_suffix, priority=800)
app.connect('config-inited', convert_highlight_options, priority=800)
app.connect('config-inited', init_numfig_format, priority=800)
app.connect('config-inited', evaluate_copyright_placeholders, priority=795)
app.connect('config-inited', correct_copyright_year, priority=800)
app.connect('config-inited', check_confval_types, priority=800)
app.connect('config-inited', check_primary_domain, priority=800)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@

import pytest

from sphinx.config import Config, correct_copyright_year
from sphinx.config import (
Config,
correct_copyright_year,
evaluate_copyright_placeholders,
)

LT = time.localtime()
LT_NEW = (2009, *LT[1:], LT.tm_zone, LT.tm_gmtoff)
Expand Down Expand Up @@ -106,6 +110,19 @@ def test_correct_year_single_no_author(expect_date):
assert cfg.copyright == copyright_date


def test_correct_year_placeholder(expect_date):
# test that copyright is substituted
copyright_date = '2006-%Y, Alice'
cfg = Config({'copyright': copyright_date}, {})
assert cfg.copyright == copyright_date
evaluate_copyright_placeholders(None, cfg) # type: ignore[arg-type]
correct_copyright_year(None, cfg) # type: ignore[arg-type]
if expect_date and expect_date <= LOCALTIME_2009.tm_year:
assert cfg.copyright == f'2006-{expect_date}, Alice'
else:
assert cfg.copyright == '2006-2009, Alice'


def test_correct_year_multi_line(expect_date):
# test that copyright is substituted
copyright_dates = (
Expand Down Expand Up @@ -159,6 +176,47 @@ def test_correct_year_multi_line_all_formats(expect_date):
assert cfg.copyright == copyright_dates


def test_correct_year_multi_line_all_formats_placeholder(expect_date):
# test that copyright is substituted
copyright_dates = (
'%Y',
'%Y Alice',
'%Y, Bob',
'2006-%Y',
'2006-%Y Charlie',
'2006-%Y, David',
# other format codes are left as-is
'2006-%y, Eve',
'%Y-%m-%d %H:%M:S %z, Francis',
)
cfg = Config({'copyright': copyright_dates}, {})
assert cfg.copyright == copyright_dates
evaluate_copyright_placeholders(None, cfg) # type: ignore[arg-type]
correct_copyright_year(None, cfg) # type: ignore[arg-type]
if expect_date and expect_date <= LOCALTIME_2009.tm_year:
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',
'2006-%y, Eve',
'2009-%m-%d %H:%M:S %z, Francis',
)
else:
assert cfg.copyright == (
'2009',
'2009 Alice',
'2009, Bob',
'2006-2009',
'2006-2009 Charlie',
'2006-2009, David',
'2006-%y, Eve',
'2009-%m-%d %H:%M:S %z, Francis',
)


def test_correct_year_app(expect_date, tmp_path, make_app):
# integration test
copyright_date = '2006-2009, Alice'
Expand Down