##// END OF EJS Templates
interfaces: make the `peer` mixin not a Protocol to fix Python 3.10 failures...
interfaces: make the `peer` mixin not a Protocol to fix Python 3.10 failures I can't find any documentation on this, but it appears that Protocol class attributes don't get inherited in subclasses that explicitly subclass a Protocol until Python 3.11, which caused a ton of failures in CI on macOS and Windows (which both test using Python 3.9). The problem started with 1df97507c6b8, and typically manifested as most tests failing to access `ui` on various `peer` classes. Here's a short proof of concept: from __future__ import annotations from typing import ( Protocol, ) class peer(Protocol): limitedarguments: bool = False def __init__(self, arg1, arg2, remotehidden: bool = False) -> None: self.arg1 = arg1 self.arg2 = arg2 class subclass(peer): def __init__(self, arg1, arg2): super(subclass, self).__init__(arg1, arg2, False) sub = subclass(1, 2) print("sub.arg1 is %r" % sub.arg1) When run with Python 3.8.10, 3.9.13, and 3.10.11, the result is: $ py -3.8 prot-test.py Traceback (most recent call last): File "prot-test.py", line 20, in <module> print("sub.arg1 is %r" % sub.arg1) AttributeError: 'subclass' object has no attribute 'arg1' On Python 3.11.9, 3.12.7, and 3.13.0, the result is: $ py -3.11 ../prot-test.py sub.arg1 is 1 Explicitly adding annotations to `peer` like `limitedarguments` didn't help.

File last commit:

r52952:b332ae61 default
r53403:199b0e62 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)