Coverage for src / competitive_verifier / log.py: 100%

52 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-04-26 12:38 +0900

1import pathlib 

2import sys 

3from contextlib import contextmanager 

4from dataclasses import asdict, dataclass 

5from logging import ( 

6 DEBUG, 

7 ERROR, 

8 INFO, 

9 NOTSET, 

10 WARNING, 

11 Handler, 

12 LogRecord, 

13 basicConfig, 

14) 

15from typing import TextIO 

16 

17import colorlog 

18from colorama import Fore, Style 

19 

20from competitive_verifier import github 

21 

22 

23@dataclass 

24class GitHubMessageParams: 

25 title: str | None = None 

26 file: str | pathlib.Path | None = None 

27 col: int | None = None 

28 end_column: int | None = None 

29 line: int | None = None 

30 end_line: int | None = None 

31 

32 

33class GitHubActionsHandler(Handler): 

34 def __init__( 

35 self, 

36 *, 

37 stream: TextIO, 

38 level: int | str = NOTSET, 

39 ) -> None: 

40 super().__init__(level) 

41 self.stream = stream 

42 

43 def emit(self, record: LogRecord) -> None: 

44 if record.levelno >= ERROR: 

45 command = "error" 

46 elif record.levelno >= WARNING: 

47 command = "warning" 

48 elif record.levelno >= INFO: 

49 command = "notice" 

50 else: 

51 if record.levelno >= DEBUG: 

52 other_name = ( 

53 "" 

54 if record.name.startswith("competitive_verifier") 

55 else f"{record.name}:{record.lineno}:" 

56 ) 

57 github.debug(other_name + record.getMessage(), stream=self.stream) 

58 return 

59 

60 if (gh := getattr(record, "github", None)) is None or not isinstance( 

61 gh, GitHubMessageParams 

62 ): 

63 return 

64 

65 if not (gh.title or record.name.startswith("competitive_verifier")): 

66 gh.title = record.name 

67 

68 github.message(command, record.getMessage(), stream=self.stream, **asdict(gh)) 

69 

70 

71def configure_stderr_logging( 

72 default_level: int | None = None, 

73) -> None: # pragma: no cover 

74 colorlog_handler = colorlog.StreamHandler(sys.stderr) 

75 colorlog_handler.setLevel(default_level or WARNING) 

76 colorlog_handler.setFormatter( 

77 colorlog.ColoredFormatter( 

78 "%(log_color)s%(levelname)s%(reset)s:%(name)s:%(lineno)d:%(message)s" 

79 ) 

80 ) 

81 

82 handlers: list[Handler] = [colorlog_handler] 

83 

84 if github.env.is_in_github_actions(): 

85 handlers.append(GitHubActionsHandler(stream=sys.stderr)) 

86 

87 basicConfig( 

88 level=NOTSET, 

89 handlers=handlers, 

90 ) 

91 

92 

93def _console_group(category: str, *, title: str, file: TextIO | None): 

94 print( 

95 ( 

96 f"<------------- {Fore.CYAN}{category}:" 

97 f"{Fore.YELLOW}{title}" 

98 f"{Style.RESET_ALL} ------------->" 

99 ), 

100 file=file, 

101 ) 

102 

103 

104@contextmanager 

105def group(title: str, *, stream: TextIO | None = None): 

106 if stream is None: 

107 stream = sys.stderr 

108 if github.env.is_in_github_actions(): 

109 try: 

110 github.begin_group(title, stream=stream) 

111 yield 

112 finally: 

113 github.end_group(stream=stream) 

114 else: 

115 try: 

116 _console_group(" Start group", title=title, file=stream) 

117 yield 

118 finally: 

119 _console_group("Finish group", title=title, file=stream)