logger.py
231 lines
| 8.2 KiB
| text/x-python
|
PythonLexer
|
r3086 | """Logger class for IPython's logging facilities. | ||
|
r0 | """ | ||
#***************************************************************************** | ||||
# Copyright (C) 2001 Janko Hauser <jhauser@zscout.de> and | ||||
|
r88 | # Copyright (C) 2001-2006 Fernando Perez <fperez@colorado.edu> | ||
|
r0 | # | ||
# 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 | ||||
# Python standard modules | ||||
|
r52 | import glob | ||
|
r7081 | import io | ||
|
r28339 | import logging | ||
|
r52 | import os | ||
|
r60 | import time | ||
|
r0 | |||
|
r7081 | |||
|
r28339 | # prevent jedi/parso's debug messages pipe into interactiveshell | ||
logging.getLogger("parso").setLevel(logging.WARNING) | ||||
|
r0 | #**************************************************************************** | ||
|
r60 | # 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""" | ||||
|
r7081 | def __init__(self, home_dir, logfname='Logger.log', loghead=u'', | ||
|
r3086 | logmode='over'): | ||
|
r0 | |||
|
r60 | # this is the full ipython instance, we need some attributes from it | ||
# which won't exist until later. What a mess, clean up later... | ||||
|
r3086 | self.home_dir = home_dir | ||
|
r60 | |||
self.logfname = logfname | ||||
self.loghead = loghead | ||||
self.logmode = logmode | ||||
self.logfile = None | ||||
|
r305 | # Whether to log raw or processed input | ||
self.log_raw_input = False | ||||
|
r60 | # 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']: | ||||
|
r7843 | raise ValueError('invalid log mode %s given' % mode) | ||
|
r60 | self._logmode = mode | ||
def _get_mode(self): | ||||
return self._logmode | ||||
logmode = property(_get_mode,_set_mode) | ||||
|
r4872 | |||
|
r7082 | def logstart(self, logfname=None, loghead=None, logmode=None, | ||
log_output=False, timestamp=False, log_raw_input=False): | ||||
|
r60 | """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) | ||||
|
r4872 | |||
|
r305 | # 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 | ||||
|
r60 | self.timestamp = timestamp | ||
self.log_output = log_output | ||||
|
r305 | self.log_raw_input = log_raw_input | ||
|
r4872 | |||
|
r60 | # init depending on the log mode requested | ||
isfile = os.path.isfile | ||||
logmode = self.logmode | ||||
if logmode == 'append': | ||||
|
r7081 | self.logfile = io.open(self.logfname, 'a', encoding='utf-8') | ||
|
r60 | |||
elif logmode == 'backup': | ||||
if isfile(self.logfname): | ||||
|
r0 | backup_logname = self.logfname+'~' | ||
# Manually remove any old backup, since os.rename may fail | ||||
# under Windows. | ||||
|
r60 | if isfile(backup_logname): | ||
|
r0 | os.remove(backup_logname) | ||
os.rename(self.logfname,backup_logname) | ||||
|
r7081 | self.logfile = io.open(self.logfname, 'w', encoding='utf-8') | ||
|
r60 | |||
elif logmode == 'global': | ||||
|
r3086 | self.logfname = os.path.join(self.home_dir,self.logfname) | ||
|
r7081 | self.logfile = io.open(self.logfname, 'a', encoding='utf-8') | ||
|
r60 | |||
elif logmode == 'over': | ||||
if isfile(self.logfname): | ||||
|
r4872 | os.remove(self.logfname) | ||
|
r7081 | self.logfile = io.open(self.logfname,'w', encoding='utf-8') | ||
|
r60 | |||
elif logmode == 'rotate': | ||||
if isfile(self.logfname): | ||||
|
r4872 | if isfile(self.logfname+'.001~'): | ||
|
r0 | 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 | ||||
|
r7815 | os.rename(f, root+'.'+repr(num).zfill(3)+'~') | ||
|
r0 | os.rename(self.logfname, self.logfname+'.001~') | ||
|
r7081 | self.logfile = io.open(self.logfname, 'w', encoding='utf-8') | ||
|
r4872 | |||
|
r60 | if logmode != 'append': | ||
self.logfile.write(self.loghead) | ||||
|
r0 | |||
|
r60 | self.logfile.flush() | ||
|
r3887 | self.log_active = True | ||
|
r0 | |||
def switch_log(self,val): | ||||
|
r60 | """Switch logging on/off. val should be ONLY a boolean.""" | ||
|
r0 | |||
|
r60 | if val not in [False,True,0,1]: | ||
|
r7843 | raise ValueError('Call switch_log ONLY with a boolean argument, ' | ||
'not with: %s' % val) | ||||
|
r4872 | |||
|
r60 | label = {0:'OFF',1:'ON',False:'OFF',True:'ON'} | ||
|
r0 | |||
|
r60 | if self.logfile is None: | ||
|
r13348 | print(""" | ||
|
r60 | Logging hasn't been started yet (use logstart for that). | ||
|
r0 | |||
%logon/%logoff are for temporarily starting and stopping logging for a logfile | ||||
which already exists. But you must first start the logging process with | ||||
|
r13348 | %logstart (optionally giving a logfile name).""") | ||
|
r4872 | |||
|
r0 | else: | ||
|
r60 | if self.log_active == val: | ||
|
r13348 | print('Logging is already',label[val]) | ||
|
r0 | else: | ||
|
r13348 | print('Switching logging',label[val]) | ||
|
r60 | self.log_active = not self.log_active | ||
self.log_active_out = self.log_active | ||||
|
r0 | |||
def logstate(self): | ||||
"""Print a status message about the logger.""" | ||||
|
r60 | if self.logfile is None: | ||
|
r13348 | print('Logging has not been activated.') | ||
|
r0 | else: | ||
|
r60 | state = self.log_active and 'active' or 'temporarily suspended' | ||
|
r13386 | 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) | ||||
|
r0 | |||
|
r3086 | def log(self, line_mod, line_ori): | ||
"""Write the sources to a log. | ||||
|
r305 | |||
Inputs: | ||||
- line_mod: possibly modified input, such as the transformations made | ||||
|
r12553 | by input prefilters or input handlers of various kinds. This should | ||
always be valid Python. | ||||
|
r305 | |||
|
r12553 | - line_ori: unmodified input line from the user. This is not | ||
necessarily valid Python. | ||||
|
r3086 | """ | ||
|
r305 | |||
# 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) | ||||
|
r60 | |||
|
r3086 | def log_write(self, data, kind='input'): | ||
|
r60 | """Write data to the log file, if active""" | ||
|
r28756 | # print('data: %r' % data) # dbg | ||
|
r60 | if self.log_active and data: | ||
write = self.logfile.write | ||||
if kind=='input': | ||||
if self.timestamp: | ||||
|
r23065 | write(time.strftime('# %a, %d %b %Y %H:%M:%S\n', time.localtime())) | ||
|
r3086 | write(data) | ||
|
r60 | elif kind=='output' and self.log_output: | ||
|
r7081 | odata = u'\n'.join([u'#[Out]# %s' % s | ||
|
r3086 | for s in data.splitlines()]) | ||
|
r7081 | write(u'%s\n' % odata) | ||
|
r28072 | try: | ||
self.logfile.flush() | ||||
except OSError: | ||||
print("Failed to flush the log file.") | ||||
print( | ||||
f"Please check that {self.logfname} exists and have the right permissions." | ||||
) | ||||
print( | ||||
"Also consider turning off the log with `%logstop` to avoid this warning." | ||||
) | ||||
|
r0 | |||
|
r895 | 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.""" | ||||
|
r4872 | |||
|
r4870 | if self.logfile is not None: | ||
self.logfile.close() | ||||
self.logfile = None | ||||
else: | ||||
|
r13348 | print("Logging hadn't been started.") | ||
|
r895 | self.log_active = False | ||
# For backwards compatibility, in case anyone was using this. | ||||
close_log = logstop | ||||