• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

roskakori / pygount / 5468364552

pending completion
5468364552

Pull #121

github

web-flow
Merge 409bf29e9 into e2f267b89
Pull Request #121: #112 add graph for git tags

397 of 426 branches covered (93.19%)

45 of 45 new or added lines in 3 files covered. (100.0%)

1118 of 1172 relevant lines covered (95.39%)

3.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.8
/pygount/command.py
1
"""
2
Command line interface for pygount.
3
"""
4
# Copyright (c) 2016-2023, Thomas Aglassinger.
5
# All rights reserved. Distributed under the BSD License.
6
import argparse
4✔
7
import contextlib
4✔
8
import logging
4✔
9
import os
4✔
10
import sys
4✔
11

12
from rich.progress import Progress
4✔
13

14
import pygount
4✔
15
import pygount.analysis
4✔
16
import pygount.common
4✔
17
import pygount.write
4✔
18

19
#: Valid formats for option --format.
20
VALID_OUTPUT_FORMATS = ("cloc-xml", "json", "sloccount", "summary")
4✔
21

22
_DEFAULT_ENCODING = "automatic"
4✔
23
_DEFAULT_OUTPUT_FORMAT = "sloccount"
4✔
24
_DEFAULT_OUTPUT = "STDOUT"
4✔
25
_DEFAULT_SOURCE_PATTERNS = os.curdir
4✔
26
_DEFAULT_SUFFIXES = "*"
4✔
27

28
_HELP_ENCODING = '''encoding to use when reading source code; use "automatic"
4✔
29
 to take BOMs, XML prolog and magic headers into account and fall back to
30
 UTF-8 or CP1252 if none fits; use "automatic;<fallback>" to specify a
31
 different fallback encoding than CP1252; use "chardet" to let the chardet
32
 package determine the encoding; default: "%(default)s"'''
33

34
_HELP_EPILOG = """SHELL-PATTERN is a pattern using *, ? and ranges like [a-z]
4✔
35
 as placeholders. PATTERNS is a comma separated list of SHELL-PATTERN. The
36
 prefix [regex] indicated that the PATTERNS use regular expression syntax. If
37
 default values are available, [...] indicates that the PATTERNS extend the
38
 existing default values."""
39

40
_HELP_FORMAT = 'output format, one of: {}; default: "%(default)s"'.format(
4✔
41
    ", ".join(['"' + format + '"' for format in VALID_OUTPUT_FORMATS])
42
)
43

44
_HELP_GENERATED = """comma separated list of regular expressions to detect
4✔
45
 generated code; default: %(default)s"""
46

47
_HELP_FOLDERS_TO_SKIP = """comma separated list of glob patterns for folder
4✔
48
 names not to analyze. Use "..." as first entry to append patterns to the
49
 default patterns; default: %(default)s"""
50

51
_HELP_NAMES_TO_SKIP = """comma separated list of glob patterns for file names
4✔
52
 not to analyze. Use "..." as first entry to append patterns to the default
53
 patterns; default: %(default)s"""
54

55
_HELP_SUFFIX = '''limit analysis on files matching any suffix in comma
4✔
56
 separated LIST; shell patterns are possible; example: "py,sql"; default:
57
 "%(default)s"'''
58

59
_OUTPUT_FORMAT_TO_WRITER_CLASS_MAP = {
4✔
60
    "cloc-xml": pygount.write.ClocXmlWriter,
61
    "json": pygount.write.JsonWriter,
62
    "sloccount": pygount.write.LineWriter,
63
    "summary": pygount.write.SummaryWriter,
64
}
65
assert set(VALID_OUTPUT_FORMATS) == set(_OUTPUT_FORMAT_TO_WRITER_CLASS_MAP.keys())
4✔
66

67
_log = logging.getLogger("pygount")
4✔
68

69

70
def _check_encoding(name, encoding_to_check, alternative_encoding, source=None):
4✔
71
    """
72
    Check that ``encoding`` is a valid Python encoding
73
    :param name: name under which the encoding is known to the user, e.g. 'default encoding'
74
    :param encoding_to_check: name of the encoding to check, e.g. 'utf-8'
75
    :param source: source where the encoding has been set, e.g. option name
76
    :raise pygount.common.OptionError if ``encoding`` is not a valid Python encoding
77
    """
78
    assert name is not None
4✔
79

80
    if encoding_to_check not in (alternative_encoding, "chardet", None):
4✔
81
        try:
4✔
82
            "".encode(encoding_to_check)
83
        except LookupError:
×
84
            raise pygount.common.OptionError(
×
85
                '{} is "{}" but must be "{}" or a known Python encoding'.format(
86
                    name, encoding_to_check, alternative_encoding
87
                ),
88
                source,
89
            )
90

91

92
class Command:
4✔
93
    """
94
    Command interface for pygount, where options starting with defaults can
95
    gradually be set and finally :py:meth:`execute()`.
96
    """
97

98
    def __init__(self):
4✔
99
        self.set_encodings(_DEFAULT_ENCODING)
4✔
100
        self._folders_to_skip = pygount.common.regexes_from(pygount.analysis.DEFAULT_FOLDER_PATTERNS_TO_SKIP_TEXT)
4✔
101
        self._generated_regexs = pygount.common.regexes_from(pygount.analysis.DEFAULT_GENERATED_PATTERNS_TEXT)
4✔
102
        self._has_duplicates = False
4✔
103
        self._has_summary = False
4✔
104
        self._is_verbose = False
4✔
105
        self._is_graph = False
4✔
106
        self._names_to_skip = pygount.common.regexes_from(pygount.analysis.DEFAULT_NAME_PATTERNS_TO_SKIP_TEXT)
4✔
107
        self._output = _DEFAULT_OUTPUT
4✔
108
        self._output_format = _DEFAULT_OUTPUT_FORMAT
4✔
109
        self._source_patterns = _DEFAULT_SOURCE_PATTERNS
4✔
110
        self._suffixes = pygount.common.regexes_from(_DEFAULT_SUFFIXES)
4✔
111

112
    def set_encodings(self, encoding, source=None):
4✔
113
        encoding_is_chardet = (encoding == "chardet") or (encoding.startswith("chardet;"))
4✔
114
        if encoding_is_chardet and not pygount.analysis.has_chardet:  # pragma: no cover
115
            raise pygount.common.OptionError('chardet must be installed to set default encoding to "chardet"')
116
        if encoding in ("automatic", "chardet"):
4✔
117
            default_encoding = encoding
4✔
118
            fallback_encoding = None
4✔
119
        else:
120
            if encoding.startswith("automatic;") or encoding.startswith("chardet;"):
4!
121
                first_encoding_semicolon_index = encoding.find(";")
4✔
122
                default_encoding = encoding[:first_encoding_semicolon_index]
4✔
123
                fallback_encoding = encoding[first_encoding_semicolon_index + 1 :]
4✔
124
            else:
125
                default_encoding = encoding
×
126
                fallback_encoding = pygount.analysis.DEFAULT_FALLBACK_ENCODING
×
127
        self.set_default_encoding(default_encoding, source)
4✔
128
        self.set_fallback_encoding(fallback_encoding, source)
4✔
129

130
    @property
4✔
131
    def default_encoding(self):
4✔
132
        return self._default_encoding
4✔
133

134
    def set_default_encoding(self, default_encoding, source=None):
4✔
135
        _check_encoding("default encoding", default_encoding, "automatic", source)
4✔
136
        self._default_encoding = default_encoding
4✔
137

138
    @property
4✔
139
    def fallback_encoding(self):
4✔
140
        return self._fallback_encoding
4✔
141

142
    def set_fallback_encoding(self, fallback_encoding, source=None):
4✔
143
        _check_encoding("fallback encoding", fallback_encoding, "automatic", source)
4✔
144
        self._fallback_encoding = fallback_encoding
4✔
145

146
    @property
4✔
147
    def folders_to_skip(self):
4✔
148
        return self._folders_to_skip
4✔
149

150
    def set_folders_to_skip(self, regexes_or_patterns_text, source=None):
4✔
151
        self._folders_to_skip = pygount.common.regexes_from(
4✔
152
            regexes_or_patterns_text, pygount.analysis.DEFAULT_FOLDER_PATTERNS_TO_SKIP_TEXT, source
153
        )
154

155
    @property
4✔
156
    def generated_regexps(self):
4✔
157
        return self._generated_regexs
×
158

159
    def set_generated_regexps(self, regexes_or_patterns_text, source=None):
4✔
160
        self._generated_regexs = pygount.common.regexes_from(
4✔
161
            regexes_or_patterns_text, pygount.analysis.DEFAULT_GENERATED_PATTERNS_TEXT, source
162
        )
163

164
    @property
4✔
165
    def has_duplicates(self):
4✔
166
        return self._has_duplicates
4✔
167

168
    def set_has_duplicates(self, has_duplicates, source=None):
4✔
169
        self._has_duplicates = bool(has_duplicates)
4✔
170

171
    @property
4✔
172
    def is_verbose(self):
4✔
173
        return self._is_verbose
4✔
174

175
    def set_is_verbose(self, is_verbose, source=None):
4✔
176
        self._is_verbose = bool(is_verbose)
4✔
177

178
    @property
4✔
179
    def is_graph(self):
4✔
180
        return self._is_graph
×
181

182
    def set_is_graph(self, is_graph, source=None):
4✔
183
        self._is_graph = bool(is_graph)
4✔
184

185
    @property
4✔
186
    def names_to_skip(self):
4✔
187
        return self._names_to_skip
4✔
188

189
    def set_names_to_skip(self, regexes_or_pattern_text, source=None):
4✔
190
        self._names_to_skip = pygount.common.regexes_from(
4✔
191
            regexes_or_pattern_text, pygount.analysis.DEFAULT_NAME_PATTERNS_TO_SKIP_TEXT, source
192
        )
193

194
    @property
4✔
195
    def output(self):
4✔
196
        return self._output
4✔
197

198
    def set_output(self, output, source=None):
4✔
199
        assert output is not None
4✔
200
        self._output = output
4✔
201

202
    @property
4✔
203
    def output_format(self):
4✔
204
        return self._output_format
4✔
205

206
    def set_output_format(self, output_format, source=None):
4✔
207
        assert output_format is not None
4✔
208
        if output_format not in VALID_OUTPUT_FORMATS:
4✔
209
            raise pygount.common.OptionError(
4✔
210
                f"format is {output_format} but must be one of: {VALID_OUTPUT_FORMATS}", source
211
            )
212
        self._output_format = output_format
4✔
213

214
    @property
4✔
215
    def source_patterns(self):
4✔
216
        return self._source_patterns
4✔
217

218
    def set_source_patterns(self, glob_patterns_or_text, source=None):
4✔
219
        assert glob_patterns_or_text is not None
4✔
220
        self._source_patterns = pygount.common.as_list(glob_patterns_or_text)
4✔
221
        assert len(self._source_patterns) >= 0
4✔
222

223
    @property
4✔
224
    def suffixes(self):
4✔
225
        return self._suffixes
4✔
226

227
    def set_suffixes(self, regexes_or_patterns_text, source=None):
4✔
228
        assert regexes_or_patterns_text is not None
4✔
229
        self._suffixes = pygount.common.regexes_from(regexes_or_patterns_text, _DEFAULT_SUFFIXES, source)
4✔
230

231
    def argument_parser(self):
4✔
232
        parser = argparse.ArgumentParser(description="count source lines of code", epilog=_HELP_EPILOG)
4✔
233
        parser.add_argument("--duplicates", "-d", action="store_true", help="analyze duplicate files")
4✔
234
        parser.add_argument("--encoding", "-e", default=_DEFAULT_ENCODING, help=_HELP_ENCODING)
4✔
235
        parser.add_argument(
4✔
236
            "--folders-to-skip",
237
            "-F",
238
            metavar="PATTERNS",
239
            default=pygount.analysis.DEFAULT_FOLDER_PATTERNS_TO_SKIP_TEXT,
240
            help=_HELP_FOLDERS_TO_SKIP,
241
        )
242
        parser.add_argument(
4✔
243
            "--format",
244
            "-f",
245
            metavar="FORMAT",
246
            choices=VALID_OUTPUT_FORMATS,
247
            default=_DEFAULT_OUTPUT_FORMAT,
248
            help=_HELP_FORMAT,
249
        )
250
        parser.add_argument(
4✔
251
            "--generated",
252
            "-g",
253
            metavar="PATTERNS",
254
            default=pygount.analysis.DEFAULT_GENERATED_PATTERNS_TEXT,
255
            help=_HELP_GENERATED,
256
        )
257
        parser.add_argument(
4✔
258
            "--graph",
259
            "-G",
260
            help="create a graph image showing how the SLOC changed over time for each git tag",
261
        )
262
        parser.add_argument(
4✔
263
            "--names-to-skip",
264
            "-N",
265
            metavar="PATTERNS",
266
            default=pygount.analysis.DEFAULT_NAME_PATTERNS_TO_SKIP_TEXT,
267
            help=_HELP_NAMES_TO_SKIP,
268
        )
269
        parser.add_argument(
4✔
270
            "--out",
271
            "-o",
272
            metavar="FILE",
273
            default=_DEFAULT_OUTPUT,
274
            help='file to write results to; use "STDOUT" for standard output; default: "%(default)s"',
275
        )
276
        parser.add_argument("--suffix", "-s", metavar="PATTERNS", default=_DEFAULT_SUFFIXES, help=_HELP_SUFFIX)
4✔
277
        parser.add_argument(
4✔
278
            "source_patterns",
279
            metavar="SHELL-PATTERN",
280
            nargs="*",
281
            default=[os.getcwd()],
282
            help="source files and directories to scan; can use glob patterns; default: current directory",
283
        )
284
        parser.add_argument("--verbose", "-v", action="store_true", help="explain what is being done")
4✔
285
        parser.add_argument("--version", action="version", version="%(prog)s " + pygount.__version__)
4✔
286
        return parser
4✔
287

288
    def parsed_args(self, arguments):
4✔
289
        assert arguments is not None
4✔
290

291
        parser = self.argument_parser()
4✔
292
        args = parser.parse_args(arguments)
4✔
293
        if args.encoding == "automatic":
4✔
294
            default_encoding = args.encoding
4✔
295
            fallback_encoding = None
4✔
296
        elif args.encoding == "chardet":
4✔
297
            if not pygount.analysis.has_chardet:  # pragma: no cover
298
                parser.error("chardet must be installed in order to specify --encoding=chardet")
299
            default_encoding = args.encoding
×
300
            fallback_encoding = None
×
301
        else:
302
            if args.encoding.startswith("automatic;"):
4!
303
                first_encoding_semicolon_index = args.encoding.find(";")
×
304
                default_encoding = args.encoding[:first_encoding_semicolon_index]
×
305
                fallback_encoding = args.encoding[first_encoding_semicolon_index + 1 :]
×
306
                encoding_to_check = ("fallback encoding", fallback_encoding)
×
307
            else:
308
                default_encoding = args.encoding
4✔
309
                fallback_encoding = None
4✔
310
                encoding_to_check = ("encoding", default_encoding)
4✔
311
            if encoding_to_check is not None:
4!
312
                name, encoding = encoding_to_check
4✔
313
                try:
4✔
314
                    "".encode(encoding)
315
                except LookupError:
4✔
316
                    parser.error(f"{name} specified with --encoding must be a known Python encoding: {encoding}")
4✔
317
        return args, default_encoding, fallback_encoding
4✔
318

319
    def apply_arguments(self, arguments=None):
4✔
320
        if arguments is None:  # pragma: no cover
321
            arguments = sys.argv[1:]
322
        args, default_encoding, fallback_encoding = self.parsed_args(arguments)
4✔
323
        self.set_default_encoding(default_encoding, "option --encoding")
4✔
324
        self.set_fallback_encoding(fallback_encoding, "option --encoding")
4✔
325
        self.set_folders_to_skip(args.folders_to_skip, "option --folders-to-skip")
4✔
326
        self.set_generated_regexps(args.generated, "option --generated")
4✔
327
        self.set_has_duplicates(args.duplicates, "option --duplicates")
4✔
328
        self.set_is_verbose(args.verbose, "option --verbose")
4✔
329
        self.set_is_graph(args.graph, "option --graph")
4✔
330
        self.set_names_to_skip(args.names_to_skip, "option --folders-to-skip")
4✔
331
        self.set_output(args.out, "option --out")
4✔
332
        self.set_output_format(args.format, "option --format")
4✔
333
        self.set_source_patterns(args.source_patterns, "option PATTERNS")
4✔
334
        self.set_suffixes(args.suffix, "option --suffix")
4✔
335

336
    def execute(self):
4✔
337
        _log.setLevel(logging.INFO if self.is_verbose else logging.WARNING)
4✔
338
        with pygount.analysis.SourceScanner(
4✔
339
            self.source_patterns, self.suffixes, self.folders_to_skip, self.names_to_skip
340
        ) as source_scanner:
341
            source_paths_and_groups_to_analyze = list(source_scanner.source_paths())
4✔
342
            duplicate_pool = pygount.analysis.DuplicatePool() if not self.has_duplicates else None
4✔
343
            writer_class = _OUTPUT_FORMAT_TO_WRITER_CLASS_MAP[self.output_format]
4✔
344
            is_stdout = self.output == "STDOUT"
4✔
345
            target_context_manager = (
4✔
346
                contextlib.nullcontext(sys.stdout)
347
                if is_stdout
348
                else open(self.output, "w", encoding="utf-8", newline="")
349
            )
350
            with target_context_manager as target_file, writer_class(target_file) as writer:
4✔
351
                with Progress(disable=not writer.has_to_track_progress, transient=True) as progress:
4✔
352
                    try:
4✔
353
                        for source_path, group in progress.track(source_paths_and_groups_to_analyze):
4✔
354
                            writer.add(
4✔
355
                                pygount.analysis.SourceAnalysis.from_file(
356
                                    source_path,
357
                                    group,
358
                                    self.default_encoding,
359
                                    self.fallback_encoding,
360
                                    generated_regexes=self._generated_regexs,
361
                                    duplicate_pool=duplicate_pool,
362
                                )
363
                            )
364
                    finally:
365
                        progress.stop()
4✔
366

367

368
def pygount_command(arguments=None):
4✔
369
    result = 1
4✔
370
    command = Command()
4✔
371
    try:
4✔
372
        command.apply_arguments(arguments)
4✔
373
        command.execute()
4✔
374
        result = 0
4✔
375
    except KeyboardInterrupt:  # pragma: no cover
376
        _log.error("interrupted as requested by user")
377
    except (pygount.common.OptionError, OSError) as error:
4✔
378
        _log.error(error)
4✔
379
    except Exception as error:
4✔
380
        _log.exception(error)
×
381

382
    return result
4✔
383

384

385
def main():  # pragma: no cover
386
    logging.basicConfig(level=logging.WARNING)
387
    sys.exit(pygount_command())
388

389

390
if __name__ == "__main__":  # pragma: no cover
391
    main()
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc