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

81 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-08 18:54 +0000

1# 

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

3# SPDX-FileCopyrightText: 2024-2025 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 get_log_level() -> LogLevel: 

82 return LogLevel(__logger.level) 

83 

84 

85def register_additional_handler(handler: logging.Handler): 

86 """ 

87 Adds a new handler to the default logger. 

88 

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

90 or its subclasses. 

91 """ 

92 __logger.addHandler(handler) 

93 

94 

95def deregister_additional_handler(handler: logging.Handler): 

96 """ 

97 Removes a registered handler from the default logger. 

98 

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

100 of this function is undefined. 

101 """ 

102 __logger.removeHandler(handler) 

103 

104 

105 

106def configure_logger(): 

107 global __logger, console 

108 

109 for level in LogLevel: 

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

111 

112 subprocess_rich_handler = rich.logging.RichHandler( 

113 console=console, 

114 show_time=False, 

115 omit_repeated_times=False, 

116 show_level=False, 

117 show_path=False, 

118 enable_link_path=False, 

119 markup=False, 

120 tracebacks_word_wrap=False, 

121 keywords=[] 

122 ) 

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

124 

125 rich_handler = rich.logging.RichHandler( 

126 console=console, 

127 omit_repeated_times=False, 

128 show_level=True, 

129 markup=True, 

130 rich_tracebacks=True, 

131 tracebacks_suppress=[], 

132 keywords=[] 

133 ) 

134 

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

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

137 

138 set_log_level(LogLevel.SUBPROCESS) 

139 

140 __logger.handlers.clear() 

141 __logger.addHandler(subprocess_rich_handler) 

142 __logger.addHandler(rich_handler) 

143 

144 

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

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

147 kwargs['stacklevel'] = 2 

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

149 

150 

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

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

153 kwargs['stacklevel'] = 2 

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

155 

156 

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

158 """ 

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

160 if the log level is <= INFO. 

161 

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

163 

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

165 """ 

166 console.rule(title) 

167 

168 

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

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

171 kwargs['stacklevel'] = 2 

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

173 

174 

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

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

177 kwargs['stacklevel'] = 2 

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

179 

180 

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

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

183 kwargs['stacklevel'] = 2 

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

185 

186 

187configure_logger()