77import pickle
88from collections import defaultdict
99from copy import copy
10- from datetime import datetime
10+ from datetime import datetime , timezone
1111from os import path
1212from typing import TYPE_CHECKING , Any , Callable , Generator , Iterator
1313
5555
5656# This is increased every time an environment attribute is added
5757# or changed to properly invalidate pickle files.
58- ENV_VERSION = 57
58+ ENV_VERSION = 58
5959
6060# config status
6161CONFIG_OK = 1
@@ -166,9 +166,9 @@ def __init__(self, app: Sphinx):
166166 # All "docnames" here are /-separated and relative and exclude
167167 # the source suffix.
168168
169- # docname -> mtime at the time of reading
169+ # docname -> time of reading (in integer microseconds)
170170 # contains all read docnames
171- self .all_docs : dict [str , float ] = {}
171+ self .all_docs : dict [str , int ] = {}
172172 # docname -> set of dependent file
173173 # names, relative to documentation root
174174 self .dependencies : dict [str , set [str ]] = defaultdict (set )
@@ -481,12 +481,14 @@ def get_outdated_files(self, config_changed: bool) -> tuple[set[str], set[str],
481481 continue
482482 # check the mtime of the document
483483 mtime = self .all_docs [docname ]
484- newmtime = path . getmtime (self .doc2path (docname ))
484+ newmtime = _last_modified_time (self .doc2path (docname ))
485485 if newmtime > mtime :
486+ # convert integer microseconds to floating-point seconds,
487+ # and then to timezone-aware datetime objects.
488+ mtime_dt = datetime .fromtimestamp (mtime / 1_000_000 , tz = timezone .utc )
489+ newmtime_dt = datetime .fromtimestamp (mtime / 1_000_000 , tz = timezone .utc )
486490 logger .debug ('[build target] outdated %r: %s -> %s' ,
487- docname ,
488- datetime .utcfromtimestamp (mtime ),
489- datetime .utcfromtimestamp (newmtime ))
491+ docname , mtime_dt , newmtime_dt )
490492 changed .add (docname )
491493 continue
492494 # finally, check the mtime of dependencies
@@ -497,7 +499,7 @@ def get_outdated_files(self, config_changed: bool) -> tuple[set[str], set[str],
497499 if not path .isfile (deppath ):
498500 changed .add (docname )
499501 break
500- depmtime = path . getmtime (deppath )
502+ depmtime = _last_modified_time (deppath )
501503 if depmtime > mtime :
502504 changed .add (docname )
503505 break
@@ -728,3 +730,18 @@ def check_consistency(self) -> None:
728730 for domain in self .domains .values ():
729731 domain .check_consistency ()
730732 self .events .emit ('env-check-consistency' , self )
733+
734+
735+ def _last_modified_time (filename : str | os .PathLike [str ]) -> int :
736+ """Return the last modified time of ``filename``.
737+
738+ The time is returned as integer microseconds.
739+ The lowest common denominator of modern file-systems seems to be
740+ microsecond-level precision.
741+
742+ We prefer to err on the side of re-rendering a file,
743+ so we round up to the nearest microsecond.
744+ """
745+
746+ # upside-down floor division to get the ceiling
747+ return - (os .stat (filename ).st_mtime_ns // - 1_000 )
0 commit comments