# coding: utf-8 """Tests for the IPython tab-completion machinery. """ #----------------------------------------------------------------------------- # Module imports #----------------------------------------------------------------------------- # stdlib import io import sqlite3 import sys import tempfile from datetime import datetime from pathlib import Path from tempfile import TemporaryDirectory # our own packages from traitlets.config.loader import Config from IPython.core.history import HistoryAccessor, HistoryManager, extract_hist_ranges def test_proper_default_encoding(): assert sys.getdefaultencoding() == "utf-8" def test_history(): ip = get_ipython() with TemporaryDirectory() as tmpdir: tmp_path = Path(tmpdir) hist_manager_ori = ip.history_manager hist_file = tmp_path / "history.sqlite" try: ip.history_manager = HistoryManager(shell=ip, hist_file=hist_file) hist = ["a=1", "def f():\n test = 1\n return test", "b='€Æ¾÷ß'"] for i, h in enumerate(hist, start=1): ip.history_manager.store_inputs(i, h) ip.history_manager.db_log_output = True # Doesn't match the input, but we'll just check it's stored. ip.history_manager.output_hist_reprs[3] = "spam" ip.history_manager.store_output(3) assert ip.history_manager.input_hist_raw == [""] + hist # Detailed tests for _get_range_session grs = ip.history_manager._get_range_session assert list(grs(start=2, stop=-1)) == list(zip([0], [2], hist[1:-1])) assert list(grs(start=-2)) == list(zip([0, 0], [2, 3], hist[-2:])) assert list(grs(output=True)) == list( zip([0, 0, 0], [1, 2, 3], zip(hist, [None, None, "spam"])) ) # Check whether specifying a range beyond the end of the current # session results in an error (gh-804) ip.run_line_magic("hist", "2-500") # Check that we can write non-ascii characters to a file ip.run_line_magic("hist", "-f %s" % (tmp_path / "test1")) ip.run_line_magic("hist", "-pf %s" % (tmp_path / "test2")) ip.run_line_magic("hist", "-nf %s" % (tmp_path / "test3")) ip.run_line_magic("save", "%s 1-10" % (tmp_path / "test4")) # New session ip.history_manager.reset() newcmds = ["z=5", "class X(object):\n pass", "k='p'", "z=5"] for i, cmd in enumerate(newcmds, start=1): ip.history_manager.store_inputs(i, cmd) gothist = ip.history_manager.get_range(start=1, stop=4) assert list(gothist) == list(zip([0, 0, 0], [1, 2, 3], newcmds)) # Previous session: gothist = ip.history_manager.get_range(-1, 1, 4) assert list(gothist) == list(zip([1, 1, 1], [1, 2, 3], hist)) newhist = [(2, i, c) for (i, c) in enumerate(newcmds, 1)] # Check get_hist_tail gothist = ip.history_manager.get_tail(5, output=True, include_latest=True) expected = [(1, 3, (hist[-1], "spam"))] \ + [(s, n, (c, None)) for (s, n, c) in newhist] assert list(gothist) == expected gothist = ip.history_manager.get_tail(2) expected = newhist[-3:-1] assert list(gothist) == expected # Check get_hist_search gothist = ip.history_manager.search("*test*") assert list(gothist) == [(1, 2, hist[1])] gothist = ip.history_manager.search("*=*") assert list(gothist) == [ (1, 1, hist[0]), (1, 2, hist[1]), (1, 3, hist[2]), newhist[0], newhist[2], newhist[3], ] gothist = ip.history_manager.search("*=*", n=4) assert list(gothist) == [ (1, 3, hist[2]), newhist[0], newhist[2], newhist[3], ] gothist = ip.history_manager.search("*=*", unique=True) assert list(gothist) == [ (1, 1, hist[0]), (1, 2, hist[1]), (1, 3, hist[2]), newhist[2], newhist[3], ] gothist = ip.history_manager.search("*=*", unique=True, n=3) assert list(gothist) == [(1, 3, hist[2]), newhist[2], newhist[3]] gothist = ip.history_manager.search("b*", output=True) assert list(gothist) == [(1, 3, (hist[2], "spam"))] # Cross testing: check that magic %save can get previous session. testfilename = (tmp_path / "test.py").resolve() ip.run_line_magic("save", str(testfilename) + " ~1/1-3") with io.open(testfilename, encoding="utf-8") as testfile: assert testfile.read() == "# coding: utf-8\n" + "\n".join(hist) + "\n" # Duplicate line numbers - check that it doesn't crash, and # gets a new session ip.history_manager.store_inputs(1, "rogue") ip.history_manager.writeout_cache() assert ip.history_manager.session_number == 3 # Check that session and line values are not just max values sessid, lineno, entry = newhist[-1] assert lineno > 1 ip.history_manager.reset() lineno = 1 ip.history_manager.store_inputs(lineno, entry) gothist = ip.history_manager.search("*=*", unique=True) hist = list(gothist)[-1] assert sessid < hist[0] assert hist[1:] == (lineno, entry) finally: # Ensure saving thread is shut down before we try to clean up the files ip.history_manager.save_thread.stop() # Forcibly close database rather than relying on garbage collection ip.history_manager.db.close() # Restore history manager ip.history_manager = hist_manager_ori def test_extract_hist_ranges(): instr = "1 2/3 ~4/5-6 ~4/7-~4/9 ~9/2-~7/5 ~10/" expected = [(0, 1, 2), # 0 == current session (2, 3, 4), (-4, 5, 7), (-4, 7, 10), (-9, 2, None), # None == to end (-8, 1, None), (-7, 1, 6), (-10, 1, None)] actual = list(extract_hist_ranges(instr)) assert actual == expected def test_extract_hist_ranges_empty_str(): instr = "" expected = [(0, 1, None)] # 0 == current session, None == to end actual = list(extract_hist_ranges(instr)) assert actual == expected def test_magic_rerun(): """Simple test for %rerun (no args -> rerun last line)""" ip = get_ipython() ip.run_cell("a = 10", store_history=True) ip.run_cell("a += 1", store_history=True) assert ip.user_ns["a"] == 11 ip.run_cell("%rerun", store_history=True) assert ip.user_ns["a"] == 12 def test_timestamp_type(): ip = get_ipython() info = ip.history_manager.get_session_info() assert isinstance(info[1], datetime) def test_hist_file_config(): cfg = Config() tfile = tempfile.NamedTemporaryFile(delete=False) cfg.HistoryManager.hist_file = Path(tfile.name) try: hm = HistoryManager(shell=get_ipython(), config=cfg) assert hm.hist_file == cfg.HistoryManager.hist_file finally: try: Path(tfile.name).unlink() except OSError: # same catch as in testing.tools.TempFileMixin # On Windows, even though we close the file, we still can't # delete it. I have no clue why pass def test_histmanager_disabled(): """Ensure that disabling the history manager doesn't create a database.""" cfg = Config() cfg.HistoryAccessor.enabled = False ip = get_ipython() with TemporaryDirectory() as tmpdir: hist_manager_ori = ip.history_manager hist_file = Path(tmpdir) / "history.sqlite" cfg.HistoryManager.hist_file = hist_file try: ip.history_manager = HistoryManager(shell=ip, config=cfg) hist = ["a=1", "def f():\n test = 1\n return test", "b='€Æ¾÷ß'"] for i, h in enumerate(hist, start=1): ip.history_manager.store_inputs(i, h) assert ip.history_manager.input_hist_raw == [""] + hist ip.history_manager.reset() ip.history_manager.end_session() finally: ip.history_manager = hist_manager_ori # hist_file should not be created assert hist_file.exists() is False def test_get_tail_session_awareness(): """Test .get_tail() is: - session specific in HistoryManager - session agnostic in HistoryAccessor same for .get_last_session_id() """ ip = get_ipython() with TemporaryDirectory() as tmpdir: tmp_path = Path(tmpdir) hist_file = tmp_path / "history.sqlite" get_source = lambda x: x[2] # hm1 creates a new session and adds history entries, # ha catches up hm1 = HistoryManager(shell=ip, hist_file=hist_file) hm1_last_sid = hm1.get_last_session_id ha = HistoryAccessor(hist_file=hist_file) ha_last_sid = ha.get_last_session_id hist1 = ["a=1", "b=1", "c=1"] for i, h in enumerate(hist1 + [""], start=1): hm1.store_inputs(i, h) assert list(map(get_source, hm1.get_tail())) == hist1 assert list(map(get_source, ha.get_tail())) == hist1 sid1 = hm1_last_sid() assert sid1 is not None assert ha_last_sid() == sid1 # hm2 creates a new session and adds entries, # ha catches up hm2 = HistoryManager(shell=ip, hist_file=hist_file) hm2_last_sid = hm2.get_last_session_id hist2 = ["a=2", "b=2", "c=2"] for i, h in enumerate(hist2 + [""], start=1): hm2.store_inputs(i, h) tail = hm2.get_tail(n=3) assert list(map(get_source, tail)) == hist2 tail = ha.get_tail(n=3) assert list(map(get_source, tail)) == hist2 sid2 = hm2_last_sid() assert sid2 is not None assert ha_last_sid() == sid2 assert sid2 != sid1 # but hm1 still maintains its point of reference # and adding more entries to it doesn't change others # immediate perspective assert hm1_last_sid() == sid1 tail = hm1.get_tail(n=3) assert list(map(get_source, tail)) == hist1 hist3 = ["a=3", "b=3", "c=3"] for i, h in enumerate(hist3 + [""], start=5): hm1.store_inputs(i, h) tail = hm1.get_tail(n=7) assert list(map(get_source, tail)) == hist1 + [""] + hist3 tail = hm2.get_tail(n=3) assert list(map(get_source, tail)) == hist2 tail = ha.get_tail(n=3) assert list(map(get_source, tail)) == hist2 assert hm1_last_sid() == sid1 assert hm2_last_sid() == sid2 assert ha_last_sid() == sid2