Coverage for cogapp / options.py: 99.05%

95 statements  

« prev     ^ index     » next       coverage.py v7.13.2, created at 2026-01-25 07:14 -0500

1import argparse 

2import copy 

3import os 

4from dataclasses import dataclass, field 

5from textwrap import dedent 

6from typing import ClassVar, Dict, List, NoReturn, Optional 

7 

8from .errors import CogUsageError 

9 

10description = """\ 

11cog - generate content with inlined Python code. 

12 

13cog [OPTIONS] [INFILE | @FILELIST | &FILELIST] ... 

14""" 

15 

16 

17class _NonEarlyExitingArgumentParser(argparse.ArgumentParser): 

18 """ 

19 Work around https://github.com/python/cpython/issues/121018 

20 (Upstream fix is available in Python 3.12+) 

21 """ 

22 

23 def error(self, message: str) -> NoReturn: 

24 raise CogUsageError(message) 

25 

26 

27def _parse_define(arg): 

28 if arg.count("=") < 1: 

29 raise argparse.ArgumentTypeError("takes a name=value argument") 

30 return arg.split("=", 1) 

31 

32 

33class _UpdateDictAction(argparse.Action): 

34 def __call__(self, _parser, ns, arg, _option_string=None): 

35 getattr(ns, self.dest).update([arg]) 

36 

37 

38@dataclass(frozen=True) 

39class Markers: 

40 begin_spec: str 

41 end_spec: str 

42 end_output: str 

43 

44 @classmethod 

45 def from_arg(cls, arg: str): 

46 parts = arg.split(" ") 

47 if len(parts) != 3: 

48 # tell argparse to prefix our error message with the option string 

49 raise argparse.ArgumentTypeError( 

50 f"requires 3 values separated by spaces, could not parse {arg!r}" 

51 ) 

52 return cls(*parts) 

53 

54 

55@dataclass 

56class CogOptions: 

57 """Options for a run of cog.""" 

58 

59 _parser: ClassVar = _NonEarlyExitingArgumentParser( 

60 prog="cog", 

61 usage=argparse.SUPPRESS, 

62 description=description, 

63 exit_on_error=False, # doesn't always work until 3.12+; see workaround above 

64 formatter_class=argparse.RawDescriptionHelpFormatter, 

65 ) 

66 

67 args: List[str] = field(default_factory=list) 

68 _parser.add_argument( 

69 "args", 

70 metavar="[INFILE | @FILELIST | &FILELIST]", 

71 nargs=argparse.ZERO_OR_MORE, 

72 help=dedent(""" 

73 FILELIST is the name of a text file containing file names or 

74 other @FILELISTs. 

75 

76 For @FILELIST, paths in the file list are relative to the working 

77 directory where cog was called. For &FILELIST, paths in the file 

78 list are relative to the file list location." 

79 """), 

80 ) 

81 

82 hash_output: bool = False 

83 _parser.add_argument( 

84 "-c", 

85 dest="hash_output", 

86 action="store_true", 

87 help="Checksum the output to protect it against accidental change.", 

88 ) 

89 

90 delete_code: bool = False 

91 _parser.add_argument( 

92 "-d", 

93 dest="delete_code", 

94 action="store_true", 

95 help="Delete the Python code from the output file.", 

96 ) 

97 

98 defines: Dict[str, str] = field(default_factory=dict) 

99 _parser.add_argument( 

100 "-D", 

101 dest="defines", 

102 type=_parse_define, 

103 metavar="name=val", 

104 action=_UpdateDictAction, 

105 help="Define a global string available to your Python code.", 

106 ) 

107 

108 warn_empty: bool = False 

109 _parser.add_argument( 

110 "-e", 

111 dest="warn_empty", 

112 action="store_true", 

113 help="Warn if a file has no cog code in it.", 

114 ) 

115 

116 include_path: List[str] = field(default_factory=list) 

117 _parser.add_argument( 

118 "-I", 

119 dest="include_path", 

120 metavar="PATH", 

121 type=lambda paths: map(os.path.abspath, paths.split(os.path.pathsep)), 

122 action="extend", 

123 help="Add PATH to the list of directories for data files and modules.", 

124 ) 

125 

126 encoding: str = "utf-8" 

127 _parser.add_argument( 

128 "-n", 

129 dest="encoding", 

130 metavar="ENCODING", 

131 help="Use ENCODING when reading and writing files.", 

132 ) 

133 

134 output_name: Optional[str] = None 

135 _parser.add_argument( 

136 "-o", 

137 dest="output_name", 

138 metavar="OUTNAME", 

139 help="Write the output to OUTNAME.", 

140 ) 

141 

142 prologue: str = "" 

143 _parser.add_argument( 

144 "-p", 

145 dest="prologue", 

146 help=dedent(""" 

147 Prepend the Python source with PROLOGUE. Useful to insert an import 

148 line. Example: -p "import math" 

149 """), 

150 ) 

151 

152 print_output: bool = False 

153 _parser.add_argument( 

154 "-P", 

155 dest="print_output", 

156 action="store_true", 

157 help="Use print() instead of cog.outl() for code output.", 

158 ) 

159 

160 replace: bool = False 

161 _parser.add_argument( 

162 "-r", 

163 dest="replace", 

164 action="store_true", 

165 help="Replace the input file with the output.", 

166 ) 

167 

168 suffix: Optional[str] = None 

169 _parser.add_argument( 

170 "-s", 

171 dest="suffix", 

172 metavar="STRING", 

173 help="Suffix all generated output lines with STRING.", 

174 ) 

175 

176 newline: str | None = None 

177 _parser.add_argument( 

178 "-U", 

179 dest="newline", 

180 action="store_const", 

181 const="\n", 

182 help="Write the output with Unix newlines (only LF line-endings).", 

183 ) 

184 

185 make_writable_cmd: Optional[str] = None 

186 _parser.add_argument( 

187 "-w", 

188 dest="make_writable_cmd", 

189 metavar="CMD", 

190 help=dedent(""" 

191 Use CMD if the output file needs to be made writable. A %%s in the CMD 

192 will be filled with the filename. 

193 """), 

194 ) 

195 

196 no_generate: bool = False 

197 _parser.add_argument( 

198 "-x", 

199 dest="no_generate", 

200 action="store_true", 

201 help="Excise all the generated output without running the Pythons.", 

202 ) 

203 

204 eof_can_be_end: bool = False 

205 _parser.add_argument( 

206 "-z", 

207 dest="eof_can_be_end", 

208 action="store_true", 

209 help="The end-output marker can be omitted, and is assumed at eof.", 

210 ) 

211 

212 show_version: bool = False 

213 _parser.add_argument( 

214 "-v", 

215 dest="show_version", 

216 action="store_true", 

217 help="Print the version of cog and exit.", 

218 ) 

219 

220 check: bool = False 

221 _parser.add_argument( 

222 "--check", 

223 action="store_true", 

224 help="Check that the files would not change if run again.", 

225 ) 

226 

227 check_fail_msg: str | None = None 

228 _parser.add_argument( 

229 "--check-fail-msg", 

230 metavar="MSG", 

231 help="If --check fails, include MSG in the output to help devs understand how to run cog in your project.", 

232 ) 

233 

234 diff: bool = False 

235 _parser.add_argument( 

236 "--diff", 

237 action="store_true", 

238 help="With --check, show a diff of what failed the check.", 

239 ) 

240 

241 markers: Markers = Markers("[[[cog", "]]]", "[[[end]]]") 

242 _parser.add_argument( 

243 "--markers", 

244 metavar="'START END END-OUTPUT'", 

245 type=Markers.from_arg, 

246 help=dedent(""" 

247 The patterns surrounding cog inline instructions. Should include three 

248 values separated by spaces, the start, end, and end-output markers. 

249 Defaults to '[[[cog ]]] [[[end]]]'. 

250 """), 

251 ) 

252 

253 # helper delegates 

254 begin_spec = property(lambda self: self.markers.begin_spec) 

255 end_spec = property(lambda self: self.markers.end_spec) 

256 end_output = property(lambda self: self.markers.end_output) 

257 

258 verbosity: int = 2 

259 _parser.add_argument( 

260 "--verbosity", 

261 type=int, 

262 help=dedent(""" 

263 Control the amount of output. 2 (the default) lists all files, 1 lists 

264 only changed files, 0 lists no files. 

265 """), 

266 ) 

267 

268 _parser.add_argument("-?", action="help", help=argparse.SUPPRESS) 

269 

270 def clone(self): 

271 """Make a clone of these options, for further refinement.""" 

272 return copy.deepcopy(self) 

273 

274 def format_help(self): 

275 """Get help text for command line options""" 

276 return self._parser.format_help() 

277 

278 def parse_args(self, argv: List[str]): 

279 try: 

280 self._parser.parse_args(argv, namespace=self) 

281 except argparse.ArgumentError as err: 

282 raise CogUsageError(str(err)) 

283 

284 if self.replace and self.delete_code: 

285 raise CogUsageError( 

286 "Can't use -d with -r (or you would delete all your source!)" 

287 ) 

288 

289 if self.replace and self.output_name: 

290 raise CogUsageError("Can't use -o with -r (they are opposites)") 

291 

292 if self.diff and not self.check: 

293 raise CogUsageError("Can't use --diff without --check")