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