-
Notifications
You must be signed in to change notification settings - Fork 22
/
command.py
375 lines (321 loc) · 14.9 KB
/
command.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
"""
Command line interface for pygount.
"""
# Copyright (c) 2016-2023, Thomas Aglassinger.
# All rights reserved. Distributed under the BSD License.
import argparse
import contextlib
import logging
import os
import sys
from rich.progress import Progress
import pygount
import pygount.analysis
import pygount.common
import pygount.write
#: Valid formats for option --format.
VALID_OUTPUT_FORMATS = ("cloc-xml", "json", "sloccount", "summary")
_DEFAULT_ENCODING = "automatic"
_DEFAULT_OUTPUT_FORMAT = "sloccount"
_DEFAULT_OUTPUT = "STDOUT"
_DEFAULT_SOURCE_PATTERNS = os.curdir
_DEFAULT_SUFFIXES = "*"
_HELP_ENCODING = '''encoding to use when reading source code; use "automatic"
to take BOMs, XML prolog and magic headers into account and fall back to
UTF-8 or CP1252 if none fits; use "automatic;<fallback>" to specify a
different fallback encoding than CP1252; use "chardet" to let the chardet
package determine the encoding; default: "%(default)s"'''
_HELP_EPILOG = """SHELL-PATTERN is a pattern using *, ? and ranges like [a-z]
as placeholders. PATTERNS is a comma separated list of SHELL-PATTERN. The
prefix [regex] indicated that the PATTERNS use regular expression syntax. If
default values are available, [...] indicates that the PATTERNS extend the
existing default values."""
_HELP_FORMAT = 'output format, one of: {}; default: "%(default)s"'.format(
", ".join(['"' + format + '"' for format in VALID_OUTPUT_FORMATS])
)
_HELP_GENERATED = """comma separated list of regular expressions to detect
generated code; default: %(default)s"""
_HELP_FOLDERS_TO_SKIP = """comma separated list of glob patterns for folder
names not to analyze. Use "..." as first entry to append patterns to the
default patterns; default: %(default)s"""
_HELP_NAMES_TO_SKIP = """comma separated list of glob patterns for file names
not to analyze. Use "..." as first entry to append patterns to the default
patterns; default: %(default)s"""
_HELP_SUFFIX = '''limit analysis on files matching any suffix in comma
separated LIST; shell patterns are possible; example: "py,sql"; default:
"%(default)s"'''
_OUTPUT_FORMAT_TO_WRITER_CLASS_MAP = {
"cloc-xml": pygount.write.ClocXmlWriter,
"json": pygount.write.JsonWriter,
"sloccount": pygount.write.LineWriter,
"summary": pygount.write.SummaryWriter,
}
assert set(VALID_OUTPUT_FORMATS) == set(_OUTPUT_FORMAT_TO_WRITER_CLASS_MAP.keys())
_log = logging.getLogger("pygount")
def _check_encoding(name, encoding_to_check, alternative_encoding, source=None):
"""
Check that ``encoding`` is a valid Python encoding
:param name: name under which the encoding is known to the user, e.g. 'default encoding'
:param encoding_to_check: name of the encoding to check, e.g. 'utf-8'
:param source: source where the encoding has been set, e.g. option name
:raise pygount.common.OptionError if ``encoding`` is not a valid Python encoding
"""
assert name is not None
if encoding_to_check not in (alternative_encoding, "chardet", None):
try:
"".encode(encoding_to_check)
except LookupError:
raise pygount.common.OptionError(
'{} is "{}" but must be "{}" or a known Python encoding'.format(
name, encoding_to_check, alternative_encoding
),
source,
)
class Command:
"""
Command interface for pygount, where options starting with defaults can
gradually be set and finally :py:meth:`execute()`.
"""
def __init__(self):
self.set_encodings(_DEFAULT_ENCODING)
self._folders_to_skip = pygount.common.regexes_from(pygount.analysis.DEFAULT_FOLDER_PATTERNS_TO_SKIP_TEXT)
self._generated_regexs = pygount.common.regexes_from(pygount.analysis.DEFAULT_GENERATED_PATTERNS_TEXT)
self._has_duplicates = False
self._has_summary = False
self._is_verbose = False
self._names_to_skip = pygount.common.regexes_from(pygount.analysis.DEFAULT_NAME_PATTERNS_TO_SKIP_TEXT)
self._output = _DEFAULT_OUTPUT
self._output_format = _DEFAULT_OUTPUT_FORMAT
self._source_patterns = _DEFAULT_SOURCE_PATTERNS
self._suffixes = pygount.common.regexes_from(_DEFAULT_SUFFIXES)
def set_encodings(self, encoding, source=None):
encoding_is_chardet = (encoding == "chardet") or (encoding.startswith("chardet;"))
if encoding_is_chardet and not pygount.analysis.has_chardet: # pragma: no cover
raise pygount.common.OptionError('chardet must be installed to set default encoding to "chardet"')
if encoding in ("automatic", "chardet"):
default_encoding = encoding
fallback_encoding = None
else:
if encoding.startswith("automatic;") or encoding.startswith("chardet;"):
first_encoding_semicolon_index = encoding.find(";")
default_encoding = encoding[:first_encoding_semicolon_index]
fallback_encoding = encoding[first_encoding_semicolon_index + 1 :]
else:
default_encoding = encoding
fallback_encoding = pygount.analysis.DEFAULT_FALLBACK_ENCODING
self.set_default_encoding(default_encoding, source)
self.set_fallback_encoding(fallback_encoding, source)
@property
def default_encoding(self):
return self._default_encoding
def set_default_encoding(self, default_encoding, source=None):
_check_encoding("default encoding", default_encoding, "automatic", source)
self._default_encoding = default_encoding
@property
def fallback_encoding(self):
return self._fallback_encoding
def set_fallback_encoding(self, fallback_encoding, source=None):
_check_encoding("fallback encoding", fallback_encoding, "automatic", source)
self._fallback_encoding = fallback_encoding
@property
def folders_to_skip(self):
return self._folders_to_skip
def set_folders_to_skip(self, regexes_or_patterns_text, source=None):
self._folders_to_skip = pygount.common.regexes_from(
regexes_or_patterns_text, pygount.analysis.DEFAULT_FOLDER_PATTERNS_TO_SKIP_TEXT, source
)
@property
def generated_regexps(self):
return self._generated_regexs
def set_generated_regexps(self, regexes_or_patterns_text, source=None):
self._generated_regexs = pygount.common.regexes_from(
regexes_or_patterns_text, pygount.analysis.DEFAULT_GENERATED_PATTERNS_TEXT, source
)
@property
def has_duplicates(self):
return self._has_duplicates
def set_has_duplicates(self, has_duplicates, source=None):
self._has_duplicates = bool(has_duplicates)
@property
def is_verbose(self):
return self._is_verbose
def set_is_verbose(self, is_verbose, source=None):
self._is_verbose = bool(is_verbose)
@property
def names_to_skip(self):
return self._names_to_skip
def set_names_to_skip(self, regexes_or_pattern_text, source=None):
self._names_to_skip = pygount.common.regexes_from(
regexes_or_pattern_text, pygount.analysis.DEFAULT_NAME_PATTERNS_TO_SKIP_TEXT, source
)
@property
def output(self):
return self._output
def set_output(self, output, source=None):
assert output is not None
self._output = output
@property
def output_format(self):
return self._output_format
def set_output_format(self, output_format, source=None):
assert output_format is not None
if output_format not in VALID_OUTPUT_FORMATS:
raise pygount.common.OptionError(
f"format is {output_format} but must be one of: {VALID_OUTPUT_FORMATS}", source
)
self._output_format = output_format
@property
def source_patterns(self):
return self._source_patterns
def set_source_patterns(self, glob_patterns_or_text, source=None):
assert glob_patterns_or_text is not None
self._source_patterns = pygount.common.as_list(glob_patterns_or_text)
assert len(self._source_patterns) >= 0
@property
def suffixes(self):
return self._suffixes
def set_suffixes(self, regexes_or_patterns_text, source=None):
assert regexes_or_patterns_text is not None
self._suffixes = pygount.common.regexes_from(regexes_or_patterns_text, _DEFAULT_SUFFIXES, source)
def argument_parser(self):
parser = argparse.ArgumentParser(description="count source lines of code", epilog=_HELP_EPILOG)
parser.add_argument("--duplicates", "-d", action="store_true", help="analyze duplicate files")
parser.add_argument("--encoding", "-e", default=_DEFAULT_ENCODING, help=_HELP_ENCODING)
parser.add_argument(
"--folders-to-skip",
"-F",
metavar="PATTERNS",
default=pygount.analysis.DEFAULT_FOLDER_PATTERNS_TO_SKIP_TEXT,
help=_HELP_FOLDERS_TO_SKIP,
)
parser.add_argument(
"--format",
"-f",
metavar="FORMAT",
choices=VALID_OUTPUT_FORMATS,
default=_DEFAULT_OUTPUT_FORMAT,
help=_HELP_FORMAT,
)
parser.add_argument(
"--generated",
"-g",
metavar="PATTERNS",
default=pygount.analysis.DEFAULT_GENERATED_PATTERNS_TEXT,
help=_HELP_GENERATED,
)
parser.add_argument(
"--names-to-skip",
"-N",
metavar="PATTERNS",
default=pygount.analysis.DEFAULT_NAME_PATTERNS_TO_SKIP_TEXT,
help=_HELP_NAMES_TO_SKIP,
)
parser.add_argument(
"--out",
"-o",
metavar="FILE",
default=_DEFAULT_OUTPUT,
help='file to write results to; use "STDOUT" for standard output; default: "%(default)s"',
)
parser.add_argument("--suffix", "-s", metavar="PATTERNS", default=_DEFAULT_SUFFIXES, help=_HELP_SUFFIX)
parser.add_argument(
"source_patterns",
metavar="SHELL-PATTERN",
nargs="*",
default=[os.getcwd()],
help="source files and directories to scan; can use glob patterns; default: current directory",
)
parser.add_argument("--verbose", "-v", action="store_true", help="explain what is being done")
parser.add_argument("--version", action="version", version="%(prog)s " + pygount.__version__)
return parser
def parsed_args(self, arguments):
assert arguments is not None
parser = self.argument_parser()
args = parser.parse_args(arguments)
if args.encoding == "automatic":
default_encoding = args.encoding
fallback_encoding = None
elif args.encoding == "chardet":
if not pygount.analysis.has_chardet: # pragma: no cover
parser.error("chardet must be installed in order to specify --encoding=chardet")
default_encoding = args.encoding
fallback_encoding = None
else:
if args.encoding.startswith("automatic;"):
first_encoding_semicolon_index = args.encoding.find(";")
default_encoding = args.encoding[:first_encoding_semicolon_index]
fallback_encoding = args.encoding[first_encoding_semicolon_index + 1 :]
encoding_to_check = ("fallback encoding", fallback_encoding)
else:
default_encoding = args.encoding
fallback_encoding = None
encoding_to_check = ("encoding", default_encoding)
if encoding_to_check is not None:
name, encoding = encoding_to_check
try:
"".encode(encoding)
except LookupError:
parser.error(f"{name} specified with --encoding must be a known Python encoding: {encoding}")
return args, default_encoding, fallback_encoding
def apply_arguments(self, arguments=None):
if arguments is None: # pragma: no cover
arguments = sys.argv[1:]
args, default_encoding, fallback_encoding = self.parsed_args(arguments)
self.set_default_encoding(default_encoding, "option --encoding")
self.set_fallback_encoding(fallback_encoding, "option --encoding")
self.set_folders_to_skip(args.folders_to_skip, "option --folders-to-skip")
self.set_generated_regexps(args.generated, "option --generated")
self.set_has_duplicates(args.duplicates, "option --duplicates")
self.set_is_verbose(args.verbose, "option --verbose")
self.set_names_to_skip(args.names_to_skip, "option --folders-to-skip")
self.set_output(args.out, "option --out")
self.set_output_format(args.format, "option --format")
self.set_source_patterns(args.source_patterns, "option PATTERNS")
self.set_suffixes(args.suffix, "option --suffix")
def execute(self):
_log.setLevel(logging.INFO if self.is_verbose else logging.WARNING)
source_scanner = pygount.analysis.SourceScanner(
self.source_patterns, self.suffixes, self.folders_to_skip, self.names_to_skip
)
source_paths_and_groups_to_analyze = list(source_scanner.source_paths())
duplicate_pool = pygount.analysis.DuplicatePool() if not self.has_duplicates else None
writer_class = _OUTPUT_FORMAT_TO_WRITER_CLASS_MAP[self.output_format]
is_stdout = self.output == "STDOUT"
target_context_manager = (
contextlib.nullcontext(sys.stdout) if is_stdout else open(self.output, "w", encoding="utf-8", newline="")
)
with target_context_manager as target_file, writer_class(target_file) as writer:
with Progress(disable=not writer.has_to_track_progress, transient=True) as progress:
try:
for source_path, group in progress.track(source_paths_and_groups_to_analyze):
writer.add(
pygount.analysis.SourceAnalysis.from_file(
source_path,
group,
self.default_encoding,
self.fallback_encoding,
generated_regexes=self._generated_regexs,
duplicate_pool=duplicate_pool,
)
)
finally:
progress.stop()
def pygount_command(arguments=None):
result = 1
command = Command()
try:
command.apply_arguments(arguments)
command.execute()
result = 0
except KeyboardInterrupt: # pragma: no cover
_log.error("interrupted as requested by user")
except (pygount.common.OptionError, OSError) as error:
_log.error(error)
except Exception as error:
_log.exception(error)
return result
def main(): # pragma: no cover
logging.basicConfig(level=logging.WARNING)
sys.exit(pygount_command())
if __name__ == "__main__": # pragma: no cover
main()