Mercurial > public > mercurial-scm > hg-stable
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) |