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

82 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-03-05 16:00 +0000

1import logging 

2import os 

3import pathlib 

4from abc import ABC, abstractmethod 

5from argparse import ArgumentParser 

6from logging import getLogger 

7from typing import TYPE_CHECKING, TypedDict, cast, get_args 

8 

9from pydantic import BaseModel, Field 

10 

11from competitive_verifier import github, summary 

12 

13from .log import GitHubMessageParams, configure_stderr_logging 

14 

15if TYPE_CHECKING: 

16 from competitive_verifier.models import VerifyCommandResult 

17 

18COMPETITIVE_VERIFY_FILES_PATH = "COMPETITIVE_VERIFY_FILES_PATH" 

19 

20logger = getLogger(__name__) 

21 

22 

23class _SubcommandInfo(TypedDict): 

24 name: str 

25 help: str | None 

26 

27 

28class BaseArguments(ABC, BaseModel): 

29 model_config = {"extra": "ignore"} 

30 

31 @classmethod 

32 def get_subcommand_info(cls) -> _SubcommandInfo | None: 

33 f = cls.model_fields.get("subcommand") 

34 if f is None or (subcommand := get_args(f.annotation)[0]) is None: 

35 return None 

36 

37 return {"name": cast("str", subcommand), "help": f.description} 

38 

39 @abstractmethod 

40 def run(self) -> bool: ... 

41 

42 @classmethod 

43 def add_parser(cls, parser: ArgumentParser): 

44 pass 

45 

46 

47class VerifyFilesJsonArgumentsMixin(BaseArguments): 

48 @classmethod 

49 def _required(cls) -> bool: 

50 return True 

51 

52 @classmethod 

53 def add_parser(cls, parser: ArgumentParser): 

54 super().add_parser(parser) 

55 

56 default = os.getenv(COMPETITIVE_VERIFY_FILES_PATH) if cls._required() else None 

57 parser.add_argument( 

58 "--verify-json", 

59 dest="verify_files_json", 

60 default=default or None, 

61 required=cls._required() and not bool(default), 

62 help="File path of verify_files.json. default: environ variable $COMPETITIVE_VERIFY_FILES_PATH", 

63 type=pathlib.Path, 

64 ) 

65 

66 

67class VerifyFilesJsonArguments(VerifyFilesJsonArgumentsMixin): 

68 verify_files_json: pathlib.Path 

69 

70 

71class OptionalVerifyFilesJsonArguments(VerifyFilesJsonArgumentsMixin): 

72 verify_files_json: pathlib.Path | None 

73 

74 @classmethod 

75 def _required(cls) -> bool: 

76 return False 

77 

78 

79class ResultJsonArguments(BaseArguments): 

80 result_json: list[pathlib.Path] 

81 

82 @classmethod 

83 def add_parser(cls, parser: ArgumentParser): 

84 super().add_parser(parser) 

85 parser.add_argument( 

86 "result_json", 

87 nargs="+", 

88 help="Json files which is result of `verify`", 

89 type=pathlib.Path, 

90 ) 

91 

92 

93class IgnoreErrorArguments(BaseArguments): 

94 ignore_error: bool = True 

95 

96 @classmethod 

97 def add_parser(cls, parser: ArgumentParser): 

98 super().add_parser(parser) 

99 parser.add_argument( 

100 "--check-error", 

101 help="Exit not zero if failed verification exists", 

102 dest="ignore_error", 

103 action="store_false", 

104 ) 

105 

106 

107class WriteSummaryArguments(BaseArguments): 

108 write_summary: bool = False 

109 

110 @classmethod 

111 def add_parser(cls, parser: ArgumentParser): 

112 super().add_parser(parser) 

113 parser.add_argument( 

114 "--write-summary", 

115 action="store_true", 

116 help="Write GitHub Actions summary", 

117 ) 

118 

119 def write_result(self, result: "VerifyCommandResult"): 

120 if self.write_summary: 

121 gh_summary_path = github.env.get_step_summary_path() 

122 if gh_summary_path and gh_summary_path.parent.exists(): 

123 with gh_summary_path.open("w", encoding="utf-8") as fp: 

124 summary.write_summary(fp, result) 

125 else: 

126 logger.warning( 

127 "write_summary=True but not found $GITHUB_STEP_SUMMARY", 

128 extra={"github": GitHubMessageParams()}, 

129 ) 

130 

131 

132class IncludeExcludeArguments(BaseArguments): 

133 include: list[str] = Field(default_factory=list) 

134 exclude: list[str] = Field(default_factory=list) 

135 

136 @classmethod 

137 def add_parser(cls, parser: ArgumentParser): 

138 super().add_parser(parser) 

139 parser.add_argument( 

140 "--include", 

141 nargs="*", 

142 help="Included file", 

143 default=[], 

144 type=str, 

145 ) 

146 parser.add_argument( 

147 "--exclude", 

148 nargs="*", 

149 help="Excluded file", 

150 default=[], 

151 type=str, 

152 ) 

153 

154 

155class VerboseArguments(BaseArguments): 

156 verbose: bool = False 

157 

158 @abstractmethod 

159 def _run(self) -> bool: ... 

160 def run(self) -> bool: 

161 default_level = logging.INFO 

162 if self.verbose: 

163 default_level = logging.DEBUG 

164 configure_stderr_logging(default_level) 

165 return self._run() 

166 

167 @classmethod 

168 def add_parser(cls, parser: ArgumentParser): 

169 super().add_parser(parser) 

170 parser.add_argument( 

171 "-v", "--verbose", action="store_true", help="Show debug level log." 

172 )