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
« 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
8from .errors import CogUsageError
10description = """\
11cog - generate content with inlined Python code.
13cog [OPTIONS] [INFILE | @FILELIST | &FILELIST] ...
14"""
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 """
23 def error(self, message: str) -> NoReturn:
24 raise CogUsageError(message)
27def _parse_define(arg):
28 if arg.count("=") < 1:
29 raise argparse.ArgumentTypeError("takes a name=value argument")
30 return arg.split("=", 1)
33class _UpdateDictAction(argparse.Action):
34 def __call__(self, _parser, ns, arg, _option_string=None):
35 getattr(ns, self.dest).update([arg])
38@dataclass(frozen=True)
39class Markers:
40 begin_spec: str
41 end_spec: str
42 end_output: str
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)
55@dataclass
56class CogOptions:
57 """Options for a run of cog."""
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 )
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.
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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 )
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)
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 )
268 _parser.add_argument("-?", action="help", help=argparse.SUPPRESS)
270 def clone(self):
271 """Make a clone of these options, for further refinement."""
272 return copy.deepcopy(self)
274 def format_help(self):
275 """Get help text for command line options"""
276 return self._parser.format_help()
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))
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 )
289 if self.replace and self.output_name:
290 raise CogUsageError("Can't use -o with -r (they are opposites)")
292 if self.diff and not self.check:
293 raise CogUsageError("Can't use --diff without --check")