comparison mercurial/util.py @ 36636:c6061cadb400

util: extract all date-related utils in utils/dateutil module With this commit, util.py lose 262 lines Note for extensions author, if this commit breaks your extension, you can pull the step-by-step split here to help you more easily pinpoint the renaming that broke your extension: hg pull https://bitbucket.org/octobus/mercurial-devel/ -r ac1f6453010d Differential Revision: https://phab.mercurial-scm.org/D2282
author Boris Feld <boris.feld@octobus.net>
date Thu, 15 Feb 2018 17:18:26 +0100
parents 281f66777ff0
children c98d1c6763a6
comparison
equal deleted inserted replaced
36635:4de15c54e59f 36636:c6061cadb400
15 15
16 from __future__ import absolute_import, print_function 16 from __future__ import absolute_import, print_function
17 17
18 import abc 18 import abc
19 import bz2 19 import bz2
20 import calendar
21 import codecs 20 import codecs
22 import collections 21 import collections
23 import contextlib 22 import contextlib
24 import datetime
25 import errno 23 import errno
26 import gc 24 import gc
27 import hashlib 25 import hashlib
28 import imp 26 import imp
29 import io 27 import io
53 node as nodemod, 51 node as nodemod,
54 policy, 52 policy,
55 pycompat, 53 pycompat,
56 urllibcompat, 54 urllibcompat,
57 ) 55 )
56 from .utils import dateutil
58 57
59 base85 = policy.importmod(r'base85') 58 base85 = policy.importmod(r'base85')
60 osutil = policy.importmod(r'osutil') 59 osutil = policy.importmod(r'osutil')
61 parsers = policy.importmod(r'parsers') 60 parsers = policy.importmod(r'parsers')
62 61
853 if n == 3: 852 if n == 3:
854 return (vints[0], vints[1], vints[2]) 853 return (vints[0], vints[1], vints[2])
855 if n == 4: 854 if n == 4:
856 return (vints[0], vints[1], vints[2], extra) 855 return (vints[0], vints[1], vints[2], extra)
857 856
858 # used by parsedate
859 defaultdateformats = (
860 '%Y-%m-%dT%H:%M:%S', # the 'real' ISO8601
861 '%Y-%m-%dT%H:%M', # without seconds
862 '%Y-%m-%dT%H%M%S', # another awful but legal variant without :
863 '%Y-%m-%dT%H%M', # without seconds
864 '%Y-%m-%d %H:%M:%S', # our common legal variant
865 '%Y-%m-%d %H:%M', # without seconds
866 '%Y-%m-%d %H%M%S', # without :
867 '%Y-%m-%d %H%M', # without seconds
868 '%Y-%m-%d %I:%M:%S%p',
869 '%Y-%m-%d %H:%M',
870 '%Y-%m-%d %I:%M%p',
871 '%Y-%m-%d',
872 '%m-%d',
873 '%m/%d',
874 '%m/%d/%y',
875 '%m/%d/%Y',
876 '%a %b %d %H:%M:%S %Y',
877 '%a %b %d %I:%M:%S%p %Y',
878 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
879 '%b %d %H:%M:%S %Y',
880 '%b %d %I:%M:%S%p %Y',
881 '%b %d %H:%M:%S',
882 '%b %d %I:%M:%S%p',
883 '%b %d %H:%M',
884 '%b %d %I:%M%p',
885 '%b %d %Y',
886 '%b %d',
887 '%H:%M:%S',
888 '%I:%M:%S%p',
889 '%H:%M',
890 '%I:%M%p',
891 )
892
893 extendeddateformats = defaultdateformats + (
894 "%Y",
895 "%Y-%m",
896 "%b",
897 "%b %Y",
898 )
899
900 def cachefunc(func): 857 def cachefunc(func):
901 '''cache the result of function calls''' 858 '''cache the result of function calls'''
902 # XXX doesn't handle keywords args 859 # XXX doesn't handle keywords args
903 if func.__code__.co_argcount == 0: 860 if func.__code__.co_argcount == 0:
904 cache = [] 861 cache = []
2301 data = self._fh.read(min(n, self._left)) 2258 data = self._fh.read(min(n, self._left))
2302 self._left -= len(data) 2259 self._left -= len(data)
2303 assert self._left >= 0 2260 assert self._left >= 0
2304 2261
2305 return data 2262 return data
2306
2307 def makedate(timestamp=None):
2308 '''Return a unix timestamp (or the current time) as a (unixtime,
2309 offset) tuple based off the local timezone.'''
2310 if timestamp is None:
2311 timestamp = time.time()
2312 if timestamp < 0:
2313 hint = _("check your clock")
2314 raise Abort(_("negative timestamp: %d") % timestamp, hint=hint)
2315 delta = (datetime.datetime.utcfromtimestamp(timestamp) -
2316 datetime.datetime.fromtimestamp(timestamp))
2317 tz = delta.days * 86400 + delta.seconds
2318 return timestamp, tz
2319
2320 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
2321 """represent a (unixtime, offset) tuple as a localized time.
2322 unixtime is seconds since the epoch, and offset is the time zone's
2323 number of seconds away from UTC.
2324
2325 >>> datestr((0, 0))
2326 'Thu Jan 01 00:00:00 1970 +0000'
2327 >>> datestr((42, 0))
2328 'Thu Jan 01 00:00:42 1970 +0000'
2329 >>> datestr((-42, 0))
2330 'Wed Dec 31 23:59:18 1969 +0000'
2331 >>> datestr((0x7fffffff, 0))
2332 'Tue Jan 19 03:14:07 2038 +0000'
2333 >>> datestr((-0x80000000, 0))
2334 'Fri Dec 13 20:45:52 1901 +0000'
2335 """
2336 t, tz = date or makedate()
2337 if "%1" in format or "%2" in format or "%z" in format:
2338 sign = (tz > 0) and "-" or "+"
2339 minutes = abs(tz) // 60
2340 q, r = divmod(minutes, 60)
2341 format = format.replace("%z", "%1%2")
2342 format = format.replace("%1", "%c%02d" % (sign, q))
2343 format = format.replace("%2", "%02d" % r)
2344 d = t - tz
2345 if d > 0x7fffffff:
2346 d = 0x7fffffff
2347 elif d < -0x80000000:
2348 d = -0x80000000
2349 # Never use time.gmtime() and datetime.datetime.fromtimestamp()
2350 # because they use the gmtime() system call which is buggy on Windows
2351 # for negative values.
2352 t = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=d)
2353 s = encoding.strtolocal(t.strftime(encoding.strfromlocal(format)))
2354 return s
2355
2356 def shortdate(date=None):
2357 """turn (timestamp, tzoff) tuple into iso 8631 date."""
2358 return datestr(date, format='%Y-%m-%d')
2359
2360 def parsetimezone(s):
2361 """find a trailing timezone, if any, in string, and return a
2362 (offset, remainder) pair"""
2363 s = pycompat.bytestr(s)
2364
2365 if s.endswith("GMT") or s.endswith("UTC"):
2366 return 0, s[:-3].rstrip()
2367
2368 # Unix-style timezones [+-]hhmm
2369 if len(s) >= 5 and s[-5] in "+-" and s[-4:].isdigit():
2370 sign = (s[-5] == "+") and 1 or -1
2371 hours = int(s[-4:-2])
2372 minutes = int(s[-2:])
2373 return -sign * (hours * 60 + minutes) * 60, s[:-5].rstrip()
2374
2375 # ISO8601 trailing Z
2376 if s.endswith("Z") and s[-2:-1].isdigit():
2377 return 0, s[:-1]
2378
2379 # ISO8601-style [+-]hh:mm
2380 if (len(s) >= 6 and s[-6] in "+-" and s[-3] == ":" and
2381 s[-5:-3].isdigit() and s[-2:].isdigit()):
2382 sign = (s[-6] == "+") and 1 or -1
2383 hours = int(s[-5:-3])
2384 minutes = int(s[-2:])
2385 return -sign * (hours * 60 + minutes) * 60, s[:-6]
2386
2387 return None, s
2388
2389 def strdate(string, format, defaults=None):
2390 """parse a localized time string and return a (unixtime, offset) tuple.
2391 if the string cannot be parsed, ValueError is raised."""
2392 if defaults is None:
2393 defaults = {}
2394
2395 # NOTE: unixtime = localunixtime + offset
2396 offset, date = parsetimezone(string)
2397
2398 # add missing elements from defaults
2399 usenow = False # default to using biased defaults
2400 for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity
2401 part = pycompat.bytestr(part)
2402 found = [True for p in part if ("%"+p) in format]
2403 if not found:
2404 date += "@" + defaults[part][usenow]
2405 format += "@%" + part[0]
2406 else:
2407 # We've found a specific time element, less specific time
2408 # elements are relative to today
2409 usenow = True
2410
2411 timetuple = time.strptime(encoding.strfromlocal(date),
2412 encoding.strfromlocal(format))
2413 localunixtime = int(calendar.timegm(timetuple))
2414 if offset is None:
2415 # local timezone
2416 unixtime = int(time.mktime(timetuple))
2417 offset = unixtime - localunixtime
2418 else:
2419 unixtime = localunixtime + offset
2420 return unixtime, offset
2421
2422 def parsedate(date, formats=None, bias=None):
2423 """parse a localized date/time and return a (unixtime, offset) tuple.
2424
2425 The date may be a "unixtime offset" string or in one of the specified
2426 formats. If the date already is a (unixtime, offset) tuple, it is returned.
2427
2428 >>> parsedate(b' today ') == parsedate(
2429 ... datetime.date.today().strftime('%b %d').encode('ascii'))
2430 True
2431 >>> parsedate(b'yesterday ') == parsedate(
2432 ... (datetime.date.today() - datetime.timedelta(days=1)
2433 ... ).strftime('%b %d').encode('ascii'))
2434 True
2435 >>> now, tz = makedate()
2436 >>> strnow, strtz = parsedate(b'now')
2437 >>> (strnow - now) < 1
2438 True
2439 >>> tz == strtz
2440 True
2441 """
2442 if bias is None:
2443 bias = {}
2444 if not date:
2445 return 0, 0
2446 if isinstance(date, tuple) and len(date) == 2:
2447 return date
2448 if not formats:
2449 formats = defaultdateformats
2450 date = date.strip()
2451
2452 if date == 'now' or date == _('now'):
2453 return makedate()
2454 if date == 'today' or date == _('today'):
2455 date = datetime.date.today().strftime(r'%b %d')
2456 date = encoding.strtolocal(date)
2457 elif date == 'yesterday' or date == _('yesterday'):
2458 date = (datetime.date.today() -
2459 datetime.timedelta(days=1)).strftime(r'%b %d')
2460 date = encoding.strtolocal(date)
2461
2462 try:
2463 when, offset = map(int, date.split(' '))
2464 except ValueError:
2465 # fill out defaults
2466 now = makedate()
2467 defaults = {}
2468 for part in ("d", "mb", "yY", "HI", "M", "S"):
2469 # this piece is for rounding the specific end of unknowns
2470 b = bias.get(part)
2471 if b is None:
2472 if part[0:1] in "HMS":
2473 b = "00"
2474 else:
2475 b = "0"
2476
2477 # this piece is for matching the generic end to today's date
2478 n = datestr(now, "%" + part[0:1])
2479
2480 defaults[part] = (b, n)
2481
2482 for format in formats:
2483 try:
2484 when, offset = strdate(date, format, defaults)
2485 except (ValueError, OverflowError):
2486 pass
2487 else:
2488 break
2489 else:
2490 raise error.ParseError(
2491 _('invalid date: %r') % pycompat.bytestr(date))
2492 # validate explicit (probably user-specified) date and
2493 # time zone offset. values must fit in signed 32 bits for
2494 # current 32-bit linux runtimes. timezones go from UTC-12
2495 # to UTC+14
2496 if when < -0x80000000 or when > 0x7fffffff:
2497 raise error.ParseError(_('date exceeds 32 bits: %d') % when)
2498 if offset < -50400 or offset > 43200:
2499 raise error.ParseError(_('impossible time zone offset: %d') % offset)
2500 return when, offset
2501
2502 def matchdate(date):
2503 """Return a function that matches a given date match specifier
2504
2505 Formats include:
2506
2507 '{date}' match a given date to the accuracy provided
2508
2509 '<{date}' on or before a given date
2510
2511 '>{date}' on or after a given date
2512
2513 >>> p1 = parsedate(b"10:29:59")
2514 >>> p2 = parsedate(b"10:30:00")
2515 >>> p3 = parsedate(b"10:30:59")
2516 >>> p4 = parsedate(b"10:31:00")
2517 >>> p5 = parsedate(b"Sep 15 10:30:00 1999")
2518 >>> f = matchdate(b"10:30")
2519 >>> f(p1[0])
2520 False
2521 >>> f(p2[0])
2522 True
2523 >>> f(p3[0])
2524 True
2525 >>> f(p4[0])
2526 False
2527 >>> f(p5[0])
2528 False
2529 """
2530
2531 def lower(date):
2532 d = {'mb': "1", 'd': "1"}
2533 return parsedate(date, extendeddateformats, d)[0]
2534
2535 def upper(date):
2536 d = {'mb': "12", 'HI': "23", 'M': "59", 'S': "59"}
2537 for days in ("31", "30", "29"):
2538 try:
2539 d["d"] = days
2540 return parsedate(date, extendeddateformats, d)[0]
2541 except error.ParseError:
2542 pass
2543 d["d"] = "28"
2544 return parsedate(date, extendeddateformats, d)[0]
2545
2546 date = date.strip()
2547
2548 if not date:
2549 raise Abort(_("dates cannot consist entirely of whitespace"))
2550 elif date[0] == "<":
2551 if not date[1:]:
2552 raise Abort(_("invalid day spec, use '<DATE'"))
2553 when = upper(date[1:])
2554 return lambda x: x <= when
2555 elif date[0] == ">":
2556 if not date[1:]:
2557 raise Abort(_("invalid day spec, use '>DATE'"))
2558 when = lower(date[1:])
2559 return lambda x: x >= when
2560 elif date[0] == "-":
2561 try:
2562 days = int(date[1:])
2563 except ValueError:
2564 raise Abort(_("invalid day spec: %s") % date[1:])
2565 if days < 0:
2566 raise Abort(_("%s must be nonnegative (see 'hg help dates')")
2567 % date[1:])
2568 when = makedate()[0] - days * 3600 * 24
2569 return lambda x: x >= when
2570 elif " to " in date:
2571 a, b = date.split(" to ")
2572 start, stop = lower(a), upper(b)
2573 return lambda x: x >= start and x <= stop
2574 else:
2575 start, stop = lower(date), upper(date)
2576 return lambda x: x >= start and x <= stop
2577 2263
2578 def stringmatcher(pattern, casesensitive=True): 2264 def stringmatcher(pattern, casesensitive=True):
2579 """ 2265 """
2580 accepts a string, possibly starting with 're:' or 'literal:' prefix. 2266 accepts a string, possibly starting with 're:' or 'literal:' prefix.
2581 returns the matcher name, pattern, and matcher function. 2267 returns the matcher name, pattern, and matcher function.
4301 byte = ord(readexactly(fh, 1)) 3987 byte = ord(readexactly(fh, 1))
4302 result |= ((byte & 0x7f) << shift) 3988 result |= ((byte & 0x7f) << shift)
4303 if not (byte & 0x80): 3989 if not (byte & 0x80):
4304 return result 3990 return result
4305 shift += 7 3991 shift += 7
3992
3993 ###
3994 # Deprecation warnings for util.py splitting
3995 ###
3996
3997 defaultdateformats = dateutil.defaultdateformats
3998
3999 extendeddateformats = dateutil.extendeddateformats
4000
4001 def makedate(*args, **kwargs):
4002 msg = ("'util.makedate' is deprecated, "
4003 "use 'utils.dateutil.makedate'")
4004 nouideprecwarn(msg, "4.6")
4005 return dateutil.makedate(*args, **kwargs)
4006
4007 def datestr(*args, **kwargs):
4008 msg = ("'util.datestr' is deprecated, "
4009 "use 'utils.dateutil.datestr'")
4010 nouideprecwarn(msg, "4.6")
4011 debugstacktrace()
4012 return dateutil.datestr(*args, **kwargs)
4013
4014 def shortdate(*args, **kwargs):
4015 msg = ("'util.shortdate' is deprecated, "
4016 "use 'utils.dateutil.shortdate'")
4017 nouideprecwarn(msg, "4.6")
4018 return dateutil.shortdate(*args, **kwargs)
4019
4020 def parsetimezone(*args, **kwargs):
4021 msg = ("'util.parsetimezone' is deprecated, "
4022 "use 'utils.dateutil.parsetimezone'")
4023 nouideprecwarn(msg, "4.6")
4024 return dateutil.parsetimezone(*args, **kwargs)
4025
4026 def strdate(*args, **kwargs):
4027 msg = ("'util.strdate' is deprecated, "
4028 "use 'utils.dateutil.strdate'")
4029 nouideprecwarn(msg, "4.6")
4030 return dateutil.strdate(*args, **kwargs)
4031
4032 def parsedate(*args, **kwargs):
4033 msg = ("'util.parsedate' is deprecated, "
4034 "use 'utils.dateutil.parsedate'")
4035 nouideprecwarn(msg, "4.6")
4036 return dateutil.parsedate(*args, **kwargs)
4037
4038 def matchdate(*args, **kwargs):
4039 msg = ("'util.matchdate' is deprecated, "
4040 "use 'utils.dateutil.matchdate'")
4041 nouideprecwarn(msg, "4.6")
4042 return dateutil.matchdate(*args, **kwargs)