1 2KINDS = [ 3 'section-major', 4 'section-minor', 5 'section-group', 6 'row', 7] 8 9 10def iter_clean_lines(lines): 11 lines = iter(lines) 12 for rawline in lines: 13 line = rawline.strip() 14 if line.startswith('#') and not rawline.startswith('##'): 15 continue 16 yield line, rawline 17 18 19def parse_table_lines(lines): 20 lines = iter_clean_lines(lines) 21 22 group = None 23 prev = '' 24 for line, rawline in lines: 25 if line.startswith('## '): 26 assert not rawline.startswith(' '), (line, rawline) 27 if group: 28 assert prev, (line, rawline) 29 kind, after, _ = group 30 assert kind and kind != 'section-group', (group, line, rawline) 31 assert after is not None, (group, line, rawline) 32 else: 33 assert not prev, (prev, line, rawline) 34 kind, after = group = ('section-group', None) 35 title = line[3:].lstrip() 36 assert title, (line, rawline) 37 if after is not None: 38 try: 39 line, rawline = next(lines) 40 except StopIteration: 41 line = None 42 if line != after: 43 raise NotImplementedError((group, line, rawline)) 44 yield kind, title 45 group = None 46 elif group: 47 raise NotImplementedError((group, line, rawline)) 48 elif line.startswith('##---'): 49 assert line.rstrip('-') == '##', (line, rawline) 50 group = ('section-minor', '', line) 51 elif line.startswith('#####'): 52 assert not line.strip('#'), (line, rawline) 53 group = ('section-major', '', line) 54 elif line: 55 yield 'row', line 56 prev = line 57 58 59def iter_sections(lines): 60 header = None 61 section = [] 62 for kind, value in parse_table_lines(lines): 63 if kind == 'row': 64 if not section: 65 if header is None: 66 header = value 67 continue 68 raise NotImplementedError(repr(value)) 69 yield tuple(section), value 70 else: 71 if header is None: 72 header = False 73 start = KINDS.index(kind) 74 section[start:] = [value] 75 76 77def collect_sections(lines): 78 sections = {} 79 for section, row in iter_sections(lines): 80 if section not in sections: 81 sections[section] = [row] 82 else: 83 sections[section].append(row) 84 return sections 85 86 87def collate_sections(lines): 88 collated = {} 89 for section, rows in collect_sections(lines).items(): 90 parent = collated 91 current = () 92 for name in section: 93 current += (name,) 94 try: 95 child, secrows, totalrows = parent[name] 96 except KeyError: 97 child = {} 98 secrows = [] 99 totalrows = [] 100 parent[name] = (child, secrows, totalrows) 101 parent = child 102 if current == section: 103 secrows.extend(rows) 104 totalrows.extend(rows) 105 return collated 106 107 108############################# 109# the commands 110 111def cmd_count_by_section(lines): 112 div = ' ' + '-' * 50 113 total = 0 114 def render_tree(root, depth=0): 115 nonlocal total 116 indent = ' ' * depth 117 for name, data in root.items(): 118 subroot, rows, totalrows = data 119 sectotal = f'({len(totalrows)})' if totalrows != rows else '' 120 count = len(rows) if rows else '' 121 if depth == 0: 122 yield div 123 yield f'{sectotal:>7} {count:>4} {indent}{name}' 124 yield from render_tree(subroot, depth+1) 125 total += len(rows) 126 sections = collate_sections(lines) 127 yield from render_tree(sections) 128 yield div 129 yield f'(total: {total})' 130 131 132############################# 133# the script 134 135def parse_args(argv=None, prog=None): 136 import argparse 137 parser = argparse.ArgumentParser(prog=prog) 138 parser.add_argument('filename') 139 140 args = parser.parse_args(argv) 141 ns = vars(args) 142 143 return ns 144 145 146def main(filename): 147 with open(filename) as infile: 148 for line in cmd_count_by_section(infile): 149 print(line) 150 151 152if __name__ == '__main__': 153 kwargs = parse_args() 154 main(**kwargs) 155