Peano
Loading...
Searching...
No Matches
Logger.py
Go to the documentation of this file.
1# This file is part of the Peano project. For conditions of distribution and
2# use, please see the copyright notice at www.peano-framework.org
3
4import atexit
5import datetime
6import enum
7import io
8import os
9import sys
10
11
12class LoggerPrintOption(enum.Enum):
13 """! Controls whether Logger.log() also prints a message to the console. """
14
15 ALWAYS_PRINT = 1
16 IF_VERBOSE = 2
17 NEVER_PRINT = 3
18
19
20class LoggerVerbosity(enum.Enum):
21 """! Controls how much detail Logger.log()/new_section() print to the console.
22
23 OFF: nothing tagged IF_VERBOSE is printed.
24 TEXT: plain, textual verbose output is printed.
25 VISUALISATION: verbose output is printed, and the run additionally
26 produces/updates a visualisation of what is going on.
27
28 TEXT and VISUALISATION both switch on the same "verbose" console output;
29 they only differ in whether a visualisation is on top of that.
30 """
31
32 OFF = 1
33 TEXT = 2
34 VISUALISATION = 3
35
36
37class Logger:
38 """!
39
40 Singleton logger for the Peano 4 Python API
41
42 Python has no dedicated singleton keyword or construct. The idiomatic way
43 to get the same effect is to override __new__, so repeated calls to
44 Logger() all return the one and only instance instead of creating a new
45 object every time.
46
47 The logger holds an in-memory output stream (self._stream), which is
48 protected and never accessed from the outside. Use log()/new_section()
49 to accumulate details into it over the course of the run; nothing is
50 written to disk until the stream is flushed into file_name.
51
52 Once the singleton is created, it registers itself with atexit, so
53 close() is invoked automatically when the program terminates - you do
54 not have to call it yourself.
55
56 """
57
58 _instance = None
59
60 def __new__(cls):
61 if cls._instance is None:
62 cls._instance = super().__new__(cls)
63 cls._instance._verbosity = LoggerVerbosity.OFF
64 cls._instance._file_name = "log.md"
65 cls._instance._stream = io.StringIO()
66 cls._instance._visualisation_window_shown = False
67 atexit.register(cls._instance.close)
68 return cls._instance
69
70 @property
71 def verbosity(self) -> LoggerVerbosity:
72 return self._verbosity
73
74 @verbosity.setter
75 def verbosity(self, value: LoggerVerbosity):
76 if value == LoggerVerbosity.VISUALISATION and not self._visualisation_window_shown:
79 self._verbosity = value
80
82 """!
83 Open a window displaying the Peano logo. Called once, the first time
84 verbosity is ever set to LoggerVerbosity.VISUALISATION. If no display
85 is available (e.g. a headless run), matplotlib prints a warning and
86 this silently is a no-op rather than failing the run.
87
88 The window is opened non-blocking, so the run continues straight
89 away. It is not closed automatically though: we register a
90 blocking plt.show() to run at program exit, so the window stays on
91 screen - the user has to close it manually - rather than vanishing
92 the moment the process would otherwise terminate.
93 """
94 import matplotlib.image as mpimg
95 import matplotlib.pyplot as plt
96
97 icon_file = os.path.join(
98 os.path.dirname(os.path.abspath(__file__)),
99 "..", "..", "..", "documentation", "Peano-icon.png",
100 )
101 image = mpimg.imread(icon_file)
102 plt.figure("Peano")
103 plt.imshow(image)
104 plt.axis("off")
105 plt.show(block=False)
106 plt.pause(0.001)
107
108 atexit.register(plt.show)
109
110 @property
111 def is_visualisation(self) -> bool:
112 """!
113
114 True if and only if verbosity is set to LoggerVerbosity.VISUALISATION.
115 Implies is_text, since visualising always dumps text alongside the
116 visualisation (@see is_text) - callers that branch on both should
117 therefore never treat these two as mutually exclusive alternatives.
118
119 """
120 return self._verbosity == LoggerVerbosity.VISUALISATION
121
122 @property
123 def is_text(self) -> bool:
124 """!
125
126 True if verbosity is set to LoggerVerbosity.TEXT or
127 LoggerVerbosity.VISUALISATION - visualising always dumps text
128 alongside the visualisation, it is never the only output, so this
129 is also true whenever is_visualisation is (@see is_visualisation;
130 the converse does not hold: plain TEXT verbosity produces no
131 visualisation).
132
133 """
134 return self._verbosity in (LoggerVerbosity.TEXT, LoggerVerbosity.VISUALISATION)
135
136 @property
137 def file_name(self) -> str:
138 return self._file_name
139
140 @file_name.setter
141 def file_name(self, value: str):
142 self._file_name = value
143
144 def log(self,
145 message: str,
146 print_option: LoggerPrintOption = LoggerPrintOption.IF_VERBOSE,
147 ):
148 """! By default, only printed to the console if verbosity is not OFF. """
149 if print_option == LoggerPrintOption.ALWAYS_PRINT or (
150 print_option == LoggerPrintOption.IF_VERBOSE and self._verbosity != LoggerVerbosity.OFF
151 ):
152 print(message)
153 self._stream.write( message )
154 self._stream.write( "\n" )
155
156 AcceptWarningsEnvironmentVariable = "PEANO_ACCEPT_WARNINGS"
157
158 def warn_and_confirm(self, message: str):
159 """!
160
161 Log a warning and let the user decide whether to continue
162
163 The warning is always printed and written into the log. Afterwards,
164 the user has to accept the warning or abort. If the user aborts, the
165 routine raises an exception, i.e. the Python script terminates.
166
167 If the environment variable PEANO_ACCEPT_WARNINGS is set to 1, all
168 warnings are accepted without asking. Use this for batch jobs. If
169 the variable is not set and there is no terminal to ask the user
170 (stdin is not a terminal), the routine aborts.
171
172 @param message: str
173 The warning. It should explain what goes wrong if the user
174 accepts it.
175
176 """
177 self.log("Warning: " + message, LoggerPrintOption.ALWAYS_PRINT)
178
179 if os.environ.get(Logger.AcceptWarningsEnvironmentVariable) == "1":
180 self.log(f"Warning accepted, as {Logger.AcceptWarningsEnvironmentVariable}=1", LoggerPrintOption.ALWAYS_PRINT)
181 return
182
183 if not sys.stdin.isatty():
184 self.log(
185 f"Abort, as there is no terminal to confirm the warning. Set {Logger.AcceptWarningsEnvironmentVariable}=1 to accept all warnings",
186 LoggerPrintOption.ALWAYS_PRINT,
187 )
188 raise Exception("Warning not accepted: " + message)
189
190 while True:
191 answer = input("[A]ccept or a[b]ort? ").strip().lower()
192 if answer in ("a", "accept"):
193 self.log("Warning accepted by user", LoggerPrintOption.NEVER_PRINT)
194 return
195 if answer in ("b", "abort"):
196 self.log("Abort requested by user", LoggerPrintOption.NEVER_PRINT)
197 raise Exception("Warning not accepted: " + message)
198
199
200 def new_section(self, section_name: str):
201 if self._verbosity != LoggerVerbosity.OFF:
202 print("------------------------------")
203 print(section_name)
204 print("------------------------------")
205 self._stream.write("\n\n## {}\n\n".format(section_name))
206
207 def close(self):
208 """!
209 Called automatically when the program terminates (see atexit
210 registration in __new__). Flushes the accumulated stream into
211 file_name, preceded by a section stating when the file was closed.
212
213 If file_name already exists, we do not overwrite it. Instead, we
214 attach a timestamp to the file name, i.e. log.md becomes
215 something similar to log-20240101-120000.md.
216
217 This routine always prints a report to the console about the newly
218 generated logging file.
219 """
220 closed_at = datetime.datetime.now()
221
222 self.new_section("File creation")
223 self.log( f"Closed at {closed_at.isoformat()}.")
224
225 file_name = self._file_name
226 if os.path.exists(file_name):
227 base, ext = os.path.splitext(file_name)
228 file_name = f"{base}-{closed_at.strftime('%Y%m%d-%H%M%S')}{ext}"
229 with open(file_name, "w") as file:
230 file.write(self._stream.getvalue())
231
232 print( "Wrote output log to file {}".format(file_name))
Controls whether Logger.log() also prints a message to the console.
Definition Logger.py:12
Controls how much detail Logger.log()/new_section() print to the console.
Definition Logger.py:20
Singleton logger for the Peano 4 Python API.
Definition Logger.py:37
LoggerVerbosity verbosity(self)
Definition Logger.py:71
_show_visualisation_window(self)
Open a window displaying the Peano logo.
Definition Logger.py:81
bool is_visualisation(self)
True if and only if verbosity is set to LoggerVerbosity.VISUALISATION.
Definition Logger.py:111
new_section(self, str section_name)
Definition Logger.py:200
warn_and_confirm(self, str message)
Log a warning and let the user decide whether to continue.
Definition Logger.py:158
bool is_text(self)
True if verbosity is set to LoggerVerbosity.TEXT or LoggerVerbosity.VISUALISATION - visualising alway...
Definition Logger.py:123
close(self)
Called automatically when the program terminates (see atexit registration in new).
Definition Logger.py:207
log(self, str message, LoggerPrintOption print_option=LoggerPrintOption.IF_VERBOSE)
By default, only printed to the console if verbosity is not OFF.
Definition Logger.py:147