update_configuration_doc.py
Go to the documentation of this file.
00001 #!/usr/bin/python3
00002 # -*- coding: utf-8 -*-
00003 
00004 # Copyright 2016 The Cartographer Authors
00005 #
00006 # Licensed under the Apache License, Version 2.0 (the "License");
00007 # you may not use this file except in compliance with the License.
00008 # You may obtain a copy of the License at
00009 #
00010 #      http://www.apache.org/licenses/LICENSE-2.0
00011 #
00012 # Unless required by applicable law or agreed to in writing, software
00013 # distributed under the License is distributed on an "AS IS" BASIS,
00014 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
00015 # See the License for the specific language governing permissions and
00016 # limitations under the License.
00017 """A dumb configuration.rst generator that relies on source comments."""
00018 
00019 import io
00020 import os
00021 
00022 TARGET = 'docs/source/configuration.rst'
00023 ROOT = 'cartographer'
00024 PREFIX = """.. Copyright 2016 The Cartographer Authors
00025 
00026 .. Licensed under the Apache License, Version 2.0 (the "License");
00027    you may not use this file except in compliance with the License.
00028    You may obtain a copy of the License at
00029 
00030 ..      http://www.apache.org/licenses/LICENSE-2.0
00031 
00032 .. Unless required by applicable law or agreed to in writing, software
00033    distributed under the License is distributed on an "AS IS" BASIS,
00034    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
00035    See the License for the specific language governing permissions and
00036    limitations under the License.
00037 
00038 =============
00039 Configuration
00040 =============
00041 
00042 .. DO NOT EDIT! This documentation is AUTOGENERATED, please edit .proto files as
00043 .. needed and run scripts/update_configuration_doc.py.
00044 
00045 """
00046 SUFFIX = """
00047 """
00048 NODOC = 'Not yet documented.'
00049 
00050 
00051 class Message(object):
00052   def __init__(self, name, package, preceding_comments):
00053     self.name = name
00054     self.package = package
00055     self.preceding_comments = preceding_comments
00056     self.trailing_comments = None
00057     self.options = []
00058 
00059   def AddTrailingComments(self, comments):
00060     self.trailing_comments = comments
00061 
00062   def AddOption(self, option_type, name, comments):
00063     self.options.append((option_type, name, comments))
00064 
00065 
00066 def ParseProtoFile(proto_file):
00067   """Computes the list of Message objects of the option messages in a file."""
00068   line_iter = iter(proto_file)
00069 
00070   # We ignore the license header and search for the 'package' line.
00071   for line in line_iter:
00072     line = line.strip()
00073     if line.startswith('package'):
00074       assert line[-1] == ';'
00075       package = line[7:-1].strip()
00076       break
00077     else:
00078       assert '}' not in line
00079 
00080   message_list = []
00081   while True:
00082     # Search for the next options message and capture preceding comments.
00083     message_comments = []
00084     for line in line_iter:
00085       line = line.strip()
00086       if '}' in line:
00087         # The preceding comments were for a different message it seems.
00088         message_comments = []
00089       elif line.startswith('//'):
00090         # We keep comments preceding an options message.
00091         comment = line[2:].strip()
00092         if not comment.startswith('NEXT ID:'):
00093           message_comments.append(comment)
00094       elif line.startswith('message') and line.endswith('Options {'):
00095         message_name = package + '.' + line[7:-1].strip()
00096         break
00097     else:
00098       # We reached the end of file.
00099       break
00100     print(" Found '%s'." % message_name)
00101     message = Message(message_name, package, message_comments)
00102     message_list.append(message)
00103 
00104     # We capture the contents of this message.
00105     option_comments = []
00106     multiline = ''
00107     for line in line_iter:
00108       line = line.strip()
00109       if '}' in line:
00110         # We reached the end of this message.
00111         message.AddTrailingComments(option_comments)
00112         break
00113       elif line.startswith('//'):
00114         comment = line[2:].strip()
00115         if not comment.startswith('NEXT ID:'):
00116           option_comments.append(comment)
00117       else:
00118         assert not line.startswith('required')
00119         multiline += ' ' + line
00120         if not multiline.endswith(';'):
00121           continue
00122         assert len(multiline) < 200
00123         option = multiline[:-1].strip().rstrip('0123456789').strip()
00124         assert option.endswith('=')
00125         if option.startswith('repeated'):
00126           option = option[8:]
00127         option_type, option_name = option[:-1].strip().split();
00128         print("  Option '%s'." % option_name)
00129         multiline = ''
00130         message.AddOption(option_type, option_name, option_comments)
00131         option_comments = []
00132 
00133   return message_list
00134 
00135 
00136 def ParseProtoFilesRecursively(root):
00137   """Recursively parses all proto files into a list of Message objects."""
00138   message_list = []
00139   for dirpath, dirnames, filenames in os.walk(root):
00140     for name in filenames:
00141       if name.endswith('.proto'):
00142         path = os.path.join(dirpath, name)
00143         print("Found '%s'..." % path)
00144         assert not os.path.islink(path)
00145         message_list.extend(ParseProtoFile(io.open(path, encoding='UTF-8')))
00146   return message_list
00147 
00148 
00149 class ResolutionError(Exception):
00150   """Raised when resolving a message name fails."""
00151 
00152 
00153 class Resolver(object):
00154   def __init__(self, name_set):
00155     self.name_set = set(iter(name_set))
00156 
00157   def Resolve(self, message_name, package_name):
00158     if message_name in ('bool', 'double', 'float', 'int32'):
00159       return message_name
00160     if message_name.startswith('.'):
00161       return message_name[1:]
00162     package = package_name.split('.')
00163     for levels in range(len(package), -1, -1):
00164       candidate = '.'.join(package[0:levels]) + '.' + message_name
00165       if candidate in self.name_set:
00166         return candidate
00167     raise ResolutionError(
00168         'Resolving %s in %s failed.' % (message_name, package_name))
00169 
00170 
00171 def GenerateDocumentation(output_file, root):
00172   """Recursively generates documentation, sorts and writes it."""
00173   message_list = ParseProtoFilesRecursively(root)
00174   resolver = Resolver(message.name for message in message_list)
00175 
00176   output_dict = {}
00177   for message in message_list:
00178     content = [message.name, '=' * len(message.name), '']
00179     assert message.name not in output_dict
00180     output_dict[message.name] = content
00181     if message.preceding_comments:
00182       content.extend(preceding_comments)
00183       content.append('')
00184     for option_type, option_name, option_comments in message.options:
00185       # TODO(whess): For now we exclude InitialTrajectoryPose from the
00186       # documentation. It is documented itself (since it has no Options suffix)
00187       # and is not parsed from the Lua files.
00188       if option_type in ('InitialTrajectoryPose',):
00189         continue
00190       content.append(
00191           resolver.Resolve(option_type, message.package) + ' ' + option_name)
00192       if not option_comments:
00193         option_comments.append(NODOC)
00194       for comment in option_comments:
00195         content.append('  ' + comment)
00196       content.append('')
00197     if message.trailing_comments:
00198       content.extend(message.trailing_comments)
00199       content.append('')
00200 
00201   output = ['\n'.join(doc) for key, doc in sorted(list(output_dict.items()))]
00202   print('\n\n'.join(output), file=output_file)
00203 
00204 
00205 def main():
00206   assert not os.path.islink(TARGET) and os.path.isfile(TARGET)
00207   assert not os.path.islink(ROOT) and os.path.isdir(ROOT)
00208   output_file = io.open(TARGET, mode='w', encoding='UTF-8', newline='\n')
00209   output_file.write(PREFIX)
00210   GenerateDocumentation(output_file, ROOT)
00211   output_file.write(SUFFIX)
00212   output_file.close()
00213 
00214 
00215 if __name__ == "__main__":
00216   main()


cartographer
Author(s): The Cartographer Authors
autogenerated on Thu May 9 2019 02:27:36