|
24 | 24 | from sphinx.transforms import SphinxTransformer
|
25 | 25 | from sphinx.util import logging
|
26 | 26 | from sphinx.util._files import DownloadFiles, FilenameUniqDict
|
27 |
| -from sphinx.util._pathlib import _StrPath, _StrPathProperty |
| 27 | +from sphinx.util._pathlib import _StrPathProperty |
28 | 28 | from sphinx.util._serialise import stable_str
|
29 | 29 | from sphinx.util._timestamps import _format_rfc3339_microseconds
|
30 | 30 | from sphinx.util.docutils import LoggingReporter
|
|
33 | 33 | from sphinx.util.osutil import _last_modified_time, _relative_path
|
34 | 34 |
|
35 | 35 | if TYPE_CHECKING:
|
36 |
| - from collections.abc import Callable, Iterable, Iterator, Mapping |
| 36 | + from collections.abc import Callable, Iterable, Iterator, Mapping, Set |
37 | 37 | from typing import Any, Final, Literal
|
38 | 38 |
|
39 | 39 | from docutils import nodes
|
|
50 | 50 | from sphinx.extension import Extension
|
51 | 51 | from sphinx.project import Project
|
52 | 52 | from sphinx.registry import SphinxComponentRegistry
|
| 53 | + from sphinx.util._pathlib import _StrPath |
53 | 54 | from sphinx.util.tags import Tags
|
54 | 55 |
|
55 | 56 | logger = logging.getLogger(__name__)
|
|
74 | 75 |
|
75 | 76 | # This is increased every time an environment attribute is added
|
76 | 77 | # or changed to properly invalidate pickle files.
|
77 |
| -ENV_VERSION = 65 |
| 78 | +ENV_VERSION = 66 |
78 | 79 |
|
79 | 80 | # config status
|
80 | 81 | CONFIG_UNSET = -1
|
@@ -519,73 +520,33 @@ def get_outdated_files(
|
519 | 520 | ) -> tuple[set[str], set[str], set[str]]:
|
520 | 521 | """Return (added, changed, removed) sets."""
|
521 | 522 | # clear all files no longer present
|
522 |
| - removed = set(self.all_docs) - self.found_docs |
| 523 | + removed = self.all_docs.keys() - self.found_docs |
523 | 524 |
|
524 | 525 | added: set[str] = set()
|
525 | 526 | changed: set[str] = set()
|
526 | 527 |
|
527 | 528 | if config_changed:
|
528 | 529 | # config values affect e.g. substitutions
|
529 | 530 | 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 |
589 | 550 |
|
590 | 551 | return added, changed, removed
|
591 | 552 |
|
@@ -649,7 +610,9 @@ def note_dependency(
|
649 | 610 | """
|
650 | 611 | if docname is None:
|
651 | 612 | 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) |
653 | 616 |
|
654 | 617 | def note_included(self, filename: str | os.PathLike[str]) -> None:
|
655 | 618 | """Add *filename* as a included from other document.
|
@@ -872,6 +835,71 @@ def _differing_config_keys(old: Config, new: Config) -> frozenset[str]:
|
872 | 835 | return frozenset(not_in_both | different_values)
|
873 | 836 |
|
874 | 837 |
|
| 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 | + |
875 | 903 | def _traverse_toctree(
|
876 | 904 | traversed: set[str],
|
877 | 905 | parent: str | None,
|
|
0 commit comments