mirror of
https://github.com/facebook/sapling.git
synced 2024-10-09 08:18:15 +03:00
e47f7dc2fa
This is done by a script [2] using RedBaron [1], a tool designed for doing code refactoring. All "default" values are decided by the script and are strongly consistent with the existing code. There are 2 changes done manually to fix tests: [warn] mercurial/exchange.py: experimental.bundle2-output-capture: default needs manual removal [warn] mercurial/localrepo.py: experimental.hook-track-tags: default needs manual removal Since RedBaron is not confident about how to indent things [2]. [1]: https://github.com/PyCQA/redbaron [2]: https://github.com/PyCQA/redbaron/issues/100 [3]: #!/usr/bin/env python # codemod_configitems.py - codemod tool to fill configitems # # Copyright 2017 Facebook, Inc. # # 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 absolute_import, print_function import os import sys import redbaron def readpath(path): with open(path) as f: return f.read() def writepath(path, content): with open(path, 'w') as f: f.write(content) _configmethods = {'config', 'configbool', 'configint', 'configbytes', 'configlist', 'configdate'} def extractstring(rnode): """get the string from a RedBaron string or call_argument node""" while rnode.type != 'string': rnode = rnode.value return rnode.value[1:-1] # unquote, "'str'" -> "str" def uiconfigitems(red): """match *.ui.config* pattern, yield (node, method, args, section, name)""" for node in red.find_all('atomtrailers'): entry = None try: obj = node[-3].value method = node[-2].value args = node[-1] section = args[0].value name = args[1].value if (obj in ('ui', 'self') and method in _configmethods and section.type == 'string' and name.type == 'string'): entry = (node, method, args, extractstring(section), extractstring(name)) except Exception: pass else: if entry: yield entry def coreconfigitems(red): """match coreconfigitem(...) pattern, yield (node, args, section, name)""" for node in red.find_all('atomtrailers'): entry = None try: args = node[1] section = args[0].value name = args[1].value if (node[0].value == 'coreconfigitem' and section.type == 'string' and name.type == 'string'): entry = (node, args, extractstring(section), extractstring(name)) except Exception: pass else: if entry: yield entry def registercoreconfig(cfgred, section, name, defaultrepr): """insert coreconfigitem to cfgred AST section and name are plain string, defaultrepr is a string """ # find a place to insert the "coreconfigitem" item entries = list(coreconfigitems(cfgred)) for node, args, nodesection, nodename in reversed(entries): if (nodesection, nodename) < (section, name): # insert after this entry node.insert_after( 'coreconfigitem(%r, %r,\n' ' default=%s,\n' ')' % (section, name, defaultrepr)) return def main(argv): if not argv: print('Usage: codemod_configitems.py FILES\n' 'For example, FILES could be "{hgext,mercurial}/*/**.py"') dirname = os.path.dirname reporoot = dirname(dirname(dirname(os.path.abspath(__file__)))) # register configitems to this destination cfgpath = os.path.join(reporoot, 'mercurial', 'configitems.py') cfgred = redbaron.RedBaron(readpath(cfgpath)) # state about what to do registered = set((s, n) for n, a, s, n in coreconfigitems(cfgred)) toregister = {} # {(section, name): defaultrepr} coreconfigs = set() # {(section, name)}, whether it's used in core # first loop: scan all files before taking any action for i, path in enumerate(argv): print('(%d/%d) scanning %s' % (i + 1, len(argv), path)) iscore = ('mercurial' in path) and ('hgext' not in path) red = redbaron.RedBaron(readpath(path)) # find all repo.ui.config* and ui.config* calls, and collect their # section, name and default value information. for node, method, args, section, name in uiconfigitems(red): if section == 'web': # [web] section has some weirdness, ignore them for now continue defaultrepr = None key = (section, name) if len(args) == 2: if key in registered: continue if method == 'configlist': defaultrepr = 'list' elif method == 'configbool': defaultrepr = 'False' else: defaultrepr = 'None' elif len(args) >= 3 and (args[2].target is None or args[2].target.value == 'default'): # try to understand the "default" value dnode = args[2].value if dnode.type == 'name': if dnode.value in {'None', 'True', 'False'}: defaultrepr = dnode.value elif dnode.type == 'string': defaultrepr = repr(dnode.value[1:-1]) elif dnode.type in ('int', 'float'): defaultrepr = dnode.value # inconsistent default if key in toregister and toregister[key] != defaultrepr: defaultrepr = None # interesting to rewrite if key not in registered: if defaultrepr is None: print('[note] %s: %s.%s: unsupported default' % (path, section, name)) registered.add(key) # skip checking it again else: toregister[key] = defaultrepr if iscore: coreconfigs.add(key) # second loop: rewrite files given "toregister" result for path in argv: # reconstruct redbaron - trade CPU for memory red = redbaron.RedBaron(readpath(path)) changed = False for node, method, args, section, name in uiconfigitems(red): key = (section, name) defaultrepr = toregister.get(key) if defaultrepr is None or key not in coreconfigs: continue if len(args) >= 3 and (args[2].target is None or args[2].target.value == 'default'): try: del args[2] changed = True except Exception: # redbaron fails to do the rewrite due to indentation # see https://github.com/PyCQA/redbaron/issues/100 print('[warn] %s: %s.%s: default needs manual removal' % (path, section, name)) if key not in registered: print('registering %s.%s' % (section, name)) registercoreconfig(cfgred, section, name, defaultrepr) registered.add(key) if changed: print('updating %s' % path) writepath(path, red.dumps()) if toregister: print('updating configitems.py') writepath(cfgpath, cfgred.dumps()) if __name__ == "__main__": sys.exit(main(sys.argv[1:]))
281 lines
10 KiB
Python
281 lines
10 KiB
Python
# progress.py progress bars related code
|
|
#
|
|
# Copyright (C) 2010 Augie Fackler <durin42@gmail.com>
|
|
#
|
|
# 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 absolute_import
|
|
|
|
import errno
|
|
import threading
|
|
import time
|
|
|
|
from .i18n import _
|
|
from . import encoding
|
|
|
|
def spacejoin(*args):
|
|
return ' '.join(s for s in args if s)
|
|
|
|
def shouldprint(ui):
|
|
return not (ui.quiet or ui.plain('progress')) and (
|
|
ui._isatty(ui.ferr) or ui.configbool('progress', 'assume-tty'))
|
|
|
|
def fmtremaining(seconds):
|
|
"""format a number of remaining seconds in human readable way
|
|
|
|
This will properly display seconds, minutes, hours, days if needed"""
|
|
if seconds < 60:
|
|
# i18n: format XX seconds as "XXs"
|
|
return _("%02ds") % (seconds)
|
|
minutes = seconds // 60
|
|
if minutes < 60:
|
|
seconds -= minutes * 60
|
|
# i18n: format X minutes and YY seconds as "XmYYs"
|
|
return _("%dm%02ds") % (minutes, seconds)
|
|
# we're going to ignore seconds in this case
|
|
minutes += 1
|
|
hours = minutes // 60
|
|
minutes -= hours * 60
|
|
if hours < 30:
|
|
# i18n: format X hours and YY minutes as "XhYYm"
|
|
return _("%dh%02dm") % (hours, minutes)
|
|
# we're going to ignore minutes in this case
|
|
hours += 1
|
|
days = hours // 24
|
|
hours -= days * 24
|
|
if days < 15:
|
|
# i18n: format X days and YY hours as "XdYYh"
|
|
return _("%dd%02dh") % (days, hours)
|
|
# we're going to ignore hours in this case
|
|
days += 1
|
|
weeks = days // 7
|
|
days -= weeks * 7
|
|
if weeks < 55:
|
|
# i18n: format X weeks and YY days as "XwYYd"
|
|
return _("%dw%02dd") % (weeks, days)
|
|
# we're going to ignore days and treat a year as 52 weeks
|
|
weeks += 1
|
|
years = weeks // 52
|
|
weeks -= years * 52
|
|
# i18n: format X years and YY weeks as "XyYYw"
|
|
return _("%dy%02dw") % (years, weeks)
|
|
|
|
# file_write() and file_flush() of Python 2 do not restart on EINTR if
|
|
# the file is attached to a "slow" device (e.g. a terminal) and raise
|
|
# IOError. We cannot know how many bytes would be written by file_write(),
|
|
# but a progress text is known to be short enough to be written by a
|
|
# single write() syscall, so we can just retry file_write() with the whole
|
|
# text. (issue5532)
|
|
#
|
|
# This should be a short-term workaround. We'll need to fix every occurrence
|
|
# of write() to a terminal or pipe.
|
|
def _eintrretry(func, *args):
|
|
while True:
|
|
try:
|
|
return func(*args)
|
|
except IOError as err:
|
|
if err.errno == errno.EINTR:
|
|
continue
|
|
raise
|
|
|
|
class progbar(object):
|
|
def __init__(self, ui):
|
|
self.ui = ui
|
|
self._refreshlock = threading.Lock()
|
|
self.resetstate()
|
|
|
|
def resetstate(self):
|
|
self.topics = []
|
|
self.topicstates = {}
|
|
self.starttimes = {}
|
|
self.startvals = {}
|
|
self.printed = False
|
|
self.lastprint = time.time() + float(self.ui.config(
|
|
'progress', 'delay'))
|
|
self.curtopic = None
|
|
self.lasttopic = None
|
|
self.indetcount = 0
|
|
self.refresh = float(self.ui.config(
|
|
'progress', 'refresh'))
|
|
self.changedelay = max(3 * self.refresh,
|
|
float(self.ui.config(
|
|
'progress', 'changedelay')))
|
|
self.order = self.ui.configlist(
|
|
'progress', 'format',
|
|
default=['topic', 'bar', 'number', 'estimate'])
|
|
|
|
def show(self, now, topic, pos, item, unit, total):
|
|
if not shouldprint(self.ui):
|
|
return
|
|
termwidth = self.width()
|
|
self.printed = True
|
|
head = ''
|
|
needprogress = False
|
|
tail = ''
|
|
for indicator in self.order:
|
|
add = ''
|
|
if indicator == 'topic':
|
|
add = topic
|
|
elif indicator == 'number':
|
|
if total:
|
|
add = ('% ' + str(len(str(total))) +
|
|
's/%s') % (pos, total)
|
|
else:
|
|
add = str(pos)
|
|
elif indicator.startswith('item') and item:
|
|
slice = 'end'
|
|
if '-' in indicator:
|
|
wid = int(indicator.split('-')[1])
|
|
elif '+' in indicator:
|
|
slice = 'beginning'
|
|
wid = int(indicator.split('+')[1])
|
|
else:
|
|
wid = 20
|
|
if slice == 'end':
|
|
add = encoding.trim(item, wid, leftside=True)
|
|
else:
|
|
add = encoding.trim(item, wid)
|
|
add += (wid - encoding.colwidth(add)) * ' '
|
|
elif indicator == 'bar':
|
|
add = ''
|
|
needprogress = True
|
|
elif indicator == 'unit' and unit:
|
|
add = unit
|
|
elif indicator == 'estimate':
|
|
add = self.estimate(topic, pos, total, now)
|
|
elif indicator == 'speed':
|
|
add = self.speed(topic, pos, unit, now)
|
|
if not needprogress:
|
|
head = spacejoin(head, add)
|
|
else:
|
|
tail = spacejoin(tail, add)
|
|
if needprogress:
|
|
used = 0
|
|
if head:
|
|
used += encoding.colwidth(head) + 1
|
|
if tail:
|
|
used += encoding.colwidth(tail) + 1
|
|
progwidth = termwidth - used - 3
|
|
if total and pos <= total:
|
|
amt = pos * progwidth // total
|
|
bar = '=' * (amt - 1)
|
|
if amt > 0:
|
|
bar += '>'
|
|
bar += ' ' * (progwidth - amt)
|
|
else:
|
|
progwidth -= 3
|
|
self.indetcount += 1
|
|
# mod the count by twice the width so we can make the
|
|
# cursor bounce between the right and left sides
|
|
amt = self.indetcount % (2 * progwidth)
|
|
amt -= progwidth
|
|
bar = (' ' * int(progwidth - abs(amt)) + '<=>' +
|
|
' ' * int(abs(amt)))
|
|
prog = ''.join(('[', bar , ']'))
|
|
out = spacejoin(head, prog, tail)
|
|
else:
|
|
out = spacejoin(head, tail)
|
|
self._writeerr('\r' + encoding.trim(out, termwidth))
|
|
self.lasttopic = topic
|
|
self._flusherr()
|
|
|
|
def clear(self):
|
|
if not self.printed or not self.lastprint or not shouldprint(self.ui):
|
|
return
|
|
self._writeerr('\r%s\r' % (' ' * self.width()))
|
|
if self.printed:
|
|
# force immediate re-paint of progress bar
|
|
self.lastprint = 0
|
|
|
|
def complete(self):
|
|
if not shouldprint(self.ui):
|
|
return
|
|
if self.ui.configbool('progress', 'clear-complete'):
|
|
self.clear()
|
|
else:
|
|
self._writeerr('\n')
|
|
self._flusherr()
|
|
|
|
def _flusherr(self):
|
|
_eintrretry(self.ui.ferr.flush)
|
|
|
|
def _writeerr(self, msg):
|
|
_eintrretry(self.ui.ferr.write, msg)
|
|
|
|
def width(self):
|
|
tw = self.ui.termwidth()
|
|
return min(int(self.ui.config('progress', 'width', default=tw)), tw)
|
|
|
|
def estimate(self, topic, pos, total, now):
|
|
if total is None:
|
|
return ''
|
|
initialpos = self.startvals[topic]
|
|
target = total - initialpos
|
|
delta = pos - initialpos
|
|
if delta > 0:
|
|
elapsed = now - self.starttimes[topic]
|
|
# experimental config: progress.estimate
|
|
if elapsed > float(
|
|
self.ui.config('progress', 'estimate')):
|
|
seconds = (elapsed * (target - delta)) // delta + 1
|
|
return fmtremaining(seconds)
|
|
return ''
|
|
|
|
def speed(self, topic, pos, unit, now):
|
|
initialpos = self.startvals[topic]
|
|
delta = pos - initialpos
|
|
elapsed = now - self.starttimes[topic]
|
|
if elapsed > float(
|
|
self.ui.config('progress', 'estimate')):
|
|
return _('%d %s/sec') % (delta / elapsed, unit)
|
|
return ''
|
|
|
|
def _oktoprint(self, now):
|
|
'''Check if conditions are met to print - e.g. changedelay elapsed'''
|
|
if (self.lasttopic is None # first time we printed
|
|
# not a topic change
|
|
or self.curtopic == self.lasttopic
|
|
# it's been long enough we should print anyway
|
|
or now - self.lastprint >= self.changedelay):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def progress(self, topic, pos, item='', unit='', total=None):
|
|
now = time.time()
|
|
self._refreshlock.acquire()
|
|
try:
|
|
if pos is None:
|
|
self.starttimes.pop(topic, None)
|
|
self.startvals.pop(topic, None)
|
|
self.topicstates.pop(topic, None)
|
|
# reset the progress bar if this is the outermost topic
|
|
if self.topics and self.topics[0] == topic and self.printed:
|
|
self.complete()
|
|
self.resetstate()
|
|
# truncate the list of topics assuming all topics within
|
|
# this one are also closed
|
|
if topic in self.topics:
|
|
self.topics = self.topics[:self.topics.index(topic)]
|
|
# reset the last topic to the one we just unwound to,
|
|
# so that higher-level topics will be stickier than
|
|
# lower-level topics
|
|
if self.topics:
|
|
self.lasttopic = self.topics[-1]
|
|
else:
|
|
self.lasttopic = None
|
|
else:
|
|
if topic not in self.topics:
|
|
self.starttimes[topic] = now
|
|
self.startvals[topic] = pos
|
|
self.topics.append(topic)
|
|
self.topicstates[topic] = pos, item, unit, total
|
|
self.curtopic = topic
|
|
if now - self.lastprint >= self.refresh and self.topics:
|
|
if self._oktoprint(now):
|
|
self.lastprint = now
|
|
self.show(now, topic, *self.topicstates[topic])
|
|
finally:
|
|
self._refreshlock.release()
|