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

Correctly synchronize root_doc and master_doc #12417

Merged
merged 5 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ Bugs fixed
* #12380: LaTeX: Footnote mark sometimes indicates ``Page N`` where ``N`` is
the current page number and the footnote does appear on that same page.
Patch by Jean-François B.
* #12416: :confval:`root_doc` is synchronized with :confval:`master_doc`
so that if either of the two values is modified, the other reflects that
modification. It is still recommended to use :confval:`root_doc`.
Patch by Bénédikt Tran.

Testing
-------
Expand Down
2 changes: 1 addition & 1 deletion sphinx/builders/epub3.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ def convert_epub_css_files(app: Sphinx, config: Config) -> None:
logger.warning(__('invalid css_file: %r, ignored'), entry)
continue

config.epub_css_files = epub_css_files # type: ignore[attr-defined]
config.epub_css_files = epub_css_files


def setup(app: Sphinx) -> ExtensionMetadata:
Expand Down
4 changes: 2 additions & 2 deletions sphinx/builders/gettext.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,9 +299,9 @@ def _gettext_compact_validator(app: Sphinx, config: Config) -> None:
gettext_compact = config.gettext_compact
# Convert 0/1 from the command line to ``bool`` types
if gettext_compact == '0':
config.gettext_compact = False # type: ignore[attr-defined]
config.gettext_compact = False
elif gettext_compact == '1':
config.gettext_compact = True # type: ignore[attr-defined]
config.gettext_compact = True


def setup(app: Sphinx) -> ExtensionMetadata:
Expand Down
8 changes: 4 additions & 4 deletions sphinx/builders/html/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1187,7 +1187,7 @@ def convert_html_css_files(app: Sphinx, config: Config) -> None:
logger.warning(__('invalid css_file: %r, ignored'), entry)
continue

config.html_css_files = html_css_files # type: ignore[attr-defined]
config.html_css_files = html_css_files


def _format_modified_time(timestamp: float) -> str:
Expand All @@ -1210,7 +1210,7 @@ def convert_html_js_files(app: Sphinx, config: Config) -> None:
logger.warning(__('invalid js_file: %r, ignored'), entry)
continue

config.html_js_files = html_js_files # type: ignore[attr-defined]
config.html_js_files = html_js_files


def setup_resource_paths(app: Sphinx, pagename: str, templatename: str,
Expand Down Expand Up @@ -1273,7 +1273,7 @@ def validate_html_logo(app: Sphinx, config: Config) -> None:
not path.isfile(path.join(app.confdir, config.html_logo)) and
not isurl(config.html_logo)):
logger.warning(__('logo file %r does not exist'), config.html_logo)
config.html_logo = None # type: ignore[attr-defined]
config.html_logo = None


def validate_html_favicon(app: Sphinx, config: Config) -> None:
Expand All @@ -1282,7 +1282,7 @@ def validate_html_favicon(app: Sphinx, config: Config) -> None:
not path.isfile(path.join(app.confdir, config.html_favicon)) and
not isurl(config.html_favicon)):
logger.warning(__('favicon file %r does not exist'), config.html_favicon)
config.html_favicon = None # type: ignore[attr-defined]
config.html_favicon = None


def error_on_html_4(_app: Sphinx, config: Config) -> None:
Expand Down
22 changes: 15 additions & 7 deletions sphinx/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,15 @@ def __repr__(self) -> str:
values.append(f"{opt_name}={opt_value!r}")
return self.__class__.__qualname__ + '(' + ', '.join(values) + ')'

def __setattr__(self, key: str, value: Any) -> None:
# if someone is still using 'master_doc', we need to update 'root_doc'
picnixz marked this conversation as resolved.
Show resolved Hide resolved
if key in ('master_doc', 'root_doc'):
super().__setattr__('root_doc', value)
super().__setattr__('master_doc', value)
return

super().__setattr__(key, value)

def __getattr__(self, name: str) -> Any:
if name in self._options:
# first check command-line overrides
Expand Down Expand Up @@ -561,10 +570,10 @@ def convert_source_suffix(app: Sphinx, config: Config) -> None:
#
# The default filetype is determined on later step.
# By default, it is considered as restructuredtext.
config.source_suffix = {source_suffix: None} # type: ignore[attr-defined]
config.source_suffix = {source_suffix: None}
elif isinstance(source_suffix, (list, tuple)):
# if list, considers as all of them are default filetype
config.source_suffix = dict.fromkeys(source_suffix, None) # type: ignore[attr-defined]
config.source_suffix = dict.fromkeys(source_suffix, None)
elif not isinstance(source_suffix, dict):
logger.warning(__("The config value `source_suffix' expects "
"a string, list of strings, or dictionary. "
Expand All @@ -580,8 +589,7 @@ def convert_highlight_options(app: Sphinx, config: Config) -> None:
options = config.highlight_options
if options and not all(isinstance(v, dict) for v in options.values()):
# old styled option detected because all values are not dictionary.
config.highlight_options = {config.highlight_language: # type: ignore[attr-defined]
options}
config.highlight_options = {config.highlight_language: options}


def init_numfig_format(app: Sphinx, config: Config) -> None:
Expand All @@ -593,7 +601,7 @@ def init_numfig_format(app: Sphinx, config: Config) -> None:

# override default labels by configuration
numfig_format.update(config.numfig_format)
config.numfig_format = numfig_format # type: ignore[attr-defined]
config.numfig_format = numfig_format


def correct_copyright_year(_app: Sphinx, config: Config) -> None:
Expand Down Expand Up @@ -713,7 +721,7 @@ def check_primary_domain(app: Sphinx, config: Config) -> None:
primary_domain = config.primary_domain
if primary_domain and not app.registry.has_domain(primary_domain):
logger.warning(__('primary_domain %r not found, ignored.'), primary_domain)
config.primary_domain = None # type: ignore[attr-defined]
config.primary_domain = None


def check_root_doc(app: Sphinx, env: BuildEnvironment, added: set[str],
Expand All @@ -726,7 +734,7 @@ def check_root_doc(app: Sphinx, env: BuildEnvironment, added: set[str],
'contents' in app.project.docnames):
logger.warning(__('Since v2.0, Sphinx uses "index" as root_doc by default. '
'Please add "root_doc = \'contents\'" to your conf.py.'))
app.config.root_doc = "contents" # type: ignore[attr-defined]
app.config.root_doc = "contents"

return changed

Expand Down
4 changes: 2 additions & 2 deletions sphinx/ext/autodoc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2721,10 +2721,10 @@ def get_doc(self) -> list[list[str]] | None:
# a docstring from the value which descriptor returns unexpectedly.
# ref: https:/sphinx-doc/sphinx/issues/7805
orig = self.config.autodoc_inherit_docstrings
self.config.autodoc_inherit_docstrings = False # type: ignore[attr-defined]
self.config.autodoc_inherit_docstrings = False
return super().get_doc()
finally:
self.config.autodoc_inherit_docstrings = orig # type: ignore[attr-defined]
self.config.autodoc_inherit_docstrings = orig

def add_content(self, more_content: StringList | None) -> None:
# Disable analyzing attribute comment on Documenter.add_content() to control it on
Expand Down
4 changes: 1 addition & 3 deletions sphinx/ext/autosummary/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -740,9 +740,7 @@ def main(argv: Sequence[str] = (), /) -> None:

if args.templates:
app.config.templates_path.append(path.abspath(args.templates))
app.config.autosummary_ignore_module_all = ( # type: ignore[attr-defined]
not args.respect_module_all
)
app.config.autosummary_ignore_module_all = (not args.respect_module_all)

generate_autosummary_docs(args.source_file, args.output_dir,
'.' + args.suffix,
Expand Down
4 changes: 2 additions & 2 deletions sphinx/transforms/i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def publish_msgstr(app: Sphinx, source: str, source_path: str, source_line: int,
try:
# clear rst_prolog temporarily
rst_prolog = config.rst_prolog
config.rst_prolog = None # type: ignore[attr-defined]
config.rst_prolog = None

from sphinx.io import SphinxI18nReader
reader = SphinxI18nReader()
Expand All @@ -81,7 +81,7 @@ def publish_msgstr(app: Sphinx, source: str, source_path: str, source_line: int,
return doc[0]
return doc
finally:
config.rst_prolog = rst_prolog # type: ignore[attr-defined]
config.rst_prolog = rst_prolog


def parse_noqa(source: str) -> tuple[str, bool]:
Expand Down
10 changes: 5 additions & 5 deletions tests/test_builders/test_build_linkcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ def do_GET(self):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-anchors-ignore-for-url', freshenv=True)
def test_anchors_ignored_for_url(app: Sphinx) -> None:
with serve_application(app, AnchorsIgnoreForUrlHandler) as address:
app.config.linkcheck_anchors_ignore_for_url = [ # type: ignore[attr-defined]
app.config.linkcheck_anchors_ignore_for_url = [
f'http://{address}/ignored', # existing page
f'http://{address}/invalid', # unknown page
]
Expand Down Expand Up @@ -402,7 +402,7 @@ def do_GET(self):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver', freshenv=True)
def test_auth_header_uses_first_match(app: Sphinx) -> None:
with serve_application(app, custom_handler(valid_credentials=("user1", "password"))) as address:
app.config.linkcheck_auth = [ # type: ignore[attr-defined]
app.config.linkcheck_auth = [
(r'^$', ('no', 'match')),
(fr'^http://{re.escape(address)}/$', ('user1', 'password')),
(r'.*local.*', ('user2', 'hunter2')),
Expand Down Expand Up @@ -456,7 +456,7 @@ def check_headers(self):
return self.headers["Accept"] == "text/html"

with serve_application(app, custom_handler(success_criteria=check_headers)) as address:
app.config.linkcheck_request_headers = { # type: ignore[attr-defined]
app.config.linkcheck_request_headers = {
f"http://{address}/": {"Accept": "text/html"},
"*": {"X-Secret": "open sesami"},
}
Expand All @@ -476,7 +476,7 @@ def check_headers(self):
return self.headers["Accept"] == "application/json"

with serve_application(app, custom_handler(success_criteria=check_headers)) as address:
app.config.linkcheck_request_headers = { # type: ignore[attr-defined]
app.config.linkcheck_request_headers = {
f"http://{address}": {"Accept": "application/json"},
"*": {"X-Secret": "open sesami"},
}
Expand Down Expand Up @@ -579,7 +579,7 @@ def test_follows_redirects_on_GET(app, capsys, warning):
@pytest.mark.sphinx('linkcheck', testroot='linkcheck-localserver-warn-redirects')
def test_linkcheck_allowed_redirects(app: Sphinx, warning: StringIO) -> None:
with serve_application(app, make_redirect_handler(support_head=False)) as address:
app.config.linkcheck_allowed_redirects = {f'http://{address}/.*1': '.*'} # type: ignore[attr-defined]
app.config.linkcheck_allowed_redirects = {f'http://{address}/.*1': '.*'}
compile_linkcheck_allowed_redirects(app, app.config)
app.build()

Expand Down
16 changes: 16 additions & 0 deletions tests/test_config/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -803,3 +803,19 @@ def test_gettext_compact_command_line_str():

# regression test for #8549 (-D gettext_compact=spam)
assert config.gettext_compact == 'spam'


def test_root_doc_and_master_doc_are_synchronized():
c = Config()
assert c.master_doc == 'index'
assert c.root_doc == c.master_doc

c = Config()
c.master_doc = '1234'
assert c.master_doc == '1234'
assert c.root_doc == c.master_doc

c = Config()
c.root_doc = '1234'
assert c.master_doc == '1234'
assert c.root_doc == c.master_doc