From c6736ccf16f5f4b55795537722f7a81c201a61dc Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Mon, 14 Sep 2020 17:22:10 +0100 Subject: [PATCH 01/46] Add REMEMBER-HISTORY environment variable --- xonsh/environ.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/xonsh/environ.py b/xonsh/environ.py index 7fb36a01d..1db4ed081 100644 --- a/xonsh/environ.py +++ b/xonsh/environ.py @@ -1615,6 +1615,15 @@ def DEFAULT_VARS(): "xonsh process threads sleep for while running command pipelines. " "The value has units of seconds [s].", ), + "XONSH_REMEMBER_HISTORY": Var( + is_bool, + to_bool, + bool_to_str, + True, + "If False, Xonsh will not save history." + "History for the session will be deleted if it exists", + doc_configurable=True + ), "XONSH_SHOW_TRACEBACK": Var( is_bool, to_bool, From 0bf1473498509d0e4008ceea10fe99448e0ec751 Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Mon, 14 Sep 2020 19:27:02 +0100 Subject: [PATCH 02/46] JSON backend, stop history dump if REMEMBER_HISTORY False --- xonsh/history/base.py | 11 +++++++++++ xonsh/history/json.py | 1 + 2 files changed, 12 insertions(+) diff --git a/xonsh/history/base.py b/xonsh/history/base.py index e8816f3d1..3ae952a67 100644 --- a/xonsh/history/base.py +++ b/xonsh/history/base.py @@ -2,6 +2,7 @@ """Base class of Xonsh History backends.""" import types import uuid +import builtins class HistoryEntry(types.SimpleNamespace): @@ -153,3 +154,13 @@ class History: If set blocking, then wait until gc action finished. """ pass + + @staticmethod + def remember_history_check(f): + def new_f(self, at_exit=False): + if builtins.__xonsh__.env["XONSH_REMEMBER_HISTORY"]: + return f(self, at_exit=at_exit) + else: + return + + return new_f diff --git a/xonsh/history/json.py b/xonsh/history/json.py index 6ccef96e5..c86b8a4bd 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -415,6 +415,7 @@ class JsonHistory(History): hf = None return hf + @History.remember_history_check def flush(self, at_exit=False): """Flushes the current command buffer to disk. From feba0337ce833cd508be0f34d203bb78bbbe20b0 Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Mon, 14 Sep 2020 21:25:10 +0100 Subject: [PATCH 03/46] Start again; for control bit use attribute rather than environment variable. --- xonsh/environ.py | 9 --------- xonsh/history/base.py | 6 +++--- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/xonsh/environ.py b/xonsh/environ.py index 1db4ed081..7fb36a01d 100644 --- a/xonsh/environ.py +++ b/xonsh/environ.py @@ -1615,15 +1615,6 @@ def DEFAULT_VARS(): "xonsh process threads sleep for while running command pipelines. " "The value has units of seconds [s].", ), - "XONSH_REMEMBER_HISTORY": Var( - is_bool, - to_bool, - bool_to_str, - True, - "If False, Xonsh will not save history." - "History for the session will be deleted if it exists", - doc_configurable=True - ), "XONSH_SHOW_TRACEBACK": Var( is_bool, to_bool, diff --git a/xonsh/history/base.py b/xonsh/history/base.py index 3ae952a67..bfa11f83d 100644 --- a/xonsh/history/base.py +++ b/xonsh/history/base.py @@ -2,7 +2,6 @@ """Base class of Xonsh History backends.""" import types import uuid -import builtins class HistoryEntry(types.SimpleNamespace): @@ -51,7 +50,7 @@ class History: is the newest. """ - def __init__(self, sessionid=None, **kwargs): + def __init__(self, sessionid=None, remember_history=None, **kwargs): """Represents a xonsh session's history. Parameters @@ -72,6 +71,7 @@ class History: self.last_cmd_out = None self.hist_size = None self.hist_units = None + self.remember_history = remember_history def __len__(self): """Return the number of items in current session.""" @@ -158,7 +158,7 @@ class History: @staticmethod def remember_history_check(f): def new_f(self, at_exit=False): - if builtins.__xonsh__.env["XONSH_REMEMBER_HISTORY"]: + if self.remember_history: return f(self, at_exit=at_exit) else: return From 8d09381af544cf4ee7afb279953114a63ec27ee3 Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Mon, 14 Sep 2020 22:00:57 +0100 Subject: [PATCH 04/46] Basic API specified --- xonsh/history/base.py | 17 +++++++++++++++-- xonsh/history/json.py | 1 + xonsh/history/main.py | 14 ++++++++++++++ xonsh/history/sqlite.py | 3 ++- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/xonsh/history/base.py b/xonsh/history/base.py index bfa11f83d..2dc39b677 100644 --- a/xonsh/history/base.py +++ b/xonsh/history/base.py @@ -50,7 +50,7 @@ class History: is the newest. """ - def __init__(self, sessionid=None, remember_history=None, **kwargs): + def __init__(self, sessionid=None, **kwargs): """Represents a xonsh session's history. Parameters @@ -71,7 +71,7 @@ class History: self.last_cmd_out = None self.hist_size = None self.hist_units = None - self.remember_history = remember_history + self.remember_history = True def __len__(self): """Return the number of items in current session.""" @@ -164,3 +164,16 @@ class History: return return new_f + + def clear(self): + """Wipes the current session from both the disk and memory.""" + self.wipe_disk() + self.wipe_memory() + + def wipe_disk(self): + """Wipes the current session's history from the disk.""" + pass + + def wipe_memory(self): + """Reinitialises History object with blank commands.""" + pass diff --git a/xonsh/history/json.py b/xonsh/history/json.py index c86b8a4bd..169be425c 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -391,6 +391,7 @@ class JsonHistory(History): def __len__(self): return self._len - self._skipped + @History.remember_history_check def append(self, cmd): """Appends command to history. Will periodically flush the history to file. diff --git a/xonsh/history/main.py b/xonsh/history/main.py index e572822e0..f7121f3d9 100644 --- a/xonsh/history/main.py +++ b/xonsh/history/main.py @@ -354,6 +354,13 @@ def _xh_create_parser(): # 'flush' subcommand subp.add_parser("flush", help="flush the current history to disk") + subp.add_parser("off", help="history will not be saved for this session") + + subp.add_parser("on", help="history will be saved for the rest of this" + " session (default)") + + subp.add_parser("clear", help="one-time wipe of session history") + return p @@ -417,5 +424,12 @@ def history_main( hf = hist.flush() if isinstance(hf, threading.Thread): hf.join() + elif ns.action == "off": + hist.remember_history = False + hist.clear() + elif ns.action == "on": + hist.remember_history = True + elif ns.action == "clear": + hist.clear() else: print("Unknown history action {}".format(ns.action), file=sys.stderr) diff --git a/xonsh/history/sqlite.py b/xonsh/history/sqlite.py index 130d08dd9..bf24c420b 100644 --- a/xonsh/history/sqlite.py +++ b/xonsh/history/sqlite.py @@ -221,7 +221,7 @@ class SqliteHistoryGC(threading.Thread): xh_sqlite_delete_items(hsize, filename=self.filename) -class SqliteHistory(History): +class SqliteHistory(History): # todo work out how to block flush on sqlite. """Xonsh history backend implemented with sqlite3.""" def __init__(self, gc=True, filename=None, **kwargs): @@ -239,6 +239,7 @@ class SqliteHistory(History): # during init rerun create command setattr(XH_SQLITE_CACHE, XH_SQLITE_CREATED_SQL_TBL, False) + @History.remember_history_check def append(self, cmd): envs = builtins.__xonsh__.env inp = cmd["inp"].rstrip() From 0e3b34d0cdb4107e39784e943209e51ba5d3dcc8 Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Mon, 14 Sep 2020 23:40:20 +0100 Subject: [PATCH 05/46] Half done, for JSON --- xonsh/history/base.py | 11 +++++++++-- xonsh/history/json.py | 35 +++++++++++++++++++++++++++-------- xonsh/history/main.py | 16 ++++++++++++---- 3 files changed, 48 insertions(+), 14 deletions(-) diff --git a/xonsh/history/base.py b/xonsh/history/base.py index 2dc39b677..94b14c4ae 100644 --- a/xonsh/history/base.py +++ b/xonsh/history/base.py @@ -157,9 +157,12 @@ class History: @staticmethod def remember_history_check(f): - def new_f(self, at_exit=False): + """Allows the decorated function to run only if + self.remember_history is True. + """ + def new_f(self, *args, **kwargs): if self.remember_history: - return f(self, at_exit=at_exit) + return f(self, *args, **kwargs) else: return @@ -177,3 +180,7 @@ class History: def wipe_memory(self): """Reinitialises History object with blank commands.""" pass + + def remake_file(self): + """Makes new file if required, after old one gets deleted.""" + pass diff --git a/xonsh/history/json.py b/xonsh/history/json.py index 169be425c..050b25fee 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -318,14 +318,17 @@ class JsonCommandField(cabc.Sequence): # 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 = xlj.LazyJSON(f, reopen=False) - rtn = lj["cmds"][key].get(self.field, self.default) - if isinstance(rtn, xlj.LJNode): - rtn = rtn.load() - queue.popleft() + if self.hist.remember_history: # todo make sure this works after clear. Reinitialise file. + 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 = xlj.LazyJSON(f, reopen=False) + rtn = lj["cmds"][key].get(self.field, self.default) + if isinstance(rtn, xlj.LJNode): + rtn = rtn.load() + queue.popleft() + else: + return "" return rtn def i_am_at_the_front(self): @@ -504,3 +507,19 @@ class JsonHistory(History): if blocking: while self.gc.is_alive(): # while waiting for gc. time.sleep(0.1) # don't monopolize the thread (or Python GIL?) + + def wipe_disk(self): + try: + os.remove(self.filename) + except FileNotFoundError: + pass + + def wipe_memory(self): # todo is this enough? + self.buffer = [] + + def remake_file(self): + meta = dict() + meta["cmds"] = [] + meta["sessionid"] = str(self.sessionid) + with open(self.filename, "w", newline="\n") as f: + xlj.ljdump(meta, f, sort_keys=True) diff --git a/xonsh/history/main.py b/xonsh/history/main.py index f7121f3d9..f99e10291 100644 --- a/xonsh/history/main.py +++ b/xonsh/history/main.py @@ -224,7 +224,8 @@ def _XH_HISTORY_SESSIONS(): } -_XH_MAIN_ACTIONS = {"show", "id", "file", "info", "diff", "gc", "flush"} +_XH_MAIN_ACTIONS = {"show", "id", "file", "info", "diff", "gc", "flush", + "off", "on", "clear"} @functools.lru_cache() @@ -354,11 +355,14 @@ def _xh_create_parser(): # 'flush' subcommand subp.add_parser("flush", help="flush the current history to disk") + # 'off' subcommand subp.add_parser("off", help="history will not be saved for this session") + # 'on' subcommand subp.add_parser("on", help="history will be saved for the rest of this" " session (default)") + # 'clear' subcommand subp.add_parser("clear", help="one-time wipe of session history") return p @@ -425,11 +429,15 @@ def history_main( if isinstance(hf, threading.Thread): hf.join() elif ns.action == "off": - hist.remember_history = False - hist.clear() + if hist.remember_history: + hist.remember_history = False + hist.clear() elif ns.action == "on": - hist.remember_history = True + if not hist.remember_history: + hist.remember_history = True + hist.remake_file() elif ns.action == "clear": hist.clear() + hist.remake_file() else: print("Unknown history action {}".format(ns.action), file=sys.stderr) From cf5067dca8ccce827a34fa8556723d5bcc3ac09f Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Mon, 14 Sep 2020 23:51:52 +0100 Subject: [PATCH 06/46] Simplify API. --- xonsh/history/base.py | 9 --------- xonsh/history/json.py | 20 ++++++++++++-------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/xonsh/history/base.py b/xonsh/history/base.py index 94b14c4ae..c0ed87655 100644 --- a/xonsh/history/base.py +++ b/xonsh/history/base.py @@ -170,15 +170,6 @@ class History: def clear(self): """Wipes the current session from both the disk and memory.""" - self.wipe_disk() - self.wipe_memory() - - def wipe_disk(self): - """Wipes the current session's history from the disk.""" - pass - - def wipe_memory(self): - """Reinitialises History object with blank commands.""" pass def remake_file(self): diff --git a/xonsh/history/json.py b/xonsh/history/json.py index 050b25fee..cdec97ee9 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -327,9 +327,9 @@ class JsonCommandField(cabc.Sequence): if isinstance(rtn, xlj.LJNode): rtn = rtn.load() queue.popleft() + return rtn else: return "" - return rtn def i_am_at_the_front(self): """Tests if the command field is at the front of the queue.""" @@ -508,14 +508,18 @@ class JsonHistory(History): while self.gc.is_alive(): # while waiting for gc. time.sleep(0.1) # don't monopolize the thread (or Python GIL?) - def wipe_disk(self): - try: - os.remove(self.filename) - except FileNotFoundError: - pass + def clear(self): + def wipe_disk(hist): + try: + os.remove(hist.filename) + except FileNotFoundError: + pass - def wipe_memory(self): # todo is this enough? - self.buffer = [] + def wipe_memory(hist): # todo is this enough? + hist.buffer = [] + + wipe_disk(self) + wipe_memory(self) def remake_file(self): meta = dict() From 3f6d5ee793a2be2166be14c1daa096aa9ffe19dd Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Tue, 15 Sep 2020 00:39:10 +0100 Subject: [PATCH 07/46] Misc changes. --- xonsh/history/base.py | 4 ---- xonsh/history/json.py | 20 +++++++++----------- xonsh/history/sqlite.py | 3 +++ 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/xonsh/history/base.py b/xonsh/history/base.py index c0ed87655..f4c31eec3 100644 --- a/xonsh/history/base.py +++ b/xonsh/history/base.py @@ -171,7 +171,3 @@ class History: def clear(self): """Wipes the current session from both the disk and memory.""" pass - - def remake_file(self): - """Makes new file if required, after old one gets deleted.""" - pass diff --git a/xonsh/history/json.py b/xonsh/history/json.py index cdec97ee9..01cced8cc 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -269,6 +269,9 @@ class JsonHistoryFlusher(threading.Thread): load_hist_len = len(hist["cmds"]) hist["cmds"].extend(cmds) if self.at_exit: + #print(hist["ts"]) + print(hist.__class__) + print(hist) hist["ts"][1] = time.time() # apply end time hist["locked"] = False if not builtins.__xonsh__.env.get("XONSH_STORE_STDOUT", False): @@ -510,20 +513,15 @@ class JsonHistory(History): def clear(self): def wipe_disk(hist): - try: - os.remove(hist.filename) - except FileNotFoundError: - pass + with open(self.filename, "r") as f: + backup_metadata = json.load(f) + backup_metadata["data"]["cmds"] = [] + print(backup_metadata) + with open(self.filename, "w") as f: + json.dump(backup_metadata, f) def wipe_memory(hist): # todo is this enough? hist.buffer = [] wipe_disk(self) wipe_memory(self) - - def remake_file(self): - meta = dict() - meta["cmds"] = [] - meta["sessionid"] = str(self.sessionid) - with open(self.filename, "w", newline="\n") as f: - xlj.ljdump(meta, f, sort_keys=True) diff --git a/xonsh/history/sqlite.py b/xonsh/history/sqlite.py index bf24c420b..efec43c23 100644 --- a/xonsh/history/sqlite.py +++ b/xonsh/history/sqlite.py @@ -297,3 +297,6 @@ class SqliteHistory(History): # todo work out how to block flush on sqlite. if blocking: while self.gc.is_alive(): continue + + def clear(self): + pass # todo implement From 9a212b834d01f71b554803b5e59f53ee08029b90 Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Tue, 15 Sep 2020 15:49:42 +0100 Subject: [PATCH 08/46] JSON done. --- xonsh/history/base.py | 7 ++++++- xonsh/history/json.py | 18 +++++++----------- xonsh/history/main.py | 4 +--- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/xonsh/history/base.py b/xonsh/history/base.py index f4c31eec3..f0ae9f35f 100644 --- a/xonsh/history/base.py +++ b/xonsh/history/base.py @@ -170,4 +170,9 @@ class History: def clear(self): """Wipes the current session from both the disk and memory.""" - pass + self.buffer = None + self.inps = None + self.rtns = None + self.tss = None + self.outs = None + diff --git a/xonsh/history/json.py b/xonsh/history/json.py index 01cced8cc..1c13c77c2 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -269,9 +269,6 @@ class JsonHistoryFlusher(threading.Thread): load_hist_len = len(hist["cmds"]) hist["cmds"].extend(cmds) if self.at_exit: - #print(hist["ts"]) - print(hist.__class__) - print(hist) hist["ts"][1] = time.time() # apply end time hist["locked"] = False if not builtins.__xonsh__.env.get("XONSH_STORE_STDOUT", False): @@ -512,16 +509,15 @@ class JsonHistory(History): time.sleep(0.1) # don't monopolize the thread (or Python GIL?) def clear(self): - def wipe_disk(hist): - with open(self.filename, "r") as f: - backup_metadata = json.load(f) - backup_metadata["data"]["cmds"] = [] - print(backup_metadata) - with open(self.filename, "w") as f: - json.dump(backup_metadata, f) def wipe_memory(hist): # todo is this enough? hist.buffer = [] + self.tss = JsonCommandField("ts", self) + self.inps = JsonCommandField("inp", self) + self.outs = JsonCommandField("out", self) + self.rtns = JsonCommandField("rtn", self) - wipe_disk(self) wipe_memory(self) + + # Flush empty history object to disk. This overwrites the old commands. + self.flush() diff --git a/xonsh/history/main.py b/xonsh/history/main.py index f99e10291..dc717710a 100644 --- a/xonsh/history/main.py +++ b/xonsh/history/main.py @@ -430,14 +430,12 @@ def history_main( hf.join() elif ns.action == "off": if hist.remember_history: - hist.remember_history = False hist.clear() + hist.remember_history = False elif ns.action == "on": if not hist.remember_history: hist.remember_history = True - hist.remake_file() elif ns.action == "clear": hist.clear() - hist.remake_file() else: print("Unknown history action {}".format(ns.action), file=sys.stderr) From d8688f9db83d40975ca39d34bf8c1908b74158d2 Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Tue, 15 Sep 2020 16:15:29 +0100 Subject: [PATCH 09/46] SQLite done. --- xonsh/history/base.py | 7 +------ xonsh/history/json.py | 2 +- xonsh/history/sqlite.py | 10 ++++++++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/xonsh/history/base.py b/xonsh/history/base.py index f0ae9f35f..f4c31eec3 100644 --- a/xonsh/history/base.py +++ b/xonsh/history/base.py @@ -170,9 +170,4 @@ class History: def clear(self): """Wipes the current session from both the disk and memory.""" - self.buffer = None - self.inps = None - self.rtns = None - self.tss = None - self.outs = None - + pass diff --git a/xonsh/history/json.py b/xonsh/history/json.py index 1c13c77c2..6f1e60389 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -318,7 +318,7 @@ class JsonCommandField(cabc.Sequence): # now we know we have to go into the file queue = self.hist._queue queue.append(self) - if self.hist.remember_history: # todo make sure this works after clear. Reinitialise file. + if self.hist.remember_history: # todo is this necessary and elegant? 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: diff --git a/xonsh/history/sqlite.py b/xonsh/history/sqlite.py index efec43c23..398f05233 100644 --- a/xonsh/history/sqlite.py +++ b/xonsh/history/sqlite.py @@ -221,7 +221,7 @@ class SqliteHistoryGC(threading.Thread): xh_sqlite_delete_items(hsize, filename=self.filename) -class SqliteHistory(History): # todo work out how to block flush on sqlite. +class SqliteHistory(History): """Xonsh history backend implemented with sqlite3.""" def __init__(self, gc=True, filename=None, **kwargs): @@ -299,4 +299,10 @@ class SqliteHistory(History): # todo work out how to block flush on sqlite. continue def clear(self): - pass # todo implement + self.inps = [] + self.rtns = [] + self.outs = [] + self.tss = [] + + # Wipe data from disk. + xh_sqlite_delete_items(size_to_keep=0, filename=self.filename) From 2ada0badbbb351c8d2921bfc7110cb1bd8c6933d Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Thu, 17 Sep 2020 00:26:19 +0100 Subject: [PATCH 10/46] Add tests, other misc changes. --- tests/test_history.py | 62 ++++++++++++++++++++++++++++++++++ tests/test_history_sqlite.py | 64 ++++++++++++++++++++++++++++++++++++ xonsh/history/json.py | 7 ++-- xonsh/history/main.py | 3 ++ 4 files changed, 134 insertions(+), 2 deletions(-) diff --git a/tests/test_history.py b/tests/test_history.py index fe8356a77..8005ff259 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -483,3 +483,65 @@ def test__xhj_gc_xx_to_rmfiles( assert minute_diff <= 60 else: assert act_size == exp_size + + +def test_hist_clear_cmd(hist, xonsh_builtins, capsys, tmpdir): + """Verify that the CLI history clear command works.""" + xonsh_builtins.__xonsh__.env.update({"XONSH_DATA_DIR": str(tmpdir)}) + xonsh_builtins.__xonsh__.history = hist + xonsh_builtins.__xonsh__.env["HISTCONTROL"] = set() + + for ts, cmd in enumerate(CMDS): # populate the shell history + hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) + assert len(xonsh_builtins.__xonsh__.history) == 6 + + history_main(["clear"]) + + out, err = capsys.readouterr() + assert err.rstrip() == "History cleared" + assert len(xonsh_builtins.__xonsh__.history) == 0 + + +def test_hist_off_cmd(hist, xonsh_builtins, capsys, tmpdir): + """Verify that the CLI history off command works.""" + xonsh_builtins.__xonsh__.env.update({"XONSH_DATA_DIR": str(tmpdir)}) + xonsh_builtins.__xonsh__.history = hist + xonsh_builtins.__xonsh__.env["HISTCONTROL"] = set() + + for ts, cmd in enumerate(CMDS): # populate the shell history + hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) + assert len(xonsh_builtins.__xonsh__.history) == 6 + + history_main(["off"]) + + out, err = capsys.readouterr() + assert err.rstrip() == "History off" + assert len(xonsh_builtins.__xonsh__.history) == 0 + + for ts, cmd in enumerate(CMDS): # attempt to populate the shell history + hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) + + assert len(xonsh_builtins.__xonsh__.history) == 0 + + +def test_hist_on_cmd(hist, xonsh_builtins, capsys, tmpdir): + """Verify that the CLI history on command works.""" + xonsh_builtins.__xonsh__.env.update({"XONSH_DATA_DIR": str(tmpdir)}) + xonsh_builtins.__xonsh__.history = hist + xonsh_builtins.__xonsh__.env["HISTCONTROL"] = set() + + for ts, cmd in enumerate(CMDS): # populate the shell history + hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) + assert len(xonsh_builtins.__xonsh__.history) == 6 + + history_main(["off"]) + history_main(["on"]) + + out, err = capsys.readouterr() + assert err.rstrip().endswith("History on") + assert len(xonsh_builtins.__xonsh__.history) == 0 + + for ts, cmd in enumerate(CMDS): # attempt to populate the shell history + hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) + + assert len(xonsh_builtins.__xonsh__.history) == 6 diff --git a/tests/test_history_sqlite.py b/tests/test_history_sqlite.py index 762f36132..dc1d6e136 100644 --- a/tests/test_history_sqlite.py +++ b/tests/test_history_sqlite.py @@ -215,3 +215,67 @@ def test_history_getitem(index, exp, hist, xonsh_builtins): assert [(e.cmd, e.out, e.rtn, e.ts) for e in entry] == exp else: assert (entry.cmd, entry.out, entry.rtn, entry.ts) == exp + + + + +def test_hist_clear_cmd(hist, xonsh_builtins, capsys, tmpdir): + """Verify that the CLI history clear command works.""" + xonsh_builtins.__xonsh__.env.update({"XONSH_DATA_DIR": str(tmpdir)}) + xonsh_builtins.__xonsh__.history = hist + xonsh_builtins.__xonsh__.env["HISTCONTROL"] = set() + + for ts, cmd in enumerate(CMDS): # populate the shell history + hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) + assert len(xonsh_builtins.__xonsh__.history) == 6 + + history_main(["clear"]) + + out, err = capsys.readouterr() + assert err.rstrip() == "History cleared" + assert len(xonsh_builtins.__xonsh__.history) == 0 + + +def test_hist_off_cmd(hist, xonsh_builtins, capsys, tmpdir): + """Verify that the CLI history off command works.""" + xonsh_builtins.__xonsh__.env.update({"XONSH_DATA_DIR": str(tmpdir)}) + xonsh_builtins.__xonsh__.history = hist + xonsh_builtins.__xonsh__.env["HISTCONTROL"] = set() + + for ts, cmd in enumerate(CMDS): # populate the shell history + hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) + assert len(xonsh_builtins.__xonsh__.history) == 6 + + history_main(["off"]) + + out, err = capsys.readouterr() + assert err.rstrip() == "History off" + assert len(xonsh_builtins.__xonsh__.history) == 0 + + for ts, cmd in enumerate(CMDS): # attempt to populate the shell history + hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) + + assert len(xonsh_builtins.__xonsh__.history) == 0 + + +def test_hist_on_cmd(hist, xonsh_builtins, capsys, tmpdir): + """Verify that the CLI history on command works.""" + xonsh_builtins.__xonsh__.env.update({"XONSH_DATA_DIR": str(tmpdir)}) + xonsh_builtins.__xonsh__.history = hist + xonsh_builtins.__xonsh__.env["HISTCONTROL"] = set() + + for ts, cmd in enumerate(CMDS): # populate the shell history + hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) + assert len(xonsh_builtins.__xonsh__.history) == 6 + + history_main(["off"]) + history_main(["on"]) + + out, err = capsys.readouterr() + assert err.rstrip().endswith("History on") + assert len(xonsh_builtins.__xonsh__.history) == 0 + + for ts, cmd in enumerate(CMDS): # attempt to populate the shell history + hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) + + assert len(xonsh_builtins.__xonsh__.history) == 6 diff --git a/xonsh/history/json.py b/xonsh/history/json.py index 6f1e60389..c493b8535 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -510,14 +510,17 @@ class JsonHistory(History): def clear(self): - def wipe_memory(hist): # todo is this enough? + def wipe_memory(hist): hist.buffer = [] self.tss = JsonCommandField("ts", self) self.inps = JsonCommandField("inp", self) self.outs = JsonCommandField("out", self) self.rtns = JsonCommandField("rtn", self) + self._len = 0 + self._skipped = 0 wipe_memory(self) - # Flush empty history object to disk. This overwrites the old commands. + # Flush empty history object to disk. This overwrites the old commands, + # but keeps basic session metadata to prevent things from breaking. self.flush() diff --git a/xonsh/history/main.py b/xonsh/history/main.py index dc717710a..6a3468561 100644 --- a/xonsh/history/main.py +++ b/xonsh/history/main.py @@ -432,10 +432,13 @@ def history_main( if hist.remember_history: hist.clear() hist.remember_history = False + print("History off", file=sys.stderr) elif ns.action == "on": if not hist.remember_history: hist.remember_history = True + print("History on", file=sys.stderr) elif ns.action == "clear": hist.clear() + print("History cleared", file=sys.stderr) else: print("Unknown history action {}".format(ns.action), file=sys.stderr) From 5e0228aa09972384a73f42cba5add1ebaf1f9eb5 Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Thu, 17 Sep 2020 01:25:56 +0100 Subject: [PATCH 11/46] Tests succeeded. --- tests/test_history_sqlite.py | 12 +++++------- xonsh/history/json.py | 24 ++++++++++++------------ xonsh/history/sqlite.py | 5 ++++- 3 files changed, 21 insertions(+), 20 deletions(-) diff --git a/tests/test_history_sqlite.py b/tests/test_history_sqlite.py index dc1d6e136..0d94e1040 100644 --- a/tests/test_history_sqlite.py +++ b/tests/test_history_sqlite.py @@ -217,8 +217,6 @@ def test_history_getitem(index, exp, hist, xonsh_builtins): assert (entry.cmd, entry.out, entry.rtn, entry.ts) == exp - - def test_hist_clear_cmd(hist, xonsh_builtins, capsys, tmpdir): """Verify that the CLI history clear command works.""" xonsh_builtins.__xonsh__.env.update({"XONSH_DATA_DIR": str(tmpdir)}) @@ -233,7 +231,7 @@ def test_hist_clear_cmd(hist, xonsh_builtins, capsys, tmpdir): out, err = capsys.readouterr() assert err.rstrip() == "History cleared" - assert len(xonsh_builtins.__xonsh__.history) == 0 + assert len(xonsh_builtins.__xonsh__.history) == 1 def test_hist_off_cmd(hist, xonsh_builtins, capsys, tmpdir): @@ -250,12 +248,12 @@ def test_hist_off_cmd(hist, xonsh_builtins, capsys, tmpdir): out, err = capsys.readouterr() assert err.rstrip() == "History off" - assert len(xonsh_builtins.__xonsh__.history) == 0 + assert len(xonsh_builtins.__xonsh__.history) == 1 for ts, cmd in enumerate(CMDS): # attempt to populate the shell history hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) - assert len(xonsh_builtins.__xonsh__.history) == 0 + assert len(xonsh_builtins.__xonsh__.history) == 1 def test_hist_on_cmd(hist, xonsh_builtins, capsys, tmpdir): @@ -273,9 +271,9 @@ def test_hist_on_cmd(hist, xonsh_builtins, capsys, tmpdir): out, err = capsys.readouterr() assert err.rstrip().endswith("History on") - assert len(xonsh_builtins.__xonsh__.history) == 0 + assert len(xonsh_builtins.__xonsh__.history) == 1 for ts, cmd in enumerate(CMDS): # attempt to populate the shell history hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) - assert len(xonsh_builtins.__xonsh__.history) == 6 + assert len(xonsh_builtins.__xonsh__.history) == 7 diff --git a/xonsh/history/json.py b/xonsh/history/json.py index c493b8535..698aebdd1 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -303,6 +303,9 @@ class JsonCommandField(cabc.Sequence): return len(self.hist) def __getitem__(self, key): + if not self.hist.remember_history: + return "" + size = len(self) if isinstance(key, slice): return [self[i] for i in range(*key.indices(size))] @@ -318,18 +321,15 @@ class JsonCommandField(cabc.Sequence): # now we know we have to go into the file queue = self.hist._queue queue.append(self) - if self.hist.remember_history: # todo is this necessary and elegant? - 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 = xlj.LazyJSON(f, reopen=False) - rtn = lj["cmds"][key].get(self.field, self.default) - if isinstance(rtn, xlj.LJNode): - rtn = rtn.load() - queue.popleft() - return rtn - else: - return "" + 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 = xlj.LazyJSON(f, reopen=False) + rtn = lj["cmds"][key].get(self.field, self.default) + if isinstance(rtn, xlj.LJNode): + rtn = rtn.load() + queue.popleft() + return rtn def i_am_at_the_front(self): """Tests if the command field is at the front of the queue.""" diff --git a/xonsh/history/sqlite.py b/xonsh/history/sqlite.py index 398f05233..fd1d3f732 100644 --- a/xonsh/history/sqlite.py +++ b/xonsh/history/sqlite.py @@ -304,5 +304,8 @@ class SqliteHistory(History): self.outs = [] self.tss = [] + # Add in dummy command. Delete items has to leave one. + self.append({"inp": "", "rtn": 0, "ts": (0, 0.5)}) + # Wipe data from disk. - xh_sqlite_delete_items(size_to_keep=0, filename=self.filename) + xh_sqlite_delete_items(size_to_keep=1, filename=self.filename) From 710fbf8a0b771eb9bbd9f10a984ed23ee443ca16 Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Thu, 17 Sep 2020 01:39:35 +0100 Subject: [PATCH 12/46] Changelog added. --- news/history_testing-ead.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 news/history_testing-ead.rst diff --git a/news/history_testing-ead.rst b/news/history_testing-ead.rst new file mode 100644 index 000000000..8eb7a0f3d --- /dev/null +++ b/news/history_testing-ead.rst @@ -0,0 +1,23 @@ +**Added:** + +`history clear`, `history off` and `history on` actions, for managing whether history in the current session is saved. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* From 49bc38e4d42b5938d20a76bf3ecfc61114026b34 Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Thu, 17 Sep 2020 02:15:52 +0100 Subject: [PATCH 13/46] Add docs. --- docs/tutorial_hist.rst | 15 +++++++++++++++ news/history_testing-ead.rst | 2 +- xonsh/history/sqlite.py | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/tutorial_hist.rst b/docs/tutorial_hist.rst index add815d0c..4383f0742 100644 --- a/docs/tutorial_hist.rst +++ b/docs/tutorial_hist.rst @@ -255,6 +255,21 @@ may be useful to share entries between shell sessions. In such a case, one can u the ``flush`` action to immediately save the session history to disk and make it accessible from other shell sessions. +``clear`` action +================ +Deletes the history from the current session up until this point. Later commands +will still be saved. + +``off`` action +================ +Deletes the history from the current session and turns off history saving for the +rest of the session. Only session metadata will be saved, not commands or output. + +``on`` action +================ +Turns history saving back on. Previous commands won't be saved, but future +commands will be. + ``gc`` action =============== Last, but certainly not least, the ``gc`` action is a manual hook into executing diff --git a/news/history_testing-ead.rst b/news/history_testing-ead.rst index 8eb7a0f3d..0edba1ec4 100644 --- a/news/history_testing-ead.rst +++ b/news/history_testing-ead.rst @@ -1,6 +1,6 @@ **Added:** -`history clear`, `history off` and `history on` actions, for managing whether history in the current session is saved. +``history clear``, ``history off`` and ``history on`` actions, for managing whether history in the current session is saved. **Changed:** diff --git a/xonsh/history/sqlite.py b/xonsh/history/sqlite.py index fd1d3f732..6c9b501fe 100644 --- a/xonsh/history/sqlite.py +++ b/xonsh/history/sqlite.py @@ -308,4 +308,4 @@ class SqliteHistory(History): self.append({"inp": "", "rtn": 0, "ts": (0, 0.5)}) # Wipe data from disk. - xh_sqlite_delete_items(size_to_keep=1, filename=self.filename) + xh_sqlite_delete_items(size_to_keep=1, filename=self.filename) # todo this leads to data loss from other sessions. Need to fix. From d7141856ae85e848598babc436d3d6fa84674ccd Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Thu, 17 Sep 2020 14:22:41 +0100 Subject: [PATCH 14/46] add docs, bugfixes, misc changes --- news/history_testing-ead.rst | 2 +- tests/test_history_sqlite.py | 10 +++++----- xonsh/history/sqlite.py | 12 ++++++++++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/news/history_testing-ead.rst b/news/history_testing-ead.rst index 0edba1ec4..0d54b6392 100644 --- a/news/history_testing-ead.rst +++ b/news/history_testing-ead.rst @@ -1,6 +1,6 @@ **Added:** -``history clear``, ``history off`` and ``history on`` actions, for managing whether history in the current session is saved. +* history clear, history off and history on actions, for managing whether history in the current session is saved. **Changed:** diff --git a/tests/test_history_sqlite.py b/tests/test_history_sqlite.py index 0d94e1040..a256cdc80 100644 --- a/tests/test_history_sqlite.py +++ b/tests/test_history_sqlite.py @@ -231,7 +231,7 @@ def test_hist_clear_cmd(hist, xonsh_builtins, capsys, tmpdir): out, err = capsys.readouterr() assert err.rstrip() == "History cleared" - assert len(xonsh_builtins.__xonsh__.history) == 1 + assert len(xonsh_builtins.__xonsh__.history) == 0 def test_hist_off_cmd(hist, xonsh_builtins, capsys, tmpdir): @@ -248,12 +248,12 @@ def test_hist_off_cmd(hist, xonsh_builtins, capsys, tmpdir): out, err = capsys.readouterr() assert err.rstrip() == "History off" - assert len(xonsh_builtins.__xonsh__.history) == 1 + assert len(xonsh_builtins.__xonsh__.history) == 0 for ts, cmd in enumerate(CMDS): # attempt to populate the shell history hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) - assert len(xonsh_builtins.__xonsh__.history) == 1 + assert len(xonsh_builtins.__xonsh__.history) == 0 def test_hist_on_cmd(hist, xonsh_builtins, capsys, tmpdir): @@ -271,9 +271,9 @@ def test_hist_on_cmd(hist, xonsh_builtins, capsys, tmpdir): out, err = capsys.readouterr() assert err.rstrip().endswith("History on") - assert len(xonsh_builtins.__xonsh__.history) == 1 + assert len(xonsh_builtins.__xonsh__.history) == 0 for ts, cmd in enumerate(CMDS): # attempt to populate the shell history hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) - assert len(xonsh_builtins.__xonsh__.history) == 7 + assert len(xonsh_builtins.__xonsh__.history) == 6 diff --git a/xonsh/history/sqlite.py b/xonsh/history/sqlite.py index 6c9b501fe..10a3b2d89 100644 --- a/xonsh/history/sqlite.py +++ b/xonsh/history/sqlite.py @@ -305,7 +305,15 @@ class SqliteHistory(History): self.tss = [] # Add in dummy command. Delete items has to leave one. - self.append({"inp": "", "rtn": 0, "ts": (0, 0.5)}) + #self.append({"inp": "", "rtn": 0, "ts": (0, 0.5)}) # Wipe data from disk. - xh_sqlite_delete_items(size_to_keep=1, filename=self.filename) # todo this leads to data loss from other sessions. Need to fix. + + #xh_sqlite_delete_items(size_to_keep=1, filename=self.filename) # todo this leads to data loss from other sessions. Need to fix. + sql = "DELETE FROM xonsh_history WHERE sessionid = ?" + with _xh_sqlite_get_conn(filename=self.filename) as conn: + c = conn.cursor() + _xh_sqlite_create_history_table(c) + c.execute(sql, (str(self.sessionid),)) + + # "DELETE FROM xonsh_history WHERE sessionid == ?", self.sessionid # check From b9e122a29a7507b9bb7b3c6c6fcdb5e9c318ed3f Mon Sep 17 00:00:00 2001 From: Bob Hyman Date: Thu, 17 Sep 2020 15:12:27 -0400 Subject: [PATCH 15/46] TAB key automatically selects first completion when starting completion. --- news/ptk-complete-select-first.rst | 25 +++++++++++++++++++++++++ xonsh/ptk_shell/key_bindings.py | 9 +++++++++ 2 files changed, 34 insertions(+) create mode 100644 news/ptk-complete-select-first.rst diff --git a/news/ptk-complete-select-first.rst b/news/ptk-complete-select-first.rst new file mode 100644 index 000000000..7a4b64bff --- /dev/null +++ b/news/ptk-complete-select-first.rst @@ -0,0 +1,25 @@ +**Added:** + +* + +**Changed:** + +* ptk key binding for TAB -- hitting TAB to start completion now automatically selects the first displayed completion (if any). + hitting TAB when in insert mode inserts TAB, as heretofore. This more exactly follows behavior of readline ``menu-complete``. + There is no configuration option for tailoring this behavior. + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/xonsh/ptk_shell/key_bindings.py b/xonsh/ptk_shell/key_bindings.py index e578e2a46..33d7d8d02 100644 --- a/xonsh/ptk_shell/key_bindings.py +++ b/xonsh/ptk_shell/key_bindings.py @@ -206,6 +206,15 @@ def load_xonsh_bindings() -> KeyBindingsBase: env = builtins.__xonsh__.env event.cli.current_buffer.insert_text(env.get("INDENT")) + @handle(Keys.Tab, filter=~tab_insert_indent) + def start_complete(event): + """If starting completions, automatically move to first option""" + buff = event.app.current_buffer + if buff.complete_state: + buff.complete_next() + else: + buff.start_completion(select_first=True) + @handle(Keys.ControlX, Keys.ControlE, filter=~has_selection) def open_editor(event): """ Open current buffer in editor """ From 4e0267fb6d96c5baf451e3a10e4969bc307afb3c Mon Sep 17 00:00:00 2001 From: Morten Enemark Lund Date: Thu, 17 Sep 2020 22:11:25 +0200 Subject: [PATCH 16/46] Detect windows AppExecution aliases --- xonsh/built_ins.py | 35 ++++++++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/xonsh/built_ins.py b/xonsh/built_ins.py index d9d54189b..bb6c723cf 100644 --- a/xonsh/built_ins.py +++ b/xonsh/built_ins.py @@ -179,16 +179,33 @@ def pathsearch(func, s, pymode=False, pathobj=False): RE_SHEBANG = LazyObject(lambda: re.compile(r"#![ \t]*(.+?)$"), globals(), "RE_SHEBANG") +def is_app_execution_alias(fname): + """ App execution aliases behave strangly on windows and python. + Here try to detect if a file is an app execution alias + """ + fname = pathlib.Path(fname) + return ( + not os.path.exists(fname) + and fname.name in os.listdir(fname.parent) + ) + + def _is_binary(fname, limit=80): - with open(fname, "rb") as f: - for i in range(limit): - char = f.read(1) - if char == b"\0": - return True - if char == b"\n": - return False - if char == b"": - return False + try: + with open(fname, "rb") as f: + for i in range(limit): + char = f.read(1) + if char == b"\0": + return True + if char == b"\n": + return False + if char == b"": + return + except OSError as e: + if ON_WINDOWS and is_app_execution_alias(fname): + return True + raise e + return False From fd6d292f07c09c98a7f61d05acd98af578cea472 Mon Sep 17 00:00:00 2001 From: Morten Enemark Lund Date: Thu, 17 Sep 2020 22:14:55 +0200 Subject: [PATCH 17/46] Add changelog entry --- news/windows_app_execution_aliases.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 news/windows_app_execution_aliases.rst diff --git a/news/windows_app_execution_aliases.rst b/news/windows_app_execution_aliases.rst new file mode 100644 index 000000000..a5689b00b --- /dev/null +++ b/news/windows_app_execution_aliases.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* Fix crash when xonsh tries to run windows app execution aliases. + +**Security:** + +* From f3ee61b6f4a79b4c17e57905c5188d2ee7f2c465 Mon Sep 17 00:00:00 2001 From: Morten Enemark Lund Date: Thu, 17 Sep 2020 22:30:08 +0200 Subject: [PATCH 18/46] fix black --- xonsh/built_ins.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/xonsh/built_ins.py b/xonsh/built_ins.py index bb6c723cf..1e00b518c 100644 --- a/xonsh/built_ins.py +++ b/xonsh/built_ins.py @@ -184,10 +184,7 @@ def is_app_execution_alias(fname): Here try to detect if a file is an app execution alias """ fname = pathlib.Path(fname) - return ( - not os.path.exists(fname) - and fname.name in os.listdir(fname.parent) - ) + return not os.path.exists(fname) and fname.name in os.listdir(fname.parent) def _is_binary(fname, limit=80): From 456dbfb8bfe11960764c374c36d1fbc2e3fbe216 Mon Sep 17 00:00:00 2001 From: Danny Sepler Date: Fri, 18 Sep 2020 00:04:26 -0400 Subject: [PATCH 19/46] Add cache on executables-in --- news/cache-executables-in.rst | 23 +++++++++++++++++++++++ xonsh/tools.py | 1 + 2 files changed, 24 insertions(+) create mode 100644 news/cache-executables-in.rst diff --git a/news/cache-executables-in.rst b/news/cache-executables-in.rst new file mode 100644 index 000000000..035dacc64 --- /dev/null +++ b/news/cache-executables-in.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added cache on finding executable files, to speedup command-not-found suggestions + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/xonsh/tools.py b/xonsh/tools.py index dfd876518..055589307 100644 --- a/xonsh/tools.py +++ b/xonsh/tools.py @@ -802,6 +802,7 @@ def _executables_in_windows(path): return +# @functools.lru_cache(128) def executables_in(path): """Returns a generator of files in path that the user could execute. """ if ON_WINDOWS: From e936b59067361d8d5a667229514179695f4e81a8 Mon Sep 17 00:00:00 2001 From: Danny Sepler Date: Fri, 18 Sep 2020 00:11:33 -0400 Subject: [PATCH 20/46] uncomment the thing --- xonsh/tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xonsh/tools.py b/xonsh/tools.py index 055589307..9e9495004 100644 --- a/xonsh/tools.py +++ b/xonsh/tools.py @@ -802,7 +802,7 @@ def _executables_in_windows(path): return -# @functools.lru_cache(128) +@functools.lru_cache(128) def executables_in(path): """Returns a generator of files in path that the user could execute. """ if ON_WINDOWS: From 7ed2769b01e2cab25a8e95cea0f153375cba68e4 Mon Sep 17 00:00:00 2001 From: Danny Sepler Date: Fri, 18 Sep 2020 00:21:43 -0400 Subject: [PATCH 21/46] clear cache at end of each test case --- tests/test_tools.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_tools.py b/tests/test_tools.py index fb8aaf149..fcbaf6577 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -1462,6 +1462,7 @@ def test_executables_in(xonsh_builtins): result = set(executables_in(test_path)) else: result = set(executables_in(test_path)) + executables_in.cache_clear() assert expected == result From a97101ee34eff71643275c2f2f6d356d95296587 Mon Sep 17 00:00:00 2001 From: Anthony Scopatz Date: Thu, 17 Sep 2020 21:24:07 -0700 Subject: [PATCH 22/46] rever changes --- rever.xsh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rever.xsh b/rever.xsh index 59d8013d9..19a1fad89 100644 --- a/rever.xsh +++ b/rever.xsh @@ -4,8 +4,12 @@ from rever.activities.ghrelease import git_archive_asset $PROJECT = $GITHUB_ORG = $GITHUB_REPO = 'xonsh' $WEBSITE_URL = 'http://xon.sh' $ACTIVITIES = ['authors', 'version_bump', 'changelog', 'pytest', 'appimage', - 'tag', 'push_tag', 'ghrelease', 'sphinx', - 'ghpages', 'pypi', 'conda_forge', + 'tag', 'push_tag', + 'ghrelease', + 'sphinx', + 'ghpages', + 'pypi', + 'conda_forge', ] $PYPI_SIGN = False From 641d4f44080bc31bdc99b7dd42b0490af4bfc611 Mon Sep 17 00:00:00 2001 From: Danny Sepler Date: Fri, 18 Sep 2020 00:47:30 -0400 Subject: [PATCH 23/46] use commands cache instead --- news/cache-executables-in.rst | 2 +- tests/test_tools.py | 1 - xonsh/tools.py | 12 ++++-------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/news/cache-executables-in.rst b/news/cache-executables-in.rst index 035dacc64..f0c347284 100644 --- a/news/cache-executables-in.rst +++ b/news/cache-executables-in.rst @@ -1,6 +1,6 @@ **Added:** -* Added cache on finding executable files, to speedup command-not-found suggestions +* Use command_cache when finding available commands, to speedup command-not-found suggestions **Changed:** diff --git a/tests/test_tools.py b/tests/test_tools.py index fcbaf6577..fb8aaf149 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -1462,7 +1462,6 @@ def test_executables_in(xonsh_builtins): result = set(executables_in(test_path)) else: result = set(executables_in(test_path)) - executables_in.cache_clear() assert expected == result diff --git a/xonsh/tools.py b/xonsh/tools.py index 9e9495004..cf96f64a4 100644 --- a/xonsh/tools.py +++ b/xonsh/tools.py @@ -802,7 +802,6 @@ def _executables_in_windows(path): return -@functools.lru_cache(128) def executables_in(path): """Returns a generator of files in path that the user could execute. """ if ON_WINDOWS: @@ -883,13 +882,10 @@ def suggest_commands(cmd, env, aliases): if levenshtein(alias.lower(), cmd, thresh) < thresh: suggested[alias] = "Alias" - for path in filter(os.path.isdir, env.get("PATH")): - for _file in executables_in(path): - if ( - _file not in suggested - and levenshtein(_file.lower(), cmd, thresh) < thresh - ): - suggested[_file] = "Command ({0})".format(os.path.join(path, _file)) + for _cmd in builtins.__xonsh__.commands_cache.all_commands: + if _cmd not in suggested: + if levenshtein(_cmd.lower(), cmd, thresh) < thresh: + suggested[_cmd] = "Command ({0})".format(_cmd) suggested = collections.OrderedDict( sorted( From ed46283da85a0f2b08accb2044754bdbfa1b0cfa Mon Sep 17 00:00:00 2001 From: Gyuri Horak Date: Sun, 20 Sep 2020 12:34:03 +0200 Subject: [PATCH 24/46] ANSI OSC escape code 'support' --- xonsh/ptk_shell/shell.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/xonsh/ptk_shell/shell.py b/xonsh/ptk_shell/shell.py index 621b933e8..8fb5b75e4 100644 --- a/xonsh/ptk_shell/shell.py +++ b/xonsh/ptk_shell/shell.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- """The prompt_toolkit based xonsh shell.""" import os +import re import sys import builtins from types import MethodType @@ -37,6 +38,7 @@ from prompt_toolkit.styles.pygments import ( ) +ANSI_OSC_PATTERN = re.compile("\x1b].*?\007") Token = _TokenType() events.transmogrify("on_ptk_create", "LoadEvent") @@ -76,6 +78,25 @@ def tokenize_ansi(tokens): return ansi_tokens +def remove_ansi_osc(prompt): + """Removes the ANSI OSC escape codes - ``prompt_toolkit`` does not support them. + Some terminal emulators - like iTerm2 - uses them for various things. + + See: https://www.iterm2.com/documentation-escape-codes.html + """ + + osc_tokens = ANSI_OSC_PATTERN.findall(prompt) + prompt = re.sub(ANSI_OSC_PATTERN, "", prompt) + + return prompt, osc_tokens + + +def write_ansi_osc(ansi_osc_code): + with open(1, "wb", closefd=False) as output: + output.write(ansi_osc_code.encode()) + output.flush() + + class PromptToolkitShell(BaseShell): """The xonsh shell for prompt_toolkit v2 and later.""" @@ -250,10 +271,22 @@ class PromptToolkitShell(BaseShell): p = self.prompt_formatter(p) except Exception: # pylint: disable=broad-except print_exception() + + p, osc_tokens = remove_ansi_osc(p) + toks = partial_color_tokenize(p) if self._first_prompt: carriage_return() self._first_prompt = False + + # handle OSC tokens + for osc in osc_tokens: + if osc[2:4] == "0;": + # set $TITLE + builtins.__xonsh__.env["TITLE"] = osc[4:-1] + else: + write_ansi_osc(osc) + self.settitle() return tokenize_ansi(PygmentsTokens(toks)) From ef6634097b363b6d9ef66e42da54b569254cfa6a Mon Sep 17 00:00:00 2001 From: Gyuri Horak Date: Sun, 20 Sep 2020 18:55:01 +0200 Subject: [PATCH 25/46] ANSI OSC - tests, news item --- news/ansi_osc.rst | 23 +++++++++++++++++++++++ tests/test_ptk_shell.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 news/ansi_osc.rst diff --git a/news/ansi_osc.rst b/news/ansi_osc.rst new file mode 100644 index 000000000..0b42302cc --- /dev/null +++ b/news/ansi_osc.rst @@ -0,0 +1,23 @@ +**Added:** + +* Support for ANSI OSC escape sequences in ``$PROMPT``, setting ``$TITLE`` for example. (#374, #1403) + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/tests/test_ptk_shell.py b/tests/test_ptk_shell.py index ab84939aa..0eb54300f 100644 --- a/tests/test_ptk_shell.py +++ b/tests/test_ptk_shell.py @@ -8,7 +8,7 @@ from xonsh.platform import minimum_required_ptk_version # verify error if ptk not installed or below min -from xonsh.ptk_shell.shell import tokenize_ansi +from xonsh.ptk_shell.shell import tokenize_ansi, remove_ansi_osc from xonsh.shell import Shell @@ -105,4 +105,31 @@ def test_tokenize_ansi(prompt_tokens, ansi_string_parts): assert token[1] == text +@pytest.mark.parametrize( + "raw_prompt, prompt, osc_tokens", + [ + # no title + ("test prompt", "test prompt", []), + # starts w/ title + ("\033]0;TITLE THIS\007test prompt", "test prompt", ["\033]0;TITLE THIS\007"]), + # ends w/ title + ("test prompt\033]0;TITLE THIS\007", "test prompt", ["\033]0;TITLE THIS\007"]), + # title in the middle + ("test \033]0;TITLE THIS\007prompt", "test prompt", ["\033]0;TITLE THIS\007"]), + # title + iTerm2 OSC exapmle + ( + "test \033]0;TITLE THIS\007prompt \033]133;A\007here", + "test prompt here", + ["\033]0;TITLE THIS\007", "\033]133;A\007"], + ), + ], +) +def test_remove_ansi_osc(raw_prompt, prompt, osc_tokens): + checked_prompt, removed_osc = remove_ansi_osc(raw_prompt) + assert prompt == checked_prompt + assert len(removed_osc) == len(osc_tokens) + for removed, ref in zip(removed_osc, osc_tokens): + assert removed == ref + + # someday: initialize PromptToolkitShell and have it actually do something. From 150947f8e34889c38a57ac40a8de1909e92ddb92 Mon Sep 17 00:00:00 2001 From: Danny Sepler Date: Sun, 20 Sep 2020 13:31:25 -0400 Subject: [PATCH 26/46] Minor performance improvements to the prompt git functions --- news/prompt-speed.rst | 23 +++++++++++++++ xonsh/prompt/vc.py | 67 +++++++++++++++++++++---------------------- 2 files changed, 55 insertions(+), 35 deletions(-) create mode 100644 news/prompt-speed.rst diff --git a/news/prompt-speed.rst b/news/prompt-speed.rst new file mode 100644 index 000000000..5ac871e51 --- /dev/null +++ b/news/prompt-speed.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* Minor improvements to the get prompt speed. (Mostly in git.) + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/xonsh/prompt/vc.py b/xonsh/prompt/vc.py index f4bcbd127..6da679021 100644 --- a/xonsh/prompt/vc.py +++ b/xonsh/prompt/vc.py @@ -24,26 +24,15 @@ RE_REMOVE_ANSI = LazyObject( def _get_git_branch(q): denv = builtins.__xonsh__.env.detype() try: - branches = xt.decode_bytes( - subprocess.check_output( - ["git", "branch"], env=denv, stderr=subprocess.DEVNULL - ) - ).splitlines() + cmd = ["git", "rev-parse", "--abbrev-ref", "HEAD"] + branch = xt.decode_bytes( + subprocess.check_output(cmd, env=denv, stderr=subprocess.DEVNULL) + ) + branch = branch.splitlines()[0] or None except (subprocess.CalledProcessError, OSError, FileNotFoundError): q.put(None) else: - for branch in branches: - if not branch.startswith("* "): - continue - elif branch.endswith(")"): - branch = branch.split()[-1][:-1] - else: - branch = branch.split()[-1] - - q.put(branch) - break - else: - q.put(None) + q.put(branch) def get_git_branch(): @@ -59,7 +48,6 @@ def get_git_branch(): t.join(timeout=timeout) try: branch = q.get_nowait() - # branch = RE_REMOVE_ANSI.sub("", branch or "") if branch: branch = RE_REMOVE_ANSI.sub("", branch) except queue.Empty: @@ -157,16 +145,17 @@ def current_branch(): """ branch = None cmds = builtins.__xonsh__.commands_cache - # check for binary only once - if cmds.is_empty(): - has_git = bool(cmds.locate_binary("git", ignore_alias=True)) - has_hg = bool(cmds.locate_binary("hg", ignore_alias=True)) - else: - has_git = bool(cmds.lazy_locate_binary("git", ignore_alias=True)) - has_hg = bool(cmds.lazy_locate_binary("hg", ignore_alias=True)) - if has_git: + + # This allows us to locate binaries after git only if necessary + def has(vc): + if cmds.is_empty(): + return bool(cmds.locate_binary(vc, ignore_alias=True)) + else: + return bool(cmds.lazy_locate_binary(vc, ignore_alias=True)) + + if has("git"): branch = get_git_branch() - if not branch and has_hg: + if not branch and has("hg"): branch = get_hg_branch() if isinstance(branch, subprocess.TimeoutExpired): branch = "" @@ -175,19 +164,27 @@ def current_branch(): def _git_dirty_working_directory(q, include_untracked): - status = None denv = builtins.__xonsh__.env.detype() try: - cmd = ["git", "status", "--porcelain"] + # Borrowing this conversation + # https://github.com/sindresorhus/pure/issues/115 if include_untracked: - cmd.append("--untracked-files=normal") + cmd = [ + "git", + "status", + "--porcelain", + "--quiet", + "--untracked-files=normal", + ] else: - cmd.append("--untracked-files=no") - status = subprocess.check_output(cmd, stderr=subprocess.DEVNULL, env=denv) + cmd = ["git", "diff", "--no-ext-diff", "--quiet"] + child = subprocess.Popen(cmd, stderr=subprocess.DEVNULL, env=denv) + child.communicate() + dwd = bool(child.returncode) except (subprocess.CalledProcessError, OSError, FileNotFoundError): q.put(None) - if status is not None: - return q.put(bool(status)) + else: + q.put(dwd) def git_dirty_working_directory(include_untracked=False): @@ -244,7 +241,7 @@ def dirty_working_directory(): cmds = builtins.__xonsh__.commands_cache if cmds.lazy_locate_binary("git", ignore_alias=True): dwd = git_dirty_working_directory() - if cmds.lazy_locate_binary("hg", ignore_alias=True) and dwd is None: + if dwd is None and cmds.lazy_locate_binary("hg", ignore_alias=True): dwd = hg_dirty_working_directory() return dwd From f02e70d9052df7126369994b68fe3097d5626b80 Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Sun, 20 Sep 2020 18:42:10 +0100 Subject: [PATCH 27/46] Change docstrings. --- xonsh/history/base.py | 11 ++++++----- xonsh/history/json.py | 6 ++++-- xonsh/history/sqlite.py | 11 +++-------- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/xonsh/history/base.py b/xonsh/history/base.py index f4c31eec3..bcbe1f08a 100644 --- a/xonsh/history/base.py +++ b/xonsh/history/base.py @@ -157,17 +157,18 @@ class History: @staticmethod def remember_history_check(f): - """Allows the decorated function to run only if - self.remember_history is True. + """Jumps over the decorated function if self.remember_history is False. """ - def new_f(self, *args, **kwargs): + def out_f(self, *args, **kwargs): if self.remember_history: return f(self, *args, **kwargs) else: return - return new_f + return out_f def clear(self): - """Wipes the current session from both the disk and memory.""" + """Clears the history of the current session from both the disk and + memory. + """ pass diff --git a/xonsh/history/json.py b/xonsh/history/json.py index 698aebdd1..fd7d55940 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -509,8 +509,11 @@ class JsonHistory(History): time.sleep(0.1) # don't monopolize the thread (or Python GIL?) def clear(self): + """Clears the current session's history from both memory and disk.""" def wipe_memory(hist): + """Wipes history entries from memory. Keeps sessionid and other + metadata.""" hist.buffer = [] self.tss = JsonCommandField("ts", self) self.inps = JsonCommandField("inp", self) @@ -521,6 +524,5 @@ class JsonHistory(History): wipe_memory(self) - # Flush empty history object to disk. This overwrites the old commands, - # but keeps basic session metadata to prevent things from breaking. + # Flush empty history object to disk, overwriting previous data. self.flush() diff --git a/xonsh/history/sqlite.py b/xonsh/history/sqlite.py index 10a3b2d89..7c1e8d64b 100644 --- a/xonsh/history/sqlite.py +++ b/xonsh/history/sqlite.py @@ -299,21 +299,16 @@ class SqliteHistory(History): continue def clear(self): + """Clears the current session's history from both memory and disk.""" + # Wipe memory self.inps = [] self.rtns = [] self.outs = [] self.tss = [] - # Add in dummy command. Delete items has to leave one. - #self.append({"inp": "", "rtn": 0, "ts": (0, 0.5)}) - - # Wipe data from disk. - - #xh_sqlite_delete_items(size_to_keep=1, filename=self.filename) # todo this leads to data loss from other sessions. Need to fix. + # Wipe the current session's entries from the database. sql = "DELETE FROM xonsh_history WHERE sessionid = ?" with _xh_sqlite_get_conn(filename=self.filename) as conn: c = conn.cursor() _xh_sqlite_create_history_table(c) c.execute(sql, (str(self.sessionid),)) - - # "DELETE FROM xonsh_history WHERE sessionid == ?", self.sessionid # check From 3933b742789678b0b1e82d3bb8e6a6b4555e911b Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Sun, 20 Sep 2020 19:28:54 +0100 Subject: [PATCH 28/46] Improve comments in tests --- tests/test_history.py | 2 +- tests/test_history_sqlite.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_history.py b/tests/test_history.py index 8005ff259..3f9162d86 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -541,7 +541,7 @@ def test_hist_on_cmd(hist, xonsh_builtins, capsys, tmpdir): assert err.rstrip().endswith("History on") assert len(xonsh_builtins.__xonsh__.history) == 0 - for ts, cmd in enumerate(CMDS): # attempt to populate the shell history + for ts, cmd in enumerate(CMDS): # populate the shell history hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) assert len(xonsh_builtins.__xonsh__.history) == 6 diff --git a/tests/test_history_sqlite.py b/tests/test_history_sqlite.py index a256cdc80..95661f667 100644 --- a/tests/test_history_sqlite.py +++ b/tests/test_history_sqlite.py @@ -273,7 +273,7 @@ def test_hist_on_cmd(hist, xonsh_builtins, capsys, tmpdir): assert err.rstrip().endswith("History on") assert len(xonsh_builtins.__xonsh__.history) == 0 - for ts, cmd in enumerate(CMDS): # attempt to populate the shell history + for ts, cmd in enumerate(CMDS): # populate the shell history hist.append({"inp": cmd, "rtn": 0, "ts": (ts + 1, ts + 1.5)}) assert len(xonsh_builtins.__xonsh__.history) == 6 From bd73cd5fe0700001f2b5e92b6099a7fb0333893e Mon Sep 17 00:00:00 2001 From: Gyuri Horak Date: Mon, 21 Sep 2020 22:14:55 +0200 Subject: [PATCH 29/46] ANSI OSC - review fixes --- xonsh/ptk_shell/shell.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/xonsh/ptk_shell/shell.py b/xonsh/ptk_shell/shell.py index 8fb5b75e4..0d54ab661 100644 --- a/xonsh/ptk_shell/shell.py +++ b/xonsh/ptk_shell/shell.py @@ -86,15 +86,13 @@ def remove_ansi_osc(prompt): """ osc_tokens = ANSI_OSC_PATTERN.findall(prompt) - prompt = re.sub(ANSI_OSC_PATTERN, "", prompt) + prompt = ANSI_OSC_PATTERN.sub("", prompt) return prompt, osc_tokens def write_ansi_osc(ansi_osc_code): - with open(1, "wb", closefd=False) as output: - output.write(ansi_osc_code.encode()) - output.flush() + print(ansi_osc_code, file=sys.__stdout__, flush=True) class PromptToolkitShell(BaseShell): From e3e6c82a22d7c206d96758537ada55e3a4ec7b2e Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Mon, 21 Sep 2020 21:34:56 +0100 Subject: [PATCH 30/46] Remove wipe_memory function. --- xonsh/history/json.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/xonsh/history/json.py b/xonsh/history/json.py index fd7d55940..26b13f282 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -511,18 +511,14 @@ class JsonHistory(History): def clear(self): """Clears the current session's history from both memory and disk.""" - def wipe_memory(hist): - """Wipes history entries from memory. Keeps sessionid and other - metadata.""" - hist.buffer = [] - self.tss = JsonCommandField("ts", self) - self.inps = JsonCommandField("inp", self) - self.outs = JsonCommandField("out", self) - self.rtns = JsonCommandField("rtn", self) - self._len = 0 - self._skipped = 0 - - wipe_memory(self) + # Wipe history from memory. Keep sessionid and other metadata. + self.buffer = [] + self.tss = JsonCommandField("ts", self) + self.inps = JsonCommandField("inp", self) + self.outs = JsonCommandField("out", self) + self.rtns = JsonCommandField("rtn", self) + self._len = 0 + self._skipped = 0 # Flush empty history object to disk, overwriting previous data. self.flush() From 7f15ff0a64631fe82ab20256d39e5dc8e466eb1b Mon Sep 17 00:00:00 2001 From: anki-code Date: Mon, 21 Sep 2020 23:51:36 +0300 Subject: [PATCH 31/46] Delete whole word with Shift+Del Here should be Control+Backspace but https://github.com/prompt-toolkit/python-prompt-toolkit/issues/257#issuecomment-190328366 --- xontrib/whole_word_jumping.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/xontrib/whole_word_jumping.py b/xontrib/whole_word_jumping.py index 4330ee50d..d09b11b45 100644 --- a/xontrib/whole_word_jumping.py +++ b/xontrib/whole_word_jumping.py @@ -28,3 +28,15 @@ def custom_keybindings(bindings, **kw): pos = buff.document.find_next_word_ending(count=event.arg, WORD=True) if pos: buff.cursor_position += pos + + @bindings.add(Keys.ShiftDelete) + def shift_delete(event): + buff = event.current_buffer + startpos, endpos = buff.document.find_boundaries_of_current_word(WORD=True) + startpos = buff.cursor_position + startpos - 1 + startpos = 0 if startpos < 0 else startpos + endpos = buff.cursor_position + endpos + endpos = endpos + 1 if startpos == 0 else endpos + buff.text = buff.text[:startpos] + buff.text[endpos:] + buff.cursor_position = startpos + From 03bacf4087bf368faf29e514fd16600749e7b28d Mon Sep 17 00:00:00 2001 From: anki-code Date: Mon, 21 Sep 2020 23:55:30 +0300 Subject: [PATCH 32/46] Create shift_del.rst --- news/shift_del.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 news/shift_del.rst diff --git a/news/shift_del.rst b/news/shift_del.rst new file mode 100644 index 000000000..9f1d2e3f9 --- /dev/null +++ b/news/shift_del.rst @@ -0,0 +1,23 @@ +**Added:** + +* Added to xontrib whole_word_jumping: Shift+Delete hotkey to delete whole word. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* From 08765c4c90aaf26cdd6be65fe78ce5b819f93b97 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 22 Sep 2020 00:11:21 +0300 Subject: [PATCH 33/46] black --- xonsh/completers/tools.py | 5 +---- xonsh/history/json.py | 2 +- xonsh/parsers/fstring_adaptor.py | 2 +- xonsh/xoreutils/ulimit.py | 8 +------- xontrib/whole_word_jumping.py | 1 - 5 files changed, 4 insertions(+), 14 deletions(-) diff --git a/xonsh/completers/tools.py b/xonsh/completers/tools.py index 809081b4c..17d2fccad 100644 --- a/xonsh/completers/tools.py +++ b/xonsh/completers/tools.py @@ -61,10 +61,7 @@ class RichCompletion(str): def __repr__(self): return "RichCompletion({}, prefix_len={}, display={}, description={})".format( - repr(str(self)), - self.prefix_len, - repr(self.display), - repr(self.description), + repr(str(self)), self.prefix_len, repr(self.display), repr(self.description) ) diff --git a/xonsh/history/json.py b/xonsh/history/json.py index 6ccef96e5..0104f3bdf 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -202,7 +202,7 @@ class JsonHistoryGC(threading.Thread): # info: file size, closing timestamp, number of commands, filename ts = lj.get("ts", (0.0, None)) files.append( - (ts[1] or ts[0], len(lj.sizes["cmds"]) - 1, f, cur_file_size,), + (ts[1] or ts[0], len(lj.sizes["cmds"]) - 1, f, cur_file_size) ) lj.close() if xonsh_debug: diff --git a/xonsh/parsers/fstring_adaptor.py b/xonsh/parsers/fstring_adaptor.py index 13e92b5fe..8aa8b53dd 100644 --- a/xonsh/parsers/fstring_adaptor.py +++ b/xonsh/parsers/fstring_adaptor.py @@ -181,7 +181,7 @@ class FStringAdaptor: ) ) field_node = ast.Tuple( - elts=elts, ctx=ast.Load(), lineno=lineno, col_offset=col_offset, + elts=elts, ctx=ast.Load(), lineno=lineno, col_offset=col_offset ) node.args[0] = field_node diff --git a/xonsh/xoreutils/ulimit.py b/xonsh/xoreutils/ulimit.py index ef801879d..7de42ff4e 100644 --- a/xonsh/xoreutils/ulimit.py +++ b/xonsh/xoreutils/ulimit.py @@ -159,13 +159,7 @@ def _ul_add_action(actions, opt, res_type, stderr): actions.append( [ _ul_show, - { - "res": r[0], - "res_type": res_type, - "desc": r[3], - "unit": r[4], - "opt": opt, - }, + {"res": r[0], "res_type": res_type, "desc": r[3], "unit": r[4], "opt": opt}, ] ) return True diff --git a/xontrib/whole_word_jumping.py b/xontrib/whole_word_jumping.py index d09b11b45..ac72fc9c9 100644 --- a/xontrib/whole_word_jumping.py +++ b/xontrib/whole_word_jumping.py @@ -39,4 +39,3 @@ def custom_keybindings(bindings, **kw): endpos = endpos + 1 if startpos == 0 else endpos buff.text = buff.text[:startpos] + buff.text[endpos:] buff.cursor_position = startpos - From dc086d39b3484f38ac14b32d680381479a80531f Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Tue, 22 Sep 2020 00:08:30 +0100 Subject: [PATCH 34/46] Remove remember_history_check decorater. --- xonsh/history/base.py | 12 ------------ xonsh/history/json.py | 5 +++-- xonsh/history/sqlite.py | 3 ++- 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/xonsh/history/base.py b/xonsh/history/base.py index bcbe1f08a..70f7d40e8 100644 --- a/xonsh/history/base.py +++ b/xonsh/history/base.py @@ -155,18 +155,6 @@ class History: """ pass - @staticmethod - def remember_history_check(f): - """Jumps over the decorated function if self.remember_history is False. - """ - def out_f(self, *args, **kwargs): - if self.remember_history: - return f(self, *args, **kwargs) - else: - return - - return out_f - def clear(self): """Clears the history of the current session from both the disk and memory. diff --git a/xonsh/history/json.py b/xonsh/history/json.py index 26b13f282..7a4dd7efa 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -394,7 +394,6 @@ class JsonHistory(History): def __len__(self): return self._len - self._skipped - @History.remember_history_check def append(self, cmd): """Appends command to history. Will periodically flush the history to file. @@ -411,6 +410,8 @@ class JsonHistory(History): hf : JsonHistoryFlusher or None The thread that was spawned to flush history """ + if not self.remember_history: + return self.buffer.append(cmd) self._len += 1 # must come before flushing if len(self.buffer) >= self.buffersize: @@ -419,7 +420,6 @@ class JsonHistory(History): hf = None return hf - @History.remember_history_check def flush(self, at_exit=False): """Flushes the current command buffer to disk. @@ -434,6 +434,7 @@ class JsonHistory(History): hf : JsonHistoryFlusher or None The thread that was spawned to flush history """ + # Implicitly covers case of self.remember_history being False. if len(self.buffer) == 0: return diff --git a/xonsh/history/sqlite.py b/xonsh/history/sqlite.py index 7c1e8d64b..29378c6db 100644 --- a/xonsh/history/sqlite.py +++ b/xonsh/history/sqlite.py @@ -239,8 +239,9 @@ class SqliteHistory(History): # during init rerun create command setattr(XH_SQLITE_CACHE, XH_SQLITE_CREATED_SQL_TBL, False) - @History.remember_history_check def append(self, cmd): + if not self.remember_history: + return envs = builtins.__xonsh__.env inp = cmd["inp"].rstrip() self.inps.append(inp) From 16e7f1441cb30cb48bcd3275f17c7187c39457d9 Mon Sep 17 00:00:00 2001 From: "Christopher J. Wright" Date: Mon, 21 Sep 2020 20:17:56 -0400 Subject: [PATCH 35/46] add name to error message from failure to validate --- news/name_in_error.rst | 23 +++++++++++++++++++++++ xonsh/environ.py | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 news/name_in_error.rst diff --git a/news/name_in_error.rst b/news/name_in_error.rst new file mode 100644 index 000000000..fa8a1d88e --- /dev/null +++ b/news/name_in_error.rst @@ -0,0 +1,23 @@ +**Added:** + +* ValueErrors from environ.register now report the name of the bad env var + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/xonsh/environ.py b/xonsh/environ.py index d342092ab..fc93d1f4f 100644 --- a/xonsh/environ.py +++ b/xonsh/environ.py @@ -2080,7 +2080,7 @@ class Env(cabc.MutableMapping): pass else: raise ValueError( - "Default value does not match type specified by validate" + f"Default value for {name} does not match type specified by validate" ) self._vars[name] = Var( From bb30e46f1e84bbdc3fb98b1f8393c4e3b7473895 Mon Sep 17 00:00:00 2001 From: Danny Sepler Date: Mon, 21 Sep 2020 20:56:25 -0400 Subject: [PATCH 36/46] Feedback --- xonsh/prompt/vc.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/xonsh/prompt/vc.py b/xonsh/prompt/vc.py index 6da679021..4499c354d 100644 --- a/xonsh/prompt/vc.py +++ b/xonsh/prompt/vc.py @@ -137,6 +137,15 @@ def _first_branch_timeout_message(): ) +def _has(binary): + """ This allows us to locate binaries after git only if necessary """ + cmds = builtins.__xonsh__.commands_cache + if cmds.is_empty(): + return bool(cmds.locate_binary(binary, ignore_alias=True)) + else: + return bool(cmds.lazy_locate_binary(binary, ignore_alias=True)) + + def current_branch(): """Gets the branch for a current working directory. Returns an empty string if the cwd is not a repository. This currently only works for git and hg @@ -144,18 +153,9 @@ def current_branch(): '' is returned. """ branch = None - cmds = builtins.__xonsh__.commands_cache - - # This allows us to locate binaries after git only if necessary - def has(vc): - if cmds.is_empty(): - return bool(cmds.locate_binary(vc, ignore_alias=True)) - else: - return bool(cmds.lazy_locate_binary(vc, ignore_alias=True)) - - if has("git"): + if _has("git"): branch = get_git_branch() - if not branch and has("hg"): + if not branch and _has("hg"): branch = get_hg_branch() if isinstance(branch, subprocess.TimeoutExpired): branch = "" @@ -178,8 +178,7 @@ def _git_dirty_working_directory(q, include_untracked): ] else: cmd = ["git", "diff", "--no-ext-diff", "--quiet"] - child = subprocess.Popen(cmd, stderr=subprocess.DEVNULL, env=denv) - child.communicate() + child = subprocess.run(cmd, stderr=subprocess.DEVNULL, env=denv) dwd = bool(child.returncode) except (subprocess.CalledProcessError, OSError, FileNotFoundError): q.put(None) @@ -238,10 +237,9 @@ def dirty_working_directory(): None. Currently supports git and hg. """ dwd = None - cmds = builtins.__xonsh__.commands_cache - if cmds.lazy_locate_binary("git", ignore_alias=True): + if _has("git"): dwd = git_dirty_working_directory() - if dwd is None and cmds.lazy_locate_binary("hg", ignore_alias=True): + if dwd is None and _has("hg"): dwd = hg_dirty_working_directory() return dwd From 50420249ae768420f539f46491bed46e450d325c Mon Sep 17 00:00:00 2001 From: Danny Sepler Date: Mon, 21 Sep 2020 21:12:38 -0400 Subject: [PATCH 37/46] more feedback --- xonsh/prompt/vc.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/xonsh/prompt/vc.py b/xonsh/prompt/vc.py index 4499c354d..5b708070f 100644 --- a/xonsh/prompt/vc.py +++ b/xonsh/prompt/vc.py @@ -137,7 +137,7 @@ def _first_branch_timeout_message(): ) -def _has(binary): +def _vc_has(binary): """ This allows us to locate binaries after git only if necessary """ cmds = builtins.__xonsh__.commands_cache if cmds.is_empty(): @@ -153,9 +153,9 @@ def current_branch(): '' is returned. """ branch = None - if _has("git"): + if _vc_has("git"): branch = get_git_branch() - if not branch and _has("hg"): + if not branch and _vc_has("hg"): branch = get_hg_branch() if isinstance(branch, subprocess.TimeoutExpired): branch = "" @@ -237,9 +237,9 @@ def dirty_working_directory(): None. Currently supports git and hg. """ dwd = None - if _has("git"): + if _vc_has("git"): dwd = git_dirty_working_directory() - if dwd is None and _has("hg"): + if dwd is None and _vc_has("hg"): dwd = hg_dirty_working_directory() return dwd From fdf5622ee8679311a16fc7a7b81ab731ffd842b3 Mon Sep 17 00:00:00 2001 From: Danny Sepler Date: Mon, 21 Sep 2020 22:31:44 -0400 Subject: [PATCH 38/46] Explaining --exit-code return values --- xonsh/prompt/vc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/xonsh/prompt/vc.py b/xonsh/prompt/vc.py index 5b708070f..7d0432777 100644 --- a/xonsh/prompt/vc.py +++ b/xonsh/prompt/vc.py @@ -166,7 +166,7 @@ def current_branch(): def _git_dirty_working_directory(q, include_untracked): denv = builtins.__xonsh__.env.detype() try: - # Borrowing this conversation + # Borrowed from this conversation # https://github.com/sindresorhus/pure/issues/115 if include_untracked: cmd = [ @@ -179,6 +179,9 @@ def _git_dirty_working_directory(q, include_untracked): else: cmd = ["git", "diff", "--no-ext-diff", "--quiet"] child = subprocess.run(cmd, stderr=subprocess.DEVNULL, env=denv) + # "--quiet" git commands imply "--exit-code", which returns: + # 1 if there are differences + # 0 if there are no differences dwd = bool(child.returncode) except (subprocess.CalledProcessError, OSError, FileNotFoundError): q.put(None) From 2ac84228f705d3204f594d135497eb77777220c5 Mon Sep 17 00:00:00 2001 From: Danny Sepler Date: Mon, 21 Sep 2020 23:30:54 -0400 Subject: [PATCH 39/46] Also include indexed files in working directory --- xonsh/prompt/vc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/xonsh/prompt/vc.py b/xonsh/prompt/vc.py index 7d0432777..c89ecfa11 100644 --- a/xonsh/prompt/vc.py +++ b/xonsh/prompt/vc.py @@ -177,7 +177,9 @@ def _git_dirty_working_directory(q, include_untracked): "--untracked-files=normal", ] else: - cmd = ["git", "diff", "--no-ext-diff", "--quiet"] + unindexed = ["git", "diff", "--no-ext-diff", "--quiet"] + indexed = unindexed + ["--cached", "HEAD"] + cmd = indexed + ["||"] + unindexed child = subprocess.run(cmd, stderr=subprocess.DEVNULL, env=denv) # "--quiet" git commands imply "--exit-code", which returns: # 1 if there are differences From 845ec4a8b752447dfd7e0e62380dd26a78ecf4af Mon Sep 17 00:00:00 2001 From: Danny Sepler Date: Mon, 21 Sep 2020 23:35:09 -0400 Subject: [PATCH 40/46] unindexed first --- xonsh/prompt/vc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xonsh/prompt/vc.py b/xonsh/prompt/vc.py index c89ecfa11..3e2a37374 100644 --- a/xonsh/prompt/vc.py +++ b/xonsh/prompt/vc.py @@ -179,7 +179,7 @@ def _git_dirty_working_directory(q, include_untracked): else: unindexed = ["git", "diff", "--no-ext-diff", "--quiet"] indexed = unindexed + ["--cached", "HEAD"] - cmd = indexed + ["||"] + unindexed + cmd = unindexed + ["||"] + indexed child = subprocess.run(cmd, stderr=subprocess.DEVNULL, env=denv) # "--quiet" git commands imply "--exit-code", which returns: # 1 if there are differences From c36482584a6828bb1bc6ef050681c379a29f2267 Mon Sep 17 00:00:00 2001 From: Gyuri Horak Date: Tue, 22 Sep 2020 09:45:10 +0200 Subject: [PATCH 41/46] removing unnecessary `write_ansi_osc` function --- xonsh/ptk_shell/shell.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/xonsh/ptk_shell/shell.py b/xonsh/ptk_shell/shell.py index 0d54ab661..ae38cfffd 100644 --- a/xonsh/ptk_shell/shell.py +++ b/xonsh/ptk_shell/shell.py @@ -91,10 +91,6 @@ def remove_ansi_osc(prompt): return prompt, osc_tokens -def write_ansi_osc(ansi_osc_code): - print(ansi_osc_code, file=sys.__stdout__, flush=True) - - class PromptToolkitShell(BaseShell): """The xonsh shell for prompt_toolkit v2 and later.""" @@ -280,10 +276,9 @@ class PromptToolkitShell(BaseShell): # handle OSC tokens for osc in osc_tokens: if osc[2:4] == "0;": - # set $TITLE builtins.__xonsh__.env["TITLE"] = osc[4:-1] else: - write_ansi_osc(osc) + print(osc, file=sys.__stdout__, flush=True) self.settitle() return tokenize_ansi(PygmentsTokens(toks)) From 3cbf4d106f604c01105ada9af776322dd80ceaf9 Mon Sep 17 00:00:00 2001 From: anki-code Date: Tue, 22 Sep 2020 10:54:39 +0300 Subject: [PATCH 42/46] xontrib description improvement --- xonsh/xontribs.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/xonsh/xontribs.json b/xonsh/xontribs.json index e53399506..cbdce2dde 100644 --- a/xonsh/xontribs.json +++ b/xonsh/xontribs.json @@ -20,7 +20,7 @@ {"name": "argcomplete", "package": "xontrib-argcomplete", "url": "https://github.com/anki-code/xontrib-argcomplete", - "description": ["Adding support of kislyuk/argcomplete to xonsh."] + "description": ["Argcomplete support to tab completion of python and xonsh scripts in xonsh."] }, {"name": "autojump", "package": "xontrib-autojump", @@ -222,7 +222,7 @@ {"name": "pipeliner", "package": "xontrib-pipeliner", "url": "https://github.com/anki-code/xontrib-pipeliner", - "description": ["Easily process the lines using pipes."] + "description": ["Let your pipe lines flow thru the Python code in xonsh."] }, {"name": "vox", "package": "xonsh", From 3966beaa4bdf34d59f8c1c9b8b986119d0f8fe44 Mon Sep 17 00:00:00 2001 From: a Date: Tue, 22 Sep 2020 10:59:55 +0300 Subject: [PATCH 43/46] news --- news/xontrib_descr.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 news/xontrib_descr.rst diff --git a/news/xontrib_descr.rst b/news/xontrib_descr.rst new file mode 100644 index 000000000..9ff290e23 --- /dev/null +++ b/news/xontrib_descr.rst @@ -0,0 +1,23 @@ +**Added:** + +* + +**Changed:** + +* xontrib-argcomplete and xontrib-pipeliner description improvement. + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* From cd1f7edddeb8990913a2db4dfb08976fd8e209a5 Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Tue, 22 Sep 2020 13:03:41 +0100 Subject: [PATCH 44/46] Reformat with Black. --- xonsh/history/base.py | 2 +- xonsh/history/main.py | 19 +++++++++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/xonsh/history/base.py b/xonsh/history/base.py index 70f7d40e8..3a0ef8ff9 100644 --- a/xonsh/history/base.py +++ b/xonsh/history/base.py @@ -157,6 +157,6 @@ class History: def clear(self): """Clears the history of the current session from both the disk and - memory. + memory. """ pass diff --git a/xonsh/history/main.py b/xonsh/history/main.py index 6a3468561..d2c76ce73 100644 --- a/xonsh/history/main.py +++ b/xonsh/history/main.py @@ -224,8 +224,18 @@ def _XH_HISTORY_SESSIONS(): } -_XH_MAIN_ACTIONS = {"show", "id", "file", "info", "diff", "gc", "flush", - "off", "on", "clear"} +_XH_MAIN_ACTIONS = { + "show", + "id", + "file", + "info", + "diff", + "gc", + "flush", + "off", + "on", + "clear", +} @functools.lru_cache() @@ -359,8 +369,9 @@ def _xh_create_parser(): subp.add_parser("off", help="history will not be saved for this session") # 'on' subcommand - subp.add_parser("on", help="history will be saved for the rest of this" - " session (default)") + subp.add_parser( + "on", help="history will be saved for the rest of the session (default)" + ) # 'clear' subcommand subp.add_parser("clear", help="one-time wipe of session history") From 5fe1eeb60fa27046f5aef45b2032bff951217b23 Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Tue, 22 Sep 2020 14:34:37 +0100 Subject: [PATCH 45/46] Reformat json.py with Black. --- xonsh/history/json.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/xonsh/history/json.py b/xonsh/history/json.py index 7a4dd7efa..2ac8ac780 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -202,7 +202,12 @@ class JsonHistoryGC(threading.Thread): # info: file size, closing timestamp, number of commands, filename ts = lj.get("ts", (0.0, None)) files.append( - (ts[1] or ts[0], len(lj.sizes["cmds"]) - 1, f, cur_file_size,), + ( + ts[1] or ts[0], + len(lj.sizes["cmds"]) - 1, + f, + cur_file_size, + ), ) lj.close() if xonsh_debug: From ae02abe0c991e269e2677901165c2f2eb17bee5b Mon Sep 17 00:00:00 2001 From: Eadaen1 Date: Tue, 22 Sep 2020 16:58:42 +0100 Subject: [PATCH 46/46] Revert "Reformat json.py with Black." This reverts commit 5fe1eeb60fa27046f5aef45b2032bff951217b23. --- xonsh/history/json.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/xonsh/history/json.py b/xonsh/history/json.py index 2ac8ac780..7a4dd7efa 100644 --- a/xonsh/history/json.py +++ b/xonsh/history/json.py @@ -202,12 +202,7 @@ class JsonHistoryGC(threading.Thread): # info: file size, closing timestamp, number of commands, filename ts = lj.get("ts", (0.0, None)) files.append( - ( - ts[1] or ts[0], - len(lj.sizes["cmds"]) - 1, - f, - cur_file_size, - ), + (ts[1] or ts[0], len(lj.sizes["cmds"]) - 1, f, cur_file_size,), ) lj.close() if xonsh_debug: