From 35e7be3dda5f0703c951d50162581e684276ad0d 2005-12-29 08:34:07 From: fperez Date: 2005-12-29 08:34:07 Subject: [PATCH] Major cleanups and changes, see changelog/changeset for full details. --- diff --git a/IPython/Logger.py b/IPython/Logger.py index 45f31cc..4deb55f 100644 --- a/IPython/Logger.py +++ b/IPython/Logger.py @@ -2,12 +2,12 @@ """ Logger class for IPython's logging facilities. -$Id: Logger.py 958 2005-12-27 23:17:51Z fperez $ +$Id: Logger.py 966 2005-12-29 08:34:07Z fperez $ """ #***************************************************************************** # Copyright (C) 2001 Janko Hauser and -# Copyright (C) 2001-2004 Fernando Perez +# Copyright (C) 2001-2005 Fernando Perez # # Distributed under the terms of the BSD License. The full license is in # the file COPYING, distributed as part of this software. @@ -24,61 +24,96 @@ __license__ = Release.license # Python standard modules import glob import os -import sys - -# Homebrewed -from IPython.genutils import * +import time #**************************************************************************** -# FIXME: The logger class shouldn't be a mixin, it throws too many things into -# the InteractiveShell namespace. Rather make it a standalone tool, and create -# a Logger instance in InteractiveShell that uses it. Doing this will require -# tracking down a *lot* of nasty uses of the Logger attributes in -# InteractiveShell, but will clean up things quite a bit. - -class Logger: - """A Logfile Mixin class with different policies for file creation""" - - # FIXME: once this isn't a mixin, log_ns should just be 'namespace', since the - # names won't collide anymore. - def __init__(self,log_ns): +# FIXME: This class isn't a mixin anymore, but it still needs attributes from +# ipython and does input cache management. Finish cleanup later... + +class Logger(object): + """A Logfile class with different policies for file creation""" + + def __init__(self,shell,logfname='Logger.log',loghead='',logmode='over'): + self._i00,self._i,self._ii,self._iii = '','','','' - self.do_full_cache = 0 # FIXME. There's also a do_full.. in OutputCache - self.log_ns = log_ns - # defaults - self.LOGMODE = 'backup' - self.defname = 'logfile' - - def create_log(self,header='',fname='',defname='.Logger.log'): - """Generate a new log-file with a default header""" - if fname: - self.LOG = fname - if self.LOG: - self.logfname = self.LOG - else: - self.logfname = defname + # this is the full ipython instance, we need some attributes from it + # which won't exist until later. What a mess, clean up later... + self.shell = shell + + self.logfname = logfname + self.loghead = loghead + self.logmode = logmode + self.logfile = None + + # whether to also log output + self.log_output = False + + # whether to put timestamps before each log entry + self.timestamp = False + + # activity control flags + self.log_active = False + + # logmode is a validated property + def _set_mode(self,mode): + if mode not in ['append','backup','global','over','rotate']: + raise ValueError,'invalid log mode %s given' % mode + self._logmode = mode + + def _get_mode(self): + return self._logmode + + logmode = property(_get_mode,_set_mode) + + def logstart(self,logfname=None,loghead=None,logmode=None, + log_output=False,timestamp=False): + """Generate a new log-file with a default header. + + Raises RuntimeError if the log has already been started""" + + if self.logfile is not None: + raise RuntimeError('Log file is already active: %s' % + self.logfname) - if self.LOGMODE == 'over': - if os.path.isfile(self.logfname): - os.remove(self.logfname) - self.logfile = open(self.logfname,'w') - if self.LOGMODE == 'backup': - if os.path.isfile(self.logfname): + self.log_active = True + + # The three parameters can override constructor defaults + if logfname: self.logfname = logfname + if loghead: self.loghead = loghead + if logmode: self.logmode = logmode + self.timestamp = timestamp + self.log_output = log_output + + # init depending on the log mode requested + isfile = os.path.isfile + logmode = self.logmode + + if logmode == 'append': + self.logfile = open(self.logfname,'a') + + elif logmode == 'backup': + if isfile(self.logfname): backup_logname = self.logfname+'~' # Manually remove any old backup, since os.rename may fail # under Windows. - if os.path.isfile(backup_logname): + if isfile(backup_logname): os.remove(backup_logname) os.rename(self.logfname,backup_logname) self.logfile = open(self.logfname,'w') - elif self.LOGMODE == 'global': - self.logfname = os.path.join(self.home_dir, self.defname) + + elif logmode == 'global': + self.logfname = os.path.join(self.shell.home_dir,self.logfname) self.logfile = open(self.logfname, 'a') - self.LOG = self.logfname - elif self.LOGMODE == 'rotate': - if os.path.isfile(self.logfname): - if os.path.isfile(self.logfname+'.001~'): + + elif logmode == 'over': + if isfile(self.logfname): + os.remove(self.logfname) + self.logfile = open(self.logfname,'w') + + elif logmode == 'rotate': + if isfile(self.logfname): + if isfile(self.logfname+'.001~'): old = glob.glob(self.logfname+'.*~') old.sort() old.reverse() @@ -88,71 +123,56 @@ class Logger: os.rename(f, root+'.'+`num`.zfill(3)+'~') os.rename(self.logfname, self.logfname+'.001~') self.logfile = open(self.logfname,'w') - elif self.LOGMODE == 'append': - self.logfile = open(self.logfname,'a') - if self.LOGMODE != 'append': - self.logfile.write(header) - self.logfile.flush() + if logmode != 'append': + self.logfile.write(self.loghead) - def logstart(self, header='',parameter_s = ''): - if not hasattr(self, 'LOG'): - logfname = self.LOG or parameter_s or './'+self.defname - self.create_log(header,logfname) - elif parameter_s and hasattr(self,'logfname') and \ - parameter_s != self.logfname: - self.close_log() - self.create_log(header,parameter_s) - - self._dolog = 1 + self.logfile.flush() def switch_log(self,val): - """Switch logging on/off. val should be ONLY 0 or 1.""" + """Switch logging on/off. val should be ONLY a boolean.""" - if not val in [0,1]: + if val not in [False,True,0,1]: raise ValueError, \ - 'Call switch_log ONLY with 0 or 1 as argument, not with:',val + 'Call switch_log ONLY with a boolean argument, not with:',val - label = {0:'OFF',1:'ON'} + label = {0:'OFF',1:'ON',False:'OFF',True:'ON'} - try: - _ = self.logfile - except AttributeError: + if self.logfile is None: print """ -Logging hasn't been started yet (use %logstart for that). +Logging hasn't been started yet (use logstart for that). %logon/%logoff are for temporarily starting and stopping logging for a logfile which already exists. But you must first start the logging process with %logstart (optionally giving a logfile name).""" else: - if self._dolog == val: + if self.log_active == val: print 'Logging is already',label[val] else: print 'Switching logging',label[val] - self._dolog = 1 - self._dolog + self.log_active = not self.log_active + self.log_active_out = self.log_active def logstate(self): """Print a status message about the logger.""" - try: - logfile = self.logfname - except: + if self.logfile is None: print 'Logging has not been activated.' else: - state = self._dolog and 'active' or 'temporarily suspended' - print """ -File:\t%s -Mode:\t%s -State:\t%s """ % (logfile,self.LOGMODE,state) + state = self.log_active and 'active' or 'temporarily suspended' + print 'Filename :',self.logfname + print 'Mode :',self.logmode + print 'Output logging:',self.log_output + print 'Timestamping :',self.timestamp + print 'State :',state - def log(self, line,continuation=None): """Write the line to a log and create input cache variables _i*.""" # update the auto _i tables #print '***logging line',line # dbg - #print '***cache_count', self.outputcache.prompt_count # dbg - input_hist = self.log_ns['_ih'] + #print '***cache_count', self.shell.outputcache.prompt_count # dbg + input_hist = self.shell.user_ns['_ih'] if not continuation and line: self._iii = self._ii self._ii = self._i @@ -164,24 +184,38 @@ State:\t%s """ % (logfile,self.LOGMODE,state) # hackish access to top-level namespace to create _i1,_i2... dynamically to_main = {'_i':self._i,'_ii':self._ii,'_iii':self._iii} - if self.do_full_cache: - in_num = self.outputcache.prompt_count - # add blank lines if the input cache fell out of sync. This can happen - # for embedded instances which get killed via C-D and then get resumed. + if self.shell.outputcache.do_full_cache: + in_num = self.shell.outputcache.prompt_count + # add blank lines if the input cache fell out of sync. This can + # happen for embedded instances which get killed via C-D and then + # get resumed. while in_num >= len(input_hist): input_hist.append('\n') new_i = '_i%s' % in_num if continuation: - self._i00 = '%s%s\n' % (self.log_ns[new_i],line) + self._i00 = '%s%s\n' % (self.shell.user_ns[new_i],line) input_hist[in_num] = self._i00 to_main[new_i] = self._i00 - self.log_ns.update(to_main) - - if self._dolog and line: - self.logfile.write(line+'\n') + self.shell.user_ns.update(to_main) + self.log_write(line) + + def log_write(self,data,kind='input'): + """Write data to the log file, if active""" + + if self.log_active and data: + write = self.logfile.write + if kind=='input': + if self.timestamp: + write(time.strftime('# %a, %d %b %Y %H:%M:%S\n', + time.localtime())) + write('%s\n' % data) + elif kind=='output' and self.log_output: + odata = '\n'.join(['#[Out]# %s' % s + for s in data.split('\n')]) + write('%s\n' % odata) self.logfile.flush() def close_log(self): - if hasattr(self, 'logfile'): - self.logfile.close() - self.logfname = '' + self.logfile.close() + self.logfile = None + self.logfname = '' diff --git a/IPython/Magic.py b/IPython/Magic.py index 64bfc97..15f4f2a 100644 --- a/IPython/Magic.py +++ b/IPython/Magic.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Magic functions for InteractiveShell. -$Id: Magic.py 965 2005-12-28 23:23:09Z fperez $""" +$Id: Magic.py 966 2005-12-29 08:34:07Z fperez $""" #***************************************************************************** # Copyright (C) 2001 Janko Hauser and @@ -48,33 +48,8 @@ from IPython.PyColorize import Parser from IPython.Struct import Struct from IPython.genutils import * -# Globals to be set later by Magic constructor -MAGIC_PREFIX = '' -MAGIC_ESCAPE = '' - #*************************************************************************** # Utility functions -def magic2python(cmd): - """Convert a command string of magic syntax to valid Python code.""" - - if cmd.startswith('#'+MAGIC_ESCAPE) or \ - cmd.startswith(MAGIC_ESCAPE): - if cmd[0]=='#': - cmd = cmd[1:] - # we need to return the proper line end later - if cmd[-1] == '\n': - endl = '\n' - else: - endl = '' - try: - func,args = cmd[1:].split(' ',1) - except: - func,args = cmd[1:].rstrip(),'' - args = args.replace('"','\\"').replace("'","\\'").rstrip() - return '%s%s ("%s")%s' % (MAGIC_PREFIX,func,args,endl) - else: - return cmd - def on_off(tag): """Return an ON/OFF string for a 1/0 input. Simple utility function.""" return ['OFF','ON'][tag] @@ -82,22 +57,15 @@ def on_off(tag): #**************************************************************************** # Utility classes -class Macro: +class Macro(list): """Simple class to store the value of macros as strings. This allows us to later exec them by checking when something is an instance of this class.""" - - def __init__(self,cmds): - """Build a macro from a list of commands.""" - # Since the list may include multi-line entries, first make sure that - # they've been all broken up before passing it to magic2python - cmdlist = map(magic2python,''.join(cmds).split('\n')) - self.value = '\n'.join(cmdlist) - - def __str__(self): - return self.value + def __init__(self,data): + list.__init__(self,data) + self.value = ''.join(data) #*************************************************************************** # Main class implementing Magic functionality @@ -120,21 +88,18 @@ class Magic: # some utility functions def __init__(self,shell): - # XXX This is hackish, clean up later to avoid these messy globals - global MAGIC_PREFIX, MAGIC_ESCAPE self.options_table = {} - MAGIC_PREFIX = shell.name+'.magic_' - MAGIC_ESCAPE = shell.ESC_MAGIC if profile is None: self.magic_prun = self.profile_missing_notice + self.shell = shell def profile_missing_notice(self, *args, **kwargs): error("""\ The profile module could not be found. If you are a Debian user, it has been removed from the standard Debian package because of its non-free license. To use profiling, please install"python2.3-profiler" from non-free.""") - + def default_option(self,fn,optstr): """Make an entry in the options_table for fn, with value optstr""" @@ -168,10 +133,6 @@ license. To use profiling, please install"python2.3-profiler" from non-free.""") out.sort() return out - def set_shell(self,shell): - self.shell = shell - self.alias_table = shell.alias_table - def extract_input_slices(self,slices): """Return as a string a set of input history slices. @@ -496,17 +457,17 @@ Currently the magic system has the following functions:\n""" This feature is only available if numbered prompts are in use.""" - if not self.do_full_cache: + if not self.shell.outputcache.do_full_cache: print 'This feature is only available if numbered prompts are in use.' return opts,args = self.parse_options(parameter_s,'n',mode='list') default_length = 40 if len(args) == 0: - final = self.outputcache.prompt_count + final = self.shell.outputcache.prompt_count init = max(1,final-default_length) elif len(args) == 1: - final = self.outputcache.prompt_count + final = self.shell.outputcache.prompt_count init = max(1,final-int(args[0])) elif len(args) == 2: init,final = map(int,args) @@ -562,11 +523,8 @@ Currently the magic system has the following functions:\n""" if input != 'ipmagic("r")\n' and \ (input.startswith(start) or input.startswith(start_magic)): #print 'match',`input` # dbg - if input.startswith(esc_magic): - input = magic2python(input) - #print 'modified',`input` # dbg print 'Executing:',input, - exec input in self.shell.user_ns + self.shell.runlines(input) return print 'No previous input matching `%s` found.' % start @@ -788,8 +746,8 @@ Currently the magic system has the following functions:\n""" typelist = parameter_s.split() for i in self.shell.user_ns.keys(): if not (i.startswith('_') or i.startswith('_i')) \ - and not (self.internal_ns.has_key(i) or - self.user_config_ns.has_key(i)): + and not (self.shell.internal_ns.has_key(i) or + self.shell.user_config_ns.has_key(i)): if typelist: if type(user_ns[i]).__name__ in typelist: out.append(i) @@ -946,9 +904,9 @@ Currently the magic system has the following functions:\n""" def magic_logstart(self,parameter_s=''): """Start logging anywhere in a session. - %logstart [log_name [log_mode]] + %logstart [-o|-t] [log_name [log_mode]] - If no name is given, it defaults to a file named 'ipython.log' in your + If no name is given, it defaults to a file named 'ipython_log.py' in your current directory, in 'rotate' mode (see below). '%logstart name' saves to file 'name' in 'backup' mode. It saves your @@ -956,67 +914,86 @@ Currently the magic system has the following functions:\n""" %logstart takes a second optional parameter: logging mode. This can be one of (note that the modes are given unquoted):\\ - over: overwrite existing log.\\ - backup: rename (if exists) to name~ and start name.\\ append: well, that says it.\\ + backup: rename (if exists) to name~ and start name.\\ + global: single logfile in your home dir, appended to.\\ + over : overwrite existing log.\\ rotate: create rotating logs name.1~, name.2~, etc. - """ - #FIXME. This function should all be moved to the Logger class. + Options: + + -o: log also IPython's output. In this mode, all commands which + generate an Out[NN] prompt are recorded to the logfile, right after + their corresponding input line. The output lines are always + prepended with a #[Out]# marker, so that the log remains valid + Python code. + + -t: put timestamps before each input line logged (these are put in + comments).""" - valid_modes = qw('over backup append rotate') - if self.LOG: - print 'Logging is already in place. Logfile:',self.LOG - return + opts,par = self.parse_options(parameter_s,'ot') + log_output = 'o' in opts + timestamp = 't' in opts - par = parameter_s.strip() - if not par: - logname = self.LOGDEF - logmode = 'rotate' # use rotate for the auto-generated logs - else: + rc = self.shell.rc + logger = self.shell.logger + + # if no args are given, the defaults set in the logger constructor by + # ipytohn remain valid + if par: try: - logname,logmode = par.split() + logfname,logmode = par.split() except: - try: - logname = par - logmode = 'backup' - except: - warn('Usage: %log [log_name [log_mode]]') - return - if not logmode in valid_modes: - warn('Logging NOT activated.\n' - 'Usage: %log [log_name [log_mode]]\n' - 'Valid modes: '+str(valid_modes)) - return - - # If we made it this far, I think we're ok: - print 'Activating auto-logging.' - print 'Current session state plus future input saved to:',logname - print 'Logging mode: ',logmode - # put logname into rc struct as if it had been called on the command line, - # so it ends up saved in the log header - # Save it in case we need to restore it... - old_logfile = self.shell.rc.opts.get('logfile','') - logname = os.path.expanduser(logname) - self.shell.rc.opts.logfile = logname - self.LOGMODE = logmode # FIXME: this should be set through a function. + logfname = par + logmode = 'backup' + else: + logfname = logger.logfname + logmode = logger.logmode + # put logfname into rc struct as if it had been called on the command + # line, so it ends up saved in the log header Save it in case we need + # to restore it... + old_logfile = rc.opts.get('logfile','') + if logfname: + logfname = os.path.expanduser(logfname) + rc.opts.logfile = logfname + loghead = self.shell.loghead_tpl % (rc.opts,rc.args) try: - header = str(self.LOGHEAD) - self.create_log(header,logname) - self.logstart(header,logname) + started = logger.logstart(logfname,loghead,logmode, + log_output,timestamp) except: - self.LOG = '' # we are NOT logging, something went wrong - self.shell.rc.opts.logfile = old_logfile - warn("Couldn't start log: "+str(sys.exc_info()[1])) - else: # log input history up to this point - self.logfile.write(self.shell.user_ns['_ih'][1:]) - self.logfile.flush() - + rc.opts.logfile = old_logfile + warn("Couldn't start log: %s" % sys.exc_info()[1]) + else: + # log input history up to this point, optionally interleaving + # output if requested + + if timestamp: + # disable timestamping for the previous history, since we've + # lost those already (no time machine here). + logger.timestamp = False + if log_output: + log_write = logger.log_write + input_hist = self.shell.input_hist + output_hist = self.shell.output_hist + for n in range(1,len(input_hist)-1): + log_write(input_hist[n].rstrip()) + if n in output_hist: + log_write(repr(output_hist[n]),'output') + else: + logger.log_write(self.shell.input_hist[1:]) + if timestamp: + # re-enable timestamping + logger.timestamp = True + + print ('Activating auto-logging. ' + 'Current session state plus future input saved.') + logger.logstate() + def magic_logoff(self,parameter_s=''): """Temporarily stop logging. You must have previously started logging.""" - self.switch_log(0) + self.shell.logger.switch_log(0) def magic_logon(self,parameter_s=''): """Restart logging. @@ -1026,12 +1003,12 @@ Currently the magic system has the following functions:\n""" must use the %logstart function, which allows you to specify an optional log filename.""" - self.switch_log(1) + self.shell.logger.switch_log(1) def magic_logstate(self,parameter_s=''): """Print the status of the logging system.""" - self.logstate() + self.shell.logger.logstate() def magic_pdb(self, parameter_s=''): """Control the calling of the pdb interactive debugger. @@ -1047,24 +1024,18 @@ Currently the magic system has the following functions:\n""" if par: try: - pdb = {'off':0,'0':0,'on':1,'1':1}[par] + new_pdb = {'off':0,'0':0,'on':1,'1':1}[par] except KeyError: - print 'Incorrect argument. Use on/1, off/0, or nothing for a toggle.' + print ('Incorrect argument. Use on/1, off/0, ' + 'or nothing for a toggle.') return - else: - self.shell.InteractiveTB.call_pdb = pdb else: + # toggle new_pdb = not self.shell.InteractiveTB.call_pdb - self.shell.InteractiveTB.call_pdb = new_pdb - if self.shell.isthreaded: - try: - self.sys_excepthook.call_pdb = new_pdb - except: - warn('Failed to activate pdb for threaded exception handler') - - print 'Automatic pdb calling has been turned',on_off(new_pdb) - + # set on the shell + self.shell.call_pdb = new_pdb + print 'Automatic pdb calling has been turned',on_off(new_pdb) def magic_prun(self, parameter_s ='',user_mode=1, opts=None,arg_lst=None,prog_ns=None): @@ -1606,12 +1577,12 @@ Currently the magic system has the following functions:\n""" args = parameter_s.split() name,ranges = args[0], args[1:] #print 'rng',ranges # dbg - cmds = self.extract_input_slices(ranges) - macro = Macro(cmds) + lines = self.extract_input_slices(ranges) + macro = Macro(lines) self.shell.user_ns.update({name:macro}) print 'Macro `%s` created. To execute, type its name (without quotes).' % name print 'Macro contents:' - print str(macro).rstrip(), + print macro def magic_save(self,parameter_s = ''): """Save a set of lines to a given filename. @@ -1906,17 +1877,18 @@ Currently the magic system has the following functions:\n""" warn('Error changing %s exception modes.\n%s' % (name,sys.exc_info()[1])) + shell = self.shell new_mode = parameter_s.strip().capitalize() try: - self.InteractiveTB.set_mode(mode=new_mode) - print 'Exception reporting mode:',self.InteractiveTB.mode + shell.InteractiveTB.set_mode(mode=new_mode) + print 'Exception reporting mode:',shell.InteractiveTB.mode except: xmode_switch_err('user') # threaded shells use a special handler in sys.excepthook - if self.isthreaded: + if shell.isthreaded: try: - self.shell.sys_excepthook.set_mode(mode=new_mode) + shell.sys_excepthook.set_mode(mode=new_mode) except: xmode_switch_err('threaded') @@ -1961,37 +1933,39 @@ http://starship.python.net/crew/theller/ctypes Defaulting color scheme to 'NoColor'""" new_scheme = 'NoColor' warn(msg) + # local shortcut + shell = self.shell # Set prompt colors try: - self.shell.outputcache.set_colors(new_scheme) + shell.outputcache.set_colors(new_scheme) except: color_switch_err('prompt') else: - self.shell.rc.colors = \ - self.shell.outputcache.color_table.active_scheme_name + shell.rc.colors = \ + shell.outputcache.color_table.active_scheme_name # Set exception colors try: - self.shell.InteractiveTB.set_colors(scheme = new_scheme) - self.shell.SyntaxTB.set_colors(scheme = new_scheme) + shell.InteractiveTB.set_colors(scheme = new_scheme) + shell.SyntaxTB.set_colors(scheme = new_scheme) except: color_switch_err('exception') # threaded shells use a verbose traceback in sys.excepthook - if self.isthreaded: + if shell.isthreaded: try: - self.shell.sys_excepthook.set_colors(scheme=new_scheme) + shell.sys_excepthook.set_colors(scheme=new_scheme) except: color_switch_err('system exception handler') # Set info (for 'object?') colors - if self.shell.rc.color_info: + if shell.rc.color_info: try: - self.shell.inspector.set_active_scheme(new_scheme) + shell.inspector.set_active_scheme(new_scheme) except: color_switch_err('object inspector') else: - self.shell.inspector.set_active_scheme('NoColor') + shell.inspector.set_active_scheme('NoColor') def magic_color_info(self,parameter_s = ''): """Toggle color_info. @@ -2284,7 +2258,7 @@ Defaulting color scheme to 'NoColor'""" else: self.shell.user_ns['_dh'].append(os.getcwd()) else: - os.chdir(self.home_dir) + os.chdir(self.shell.home_dir) self.shell.user_ns['_dh'].append(os.getcwd()) if not 'q' in opts: print self.shell.user_ns['_dh'][-1] @@ -2323,7 +2297,6 @@ Defaulting color scheme to 'NoColor'""" def magic_env(self, parameter_s=''): """List environment variables.""" - # environ is an instance of UserDict return os.environ.data def magic_pushd(self, parameter_s=''): @@ -2335,11 +2308,12 @@ Defaulting color scheme to 'NoColor'""" %pushd with no arguments does a %pushd to your home directory. """ if parameter_s == '': parameter_s = '~' - if len(self.dir_stack)>0 and os.path.expanduser(parameter_s) != \ - os.path.expanduser(self.dir_stack[0]): + dir_s = self.shell.dir_stack + if len(dir_s)>0 and os.path.expanduser(parameter_s) != \ + os.path.expanduser(self.shell.dir_stack[0]): try: self.magic_cd(parameter_s) - self.dir_stack.insert(0,os.getcwd().replace(self.home_dir,'~')) + dir_s.insert(0,os.getcwd().replace(self.home_dir,'~')) self.magic_dirs() except: print 'Invalid directory' @@ -2349,18 +2323,18 @@ Defaulting color scheme to 'NoColor'""" def magic_popd(self, parameter_s=''): """Change to directory popped off the top of the stack. """ - if len (self.dir_stack) > 1: - self.dir_stack.pop(0) - self.magic_cd(self.dir_stack[0]) - print self.dir_stack[0] + if len (self.shell.dir_stack) > 1: + self.shell.dir_stack.pop(0) + self.magic_cd(self.shell.dir_stack[0]) + print self.shell.dir_stack[0] else: print "You can't remove the starting directory from the stack:",\ - self.dir_stack + self.shell.dir_stack def magic_dirs(self, parameter_s=''): """Return the current directory stack.""" - return self.dir_stack[:] + return self.shell.dir_stack[:] def magic_sc(self, parameter_s=''): """Shell capture - execute a shell command and capture its output. @@ -2605,7 +2579,7 @@ Defaulting color scheme to 'NoColor'""" bkms[args[0]] = os.getcwd() elif len(args)==2: bkms[args[0]] = args[1] - self.persist['bookmarks'] = bkms + self.shell.persist['bookmarks'] = bkms def magic_pycat(self, parameter_s=''): """Show a syntax-highlighted file through a pager. diff --git a/IPython/Prompts.py b/IPython/Prompts.py index 99acbd7..72b7777 100644 --- a/IPython/Prompts.py +++ b/IPython/Prompts.py @@ -2,7 +2,7 @@ """ Classes for handling input/output prompts. -$Id: Prompts.py 960 2005-12-28 06:51:01Z fperez $""" +$Id: Prompts.py 966 2005-12-29 08:34:07Z fperez $""" #***************************************************************************** # Copyright (C) 2001-2004 Fernando Perez @@ -392,10 +392,10 @@ class CachedOutput: Initialize with initial and final values for cache counter (this defines the maximum size of the cache.""" - def __init__(self,cache_size,Pprint,colors='NoColor',input_sep='\n', - output_sep='\n',output_sep2='',user_ns={}, - ps1 = None, ps2 = None,ps_out = None, - input_hist = None,pad_left=True): + def __init__(self,shell,cache_size,Pprint, + colors='NoColor',input_sep='\n', + output_sep='\n',output_sep2='', + ps1 = None, ps2 = None,ps_out = None,pad_left=True): cache_size_min = 20 if cache_size <= 0: @@ -413,9 +413,12 @@ class CachedOutput: self.input_sep = input_sep # we need a reference to the user-level namespace - self.user_ns = user_ns + self.shell = shell + self.user_ns = shell.user_ns # and to the user's input - self.input_hist = input_hist + self.input_hist = shell.input_hist + # and to the user's logger, for logging output + self.logger = shell.logger # Set input prompt strings and colors if cache_size == 0: @@ -509,11 +512,13 @@ class CachedOutput: print 'Executing Macro...' # in case the macro takes a long time to execute Term.cout.flush() - exec arg.value in self.user_ns + self.shell.runlines(arg.value) return None # and now call a possibly user-defined print mechanism self.display(arg) + if self.logger.log_output: + self.logger.log_write(repr(arg),'output') cout_write(self.output_sep2) Term.cout.flush() diff --git a/IPython/iplib.py b/IPython/iplib.py index 807b777..f494950 100644 --- a/IPython/iplib.py +++ b/IPython/iplib.py @@ -6,7 +6,7 @@ Requires Python 2.1 or newer. This file contains all the classes and helper functions specific to IPython. -$Id: iplib.py 965 2005-12-28 23:23:09Z fperez $ +$Id: iplib.py 966 2005-12-29 08:34:07Z fperez $ """ #***************************************************************************** @@ -67,7 +67,8 @@ from IPython.ColorANSI import ColorScheme,ColorSchemeTable # too long names from IPython.FakeModule import FakeModule from IPython.Itpl import Itpl,itpl,printpl,ItplNS,itplns from IPython.Logger import Logger -from IPython.Magic import Magic,magic2python +from IPython.Magic import Magic +from IPython.Prompts import CachedOutput from IPython.Struct import Struct from IPython.background_jobs import BackgroundJobManager from IPython.usage import cmd_line_usage,interactive_usage @@ -229,7 +230,27 @@ class SyntaxTB(ultraTB.ListTB): #**************************************************************************** # Main IPython class -class InteractiveShell(Logger, Magic): + +# FIXME: the Magic class is a mixin for now, and will unfortunately remain so +# until a full rewrite is made. I've cleaned all cross-class uses of +# attributes and methods, but too much user code out there relies on the +# equlity %foo == __IP.magic_foo, so I can't actually remove the mixin usage. +# +# But at least now, all the pieces have been separated and we could, in +# principle, stop using the mixin. This will ease the transition to the +# chainsaw branch. + +# For reference, the following is the list of 'self.foo' uses in the Magic +# class as of 2005-12-28. These are names we CAN'T use in the main ipython +# class, to prevent clashes. + +# ['self.__class__', 'self.__dict__', 'self._inspect', 'self._ofind', +# 'self.arg_err', 'self.extract_input', 'self.format_', 'self.lsmagic', +# 'self.magic_', 'self.options_table', 'self.parse', 'self.shell', +# 'self.value'] + + +class InteractiveShell(Magic): """An enhanced console for Python.""" # class attribute to indicate whether the class supports threads or not. @@ -305,7 +326,6 @@ class InteractiveShell(Logger, Magic): # Von: Alex Martelli # Datum: Freitag 01 Oktober 2004 04:45:34 nachmittags/abends # Gruppen: comp.lang.python - # Referenzen: 1 # Michael Hohn wrote: # > >>> print type(builtin_check.get_global_binding('__builtins__')) @@ -432,22 +452,17 @@ class InteractiveShell(Logger, Magic): self.ESC_PAREN = '/' # And their associated handlers - self.esc_handlers = {self.ESC_PAREN:self.handle_auto, - self.ESC_QUOTE:self.handle_auto, - self.ESC_QUOTE2:self.handle_auto, - self.ESC_MAGIC:self.handle_magic, - self.ESC_HELP:self.handle_help, - self.ESC_SHELL:self.handle_shell_escape, + self.esc_handlers = {self.ESC_PAREN : self.handle_auto, + self.ESC_QUOTE : self.handle_auto, + self.ESC_QUOTE2 : self.handle_auto, + self.ESC_MAGIC : self.handle_magic, + self.ESC_HELP : self.handle_help, + self.ESC_SHELL : self.handle_shell_escape, } # class initializations - Logger.__init__(self,log_ns = self.user_ns) Magic.__init__(self,self) - # an ugly hack to get a pointer to the shell, so I can start writing - # magic code via this pointer instead of the current mixin salad. - Magic.set_shell(self,self) - # Python source parser/formatter for syntax highlighting pyformat = PyColorize.Parser().format self.pycolorize = lambda src: pyformat(src,'str',self.rc['colors']) @@ -488,6 +503,16 @@ class InteractiveShell(Logger, Magic): # Keep track of readline usage (later set by init_readline) self.has_readline = False + # template for logfile headers. It gets resolved at runtime by the + # logstart method. + self.loghead_tpl = \ +"""#log# Automatic Logger file. *** THIS MUST BE THE FIRST LINE *** +#log# DO NOT CHANGE THIS LINE OR THE TWO BELOW +#log# opts = %s +#log# args = %s +#log# It is safe to make manual edits below here. +#log#----------------------------------------------------------------------- +""" # for pushd/popd management try: self.home_dir = get_home_dir() @@ -550,19 +575,6 @@ class InteractiveShell(Logger, Magic): # keep track of where we started running (mainly for crash post-mortem) self.starting_dir = os.getcwd() - # Attributes for Logger mixin class, make defaults here - self._dolog = False - self.LOG = '' - self.LOGDEF = '.InteractiveShell.log' - self.LOGMODE = 'over' - self.LOGHEAD = Itpl( -"""#log# Automatic Logger file. *** THIS MUST BE THE FIRST LINE *** -#log# DO NOT CHANGE THIS LINE OR THE TWO BELOW -#log# opts = $self.rc.opts -#log# args = $self.rc.args -#log# It is safe to make manual edits below here. -#log#----------------------------------------------------------------------- -""") # Various switches which can be set self.CACHELENGTH = 5000 # this is cheap, it's just text self.BANNER = "Python %(version)s on %(platform)s\n" % sys.__dict__ @@ -655,10 +667,42 @@ class InteractiveShell(Logger, Magic): if rc.readline: self.init_readline() + # log system + self.logger = Logger(self,logfname='ipython_log.py',logmode='rotate') + # local shortcut, this is used a LOT + self.log = self.logger.log + + # Initialize cache, set in/out prompts and printing system + self.outputcache = CachedOutput(self, + rc.cache_size, + rc.pprint, + input_sep = rc.separate_in, + output_sep = rc.separate_out, + output_sep2 = rc.separate_out2, + ps1 = rc.prompt_in1, + ps2 = rc.prompt_in2, + ps_out = rc.prompt_out, + pad_left = rc.prompts_pad_left) + + # user may have over-ridden the default print hook: + try: + self.outputcache.__class__.display = self.hooks.display + except AttributeError: + pass + + # I don't like assigning globally to sys, because it means when embedding + # instances, each embedded instance overrides the previous choice. But + # sys.displayhook seems to be called internally by exec, so I don't see a + # way around it. + sys.displayhook = self.outputcache + # Set user colors (don't do it in the constructor above so that it # doesn't crash if colors option is invalid) self.magic_colors(rc.colors) + # Set calling of pdb on exceptions + self.call_pdb = rc.pdb + # Load user aliases for alias in rc.alias: self.magic_alias(alias) @@ -742,6 +786,28 @@ class InteractiveShell(Logger, Magic): self.Completer.__class__) self.Completer.matchers.insert(pos,newcomp) + def _get_call_pdb(self): + return self._call_pdb + + def _set_call_pdb(self,val): + + if val not in (0,1,False,True): + raise ValueError,'new call_pdb value must be boolean' + + # store value in instance + self._call_pdb = val + + # notify the actual exception handlers + self.InteractiveTB.call_pdb = val + if self.isthreaded: + try: + self.sys_excepthook.call_pdb = val + except: + warn('Failed to activate pdb for threaded exception handler') + + call_pdb = property(_get_call_pdb,_set_call_pdb,None, + 'Control auto-activation of pdb at exceptions') + def complete(self,text): """Return a sorted list of all possible completions on text. @@ -1907,10 +1973,10 @@ want to merge them back into the new files.""" % locals() kw.setdefault('quiet',1) kw.setdefault('exit_ignore',0) first = xfile.readline() - _LOGHEAD = str(self.LOGHEAD).split('\n',1)[0].strip() + loghead = str(self.loghead_tpl).split('\n',1)[0].strip() xfile.close() # line by line execution - if first.startswith(_LOGHEAD) or kw['islog']: + if first.startswith(loghead) or kw['islog']: print 'Loading log file <%s> one line at a time...' % fname if kw['quiet']: stdout_save = sys.stdout @@ -1942,9 +2008,6 @@ want to merge them back into the new files.""" % locals() # don't re-insert logger status info into cache if line.startswith('#log#'): continue - elif line.startswith('#%s'% self.ESC_MAGIC): - self.update_cache(line[1:]) - line = magic2python(line) elif line.startswith('#!'): self.update_cache(line[1:]) else: diff --git a/IPython/ipmaker.py b/IPython/ipmaker.py index 85bdee3..ae3c393 100644 --- a/IPython/ipmaker.py +++ b/IPython/ipmaker.py @@ -6,7 +6,7 @@ Requires Python 2.1 or better. This file contains the main make_IPython() starter function. -$Id: ipmaker.py 965 2005-12-28 23:23:09Z fperez $""" +$Id: ipmaker.py 966 2005-12-29 08:34:07Z fperez $""" #***************************************************************************** # Copyright (C) 2001-2004 Fernando Perez. @@ -51,7 +51,6 @@ from IPython.OutputTrap import OutputTrap from IPython.ConfigLoader import ConfigLoader from IPython.iplib import InteractiveShell,qw_lol,import_fail_info from IPython.usage import cmd_line_usage,interactive_usage -from IPython.Prompts import CachedOutput from IPython.genutils import * #----------------------------------------------------------------------------- @@ -322,9 +321,6 @@ object? -> Details about 'object'. ?object also works, ?? prints more. mutex_opts(opts,[qw('log logfile'),qw('rcfile profile'), qw('classic profile'),qw('classic rcfile')]) - # default logfilename used when -log is called. - IP.LOGDEF = 'ipython.log' - #--------------------------------------------------------------------------- # Log replay @@ -681,37 +677,7 @@ object? -> Details about 'object'. ?object also works, ?? prints more. # paged: num_lines_bot = IP_rc.separate_in.count('\n')+1 IP_rc.screen_length = IP_rc.screen_length - num_lines_bot - # Initialize cache, set in/out prompts and printing system - IP.outputcache = CachedOutput(IP_rc.cache_size, - IP_rc.pprint, - input_sep = IP_rc.separate_in, - output_sep = IP_rc.separate_out, - output_sep2 = IP_rc.separate_out2, - ps1 = IP_rc.prompt_in1, - ps2 = IP_rc.prompt_in2, - ps_out = IP_rc.prompt_out, - user_ns = IP.user_ns, - input_hist = IP.input_hist, - pad_left = IP_rc.prompts_pad_left) - - # user may have over-ridden the default print hook: - try: - IP.outputcache.__class__.display = IP.hooks.display - except AttributeError: - pass - # Set calling of pdb on exceptions - IP.InteractiveTB.call_pdb = IP_rc.pdb - - # I don't like assigning globally to sys, because it means when embedding - # instances, each embedded instance overrides the previous choice. But - # sys.displayhook seems to be called internally by exec, so I don't see a - # way around it. - sys.displayhook = IP.outputcache - - # we need to know globally if we're caching i/o or not - IP.do_full_cache = IP.outputcache.do_full_cache - # configure startup banner if IP_rc.c: # regular python doesn't print the banner with -c IP_rc.banner = 0 diff --git a/IPython/usage.py b/IPython/usage.py index dd765e0..268d1d9 100644 --- a/IPython/usage.py +++ b/IPython/usage.py @@ -6,7 +6,7 @@ # the file COPYING, distributed as part of this software. #***************************************************************************** -# $Id: usage.py 960 2005-12-28 06:51:01Z fperez $ +# $Id: usage.py 966 2005-12-29 08:34:07Z fperez $ from IPython import Release __author__ = '%s <%s>' % Release.authors['Fernando'] @@ -236,14 +236,14 @@ REGULAR OPTIONS This can also be specified through the environment variable IPYTHONDIR. - -log|l Generate a log file of all input. The file is named ipython.log - in your current directory (which prevents logs from multiple - IPython sessions from trampling each other). You can use this to - later restore a session by loading your logfile as a file to be - executed with option -logplay (see below). + -log|l Generate a log file of all input. The file is named + ipython_log.py in your current directory (which prevents logs + from multiple IPython sessions from trampling each other). You + can use this to later restore a session by loading your logfile + as a file to be executed with option -logplay (see below). -logfile|lf - Specifu the name of your logfile. + Specify the name of your logfile. -logplay|lp Replay a previous log. For restoring a session as close as pos- diff --git a/doc/ChangeLog b/doc/ChangeLog index 53c22a2..a60a1aa 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -1,3 +1,31 @@ +2005-12-29 Fernando Perez + + * IPython/Magic.py (Macro): simplified Macro class to just + subclass list. We've had only 2.2 compatibility for a very long + time, yet I was still avoiding subclassing the builtin types. No + more (I'm also starting to use properties, though I won't shift to + 2.3-specific features quite yet). + + * IPython/iplib.py (InteractiveShell.post_config_initialization): + changed the default logfile name from 'ipython.log' to + 'ipython_log.py'. These logs are real python files, and now that + we have much better multiline support, people are more likely to + want to use them as such. Might as well name them correctly. + + * IPython/Magic.py: substantial cleanup. While we can't stop + using magics as mixins, due to the existing customizations 'out + there' which rely on the mixin naming conventions, at least I + cleaned out all cross-class name usage. So once we are OK with + breaking compatibility, the two systems can be separated. + + * IPython/Logger.py: major cleanup. This one is NOT a mixin + anymore, and the class is a fair bit less hideous as well. New + features were also introduced: timestamping of input, and logging + of output results. These are user-visible with the -t and -o + options to %logstart. Closes + http://www.scipy.net/roundup/ipython/issue11 and a request by + William Stein (SAGE developer - http://modular.ucsd.edu/sage). + 2005-12-28 Fernando Perez * IPython/iplib.py (handle_shell_escape): add Ville's patch to diff --git a/doc/ipython.1 b/doc/ipython.1 index 3f2da8a..b522613 100644 --- a/doc/ipython.1 +++ b/doc/ipython.1 @@ -205,14 +205,13 @@ The name of your IPython configuration directory IPYTHONDIR. This can also be specified through the environment variable IPYTHONDIR. .TP .B \-log|l -Generate a log file of all input. The file is named ipython.log in -your current directory (which prevents logs from multiple IPython -sessions from trampling each other). You can use this to later restore -a session by loading your logfile as a file to be executed with option --logplay (see below). +Generate a log file of all input. The file is named ipython_log.py in your +current directory (which prevents logs from multiple IPython sessions from +trampling each other). You can use this to later restore a session by loading +your logfile as a file to be executed with option -logplay (see below). .TP .B \-logfile|lf -Specifu the name of your logfile. +Specify the name of your logfile. .TP .B \-logplay|lp Replay a previous log. For restoring a session as close as possible to diff --git a/doc/manual_base.lyx b/doc/manual_base.lyx index e87220b..8b27d71 100644 --- a/doc/manual_base.lyx +++ b/doc/manual_base.lyx @@ -2957,11 +2957,12 @@ IPYTHONDIR \family default \series default : generate a log file of all input. - Defaults to + The file is named \family typewriter -$IPYTHONDIR/log +ipython_log.py \family default -. + in your current directory (which prevents logs from multiple IPython sessions + from trampling each other). You can use this to later restore a session by loading your logfile as a file to be executed with option \family typewriter