|
1 | | -from logging import DEBUG, ERROR, INFO, WARNING, Logger |
| 1 | +from logging import ( |
| 2 | + DEBUG, |
| 3 | + ERROR, |
| 4 | + INFO, |
| 5 | + WARNING, |
| 6 | + NOTSET, |
| 7 | + Logger, |
| 8 | + StreamHandler, |
| 9 | + basicConfig, |
| 10 | + Handler |
| 11 | +) |
| 12 | +from sys import stdout, stderr |
| 13 | +from logging.handlers import RotatingFileHandler |
2 | 14 | import os |
3 | 15 | from typing import Any, Callable, cast, TypeVar |
4 | 16 | from re import compile |
| 17 | +from pathlib import Path |
5 | 18 |
|
6 | 19 | from click_extra import ( |
7 | 20 | Color, |
|
12 | 25 | argument, |
13 | 26 | Choice, |
14 | 27 | IntRange, |
| 28 | + file_path, |
15 | 29 | ParamType, |
16 | 30 | Context, |
17 | 31 | Parameter |
18 | 32 | ) |
19 | | -from cloup.constraints import mutually_exclusive |
| 33 | +from cloup.constraints import ( |
| 34 | + ErrorFmt, |
| 35 | + constraint, |
| 36 | + mutually_exclusive, |
| 37 | + require_one, |
| 38 | + require_all, |
| 39 | + If, |
| 40 | + AnySet |
| 41 | +) |
20 | 42 |
|
21 | 43 |
|
22 | 44 | F = TypeVar('F', bound=Callable[..., Any]) |
@@ -126,7 +148,78 @@ def logging_option_group() -> Callable[[F], F]: |
126 | 148 | "--error", |
127 | 149 | is_flag=True |
128 | 150 | ), |
129 | | - constraint=mutually_exclusive |
| 151 | + option( |
| 152 | + "--file", |
| 153 | + help="log to file", |
| 154 | + type=file_path(readable=False) |
| 155 | + ), |
| 156 | + option( |
| 157 | + "--stdout", |
| 158 | + help="log to standard output", |
| 159 | + is_flag=True |
| 160 | + ), |
| 161 | + option( |
| 162 | + "--stderr", |
| 163 | + help="log to standard error", |
| 164 | + is_flag=True |
| 165 | + ), |
| 166 | + option( |
| 167 | + "--format", |
| 168 | + help=( |
| 169 | + "logging format string (as accepted by the `logging` package " |
| 170 | + "in '{' style)" |
| 171 | + ), |
| 172 | + type=str, |
| 173 | + default="{asctime} <{name}> [{levelname}] {message}" |
| 174 | + ), |
| 175 | + option( |
| 176 | + "--dateformat", |
| 177 | + help="date-time format spec (as accepted by `strftime`)", |
| 178 | + type=str, |
| 179 | + default="%Y-%m-%d %H:%M:%S" |
| 180 | + ), |
| 181 | + option( |
| 182 | + "--rotate", |
| 183 | + help=( |
| 184 | + "number of backup log files to rotate, and maximum size " |
| 185 | + "(in bytes) of a log file before rotation" |
| 186 | + ), |
| 187 | + type=(IntRange(1), IntRange(1)) |
| 188 | + ) |
| 189 | + ) |
| 190 | + |
| 191 | + |
| 192 | +def logging_levels_constraint() -> Callable[[F], F]: |
| 193 | + return constraint( |
| 194 | + mutually_exclusive, |
| 195 | + ["debug", "info", "warning", "error"] |
| 196 | + ) |
| 197 | + |
| 198 | + |
| 199 | +def logging_output_constraint() -> Callable[[F], F]: |
| 200 | + return constraint( |
| 201 | + If(AnySet("file", "stdout", "stderr"), require_one), |
| 202 | + ["debug", "info", "warning", "error"] |
| 203 | + ) |
| 204 | + |
| 205 | + |
| 206 | +def logging_target_constraint() -> Callable[[F], F]: |
| 207 | + return constraint( |
| 208 | + If(AnySet("debug", "info", "warning", "error"), require_one), |
| 209 | + ["file", "stdout", "stderr"] |
| 210 | + ) |
| 211 | + |
| 212 | + |
| 213 | +def logging_rotation_constraint() -> Callable[[F], F]: |
| 214 | + return constraint( |
| 215 | + If("rotate", require_all).rephrased( |
| 216 | + help="required if --rotate is set", |
| 217 | + error=( |
| 218 | + "when --rotate is set, the following parameter must also be " |
| 219 | + f"set:\n{ErrorFmt.param_list}" |
| 220 | + ) |
| 221 | + ), |
| 222 | + ["file"] |
130 | 223 | ) |
131 | 224 |
|
132 | 225 |
|
@@ -200,6 +293,62 @@ def make_directory(filepath: str) -> None: |
200 | 293 | os.makedirs(dirname, exist_ok=True) |
201 | 294 |
|
202 | 295 |
|
| 296 | +def configure_logging( |
| 297 | + debug: bool = False, |
| 298 | + info: bool = False, |
| 299 | + warning: bool = False, |
| 300 | + error: bool = False, |
| 301 | + to_path: Path | None = None, |
| 302 | + to_stdout: bool = False, |
| 303 | + to_stderr: bool = False, |
| 304 | + format: str = "{message}", |
| 305 | + dateformat: str = "%Y-%m-%d %H:%M:%S", |
| 306 | + rotate: tuple[int, int] | None = None |
| 307 | +) -> None: |
| 308 | + if not any((debug, info, warning, error)): |
| 309 | + return |
| 310 | + |
| 311 | + level = NOTSET |
| 312 | + if debug: |
| 313 | + level = DEBUG |
| 314 | + elif info: |
| 315 | + level = INFO |
| 316 | + elif warning: |
| 317 | + level = WARNING |
| 318 | + elif error: |
| 319 | + level = ERROR |
| 320 | + |
| 321 | + handlers: list[Handler] = [] |
| 322 | + if to_path is not None: |
| 323 | + max_size = 0 |
| 324 | + backups = 0 |
| 325 | + if rotate is not None: |
| 326 | + backups, max_size = rotate |
| 327 | + |
| 328 | + handlers.append( |
| 329 | + RotatingFileHandler( |
| 330 | + to_path, |
| 331 | + encoding="utf8", |
| 332 | + maxBytes=max_size, |
| 333 | + backupCount=backups |
| 334 | + ) |
| 335 | + ) |
| 336 | + |
| 337 | + if to_stdout: |
| 338 | + handlers.append(StreamHandler(stdout)) |
| 339 | + |
| 340 | + if to_stderr: |
| 341 | + handlers.append(StreamHandler(stderr)) |
| 342 | + |
| 343 | + basicConfig( |
| 344 | + format=format, |
| 345 | + datefmt=dateformat, |
| 346 | + style="{", |
| 347 | + level=level, |
| 348 | + handlers=handlers |
| 349 | + ) |
| 350 | + |
| 351 | + |
203 | 352 | def make_logger( |
204 | 353 | name: str, |
205 | 354 | debug: bool = False, |
|
0 commit comments