def run(self): try: import readline except ImportError: return hist = builtins.__xonsh_history__ while self.wait_for_gc and hist.gc.is_alive(): time.sleep(0.011) # gc sleeps for 0.01 secs, sleep a beat longer files = hist.gc.files() i = 1 for _, _, f in files: try: lj = LazyJSON(f, reopen=False) for command in lj['cmds']: inp = command['inp'].splitlines() for line in inp: if line == 'EOF': continue readline.add_history(line) if RL_LIB is not None: RL_LIB.history_set_pos(i) i += 1 lj.close() except (IOError, OSError, ValueError): continue
def files(self, only_unlocked=False): """Find and return the history files. Optionally locked files may be excluded. This is sorted by the last closed time. Returns a list of (timestamp, file) tuples. """ # pylint: disable=no-member xdd = builtins.__xonsh_env__.get('XONSH_DATA_DIR') xdd = expanduser_abs_path(xdd) fs = [f for f in glob.iglob(os.path.join(xdd, 'xonsh-*.json'))] files = [] for f in fs: try: lj = LazyJSON(f, reopen=False) if only_unlocked and lj['locked']: continue # info: closing timestamp, number of commands, filename files.append((lj['ts'][1] or time.time(), len(lj.sizes['cmds']) - 1, f)) lj.close() except (IOError, OSError, ValueError): continue files.sort() return files
def files(self, only_unlocked=False): """Find and return the history files. Optionally locked files may be excluded. This is sorted by the last closed time. Returns a list of (timestamp, number of cmds, file name) tuples. """ # pylint: disable=no-member env = getattr(builtins, '__xonsh_env__', None) if env is None: return [] fs = _get_history_files(sort=False) files = [] for f in fs: try: if os.path.getsize(f) == 0: # collect empty files (for gc) files.append((time.time(), 0, f)) continue lj = LazyJSON(f, reopen=False) if only_unlocked and lj['locked']: continue # info: closing timestamp, number of commands, filename files.append((lj['ts'][1] or time.time(), len(lj.sizes['cmds']) - 1, f)) lj.close() except (IOError, OSError, ValueError): continue files.sort() return files
class Replayer(object): """Replays a xonsh history file.""" def __init__(self, f, reopen=True): """ Parameters ---------- f : file handle or str Path to xonsh history file. reopen : bool, optional Whether new file handle should be opened for each load, passed directly into LazyJSON class. """ self._lj = LazyJSON(f, reopen=reopen) def __del__(self): self._lj.close() def replay(self, merge_envs=DEFAULT_MERGE_ENVS, target=None): """Replays the history specified, returns the history object where the code was executed. Parameters ---------- merge_env : tuple of str or Mappings, optional Describes how to merge the environments, in order of increasing precednce. Available strings are 'replay' and 'native'. The 'replay' env comes from the history file that we are replaying. The 'native' env comes from what this instance of xonsh was started up with. Instead of a string, a dict or other mapping may be passed in as well. Defaults to ('replay', 'native'). target : str, optional Path to new history file. """ shell = builtins.__xonsh_shell__ re_env = self._lj['env'].load() new_env = self._merge_envs(merge_envs, re_env) new_hist = History(env=new_env.detype(), locked=True, ts=[time.time(), None], gc=False, filename=target) with swap(builtins, '__xonsh_env__', new_env), swap(builtins, '__xonsh_history__', new_hist): for cmd in self._lj['cmds']: inp = cmd['inp'] shell.default(inp) if builtins.__xonsh_exit__: # prevent premature exit builtins.__xonsh_exit__ = False new_hist.flush(at_exit=True) return new_hist def _merge_envs(self, merge_envs, re_env): new_env = {} for e in merge_envs: if e == 'replay': new_env.update(re_env) elif e == 'native': new_env.update(builtins.__xonsh_env__) elif isinstance(e, cabc.Mapping): new_env.update(e) else: raise TypeError('Type of env not understood: {0!r}'.format(e)) new_env = Env(**new_env) return new_env
class HistoryDiffer(object): """This class helps diff two xonsh history files.""" def __init__(self, afile, bfile, reopen=False, verbose=False): """ Parameters ---------- afile : file handle or str The first file to diff bfile : file handle or str The second file to diff reopen : bool, optional Whether or not to reopen the file handles each time. The default here is opposite from the LazyJSON default because we know that we will be doing a lot of reading so it is best to keep the handles open. verbose : bool, optional Whether to print a verbose amount of information. """ self.a = LazyJSON(afile, reopen=reopen) self.b = LazyJSON(bfile, reopen=reopen) self.verbose = verbose self.sm = difflib.SequenceMatcher(autojunk=False) def __del__(self): self.a.close() self.b.close() def __str__(self): return self.format() def _header_line(self, lj): s = lj._f.name if hasattr(lj._f, 'name') else '' s += ' (' + lj['sessionid'] + ')' s += ' [locked]' if lj['locked'] else ' [unlocked]' ts = lj['ts'].load() ts0 = datetime.datetime.fromtimestamp(ts[0]) s += ' started: ' + ts0.isoformat(' ') if ts[1] is not None: ts1 = datetime.datetime.fromtimestamp(ts[1]) s += ' stopped: ' + ts1.isoformat(' ') + ' runtime: ' + str(ts1 - ts0) return s def header(self): """Computes a header string difference.""" s = ('{red}--- {aline}{no_color}\n' '{green}+++ {bline}{no_color}') s = s.format(aline=self._header_line(self.a), bline=self._header_line(self.b), red=RED, green=GREEN, no_color=NO_COLOR) return s def _env_both_diff(self, in_both, aenv, benv): sm = self.sm s = '' for key in sorted(in_both): aval = aenv[key] bval = benv[key] if aval == bval: continue s += '{0!r} is in both, but differs\n'.format(key) s += bold_str_diff(aval, bval, sm=sm) + '\n' return s def _env_in_one_diff(self, x, y, color, xid, xenv): only_x = sorted(x - y) if len(only_x) == 0: return '' if self.verbose: xstr = ',\n'.join([' {0!r}: {1!r}'.format(key, xenv[key]) \ for key in only_x]) xstr = '\n' + xstr else: xstr = ', '.join(['{0!r}'.format(key) for key in only_x]) in_x = 'These vars are only in {color}{xid}{no_color}: {{{xstr}}}\n\n' return in_x.format(xid=xid, color=color, no_color=NO_COLOR, xstr=xstr) def envdiff(self): """Computes the difference between the environments.""" aenv = self.a['env'].load() benv = self.b['env'].load() akeys = frozenset(aenv) bkeys = frozenset(benv) in_both = akeys & bkeys if len(in_both) == len(akeys) == len(bkeys): keydiff = self._env_both_diff(in_both, aenv, benv) if len(keydiff) == 0: return '' in_a = in_b = '' else: keydiff = self._env_both_diff(in_both, aenv, benv) in_a = self._env_in_one_diff(akeys, bkeys, RED, self.a['sessionid'], aenv) in_b = self._env_in_one_diff(bkeys, akeys, GREEN, self.b['sessionid'], benv) s = 'Environment\n-----------\n' + in_a + keydiff + in_b return s def _cmd_in_one_diff(self, inp, i, xlj, xid, color): s = 'cmd #{i} only in {color}{xid}{no_color}:\n' s = s.format(i=i, color=color, xid=xid, no_color=NO_COLOR) lines = inp.splitlines() lt = '{color}{pre}{no_color} {line}\n' s += lt.format(color=color, no_color=NO_COLOR, line=lines[0], pre='>>>') for line in lines[1:]: s += lt.format(color=color, no_color=NO_COLOR, line=line, pre='...') if not self.verbose: return s + '\n' out = xlj['cmds'][0].get('out', 'Note: no output stored') s += out.rstrip() + '\n\n' return s def _cmd_out_and_rtn_diff(self, i, j): s = '' aout = self.a['cmds'][i].get('out', None) bout = self.b['cmds'][j].get('out', None) if aout is None and bout is None: #s += 'Note: neither output stored\n' pass elif bout is None: aid = self.a['sessionid'] s += 'Note: only {red}{aid}{no_color} output stored\n'.format(red=RED, aid=aid, no_color=NO_COLOR) elif aout is None: bid = self.b['sessionid'] s += 'Note: only {green}{bid}{no_color} output stored\n'.format(green=GREEN, bid=bid, no_color=NO_COLOR) elif aout != bout: s += 'Outputs differ\n' s += highlighted_ndiff(aout.splitlines(), bout.splitlines()) else: pass artn = self.a['cmds'][i]['rtn'] brtn = self.b['cmds'][j]['rtn'] if artn != brtn: s += ('Return vals {red}{artn}{no_color} & {green}{brtn}{no_color} differ\n' ).format(red=RED, green=GREEN, no_color=NO_COLOR, artn=artn, brtn=brtn) return s def _cmd_replace_diff(self, i, ainp, aid, j, binp, bid): s = ('cmd #{i} in {red}{aid}{no_color} is replaced by \n' 'cmd #{j} in {green}{bid}{no_color}:\n') s = s.format(i=i, aid=aid, j=j, bid=bid, red=RED, green=GREEN, no_color=NO_COLOR) s += highlighted_ndiff(ainp.splitlines(), binp.splitlines()) if not self.verbose: return s + '\n' s += self._cmd_out_and_rtn_diff(i, j) return s + '\n' def cmdsdiff(self): """Computes the difference of the commands themselves.""" aid = self.a['sessionid'] bid = self.b['sessionid'] ainps = [c['inp'] for c in self.a['cmds']] binps = [c['inp'] for c in self.b['cmds']] sm = self.sm sm.set_seqs(ainps, binps) s = '' for tag, i1, i2, j1, j2 in sm.get_opcodes(): if tag == REPLACE: zipper = itertools.zip_longest for i, ainp, j, binp in zipper(range(i1, i2), ainps[i1:i2], range(j1, j2), binps[j1:j2]): if j is None: s += self._cmd_in_one_diff(ainp, i, self.a, aid, RED) elif i is None: s += self._cmd_in_one_diff(binp, j, self.b, bid, GREEN) else: self._cmd_replace_diff(i, ainp, aid, j, binp, bid) elif tag == DELETE: for i, inp in enumerate(ainps[i1:i2], i1): s += self._cmd_in_one_diff(inp, i, self.a, aid, RED) elif tag == INSERT: for j, inp in enumerate(binps[j1:j2], j1): s += self._cmd_in_one_diff(inp, j, self.b, bid, GREEN) elif tag == EQUAL: for i, j, in zip(range(i1, i2), range(j1, j2)): odiff = self._cmd_out_and_rtn_diff(i, j) if len(odiff) > 0: h = ('cmd #{i} in {red}{aid}{no_color} input is the same as \n' 'cmd #{j} in {green}{bid}{no_color}, but output differs:\n') s += h.format(i=i, aid=aid, j=j, bid=bid, red=RED, green=GREEN, no_color=NO_COLOR) s += odiff + '\n' else: raise RuntimeError('tag not understood') if len(s) == 0: return s return 'Commands\n--------\n' + s def format(self): """Formats the difference between the two history files.""" s = self.header() ed = self.envdiff() if len(ed) > 0: s += '\n\n' + ed cd = self.cmdsdiff() if len(cd) > 0: s += '\n\n' + cd return s.rstrip()
class Replayer(object): """Replays a xonsh history file.""" def __init__(self, f, reopen=True): """ Parameters ---------- f : file handle or str Path to xonsh history file. reopen : bool, optional Whether new file handle should be opened for each load, passed directly into LazyJSON class. """ self._lj = LazyJSON(f, reopen=reopen) def __del__(self): self._lj.close() def replay(self, merge_envs=DEFAULT_MERGE_ENVS, target=None): """Replays the history specified, returns the history object where the code was executed. Parameters ---------- merge_env : tuple of str or Mappings, optional Describes how to merge the environments, in order of increasing precednce. Available strings are 'replay' and 'native'. The 'replay' env comes from the history file that we are replaying. The 'native' env comes from what this instance of xonsh was started up with. Instead of a string, a dict or other mapping may be passed in as well. Defaults to ('replay', 'native'). target : str, optional Path to new history file. """ shell = builtins.__xonsh_shell__ re_env = self._lj['env'].load() new_env = self._merge_envs(merge_envs, re_env) new_hist = History(env=new_env.detype(), locked=True, ts=[time.time(), None], gc=False, filename=target) with swap(builtins, '__xonsh_env__', new_env), \ swap(builtins, '__xonsh_history__', new_hist): for cmd in self._lj['cmds']: inp = cmd['inp'] shell.default(inp) if builtins.__xonsh_exit__: # prevent premature exit builtins.__xonsh_exit__ = False new_hist.flush(at_exit=True) return new_hist def _merge_envs(self, merge_envs, re_env): new_env = {} for e in merge_envs: if e == 'replay': new_env.update(re_env) elif e == 'native': new_env.update(builtins.__xonsh_env__) elif isinstance(e, Mapping): new_env.update(e) else: raise TypeError('Type of env not understood: {0!r}'.format(e)) new_env = Env(**new_env) return new_env
class HistoryDiffer(object): """This class helps diff two xonsh history files.""" def __init__(self, afile, bfile, reopen=False, verbose=False): """ Parameters ---------- afile : file handle or str The first file to diff bfile : file handle or str The second file to diff reopen : bool, optional Whether or not to reopen the file handles each time. The default here is opposite from the LazyJSON default because we know that we will be doing a lot of reading so it is best to keep the handles open. verbose : bool, optional Whether to print a verbose amount of information. """ self.a = LazyJSON(afile, reopen=reopen) self.b = LazyJSON(bfile, reopen=reopen) self.verbose = verbose self.sm = difflib.SequenceMatcher(autojunk=False) def __del__(self): self.a.close() self.b.close() def __str__(self): return self.format() def _header_line(self, lj): s = lj._f.name if hasattr(lj._f, 'name') else '' s += ' (' + lj['sessionid'] + ')' s += ' [locked]' if lj['locked'] else ' [unlocked]' ts = lj['ts'].load() ts0 = datetime.datetime.fromtimestamp(ts[0]) s += ' started: ' + ts0.isoformat(' ') if ts[1] is not None: ts1 = datetime.datetime.fromtimestamp(ts[1]) s += ' stopped: ' + ts1.isoformat(' ') + ' runtime: ' + str(ts1 - ts0) return s def header(self): """Computes a header string difference.""" s = ('{red}--- {aline}{no_color}\n' '{green}+++ {bline}{no_color}') s = s.format(aline=self._header_line(self.a), bline=self._header_line(self.b), red=RED_S, green=GREEN_S, no_color=NO_COLOR_S) return s def _env_both_diff(self, in_both, aenv, benv): sm = self.sm s = '' for key in sorted(in_both): aval = aenv[key] bval = benv[key] if aval == bval: continue s += '{0!r} is in both, but differs\n'.format(key) s += bold_str_diff(aval, bval, sm=sm) + '\n' return s def _env_in_one_diff(self, x, y, color, xid, xenv): only_x = sorted(x - y) if len(only_x) == 0: return '' if self.verbose: xstr = ',\n'.join([' {0!r}: {1!r}'.format(key, xenv[key]) for key in only_x]) xstr = '\n' + xstr else: xstr = ', '.join(['{0!r}'.format(key) for key in only_x]) in_x = 'These vars are only in {color}{xid}{no_color}: {{{xstr}}}\n\n' return in_x.format(xid=xid, color=color, no_color=NO_COLOR_S, xstr=xstr) def envdiff(self): """Computes the difference between the environments.""" aenv = self.a['env'].load() benv = self.b['env'].load() akeys = frozenset(aenv) bkeys = frozenset(benv) in_both = akeys & bkeys if len(in_both) == len(akeys) == len(bkeys): keydiff = self._env_both_diff(in_both, aenv, benv) if len(keydiff) == 0: return '' in_a = in_b = '' else: keydiff = self._env_both_diff(in_both, aenv, benv) in_a = self._env_in_one_diff(akeys, bkeys, RED_S, self.a['sessionid'], aenv) in_b = self._env_in_one_diff(bkeys, akeys, GREEN_S, self.b['sessionid'], benv) s = 'Environment\n-----------\n' + in_a + keydiff + in_b return s def _cmd_in_one_diff(self, inp, i, xlj, xid, color): s = 'cmd #{i} only in {color}{xid}{no_color}:\n' s = s.format(i=i, color=color, xid=xid, no_color=NO_COLOR_S) lines = inp.splitlines() lt = '{color}{pre}{no_color} {line}\n' s += lt.format(color=color, no_color=NO_COLOR_S, line=lines[0], pre='>>>') for line in lines[1:]: s += lt.format(color=color, no_color=NO_COLOR_S, line=line, pre='...') if not self.verbose: return s + '\n' out = xlj['cmds'][0].get('out', 'Note: no output stored') s += out.rstrip() + '\n\n' return s def _cmd_out_and_rtn_diff(self, i, j): s = '' aout = self.a['cmds'][i].get('out', None) bout = self.b['cmds'][j].get('out', None) if aout is None and bout is None: # s += 'Note: neither output stored\n' pass elif bout is None: aid = self.a['sessionid'] s += 'Note: only {red}{aid}{no_color} output stored\n'.format( red=RED_S, aid=aid, no_color=NO_COLOR_S) elif aout is None: bid = self.b['sessionid'] s += 'Note: only {green}{bid}{no_color} output stored\n'.format( green=GREEN_S, bid=bid, no_color=NO_COLOR_S) elif aout != bout: s += 'Outputs differ\n' s += highlighted_ndiff(aout.splitlines(), bout.splitlines()) else: pass artn = self.a['cmds'][i]['rtn'] brtn = self.b['cmds'][j]['rtn'] if artn != brtn: s += ('Return vals {red}{artn}{no_color} & {green}{brtn}{no_color} differ\n' ).format(red=RED_S, green=GREEN_S, no_color=NO_COLOR_S, artn=artn, brtn=brtn) return s def _cmd_replace_diff(self, i, ainp, aid, j, binp, bid): s = ('cmd #{i} in {red}{aid}{no_color} is replaced by \n' 'cmd #{j} in {green}{bid}{no_color}:\n') s = s.format(i=i, aid=aid, j=j, bid=bid, red=RED_S, green=GREEN_S, no_color=NO_COLOR_S) s += highlighted_ndiff(ainp.splitlines(), binp.splitlines()) if not self.verbose: return s + '\n' s += self._cmd_out_and_rtn_diff(i, j) return s + '\n' def cmdsdiff(self): """Computes the difference of the commands themselves.""" aid = self.a['sessionid'] bid = self.b['sessionid'] ainps = [c['inp'] for c in self.a['cmds']] binps = [c['inp'] for c in self.b['cmds']] sm = self.sm sm.set_seqs(ainps, binps) s = '' for tag, i1, i2, j1, j2 in sm.get_opcodes(): if tag == REPLACE_S: zipper = itertools.zip_longest for i, ainp, j, binp in zipper(range(i1, i2), ainps[i1:i2], range(j1, j2), binps[j1:j2]): if j is None: s += self._cmd_in_one_diff(ainp, i, self.a, aid, RED_S) elif i is None: s += self._cmd_in_one_diff(binp, j, self.b, bid, GREEN_S) else: self._cmd_replace_diff(i, ainp, aid, j, binp, bid) elif tag == DELETE_S: for i, inp in enumerate(ainps[i1:i2], i1): s += self._cmd_in_one_diff(inp, i, self.a, aid, RED_S) elif tag == INSERT_S: for j, inp in enumerate(binps[j1:j2], j1): s += self._cmd_in_one_diff(inp, j, self.b, bid, GREEN_S) elif tag == EQUAL_S: for i, j, in zip(range(i1, i2), range(j1, j2)): odiff = self._cmd_out_and_rtn_diff(i, j) if len(odiff) > 0: h = ('cmd #{i} in {red}{aid}{no_color} input is the same as \n' 'cmd #{j} in {green}{bid}{no_color}, but output differs:\n') s += h.format(i=i, aid=aid, j=j, bid=bid, red=RED_S, green=GREEN_S, no_color=NO_COLOR_S) s += odiff + '\n' else: raise RuntimeError('tag not understood') if len(s) == 0: return s return 'Commands\n--------\n' + s def format(self): """Formats the difference between the two history files.""" s = self.header() ed = self.envdiff() if len(ed) > 0: s += '\n\n' + ed cd = self.cmdsdiff() if len(cd) > 0: s += '\n\n' + cd return s.rstrip()
class HistoryDiffer: """This class helps diff two xonsh history files.""" def __init__(self, afile, bfile, reopen=False, verbose=False): """ Parameters ---------- afile : file handle or str The first file to diff bfile : file handle or str The second file to diff reopen : bool, optional Whether or not to reopen the file handles each time. The default here is opposite from the LazyJSON default because we know that we will be doing a lot of reading so it is best to keep the handles open. verbose : bool, optional Whether to print a verbose amount of information. """ self.a = LazyJSON(afile, reopen=reopen) self.b = LazyJSON(bfile, reopen=reopen) self.verbose = verbose self.sm = difflib.SequenceMatcher(autojunk=False) def __del__(self): self.a.close() self.b.close() def __str__(self): return self.format() def _header_line(self, lj): s = lj._f.name if hasattr(lj._f, "name") else "" s += " (" + lj["sessionid"] + ")" s += " [locked]" if lj.get("locked", False) else " [unlocked]" if lj.get("ts"): ts = lj["ts"].load() ts0 = datetime.datetime.fromtimestamp(ts[0]) s += " started: " + ts0.isoformat(" ") if ts[1] is not None: ts1 = datetime.datetime.fromtimestamp(ts[1]) s += " stopped: " + ts1.isoformat(" ") + " runtime: " + str( ts1 - ts0) return s def header(self): """Computes a header string difference.""" s = "{red}--- {aline}{reset}\n" "{green}+++ {bline}{reset}" s = s.format( aline=self._header_line(self.a), bline=self._header_line(self.b), red=COLORS.RED, green=COLORS.GREEN, reset=COLORS.RESET, ) return s def _env_both_diff(self, in_both, aenv, benv): sm = self.sm s = "" for key in sorted(in_both): aval = aenv[key] bval = benv[key] if aval == bval: continue s += f"{key!r} is in both, but differs\n" s += bold_str_diff(aval, bval, sm=sm) + "\n" return s def _env_in_one_diff(self, x, y, color, xid, xenv): only_x = sorted(x - y) if len(only_x) == 0: return "" if self.verbose: xstr = ",\n".join( [f" {key!r}: {xenv[key]!r}" for key in only_x]) xstr = "\n" + xstr else: xstr = ", ".join([f"{key!r}" for key in only_x]) in_x = "These vars are only in {color}{xid}{reset}: {{{xstr}}}\n\n" return in_x.format(xid=xid, color=color, reset=COLORS.RESET, xstr=xstr) def envdiff(self): """Computes the difference between the environments.""" if (not self.a.get("env")) or (not self.b.get("env")): return "" aenv = self.a["env"].load() benv = self.b["env"].load() akeys = frozenset(aenv) bkeys = frozenset(benv) in_both = akeys & bkeys if len(in_both) == len(akeys) == len(bkeys): keydiff = self._env_both_diff(in_both, aenv, benv) if len(keydiff) == 0: return "" in_a = in_b = "" else: keydiff = self._env_both_diff(in_both, aenv, benv) in_a = self._env_in_one_diff(akeys, bkeys, COLORS.RED, self.a["sessionid"], aenv) in_b = self._env_in_one_diff(bkeys, akeys, COLORS.GREEN, self.b["sessionid"], benv) s = "Environment\n-----------\n" + in_a + keydiff + in_b return s def _cmd_in_one_diff(self, inp, i, xlj, xid, color): s = "cmd #{i} only in {color}{xid}{reset}:\n" s = s.format(i=i, color=color, xid=xid, reset=COLORS.RESET) lines = inp.splitlines() lt = "{color}{pre}{reset} {line}\n" s += lt.format(color=color, reset=COLORS.RESET, line=lines[0], pre=">>>") for line in lines[1:]: s += lt.format(color=color, reset=COLORS.RESET, line=line, pre="...") if not self.verbose: return s + "\n" out = xlj["cmds"][0].get("out", "Note: no output stored") s += out.rstrip() + "\n\n" return s def _cmd_out_and_rtn_diff(self, i, j): s = "" aout = self.a["cmds"][i].get("out", None) bout = self.b["cmds"][j].get("out", None) if aout is None and bout is None: # s += 'Note: neither output stored\n' pass elif bout is None: aid = self.a["sessionid"] s += "Note: only {red}{aid}{reset} output stored\n".format( red=COLORS.RED, aid=aid, reset=COLORS.RESET) elif aout is None: bid = self.b["sessionid"] s += "Note: only {green}{bid}{reset} output stored\n".format( green=COLORS.GREEN, bid=bid, reset=COLORS.RESET) elif aout != bout: s += "Outputs differ\n" s += highlighted_ndiff(aout.splitlines(), bout.splitlines()) else: pass artn = self.a["cmds"][i]["rtn"] brtn = self.b["cmds"][j]["rtn"] if artn != brtn: s += ( "Return vals {red}{artn}{reset} & {green}{brtn}{reset} differ\n" ).format( red=COLORS.RED, green=COLORS.GREEN, reset=COLORS.RESET, artn=artn, brtn=brtn, ) return s def _cmd_replace_diff(self, i, ainp, aid, j, binp, bid): s = ("cmd #{i} in {red}{aid}{reset} is replaced by \n" "cmd #{j} in {green}{bid}{reset}:\n") s = s.format( i=i, aid=aid, j=j, bid=bid, red=COLORS.RED, green=COLORS.GREEN, reset=COLORS.RESET, ) s += highlighted_ndiff(ainp.splitlines(), binp.splitlines()) if not self.verbose: return s + "\n" s += self._cmd_out_and_rtn_diff(i, j) return s + "\n" def cmdsdiff(self): """Computes the difference of the commands themselves.""" aid = self.a["sessionid"] bid = self.b["sessionid"] ainps = [c["inp"] for c in self.a["cmds"]] binps = [c["inp"] for c in self.b["cmds"]] sm = self.sm sm.set_seqs(ainps, binps) s = "" for tag, i1, i2, j1, j2 in sm.get_opcodes(): if tag == REPLACE_S: zipper = itertools.zip_longest for i, ainp, j, binp in zipper(range(i1, i2), ainps[i1:i2], range(j1, j2), binps[j1:j2]): if j is None: s += self._cmd_in_one_diff(ainp, i, self.a, aid, COLORS.RED) elif i is None: s += self._cmd_in_one_diff(binp, j, self.b, bid, COLORS.GREEN) else: self._cmd_replace_diff(i, ainp, aid, j, binp, bid) elif tag == DELETE_S: for i, inp in enumerate(ainps[i1:i2], i1): s += self._cmd_in_one_diff(inp, i, self.a, aid, COLORS.RED) elif tag == INSERT_S: for j, inp in enumerate(binps[j1:j2], j1): s += self._cmd_in_one_diff(inp, j, self.b, bid, COLORS.GREEN) elif tag == EQUAL_S: for i, j in zip(range(i1, i2), range(j1, j2)): odiff = self._cmd_out_and_rtn_diff(i, j) if len(odiff) > 0: h = ( "cmd #{i} in {red}{aid}{reset} input is the same as \n" "cmd #{j} in {green}{bid}{reset}, but output differs:\n" ) s += h.format( i=i, aid=aid, j=j, bid=bid, red=COLORS.RED, green=COLORS.GREEN, reset=COLORS.RESET, ) s += odiff + "\n" else: raise RuntimeError("tag not understood") if len(s) == 0: return s return "Commands\n--------\n" + s def format(self): """Formats the difference between the two history files.""" s = self.header() ed = self.envdiff() if len(ed) > 0: s += "\n\n" + ed cd = self.cmdsdiff() if len(cd) > 0: s += "\n\n" + cd return s.rstrip()
class HistoryDiffer(object): """This class helps diff two xonsh history files.""" def __init__(self, afile, bfile, reopen=False, verbose=False): """ Parameters ---------- afile : file handle or str The first file to diff bfile : file handle or str The second file to diff reopen : bool, optional Whether or not to reopen the file handles each time. The default here is opposite from the LazyJSON default because we know that we will be doing a lot of reading so it is best to keep the handles open. verbose : bool, optional Whether to print a verbose amount of information. """ self.a = LazyJSON(afile, reopen=reopen) self.b = LazyJSON(bfile, reopen=reopen) self.verbose = verbose self.sm = difflib.SequenceMatcher(autojunk=False) def __del__(self): self.a.close() self.b.close() def __str__(self): return self.format() def _header_line(self, lj): s = lj._f.name if hasattr(lj._f, "name") else "" s += " (" + lj["sessionid"] + ")" s += " [locked]" if lj["locked"] else " [unlocked]" ts = lj["ts"].load() ts0 = datetime.datetime.fromtimestamp(ts[0]) s += " started: " + ts0.isoformat(" ") if ts[1] is not None: ts1 = datetime.datetime.fromtimestamp(ts[1]) s += " stopped: " + ts1.isoformat(" ") + " runtime: " + str(ts1 - ts0) return s def header(self): """Computes a header string difference.""" s = "{red}--- {aline}{no_color}\n" "{green}+++ {bline}{no_color}" s = s.format( aline=self._header_line(self.a), bline=self._header_line(self.b), red=RED_S, green=GREEN_S, no_color=NO_COLOR_S, ) return s def _env_both_diff(self, in_both, aenv, benv): sm = self.sm s = "" for key in sorted(in_both): aval = aenv[key] bval = benv[key] if aval == bval: continue s += "{0!r} is in both, but differs\n".format(key) s += bold_str_diff(aval, bval, sm=sm) + "\n" return s def _env_in_one_diff(self, x, y, color, xid, xenv): only_x = sorted(x - y) if len(only_x) == 0: return "" if self.verbose: xstr = ",\n".join( [" {0!r}: {1!r}".format(key, xenv[key]) for key in only_x] ) xstr = "\n" + xstr else: xstr = ", ".join(["{0!r}".format(key) for key in only_x]) in_x = "These vars are only in {color}{xid}{no_color}: {{{xstr}}}\n\n" return in_x.format(xid=xid, color=color, no_color=NO_COLOR_S, xstr=xstr) def envdiff(self): """Computes the difference between the environments.""" aenv = self.a["env"].load() benv = self.b["env"].load() akeys = frozenset(aenv) bkeys = frozenset(benv) in_both = akeys & bkeys if len(in_both) == len(akeys) == len(bkeys): keydiff = self._env_both_diff(in_both, aenv, benv) if len(keydiff) == 0: return "" in_a = in_b = "" else: keydiff = self._env_both_diff(in_both, aenv, benv) in_a = self._env_in_one_diff(akeys, bkeys, RED_S, self.a["sessionid"], aenv) in_b = self._env_in_one_diff( bkeys, akeys, GREEN_S, self.b["sessionid"], benv ) s = "Environment\n-----------\n" + in_a + keydiff + in_b return s def _cmd_in_one_diff(self, inp, i, xlj, xid, color): s = "cmd #{i} only in {color}{xid}{no_color}:\n" s = s.format(i=i, color=color, xid=xid, no_color=NO_COLOR_S) lines = inp.splitlines() lt = "{color}{pre}{no_color} {line}\n" s += lt.format(color=color, no_color=NO_COLOR_S, line=lines[0], pre=">>>") for line in lines[1:]: s += lt.format(color=color, no_color=NO_COLOR_S, line=line, pre="...") if not self.verbose: return s + "\n" out = xlj["cmds"][0].get("out", "Note: no output stored") s += out.rstrip() + "\n\n" return s def _cmd_out_and_rtn_diff(self, i, j): s = "" aout = self.a["cmds"][i].get("out", None) bout = self.b["cmds"][j].get("out", None) if aout is None and bout is None: # s += 'Note: neither output stored\n' pass elif bout is None: aid = self.a["sessionid"] s += "Note: only {red}{aid}{no_color} output stored\n".format( red=RED_S, aid=aid, no_color=NO_COLOR_S ) elif aout is None: bid = self.b["sessionid"] s += "Note: only {green}{bid}{no_color} output stored\n".format( green=GREEN_S, bid=bid, no_color=NO_COLOR_S ) elif aout != bout: s += "Outputs differ\n" s += highlighted_ndiff(aout.splitlines(), bout.splitlines()) else: pass artn = self.a["cmds"][i]["rtn"] brtn = self.b["cmds"][j]["rtn"] if artn != brtn: s += ( "Return vals {red}{artn}{no_color} & {green}{brtn}{no_color} differ\n" ).format( red=RED_S, green=GREEN_S, no_color=NO_COLOR_S, artn=artn, brtn=brtn ) return s def _cmd_replace_diff(self, i, ainp, aid, j, binp, bid): s = ( "cmd #{i} in {red}{aid}{no_color} is replaced by \n" "cmd #{j} in {green}{bid}{no_color}:\n" ) s = s.format( i=i, aid=aid, j=j, bid=bid, red=RED_S, green=GREEN_S, no_color=NO_COLOR_S ) s += highlighted_ndiff(ainp.splitlines(), binp.splitlines()) if not self.verbose: return s + "\n" s += self._cmd_out_and_rtn_diff(i, j) return s + "\n" def cmdsdiff(self): """Computes the difference of the commands themselves.""" aid = self.a["sessionid"] bid = self.b["sessionid"] ainps = [c["inp"] for c in self.a["cmds"]] binps = [c["inp"] for c in self.b["cmds"]] sm = self.sm sm.set_seqs(ainps, binps) s = "" for tag, i1, i2, j1, j2 in sm.get_opcodes(): if tag == REPLACE_S: zipper = itertools.zip_longest for i, ainp, j, binp in zipper( range(i1, i2), ainps[i1:i2], range(j1, j2), binps[j1:j2] ): if j is None: s += self._cmd_in_one_diff(ainp, i, self.a, aid, RED_S) elif i is None: s += self._cmd_in_one_diff(binp, j, self.b, bid, GREEN_S) else: self._cmd_replace_diff(i, ainp, aid, j, binp, bid) elif tag == DELETE_S: for i, inp in enumerate(ainps[i1:i2], i1): s += self._cmd_in_one_diff(inp, i, self.a, aid, RED_S) elif tag == INSERT_S: for j, inp in enumerate(binps[j1:j2], j1): s += self._cmd_in_one_diff(inp, j, self.b, bid, GREEN_S) elif tag == EQUAL_S: for i, j in zip(range(i1, i2), range(j1, j2)): odiff = self._cmd_out_and_rtn_diff(i, j) if len(odiff) > 0: h = ( "cmd #{i} in {red}{aid}{no_color} input is the same as \n" "cmd #{j} in {green}{bid}{no_color}, but output differs:\n" ) s += h.format( i=i, aid=aid, j=j, bid=bid, red=RED_S, green=GREEN_S, no_color=NO_COLOR_S, ) s += odiff + "\n" else: raise RuntimeError("tag not understood") if len(s) == 0: return s return "Commands\n--------\n" + s def format(self): """Formats the difference between the two history files.""" s = self.header() ed = self.envdiff() if len(ed) > 0: s += "\n\n" + ed cd = self.cmdsdiff() if len(cd) > 0: s += "\n\n" + cd return s.rstrip()