##// END OF EJS Templates
typing: add minimal annotations to cmd_impls/graft.py to pytype with 3.10...
typing: add minimal annotations to cmd_impls/graft.py to pytype with 3.10 I'm not sure why the same version of pytype passed in CI with Python 3.11. What's failing on 3.10 is related to `statedata`, which is keyed on bytes, but has various value types. It looks like these several types are treated as a union when run with 3.10, and then all of them need to have the same attributes. This will take awhile to untangle, because `TypedDict` requires str keys, so we'll either have to change the keys (and whoever calls this), or migrate to a class with typed fields (and change all of the callers). There are some changes to this module currently in-flight, so I'm opting for the minimal changes here to minimally affect that, while keeping my ability to run pytype locally and track the changes. It's worth pointing out that I'm starting to use py3.9 type hints here, i.e. `Foo | None` instead of `Optional[Foo]`. That's fine even with py3.8 support because of the `from __future__ import annotations`, which delays evaluation. We already don't support pytype checking with all of the runtime supported versions of Python since at least 0851d94bfdaa, with the `ByteString` usage. The errors at the start of this series were: File "/mnt/c/Users/Matt/hg/mercurial/cmd_impls/graft.py", line 238, in _graft_revisions: No attribute 'get' on bool [attribute-error] In Union[Any, Callable, Dict[bytes, Optional[Any]], bool, bytes, dict] Called from (traceback): line 21, in cmd_graft File "/mnt/c/Users/Matt/hg/mercurial/cmd_impls/graft.py", line 238, in _graft_revisions: No attribute 'get' on bytes [attribute-error] In Union[Any, Callable, Dict[bytes, Optional[Any]], bool, bytes, dict] Called from (traceback): line 21, in cmd_graft File "/mnt/c/Users/Matt/hg/mercurial/cmd_impls/graft.py", line 239, in _graft_revisions: No attribute 'get' on bool [attribute-error] In Union[Any, Callable, Dict[bytes, Optional[Any]], bool, bytes, dict] Called from (traceback): line 21, in cmd_graft File "/mnt/c/Users/Matt/hg/mercurial/cmd_impls/graft.py", line 239, in _graft_revisions: No attribute 'get' on bytes [attribute-error] In Union[Any, Callable, Dict[bytes, Optional[Any]], bool, bytes, dict] Called from (traceback): line 21, in cmd_graft File "/mnt/c/Users/Matt/hg/mercurial/cmd_impls/graft.py", line 241, in _graft_revisions: No attribute 'get' on bool [attribute-error] In Union[Any, Callable, Dict[bytes, Optional[Any]], bool, bytes, dict] Called from (traceback): line 21, in cmd_graft File "/mnt/c/Users/Matt/hg/mercurial/cmd_impls/graft.py", line 241, in _graft_revisions: No attribute 'get' on bytes [attribute-error] In Union[Any, Callable, Dict[bytes, Optional[Any]], bool, bytes, dict] Called from (traceback): line 21, in cmd_graft File "/mnt/c/Users/Matt/hg/mercurial/cmd_impls/graft.py", line 260, in _graft_revisions: unsupported operand type(s) for item assignment: bool [unsupported-operands] No attribute '__setitem__' on bool Called from (traceback): line 21, in cmd_graft File "/mnt/c/Users/Matt/hg/mercurial/cmd_impls/graft.py", line 260, in _graft_revisions: unsupported operand type(s) for item assignment: bytes [unsupported-operands] No attribute '__setitem__' on bytes Called from (traceback): line 21, in cmd_graft File "/mnt/c/Users/Matt/hg/mercurial/cmd_impls/graft.py", line 270, in _graft_revisions: No attribute 'get' on bool [attribute-error] In Union[Any, Callable, Dict[bytes, Optional[Any]], bool, bytes, dict] Called from (traceback): line 21, in cmd_graft File "/mnt/c/Users/Matt/hg/mercurial/cmd_impls/graft.py", line 270, in _graft_revisions: No attribute 'get' on bytes [attribute-error] In Union[Any, Callable, Dict[bytes, Optional[Any]], bool, bytes, dict] Called from (traceback): line 21, in cmd_graft File "/mnt/c/Users/Matt/hg/mercurial/cmd_impls/graft.py", line 280, in _graft_revisions: No attribute 'get' on bool [attribute-error] In Union[Any, Callable, Dict[bytes, Optional[Any]], bool, bytes, dict] Called from (traceback): line 21, in cmd_graft File "/mnt/c/Users/Matt/hg/mercurial/cmd_impls/graft.py", line 280, in _graft_revisions: No attribute 'get' on bytes [attribute-error] In Union[Any, Callable, Dict[bytes, Optional[Any]], bool, bytes, dict] Called from (traceback): line 21, in cmd_graft

File last commit:

r52952:b332ae61 default
r53248:9042ffea default
Show More
timestamp.py
180 lines | 5.7 KiB | text/x-python | PythonLexer
# Copyright Mercurial Contributors
#
# This software may be used and distributed according to the terms of the
# GNU General Public License version 2 or any later version.
from __future__ import annotations
import functools
import os
import stat
import time
from typing import Optional, Tuple
from .. import error
rangemask = 0x7FFFFFFF
@functools.total_ordering
class timestamp(tuple):
"""
A Unix timestamp with optional nanoseconds precision,
modulo 2**31 seconds.
A 3-tuple containing:
`truncated_seconds`: seconds since the Unix epoch,
truncated to its lower 31 bits
`subsecond_nanoseconds`: number of nanoseconds since `truncated_seconds`.
When this is zero, the sub-second precision is considered unknown.
`second_ambiguous`: whether this timestamp is still "reliable"
(see `reliable_mtime_of`) if we drop its sub-second component.
"""
def __new__(cls, value):
truncated_seconds, subsec_nanos, second_ambiguous = value
value = (truncated_seconds & rangemask, subsec_nanos, second_ambiguous)
return super(timestamp, cls).__new__(cls, value)
def __eq__(self, other):
raise error.ProgrammingError(
'timestamp should never be compared directly'
)
def __gt__(self, other):
raise error.ProgrammingError(
'timestamp should never be compared directly'
)
def get_fs_now(vfs) -> Optional[timestamp]:
"""return a timestamp for "now" in the current vfs
This will raise an exception if no temporary files could be created.
"""
tmpfd, tmpname = vfs.mkstemp()
try:
return mtime_of(os.fstat(tmpfd))
finally:
os.close(tmpfd)
vfs.unlink(tmpname)
def zero() -> timestamp:
"""
Returns the `timestamp` at the Unix epoch.
"""
return tuple.__new__(timestamp, (0, 0))
def mtime_of(stat_result: os.stat_result) -> timestamp:
"""
Takes an `os.stat_result`-like object and returns a `timestamp` object
for its modification time.
"""
try:
# TODO: add this attribute to `osutil.stat` objects,
# see `mercurial/cext/osutil.c`.
#
# This attribute is also not available on Python 2.
nanos = stat_result.st_mtime_ns
except AttributeError:
# https://docs.python.org/2/library/os.html#os.stat_float_times
# "For compatibility with older Python versions,
# accessing stat_result as a tuple always returns integers."
secs = stat_result[stat.ST_MTIME]
subsec_nanos = 0
else:
billion = int(1e9)
secs = nanos // billion
subsec_nanos = nanos % billion
return timestamp((secs, subsec_nanos, False))
def reliable_mtime_of(
stat_result: os.stat_result, present_mtime: timestamp
) -> Optional[timestamp]:
"""Wrapper for `make_mtime_reliable` for stat objects"""
file_mtime = mtime_of(stat_result)
return make_mtime_reliable(file_mtime, present_mtime)
def make_mtime_reliable(
file_timestamp: timestamp, present_mtime: timestamp
) -> Optional[timestamp]:
"""Same as `mtime_of`, but return `None` or a `Timestamp` with
`second_ambiguous` set if the date might be ambiguous.
A modification time is reliable if it is older than "present_time" (or
sufficiently in the future).
Otherwise a concurrent modification might happens with the same mtime.
"""
file_second = file_timestamp[0]
file_ns = file_timestamp[1]
boundary_second = present_mtime[0]
boundary_ns = present_mtime[1]
# If the mtime of the ambiguous file is younger (or equal) to the starting
# point of the `status` walk, we cannot garantee that another, racy, write
# will not happen right after with the same mtime and we cannot cache the
# information.
#
# However if the mtime is far away in the future, this is likely some
# mismatch between the current clock and previous file system operation. So
# mtime more than one days in the future are considered fine.
if boundary_second == file_second:
if file_ns and boundary_ns:
if file_ns < boundary_ns:
return timestamp((file_second, file_ns, True))
return None
elif boundary_second < file_second < (3600 * 24 + boundary_second):
return None
else:
return file_timestamp
FS_TICK_WAIT_TIMEOUT = 0.1 # 100 milliseconds
def wait_until_fs_tick(vfs) -> Optional[Tuple[timestamp, bool]]:
"""Wait until the next update from the filesystem time by writing in a loop
a new temporary file inside the working directory and checking if its time
differs from the first one observed.
Returns `None` if we are unable to get the filesystem time,
`(timestamp, True)` if we've timed out waiting for the filesystem clock
to tick, and `(timestamp, False)` if we've waited successfully.
On Linux, your average tick is going to be a "jiffy", or 1/HZ.
HZ is your kernel's tick rate (if it has one configured) and the value
is the one returned by `grep 'CONFIG_HZ=' /boot/config-$(uname -r)`,
again assuming a normal setup.
In my case (Alphare) at the time of writing, I get `CONFIG_HZ=250`,
which equates to 4ms.
This might change with a series that could make it to Linux 6.12:
https://lore.kernel.org/all/20241002-mgtime-v10-8-d1c4717f5284@kernel.org
"""
start = time.monotonic()
try:
old_fs_time = get_fs_now(vfs)
new_fs_time = get_fs_now(vfs)
while (
new_fs_time[0] == old_fs_time[0]
and new_fs_time[1] == old_fs_time[1]
):
if time.monotonic() - start > FS_TICK_WAIT_TIMEOUT:
return (old_fs_time, True)
new_fs_time = get_fs_now(vfs)
except OSError:
return None
else:
return (new_fs_time, False)