Coverage for stlog/output.py: 86%

90 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-08-21 07:31 +0000

1from __future__ import annotations 

2 

3import logging 

4import logging.handlers 

5import os 

6import sys 

7import typing 

8from dataclasses import dataclass, field 

9 

10from stlog.base import ( 

11 GLOBAL_LOGGING_CONFIG, 

12 StlogError, 

13 check_env_false, 

14) 

15from stlog.filter import ContextReinjectFilter 

16from stlog.formatter import ( 

17 Formatter, 

18 HumanFormatter, 

19 RichHumanFormatter, 

20) 

21from stlog.handler import CustomRichHandler 

22 

23RICH_INSTALLED: bool = False 

24try: 

25 from rich.console import Console 

26 

27 RICH_INSTALLED = True 

28except ImportError: 

29 pass 

30 

31 

32def _get_default_use_rich() -> bool | None: 

33 tmp = os.environ.get("STLOG_USE_RICH") 

34 if tmp is None: 

35 return None 

36 if tmp.strip().upper() in ("NONE", "AUTO", ""): 

37 return None 

38 return tmp.strip().upper() in ("1", "TRUE", "YES") 

39 

40 

41DEFAULT_USE_RICH = _get_default_use_rich() 

42 

43 

44@dataclass 

45class Output: 

46 """Abstract output base class. 

47 

48 Attributes: 

49 formatter: the Python logging Formatter to use. 

50 level: python logging level specific to this output 

51 (None means "use the global logging level"). 

52 filters: list of logging Filters (or simple callables) to filter some LogRecord 

53 for this specific output. 

54 reinject_context_in_standard_logging: if True, reinject the LogContext 

55 in log record emitted with python standard loggers 

56 (note: override `stlog.setup` default value for this output). 

57 read_extra_kwargs_from_standard_logging: if try to reinject the extra kwargs from standard logging 

58 (note: override `stlog.setup` default value for this output). 

59 

60 """ 

61 

62 _handler: logging.Handler = field(init=False, default_factory=logging.NullHandler) 

63 formatter: logging.Formatter | None = None 

64 level: int | str | None = None 

65 filters: typing.Iterable[ 

66 typing.Callable[[logging.LogRecord], bool] | logging.Filter 

67 ] = field(default_factory=list) 

68 reinject_context_in_standard_logging: bool | None = None 

69 read_extra_kwargs_from_standard_logging: bool | None = None 

70 

71 @property 

72 def _reinject_context_in_standard_logging(self) -> bool: 

73 # lazy evaluation because Output are built before setup() call 

74 if self.reinject_context_in_standard_logging is not None: 

75 return self.reinject_context_in_standard_logging 

76 if GLOBAL_LOGGING_CONFIG.reinject_context_in_standard_logging is not None: 

77 return GLOBAL_LOGGING_CONFIG.reinject_context_in_standard_logging 

78 return check_env_false("STLOG_REINJECT_CONTEXT_IN_STANDARD_LOGGING", True) 

79 

80 @property 

81 def _read_extra_kwargs_from_standard_logging(self) -> bool: 

82 # lazy evaluation because Output are built before setup() call 

83 if self.read_extra_kwargs_from_standard_logging is not None: 

84 return self.read_extra_kwargs_from_standard_logging 

85 if GLOBAL_LOGGING_CONFIG.read_extra_kwargs_from_standard_logging is not None: 

86 return GLOBAL_LOGGING_CONFIG.read_extra_kwargs_from_standard_logging 

87 return check_env_false("STLOG_READ_EXTRA_KWARGS_FROM_STANDARD_LOGGING", True) 

88 

89 def set_handler( 

90 self, 

91 handler: logging.Handler, 

92 ): 

93 """Configure the Python logging Handler to use.""" 

94 self._handler = handler 

95 self._handler.setFormatter(self.get_formatter_or_raise()) 

96 if self.level is not None: 

97 self._handler.setLevel(self.level) 

98 if self._reinject_context_in_standard_logging: 

99 self._handler.addFilter( 

100 ContextReinjectFilter( 

101 read_extra_kwargs_from_standard_logging=self._read_extra_kwargs_from_standard_logging 

102 ) 

103 ) 

104 for filter in self.filters: 

105 self._handler.addFilter(filter) 

106 

107 def get_handler(self) -> logging.Handler: 

108 """Get the configured Python logging Handler.""" 

109 return self._handler 

110 

111 def get_formatter_or_raise(self) -> logging.Formatter: 

112 if self.formatter is None: 

113 raise StlogError("formatter is not set") 

114 return self.formatter 

115 

116 

117@dataclass 

118class StreamOutput(Output): 

119 """Represent an output to a stream (stdout, stderr...). 

120 

121 Attributes: 

122 stream: the stream to use (`typing.TextIO`), default to `sys.stderr`. 

123 

124 """ 

125 

126 stream: typing.TextIO = sys.stderr 

127 

128 def __post_init__(self): 

129 if self.formatter is None: 

130 self.formatter = HumanFormatter() 

131 self.set_handler( 

132 logging.StreamHandler(self.stream), 

133 ) 

134 

135 

136@dataclass 

137class RichStreamOutput(StreamOutput): 

138 force_terminal: bool = False 

139 console: typing.Any = None 

140 

141 def __post_init__(self): 

142 if not RICH_INSTALLED: 

143 raise StlogError("Rich is not installed and RichStreamOutput is specified") 

144 if self.formatter is None: 

145 self.formatter = RichHumanFormatter() 

146 if self.console is None: 

147 self.console = Console( 

148 file=self.stream, 

149 force_terminal=True if self.force_terminal else None, 

150 highlight=False, 

151 ) 

152 self.set_handler(CustomRichHandler(console=self.console)) 

153 

154 

155def make_stream_or_rich_stream_output( 

156 stream: typing.TextIO = sys.stderr, 

157 use_rich: bool | None = DEFAULT_USE_RICH, 

158 rich_formatter: Formatter | None = None, 

159 not_rich_formatter: Formatter | None = None, 

160 **kwargs, 

161) -> StreamOutput: 

162 """Create automatically a `stlog.output.RichStreamOutput` or a (classic)`stlog.output.StreamOutput`. 

163 

164 To get a `stlog.output.RichStreamOutput`, following conditions must be true: 

165 

166 - `rich` library must be installed and available in python path 

167 - `use_rich` parameter must be `True` (forced mode) or `None` (automatic mode) 

168 - (if `use_rich` is `None`): the selected `stream` must "output" in a real terminal (not in a shell filter 

169 or in a file through redirection...) 

170 

171 NOTE: the default value of the `use_rich` parameter is `None` (automatic) but it can be forced by 

172 the `STLOG_USE_RICH` env variable. 

173 

174 Attributes: 

175 stream: the stream to use (`typing.TextIO`), default to `sys.stderr`. 

176 use_rich: if None, use [rich output](https://github.com/Textualize/rich/blob/master/README.md) if possible 

177 (rich installed and supported tty), if True/False force the usage (or not). 

178 rich_formatter: Formatter to use if rich is available/selected (None => default RichHumanFormatter instance). 

179 not_rich_formatter: Formatter to use if rich is not available/selected (None => default HumanFormatter instance). 

180 

181 """ 

182 if "formatter" in kwargs: 

183 raise StlogError( 

184 "you can't use formatter in kwargs for this function (buy you can use rich_formatter/not_rich_formatter instead)" 

185 ) 

186 _use_rich: bool = False 

187 if use_rich is not None: 

188 # manual mode 

189 _use_rich = use_rich 

190 else: 

191 # automatic mode 

192 if RICH_INSTALLED: 

193 c = Console(file=stream) 

194 _use_rich = c.is_terminal 

195 if _use_rich: 

196 return RichStreamOutput( 

197 stream=stream, 

198 formatter=rich_formatter or RichHumanFormatter(), 

199 **kwargs, 

200 ) 

201 else: 

202 return StreamOutput( 

203 stream=stream, 

204 formatter=not_rich_formatter or HumanFormatter(), 

205 **kwargs, 

206 )