From be4887fea7acf77b0d53f8a42df7988150bc2ded 2021-07-31 13:48:14 From: Matthias Bussonnier Date: 2021-07-31 13:48:14 Subject: [PATCH] Merge pull request #13049 from MrMino/empty_histrange_means_all Accept empty history ranges --- diff --git a/IPython/core/history.py b/IPython/core/history.py index 08db18f..ff931d2 100644 --- a/IPython/core/history.py +++ b/IPython/core/history.py @@ -445,8 +445,11 @@ class HistoryAccessor(HistoryAccessorBase): Parameters ---------- rangestr : str - A string specifying ranges, e.g. "5 ~2/1-4". See - :func:`magic_history` for full details. + A string specifying ranges, e.g. "5 ~2/1-4". If empty string is used, + this will return everything from current session's history. + + See the documentation of :func:`%history` for the full details. + raw, output : bool As :meth:`get_range` @@ -851,11 +854,18 @@ $""", re.VERBOSE) def extract_hist_ranges(ranges_str): """Turn a string of history ranges into 3-tuples of (session, start, stop). + Empty string results in a `[(0, 1, None)]`, i.e. "everything from current + session". + Examples -------- >>> list(extract_hist_ranges("~8/5-~7/4 2")) [(-8, 5, None), (-7, 1, 5), (0, 2, 3)] """ + if ranges_str == "": + yield (0, 1, None) # Everything from current session + return + for range_str in ranges_str.split(): rmatch = range_re.match(range_str) if not rmatch: diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py index f6145d5..499d6b7 100644 --- a/IPython/core/interactiveshell.py +++ b/IPython/core/interactiveshell.py @@ -3696,12 +3696,15 @@ class InteractiveShell(SingletonConfigurable): Parameters ---------- - range_str : string + range_str : str The set of slices is given as a string, like "~5/6-~4/2 4:8 9", since this function is for use by magic functions which get their arguments as strings. The number before the / is the session number: ~n goes n back from the current session. + If empty string is given, returns history of current session + without the last input. + raw : bool, optional By default, the processed input is used. If this is true, the raw input history is used instead. @@ -3715,7 +3718,16 @@ class InteractiveShell(SingletonConfigurable): * ``N-M`` -> include items N..M (closed endpoint). """ lines = self.history_manager.get_range_by_str(range_str, raw=raw) - return "\n".join(x for _, _, x in lines) + text = "\n".join(x for _, _, x in lines) + + # Skip the last line, as it's probably the magic that called this + if not range_str: + if "\n" not in text: + text = "" + else: + text = text[: text.rfind("\n")] + + return text def find_user_code(self, target, raw=True, py_only=False, skip_encoding_cookie=True, search_ns=False): """Get a code string from history, file, url, or a string or macro. @@ -3724,14 +3736,15 @@ class InteractiveShell(SingletonConfigurable): Parameters ---------- - target : str - A string specifying code to retrieve. This will be tried respectively as: ranges of input history (see %history for syntax), url, corresponding .py file, filename, or an expression evaluating to a string or Macro in the user namespace. + If empty string is given, returns complete history of current + session, without the last line. + raw : bool If true (default), retrieve raw history. Has no effect on the other retrieval mechanisms. diff --git a/IPython/core/magics/code.py b/IPython/core/magics/code.py index 82ce71b..e4b85e3 100644 --- a/IPython/core/magics/code.py +++ b/IPython/core/magics/code.py @@ -202,6 +202,9 @@ class CodeMagics(Magics): This function uses the same syntax as %history for input ranges, then saves the lines to the filename you specify. + If no ranges are specified, saves history of the current session up to + this point. + It adds a '.py' extension to the file if you don't do so yourself, and it asks for confirmation before overwriting existing files. @@ -254,6 +257,9 @@ class CodeMagics(Magics): The argument can be an input history range, a filename, or the name of a string or macro. + If no arguments are given, uploads the history of this session up to + this point. + Options: -d: Pass a custom description. The default will say @@ -315,6 +321,9 @@ class CodeMagics(Magics): where source can be a filename, URL, input history range, macro, or element in the user namespace + If no arguments are given, loads the history of this session up to this + point. + Options: -r : Specify lines or ranges of lines to load from the source. @@ -333,6 +342,7 @@ class CodeMagics(Magics): confirmation before loading source with more than 200 000 characters, unless -y flag is passed or if the frontend does not support raw_input:: + %load %load myscript.py %load 7-27 %load myMacro @@ -344,13 +354,7 @@ class CodeMagics(Magics): %load -n my_module.wonder_function """ opts,args = self.parse_options(arg_s,'yns:r:') - - if not args: - raise UsageError('Missing filename, URL, input history range, ' - 'macro, or element in the user namespace.') - search_ns = 'n' in opts - contents = self.shell.find_user_code(args, search_ns=search_ns) if 's' in opts: diff --git a/IPython/core/magics/history.py b/IPython/core/magics/history.py index 6703f26..28f91fa 100644 --- a/IPython/core/magics/history.py +++ b/IPython/core/magics/history.py @@ -184,14 +184,12 @@ class HistoryMagics(Magics): n = 10 if limit is None else limit hist = history_manager.get_tail(n, raw=raw, output=get_output) else: - if args.range: # Get history by ranges - if args.pattern: - range_pattern = "*" + " ".join(args.pattern) + "*" - print_nums = True - hist = history_manager.get_range_by_str(" ".join(args.range), - raw, get_output) - else: # Just get history for the current session - hist = history_manager.get_range(raw=raw, output=get_output) + if args.pattern: + range_pattern = "*" + " ".join(args.pattern) + "*" + print_nums = True + hist = history_manager.get_range_by_str( + " ".join(args.range), raw, get_output + ) # We could be displaying the entire history, so let's not try to pull # it into a list in memory. Anything that needs more space will just diff --git a/IPython/core/magics/osm.py b/IPython/core/magics/osm.py index c2d3963..c0cb209 100644 --- a/IPython/core/magics/osm.py +++ b/IPython/core/magics/osm.py @@ -806,18 +806,17 @@ class OSMagics(Magics): to be Python source and will show it with syntax highlighting. This magic command can either take a local filename, an url, - an history range (see %history) or a macro as argument :: + an history range (see %history) or a macro as argument. + + If no parameter is given, prints out history of current session up to + this point. :: %pycat myscript.py %pycat 7-27 %pycat myMacro %pycat http://www.example.com/myscript.py """ - if not parameter_s: - raise UsageError('Missing filename, URL, input history range, ' - 'or macro.') - - try : + try: cont = self.shell.find_user_code(parameter_s, skip_encoding_cookie=False) except (ValueError, IOError): print("Error: no such file, variable, URL, history range or macro") diff --git a/IPython/core/tests/test_history.py b/IPython/core/tests/test_history.py index 57266e5..824ba1e 100644 --- a/IPython/core/tests/test_history.py +++ b/IPython/core/tests/test_history.py @@ -160,6 +160,14 @@ def test_extract_hist_ranges(): actual = list(extract_hist_ranges(instr)) nt.assert_equal(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)) + nt.assert_equal(actual, expected) + + def test_magic_rerun(): """Simple test for %rerun (no args -> rerun last line)""" ip = get_ipython() diff --git a/IPython/core/tests/test_magic.py b/IPython/core/tests/test_magic.py index 226c460..fd5696d 100644 --- a/IPython/core/tests/test_magic.py +++ b/IPython/core/tests/test_magic.py @@ -1089,6 +1089,29 @@ def test_save(): nt.assert_in("coding: utf-8", content) +def test_save_with_no_args(): + ip = get_ipython() + ip.history_manager.reset() # Clear any existing history. + cmds = [u"a=1", u"def b():\n return a**2", u"print(a, b())", "%save"] + for i, cmd in enumerate(cmds, start=1): + ip.history_manager.store_inputs(i, cmd) + + with TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "testsave.py") + ip.run_line_magic("save", path) + content = Path(path).read_text() + expected_content = dedent( + """\ + # coding: utf-8 + a=1 + def b(): + return a**2 + print(a, b()) + """ + ) + nt.assert_equal(content, expected_content) + + def test_store(): """Test %store.""" ip = get_ipython() diff --git a/docs/source/whatsnew/pr/empty-hist-range.rst b/docs/source/whatsnew/pr/empty-hist-range.rst new file mode 100644 index 0000000..a36789f --- /dev/null +++ b/docs/source/whatsnew/pr/empty-hist-range.rst @@ -0,0 +1,19 @@ +Empty History Ranges +==================== + +A number of magics that take history ranges can now be used with an empty +range. These magics are: + + * ``%save`` + * ``%load`` + * ``%pastebin`` + * ``%pycat`` + +Using them this way will make them take the history of the current session up +to the point of the magic call (such that the magic itself will not be +included). + +Therefore it is now possible to save the whole history to a file using simple +``%save ``, load and edit it using ``%load`` (makes for a nice usage +when followed with :kbd:`F2`), send it to dpaste.org using ``%pastebin``, or +view the whole thing syntax-highlighted with a single ``%pycat``.