|
1 | 1 | #!/usr/bin/env python3
|
2 | 2 | # -*- coding: utf-8 -*-
|
3 |
| -"""Estimate project size in lines of code. |
| 3 | +"""Estimate project size in lines of code.""" |
4 | 4 |
|
5 |
| -Ignores blank lines, docstrings, and whole-line comments.""" |
| 5 | +# TODO: add sorting options: name, code count, SLOC count, code ratio. |
6 | 6 |
|
7 | 7 | import os
|
8 | 8 | import re
|
9 | 9 | from operator import itemgetter
|
10 | 10 |
|
11 | 11 | def listpy(path):
|
12 |
| - return list(sorted(fn for fn in os.listdir(path) if fn.endswith(".py"))) |
| 12 | + return list(sorted(filename for filename in os.listdir(path) if filename.endswith(".py"))) |
13 | 13 |
|
14 |
| -def loc(code, blanks, docstrings, comments): # blanks et al.: include this item? |
| 14 | +def count_sloc(code, *, blanks, docstrings, comments): |
| 15 | + """blanks et al.: include this item?""" |
15 | 16 | if not docstrings:
|
16 | 17 | # TODO: make sure it's a docstring (and not some other """...""" string)
|
17 | 18 | code = re.sub(r'""".*?"""', r'', code, flags=(re.MULTILINE + re.DOTALL))
|
| 19 | + code = re.sub(r"'''.*?'''", r'', code, flags=(re.MULTILINE + re.DOTALL)) |
18 | 20 | lines = code.split("\n")
|
19 | 21 | if not blanks:
|
20 | 22 | lines = [line for line in lines if line.strip()]
|
21 | 23 | if not comments:
|
22 |
| - # TODO: removes only whole-line comments. |
23 |
| - lines = [line for line in lines if not line.strip().startswith("#")] |
| 24 | + lines = [line for line in lines if not line.strip().startswith("#")] # ignore whole-line comments |
24 | 25 | return len(lines)
|
25 | 26 |
|
26 |
| -def analyze(items, blanks=False, docstrings=False, comments=False): |
27 |
| - grandtotal = 0 |
28 |
| - for name, p in items: |
29 |
| - path = os.path.join(*p) |
30 |
| - files = listpy(path) |
31 |
| - ns = [] |
32 |
| - for fn in files: |
33 |
| - with open(os.path.join(path, fn), "rt", encoding="utf-8") as f: |
| 27 | +def report(paths): |
| 28 | + print(f"Code size for {os.getcwd()}") |
| 29 | + def format_name(s, width=25): |
| 30 | + return s.ljust(width) |
| 31 | + def format_number(n, width=5): |
| 32 | + return str(n).rjust(width) |
| 33 | + def format_path(s): # ./subdir/something |
| 34 | + def label(s): |
| 35 | + if s == ".": |
| 36 | + return "top level" |
| 37 | + return s[2:] |
| 38 | + return format_name(label(s)) |
| 39 | + codes_grandtotal = 0 |
| 40 | + slocs_grandtotal = 0 |
| 41 | + for path in paths: |
| 42 | + filenames = listpy(path) |
| 43 | + results = [] |
| 44 | + for filename in filenames: |
| 45 | + with open(os.path.join(path, filename), "rt", encoding="utf-8") as f: |
34 | 46 | content = f.read()
|
35 |
| - ns.append(loc(content, blanks, docstrings, comments)) |
36 |
| - # report |
37 |
| - print(f"{name}:") |
38 |
| - for fn, n in sorted(zip(files, ns), key=itemgetter(1)): |
39 |
| - print(f" {fn} {n}") |
40 |
| - grouptotal = sum(ns) |
41 |
| - print(f" total for {name} {grouptotal}") |
42 |
| - grandtotal += grouptotal |
43 |
| - print(f"grand total {grandtotal}") |
| 47 | + code = count_sloc(content, blanks=False, docstrings=False, comments=False) |
| 48 | + sloc = count_sloc(content, blanks=True, docstrings=True, comments=True) |
| 49 | + results.append((code, sloc)) |
| 50 | + |
| 51 | + if results: |
| 52 | + codes, slocs = zip(*results) |
| 53 | + codes = sum(codes) |
| 54 | + slocs = sum(slocs) |
| 55 | + print(f"\n {format_path(path)} {format_number(codes)} / {format_number(slocs)} {int(codes / slocs * 100):d}% code") |
| 56 | + for filename, (code, sloc) in sorted(zip(filenames, results), key=itemgetter(1)): |
| 57 | + print(f" {format_name(filename)} {format_number(code)} / {format_number(sloc)} {int(code / sloc * 100):d}% code") |
| 58 | + codes_grandtotal += codes |
| 59 | + slocs_grandtotal += slocs |
| 60 | + print(f"\n{format_name('Total')} {format_number(codes_grandtotal)} / {format_number(slocs_grandtotal)} {int(codes_grandtotal / slocs_grandtotal * 100):d}% code") |
44 | 61 |
|
45 | 62 | def main():
|
46 |
| - items = (("top level", ["."]), |
47 |
| - ("regular code", ["unpythonic"]), |
48 |
| - ("regular code tests", ["unpythonic", "tests"]), |
49 |
| - ("testing framework (not counting macros)", ["unpythonic", "test"]), |
50 |
| - ("REPL/networking code", ["unpythonic", "net"]), |
51 |
| - ("REPL/networking tests", ["unpythonic", "net", "tests"]), |
52 |
| - ("macros", ["unpythonic", "syntax"]), |
53 |
| - ("macro tests", ["unpythonic", "syntax", "tests"])) |
54 |
| - print("Raw (with blanks, docstrings and comments)") |
55 |
| - analyze(items, blanks=True, docstrings=True, comments=True) |
56 |
| - print("\nFiltered (non-blank code lines only)") |
57 |
| - analyze(items) |
| 63 | + blacklist = [".git", "build", "dist", "__pycache__", "00_stuff"] |
| 64 | + paths = [] |
| 65 | + for root, dirs, files in os.walk("."): |
| 66 | + paths.append(root) |
| 67 | + for x in blacklist: |
| 68 | + if x in dirs: |
| 69 | + dirs.remove(x) |
| 70 | + report(sorted(paths)) |
58 | 71 |
|
59 | 72 | if __name__ == '__main__':
|
60 | 73 | main()
|
0 commit comments