#!/usr/bin/python3 # -*- coding: utf-8 -*- # Copyright 2016 The Cartographer Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """A dumb configuration.rst generator that relies on source comments.""" import io import os TARGET = 'docs/source/configuration.rst' ROOT = 'cartographer' PREFIX = """.. Copyright 2016 The Cartographer Authors .. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at .. http://www.apache.org/licenses/LICENSE-2.0 .. Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ============= Configuration ============= .. DO NOT EDIT! This documentation is AUTOGENERATED, please edit .proto files as .. needed and run scripts/update_configuration_doc.py. """ SUFFIX = """ """ NODOC = 'Not yet documented.' class Message(object): def __init__(self, name, package, preceding_comments): self.name = name self.package = package self.preceding_comments = preceding_comments self.trailing_comments = None self.options = [] def AddTrailingComments(self, comments): self.trailing_comments = comments def AddOption(self, option_type, name, comments): self.options.append((option_type, name, comments)) def ParseProtoFile(proto_file): """Computes the list of Message objects of the option messages in a file.""" line_iter = iter(proto_file) # We ignore the license header and search for the 'package' line. for line in line_iter: line = line.strip() if line.startswith('package'): assert line[-1] == ';' package = line[7:-1].strip() break else: assert '}' not in line message_list = [] while True: # Search for the next options message and capture preceding comments. message_comments = [] for line in line_iter: line = line.strip() if '}' in line: # The preceding comments were for a different message it seems. message_comments = [] elif line.startswith('//'): # We keep comments preceding an options message. comment = line[2:].strip() if not comment.startswith('NEXT ID:'): message_comments.append(comment) elif line.startswith('message') and line.endswith('Options {'): message_name = package + '.' + line[7:-1].strip() break else: # We reached the end of file. break print(" Found '%s'." % message_name) message = Message(message_name, package, message_comments) message_list.append(message) # We capture the contents of this message. option_comments = [] multiline = '' for line in line_iter: line = line.strip() if '}' in line: # We reached the end of this message. message.AddTrailingComments(option_comments) break elif line.startswith('//'): comment = line[2:].strip() if not comment.startswith('NEXT ID:'): option_comments.append(comment) else: assert not line.startswith('required') multiline += ' ' + line if not multiline.endswith(';'): continue assert len(multiline) < 200 option = multiline[:-1].strip().rstrip('0123456789').strip() assert option.endswith('=') if option.startswith('repeated'): option = option[8:] option_type, option_name = option[:-1].strip().split(); print(" Option '%s'." % option_name) multiline = '' message.AddOption(option_type, option_name, option_comments) option_comments = [] return message_list def ParseProtoFilesRecursively(root): """Recursively parses all proto files into a list of Message objects.""" message_list = [] for dirpath, dirnames, filenames in os.walk(root): for name in filenames: if name.endswith('.proto'): path = os.path.join(dirpath, name) print("Found '%s'..." % path) assert not os.path.islink(path) message_list.extend(ParseProtoFile(io.open(path, encoding='UTF-8'))) return message_list class ResolutionError(Exception): """Raised when resolving a message name fails.""" class Resolver(object): def __init__(self, name_set): self.name_set = set(iter(name_set)) def Resolve(self, message_name, package_name): if message_name in ('bool', 'double', 'float', 'int32'): return message_name if message_name.startswith('.'): return message_name[1:] package = package_name.split('.') for levels in range(len(package), -1, -1): candidate = '.'.join(package[0:levels]) + '.' + message_name if candidate in self.name_set: return candidate raise ResolutionError( 'Resolving %s in %s failed.' % (message_name, package_name)) def GenerateDocumentation(output_file, root): """Recursively generates documentation, sorts and writes it.""" message_list = ParseProtoFilesRecursively(root) resolver = Resolver(message.name for message in message_list) output_dict = {} for message in message_list: content = [message.name, '=' * len(message.name), ''] assert message.name not in output_dict output_dict[message.name] = content if message.preceding_comments: content.extend(preceding_comments) content.append('') for option_type, option_name, option_comments in message.options: # TODO(whess): For now we exclude InitialTrajectoryPose from the # documentation. It is documented itself (since it has no Options suffix) # and is not parsed from the Lua files. if option_type in ('InitialTrajectoryPose',): continue content.append( resolver.Resolve(option_type, message.package) + ' ' + option_name) if not option_comments: option_comments.append(NODOC) for comment in option_comments: content.append(' ' + comment) content.append('') if message.trailing_comments: content.extend(message.trailing_comments) content.append('') output = ['\n'.join(doc) for key, doc in sorted(list(output_dict.items()))] print('\n\n'.join(output), file=output_file) def main(): assert not os.path.islink(TARGET) and os.path.isfile(TARGET) assert not os.path.islink(ROOT) and os.path.isdir(ROOT) output_file = io.open(TARGET, mode='w', encoding='UTF-8', newline='\n') output_file.write(PREFIX) GenerateDocumentation(output_file, ROOT) output_file.write(SUFFIX) output_file.close() if __name__ == "__main__": main()