1"""Logger class for IPython's logging facilities.
2"""
3
4#*****************************************************************************
5# Copyright (C) 2001 Janko Hauser <jhauser@zscout.de> and
6# Copyright (C) 2001-2006 Fernando Perez <fperez@colorado.edu>
7#
8# Distributed under the terms of the BSD License. The full license is in
9# the file COPYING, distributed as part of this software.
10#*****************************************************************************
11
12#****************************************************************************
13# Modules and globals
14
15# Python standard modules
16import glob
17import io
18import logging
19import os
20import time
21
22
23# prevent jedi/parso's debug messages pipe into interactiveshell
24logging.getLogger("parso").setLevel(logging.WARNING)
25
26#****************************************************************************
27# FIXME: This class isn't a mixin anymore, but it still needs attributes from
28# ipython and does input cache management. Finish cleanup later...
29
30class Logger:
31 """A Logfile class with different policies for file creation"""
32
33 def __init__(self, home_dir, logfname='Logger.log', loghead=u'',
34 logmode='over'):
35
36 # this is the full ipython instance, we need some attributes from it
37 # which won't exist until later. What a mess, clean up later...
38 self.home_dir = home_dir
39
40 self.logfname = logfname
41 self.loghead = loghead
42 self.logmode = logmode
43 self.logfile = None
44
45 # Whether to log raw or processed input
46 self.log_raw_input = False
47
48 # whether to also log output
49 self.log_output = False
50
51 # whether to put timestamps before each log entry
52 self.timestamp = False
53
54 # activity control flags
55 self.log_active = False
56
57 # logmode is a validated property
58 def _set_mode(self,mode):
59 if mode not in ['append','backup','global','over','rotate']:
60 raise ValueError('invalid log mode %s given' % mode)
61 self._logmode = mode
62
63 def _get_mode(self):
64 return self._logmode
65
66 logmode = property(_get_mode,_set_mode)
67
68 def logstart(self, logfname=None, loghead=None, logmode=None,
69 log_output=False, timestamp=False, log_raw_input=False):
70 """Generate a new log-file with a default header.
71
72 Raises RuntimeError if the log has already been started"""
73
74 if self.logfile is not None:
75 raise RuntimeError('Log file is already active: %s' %
76 self.logfname)
77
78 # The parameters can override constructor defaults
79 if logfname is not None: self.logfname = logfname
80 if loghead is not None: self.loghead = loghead
81 if logmode is not None: self.logmode = logmode
82
83 # Parameters not part of the constructor
84 self.timestamp = timestamp
85 self.log_output = log_output
86 self.log_raw_input = log_raw_input
87
88 # init depending on the log mode requested
89 isfile = os.path.isfile
90 logmode = self.logmode
91
92 if logmode == 'append':
93 self.logfile = io.open(self.logfname, 'a', encoding='utf-8')
94
95 elif logmode == 'backup':
96 if isfile(self.logfname):
97 backup_logname = self.logfname+'~'
98 # Manually remove any old backup, since os.rename may fail
99 # under Windows.
100 if isfile(backup_logname):
101 os.remove(backup_logname)
102 os.rename(self.logfname,backup_logname)
103 self.logfile = io.open(self.logfname, 'w', encoding='utf-8')
104
105 elif logmode == 'global':
106 self.logfname = os.path.join(self.home_dir,self.logfname)
107 self.logfile = io.open(self.logfname, 'a', encoding='utf-8')
108
109 elif logmode == 'over':
110 if isfile(self.logfname):
111 os.remove(self.logfname)
112 self.logfile = io.open(self.logfname,'w', encoding='utf-8')
113
114 elif logmode == 'rotate':
115 if isfile(self.logfname):
116 if isfile(self.logfname+'.001~'):
117 old = glob.glob(self.logfname+'.*~')
118 old.sort()
119 old.reverse()
120 for f in old:
121 root, ext = os.path.splitext(f)
122 num = int(ext[1:-1])+1
123 os.rename(f, root+'.'+repr(num).zfill(3)+'~')
124 os.rename(self.logfname, self.logfname+'.001~')
125 self.logfile = io.open(self.logfname, 'w', encoding='utf-8')
126
127 if logmode != 'append':
128 self.logfile.write(self.loghead)
129
130 self.logfile.flush()
131 self.log_active = True
132
133 def switch_log(self,val):
134 """Switch logging on/off. val should be ONLY a boolean."""
135
136 if val not in [False,True,0,1]:
137 raise ValueError('Call switch_log ONLY with a boolean argument, '
138 'not with: %s' % val)
139
140 label = {0:'OFF',1:'ON',False:'OFF',True:'ON'}
141
142 if self.logfile is None:
143 print("""
144Logging hasn't been started yet (use logstart for that).
145
146%logon/%logoff are for temporarily starting and stopping logging for a logfile
147which already exists. But you must first start the logging process with
148%logstart (optionally giving a logfile name).""")
149
150 else:
151 if self.log_active == val:
152 print('Logging is already',label[val])
153 else:
154 print('Switching logging',label[val])
155 self.log_active = not self.log_active
156 self.log_active_out = self.log_active
157
158 def logstate(self):
159 """Print a status message about the logger."""
160 if self.logfile is None:
161 print('Logging has not been activated.')
162 else:
163 state = self.log_active and 'active' or 'temporarily suspended'
164 print('Filename :', self.logfname)
165 print('Mode :', self.logmode)
166 print('Output logging :', self.log_output)
167 print('Raw input log :', self.log_raw_input)
168 print('Timestamping :', self.timestamp)
169 print('State :', state)
170
171 def log(self, line_mod, line_ori):
172 """Write the sources to a log.
173
174 Inputs:
175
176 - line_mod: possibly modified input, such as the transformations made
177 by input prefilters or input handlers of various kinds. This should
178 always be valid Python.
179
180 - line_ori: unmodified input line from the user. This is not
181 necessarily valid Python.
182 """
183
184 # Write the log line, but decide which one according to the
185 # log_raw_input flag, set when the log is started.
186 if self.log_raw_input:
187 self.log_write(line_ori)
188 else:
189 self.log_write(line_mod)
190
191 def log_write(self, data, kind='input'):
192 """Write data to the log file, if active"""
193
194 # print('data: %r' % data) # dbg
195 if self.log_active and data:
196 write = self.logfile.write
197 if kind=='input':
198 if self.timestamp:
199 write(time.strftime('# %a, %d %b %Y %H:%M:%S\n', time.localtime()))
200 write(data)
201 elif kind=='output' and self.log_output:
202 odata = u'\n'.join([u'#[Out]# %s' % s
203 for s in data.splitlines()])
204 write(u'%s\n' % odata)
205 try:
206 self.logfile.flush()
207 except OSError:
208 print("Failed to flush the log file.")
209 print(
210 f"Please check that {self.logfname} exists and have the right permissions."
211 )
212 print(
213 "Also consider turning off the log with `%logstop` to avoid this warning."
214 )
215
216 def logstop(self):
217 """Fully stop logging and close log file.
218
219 In order to start logging again, a new logstart() call needs to be
220 made, possibly (though not necessarily) with a new filename, mode and
221 other options."""
222
223 if self.logfile is not None:
224 self.logfile.close()
225 self.logfile = None
226 else:
227 print("Logging hadn't been started.")
228 self.log_active = False
229
230 # For backwards compatibility, in case anyone was using this.
231 close_log = logstop