Skip to content

Commit 4dcb29b

Browse files
authored
Merge pull request #654 from vkarak/refactor/colorize-logger
[refactor] Refactor `PrettyPrinter` and enable colors in the general logger
2 parents e5ef7fd + 3aca1e1 commit 4dcb29b

File tree

9 files changed

+165
-83
lines changed

9 files changed

+165
-83
lines changed

reframe/core/logging.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from datetime import datetime
1212

1313
import reframe
14+
import reframe.utility.color as color
1415
import reframe.core.debug as debug
1516
import reframe.utility.os_ext as os_ext
1617
from reframe.core.exceptions import ConfigError, LoggingError
@@ -352,6 +353,7 @@ def __init__(self, logger=None, check=None):
352353
}
353354
)
354355
self.check = check
356+
self.colorize = False
355357

356358
def __repr__(self):
357359
return debug.repr(self)
@@ -415,6 +417,21 @@ def log(self, level, msg, *args, **kwargs):
415417
def verbose(self, message, *args, **kwargs):
416418
self.log(VERBOSE, message, *args, **kwargs)
417419

420+
def warning(self, message, *args, **kwargs):
421+
message = '%s: %s' % (sys.argv[0], message)
422+
if self.colorize:
423+
message = color.colorize(message, color.YELLOW)
424+
425+
super().warning(message, *args, **kwargs)
426+
427+
def error(self, message, *args, **kwargs):
428+
message = '%s: %s' % (sys.argv[0], message)
429+
if self.colorize:
430+
message = color.colorize(message, color.RED)
431+
432+
super().error(message, *args, **kwargs)
433+
434+
418435

419436
# A logger that doesn't log anything
420437
null_logger = LoggerAdapter()

reframe/frontend/argparse.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,3 +138,11 @@ def parse_args(self, args=None, namespace=None):
138138
)
139139

140140
return options
141+
142+
143+
def format_options(namespace):
144+
"""Format parsed arguments in ``namespace``."""
145+
ret = 'Command-line configuration:\n'
146+
ret += '\n'.join([' %s=%s' % (attr, val)
147+
for attr, val in sorted(namespace.__dict__.items())])
148+
return ret

reframe/frontend/cli.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,20 @@
88
import reframe.core.config as config
99
import reframe.core.logging as logging
1010
import reframe.core.runtime as runtime
11+
import reframe.frontend.argparse as argparse
1112
import reframe.frontend.check_filters as filters
1213
import reframe.utility as util
1314
import reframe.utility.os_ext as os_ext
1415
from reframe.core.exceptions import (EnvironError, ConfigError, ReframeError,
1516
ReframeFatalError, format_exception,
1617
SystemAutodetectionError)
17-
from reframe.frontend.argparse import ArgumentParser
1818
from reframe.frontend.executors import Runner
1919
from reframe.frontend.executors.policies import (SerialExecutionPolicy,
2020
AsynchronousExecutionPolicy)
2121
from reframe.frontend.loader import RegressionCheckLoader
2222
from reframe.frontend.printer import PrettyPrinter
2323

24+
2425
def format_check(check, detailed):
2526
lines = [' * %s (found in %s)' % (check.name,
2627
inspect.getfile(type(check)))]
@@ -48,7 +49,7 @@ def list_checks(checks, printer, detailed=False):
4849

4950
def main():
5051
# Setup command line options
51-
argparser = ArgumentParser()
52+
argparser = argparse.ArgumentParser()
5253
output_options = argparser.add_argument_group(
5354
'Options controlling regression directories')
5455
locate_options = argparser.add_argument_group(
@@ -255,6 +256,9 @@ def main():
255256
sys.stderr.write('could not configure logging: %s\n' % e)
256257
sys.exit(1)
257258

259+
# Set colors in logger
260+
logging.getlogger().colorize = options.colorize
261+
258262
# Setup printer
259263
printer = PrettyPrinter()
260264
printer.colorize = options.colorize
@@ -377,7 +381,7 @@ def main():
377381
prefix=reframe.INSTALL_PREFIX,
378382
recurse=settings.checks_path_recurse)
379383

380-
printer.log_config(options)
384+
printer.debug(argparse.format_options(options))
381385

382386
# Print command line
383387
printer.info('Command line: %s' % ' '.join(sys.argv))
@@ -452,7 +456,7 @@ def main():
452456
rt.modules_system.load_module(m, force=True)
453457
raise EnvironError("test")
454458
except EnvironError as e:
455-
printer.warning("could not load module '%s' correctly: "
459+
printer.warning("could not load module '%s' correctly: "
456460
"Skipping..." % m)
457461
printer.debug(str(e))
458462

reframe/frontend/executors/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ def runall(self, checks):
172172
self._printer.separator('short double line',
173173
'Running %d check(s)' % len(checks))
174174
self._printer.timestamp('Started on', 'short double line')
175-
self._printer.info()
175+
self._printer.info('')
176176
self._runall(checks)
177177
if self._max_retries:
178178
self._retry_failed(checks)

reframe/frontend/printer.py

Lines changed: 14 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,24 @@
1-
import abc
21
import datetime
32
import sys
43

5-
import reframe.core.debug as debug
64
import reframe.core.logging as logging
7-
8-
9-
class Colorizer(abc.ABC):
10-
def __repr__(self):
11-
return debug.repr(self)
12-
13-
@abc.abstractmethod
14-
def colorize(string, foreground, background):
15-
"""Colorize a string.
16-
17-
Keyword arguments:
18-
string -- the string to be colorized
19-
foreground -- the foreground color
20-
background -- the background color
21-
"""
22-
23-
24-
class AnsiColorizer(Colorizer):
25-
escape_seq = '\033'
26-
reset_term = '[0m'
27-
28-
# Escape sequences for fore/background colors
29-
fgcolor = '[3'
30-
bgcolor = '[4'
31-
32-
# color values
33-
black = '0m'
34-
red = '1m'
35-
green = '2m'
36-
yellow = '3m'
37-
blue = '4m'
38-
magenta = '5m'
39-
cyan = '6m'
40-
white = '7m'
41-
default = '9m'
42-
43-
def colorize(string, foreground, background=None):
44-
return (AnsiColorizer.escape_seq +
45-
AnsiColorizer.fgcolor + foreground + string +
46-
AnsiColorizer.escape_seq + AnsiColorizer.reset_term)
5+
import reframe.utility.color as color
476

487

498
class PrettyPrinter:
509
"""Pretty printing facility for the framework.
5110
52-
Final printing is delegated to an internal logger, which is responsible for
53-
printing both to standard output and in a special output file."""
11+
It takes care of formatting the progress output and adds some more
12+
cosmetics to specific levels of messages, such as warnings and errors.
13+
14+
The actual printing is delegated to an internal logger, which is
15+
responsible for printing.
16+
"""
5417

5518
def __init__(self):
5619
self.colorize = True
5720
self.line_width = 78
5821
self.status_width = 10
59-
self._logger = logging.getlogger()
60-
61-
def __repr__(self):
62-
return debug.repr(self)
6322

6423
def separator(self, linestyle, msg=''):
6524
if linestyle == 'short double line':
@@ -82,13 +41,13 @@ def status(self, status, message='', just=None, level=logging.INFO):
8241
if self.colorize:
8342
status_stripped = status.strip().lower()
8443
if status_stripped == 'skip':
85-
status = AnsiColorizer.colorize(status, AnsiColorizer.yellow)
44+
status = color.colorize(status, color.YELLOW)
8645
elif status_stripped in ['fail', 'failed']:
87-
status = AnsiColorizer.colorize(status, AnsiColorizer.red)
46+
status = color.colorize(status, color.RED)
8847
else:
89-
status = AnsiColorizer.colorize(status, AnsiColorizer.green)
48+
status = color.colorize(status, color.GREEN)
9049

91-
self._logger.log(level, '[ %s ] %s' % (status, message))
50+
logging.getlogger().log(level, '[ %s ] %s' % (status, message))
9251

9352
def result(self, check, partition, environ, success):
9453
if success:
@@ -107,24 +66,6 @@ def timestamp(self, msg='', separator=None):
10766
else:
10867
self.info(msg)
10968

110-
def info(self, msg=''):
111-
self._logger.info(msg)
112-
113-
def debug(self, msg=''):
114-
self._logger.debug(msg)
115-
116-
def warning(self, msg):
117-
msg = AnsiColorizer.colorize('%s: %s' % (sys.argv[0], msg),
118-
AnsiColorizer.yellow)
119-
self._logger.warning(msg)
120-
121-
def error(self, msg):
122-
msg = AnsiColorizer.colorize('%s: %s' % (sys.argv[0], msg),
123-
AnsiColorizer.red)
124-
self._logger.error(msg)
125-
126-
def log_config(self, options):
127-
opt_list = [' %s=%s' % (attr, val)
128-
for attr, val in sorted(options.__dict__.items())]
129-
130-
self._logger.debug('configuration\n%s' % '\n'.join(opt_list))
69+
def __getattr__(self, attr):
70+
# delegate all other attribute lookup to the underlying logger
71+
return getattr(logging.getlogger(), attr)

reframe/utility/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import abc
12
import collections
23
import importlib
34
import importlib.util

reframe/utility/color.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
class ColorRGB:
2+
def __init__(self, r, g, b):
3+
self.__check_rgb(r)
4+
self.__check_rgb(g)
5+
self.__check_rgb(b)
6+
self.__r = r
7+
self.__g = g
8+
self.__b = b
9+
10+
def __check_rgb(self, x):
11+
if (x < 0) or x > 255:
12+
raise ValueError('RGB color code must be in [0,255]')
13+
14+
@property
15+
def r(self):
16+
return self.__r
17+
18+
@property
19+
def g(self):
20+
return self.__g
21+
22+
@property
23+
def b(self):
24+
return self.__b
25+
26+
def __repr__(self):
27+
return 'ColorRGB(%s, %s, %s)' % (self.__r, self.__g, self.__b)
28+
29+
30+
# Predefined colors
31+
BLACK = ColorRGB(0, 0, 0)
32+
RED = ColorRGB(255, 0, 0)
33+
GREEN = ColorRGB(0, 255, 0)
34+
YELLOW = ColorRGB(255, 255, 0)
35+
BLUE = ColorRGB(0, 0, 255)
36+
MAGENTA = ColorRGB(255, 0, 255)
37+
CYAN = ColorRGB(0, 255, 255)
38+
WHITE = ColorRGB(255, 255, 255)
39+
40+
41+
class _AnsiPalette:
42+
"""Class for colorizing strings using ANSI meta-characters."""
43+
44+
escape_seq = '\033'
45+
reset_term = '[0m'
46+
47+
# Escape sequences for fore/background colors
48+
fgcolor = '[3'
49+
bgcolor = '[4'
50+
51+
# color values
52+
colors = {
53+
BLACK: '0m',
54+
RED: '1m',
55+
GREEN: '2m',
56+
YELLOW: '3m',
57+
BLUE: '4m',
58+
MAGENTA: '5m',
59+
CYAN: '6m',
60+
WHITE: '7m'
61+
}
62+
63+
def colorize(string, foreground):
64+
try:
65+
foreground = _AnsiPalette.colors[foreground]
66+
except KeyError:
67+
raise ValueError('could not find an ANSI representation '
68+
'for color: %s' % foreground) from None
69+
70+
return (_AnsiPalette.escape_seq +
71+
_AnsiPalette.fgcolor + foreground + string +
72+
_AnsiPalette.escape_seq + _AnsiPalette.reset_term)
73+
74+
75+
def colorize(string, foreground, *, palette='ANSI'):
76+
"""Colorize a string.
77+
78+
:arg string: The string to be colorized.
79+
:arg foreground: The foreground color.
80+
:arg palette: The palette to get colors from.
81+
"""
82+
if palette != 'ANSI':
83+
raise ValueError('unknown color palette: %s' % palette)
84+
85+
return _AnsiPalette.colorize(string, foreground)

unittests/test_color.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import unittest
2+
3+
import reframe.utility.color as color
4+
5+
6+
class TestColors(unittest.TestCase):
7+
def test_color_rgb(self):
8+
c = color.ColorRGB(128, 0, 34)
9+
self.assertEqual(128, c.r)
10+
self.assertEqual(0, c.g)
11+
self.assertEqual(34, c.b)
12+
13+
self.assertRaises(ValueError, color.ColorRGB, -1, 0, 34)
14+
self.assertRaises(ValueError, color.ColorRGB, 0, -1, 34)
15+
self.assertRaises(ValueError, color.ColorRGB, 0, 28, -1)
16+
17+
def test_colorize(self):
18+
s = color.colorize('hello', color.RED, palette='ANSI')
19+
self.assertIn('\033', s)
20+
self.assertIn('[3', s)
21+
self.assertIn('1m', s)
22+
23+
with self.assertRaises(ValueError):
24+
color.colorize('hello', color.RED, palette='FOO')
25+
26+
with self.assertRaises(ValueError):
27+
color.colorize('hello', color.ColorRGB(128, 0, 34), palette='ANSI')

unittests/test_logging.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -358,11 +358,10 @@ def test_logging_context_check(self):
358358
rlog.getlogger().error('error from context')
359359

360360
rlog.getlogger().error('error outside context')
361-
362-
self.assertTrue(
363-
self.found_in_logfile('random_check: error from context'))
364-
self.assertTrue(
365-
self.found_in_logfile('reframe: error outside context'))
361+
self.assertTrue(self.found_in_logfile(
362+
'random_check: %s: error from context' % sys.argv[0]))
363+
self.assertTrue(self.found_in_logfile(
364+
'reframe: %s: error outside context' % sys.argv[0]))
366365

367366
def test_logging_context_error(self):
368367
rlog.configure_logging(self.logging_config)

0 commit comments

Comments
 (0)