Skip to content

Commit f2bf37d

Browse files
authored
Extract change detection into _has_doc_changed() function (#13681)
1 parent 711eb2b commit f2bf37d

File tree

6 files changed

+97
-71
lines changed

6 files changed

+97
-71
lines changed

sphinx/environment/__init__.py

Lines changed: 92 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from sphinx.transforms import SphinxTransformer
2525
from sphinx.util import logging
2626
from sphinx.util._files import DownloadFiles, FilenameUniqDict
27-
from sphinx.util._pathlib import _StrPath, _StrPathProperty
27+
from sphinx.util._pathlib import _StrPathProperty
2828
from sphinx.util._serialise import stable_str
2929
from sphinx.util._timestamps import _format_rfc3339_microseconds
3030
from sphinx.util.docutils import LoggingReporter
@@ -33,7 +33,7 @@
3333
from sphinx.util.osutil import _last_modified_time, _relative_path
3434

3535
if TYPE_CHECKING:
36-
from collections.abc import Callable, Iterable, Iterator, Mapping
36+
from collections.abc import Callable, Iterable, Iterator, Mapping, Set
3737
from typing import Any, Final, Literal
3838

3939
from docutils import nodes
@@ -50,6 +50,7 @@
5050
from sphinx.extension import Extension
5151
from sphinx.project import Project
5252
from sphinx.registry import SphinxComponentRegistry
53+
from sphinx.util._pathlib import _StrPath
5354
from sphinx.util.tags import Tags
5455

5556
logger = logging.getLogger(__name__)
@@ -74,7 +75,7 @@
7475

7576
# This is increased every time an environment attribute is added
7677
# or changed to properly invalidate pickle files.
77-
ENV_VERSION = 65
78+
ENV_VERSION = 66
7879

7980
# config status
8081
CONFIG_UNSET = -1
@@ -519,73 +520,33 @@ def get_outdated_files(
519520
) -> tuple[set[str], set[str], set[str]]:
520521
"""Return (added, changed, removed) sets."""
521522
# clear all files no longer present
522-
removed = set(self.all_docs) - self.found_docs
523+
removed = self.all_docs.keys() - self.found_docs
523524

524525
added: set[str] = set()
525526
changed: set[str] = set()
526527

527528
if config_changed:
528529
# config values affect e.g. substitutions
529530
added = self.found_docs
530-
else:
531-
for docname in self.found_docs:
532-
if docname not in self.all_docs:
533-
logger.debug('[build target] added %r', docname)
534-
added.add(docname)
535-
continue
536-
# if the doctree file is not there, rebuild
537-
filename = self.doctreedir / f'{docname}.doctree'
538-
if not filename.is_file():
539-
logger.debug('[build target] changed %r', docname)
540-
changed.add(docname)
541-
continue
542-
# check the "reread always" list
543-
if docname in self.reread_always:
544-
logger.debug('[build target] changed %r', docname)
545-
changed.add(docname)
546-
continue
547-
# check the mtime of the document
548-
mtime = self.all_docs[docname]
549-
newmtime = _last_modified_time(self.doc2path(docname))
550-
if newmtime > mtime:
551-
logger.debug(
552-
'[build target] outdated %r: %s -> %s',
553-
docname,
554-
_format_rfc3339_microseconds(mtime),
555-
_format_rfc3339_microseconds(newmtime),
556-
)
557-
changed.add(docname)
558-
continue
559-
# finally, check the mtime of dependencies
560-
if docname not in self.dependencies:
561-
continue
562-
for dep in self.dependencies[docname]:
563-
try:
564-
# this will do the right thing when dep is absolute too
565-
dep_path = self.srcdir / dep
566-
if not dep_path.is_file():
567-
logger.debug(
568-
'[build target] changed %r missing dependency %r',
569-
docname,
570-
dep_path,
571-
)
572-
changed.add(docname)
573-
break
574-
depmtime = _last_modified_time(dep_path)
575-
if depmtime > mtime:
576-
logger.debug(
577-
'[build target] outdated %r from dependency %r: %s -> %s',
578-
docname,
579-
dep_path,
580-
_format_rfc3339_microseconds(mtime),
581-
_format_rfc3339_microseconds(depmtime),
582-
)
583-
changed.add(docname)
584-
break
585-
except OSError:
586-
# give it another chance
587-
changed.add(docname)
588-
break
531+
return added, changed, removed
532+
533+
for docname in self.found_docs:
534+
if docname not in self.all_docs:
535+
logger.debug('[build target] added %r', docname)
536+
added.add(docname)
537+
continue
538+
539+
# if the document has changed, rebuild
540+
if _has_doc_changed(
541+
docname,
542+
filename=self.doc2path(docname),
543+
reread_always=self.reread_always,
544+
doctreedir=self.doctreedir,
545+
all_docs=self.all_docs,
546+
dependencies=self.dependencies,
547+
):
548+
changed.add(docname)
549+
continue
589550

590551
return added, changed, removed
591552

@@ -649,7 +610,9 @@ def note_dependency(
649610
"""
650611
if docname is None:
651612
docname = self.docname
652-
self.dependencies.setdefault(docname, set()).add(_StrPath(filename))
613+
# this will do the right thing when *filename* is absolute too
614+
filename = self.srcdir / filename
615+
self.dependencies.setdefault(docname, set()).add(filename)
653616

654617
def note_included(self, filename: str | os.PathLike[str]) -> None:
655618
"""Add *filename* as a included from other document.
@@ -872,6 +835,71 @@ def _differing_config_keys(old: Config, new: Config) -> frozenset[str]:
872835
return frozenset(not_in_both | different_values)
873836

874837

838+
def _has_doc_changed(
839+
docname: str,
840+
*,
841+
filename: Path,
842+
reread_always: Set[str],
843+
doctreedir: Path,
844+
all_docs: Mapping[str, int],
845+
dependencies: Mapping[str, Set[Path]],
846+
) -> bool:
847+
# check the "reread always" list
848+
if docname in reread_always:
849+
logger.debug('[build target] changed %r: re-read forced', docname)
850+
return True
851+
852+
# if the doctree file is not there, rebuild
853+
doctree_path = doctreedir / f'{docname}.doctree'
854+
if not doctree_path.is_file():
855+
logger.debug('[build target] changed %r: doctree file does not exist', docname)
856+
return True
857+
858+
# check the mtime of the document
859+
mtime = all_docs[docname]
860+
new_mtime = _last_modified_time(filename)
861+
if new_mtime > mtime:
862+
logger.debug(
863+
'[build target] changed: %r is outdated (%s -> %s)',
864+
docname,
865+
_format_rfc3339_microseconds(mtime),
866+
_format_rfc3339_microseconds(new_mtime),
867+
)
868+
return True
869+
870+
# finally, check the mtime of dependencies
871+
if docname not in dependencies:
872+
return False
873+
for dep_path in dependencies[docname]:
874+
try:
875+
dep_path_is_file = dep_path.is_file()
876+
except OSError:
877+
return True # give it another chance
878+
if not dep_path_is_file:
879+
logger.debug(
880+
'[build target] changed: %r is missing dependency %r',
881+
docname,
882+
dep_path,
883+
)
884+
return True
885+
886+
try:
887+
dep_mtime = _last_modified_time(dep_path)
888+
except OSError:
889+
return True # give it another chance
890+
if dep_mtime > mtime:
891+
logger.debug(
892+
'[build target] changed: %r is outdated due to dependency %r (%s -> %s)',
893+
docname,
894+
dep_path,
895+
_format_rfc3339_microseconds(mtime),
896+
_format_rfc3339_microseconds(dep_mtime),
897+
)
898+
return True
899+
900+
return False
901+
902+
875903
def _traverse_toctree(
876904
traversed: set[str],
877905
parent: str | None,

tests/js/fixtures/cpp/searchindex.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/js/fixtures/multiterm/searchindex.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/js/fixtures/partial/searchindex.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/js/fixtures/titles/searchindex.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/test_environment/test_environment_record_dependencies.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
import pytest
88

9-
from sphinx.util._pathlib import _StrPath
10-
119
if TYPE_CHECKING:
1210
from sphinx.testing.util import SphinxTestApp
1311

@@ -16,4 +14,4 @@
1614
def test_record_dependencies_cleared(app: SphinxTestApp) -> None:
1715
app.builder.read()
1816
assert 'index' not in app.env.dependencies
19-
assert app.env.dependencies['api'] == {_StrPath('example_module.py')}
17+
assert app.env.dependencies['api'] == {app.srcdir / 'example_module.py'}

0 commit comments

Comments
 (0)