Adapt magic commands to new history system. #261

Closed
wants to merge 9 commits into
from
View
120 IPython/core/history.py
@@ -54,6 +54,10 @@ class HistoryManager(object):
# ShadowHist instance with the actual shadow history
shadow_hist = None
+ # Offset so the first line of the current session is #1. Can be
+ # updated after loading history from file.
+ session_offset = -1
+
# Private interface
# Variables used to store the three last inputs from the user. On each new
# history update, we populate the user's namespace with these, shifted as
@@ -65,8 +69,15 @@ class HistoryManager(object):
# call).
_exit_commands = None
- def __init__(self, shell):
+ def __init__(self, shell, load_history=False):
"""Create a new history manager associated with a shell instance.
+
@fperez
IPython member
fperez added a line comment Feb 9, 2011

Docstring should be written with

Parameters
---------------
load_history : bool
 ....

as per our doc guidelines.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ Parameters
+ ----------
+ load_history: bool, optional
+ If True, history will be loaded from file, and the session
+ offset set, so that the next line entered can be retrieved
+ as #1.
"""
# We need a pointer back to the shell for various tasks.
self.shell = shell
@@ -104,8 +115,9 @@ def __init__(self, shell):
# Object is fully initialized, we can now call methods on it.
- # Fill the history zero entry, user counter starts at 1
- self.store_inputs('\n', '\n')
+ if load_history:
+ self.reload_history()
+ self.session_offset = len(self.input_hist_raw) -1
# Create and start the autosaver.
self.autosave_flag = threading.Event()
@@ -114,6 +126,7 @@ def __init__(self, shell):
# Register the autosave handler to be triggered as a post execute
# callback.
self.shell.register_post_execute(self.autosave_if_due)
+
def _init_shadow_hist(self):
try:
@@ -172,53 +185,60 @@ def reload_history(self):
if self.shell.has_readline:
self.populate_readline_history()
- def get_history(self, index=None, raw=False, output=True):
+ def get_history(self, index=None, raw=False, output=True,this_session=True):
"""Get the history list.
Get the input and output history.
Parameters
----------
index : n or (n1, n2) or None
- If n, then the last entries. If a tuple, then all in
+ If n, then the last n entries. If a tuple, then all in
range(n1, n2). If None, then all entries. Raises IndexError if
the format of index is incorrect.
raw : bool
If True, return the raw input.
output : bool
If True, then return the output as well.
+ this_session : bool
+ If True, indexing is from 1 at the start of this session.
+ If False, indexing is from 1 at the start of the whole history.
Returns
-------
If output is True, then return a dict of tuples, keyed by the prompt
numbers and with values of (input, output). If output is False, then
- a dict, keyed by the prompt number with the values of input. Raises
- IndexError if no history is found.
+ a dict, keyed by the prompt number with the values of input.
"""
if raw:
input_hist = self.input_hist_raw
else:
input_hist = self.input_hist_parsed
if output:
output_hist = self.output_hist
+
+ if this_session:
+ offset = self.session_offset
+ else:
+ offset = -1
+
n = len(input_hist)
if index is None:
- start=0; stop=n
+ start=offset+1; stop=n
elif isinstance(index, int):
start=n-index; stop=n
- elif isinstance(index, tuple) and len(index) == 2:
- start=index[0]; stop=index[1]
+ elif len(index) == 2:
+ start = index[0] + offset
+ stop = index[1] + offset
else:
raise IndexError('Not a valid index for the input history: %r'
% index)
hist = {}
for i in range(start, stop):
if output:
- hist[i] = (input_hist[i], output_hist.get(i))
+ hist[i-offset] = (input_hist[i], output_hist.get(i-offset))
else:
- hist[i] = input_hist[i]
- if not hist:
@fperez
IPython member
fperez added a line comment Feb 9, 2011

Why was this if statement removed? Can the condition not happen anymore?

@takluyver
IPython member
takluyver added a line comment Feb 9, 2011

It couldn't happen before, because of the blank first entry in the hist lists (they started as [''], which evaluates as True). With these changes, it can happen if you request the entire history (using index=None) when it's empty. The Qt console requests the entire history when it starts up. To my mind, it makes more sense to return the empty list in that case than to raise an IndexError. The IndexError isn't currently caught anywhere, so it crashes the kernel.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
- raise IndexError('No history for range of indices: %r' % index)
+ hist[i-offset] = input_hist[i]
return hist
def store_inputs(self, source, source_raw=None):
@@ -271,6 +291,8 @@ def reset(self):
self.output_hist.clear()
# The directory history can't be completely empty
self.dir_hist[:] = [os.getcwd()]
+ # Reset session offset to -1, so next command counts as #1
+ self.session_offset = -1
class HistorySaveThread(threading.Thread):
"""This thread makes IPython save history periodically.
@@ -353,6 +375,9 @@ def magic_history(self, parameter_s = ''):
print('This feature is only available if numbered prompts are in use.')
return
opts,args = self.parse_options(parameter_s,'gnoptsrf:',mode='list')
+
+ # For brevity
+ history_manager = self.shell.history_manager
# Check if output to specific file was requested.
try:
@@ -369,47 +394,41 @@ def magic_history(self, parameter_s = ''):
outfile = open(outfname,'w')
close_at_end = True
-
- if 't' in opts:
- input_hist = self.shell.history_manager.input_hist_parsed
- elif 'r' in opts:
- input_hist = self.shell.history_manager.input_hist_raw
- else:
- # Raw history is the default
- input_hist = self.shell.history_manager.input_hist_raw
+
+ print_nums = 'n' in opts
+ print_outputs = 'o' in opts
+ pyprompts = 'p' in opts
+ # Raw history is the default
+ raw = not('t' in opts)
default_length = 40
pattern = None
if 'g' in opts:
- init = 1
- final = len(input_hist)
+ index = None
parts = parameter_s.split(None, 1)
if len(parts) == 1:
parts += '*'
head, pattern = parts
pattern = "*" + pattern + "*"
elif len(args) == 0:
- final = len(input_hist)-1
- init = max(1,final-default_length)
+ index = None
elif len(args) == 1:
- final = len(input_hist)
- init = max(1, final-int(args[0]))
+ index = int(args[0])
elif len(args) == 2:
- init, final = map(int, args)
+ index = map(int, args)
else:
warn('%hist takes 0, 1 or 2 arguments separated by spaces.')
print(self.magic_hist.__doc__, file=IPython.utils.io.Term.cout)
return
+
+ hist = history_manager.get_history(index, raw, print_outputs)
- width = len(str(final))
+ width = len(str(max(hist.iterkeys())))
line_sep = ['','\n']
- print_nums = 'n' in opts
- print_outputs = 'o' in opts
- pyprompts = 'p' in opts
found = False
if pattern is not None:
- sh = self.shell.history_manager.shadowhist.all()
+ sh = history_manager.shadow_hist.all()
for idx, s in sh:
if fnmatch.fnmatch(s, pattern):
print("0%d: %s" %(idx, s.expandtabs(4)), file=outfile)
@@ -421,41 +440,34 @@ def magic_history(self, parameter_s = ''):
file=outfile)
print("=== start of normal history ===", file=outfile)
- for in_num in range(init, final):
+ for in_num, inline in sorted(hist.iteritems()):
# Print user history with tabs expanded to 4 spaces. The GUI clients
# use hard tabs for easier usability in auto-indented code, but we want
# to produce PEP-8 compliant history for safe pasting into an editor.
- inline = input_hist[in_num].expandtabs(4).rstrip()+'\n'
+ if print_outputs:
+ inline, output = inline
+ inline = inline.expandtabs(4).rstrip()
if pattern is not None and not fnmatch.fnmatch(inline, pattern):
continue
- multiline = int(inline.count('\n') > 1)
+ multiline = "\n" in inline
if print_nums:
print('%s:%s' % (str(in_num).ljust(width), line_sep[multiline]),
- file=outfile)
+ file=outfile, end='')
if pyprompts:
- print('>>>', file=outfile)
+ print(">>> ", end="", file=outfile)
if multiline:
- lines = inline.splitlines()
- print('\n... '.join(lines), file=outfile)
- print('... ', file=outfile)
- else:
- print(inline, end='', file=outfile)
- else:
- print(inline, end='', file=outfile)
- if print_outputs:
- output = self.shell.history_manager.output_hist.get(in_num)
- if output is not None:
- print(repr(output), file=outfile)
+ inline = "\n... ".join(inline.splitlines()) + "\n..."
+ print(inline, file=outfile)
+ if print_outputs and output:
+ print(repr(output), file=outfile)
if close_at_end:
outfile.close()
-
-def magic_hist(self, parameter_s=''):
- """Alternate name for %history."""
- return self.magic_history(parameter_s)
+# %hist is an alternative name
+magic_hist = magic_history
def rep_f(self, arg):
View
13 IPython/core/interactiveshell.py
@@ -1248,7 +1248,7 @@ def object_inspect(self, oname):
def init_history(self):
"""Sets up the command history, and starts regular autosaves."""
- self.history_manager = HistoryManager(shell=self)
+ self.history_manager = HistoryManager(shell=self, load_history=True)
def save_history(self):
"""Save input history to a file (via readline library)."""
@@ -1277,8 +1277,8 @@ def wrapper():
self.reload_history()
return wrapper
- def get_history(self, index=None, raw=False, output=True):
- return self.history_manager.get_history(index, raw, output)
+ def get_history(self, index=None, raw=False, output=True,this_session=True):
+ return self.history_manager.get_history(index, raw, output,this_session)
#-------------------------------------------------------------------------
@@ -1560,11 +1560,8 @@ def init_readline(self):
readline.set_completer_delims(delims)
# otherwise we end up with a monster history after a while:
readline.set_history_length(self.history_length)
- try:
- #print '*** Reading readline history' # dbg
- self.reload_history()
- except IOError:
- pass # It doesn't exist yet.
+
+ self.history_manager.populate_readline_history()
# Configure auto-indent for all platforms
self.set_autoindent(self.autoindent)
View
4 IPython/core/macro.py
@@ -19,9 +19,9 @@ class Macro(IPyAutocall):
Args to macro are available in _margv list if you need them.
"""
- def __init__(self,data):
+ def __init__(self,code):
"""store the macro value, as a single string which can be executed"""
- self.value = ''.join(data).rstrip()+'\n'
+ self.value = code.rstrip()+'\n'
def __str__(self):
return self.value
View
57 IPython/core/magic.py
@@ -57,7 +57,7 @@
from IPython.utils.path import get_py_filename
from IPython.utils.process import arg_split, abbrev_cwd
from IPython.utils.terminal import set_term_title
-from IPython.utils.text import LSString, SList, StringTypes, format_screen
+from IPython.utils.text import LSString, SList, format_screen
from IPython.utils.timing import clock, clock2
from IPython.utils.warn import warn, error
from IPython.utils.ipstruct import Struct
@@ -184,11 +184,7 @@ def extract_input_slices(self,slices,raw=False):
N:M -> standard python form, means including items N...(M-1).
N-M -> include items N..M (closed endpoint)."""
-
- if raw:
- hist = self.shell.history_manager.input_hist_raw
- else:
- hist = self.shell.history_manager.input_hist_parsed
+ history_manager = self.shell.history_manager
cmds = []
for chunk in slices:
@@ -200,7 +196,8 @@ def extract_input_slices(self,slices,raw=False):
else:
ini = int(chunk)
fin = ini+1
- cmds.append(''.join(hist[ini:fin]))
+ hist = history_manager.get_history((ini,fin), raw=raw, output=False)
+ cmds.append('\n'.join(hist[i] for i in sorted(hist.iterkeys())))
return cmds
def arg_err(self,func):
@@ -1967,18 +1964,17 @@ def magic_macro(self,parameter_s = ''):
In [60]: exec In[44:48]+In[49]"""
opts,args = self.parse_options(parameter_s,'r',mode='list')
- if not args:
- macs = [k for k,v in self.shell.user_ns.items() if isinstance(v, Macro)]
- macs.sort()
- return macs
+ if not args: # List existing macros
+ return sorted(k for k,v in self.shell.user_ns.iteritems() if\
+ isinstance(v, Macro))
if len(args) == 1:
raise UsageError(
"%macro insufficient args; usage '%macro name n1-n2 n3-4...")
name,ranges = args[0], args[1:]
#print 'rng',ranges # dbg
- lines = self.extract_input_slices(ranges,opts.has_key('r'))
- macro = Macro(lines)
+ lines = self.extract_input_slices(ranges,'r' in opts)
+ macro = Macro("\n".join(lines))
self.shell.define_macro(name, macro)
print 'Macro `%s` created. To execute, type its name (without quotes).' % name
print 'Macro contents:'
@@ -2013,10 +2009,9 @@ def magic_save(self,parameter_s = ''):
if ans.lower() not in ['y','yes']:
print 'Operation cancelled.'
return
- cmds = ''.join(self.extract_input_slices(ranges,opts.has_key('r')))
- f = file(fname,'w')
- f.write(cmds)
- f.close()
+ cmds = '\n'.join(self.extract_input_slices(ranges, 'r' in opts))
+ with open(fname,'w') as f:
+ f.write(cmds)
print 'The following commands were written to file `%s`:' % fname
print cmds
@@ -2222,26 +2217,26 @@ class DataIsObject(Exception): pass
# by default this is done with temp files, except when the given
# arg is a filename
- use_temp = 1
+ use_temp = True
- if re.match(r'\d',args):
+ data = ''
+ if args[0].isdigit():
# Mode where user specifies ranges of lines, like in %macro.
# This means that you can't edit files whose names begin with
# numbers this way. Tough.
ranges = args.split()
- data = ''.join(self.extract_input_slices(ranges,opts_r))
+ data = '\n'.join(self.extract_input_slices(ranges,opts_r))
elif args.endswith('.py'):
filename = make_filename(args)
- data = ''
- use_temp = 0
+ use_temp = False
elif args:
try:
# Load the parameter given as a variable. If not a string,
# process it as an object instead (below)
#print '*** args',args,'type',type(args) # dbg
- data = eval(args,self.shell.user_ns)
- if not type(data) in StringTypes:
+ data = eval(args, self.shell.user_ns)
+ if not isinstance(data, basestring):
raise DataIsObject
except (NameError,SyntaxError):
@@ -2251,13 +2246,11 @@ class DataIsObject(Exception): pass
warn("Argument given (%s) can't be found as a variable "
"or as a filename." % args)
return
-
- data = ''
- use_temp = 0
+ use_temp = False
+
except DataIsObject:
-
# macros have a special edit function
- if isinstance(data,Macro):
+ if isinstance(data, Macro):
self._edit_macro(args,data)
return
@@ -2296,9 +2289,7 @@ class DataIsObject(Exception): pass
warn('The file `%s` where `%s` was defined cannot '
'be read.' % (filename,data))
return
- use_temp = 0
- else:
- data = ''
+ use_temp = False
if use_temp:
filename = self.shell.mktempfile(data)
@@ -2321,7 +2312,7 @@ class DataIsObject(Exception): pass
if args.strip() == 'pasted_block':
self.shell.user_ns['pasted_block'] = file_read(filename)
- if opts.has_key('x'): # -x prevents actual execution
+ if 'x' in opts: # -x prevents actual execution
print
else:
print 'done. Executing edited code...'
View
21 IPython/core/tests/test_history.py
@@ -30,11 +30,11 @@ def test_history():
ip.history_manager = HistoryManager(ip)
ip.history_manager.hist_file = histfile
print 'test',histfile
- hist = ['a=1\n', 'def f():\n test = 1\n return test\n', 'b=2\n']
+ hist = ['a=1', 'def f():\n test = 1\n return test', 'b=2']
# test save and load
ip.history_manager.input_hist_raw[:] = []
for h in hist:
- ip.history_manager.input_hist_raw.append(h)
+ ip.history_manager.store_inputs(h)
ip.save_history()
ip.history_manager.input_hist_raw[:] = []
ip.reload_history()
@@ -43,6 +43,23 @@ def test_history():
nt.assert_equal(len(ip.history_manager.input_hist_raw), len(hist))
for i,h in enumerate(hist):
nt.assert_equal(hist[i], ip.history_manager.input_hist_raw[i])
+
+ # Test that session offset works.
+ ip.history_manager.session_offset = \
+ len(ip.history_manager.input_hist_raw) -1
+ newcmds = ["z=5","class X(object):\n pass", "k='p'"]
+ for cmd in newcmds:
+ ip.history_manager.store_inputs(cmd)
+ gothist = ip.history_manager.get_history((1,4),
+ raw=True, output=False)
+ nt.assert_equal(gothist, dict(zip([1,2,3], newcmds)))
+
+ # Cross testing: check that magic %save picks up on the session
+ # offset.
+ testfilename = os.path.realpath(os.path.join(tmpdir, "test.py"))
+ ip.magic_save(testfilename + " 1-3")
+ testfile = open(testfilename, "r")
+ nt.assert_equal(testfile.read(), "\n".join(newcmds))
finally:
# Restore history manager
ip.history_manager = hist_manager_ori
View
12 IPython/core/tests/test_magic.py
@@ -173,6 +173,18 @@ def test_shist():
yield nt.assert_equal,s.get(2),'world'
shutil.rmtree(tfile)
+
+def test_macro():
+ ip = get_ipython()
+ ip.history_manager.reset() # Clear any existing history.
+ cmds = ["a=1", "def b():\n return a**2", "print(a,b())"]
+ for cmd in cmds:
+ ip.history_manager.store_inputs(cmd)
+ ip.magic("macro test 1-3")
+ nt.assert_equal(ip.user_ns["test"].value, "\n".join(cmds)+"\n")
+
+ # List macros.
+ assert "test" in ip.magic("macro")
# XXX failing for now, until we get clearcmd out of quarantine. But we should
View
3 IPython/frontend/qt/console/ipython_widget.py
@@ -217,7 +217,8 @@ def _started_channels(self):
""" Reimplemented to make a history request.
"""
super(IPythonWidget, self)._started_channels()
- self.kernel_manager.xreq_channel.history(raw=True, output=False)
+ self.kernel_manager.xreq_channel.history(raw=True, output=False,
+ this_session=False)
#---------------------------------------------------------------------------
# 'ConsoleWidget' public interface
View
7 IPython/utils/text.py
@@ -19,7 +19,6 @@
import os
import re
import shutil
-import types
from IPython.external.path import path
@@ -30,8 +29,6 @@
# Code
#-----------------------------------------------------------------------------
-StringTypes = types.StringTypes
-
def unquote_ends(istr):
"""Remove a single pair of quotes from the endpoints of a string."""
@@ -325,7 +322,7 @@ def qw(words,flat=0,sep=None,maxsplit=-1):
['a', 'b', '1', '2', 'm', 'n', 'p', 'q']
"""
- if type(words) in StringTypes:
+ if isinstance(words, basestring):
return [word.strip() for word in words.split(sep,maxsplit)
if word and not word.isspace() ]
if flat:
@@ -345,7 +342,7 @@ def qw_lol(indata):
We need this to make sure the modules_some keys *always* end up as a
list of lists."""
- if type(indata) in StringTypes:
+ if isinstance(indata, basestring):
return [qw(indata)]
else:
return qw(indata)
View
7 IPython/zmq/ipkernel.py
@@ -301,10 +301,9 @@ def object_info_request(self, ident, parent):
io.raw_print(msg)
def history_request(self, ident, parent):
- output = parent['content']['output']
- index = parent['content']['index']
- raw = parent['content']['raw']
- hist = self.shell.get_history(index=index, raw=raw, output=output)
+ # parent['content'] should contain keys "index", "raw", "output" and
+ # "this_session".
+ hist = self.shell.get_history(**parent['content'])
content = {'history' : hist}
msg = self.session.send(self.reply_socket, 'history_reply',
content, parent, ident)
View
8 IPython/zmq/kernelmanager.py
@@ -281,7 +281,7 @@ def object_info(self, oname):
self._queue_request(msg)
return msg['header']['msg_id']
- def history(self, index=None, raw=False, output=True):
+ def history(self, index=None, raw=False, output=True, this_session=True):
"""Get the history list.
Parameters
@@ -294,12 +294,16 @@ def history(self, index=None, raw=False, output=True):
If True, return the raw input.
output : bool
If True, then return the output as well.
+ this_session : bool
+ If True, returns only history from the current session. Otherwise,
+ includes reloaded history from previous sessions.
Returns
-------
The msg_id of the message sent.
"""
- content = dict(index=index, raw=raw, output=output)
+ content = dict(index=index, raw=raw, output=output,
+ this_session=this_session)
msg = self.session.msg('history_request', content)
self._queue_request(msg)
return msg['header']['msg_id']
View
26 IPython/zmq/zmqshell.py
@@ -18,7 +18,6 @@
# Stdlib
import inspect
import os
-import re
# Our own
from IPython.core.interactiveshell import (
@@ -31,7 +30,6 @@
from IPython.core.payloadpage import install_payload_page
from IPython.utils import io
from IPython.utils.path import get_py_filename
-from IPython.utils.text import StringTypes
from IPython.utils.traitlets import Instance, Type, Dict
from IPython.utils.warn import warn
from IPython.zmq.session import extract_header
@@ -433,26 +431,26 @@ class DataIsObject(Exception): pass
# by default this is done with temp files, except when the given
# arg is a filename
- use_temp = 1
+ use_temp = True
- if re.match(r'\d',args):
+ data = ''
+ if args[0].isdigit():
# Mode where user specifies ranges of lines, like in %macro.
# This means that you can't edit files whose names begin with
# numbers this way. Tough.
ranges = args.split()
data = ''.join(self.extract_input_slices(ranges,opts_r))
elif args.endswith('.py'):
filename = make_filename(args)
- data = ''
- use_temp = 0
+ use_temp = False
elif args:
try:
# Load the parameter given as a variable. If not a string,
# process it as an object instead (below)
#print '*** args',args,'type',type(args) # dbg
- data = eval(args,self.shell.user_ns)
- if not type(data) in StringTypes:
+ data = eval(args, self.shell.user_ns)
+ if not isinstance(data, basestring):
raise DataIsObject
except (NameError,SyntaxError):
@@ -462,13 +460,11 @@ class DataIsObject(Exception): pass
warn("Argument given (%s) can't be found as a variable "
"or as a filename." % args)
return
-
- data = ''
- use_temp = 0
+ use_temp = False
+
except DataIsObject:
-
# macros have a special edit function
- if isinstance(data,Macro):
+ if isinstance(data, Macro):
self._edit_macro(args,data)
return
@@ -507,9 +503,7 @@ class DataIsObject(Exception): pass
warn('The file `%s` where `%s` was defined cannot '
'be read.' % (filename,data))
return
- use_temp = 0
- else:
- data = ''
+ use_temp = False
if use_temp:
filename = self.shell.mktempfile(data)