Logger.py
270 lines
| 10.0 KiB
| text/x-python
|
PythonLexer
/ IPython / Logger.py
ville
|
r988 | # -*- coding: utf-8 -*- | ||
""" | ||||
Logger class for IPython's logging facilities. | ||||
$Id: Logger.py 2875 2007-11-26 08:37:39Z fperez $ | ||||
""" | ||||
#***************************************************************************** | ||||
# Copyright (C) 2001 Janko Hauser <jhauser@zscout.de> and | ||||
# Copyright (C) 2001-2006 Fernando Perez <fperez@colorado.edu> | ||||
# | ||||
# Distributed under the terms of the BSD License. The full license is in | ||||
# the file COPYING, distributed as part of this software. | ||||
#***************************************************************************** | ||||
#**************************************************************************** | ||||
# Modules and globals | ||||
from IPython import Release | ||||
__author__ = '%s <%s>\n%s <%s>' % \ | ||||
( Release.authors['Janko'] + Release.authors['Fernando'] ) | ||||
__license__ = Release.license | ||||
# Python standard modules | ||||
import glob | ||||
import os | ||||
import time | ||||
#**************************************************************************** | ||||
# 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 = '','','','' | ||||
# 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 log raw or processed input | ||||
self.log_raw_input = False | ||||
# 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,log_raw_input=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) | ||||
self.log_active = True | ||||
# The parameters can override constructor defaults | ||||
if logfname is not None: self.logfname = logfname | ||||
if loghead is not None: self.loghead = loghead | ||||
if logmode is not None: self.logmode = logmode | ||||
# Parameters not part of the constructor | ||||
self.timestamp = timestamp | ||||
self.log_output = log_output | ||||
self.log_raw_input = log_raw_input | ||||
# 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 isfile(backup_logname): | ||||
os.remove(backup_logname) | ||||
os.rename(self.logfname,backup_logname) | ||||
self.logfile = open(self.logfname,'w') | ||||
elif logmode == 'global': | ||||
self.logfname = os.path.join(self.shell.home_dir,self.logfname) | ||||
self.logfile = open(self.logfname, 'a') | ||||
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() | ||||
for f in old: | ||||
root, ext = os.path.splitext(f) | ||||
num = int(ext[1:-1])+1 | ||||
os.rename(f, root+'.'+`num`.zfill(3)+'~') | ||||
os.rename(self.logfname, self.logfname+'.001~') | ||||
self.logfile = open(self.logfname,'w') | ||||
if logmode != 'append': | ||||
self.logfile.write(self.loghead) | ||||
self.logfile.flush() | ||||
def switch_log(self,val): | ||||
"""Switch logging on/off. val should be ONLY a boolean.""" | ||||
if val not in [False,True,0,1]: | ||||
raise ValueError, \ | ||||
'Call switch_log ONLY with a boolean argument, not with:',val | ||||
label = {0:'OFF',1:'ON',False:'OFF',True:'ON'} | ||||
if self.logfile is None: | ||||
print """ | ||||
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.log_active == val: | ||||
print 'Logging is already',label[val] | ||||
else: | ||||
print 'Switching logging',label[val] | ||||
self.log_active = not self.log_active | ||||
self.log_active_out = self.log_active | ||||
def logstate(self): | ||||
"""Print a status message about the logger.""" | ||||
if self.logfile is None: | ||||
print 'Logging has not been activated.' | ||||
else: | ||||
state = self.log_active and 'active' or 'temporarily suspended' | ||||
print 'Filename :',self.logfname | ||||
print 'Mode :',self.logmode | ||||
print 'Output logging :',self.log_output | ||||
print 'Raw input log :',self.log_raw_input | ||||
print 'Timestamping :',self.timestamp | ||||
print 'State :',state | ||||
def log(self,line_ori,line_mod,continuation=None): | ||||
"""Write the line to a log and create input cache variables _i*. | ||||
Inputs: | ||||
- line_ori: unmodified input line from the user. This is not | ||||
necessarily valid Python. | ||||
- line_mod: possibly modified input, such as the transformations made | ||||
by input prefilters or input handlers of various kinds. This should | ||||
always be valid Python. | ||||
- continuation: if True, indicates this is part of multi-line input.""" | ||||
# update the auto _i tables | ||||
#print '***logging line',line_mod # dbg | ||||
#print '***cache_count', self.shell.outputcache.prompt_count # dbg | ||||
try: | ||||
input_hist = self.shell.user_ns['_ih'] | ||||
except: | ||||
print 'userns:',self.shell.user_ns.keys() | ||||
return | ||||
out_cache = self.shell.outputcache | ||||
# add blank lines if the input cache fell out of sync. | ||||
if out_cache.do_full_cache and \ | ||||
out_cache.prompt_count +1 > len(input_hist): | ||||
input_hist.extend(['\n'] * (out_cache.prompt_count - len(input_hist))) | ||||
if not continuation and line_mod: | ||||
self._iii = self._ii | ||||
self._ii = self._i | ||||
self._i = self._i00 | ||||
# put back the final \n of every input line | ||||
self._i00 = line_mod+'\n' | ||||
#print 'Logging input:<%s>' % line_mod # dbg | ||||
input_hist.append(self._i00) | ||||
#print '---[%s]' % (len(input_hist)-1,) # dbg | ||||
# hackish access to top-level namespace to create _i1,_i2... dynamically | ||||
to_main = {'_i':self._i,'_ii':self._ii,'_iii':self._iii} | ||||
if self.shell.outputcache.do_full_cache: | ||||
in_num = self.shell.outputcache.prompt_count | ||||
# but if the opposite is true (a macro can produce multiple inputs | ||||
# with no output display called), then bring the output counter in | ||||
# sync: | ||||
last_num = len(input_hist)-1 | ||||
if in_num != last_num: | ||||
in_num = self.shell.outputcache.prompt_count = last_num | ||||
new_i = '_i%s' % in_num | ||||
if continuation: | ||||
self._i00 = '%s%s\n' % (self.shell.user_ns[new_i],line_mod) | ||||
input_hist[in_num] = self._i00 | ||||
to_main[new_i] = self._i00 | ||||
self.shell.user_ns.update(to_main) | ||||
# Write the log line, but decide which one according to the | ||||
# log_raw_input flag, set when the log is started. | ||||
if self.log_raw_input: | ||||
self.log_write(line_ori) | ||||
else: | ||||
self.log_write(line_mod) | ||||
def log_write(self,data,kind='input'): | ||||
"""Write data to the log file, if active""" | ||||
#print 'data: %r' % data # dbg | ||||
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 logstop(self): | ||||
"""Fully stop logging and close log file. | ||||
In order to start logging again, a new logstart() call needs to be | ||||
made, possibly (though not necessarily) with a new filename, mode and | ||||
other options.""" | ||||
self.logfile.close() | ||||
self.logfile = None | ||||
self.log_active = False | ||||
# For backwards compatibility, in case anyone was using this. | ||||
close_log = logstop | ||||