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
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
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
def _all_xonsh_parser(*args): """ Returns all history as found in XONSH_DATA_DIR. return format: (name, start_time, index) """ data_dir = builtins.__xonsh_env__.get('XONSH_DATA_DIR') data_dir = expanduser_abs_path(data_dir) files = [ os.path.join(data_dir, f) for f in os.listdir(data_dir) if f.startswith('xonsh-') and f.endswith('.json') ] file_hist = [] for f in files: try: json_file = LazyJSON(f, reopen=False) file_hist.append(json_file.load()['cmds']) except ValueError: # Invalid json file pass commands = [(c['inp'][:-1] if c['inp'].endswith('\n') else c['inp'], c['ts'][0]) for commands in file_hist for c in commands if c] commands.sort(key=operator.itemgetter(1)) return [(c, t, ind) for ind, (c, t) in enumerate(commands)]
def test_lazy_list_empty(): x = [] f = StringIO() ljdump(x, f) f.seek(0) lj = LazyJSON(f) assert 0 == len(lj) assert x == lj.load()
def test_lazy_list_empty(): x = [] f = StringIO() ljdump(x, f) f.seek(0) lj = LazyJSON(f) assert_equal(0, len(lj)) assert_equal(x, lj.load())
def test_lazy_list_empty(): x = [] f = StringIO() dump(x, f) f.seek(0) lj = LazyJSON(f) assert_equal(0, len(lj)) assert_equal(x, lj.load())
def test_lazy_dict(): f = StringIO() ljdump({"wakka": 42}, f) f.seek(0) lj = LazyJSON(f) assert ["wakka"] == list(lj.keys()) assert 42 == lj["wakka"] assert 1 == len(lj) assert {"wakka": 42} == lj.load()
def test_lazy_dict(): f = StringIO() dump({'wakka': 42}, f) f.seek(0) lj = LazyJSON(f) assert_equal(['wakka'], list(lj.keys())) assert_equal(42, lj['wakka']) assert_equal(1, len(lj)) assert_equal({'wakka': 42}, lj.load())
def test_lazy_list_str(): x = ["I", "have", "seen", "the", "wind", "blow"] f = StringIO() ljdump(x, f) f.seek(0) lj = LazyJSON(f) assert "the" == lj[3] assert x[:2:-2] == lj[:2:-2] assert x == [_ for _ in lj] assert x == lj.load()
def test_lazy_list_list_ints(): x = [[0, 1], [6, 28], [496, 8128]] f = StringIO() ljdump(x, f) f.seek(0) lj = LazyJSON(f) assert isinstance(lj[1], LJNode) assert 28 == lj[1][1] assert [6 == 28], lj[1].load() assert x == lj.load()
def test_lazy_list_ints(): x = [0, 1, 6, 28, 496, 8128] f = StringIO() ljdump(x, f) f.seek(0) lj = LazyJSON(f) assert 28 == lj[3] assert x[:2:-2] == lj[:2:-2] assert x == [_ for _ in lj] assert x == lj.load()
def test_lazy_list_ints(): x = [0, 1, 6, 28, 496, 8128] f = StringIO() dump(x, f) f.seek(0) lj = LazyJSON(f) assert_equal(28, lj[3]) assert_equal(x[:2:-2], lj[:2:-2]) assert_equal(x, [_ for _ in lj]) assert_equal(x, lj.load())
def test_lazy_list_str(): x = ['I', 'have', 'seen', 'the', 'wind', 'blow'] f = StringIO() ljdump(x, f) f.seek(0) lj = LazyJSON(f) assert 'the' == lj[3] assert x[:2:-2] == lj[:2:-2] assert x == [_ for _ in lj] assert x == lj.load()
def test_lazy_list_list_ints(): x = [[0, 1], [6, 28], [496, 8128]] f = StringIO() dump(x, f) f.seek(0) lj = LazyJSON(f) assert_is_instance(lj[1], Node) assert_equal(28, lj[1][1]) assert_equal([6, 28], lj[1].load()) assert_equal(x, lj.load())
def test_lazy_list_str(): x = ['I', 'have', 'seen', 'the', 'wind', 'blow'] f = StringIO() dump(x, f) f.seek(0) lj = LazyJSON(f) assert_equal('the', lj[3]) assert_equal(x[:2:-2], lj[:2:-2]) assert_equal(x, [_ for _ in lj]) assert_equal(x, lj.load())
def test_lazy_dict_dict_int(): x = {"wakka": {"jawaka": 42}} f = StringIO() ljdump(x, f) f.seek(0) lj = LazyJSON(f) assert ["wakka"] == list(lj.keys()) assert isinstance(lj["wakka"], LJNode) assert 42 == lj["wakka"]["jawaka"] assert 1 == len(lj) assert x == lj.load()
def test_lazy_dict_dict_int(): x = {'wakka': {'jawaka': 42}} f = StringIO() dump(x, f) f.seek(0) lj = LazyJSON(f) assert_equal(['wakka'], list(lj.keys())) assert_is_instance(lj['wakka'], Node) assert_equal(42, lj['wakka']['jawaka']) assert_equal(1, len(lj)) assert_equal(x, lj.load())
def test_lazy_dict_dict_int(): x = {'wakka': {'jawaka': 42}} f = StringIO() ljdump(x, f) f.seek(0) lj = LazyJSON(f) assert ['wakka'] == list(lj.keys()) assert isinstance(lj['wakka'], LJNode) assert 42 == lj['wakka']['jawaka'] assert 1 == len(lj) assert x == lj.load()
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 __getitem__(self, key): size = len(self) if isinstance(key, slice): return [self[i] for i in range(*key.indices(size))] elif not isinstance(key, int): raise IndexError( 'CommandField may only be indexed by int or slice.') elif size == 0: raise IndexError('CommandField is empty.') # now we know we have an int key = size + key if key < 0 else key # ensure key is non-negative bufsize = len(self.hist.buffer) if size - bufsize <= key: # key is in buffer return self.hist.buffer[key + bufsize - size].get( self.field, self.default) # now we know we have to go into the file queue = self.hist._queue queue.append(self) with self.hist._cond: self.hist._cond.wait_for(self.i_am_at_the_front) with open(self.hist.filename, 'r', newline='\n') as f: lj = LazyJSON(f, reopen=False) rtn = lj['cmds'][key].get(self.field, self.default) if isinstance(rtn, LJNode): rtn = rtn.load() queue.popleft() return rtn
def test_hist_flush_with_store_cwd(hist, xession): hf = hist.flush() assert hf is None hist.save_cwd = True hist.append({ "inp": "# saving with cwd", "rtn": 0, "out": "yes", "cwd": "/tmp" }) hf = hist.flush() assert hf is not None hist.save_cwd = False hist.append({ "inp": "# saving without cwd", "rtn": 0, "out": "yes", "cwd": "/tmp" }) hf = hist.flush() assert hf is not None while hf.is_alive(): pass with LazyJSON(hist.filename) as lj: assert len(lj["cmds"]) == 2 assert lj["cmds"][0]["cwd"] == "/tmp" assert "cwd" not in lj["cmds"][1]
def test_lazy_load_index(): f = StringIO() ljdump({"wakka": 42}, f) f.seek(0) lj = LazyJSON(f) assert {"wakka": 10, "__total__": 0} == lj.offsets assert {"wakka": 2, "__total__": 14} == lj.sizes
def test_lazy_load_index(): f = StringIO() ljdump({'wakka': 42}, f) f.seek(0) lj = LazyJSON(f) assert {'wakka': 10, '__total__': 0} == lj.offsets assert {'wakka': 2, '__total__': 14} == lj.sizes
def _all_xonsh_parser(**kwargs): """ Returns all history as found in XONSH_DATA_DIR. return format: (cmd, start_time, index) """ ind = 0 for f in _get_history_files(): try: json_file = LazyJSON(f, reopen=False) except ValueError: # Invalid json file continue commands = json_file.load()['cmds'] for c in commands: yield (c['inp'].rstrip(), c['ts'][0], ind) ind += 1
def test_hist_init(): FNAME = 'xonsh-SESSIONID.json' FNAME += '.init' hist = History(filename=FNAME, here='yup', **HIST_TEST_KWARGS) with LazyJSON(FNAME) as lj: obs = lj['here'] assert_equal('yup', obs) os.remove(FNAME)
def test_hist_init(): """Test initialization of the shell history.""" FNAME = 'xonsh-SESSIONID.json' FNAME += '.init' History(filename=FNAME, here='yup', **HIST_TEST_KWARGS) with LazyJSON(FNAME) as lj: obs = lj['here'] assert_equal('yup', obs) os.remove(FNAME)
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 dump(self): """Write the cached history to external storage.""" with open(self.filename, 'r', newline='\n') as f: hist = LazyJSON(f).load() hist['cmds'].extend(self.buffer) if self.at_exit: hist['ts'][1] = time.time() # apply end time hist['locked'] = False with open(self.filename, 'w', newline='\n') as f: ljdump(hist, f, sort_keys=True)
def _all_xonsh_parser(**kwargs): """ Returns all history as found in XONSH_DATA_DIR. return format: (cmd, start_time, index) """ data_dir = builtins.__xonsh_env__.get('XONSH_DATA_DIR') data_dir = expanduser_abs_path(data_dir) files = [os.path.join(data_dir, f) for f in os.listdir(data_dir) if f.startswith('xonsh-') and f.endswith('.json')] ind = 0 for f in files: try: json_file = LazyJSON(f, reopen=False) except ValueError: # Invalid json file pass commands = json_file.load()['cmds'] for c in commands: yield (c['inp'].rstrip(), c['ts'][0], ind) ind += 1
def test_hist_flush(hist, xonsh_builtins): """Verify explicit flushing of the history works.""" hf = hist.flush() assert hf is None xonsh_builtins.__xonsh_env__['HISTCONTROL'] = set() hist.append({'joco': 'still alive'}) hf = hist.flush() assert hf is not None while hf.is_alive(): pass with LazyJSON(hist.filename) as lj: obs = lj['cmds'][0]['joco'] assert 'still alive' == obs
def _all_xonsh_parser(**kwargs): """ Returns all history as found in XONSH_DATA_DIR. return format: (name, start_time, index) """ data_dir = builtins.__xonsh_env__.get('XONSH_DATA_DIR') data_dir = expanduser_abs_path(data_dir) files = [os.path.join(data_dir, f) for f in os.listdir(data_dir) if f.startswith('xonsh-') and f.endswith('.json')] file_hist = [] for f in files: try: json_file = LazyJSON(f, reopen=False) file_hist.append(json_file.load()['cmds']) except ValueError: # Invalid json file pass commands = [(c['inp'][:-1] if c['inp'].endswith('\n') else c['inp'], c['ts'][0]) for commands in file_hist for c in commands if c] commands.sort(key=operator.itemgetter(1)) return [(c, t, ind) for ind, (c, t) in enumerate(commands)]
def test_lazy_str(): f = StringIO() ljdump('wakka', f) f.seek(0) lj = LazyJSON(f) assert 'wakka' == lj.load()
def test_lazy_str(): f = StringIO() dump('wakka', f) f.seek(0) lj = LazyJSON(f) assert_equal('wakka', lj.load())
def test_lazy_str(): f = StringIO() ljdump("wakka", f) f.seek(0) lj = LazyJSON(f) assert "wakka" == lj.load()
def test_lazy_int(): f = StringIO() dump(42, f) f.seek(0) lj = LazyJSON(f) assert_equal(42, lj.load())
def test_lazy_int(): f = StringIO() ljdump(42, f) f.seek(0) lj = LazyJSON(f) assert 42 == lj.load()
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(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()