Skip to content

Commit cfde4e7

Browse files
authored
Add a task base class and basic logger (#1160)
- Adds a very basic base class `Task` that will be used when creating new tasks or refactoring existing tasks. - Adds a very basic logger that can write output to stdout as well as a file. - Adds a test for Logger
1 parent cab4faf commit cfde4e7

File tree

3 files changed

+325
-0
lines changed

3 files changed

+325
-0
lines changed

ush/python/pygw/src/pygw/logger.py

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
"""
2+
Logger
3+
"""
4+
5+
import sys
6+
from pathlib import Path
7+
from typing import Union, List
8+
import logging
9+
10+
11+
class ColoredFormatter(logging.Formatter):
12+
"""
13+
Logging colored formatter
14+
adapted from https://stackoverflow.com/a/56944256/3638629
15+
"""
16+
17+
grey = '\x1b[38;21m'
18+
blue = '\x1b[38;5;39m'
19+
yellow = '\x1b[38;5;226m'
20+
red = '\x1b[38;5;196m'
21+
bold_red = '\x1b[31;1m'
22+
reset = '\x1b[0m'
23+
24+
def __init__(self, fmt):
25+
super().__init__()
26+
self.fmt = fmt
27+
self.formats = {
28+
logging.DEBUG: self.blue + self.fmt + self.reset,
29+
logging.INFO: self.grey + self.fmt + self.reset,
30+
logging.WARNING: self.yellow + self.fmt + self.reset,
31+
logging.ERROR: self.red + self.fmt + self.reset,
32+
logging.CRITICAL: self.bold_red + self.fmt + self.reset
33+
}
34+
35+
def format(self, record):
36+
log_fmt = self.formats.get(record.levelno)
37+
formatter = logging.Formatter(log_fmt)
38+
return formatter.format(record)
39+
40+
41+
class Logger:
42+
"""
43+
Improved logging
44+
"""
45+
LOG_LEVELS = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL']
46+
DEFAULT_LEVEL = 'INFO'
47+
DEFAULT_FORMAT = '%(asctime)s - %(levelname)-8s - %(name)-12s: %(message)s'
48+
def __init__(self, name: str = None,
49+
level: str = DEFAULT_LEVEL,
50+
_format: str = DEFAULT_FORMAT,
51+
colored_log: bool = False,
52+
logfile_path: Union[str, Path] = None):
53+
"""
54+
Initialize Logger
55+
56+
Parameters
57+
----------
58+
name : str
59+
Name of the Logger object
60+
default : None
61+
level : str
62+
Desired Logging level
63+
default : 'INFO'
64+
_format : str
65+
Desired Logging Format
66+
default : '%(asctime)s - %(levelname)-8s - %(name)-12s: %(message)s'
67+
colored_log : bool
68+
Use colored logging for stdout
69+
default: False
70+
logfile_path : str or Path
71+
Path for logging to a file
72+
default : None
73+
"""
74+
75+
self.name = name
76+
self.level = level.upper()
77+
self.format = _format
78+
self.colored_log = colored_log
79+
80+
if self.level not in Logger.LOG_LEVELS:
81+
raise LookupError('{self.level} is unknown logging level\n' +
82+
'Currently supported log levels are:\n' +
83+
f'{" | ".join(Logger.LOG_LEVELS)}')
84+
85+
# Initialize the root logger if no name is present
86+
self._logger = logging.getLogger(name) if name else logging.getLogger()
87+
88+
self._logger.setLevel(self.level)
89+
90+
_handlers = []
91+
# Add console handler for logger
92+
_handler = Logger.add_stream_handler(
93+
level=self.level,
94+
_format=self.format,
95+
colored_log=self.colored_log,
96+
)
97+
_handlers.append(_handler)
98+
self._logger.addHandler(_handler)
99+
100+
# Add file handler for logger
101+
if logfile_path is not None:
102+
_handler = Logger.add_file_handler(logfile_path, level=self.level, _format=self.format)
103+
self._logger.addHandler(_handler)
104+
_handlers.append(_handler)
105+
106+
def __getattr__(self, attribute):
107+
"""
108+
Allows calling logging module methods directly
109+
110+
Parameters
111+
----------
112+
attribute : str
113+
attribute name of a logging object
114+
115+
Returns
116+
-------
117+
attribute : logging attribute
118+
"""
119+
return getattr(self._logger, attribute)
120+
121+
def get_logger(self):
122+
"""
123+
Return the logging object
124+
125+
Returns
126+
-------
127+
logger : Logger object
128+
"""
129+
return self._logger
130+
131+
@classmethod
132+
def add_handlers(cls, logger: logging.Logger, handlers: List[logging.Handler]):
133+
"""
134+
Add a list of handlers to a logger
135+
136+
Parameters
137+
----------
138+
logger : logging.Logger
139+
Logger object to add a new handler to
140+
handlers: list
141+
A list of handlers to be added to the logger object
142+
143+
Returns
144+
-------
145+
logger : Logger object
146+
"""
147+
for handler in handlers:
148+
logger.addHandler(handler)
149+
150+
return logger
151+
152+
@classmethod
153+
def add_stream_handler(cls, level: str = DEFAULT_LEVEL,
154+
_format: str = DEFAULT_FORMAT,
155+
colored_log: bool = False):
156+
"""
157+
Create stream handler
158+
This classmethod will allow setting a custom stream handler on children
159+
160+
Parameters
161+
----------
162+
level : str
163+
logging level
164+
default : 'INFO'
165+
_format : str
166+
logging format
167+
default : '%(asctime)s - %(levelname)-8s - %(name)-12s: %(message)s'
168+
colored_log : bool
169+
enable colored output for stdout
170+
default : False
171+
172+
Returns
173+
-------
174+
handler : logging.Handler
175+
stream handler of a logging object
176+
"""
177+
178+
handler = logging.StreamHandler(sys.stdout)
179+
handler.setLevel(level)
180+
_format = ColoredFormatter(_format) if colored_log else logging.Formatter(_format)
181+
handler.setFormatter(_format)
182+
183+
return handler
184+
185+
@classmethod
186+
def add_file_handler(cls, logfile_path: Union[str, Path],
187+
level: str = DEFAULT_LEVEL,
188+
_format: str = DEFAULT_FORMAT):
189+
"""
190+
Create file handler.
191+
This classmethod will allow setting custom file handler on children
192+
Create stream handler
193+
This classmethod will allow setting a custom stream handler on children
194+
195+
Parameters
196+
----------
197+
logfile_path: str or Path
198+
Path for writing out logfiles from logging
199+
default : False
200+
level : str
201+
logging level
202+
default : 'INFO'
203+
_format : str
204+
logging format
205+
default : '%(asctime)s - %(levelname)-8s - %(name)-12s: %(message)s'
206+
207+
Returns
208+
-------
209+
handler : logging.Handler
210+
file handler of a logging object
211+
"""
212+
213+
logfile_path = Path(logfile_path)
214+
215+
# Create the directory containing the logfile_path
216+
if not logfile_path.parent.is_dir():
217+
logfile_path.mkdir(parents=True, exist_ok=True)
218+
219+
handler = logging.FileHandler(str(logfile_path))
220+
handler.setLevel(level)
221+
handler.setFormatter(logging.Formatter(_format))
222+
223+
return handler

ush/python/pygw/src/pygw/task.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
class Task:
2+
"""
3+
Base class for all tasks
4+
"""
5+
6+
def __init__(self, config, *args, **kwargs):
7+
"""
8+
Every task needs a config.
9+
Additional arguments (or key-value arguments) can be provided.
10+
11+
Parameters
12+
----------
13+
config : Dict
14+
dictionary object containing task configuration
15+
16+
*args : tuple
17+
Additional arguments to `Task`
18+
19+
**kwargs : dict, optional
20+
Extra keyword arguments to `Task`
21+
"""
22+
23+
# Store the config and arguments as attributes of the object
24+
self.config = config
25+
26+
for arg in args:
27+
setattr(self, str(arg), arg)
28+
29+
for key, value in kwargs.items():
30+
setattr(self, key, value)
31+
32+
def initialize(self):
33+
"""
34+
Initialize methods for a task
35+
"""
36+
pass
37+
38+
def configure(self):
39+
"""
40+
Configuration methods for a task in preparation for execution
41+
"""
42+
pass
43+
44+
def execute(self):
45+
"""
46+
Execute methods for a task
47+
"""
48+
pass
49+
50+
def finalize(self):
51+
"""
52+
Methods for after the execution that produces output task
53+
"""
54+
pass
55+
56+
def clean(self):
57+
"""
58+
Methods to clean after execution and finalization prior to closing out a task
59+
"""
60+
pass
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from pygw.logger import Logger
2+
3+
level = 'debug'
4+
number_of_log_msgs = 5
5+
reference = {'debug': "Logging test has started",
6+
'info': "Logging to 'logger.log' in the script dir",
7+
'warning': "This is my last warning, take heed",
8+
'error': "This is an error",
9+
'critical': "He's dead, She's dead. They are all dead!"}
10+
11+
12+
def test_logger(tmp_path):
13+
"""Test log file"""
14+
15+
logfile = tmp_path / "logger.log"
16+
17+
try:
18+
log = Logger('test_logger', level=level, logfile_path=logfile, colored_log=True)
19+
log.debug(reference['debug'])
20+
log.info(reference['info'])
21+
log.warning(reference['warning'])
22+
log.error(reference['error'])
23+
log.critical(reference['critical'])
24+
except Exception as e:
25+
raise AssertionError(f'logging failed as {e}')
26+
27+
# Make sure log to file created messages
28+
try:
29+
with open(logfile, 'r') as fh:
30+
log_msgs = fh.readlines()
31+
except Exception as e:
32+
raise AssertionError(f'failed reading log file as {e}')
33+
34+
# Ensure number of messages are same
35+
log_msgs_in_logfile = len(log_msgs)
36+
assert log_msgs_in_logfile == number_of_log_msgs
37+
38+
# Ensure messages themselves are same
39+
for count, line in enumerate(log_msgs):
40+
lev = line.split('-')[3].strip().lower()
41+
message = line.split(':')[-1].strip()
42+
assert reference[lev] == message

0 commit comments

Comments
 (0)