Skip to content

Commit 812acdf

Browse files
authored
Merge pull request #11 from InnoFang/dev
♻️ ⚡ ❇️ 🎨 refactor CodeCounter and improve structure of the code
2 parents 58d7b18 + 5985a8d commit 812acdf

File tree

6 files changed

+169
-128
lines changed

6 files changed

+169
-128
lines changed

code_counter/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def main():
1515
config.invoke(args.config())
1616
return
1717

18-
code_counter = CodeCounter(config)
18+
code_counter = CodeCounter()
1919

2020
search_args = args.search()
2121
code_counter.setSearchArgs(search_args)

code_counter/conf/config.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python3
2-
# coding:utf8
2+
# -*- coding: utf-8 -*-
33

44
import json
55
import pkg_resources
@@ -12,8 +12,16 @@ def default(self, obj):
1212
return json.JSONEncoder.default(self, obj)
1313

1414

15-
class Config:
15+
class SingletonMeta(type):
16+
_instance = {}
1617

18+
def __call__(cls, *args, **kwargs):
19+
if cls not in cls._instance:
20+
cls._instance[cls] = super(SingletonMeta, cls).__call__(*args, **kwargs)
21+
return cls._instance[cls]
22+
23+
24+
class Config(metaclass=SingletonMeta):
1725
def __init__(self):
1826
conf = self.__load()
1927

code_counter/core/countable/__init__.py

Whitespace-only changes.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
3+
4+
import os
5+
from code_counter.conf.config import Config
6+
7+
8+
class CountableFile:
9+
def __init__(self, url_path: str):
10+
self._url_path = url_path
11+
self._format_output = []
12+
self._comment_symbol = tuple(Config().comment)
13+
14+
self.file_type = os.path.splitext(url_path)[1][1:]
15+
self.file_lines = 0
16+
self.code_lines = 0
17+
self.blank_lines = 0
18+
self.comment_lines = 0
19+
20+
def __file_content(self):
21+
with open(self._url_path, 'rb') as content:
22+
return content.readlines()
23+
24+
def count(self):
25+
single = {
26+
'file_lines': 0,
27+
'code_lines': 0,
28+
'blank_lines': 0,
29+
'comment_lines': 0,
30+
}
31+
32+
for line_number, raw_line in enumerate(self.__file_content()):
33+
try:
34+
line = raw_line.strip().decode('utf8')
35+
except UnicodeDecodeError:
36+
try:
37+
# If the code line contain Chinese string, decode it as gbk
38+
line = raw_line.strip().decode('gbk')
39+
except UnicodeDecodeError:
40+
self._format_output.append(
41+
'\n\t{:>10} | decode line occurs a problem, non-count it, at File "{}", line {}:'.format(
42+
'WARN', self._url_path, line_number))
43+
self._format_output.append('\t{:>10} | {}\n'.format(' ', raw_line))
44+
continue
45+
46+
single['file_lines'] += 1
47+
if not line:
48+
single['blank_lines'] += 1
49+
elif line.startswith(tuple(self._comment_symbol)):
50+
single['comment_lines'] += 1
51+
else:
52+
single['code_lines'] += 1
53+
54+
self.file_lines = single['file_lines']
55+
self.code_lines = single['code_lines']
56+
self.blank_lines = single['blank_lines']
57+
self.comment_lines = single['comment_lines']
58+
59+
def __str__(self):
60+
self._format_output.append(
61+
'\t{:>10} |{:>10} |{:>10} |{:>10} |{:>10} | {}'.format(self.file_type, self.file_lines,
62+
self.code_lines, self.blank_lines,
63+
self.comment_lines,
64+
self._url_path))
65+
return '\n'.join(self._format_output)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
from collections.abc import Iterator
2+
import os
3+
# !/usr/bin/env python3
4+
# -*- coding: utf-8 -*-
5+
6+
from code_counter.core.countable.file import CountableFile
7+
from code_counter.conf.config import Config
8+
from collections import deque
9+
10+
11+
class CountableFileIterator(Iterator):
12+
def __init__(self, path):
13+
self._ignore = Config().ignore
14+
self._suffix = Config().suffix
15+
self._file_queue = deque()
16+
self.__search(path)
17+
18+
def __search(self, input_path):
19+
if os.path.isdir(input_path):
20+
files = os.listdir(input_path)
21+
for file in files:
22+
file_path = os.path.join(input_path, file)
23+
if os.path.isdir(file_path):
24+
if file_path in self._ignore:
25+
continue
26+
self.__search(file_path)
27+
else:
28+
suffix = os.path.splitext(file)[1]
29+
if len(suffix) == 0 or suffix[1:] not in self._suffix:
30+
continue
31+
file_path = os.path.join(input_path, file)
32+
self._file_queue.append(CountableFile(file_path))
33+
elif os.path.isfile(input_path):
34+
suffix = os.path.splitext(input_path)[1]
35+
if len(suffix) > 0 and suffix[1:] in self._suffix:
36+
self._file_queue.append(CountableFile(input_path))
37+
38+
def __iter__(self):
39+
return self
40+
41+
def __next__(self) -> CountableFile:
42+
if self._file_queue:
43+
fc = self._file_queue.popleft()
44+
return fc
45+
else:
46+
raise StopIteration

code_counter/core/counter.py

Lines changed: 47 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
#!/usr/bin/env python3
2-
# coding:utf8
2+
# -*- coding: utf-8 -*-
3+
34
import os
4-
import re
55
from collections import defaultdict
6+
from code_counter.conf.config import Config
7+
from code_counter.core.countable.iterator import CountableFileIterator
68

79

810
class CodeCounter:
911

10-
def __init__(self, config):
12+
def __init__(self):
13+
self.config = Config()
14+
1115
self.total_file_lines = 0
1216
self.total_code_lines = 0
1317
self.total_blank_lines = 0
1418
self.total_comment_lines = 0
1519
self.files_of_language = defaultdict(int)
16-
self.config = config
1720

1821
self.search_args = None
19-
self.comment_symbol = ()
20-
self.ignore = ()
2122
self.lines_of_language = {}
22-
self.pattern = None
2323

2424
self.result = {
2525
'total': {
@@ -33,42 +33,59 @@ def __init__(self, config):
3333

3434
def setSearchArgs(self, args):
3535
self.search_args = args
36+
if args.suffix:
37+
self.config.suffix = set(args.suffix)
38+
if args.comment:
39+
self.config.comment = set(args.comment)
40+
if args.ignore:
41+
self.config.ignore = set(args.ignore)
3642

37-
suffix = args.suffix if args.suffix else self.config.suffix
38-
self.comment_symbol = tuple(args.comment if args.comment else self.config.comment)
39-
self.ignore = tuple(args.ignore if args.ignore else self.config.ignore)
40-
41-
self.lines_of_language = {suffix: 0 for suffix in suffix}
42-
43-
regex = '.*\.({})$'.format('|'.join(suffix))
44-
self.pattern = re.compile(regex)
43+
self.lines_of_language = {suffix: 0 for suffix in self.config.suffix}
4544

4645
def search(self):
47-
if not self.search_args:
48-
raise Exception('search_args is None, please invoke setSearchArgs first.')
46+
if self.search_args is None:
47+
raise Exception('search_args is None, please invoke the `setSearchArgs` function first.')
4948

5049
input_path = self.search_args.input_path
51-
output_path = self.search_args.output_path
50+
if not input_path:
51+
print('{} is not a validate path.'.format(input_path))
52+
return
5253

54+
output_path = self.search_args.output_path
5355
output_file = open(output_path, 'w') if output_path else None
5456

5557
if self.search_args.verbose:
56-
print('\n\t{}'.format("SEARCHING"), file=output_file)
57-
print("\t{}".format('=' * 20), file=output_file)
58-
print('\t{:>10} |{:>10} |{:>10} |{:>10} |{:>10} | {}'
59-
.format("File Type", "Lines", "Code", "Blank", "Comment", "File Path"), file=output_file)
60-
print("\t{}".format('-' * 90), file=output_file)
61-
62-
if not input_path:
63-
print('{} is not a validate path.'.format(input_path))
64-
return
58+
self.print_searching_verbose_info(output_file)
6559

6660
for path in input_path:
6761
if os.path.exists(path):
68-
self.__search(path, output_file)
62+
for cf in CountableFileIterator(path):
63+
cf.count()
64+
if self.search_args.verbose:
65+
print(cf, file=output_file)
66+
self.files_of_language[cf.file_type] += 1
67+
self.total_file_lines += cf.file_lines
68+
self.total_code_lines += cf.code_lines
69+
self.total_blank_lines += cf.blank_lines
70+
self.total_comment_lines += cf.comment_lines
71+
self.lines_of_language[cf.file_type] += cf.code_lines
6972
else:
7073
print('{} is not a validate path.'.format(path))
74+
exit(1)
75+
76+
self.print_result_info(output_file)
77+
78+
if output_file:
79+
output_file.close()
80+
81+
def print_searching_verbose_info(self, output_file=None):
82+
print('\n\t{}'.format("SEARCHING"), file=output_file)
83+
print("\t{}".format('=' * 20), file=output_file)
84+
print('\t{:>10} |{:>10} |{:>10} |{:>10} |{:>10} | {}'
85+
.format("File Type", "Lines", "Code", "Blank", "Comment", "File Path"), file=output_file)
86+
print("\t{}".format('-' * 90), file=output_file)
7187

88+
def print_result_info(self, output_file=None):
7289
print('\n\t{}'.format("RESULT"), file=output_file)
7390
print("\t{}".format('=' * 20), file=output_file)
7491
print("\t{:<20}:{:>8} ({:>7})"
@@ -106,106 +123,11 @@ def search(self):
106123

107124
for tp, cnt in self.files_of_language.items():
108125
code_line = self.lines_of_language[tp]
126+
self.result['code'][tp] = code_line
127+
self.result['file'][tp] = cnt
109128
print("\t{:>10} |{:>10} |{:>10} |{:>10} |{:>10}".format(
110129
tp, cnt, '%.2f%%' % (cnt / total_files * 100),
111130
code_line, '%.2f%%' % (code_line / self.total_code_lines * 100)), file=output_file)
112-
self.result['code'][tp] = code_line
113-
self.result['file'][tp] = cnt
114-
115-
if output_file:
116-
output_file.close()
117-
118-
def __search(self, input_path, output_file=None):
119-
"""
120-
:param input_path: input file path
121-
:param output_file: output file path
122-
:return:
123-
"""
124-
if os.path.isdir(input_path):
125-
files = os.listdir(input_path)
126-
for file in files:
127-
file_path = os.path.join(input_path, file)
128-
if os.path.isdir(file_path):
129-
if os.path.split(file_path)[-1] in self.ignore:
130-
continue
131-
self.__search(file_path, output_file)
132-
elif os.path:
133-
file_path = os.path.join(input_path, file)
134-
self.__format_output(file_path, output_file)
135-
elif os.path.isfile(input_path):
136-
self.__format_output(input_path, output_file)
137-
138-
def __format_output(self, file_path, output_file=None):
139-
"""
140-
:param file_path: input file path
141-
:param output_file: output file path
142-
:return:
143-
"""
144-
try:
145-
res = re.match(self.pattern, file_path)
146-
if res:
147-
single = self.count_single(file_path, output_file)
148-
file_lines = single['file_lines']
149-
code_lines = single['code_lines']
150-
blank_lines = single['blank_lines']
151-
comment_lines = single['comment_lines']
152-
153-
file_type = os.path.splitext(file_path)[1][1:]
154-
self.files_of_language[file_type] += 1
155-
156-
if self.search_args.verbose:
157-
print('\t{:>10} |{:>10} |{:>10} |{:>10} |{:>10} | {}'
158-
.format(file_type, file_lines, code_lines, blank_lines, comment_lines, file_path),
159-
file=output_file)
160-
161-
self.total_file_lines += file_lines
162-
self.total_code_lines += code_lines
163-
self.total_blank_lines += blank_lines
164-
self.total_comment_lines += comment_lines
165-
self.lines_of_language[file_type] += code_lines
166-
except AttributeError as e:
167-
print(e)
168-
169-
def count_single(self, file_path, output_file=None):
170-
"""
171-
:param file_path: the file you want to count
172-
:param output_file: output file path
173-
:return: single { file_lines, code_lines, blank_lines, comment_lines }
174-
"""
175-
assert os.path.isfile(file_path), "Function: 'code_counter' need a file path, but {} is not.".format(file_path)
176-
177-
single = {
178-
'file_lines': 0,
179-
'code_lines': 0,
180-
'blank_lines': 0,
181-
'comment_lines': 0,
182-
}
183-
184-
with open(file_path, 'rb') as handle:
185-
for line_number, raw_line in enumerate(handle):
186-
try:
187-
line = raw_line.strip().decode('utf8')
188-
except UnicodeDecodeError:
189-
try:
190-
# If the code line contain Chinese string, decode it as gbk
191-
line = raw_line.strip().decode('gbk')
192-
except UnicodeDecodeError:
193-
if self.search_args.verbose:
194-
print('\n\t{:>10} | decode line occurs a problem, non-count it, at File "{}", line {}:'
195-
.format('WARN', file_path, line_number),
196-
file=output_file)
197-
print('\t{:>10} | {}\n'
198-
.format(' ', raw_line))
199-
continue
200-
201-
single['file_lines'] += 1
202-
if not line:
203-
single['blank_lines'] += 1
204-
elif line.startswith(self.comment_symbol):
205-
single['comment_lines'] += 1
206-
else:
207-
single['code_lines'] += 1
208-
return single
209131

210132
def visualize(self):
211133
from matplotlib import pyplot as plt

0 commit comments

Comments
 (0)