Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 57a63ea

Browse files
authored
Use more precise last-modified times for files (sphinx-doc#12661)
1 parent de15d61 commit 57a63ea

14 files changed

Lines changed: 94 additions & 75 deletions

File tree

sphinx/builders/html/__init__.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import posixpath
1010
import re
1111
import sys
12-
import time
1312
import types
1413
import warnings
1514
from os import path
@@ -40,13 +39,21 @@
4039
from sphinx.search import js_index
4140
from sphinx.theming import HTMLThemeFactory
4241
from sphinx.util import isurl, logging
42+
from sphinx.util._timestamps import _format_rfc3339_microseconds
4343
from sphinx.util.display import progress_message, status_iterator
4444
from sphinx.util.docutils import new_document
4545
from sphinx.util.fileutil import copy_asset
4646
from sphinx.util.i18n import format_date
4747
from sphinx.util.inventory import InventoryFile
4848
from sphinx.util.matching import DOTFILES, Matcher, patmatch
49-
from sphinx.util.osutil import SEP, copyfile, ensuredir, os_path, relative_uri
49+
from sphinx.util.osutil import (
50+
SEP,
51+
_last_modified_time,
52+
copyfile,
53+
ensuredir,
54+
os_path,
55+
relative_uri,
56+
)
5057
from sphinx.writers.html import HTMLWriter
5158
from sphinx.writers.html5 import HTML5Translator
5259

@@ -397,7 +404,7 @@ def get_outdated_docs(self) -> Iterator[str]:
397404
pass
398405

399406
if self.templates:
400-
template_mtime = self.templates.newest_template_mtime()
407+
template_mtime = int(self.templates.newest_template_mtime() * 10**6)
401408
else:
402409
template_mtime = 0
403410
for docname in self.env.found_docs:
@@ -407,19 +414,19 @@ def get_outdated_docs(self) -> Iterator[str]:
407414
continue
408415
targetname = self.get_outfilename(docname)
409416
try:
410-
targetmtime = path.getmtime(targetname)
417+
targetmtime = _last_modified_time(targetname)
411418
except Exception:
412419
targetmtime = 0
413420
try:
414-
srcmtime = max(path.getmtime(self.env.doc2path(docname)), template_mtime)
421+
srcmtime = max(_last_modified_time(self.env.doc2path(docname)), template_mtime)
415422
if srcmtime > targetmtime:
416423
logger.debug(
417424
'[build target] targetname %r(%s), template(%s), docname %r(%s)',
418425
targetname,
419-
_format_modified_time(targetmtime),
420-
_format_modified_time(template_mtime),
426+
_format_rfc3339_microseconds(targetmtime),
427+
_format_rfc3339_microseconds(template_mtime),
421428
docname,
422-
_format_modified_time(path.getmtime(self.env.doc2path(docname))),
429+
_format_rfc3339_microseconds(_last_modified_time(self.env.doc2path(docname))),
423430
)
424431
yield docname
425432
except OSError:
@@ -1224,12 +1231,6 @@ def convert_html_css_files(app: Sphinx, config: Config) -> None:
12241231
config.html_css_files = html_css_files
12251232

12261233

1227-
def _format_modified_time(timestamp: float) -> str:
1228-
"""Return an RFC 3339 formatted string representing the given timestamp."""
1229-
seconds, fraction = divmod(timestamp, 1)
1230-
return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(seconds)) + f'.{fraction:.3f}'
1231-
1232-
12331234
def convert_html_js_files(app: Sphinx, config: Config) -> None:
12341235
"""Convert string styled html_js_files to tuple styled one."""
12351236
html_js_files: list[tuple[str, dict[str, str]]] = []

sphinx/builders/text.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
from sphinx.builders import Builder
1111
from sphinx.locale import __
1212
from sphinx.util import logging
13-
from sphinx.util.osutil import ensuredir, os_path
13+
from sphinx.util.osutil import (
14+
_last_modified_time,
15+
ensuredir,
16+
os_path,
17+
)
1418
from sphinx.writers.text import TextTranslator, TextWriter
1519

1620
if TYPE_CHECKING:
@@ -46,11 +50,11 @@ def get_outdated_docs(self) -> Iterator[str]:
4650
continue
4751
targetname = path.join(self.outdir, docname + self.out_suffix)
4852
try:
49-
targetmtime = path.getmtime(targetname)
53+
targetmtime = _last_modified_time(targetname)
5054
except Exception:
5155
targetmtime = 0
5256
try:
53-
srcmtime = path.getmtime(self.env.doc2path(docname))
57+
srcmtime = _last_modified_time(self.env.doc2path(docname))
5458
if srcmtime > targetmtime:
5559
yield docname
5660
except OSError:

sphinx/builders/xml.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
from sphinx.builders import Builder
1313
from sphinx.locale import __
1414
from sphinx.util import logging
15-
from sphinx.util.osutil import ensuredir, os_path
15+
from sphinx.util.osutil import (
16+
_last_modified_time,
17+
ensuredir,
18+
os_path,
19+
)
1620
from sphinx.writers.xml import PseudoXMLWriter, XMLWriter
1721

1822
if TYPE_CHECKING:
@@ -52,11 +56,11 @@ def get_outdated_docs(self) -> Iterator[str]:
5256
continue
5357
targetname = path.join(self.outdir, docname + self.out_suffix)
5458
try:
55-
targetmtime = path.getmtime(targetname)
59+
targetmtime = _last_modified_time(targetname)
5660
except Exception:
5761
targetmtime = 0
5862
try:
59-
srcmtime = path.getmtime(self.env.doc2path(docname))
63+
srcmtime = _last_modified_time(self.env.doc2path(docname))
6064
if srcmtime > targetmtime:
6165
yield docname
6266
except OSError:

sphinx/environment/__init__.py

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import functools
66
import os
77
import pickle
8-
import time
98
from collections import defaultdict
109
from copy import copy
1110
from os import path
@@ -17,10 +16,11 @@
1716
from sphinx.locale import __
1817
from sphinx.transforms import SphinxTransformer
1918
from sphinx.util import DownloadFiles, FilenameUniqDict, logging
19+
from sphinx.util._timestamps import _format_rfc3339_microseconds
2020
from sphinx.util.docutils import LoggingReporter
2121
from sphinx.util.i18n import CatalogRepository, docname_to_domain
2222
from sphinx.util.nodes import is_translatable
23-
from sphinx.util.osutil import canon_path, os_path
23+
from sphinx.util.osutil import _last_modified_time, canon_path, os_path
2424

2525
if TYPE_CHECKING:
2626
from collections.abc import Callable, Iterator
@@ -508,7 +508,8 @@ def get_outdated_files(self, config_changed: bool) -> tuple[set[str], set[str],
508508
if newmtime > mtime:
509509
logger.debug('[build target] outdated %r: %s -> %s',
510510
docname,
511-
_format_modified_time(mtime), _format_modified_time(newmtime))
511+
_format_rfc3339_microseconds(mtime),
512+
_format_rfc3339_microseconds(newmtime))
512513
changed.add(docname)
513514
continue
514515
# finally, check the mtime of dependencies
@@ -528,7 +529,8 @@ def get_outdated_files(self, config_changed: bool) -> tuple[set[str], set[str],
528529
logger.debug(
529530
'[build target] outdated %r from dependency %r: %s -> %s',
530531
docname, deppath,
531-
_format_modified_time(mtime), _format_modified_time(depmtime),
532+
_format_rfc3339_microseconds(mtime),
533+
_format_rfc3339_microseconds(depmtime),
532534
)
533535
changed.add(docname)
534536
break
@@ -756,26 +758,6 @@ def check_consistency(self) -> None:
756758
self.events.emit('env-check-consistency', self)
757759

758760

759-
def _last_modified_time(filename: str | os.PathLike[str]) -> int:
760-
"""Return the last modified time of ``filename``.
761-
762-
The time is returned as integer microseconds.
763-
The lowest common denominator of modern file-systems seems to be
764-
microsecond-level precision.
765-
766-
We prefer to err on the side of re-rendering a file,
767-
so we round up to the nearest microsecond.
768-
"""
769-
# upside-down floor division to get the ceiling
770-
return -(os.stat(filename).st_mtime_ns // -1_000)
771-
772-
773-
def _format_modified_time(timestamp: int) -> str:
774-
"""Return an RFC 3339 formatted string representing the given timestamp."""
775-
seconds, fraction = divmod(timestamp, 10**6)
776-
return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(seconds)) + f'.{fraction // 1_000}'
777-
778-
779761
def _traverse_toctree(
780762
traversed: set[str],
781763
parent: str | None,

sphinx/ext/viewcode.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from sphinx.util import logging
2222
from sphinx.util.display import status_iterator
2323
from sphinx.util.nodes import make_refnode
24+
from sphinx.util.osutil import _last_modified_time
2425

2526
if TYPE_CHECKING:
2627
from collections.abc import Iterable, Iterator
@@ -231,7 +232,7 @@ def should_generate_module_page(app: Sphinx, modname: str) -> bool:
231232
page_filename = path.join(app.outdir, '_modules/', basename)
232233

233234
try:
234-
if path.getmtime(module_filename) <= path.getmtime(page_filename):
235+
if _last_modified_time(module_filename) <= _last_modified_time(page_filename):
235236
# generation is not needed if the HTML page is newer than module file.
236237
return False
237238
except OSError:

sphinx/jinja2glue.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
import os
56
from os import path
67
from pprint import pformat
78
from typing import TYPE_CHECKING, Any
@@ -12,7 +13,7 @@
1213

1314
from sphinx.application import TemplateBridge
1415
from sphinx.util import logging
15-
from sphinx.util.osutil import mtimes_of_files
16+
from sphinx.util.osutil import _last_modified_time
1617

1718
if TYPE_CHECKING:
1819
from collections.abc import Callable, Iterator
@@ -127,11 +128,11 @@ def get_source(self, environment: Environment, template: str) -> tuple[str, str,
127128
with f:
128129
contents = f.read().decode(self.encoding)
129130

130-
mtime = path.getmtime(filename)
131+
mtime = _last_modified_time(filename)
131132

132133
def uptodate() -> bool:
133134
try:
134-
return path.getmtime(filename) == mtime
135+
return _last_modified_time(filename) == mtime
135136
except OSError:
136137
return False
137138

@@ -203,7 +204,13 @@ def render_string(self, source: str, context: dict) -> str:
203204
return self.environment.from_string(source).render(context)
204205

205206
def newest_template_mtime(self) -> float:
206-
return max(mtimes_of_files(self.pathchain, '.html'))
207+
return max(
208+
os.stat(os.path.join(root, sfile)).st_mtime_ns / 10**9
209+
for dirname in self.pathchain
210+
for root, _dirs, files in os.walk(dirname)
211+
for sfile in files
212+
if sfile.endswith('.html')
213+
)
207214

208215
# Loader interface
209216

sphinx/util/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,8 @@
2929
from sphinx.util.osutil import ( # NoQA: F401
3030
SEP,
3131
copyfile,
32-
copytimes,
3332
ensuredir,
3433
make_filename,
35-
mtimes_of_files,
3634
os_path,
3735
relative_uri,
3836
)

sphinx/util/_timestamps.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from __future__ import annotations
2+
3+
import time
4+
5+
6+
def _format_rfc3339_microseconds(timestamp: int, /) -> str:
7+
"""Return an RFC 3339 formatted string representing the given timestamp.
8+
9+
:param timestamp: The timestamp to format, in microseconds.
10+
"""
11+
seconds, fraction = divmod(timestamp, 10**6)
12+
return time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(seconds)) + f'.{fraction // 1_000}'

sphinx/util/i18n.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
from sphinx.errors import SphinxError
1616
from sphinx.locale import __
1717
from sphinx.util import logging
18-
from sphinx.util.osutil import SEP, canon_path, relpath
18+
from sphinx.util.osutil import (
19+
SEP,
20+
_last_modified_time,
21+
canon_path,
22+
relpath,
23+
)
1924

2025
if TYPE_CHECKING:
2126
import datetime as dt
@@ -84,7 +89,7 @@ def mo_path(self) -> str:
8489
def is_outdated(self) -> bool:
8590
return (
8691
not path.exists(self.mo_path) or
87-
path.getmtime(self.mo_path) < path.getmtime(self.po_path))
92+
_last_modified_time(self.mo_path) < _last_modified_time(self.po_path))
8893

8994
def write_mo(self, locale: str, use_fuzzy: bool = False) -> None:
9095
with open(self.po_path, encoding=self.charset) as file_po:

sphinx/util/osutil.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from sphinx.locale import __
1717

1818
if TYPE_CHECKING:
19-
from collections.abc import Iterator
2019
from pathlib import Path
2120
from types import TracebackType
2221
from typing import Any
@@ -72,20 +71,25 @@ def ensuredir(file: str | os.PathLike[str]) -> None:
7271
os.makedirs(file, exist_ok=True)
7372

7473

75-
def mtimes_of_files(dirnames: list[str], suffix: str) -> Iterator[float]:
76-
for dirname in dirnames:
77-
for root, _dirs, files in os.walk(dirname):
78-
for sfile in files:
79-
if sfile.endswith(suffix):
80-
with contextlib.suppress(OSError):
81-
yield path.getmtime(path.join(root, sfile))
74+
def _last_modified_time(source: str | os.PathLike[str], /) -> int:
75+
"""Return the last modified time of ``filename``.
8276
77+
The time is returned as integer microseconds.
78+
The lowest common denominator of modern file-systems seems to be
79+
microsecond-level precision.
8380
84-
def copytimes(source: str | os.PathLike[str], dest: str | os.PathLike[str]) -> None:
81+
We prefer to err on the side of re-rendering a file,
82+
so we round up to the nearest microsecond.
83+
"""
84+
st = source.stat() if isinstance(source, os.DirEntry) else os.stat(source)
85+
# upside-down floor division to get the ceiling
86+
return -(st.st_mtime_ns // -1_000)
87+
88+
89+
def _copy_times(source: str | os.PathLike[str], dest: str | os.PathLike[str]) -> None:
8590
"""Copy a file's modification times."""
86-
st = os.stat(source)
87-
if hasattr(os, 'utime'):
88-
os.utime(dest, (st.st_atime, st.st_mtime))
91+
st = source.stat() if isinstance(source, os.DirEntry) else os.stat(source)
92+
os.utime(dest, ns=(st.st_atime_ns, st.st_mtime_ns))
8993

9094

9195
def copyfile(
@@ -128,7 +132,7 @@ def copyfile(
128132
shutil.copyfile(source, dest)
129133
with contextlib.suppress(OSError):
130134
# don't do full copystat because the source may be read-only
131-
copytimes(source, dest)
135+
_copy_times(source, dest)
132136

133137

134138
_no_fn_re = re.compile(r'[^a-zA-Z0-9_-]')

0 commit comments

Comments
 (0)