204 lines
6.6 KiB
Python
Executable File
204 lines
6.6 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
#
|
|
# Copyright (c) 2019 Collabora, Ltd.
|
|
#
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# Author(s): Ryan Pavlik <ryan.pavlik@collabora.com>
|
|
#
|
|
# Purpose: This script converts leading comments on some Python
|
|
# classes and functions into docstrings.
|
|
# It doesn't attempt to deal with line continuations, etc.
|
|
# so you may want to "join line" on your def statements
|
|
# temporarily before running.
|
|
|
|
import re
|
|
|
|
from spec_tools.file_process import LinewiseFileProcessor
|
|
|
|
COMMENT_RE = re.compile(r" *#(!.*| (?P<content>.*))?")
|
|
CONVERTIBLE_DEF_RE = re.compile(r"(?P<indentation> *)(def|class) .*:")
|
|
|
|
|
|
class CommentConverter(LinewiseFileProcessor):
|
|
def __init__(self, single_line_quotes=False, allow_blank_lines=False):
|
|
super().__init__()
|
|
self.comment_lines = []
|
|
"Temporary storage for contiguous comment lines."
|
|
|
|
self.trailing_empty_lines = []
|
|
"Temporary storage for empty lines following a comment."
|
|
|
|
self.output_lines = []
|
|
"Fully-processed output lines."
|
|
|
|
self.single_line_quotes = single_line_quotes
|
|
"Whether we generate simple, single-line quotes for single line comments."
|
|
|
|
self.allow_blank_lines = allow_blank_lines
|
|
"Whether we allow blank lines between a comment and the thing it's considered to document."
|
|
|
|
self.done_with_initial_comment = False
|
|
"Have we read our first non-comment line yet?"
|
|
|
|
def output_line(self, line=None):
|
|
if line:
|
|
self.output_lines.append(line)
|
|
else:
|
|
self.output_lines.append("")
|
|
|
|
def output_normal_line(self, line):
|
|
# flush any comment lines we had stored and output this line.
|
|
self.dump_comment_lines()
|
|
self.output_line(line)
|
|
|
|
def dump_comment_lines(self):
|
|
# Early out for empty
|
|
if not self.comment_lines:
|
|
return
|
|
|
|
for line in self.comment_lines:
|
|
self.output_line(line)
|
|
self.comment_lines = []
|
|
|
|
for line in self.trailing_empty_lines:
|
|
self.output_line(line)
|
|
self.trailing_empty_lines = []
|
|
|
|
def dump_converted_comment_lines(self, indent):
|
|
# Early out for empty
|
|
if not self.comment_lines:
|
|
return
|
|
|
|
for line in self.trailing_empty_lines:
|
|
self.output_line(line)
|
|
self.trailing_empty_lines = []
|
|
|
|
indent = indent + ' '
|
|
|
|
def extract(line):
|
|
match = COMMENT_RE.match(line)
|
|
content = match.group('content')
|
|
if content:
|
|
return content
|
|
return ""
|
|
|
|
# Extract comment content
|
|
lines = [extract(line) for line in self.comment_lines]
|
|
|
|
# Drop leading empty comments.
|
|
while lines and not lines[0].strip():
|
|
lines.pop(0)
|
|
|
|
# Drop trailing empty comments.
|
|
while lines and not lines[-1].strip():
|
|
lines.pop()
|
|
|
|
# Add single- or multi-line-string quote
|
|
if self.single_line_quotes \
|
|
and len(lines) == 1 \
|
|
and '"' not in lines[0]:
|
|
quote = '"'
|
|
else:
|
|
quote = '"""'
|
|
lines[0] = quote + lines[0]
|
|
lines[-1] = lines[-1] + quote
|
|
|
|
# Output lines, indenting content as required.
|
|
for line in lines:
|
|
if line:
|
|
self.output_line(indent + line)
|
|
else:
|
|
# Don't indent empty comment lines
|
|
self.output_line()
|
|
|
|
# Clear stored comment lines since we processed them
|
|
self.comment_lines = []
|
|
|
|
def queue_comment_line(self, line):
|
|
if self.trailing_empty_lines:
|
|
# If we had blank lines between comment lines, they are separate blocks
|
|
self.dump_comment_lines()
|
|
self.comment_lines.append(line)
|
|
|
|
def handle_empty_line(self, line):
|
|
"""Handle an empty line.
|
|
|
|
Contiguous empty lines between a comment and something documentable do not
|
|
disassociate the comment from the documentable thing.
|
|
We have someplace else to store these lines in case there isn't something
|
|
documentable coming up."""
|
|
if self.comment_lines and self.allow_blank_lines:
|
|
self.trailing_empty_lines.append(line)
|
|
else:
|
|
self.output_normal_line(line)
|
|
|
|
def is_next_line_doc_comment(self):
|
|
next_line = self.next_line_rstripped
|
|
if next_line is None:
|
|
return False
|
|
|
|
return next_line.strip().startswith('"')
|
|
|
|
def process_line(self, line_num, line):
|
|
line = line.rstrip()
|
|
comment_match = COMMENT_RE.match(line)
|
|
def_match = CONVERTIBLE_DEF_RE.match(line)
|
|
|
|
# First check if this is a comment line.
|
|
if comment_match:
|
|
if self.done_with_initial_comment:
|
|
self.queue_comment_line(line)
|
|
else:
|
|
self.output_line(line)
|
|
else:
|
|
# If not a comment line, then by definition we're done with the comment header.
|
|
self.done_with_initial_comment = True
|
|
if not line.strip():
|
|
self.handle_empty_line(line)
|
|
elif def_match and not self.is_next_line_doc_comment():
|
|
# We got something we can make a docstring for:
|
|
# print the thing the docstring is for first,
|
|
# then the converted comment.
|
|
|
|
indent = def_match.group('indentation')
|
|
self.output_line(line)
|
|
self.dump_converted_comment_lines(indent)
|
|
else:
|
|
# Can't make a docstring for this line:
|
|
self.output_normal_line(line)
|
|
|
|
def process(self, fn, write=False):
|
|
self.process_file(fn)
|
|
|
|
if write:
|
|
with open(fn, 'w', encoding='utf-8') as fp:
|
|
for line in self.output_lines:
|
|
fp.write(line)
|
|
fp.write('\n')
|
|
|
|
# Reset state
|
|
self.__init__(self.single_line_quotes, self.allow_blank_lines)
|
|
|
|
|
|
def main():
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('filenames', metavar='filename',
|
|
type=str, nargs='+',
|
|
help='A Python file to transform.')
|
|
parser.add_argument('-b', '--blanklines', action='store_true',
|
|
help='Allow blank lines between a comment and a define and still convert that comment.')
|
|
|
|
args = parser.parse_args()
|
|
|
|
converter = CommentConverter(allow_blank_lines=args.blanklines)
|
|
for fn in args.filenames:
|
|
print("Processing", fn)
|
|
converter.process(fn, write=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|