Coverage for klayout_pex/log/logger.py: 95%

79 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-17 17:24 +0000

1# 

2# -------------------------------------------------------------------------------- 

3# SPDX-FileCopyrightText: 2024 Martin Jan Köhler and Harald Pretl 

4# Johannes Kepler University, Institute for Integrated Circuits. 

5# 

6# This file is part of KPEX  

7# (see https://github.com/martinjankoehler/klayout-pex). 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <http://www.gnu.org/licenses/>. 

21# SPDX-License-Identifier: GPL-3.0-or-later 

22# -------------------------------------------------------------------------------- 

23# 

24from __future__ import annotations 

25from enum import IntEnum 

26from functools import cached_property 

27import logging 

28import rich.console 

29import rich.logging 

30from typing import * 

31 

32 

33class LogLevel(IntEnum): 

34 ALL = 0 

35 DEBUG = 10 

36 SUBPROCESS = 12 

37 VERBOSE = 15 

38 INFO = 20 

39 WARNING = 30 

40 ERROR = 40 

41 CRITICAL = 50 

42 DEFAULT = SUBPROCESS 

43 

44 @classmethod 

45 @cached_property 

46 def level_by_name(cls) -> Dict[str, LogLevel]: 

47 return {e.name: e for e in cls} 

48 

49 

50class LogLevelFormatter(logging.Formatter): 

51 def format(self, record: logging.LogRecord) -> str: 

52 msg = record.getMessage() 

53 match record.levelno: 

54 case LogLevel.WARNING.value: return f"[yellow]{msg}" 

55 case LogLevel.ERROR.value: return f"[red]{msg}" 

56 case _: 

57 return msg 

58 

59 

60class LogLevelFilter(logging.Filter): 

61 def __init__(self, levels: Iterable[str], invert: bool = False): 

62 super().__init__() 

63 self.levels = levels 

64 self.invert = invert 

65 

66 def filter(self, record: logging.LogRecord) -> bool: 

67 if self.invert: 

68 return record.levelname not in self.levels 

69 else: 

70 return record.levelname in self.levels 

71 

72 

73console = rich.console.Console() 

74__logger = logging.getLogger("__kpex__") 

75 

76 

77def set_log_level(log_level: LogLevel): 

78 __logger.setLevel(log_level) 

79 

80 

81def register_additional_handler(handler: logging.Handler): 

82 """ 

83 Adds a new handler to the default logger. 

84 

85 :param handler: The new handler. Must be of type ``logging.Handler`` 

86 or its subclasses. 

87 """ 

88 __logger.addHandler(handler) 

89 

90 

91def deregister_additional_handler(handler: logging.Handler): 

92 """ 

93 Removes a registered handler from the default logger. 

94 

95 :param handler: The handler. If not registered, the behavior 

96 of this function is undefined. 

97 """ 

98 __logger.removeHandler(handler) 

99 

100 

101 

102def configure_logger(): 

103 global __logger, console 

104 

105 for level in LogLevel: 

106 logging.addLevelName(level=level.value, levelName=level.name) 

107 

108 subprocess_rich_handler = rich.logging.RichHandler( 

109 console=console, 

110 show_time=False, 

111 omit_repeated_times=False, 

112 show_level=False, 

113 show_path=False, 

114 enable_link_path=False, 

115 markup=False, 

116 tracebacks_word_wrap=False, 

117 keywords=[] 

118 ) 

119 subprocess_rich_handler.addFilter(LogLevelFilter(['SUBPROCESS'])) 

120 

121 rich_handler = rich.logging.RichHandler( 

122 console=console, 

123 omit_repeated_times=False, 

124 show_level=True, 

125 markup=True, 

126 rich_tracebacks=True, 

127 tracebacks_suppress=[], 

128 keywords=[] 

129 ) 

130 

131 rich_handler.setFormatter(LogLevelFormatter(fmt='%(message)s', datefmt='[%X]')) 

132 rich_handler.addFilter(LogLevelFilter(['SUBPROCESS'], invert=True)) 

133 

134 set_log_level(LogLevel.SUBPROCESS) 

135 

136 __logger.handlers.clear() 

137 __logger.addHandler(subprocess_rich_handler) 

138 __logger.addHandler(rich_handler) 

139 

140 

141def debug(*args, **kwargs): 

142 if not kwargs.get('stacklevel'): # ensure logged file location is correct 

143 kwargs['stacklevel'] = 2 

144 __logger.debug(*args, **kwargs) 

145 

146 

147def subproc(msg: object, **kwargs): 

148 if not kwargs.get('stacklevel'): # ensure logged file location is correct 

149 kwargs['stacklevel'] = 2 

150 __logger.log(LogLevel.SUBPROCESS, msg, **kwargs) 

151 

152 

153def rule(title: str = '', **kwargs): # pragma: no cover 

154 """ 

155 Prints a horizontal line on the terminal enclosing the first argument 

156 if the log level is <= INFO. 

157 

158 Kwargs are passed to https://rich.readthedocs.io/en/stable/reference/console.html#rich.console.Console.rule 

159 

160 :param title: A title string to enclose in the console rule 

161 """ 

162 console.rule(title) 

163 

164 

165def info(*args, **kwargs): 

166 if not kwargs.get('stacklevel'): # ensure logged file location is correct 

167 kwargs['stacklevel'] = 2 

168 __logger.info(*args, **kwargs) 

169 

170 

171def warning(*args, **kwargs): 

172 if not kwargs.get('stacklevel'): # ensure logged file location is correct 

173 kwargs['stacklevel'] = 2 

174 __logger.warning(*args, **kwargs) 

175 

176 

177def error(*args, **kwargs): 

178 if not kwargs.get('stacklevel'): # ensure logged file location is correct 

179 kwargs['stacklevel'] = 2 

180 __logger.error(*args, **kwargs) 

181 

182 

183configure_logger()