generate_launches_md.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
00002 # Copyright (c) 2015 by Wayne C. Gramlich.  All rights reserved.
00003 
00004 # This program sweeps through the `ubiquity_launches` directory
00005 # and scans executables in the `bin` sub-directory and any launch
00006 # file anywhere under `ubiquity_launches`.
00007 #
00008 # Two operations are performed:
00009 #
00010 # * It scrapes executables and launch files to write documentation
00011 #   into the `launches.md` file.
00012 #
00013 # * Using the binary executables, it recursively visits all used
00014 #   launch files.  If a launch file is not used, it is flagged as
00015 #   unused and it should be deleted from the repository.
00016 
00017 # Import some libraries:
00018 import glob
00019 import os
00020 import re
00021 import sys
00022 import xml.etree.ElementTree as ET
00023 
00024 def main():
00025     """ This is the main program. """
00026 
00027     # List each supported robot base in *robot_bases*:
00028     robot_bases = ["loki", "magni"]
00029 
00030     # Search for launch file names:
00031     launch_file_names = []
00032     for root, directory_names, file_names in os.walk("."):
00033         #print("root={0}, directory_names={1}, file_names={2}".
00034         #  format(root, directory_names, file_names))
00035 
00036         for file_name in file_names:
00037             full_file_name = os.path.join(root, file_name)
00038             if file_name.endswith(".launch.xml") or \
00039               file_name.endswith(".launch"):
00040                 launch_file_names.append(full_file_name)
00041                 #print("launch_file_name={0}".format(launch_file_name))
00042 
00043     # Search for executable files in `bin` directory:
00044     executable_file_names = []
00045     for root, directory_names, file_names in os.walk("./bin"):
00046         for file_name in file_names:
00047             full_file_name = os.path.join(root, file_name)
00048             if os.access(full_file_name, os.X_OK) and \
00049               os.path.isfile(full_file_name) and \
00050               not full_file_name.endswith("~"):
00051                 executable_file_names.append(full_file_name)
00052                 #print("executable: {0}".format(full_file_name))
00053 
00054     # Make sure we do everything in sorted order:
00055     launch_file_names.sort()
00056     executable_file_names.sort()
00057 
00058     # Now process each *execubale_file_name*:
00059     executable_files = []
00060     for executable_file_name in executable_file_names:
00061         executable_file = Executable_File.file_parse(executable_file_name)
00062         executable_files.append(executable_file)
00063 
00064     # Now process each *launch_file_name*:
00065     launch_files = []
00066     for launch_file_name in launch_file_names:
00067         launch_file = Launch_File.file_parse(launch_file_name, robot_bases)
00068         launch_files.append(launch_file)
00069 
00070     # Open the markdown file:
00071     md_file = open("launches.md", "wa")
00072     md_file.write("# Ubiquity Launches\n\n")
00073 
00074     # Write out each executable file summary:
00075     md_file.write("The following executables are available in `bin`:\n\n")
00076     for executable_file in executable_files:
00077         executable_file.summary_write(md_file)
00078 
00079     # Write out each launch file summary:
00080     md_file.write("The following launch file directories are available:\n\n")
00081     for launch_file in launch_files:
00082         launch_file.summary_write(md_file)
00083 
00084     # Write out each executable file section:
00085     md_file.write("## Executables\n\n")
00086     for executable_file in executable_files:
00087         executable_file.section_write(md_file)
00088 
00089     # Write out each launch file section:
00090     md_file.write("## Launch File Directories\n\n")
00091     for launch_file in launch_files:
00092         launch_file.section_write(md_file)
00093 
00094     # Close the markdown file:
00095     md_file.close()
00096 
00097     # Create *launch_files_table*:
00098     launch_files_table = {}
00099     for launch_file in launch_files:
00100         launch_files_table[launch_file.name] = launch_file
00101 
00102     # Recursively visit each *executable_file*:
00103     for executable_file in executable_files:
00104         executable_file.visit(launch_files_table)
00105 
00106     # Print out each *launch_file* that was not visited:
00107     for launch_file in launch_files:
00108         if not launch_file.visited:
00109             print("Launch File: '{0}' is unused".format(launch_file.name))
00110 
00111 def macro_replace(match, macros):
00112     """ Replace "$(arg VALUE)" with the value from *macros* and return it.
00113     """
00114 
00115     # Verify arguments:
00116     #assert isinstance(match, re.MatchObject)
00117     assert isinstance(macros, dict)
00118 
00119     # We ASSUME that the format of the string is "$(COMMAND VALUE)".
00120     # First split the match string at the space:
00121     splits = match.group().split()
00122 
00123     # Grab the *command* and *value*:
00124     command = splits[0][2:]
00125     value = splits[1][:-1]
00126 
00127     # Now only substitute argument values:
00128     result = ""
00129     #print("macros=", macros)
00130     if command == "arg" and value in macros:
00131         # We have an argument value:
00132         result = macros[value]
00133     else:
00134         # Leave everything else more or less alone:
00135         result = "[{0}:{1}]".format(command, value)
00136     return result
00137 
00138 class Executable_File:
00139     """ *Executable_File* is a class that represents an executable file.
00140     """
00141 
00142     def __init__(self, name, summary, overview_lines, launch_base_name):
00143         """ *Executable_File*: Initialize the *Executable_File* object
00144             (i.e. *self*) with *name*, *summary*, *overview_lines*, and
00145             *launch_base_name*.
00146         """
00147 
00148         # Verify argument types:
00149         assert isinstance(name, str)
00150         assert isinstance(summary, str)
00151         assert isinstance(overview_lines, list)
00152         assert launch_base_name == None or isinstance(launch_base_name, str)
00153 
00154         # Load up *self*:
00155         self.name = name                        # Executable base name
00156         self.summary = summary                  # One line summary
00157         self.overview_lines = overview_lines    # Multi-line overview
00158         self.launch_base_name = launch_base_name # Root launch file that is used
00159 
00160     @staticmethod
00161     def file_parse(full_file_name):
00162         """ *Executable_File*: Parse *full_file_name* and scrape out
00163             the usable documentation.
00164         """
00165 
00166         # Verify argument types:
00167         assert isinstance(full_file_name, str)
00168         splits = full_file_name.split('/')
00169         executable_name = splits[2]
00170         
00171         # Open *full_file_name* and slurp it into *lines*:
00172         in_file = open(full_file_name, "ra")
00173         lines = in_file.readlines()
00174         in_file.close()
00175 
00176         # Sweep though *lines* and extract *summary*, *overview_lines*,
00177         # and *launch_base_name*.  Leave *launch_base_name* as *None*
00178         # if we do not find a `roslaunch ...` comman in the executable:
00179         launch_base_name = None
00180         summary = ""
00181         overview_lines = []
00182         for line in lines:
00183             # Comments that we scan start with `##`:
00184             if line.startswith("##"):
00185                 # Strip off the `##`:
00186                 comment_line = line[2:].strip()
00187 
00188                 # `##Sumarry: ...` is the one line *summary*:
00189                 if comment_line.startswith("Summary:"):
00190                     # We have a one line *summary*:
00191                     summary = comment_line[8:].strip()
00192                 elif comment_line.startswith("Overview:"):
00193                     # Ignore the `##Overview:` line:
00194                     pass
00195                 else:
00196                     # Everything else that starts with `##` is an overview line:
00197                     overview_lines.append(comment_line)
00198 
00199             # Deal with `roslaunch ...` command:
00200             if line.startswith("roslaunch"):
00201                 # Grab *launch_file_name*:
00202                 splits = line.split(' ')
00203                 launch_file_name = splits[2]
00204                 #print("lauch_file_name={0}".format(launch_file_name))
00205 
00206                 # Grab the *launch_base_name*:
00207                 splits = launch_file_name.split('.')
00208                 launch_base_name = splits[0]
00209 
00210         # Construct and return the *Executable_File* object:
00211         return Executable_File(
00212           executable_name, summary, overview_lines, launch_base_name)
00213 
00214     def section_write(self, md_file):
00215         """ *Executable_File*: Write the section for the *Executable_File*
00216             object (i.e. *self*) out to *md_file*.
00217         """
00218 
00219         # Verify argument types:
00220         assert isinstance(md_file, file)
00221 
00222         # Grab some values from *self*:
00223         name = self.name
00224         overview_lines = self.overview_lines
00225 
00226         # Write out the executable section:
00227         md_file.write("### `{0}` Executable:\n\n".format(name))
00228         for overview_line in overview_lines:
00229             md_file.write("{0}\n".format(overview_line))
00230         md_file.write("\n")
00231 
00232     def summary_write(self, md_file):
00233         """ *Executable_File*: Write the summary for the *Executable_File*
00234             object (i.e. *self*) out to *md_file*.
00235         """
00236 
00237         # Verify argument types:
00238         assert isinstance(md_file, file)
00239 
00240         # Grab some values from *self*:
00241         name = self.name
00242         summary = self.summary
00243 
00244         # Write out the *summary*:
00245         md_file.write("* `{0}`: {1}\n\n".format(name, summary))
00246 
00247     def visit(self, launch_files_table):
00248         """ *Executable_File*: Recursively visit the *Launch_File* object
00249             referenced by the *Executable_File* object (i.e. *self*).
00250         """
00251 
00252         # Verify argument types:
00253         assert isinstance(launch_files_table, dict)
00254 
00255         # Grab *launch_base_name* which can be either *None* or a string:
00256         launch_base_name = self.launch_base_name
00257 
00258         # Dispatch on *launch_base_name*:
00259         if launch_base_name == None:
00260             # Ignore executable that do not have `roslaunch ...`:
00261             pass
00262         elif launch_base_name in launch_files_table:
00263             # *launch_base_name* exists and should be recursivly visited:
00264             launch_file = launch_files_table[launch_base_name]
00265             launch_file.visit(launch_files_table)
00266         else:
00267             # Print out an error message:
00268             print("Executable '{0}' can not find launch file directory '{1}'".
00269               format(self.name, launch_base_name))
00270 
00271 class Launch_File:
00272     def __init__(self, name,
00273       argument_comments, requireds, optionals, macros, includes, conditionals):
00274         """ *Launch_File*: Initialize the *Launch_File* object (i.e. *self*)
00275             with *name*, *argument_comments*, *requireds*, *optionals*,
00276             *macros*, *includes*, and *conditionals*.
00277         """
00278 
00279         # Verify argument types:
00280         assert isinstance(name, str)
00281         assert isinstance(argument_comments, dict)
00282         assert isinstance(requireds, list)
00283         assert isinstance(optionals, list)
00284         assert isinstance(macros, dict)
00285         assert isinstance(includes, list)
00286         assert isinstance(conditionals, list)
00287 
00288         # Load up *self*:
00289         self.name = name                # Launch file base name
00290         self.argument_comments = argument_comments # All `<!-- ... -->` comments
00291         self.requireds = requireds      # Required arguments
00292         self.optionals = optionals      # Option arguments
00293         self.macros = macros            # Convenience arguments (i.e. macros)
00294         self.includes = includes        # Included launch files
00295         self.conditionals = conditionals # Launch files that use robot_base arg.
00296         self.visited = False            # Flag for mark/sweep recursive visit
00297 
00298     @staticmethod
00299     def file_parse(full_file_name, robot_bases):
00300         """ *Launch_File*: Process one launch file.
00301         """
00302 
00303         # Verify argument types:
00304         assert isinstance(full_file_name, str)
00305         assert isinstance(robot_bases, list)
00306 
00307         # Do some file/directory stuff:
00308         root_path = full_file_name.split('/')
00309         assert len(root_path) >= 1, \
00310           "Root path '{0}' is too short".format(root_path)
00311         launch_file_name = root_path[1]
00312 
00313         # Read in *full_file_name*:
00314         xml_file = open(full_file_name, "ra")
00315         xml_text = xml_file.read()
00316         xml_file.close()
00317 
00318         # Find all comments in *xml_text* (*re.DOTALL* allows regular
00319         # expressons to span multiple lines):
00320         comments = re.findall("<!--.*?-->", xml_text, re.DOTALL)
00321 
00322         # Now we war interested in comments of the form "<!--NAME:...-->"
00323         # where "NAME" is an alphanumeric identifier.  When we find such
00324         # a comment, we stuff the comment contents into *argument_comments*
00325         # keyed by "NAME":
00326         argument_comments = {}
00327         for comment in comments:
00328             # Strip the "<!--" and "-->" off of *comment*:
00329             comment = comment[4:-3]
00330             #print("    comment1: '{0}'".format(comment))
00331 
00332             if not comment.startswith(' '):
00333                 # Grab *argument_name*:
00334                 colon_index = comment.find(':')
00335                 if colon_index >= 0:
00336                     argument_name = comment[:colon_index]
00337                     comment = comment[colon_index + 1:]
00338                     #print("    comment2: '{0}'".format(comment))
00339 
00340                     # Now reformat multiple lines so that they all are indented
00341                     # by *prefix*:
00342                     prefix = "  "
00343                     if argument_name == "Overview":
00344                         prefix = ""
00345                     lines = comment.split('\n')
00346                     for index in range(len(lines)):
00347                         lines[index] = prefix + lines[index].strip()
00348                         #print("line='{0}'".format(line))
00349                     comment = '\n'.join(lines)
00350                     #print("    comment3: '{0}'".format(comment))
00351 
00352                     # Stuff the resulting *comment* into *argument_names*:
00353                     argument_comments[argument_name] = comment
00354                     #print("    comment4: '{0}'".format(comment))
00355 
00356         # Parse the XML:
00357         #print("{0}:".format(full_file_name))
00358         try:
00359             tree = ET.fromstring(xml_text)
00360         except ET.ParseError as error:
00361             position = error.position
00362             line = position[0]
00363             column = position[1]
00364             print("XML Error in file '{0}' at line:{1} column:{2}".
00365               format(full_file_name, line, column))
00366             sys.exit(1)
00367         requireds = []
00368         optionals = []
00369 
00370         # Create *includes* and *conditionals* list, in addition to,
00371         # the *macros* table:
00372         macros = {}
00373         includes = []
00374         conditionals = []
00375 
00376         # Visit all of the tags under the <Launch> tag:
00377         for child in tree:
00378             # We only care about <Arg ...> tags:
00379             child_tag = child.tag
00380             attributes = child.attrib
00381             if child_tag == "arg":
00382                 # We have <arg ...>:
00383                 name = attributes["name"]
00384                 if "default" in attributes:
00385                     # We have an optional argument:
00386                     optionals.append(child)
00387                 elif "value" in attributes:
00388                     # We have a convenience argument (i.e. macro):
00389                     value = attributes["value"]
00390                     macros[name] = value
00391                     #print("macros['{0}'] = '{1}'".format(name, value))
00392                 else:
00393                     # We have a required argument:
00394                     requireds.append(child)
00395             elif child_tag == "include":
00396                 # We have <include ...>:
00397                 if "file" in attributes:
00398                     # Repeatably perform macro substitution on *file_name*:
00399                     file_name = attributes["file"]
00400                     file_name_previous = ""
00401                     while file_name_previous != file_name:
00402                         file_name_previous = file_name
00403                         file_name = re.sub(r"\$\(arg .*?\)",
00404                           lambda match: macro_replace(match, macros), file_name)
00405                         #print("'{0}'=>'{1}'".
00406                         #  format(file_name_previous, file_name))
00407 
00408                     # Determine if we have a `robot_base` argument to deal with:
00409                     if file_name.find("[arg:robot_base]") >= 0:
00410                         # We have a `robot_base` argument:
00411 
00412                         #print("robot_base <include...>: {0}".
00413                         #  format(file_after))
00414 
00415                         # Search for each *robot_base*:
00416                         for robot_base in robot_bases:
00417                             # Subsitute in *robot_base*:
00418                             #print("robot_base='{0}'".format(robot_base))
00419                             #print("file_name before='{0}'".format(file_name))
00420                             conditional_file_name = \
00421                               re.sub(r"(\[arg:robot_base\])",
00422                               robot_base, file_name)
00423                             #print("file_name after='{0}'".
00424                             #  format(conditional_file_name))
00425 
00426                             # Extract the *conditional_base_name*:
00427                             splits = conditional_file_name.split('/')
00428                             include_xml_name = splits[-1]
00429                             splits = include_xml_name.split('.')
00430                             conditional_base_name = splits[0]
00431                             #print("condtional_base_name='{0}:{1}'\n".
00432                             #  format(conditional_base_name, robot_base))
00433 
00434                             # Keep track of *conditional_base_name*
00435                             # in *conditionals* list:
00436                             conditionals.append(conditional_base_name)
00437                     else:
00438                         # Now grab the *include_base_name*:
00439                         splits = file_name.split('/')
00440                         include_xml_name = splits[-1]
00441                         splits = include_xml_name.split('.')
00442                         include_base_name = splits[0]
00443                         #print("include_base_name='{0}'".
00444                         #  format(include_base_name))
00445 
00446                         # Collect *include_base_name* in *includes* list:
00447                         includes.append(include_base_name)
00448 
00449         # Construct and return *launch_file*:
00450         launch_file = Launch_File(launch_file_name, argument_comments,
00451           requireds, optionals, macros, includes, conditionals)
00452         return launch_file
00453  
00454     def section_write(self, md_file):
00455         """ *Launch_File*: Write out the section for the *Launch_File* object
00456             (i.e. *self*) to *md_file*.
00457         """
00458                 
00459         # Verify argument types:
00460         assert isinstance(md_file, file)
00461 
00462         # Grab some values from *self*:
00463         name = self.name
00464         argument_comments = self.argument_comments
00465         requireds = self.requireds
00466         optionals = self.optionals
00467         macros = self.macros
00468 
00469         # Output the section heading:
00470         md_file.write("### `{0}` Launch File Directory\n\n".format(name))
00471 
00472         # Output the overview comment:
00473         if "Overview" in argument_comments:
00474             #print("Overview:{0}".format(argument_comments["Overview"]))
00475             overview_comments = argument_comments["Overview"]
00476             md_file.write("{0}\n\n".format(overview_comments))
00477 
00478         # Output each *requried* and *optional* argument:
00479         arguments_count = len(requireds) + len(optionals)
00480         if arguments_count == 0:
00481             md_file.write("This launch file has no arguments.\n\n")
00482         elif arguments_count == 1:
00483             md_file.write("This launch file has the following argument:\n\n")
00484         else:
00485             md_file.write("This launch file has the following arguments:\n\n")
00486 
00487         # Output each *required* argument:
00488         for required in requireds:
00489             attributes = required.attrib
00490             name = attributes["name"]
00491             md_file.write("* {0} (Required):\n".format(name))
00492             if name in argument_comments:
00493                 md_file.write("{0}\n".format(argument_comments[name]))
00494             md_file.write("\n")
00495             
00496         # Output each *optional* argument:
00497         for optional in optionals:
00498             attributes = optional.attrib
00499             name = attributes["name"]
00500             default = attributes["default"]
00501             md_file.write("* {0} (Optional, default: '{1}'):\n".
00502               format(name, default))
00503             if name in argument_comments:
00504                 md_file.write("{0}\n".format(argument_comments[name]))
00505             md_file.write("\n")
00506 
00507     def show(self):
00508         """ *Launch_File*: Print short contents of the *Launch_File* object
00509             (i.e. *self*).
00510         """
00511 
00512         # Show *self*:
00513         print("Name: {0}".format(self.name))
00514         for include in self.includes:
00515             print("  Include: {0}".format(include))
00516 
00517     def summary_write(self, md_file):
00518         """ *Launch_File*: Write out the summary item for the *Launch_File*
00519             object (i.e. *self*) to *md_file*:
00520         """
00521 
00522         # Verify argument types:
00523         assert isinstance(md_file, file)
00524 
00525         # Grab some values from *self*:
00526         name = self.name
00527         argument_comments = self.argument_comments
00528 
00529         # Write out an item:
00530         if "Summary" in argument_comments:
00531             md_file.write("* `{0}`:\n{1}\n".
00532               format(name, argument_comments["Summary"]))
00533         else:
00534             md_file.write("* {0}: (No Summary Available)\n".format(name))
00535         md_file.write("\n")
00536 
00537     def visit(self, launch_files_table):
00538         """ *Launch_File*: Recursively visit the *Launch_File* object
00539             (i.e. *self*) using *launch_files_table*.
00540         """
00541         
00542         # Verify argument types:
00543         assert isinstance(launch_files_table, dict)
00544 
00545         # Only work on launch files that have not been *visited*:
00546         if not self.visited:
00547             self.visited = True
00548 
00549             # Recursively visit each mandatory *include* launch file:
00550             for include in self.includes:
00551                 if include in launch_files_table:
00552                     child = launch_files_table[include]
00553                     child.visit(launch_files_table)
00554                 else:
00555                     print("Launch file '{0}' references non-existant '{1}'".
00556                       format(self.name, include))
00557 
00558             # Also visit each *conditional* launch file (i.e. it has
00559             # `robot_base` argument in the name):
00560             for conditional in self.conditionals:
00561                 #print("conditional:{0}".format(conditional))
00562                 if conditional in launch_files_table:
00563                     child = launch_files_table[conditional]
00564                     child.visit(launch_files_table)
00565                 else:
00566                     print("Launch file '{0}' uses non-existant base file '{1}'".
00567                       format(self.name, conditional))
00568         
00569 if __name__ == "__main__":
00570     main()
00571 


ubiquity_launches
Author(s): Wayne Gramlich
autogenerated on Thu Jun 6 2019 18:36:40