1e5c31af7Sopenharmony_ci#!/usr/bin/env python3 2e5c31af7Sopenharmony_ci# 3e5c31af7Sopenharmony_ci# Copyright (c) 2019 Collabora, Ltd. 4e5c31af7Sopenharmony_ci# 5e5c31af7Sopenharmony_ci# SPDX-License-Identifier: Apache-2.0 6e5c31af7Sopenharmony_ci# 7e5c31af7Sopenharmony_ci# Author(s): Ryan Pavlik <ryan.pavlik@collabora.com> 8e5c31af7Sopenharmony_ci# 9e5c31af7Sopenharmony_ci# Purpose: This script converts leading comments on some Python 10e5c31af7Sopenharmony_ci# classes and functions into docstrings. 11e5c31af7Sopenharmony_ci# It doesn't attempt to deal with line continuations, etc. 12e5c31af7Sopenharmony_ci# so you may want to "join line" on your def statements 13e5c31af7Sopenharmony_ci# temporarily before running. 14e5c31af7Sopenharmony_ci 15e5c31af7Sopenharmony_ciimport re 16e5c31af7Sopenharmony_ci 17e5c31af7Sopenharmony_cifrom spec_tools.file_process import LinewiseFileProcessor 18e5c31af7Sopenharmony_ci 19e5c31af7Sopenharmony_ciCOMMENT_RE = re.compile(r" *#(!.*| (?P<content>.*))?") 20e5c31af7Sopenharmony_ciCONVERTIBLE_DEF_RE = re.compile(r"(?P<indentation> *)(def|class) .*:") 21e5c31af7Sopenharmony_ci 22e5c31af7Sopenharmony_ci 23e5c31af7Sopenharmony_ciclass CommentConverter(LinewiseFileProcessor): 24e5c31af7Sopenharmony_ci def __init__(self, single_line_quotes=False, allow_blank_lines=False): 25e5c31af7Sopenharmony_ci super().__init__() 26e5c31af7Sopenharmony_ci self.comment_lines = [] 27e5c31af7Sopenharmony_ci "Temporary storage for contiguous comment lines." 28e5c31af7Sopenharmony_ci 29e5c31af7Sopenharmony_ci self.trailing_empty_lines = [] 30e5c31af7Sopenharmony_ci "Temporary storage for empty lines following a comment." 31e5c31af7Sopenharmony_ci 32e5c31af7Sopenharmony_ci self.output_lines = [] 33e5c31af7Sopenharmony_ci "Fully-processed output lines." 34e5c31af7Sopenharmony_ci 35e5c31af7Sopenharmony_ci self.single_line_quotes = single_line_quotes 36e5c31af7Sopenharmony_ci "Whether we generate simple, single-line quotes for single line comments." 37e5c31af7Sopenharmony_ci 38e5c31af7Sopenharmony_ci self.allow_blank_lines = allow_blank_lines 39e5c31af7Sopenharmony_ci "Whether we allow blank lines between a comment and the thing it's considered to document." 40e5c31af7Sopenharmony_ci 41e5c31af7Sopenharmony_ci self.done_with_initial_comment = False 42e5c31af7Sopenharmony_ci "Have we read our first non-comment line yet?" 43e5c31af7Sopenharmony_ci 44e5c31af7Sopenharmony_ci def output_line(self, line=None): 45e5c31af7Sopenharmony_ci if line: 46e5c31af7Sopenharmony_ci self.output_lines.append(line) 47e5c31af7Sopenharmony_ci else: 48e5c31af7Sopenharmony_ci self.output_lines.append("") 49e5c31af7Sopenharmony_ci 50e5c31af7Sopenharmony_ci def output_normal_line(self, line): 51e5c31af7Sopenharmony_ci # flush any comment lines we had stored and output this line. 52e5c31af7Sopenharmony_ci self.dump_comment_lines() 53e5c31af7Sopenharmony_ci self.output_line(line) 54e5c31af7Sopenharmony_ci 55e5c31af7Sopenharmony_ci def dump_comment_lines(self): 56e5c31af7Sopenharmony_ci # Early out for empty 57e5c31af7Sopenharmony_ci if not self.comment_lines: 58e5c31af7Sopenharmony_ci return 59e5c31af7Sopenharmony_ci 60e5c31af7Sopenharmony_ci for line in self.comment_lines: 61e5c31af7Sopenharmony_ci self.output_line(line) 62e5c31af7Sopenharmony_ci self.comment_lines = [] 63e5c31af7Sopenharmony_ci 64e5c31af7Sopenharmony_ci for line in self.trailing_empty_lines: 65e5c31af7Sopenharmony_ci self.output_line(line) 66e5c31af7Sopenharmony_ci self.trailing_empty_lines = [] 67e5c31af7Sopenharmony_ci 68e5c31af7Sopenharmony_ci def dump_converted_comment_lines(self, indent): 69e5c31af7Sopenharmony_ci # Early out for empty 70e5c31af7Sopenharmony_ci if not self.comment_lines: 71e5c31af7Sopenharmony_ci return 72e5c31af7Sopenharmony_ci 73e5c31af7Sopenharmony_ci for line in self.trailing_empty_lines: 74e5c31af7Sopenharmony_ci self.output_line(line) 75e5c31af7Sopenharmony_ci self.trailing_empty_lines = [] 76e5c31af7Sopenharmony_ci 77e5c31af7Sopenharmony_ci indent = indent + ' ' 78e5c31af7Sopenharmony_ci 79e5c31af7Sopenharmony_ci def extract(line): 80e5c31af7Sopenharmony_ci match = COMMENT_RE.match(line) 81e5c31af7Sopenharmony_ci content = match.group('content') 82e5c31af7Sopenharmony_ci if content: 83e5c31af7Sopenharmony_ci return content 84e5c31af7Sopenharmony_ci return "" 85e5c31af7Sopenharmony_ci 86e5c31af7Sopenharmony_ci # Extract comment content 87e5c31af7Sopenharmony_ci lines = [extract(line) for line in self.comment_lines] 88e5c31af7Sopenharmony_ci 89e5c31af7Sopenharmony_ci # Drop leading empty comments. 90e5c31af7Sopenharmony_ci while lines and not lines[0].strip(): 91e5c31af7Sopenharmony_ci lines.pop(0) 92e5c31af7Sopenharmony_ci 93e5c31af7Sopenharmony_ci # Drop trailing empty comments. 94e5c31af7Sopenharmony_ci while lines and not lines[-1].strip(): 95e5c31af7Sopenharmony_ci lines.pop() 96e5c31af7Sopenharmony_ci 97e5c31af7Sopenharmony_ci # Add single- or multi-line-string quote 98e5c31af7Sopenharmony_ci if self.single_line_quotes \ 99e5c31af7Sopenharmony_ci and len(lines) == 1 \ 100e5c31af7Sopenharmony_ci and '"' not in lines[0]: 101e5c31af7Sopenharmony_ci quote = '"' 102e5c31af7Sopenharmony_ci else: 103e5c31af7Sopenharmony_ci quote = '"""' 104e5c31af7Sopenharmony_ci lines[0] = quote + lines[0] 105e5c31af7Sopenharmony_ci lines[-1] = lines[-1] + quote 106e5c31af7Sopenharmony_ci 107e5c31af7Sopenharmony_ci # Output lines, indenting content as required. 108e5c31af7Sopenharmony_ci for line in lines: 109e5c31af7Sopenharmony_ci if line: 110e5c31af7Sopenharmony_ci self.output_line(indent + line) 111e5c31af7Sopenharmony_ci else: 112e5c31af7Sopenharmony_ci # Don't indent empty comment lines 113e5c31af7Sopenharmony_ci self.output_line() 114e5c31af7Sopenharmony_ci 115e5c31af7Sopenharmony_ci # Clear stored comment lines since we processed them 116e5c31af7Sopenharmony_ci self.comment_lines = [] 117e5c31af7Sopenharmony_ci 118e5c31af7Sopenharmony_ci def queue_comment_line(self, line): 119e5c31af7Sopenharmony_ci if self.trailing_empty_lines: 120e5c31af7Sopenharmony_ci # If we had blank lines between comment lines, they are separate blocks 121e5c31af7Sopenharmony_ci self.dump_comment_lines() 122e5c31af7Sopenharmony_ci self.comment_lines.append(line) 123e5c31af7Sopenharmony_ci 124e5c31af7Sopenharmony_ci def handle_empty_line(self, line): 125e5c31af7Sopenharmony_ci """Handle an empty line. 126e5c31af7Sopenharmony_ci 127e5c31af7Sopenharmony_ci Contiguous empty lines between a comment and something documentable do not 128e5c31af7Sopenharmony_ci disassociate the comment from the documentable thing. 129e5c31af7Sopenharmony_ci We have someplace else to store these lines in case there isn't something 130e5c31af7Sopenharmony_ci documentable coming up.""" 131e5c31af7Sopenharmony_ci if self.comment_lines and self.allow_blank_lines: 132e5c31af7Sopenharmony_ci self.trailing_empty_lines.append(line) 133e5c31af7Sopenharmony_ci else: 134e5c31af7Sopenharmony_ci self.output_normal_line(line) 135e5c31af7Sopenharmony_ci 136e5c31af7Sopenharmony_ci def is_next_line_doc_comment(self): 137e5c31af7Sopenharmony_ci next_line = self.next_line_rstripped 138e5c31af7Sopenharmony_ci if next_line is None: 139e5c31af7Sopenharmony_ci return False 140e5c31af7Sopenharmony_ci 141e5c31af7Sopenharmony_ci return next_line.strip().startswith('"') 142e5c31af7Sopenharmony_ci 143e5c31af7Sopenharmony_ci def process_line(self, line_num, line): 144e5c31af7Sopenharmony_ci line = line.rstrip() 145e5c31af7Sopenharmony_ci comment_match = COMMENT_RE.match(line) 146e5c31af7Sopenharmony_ci def_match = CONVERTIBLE_DEF_RE.match(line) 147e5c31af7Sopenharmony_ci 148e5c31af7Sopenharmony_ci # First check if this is a comment line. 149e5c31af7Sopenharmony_ci if comment_match: 150e5c31af7Sopenharmony_ci if self.done_with_initial_comment: 151e5c31af7Sopenharmony_ci self.queue_comment_line(line) 152e5c31af7Sopenharmony_ci else: 153e5c31af7Sopenharmony_ci self.output_line(line) 154e5c31af7Sopenharmony_ci else: 155e5c31af7Sopenharmony_ci # If not a comment line, then by definition we're done with the comment header. 156e5c31af7Sopenharmony_ci self.done_with_initial_comment = True 157e5c31af7Sopenharmony_ci if not line.strip(): 158e5c31af7Sopenharmony_ci self.handle_empty_line(line) 159e5c31af7Sopenharmony_ci elif def_match and not self.is_next_line_doc_comment(): 160e5c31af7Sopenharmony_ci # We got something we can make a docstring for: 161e5c31af7Sopenharmony_ci # print the thing the docstring is for first, 162e5c31af7Sopenharmony_ci # then the converted comment. 163e5c31af7Sopenharmony_ci 164e5c31af7Sopenharmony_ci indent = def_match.group('indentation') 165e5c31af7Sopenharmony_ci self.output_line(line) 166e5c31af7Sopenharmony_ci self.dump_converted_comment_lines(indent) 167e5c31af7Sopenharmony_ci else: 168e5c31af7Sopenharmony_ci # Can't make a docstring for this line: 169e5c31af7Sopenharmony_ci self.output_normal_line(line) 170e5c31af7Sopenharmony_ci 171e5c31af7Sopenharmony_ci def process(self, fn, write=False): 172e5c31af7Sopenharmony_ci self.process_file(fn) 173e5c31af7Sopenharmony_ci 174e5c31af7Sopenharmony_ci if write: 175e5c31af7Sopenharmony_ci with open(fn, 'w', encoding='utf-8') as fp: 176e5c31af7Sopenharmony_ci for line in self.output_lines: 177e5c31af7Sopenharmony_ci fp.write(line) 178e5c31af7Sopenharmony_ci fp.write('\n') 179e5c31af7Sopenharmony_ci 180e5c31af7Sopenharmony_ci # Reset state 181e5c31af7Sopenharmony_ci self.__init__(self.single_line_quotes, self.allow_blank_lines) 182e5c31af7Sopenharmony_ci 183e5c31af7Sopenharmony_ci 184e5c31af7Sopenharmony_cidef main(): 185e5c31af7Sopenharmony_ci import argparse 186e5c31af7Sopenharmony_ci 187e5c31af7Sopenharmony_ci parser = argparse.ArgumentParser() 188e5c31af7Sopenharmony_ci parser.add_argument('filenames', metavar='filename', 189e5c31af7Sopenharmony_ci type=str, nargs='+', 190e5c31af7Sopenharmony_ci help='A Python file to transform.') 191e5c31af7Sopenharmony_ci parser.add_argument('-b', '--blanklines', action='store_true', 192e5c31af7Sopenharmony_ci help='Allow blank lines between a comment and a define and still convert that comment.') 193e5c31af7Sopenharmony_ci 194e5c31af7Sopenharmony_ci args = parser.parse_args() 195e5c31af7Sopenharmony_ci 196e5c31af7Sopenharmony_ci converter = CommentConverter(allow_blank_lines=args.blanklines) 197e5c31af7Sopenharmony_ci for fn in args.filenames: 198e5c31af7Sopenharmony_ci print("Processing", fn) 199e5c31af7Sopenharmony_ci converter.process(fn, write=True) 200e5c31af7Sopenharmony_ci 201e5c31af7Sopenharmony_ci 202e5c31af7Sopenharmony_ciif __name__ == "__main__": 203e5c31af7Sopenharmony_ci main() 204