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

roskakori / pygount / 15242760168

25 May 2025 10:51PM UTC coverage: 96.252% (-0.007%) from 96.259%
15242760168

Pull #202

github

web-flow
Merge de4b04fee into 8fc754c65
Pull Request #202: 166 replace format with f strings

216 of 242 branches covered (89.26%)

2 of 2 new or added lines in 1 file covered. (100.0%)

10 existing lines in 1 file now uncovered.

1130 of 1174 relevant lines covered (96.25%)

0.96 hits per line

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

94.26
/pygount/command.py
1
"""
2
Command line interface for pygount.
3
"""
4

5
# Copyright (c) 2016-2024, Thomas Aglassinger.
6
# All rights reserved. Distributed under the BSD License.
7
import argparse
1✔
8
import contextlib
1✔
9
import logging
1✔
10
import os
1✔
11
import sys
1✔
12

13
from rich.progress import Progress
1✔
14

15
import pygount
1✔
16
import pygount.analysis
1✔
17
import pygount.common
1✔
18
import pygount.write
1✔
19

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

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

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

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

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

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

48
_HELP_MERGE_EMBEDDED_LANGUAGES = """merge counts for embedded languages into
1✔
49
 their base language; for example, HTML+Jinja2 counts as HTML"""
50

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

55
_HELP_NAMES_TO_SKIP = """comma separated list of glob patterns for file names
1✔
56
 not to analyze. Use "..." as first entry to append patterns to the default
57
 patterns; default: %(default)s"""
58

59
_HELP_SUFFIX = '''limit analysis on files matching any suffix in comma
1✔
60
 separated LIST; shell patterns are possible; example: "py,sql"; default:
61
 "%(default)s"'''
62

63
_OUTPUT_FORMAT_TO_WRITER_CLASS_MAP = {
1✔
64
    "cloc-xml": pygount.write.ClocXmlWriter,
65
    "json": pygount.write.JsonWriter,
66
    "sloccount": pygount.write.LineWriter,
67
    "summary": pygount.write.SummaryWriter,
68
}
69
assert set(VALID_OUTPUT_FORMATS) == set(_OUTPUT_FORMAT_TO_WRITER_CLASS_MAP.keys())
1✔
70

71
_log = logging.getLogger("pygount")
1✔
72

73

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

84
    if encoding_to_check not in (alternative_encoding, "chardet", None):
1✔
85
        try:
1✔
86
            "".encode(encoding_to_check)
1✔
UNCOV
87
        except LookupError:
×
UNCOV
88
            raise pygount.common.OptionError(
×
89
                f'{name} is "{encoding_to_check}" but must be "{alternative_encoding}" or a known Python encoding',
90
                source,
91
            ) from None
92

93

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

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

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

131
    @property
1✔
132
    def default_encoding(self):
1✔
133
        return self._default_encoding
1✔
134

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

139
    @property
1✔
140
    def fallback_encoding(self):
1✔
141
        return self._fallback_encoding
1✔
142

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

147
    @property
1✔
148
    def folders_to_skip(self):
1✔
149
        return self._folders_to_skip
1✔
150

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

156
    @property
1✔
157
    def generated_regexps(self):
1✔
UNCOV
158
        return self._generated_regexs
×
159

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

165
    @property
1✔
166
    def has_duplicates(self):
1✔
167
        return self._has_duplicates
1✔
168

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

172
    @property
1✔
173
    def has_to_merge_embedded_languages(self):
1✔
174
        return self._has_to_merge_embedded_languages
1✔
175

176
    def set_has_to_merge_embedded_languages(self, has_to_merge_embedded_languages, source=None):
1✔
177
        self._has_to_merge_embedded_languages = bool(has_to_merge_embedded_languages)
1✔
178

179
    @property
1✔
180
    def is_verbose(self):
1✔
181
        return self._is_verbose
1✔
182

183
    def set_is_verbose(self, is_verbose, source=None):
1✔
184
        self._is_verbose = bool(is_verbose)
1✔
185

186
    @property
1✔
187
    def names_to_skip(self):
1✔
188
        return self._names_to_skip
1✔
189

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

195
    @property
1✔
196
    def output(self):
1✔
197
        return self._output
1✔
198

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

203
    @property
1✔
204
    def output_format(self):
1✔
205
        return self._output_format
1✔
206

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

215
    @property
1✔
216
    def source_patterns(self):
1✔
217
        return self._source_patterns
1✔
218

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

224
    @property
1✔
225
    def suffixes(self):
1✔
226
        return self._suffixes
1✔
227

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

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

290
    def parsed_args(self, arguments):
1✔
291
        assert arguments is not None
1✔
292

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

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

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

374

375
def pygount_command(arguments=None):
1✔
376
    result = 1
1✔
377
    command = Command()
1✔
378
    try:
1✔
379
        command.apply_arguments(arguments)
1✔
380
        command.execute()
1✔
381
        result = 0
1✔
382
    except KeyboardInterrupt:  # pragma: no cover
383
        _log.error("interrupted as requested by user")
384
    except (pygount.common.OptionError, OSError) as error:
1✔
385
        _log.error(error)
1✔
386
    except Exception as error:
1✔
UNCOV
387
        _log.exception(error)
×
388

389
    return result
1✔
390

391

392
def main():  # pragma: no cover
393
    logging.basicConfig(level=logging.WARNING)
394
    sys.exit(pygount_command())
395

396

397
if __name__ == "__main__":  # pragma: no cover
398
    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

© 2026 Coveralls, Inc