| KINDS = [ | |
| 'section-major', | |
| 'section-minor', | |
| 'section-group', | |
| 'row', | |
| ] | |
| def iter_clean_lines(lines): | |
| lines = iter(lines) | |
| for rawline in lines: | |
| line = rawline.strip() | |
| if line.startswith('#') and not rawline.startswith('##'): | |
| continue | |
| yield line, rawline | |
| def parse_table_lines(lines): | |
| lines = iter_clean_lines(lines) | |
| group = None | |
| prev = '' | |
| for line, rawline in lines: | |
| if line.startswith('## '): | |
| assert not rawline.startswith(' '), (line, rawline) | |
| if group: | |
| assert prev, (line, rawline) | |
| kind, after, _ = group | |
| assert kind and kind != 'section-group', (group, line, rawline) | |
| assert after is not None, (group, line, rawline) | |
| else: | |
| assert not prev, (prev, line, rawline) | |
| kind, after = group = ('section-group', None) | |
| title = line[3:].lstrip() | |
| assert title, (line, rawline) | |
| if after is not None: | |
| try: | |
| line, rawline = next(lines) | |
| except StopIteration: | |
| line = None | |
| if line != after: | |
| raise NotImplementedError((group, line, rawline)) | |
| yield kind, title | |
| group = None | |
| elif group: | |
| raise NotImplementedError((group, line, rawline)) | |
| elif line.startswith('##---'): | |
| assert line.rstrip('-') == '##', (line, rawline) | |
| group = ('section-minor', '', line) | |
| elif line.startswith('#####'): | |
| assert not line.strip('#'), (line, rawline) | |
| group = ('section-major', '', line) | |
| elif line: | |
| yield 'row', line | |
| prev = line | |
| def iter_sections(lines): | |
| header = None | |
| section = [] | |
| for kind, value in parse_table_lines(lines): | |
| if kind == 'row': | |
| if not section: | |
| if header is None: | |
| header = value | |
| continue | |
| raise NotImplementedError(repr(value)) | |
| yield tuple(section), value | |
| else: | |
| if header is None: | |
| header = False | |
| start = KINDS.index(kind) | |
| section[start:] = [value] | |
| def collect_sections(lines): | |
| sections = {} | |
| for section, row in iter_sections(lines): | |
| if section not in sections: | |
| sections[section] = [row] | |
| else: | |
| sections[section].append(row) | |
| return sections | |
| def collate_sections(lines): | |
| collated = {} | |
| for section, rows in collect_sections(lines).items(): | |
| parent = collated | |
| current = () | |
| for name in section: | |
| current += (name,) | |
| try: | |
| child, secrows, totalrows = parent[name] | |
| except KeyError: | |
| child = {} | |
| secrows = [] | |
| totalrows = [] | |
| parent[name] = (child, secrows, totalrows) | |
| parent = child | |
| if current == section: | |
| secrows.extend(rows) | |
| totalrows.extend(rows) | |
| return collated | |
| ############################# | |
| # the commands | |
| def cmd_count_by_section(lines): | |
| div = ' ' + '-' * 50 | |
| total = 0 | |
| def render_tree(root, depth=0): | |
| nonlocal total | |
| indent = ' ' * depth | |
| for name, data in root.items(): | |
| subroot, rows, totalrows = data | |
| sectotal = f'({len(totalrows)})' if totalrows != rows else '' | |
| count = len(rows) if rows else '' | |
| if depth == 0: | |
| yield div | |
| yield f'{sectotal:>7} {count:>4} {indent}{name}' | |
| yield from render_tree(subroot, depth+1) | |
| total += len(rows) | |
| sections = collate_sections(lines) | |
| yield from render_tree(sections) | |
| yield div | |
| yield f'(total: {total})' | |
| ############################# | |
| # the script | |
| def parse_args(argv=None, prog=None): | |
| import argparse | |
| parser = argparse.ArgumentParser(prog=prog) | |
| parser.add_argument('filename') | |
| args = parser.parse_args(argv) | |
| ns = vars(args) | |
| return ns | |
| def main(filename): | |
| with open(filename) as infile: | |
| for line in cmd_count_by_section(infile): | |
| print(line) | |
| if __name__ == '__main__': | |
| kwargs = parse_args() | |
| main(**kwargs) |