Author: Armin Rigo <ar...@tunes.org> Branch: Changeset: r87262:038a80811172 Date: 2016-09-21 13:35 +0200 http://bitbucket.org/pypy/pypy/changeset/038a80811172/
Log: os.stat(): support full-precision nanosecond times in translated RPython programs, as rbigint numbers. Before translation, it relies on Python 2.7's os.stat_result. This means that before translation you get lower-precision rbigints. diff --git a/rpython/rlib/rposix_stat.py b/rpython/rlib/rposix_stat.py --- a/rpython/rlib/rposix_stat.py +++ b/rpython/rlib/rposix_stat.py @@ -17,7 +17,7 @@ from rpython.rtyper.error import TyperError from rpython.rlib._os_support import _preferred_traits, string_traits -from rpython.rlib.objectmodel import specialize +from rpython.rlib.objectmodel import specialize, we_are_translated from rpython.rtyper.lltypesystem import lltype, rffi from rpython.translator.tool.cbuild import ExternalCompilationInfo from rpython.rlib.rarithmetic import intmask @@ -51,15 +51,18 @@ ("st_uid", lltype.Signed), ("st_gid", lltype.Signed), ("st_size", lltype.SignedLongLong), - ("st_atime", lltype.Float), - ("st_mtime", lltype.Float), - ("st_ctime", lltype.Float), + ("st_atime", lltype.SignedLongLong), # integral number of seconds + ("st_mtime", lltype.SignedLongLong), # + ("st_ctime", lltype.SignedLongLong), # ("st_blksize", lltype.Signed), ("st_blocks", lltype.Signed), ("st_rdev", lltype.Signed), ("st_flags", lltype.Signed), #("st_gen", lltype.Signed), -- new in CPy 2.5, not implemented #("st_birthtime", lltype.Float), -- new in CPy 2.5, not implemented + ("nsec_atime", lltype.Signed), # number of nanoseconds + ("nsec_mtime", lltype.Signed), # + ("nsec_ctime", lltype.Signed), # ] N_INDEXABLE_FIELDS = 10 @@ -79,6 +82,37 @@ ("f_namemax", lltype.Signed), ] +@specialize.arg(1) +def get_stat_ns_as_bigint(st, name): + """'name' is one of the strings "atime", "mtime" or "ctime". + Returns a bigint that represents the number of nanoseconds + stored inside the RPython-level os.stat_result 'st'. + + Note that when running untranslated, the os.stat_result type + is from Python 2.7, which doesn't store more precision than + a float anyway. You will only get more after translation. + """ + from rpython.rlib.rbigint import rbigint + + if not we_are_translated(): + as_float = getattr(st, "st_" + name) + return rbigint.fromfloat(as_float * 1e9) + + if name == "atime": + i, j = 7, -3 + elif name == "mtime": + i, j = 8, -2 + elif name == "ctime": + i, j = 9, -1 + else: + raise AssertionError(name) + + sec = st[i] + nsec = st[j] + result = rbigint.fromrarith_int(sec).int_mul(1000000000) + result = result.int_add(nsec) + return result + # ____________________________________________________________ # @@ -97,7 +131,15 @@ if not s_attr.is_constant(): raise annmodel.AnnotatorError("non-constant attr name in getattr()") attrname = s_attr.const - TYPE = STAT_FIELD_TYPES[attrname] + if attrname in ('st_atime', 'st_mtime', 'st_ctime'): + # like CPython, in RPython we can read the st_Xtime + # attribute and get a floating-point result. We can also + # get a full-precision bigint with get_stat_ns_as_bigint(). + # The floating-point result is computed like a property + # by _ll_get_st_Xtime(). + TYPE = lltype.Float + else: + TYPE = STAT_FIELD_TYPES[attrname] return lltype_to_annotation(TYPE) def _get_rmarshall_support_(self): # for rlib.rmarshal @@ -105,13 +147,14 @@ # (we ignore the extra values here for simplicity and portability) def stat_result_reduce(st): return (st[0], st[1], st[2], st[3], st[4], - st[5], st[6], st[7], st[8], st[9]) + st[5], st[6], st[7], st[8], st[9], + st[-3], st[-2], st[-1]) def stat_result_recreate(tup): - return make_stat_result(tup + extra_zeroes) + return make_stat_result(tup[:10] + extra_zeroes + tup[-3:]) s_reduced = annmodel.SomeTuple([lltype_to_annotation(TYPE) for name, TYPE in PORTABLE_STAT_FIELDS]) - extra_zeroes = (0,) * (len(STAT_FIELDS) - len(PORTABLE_STAT_FIELDS)) + extra_zeroes = (0,) * (len(STAT_FIELDS) - len(PORTABLE_STAT_FIELDS) - 3) return s_reduced, stat_result_reduce, stat_result_recreate @@ -119,7 +162,7 @@ def getitem((s_sta, s_int)): assert s_int.is_constant(), "os.stat()[index]: index must be constant" index = s_int.const - assert 0 <= index < N_INDEXABLE_FIELDS, "os.stat()[index] out of range" + assert -3 <= index < N_INDEXABLE_FIELDS, "os.stat()[index] out of range" name, TYPE = STAT_FIELDS[index] return lltype_to_annotation(TYPE) @@ -152,28 +195,61 @@ def rtype_getattr(self, hop): s_attr = hop.args_s[1] attr = s_attr.const + if attr in ('st_atime', 'st_mtime', 'st_ctime'): + ll_func = globals()['_ll_get_' + attr] + v_tuple = hop.inputarg(self, arg=0) + return hop.gendirectcall(ll_func, v_tuple) try: index = self.stat_field_indexes[attr] except KeyError: raise TyperError("os.stat().%s: field not available" % (attr,)) return self.redispatch_getfield(hop, index) +@specialize.memo() +def _stfld(name): + index = STAT_FIELD_NAMES.index(name) + return 'item%d' % index + +def _ll_get_st_atime(tup): + return (float(getattr(tup, _stfld("st_atime"))) + + 1E-9 * getattr(tup, _stfld("nsec_atime"))) + +def _ll_get_st_mtime(tup): + return (float(getattr(tup, _stfld("st_mtime"))) + + 1E-9 * getattr(tup, _stfld("nsec_mtime"))) + +def _ll_get_st_ctime(tup): + return (float(getattr(tup, _stfld("st_ctime"))) + + 1E-9 * getattr(tup, _stfld("nsec_ctime"))) + class __extend__(pairtype(StatResultRepr, IntegerRepr)): def rtype_getitem((r_sta, r_int), hop): s_int = hop.args_s[1] index = s_int.const + if index < 0: + index += len(STAT_FIELDS) return r_sta.redispatch_getfield(hop, index) s_StatResult = SomeStatResult() + def make_stat_result(tup): - """Turn a tuple into an os.stat_result object.""" - positional = tuple( - lltype.cast_primitive(TYPE, value) for value, (name, TYPE) in - zip(tup, STAT_FIELDS)[:N_INDEXABLE_FIELDS]) + """NOT_RPYTHON: Turn a tuple into an os.stat_result object.""" + assert len(tup) == len(STAT_FIELDS) + assert float not in [type(x) for x in tup] + positional = [] + for i in range(N_INDEXABLE_FIELDS): + name, TYPE = STAT_FIELDS[i] + value = lltype.cast_primitive(TYPE, tup[i]) + positional.append(value) kwds = {} + kwds['st_atime'] = tup[7] + 1e-9 * tup[-3] + kwds['st_mtime'] = tup[8] + 1e-9 * tup[-2] + kwds['st_ctime'] = tup[9] + 1e-9 * tup[-1] for value, (name, TYPE) in zip(tup, STAT_FIELDS)[N_INDEXABLE_FIELDS:]: + if name.startswith('nsec_'): + continue # ignore the nsec_Xtime here kwds[name] = lltype.cast_primitive(TYPE, value) return os.stat_result(positional, kwds) @@ -360,6 +436,8 @@ posix_declaration(ALL_STAT_FIELDS[_i]) del _i +STAT_FIELDS += ALL_STAT_FIELDS[-3:] # nsec_Xtime + # these two global vars only list the fields defined in the underlying platform STAT_FIELD_TYPES = dict(STAT_FIELDS) # {'st_xxx': TYPE} STAT_FIELD_NAMES = [_name for (_name, _TYPE) in STAT_FIELDS] @@ -368,16 +446,20 @@ STATVFS_FIELD_TYPES = dict(STATVFS_FIELDS) STATVFS_FIELD_NAMES = [name for name, tp in STATVFS_FIELDS] + def build_stat_result(st): # only for LL backends if TIMESPEC is not None: - atim = st.c_st_atim; atime = int(atim.c_tv_sec) + 1E-9 * int(atim.c_tv_nsec) - mtim = st.c_st_mtim; mtime = int(mtim.c_tv_sec) + 1E-9 * int(mtim.c_tv_nsec) - ctim = st.c_st_ctim; ctime = int(ctim.c_tv_sec) + 1E-9 * int(ctim.c_tv_nsec) + atim = st.c_st_atim + mtim = st.c_st_mtim + ctim = st.c_st_ctim + atime, extra_atime = atim.c_tv_sec, int(atim.c_tv_nsec) + mtime, extra_mtime = mtim.c_tv_sec, int(mtim.c_tv_nsec) + ctime, extra_ctime = ctim.c_tv_sec, int(ctim.c_tv_nsec) else: - atime = st.c_st_atime - mtime = st.c_st_mtime - ctime = st.c_st_ctime + atime, extra_atime = st.c_st_atime, 0 + mtime, extra_mtime = st.c_st_mtime, 0 + ctime, extra_ctime = st.c_st_ctime, 0 result = (st.c_st_mode, st.c_st_ino, @@ -395,6 +477,10 @@ if "st_rdev" in STAT_FIELD_TYPES: result += (st.c_st_rdev,) if "st_flags" in STAT_FIELD_TYPES: result += (st.c_st_flags,) + result += (extra_atime, + extra_mtime, + extra_ctime) + return make_stat_result(result) @@ -455,12 +541,14 @@ # console or LPT device return make_stat_result((win32traits._S_IFCHR, 0, 0, 0, 0, 0, - 0, 0, 0, 0)) + 0, 0, 0, 0, + 0, 0, 0)) elif filetype == win32traits.FILE_TYPE_PIPE: # socket or named pipe return make_stat_result((win32traits._S_IFIFO, 0, 0, 0, 0, 0, - 0, 0, 0, 0)) + 0, 0, 0, 0, + 0, 0, 0)) elif filetype == win32traits.FILE_TYPE_UNKNOWN: error = rwin32.GetLastError_saved() if error != 0: @@ -539,14 +627,11 @@ #__________________________________________________ # Helper functions for win32 if _WIN32: - from rpython.rlib.rwin32file import FILE_TIME_to_time_t_float + from rpython.rlib.rwin32file import FILE_TIME_to_time_t_nsec def make_longlong(high, low): return (rffi.r_longlong(high) << 32) + rffi.r_longlong(low) - # Seconds between 1.1.1601 and 1.1.1970 - secs_between_epochs = rffi.r_longlong(11644473600) - @specialize.arg(0) def win32_xstat(traits, path, traverse=False): win32traits = make_win32_traits(traits) @@ -582,14 +667,15 @@ def win32_attribute_data_to_stat(win32traits, info): st_mode = win32_attributes_to_mode(win32traits, info.c_dwFileAttributes) st_size = make_longlong(info.c_nFileSizeHigh, info.c_nFileSizeLow) - ctime = FILE_TIME_to_time_t_float(info.c_ftCreationTime) - mtime = FILE_TIME_to_time_t_float(info.c_ftLastWriteTime) - atime = FILE_TIME_to_time_t_float(info.c_ftLastAccessTime) + ctime, extra_ctime = FILE_TIME_to_time_t_nsec(info.c_ftCreationTime) + mtime, extra_mtime = FILE_TIME_to_time_t_nsec(info.c_ftLastWriteTime) + atime, extra_atime = FILE_TIME_to_time_t_nsec(info.c_ftLastAccessTime) result = (st_mode, 0, 0, 0, 0, 0, st_size, - atime, mtime, ctime) + atime, mtime, ctime, + extra_atime, extra_mtime, extra_ctime) return make_stat_result(result) @@ -597,9 +683,9 @@ # similar to the one above st_mode = win32_attributes_to_mode(win32traits, info.c_dwFileAttributes) st_size = make_longlong(info.c_nFileSizeHigh, info.c_nFileSizeLow) - ctime = FILE_TIME_to_time_t_float(info.c_ftCreationTime) - mtime = FILE_TIME_to_time_t_float(info.c_ftLastWriteTime) - atime = FILE_TIME_to_time_t_float(info.c_ftLastAccessTime) + ctime, extra_ctime = FILE_TIME_to_time_t_nsec(info.c_ftCreationTime) + mtime, extra_mtime = FILE_TIME_to_time_t_nsec(info.c_ftLastWriteTime) + atime, extra_atime = FILE_TIME_to_time_t_nsec(info.c_ftLastAccessTime) # specific to fstat() st_ino = make_longlong(info.c_nFileIndexHigh, info.c_nFileIndexLow) @@ -608,7 +694,8 @@ result = (st_mode, st_ino, 0, st_nlink, 0, 0, st_size, - atime, mtime, ctime) + atime, mtime, ctime, + extra_atime, extra_mtime, extra_ctime) return make_stat_result(result) diff --git a/rpython/rlib/rwin32file.py b/rpython/rlib/rwin32file.py --- a/rpython/rlib/rwin32file.py +++ b/rpython/rlib/rwin32file.py @@ -6,6 +6,7 @@ from rpython.translator.tool.cbuild import ExternalCompilationInfo from rpython.rtyper.tool import rffi_platform as platform from rpython.rlib.objectmodel import specialize +from rpython.rlib.rarithmetic import intmask @specialize.memo() def make_win32_traits(traits): @@ -213,13 +214,25 @@ return (rffi.r_longlong(high) << 32) + rffi.r_longlong(low) # Seconds between 1.1.1601 and 1.1.1970 -secs_between_epochs = rffi.r_longlong(11644473600) +secs_between_epochs = 11644473600.0 +hns_between_epochs = rffi.r_longlong(116444736000000000) # units of 100 nsec def FILE_TIME_to_time_t_float(filetime): ft = make_longlong(filetime.c_dwHighDateTime, filetime.c_dwLowDateTime) # FILETIME is in units of 100 nsec return float(ft) * (1.0 / 10000000.0) - secs_between_epochs +def FILE_TIME_to_time_t_nsec(filetime): + """Like the previous function, but returns a pair: (integer part + 'time_t' as a r_longlong, fractional part as an int measured in + nanoseconds). + """ + ft = make_longlong(filetime.c_dwHighDateTime, filetime.c_dwLowDateTime) + ft -= hns_between_epochs + int_part = ft / 10000000 + frac_part = ft - (int_part * 10000000) + return (int_part, intmask(frac_part) * 100) + def time_t_to_FILE_TIME(time, filetime): ft = rffi.r_longlong((time + secs_between_epochs) * 10000000) filetime.c_dwHighDateTime = rffi.r_uint(ft >> 32) diff --git a/rpython/rlib/test/test_rposix_stat.py b/rpython/rlib/test/test_rposix_stat.py --- a/rpython/rlib/test/test_rposix_stat.py +++ b/rpython/rlib/test/test_rposix_stat.py @@ -2,12 +2,20 @@ import py from rpython.rlib import rposix_stat from rpython.tool.udir import udir +from rpython.translator.c.test.test_genc import compile +from rpython.rtyper.lltypesystem import lltype + class TestPosixStatFunctions: @py.test.mark.skipif("sys.platform == 'win32'", reason="win32 only has the portable fields") def test_has_all_fields(self): - assert rposix_stat.STAT_FIELDS == rposix_stat.ALL_STAT_FIELDS[:13] + # XXX this test is obscure! it will fail if the exact set of + # XXX stat fields found differs from the one we expect on Linux. + # XXX Why? + assert rposix_stat.STAT_FIELDS == ( + rposix_stat.ALL_STAT_FIELDS[:13] + + rposix_stat.ALL_STAT_FIELDS[-3:]) def test_stat(self): def check(f): @@ -66,3 +74,27 @@ finally: os.close(dirfd) assert result.st_atime == tmpdir.join('file').atime() + +def test_high_precision_stat_time(): + def f(): + st = os.stat('.') + # should be supported on all platforms, but give a result whose + # precision might be lower than full nanosecond + highprec = rposix_stat.get_stat_ns_as_bigint(st, "ctime") + return '%s;%s' % (st.st_ctime, highprec.str()) + fc = compile(f, []) + as_string = fc() + asfloat, highprec = as_string.split(';') + asfloat = float(asfloat) + highprec = int(highprec) + st = os.stat('.') + assert abs(asfloat - st.st_ctime) < 500e-9 + assert abs(highprec - int(st.st_ctime * 1e9)) < 500 + assert abs(rposix_stat.get_stat_ns_as_bigint(st, "ctime").tolong() + - st.st_ctime * 1e9) < 3 + if rposix_stat.TIMESPEC is not None: + with lltype.scoped_alloc(rposix_stat.STAT_STRUCT.TO) as stresult: + rposix_stat.c_stat(".", stresult) + assert 0 <= stresult.c_st_ctim.c_tv_nsec <= 999999999 + assert highprec == (int(stresult.c_st_ctim.c_tv_sec) * 1000000000 + + int(stresult.c_st_ctim.c_tv_nsec)) _______________________________________________ pypy-commit mailing list pypy-commit@python.org https://mail.python.org/mailman/listinfo/pypy-commit