From 07c2bc50757938c710e7787d662587a5fe799b34 Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Sat, 20 Dec 2014 16:21:46 +0100 Subject: [PATCH] doc generation for commandline now extracted into an RST directly. --- .gitignore | 3 +- Makefile | 2 +- bam/cli.py | 48 ++++++++- doc/bam_cli_argparse.py | 181 +++++++++++++++++++++++++++++++++ doc/bam_cli_argparse.rst | 20 ++++ doc/exts/sphinxarg/ext.py | 32 +++++- doc/exts/sphinxarg/parser.py | 3 +- doc/source/conf.py | 13 +-- doc/source/reference/index.rst | 9 -- 9 files changed, 288 insertions(+), 23 deletions(-) create mode 100644 doc/bam_cli_argparse.py create mode 100644 doc/bam_cli_argparse.rst delete mode 100644 doc/source/reference/index.rst diff --git a/.gitignore b/.gitignore index 88d3fb7..532f6a3 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ client/venv/ client/client/config.json webservice/bam/config.py - +# generated +doc/source/reference/index.rst diff --git a/Makefile b/Makefile index ef62a40..3a3ccf7 100644 --- a/Makefile +++ b/Makefile @@ -15,6 +15,6 @@ doc: FORCE @echo "xdg-open doc/build/html/index.html" clean: FORCE - $(MAKE) -C docs clean + $(MAKE) -C doc clean FORCE: diff --git a/bam/cli.py b/bam/cli.py index 8051a19..108d6e4 100755 --- a/bam/cli.py +++ b/bam/cli.py @@ -1128,9 +1128,20 @@ def create_argparse_deps(subparsers): def create_argparse_pack(subparsers): + import argparse subparse = subparsers.add_parser( "pack", aliases=("pk",), help="Pack a blend file and its dependencies into an archive", + description= + """ + This command is used for packing a ``.blend`` file into a ``.zip`` file for redistribution. + + .. code-block:: sh + + # pack a blend with maximum compression for online downloads + bam pack /path/to/scene.blend --output scene.zip --compress + """, + formatter_class=argparse.RawDescriptionHelpFormatter, ) subparse.add_argument( dest="paths", nargs="+", @@ -1155,9 +1166,41 @@ def create_argparse_pack(subparsers): def create_argparse_remap(subparsers): + import argparse + subparse = subparsers.add_parser( "remap", help="Remap blend file paths", + description= + """ + This command is a 3 step process: + + - first run ``bam remap start .`` which stores the current state of your project (recursively). + - then re-arrange the files on the filesystem (rename, relocate). + - finally run ``bam remap finish`` to apply the changes, updating the ``.blend`` files internal paths. + + + .. code-block:: sh + + cd /my/project + + bam remap start . + mv photos textures + mv house_v14_library.blend house_libraray.blend + bam remap finish + + .. note:: + + Remapping creates a file called ``bam_remap.data`` in the current directory. + You can relocate the entire project to a new location but on executing ``finish``, + this file must be accessible from the current directory. + + .. note:: + + This command depends on files unique contents, + take care not to modify the files once remap is started. + """, + formatter_class=argparse.RawDescriptionHelpFormatter, ) subparse_remap_commands = subparse.add_subparsers( @@ -1234,7 +1277,10 @@ def create_argparse(): __doc__ ) - parser = argparse.ArgumentParser(description=usage_text) + parser = argparse.ArgumentParser( + prog="bam", + description=usage_text, + ) subparsers = parser.add_subparsers( title='subcommands', diff --git a/doc/bam_cli_argparse.py b/doc/bam_cli_argparse.py new file mode 100644 index 0000000..2821dcb --- /dev/null +++ b/doc/bam_cli_argparse.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 + +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# +# ***** END GPL LICENCE BLOCK ***** + + +# ------------------ +# Ensure module path +import os +import sys +path = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "exts")) +if path not in sys.path: + sys.path.append(path) +del os, sys, path +# -------- + +# ------------------ +# Ensure module path +import os +import sys +path = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..")) +if path not in sys.path: + sys.path.append(path) +del os, sys, path +# -------- + + +import os +CURRENT_DIR = os.path.normpath(os.path.dirname(__file__)) +INDENT = " " + +import bam.cli + +# start at this level +title_level = 2 + +title_chars = ( + '%', # not from python docs! + '#', + '*', + '=', + '-', + '^', + '"', + "'", # not from python docs! + ) + + +def write_title(title, ch, double=False): + ret = [""] + line = ch * len(title) + assert(len(ch) == 1) + if double: + ret.append(line) + ret.append(title) + ret.append(line) + ret.append("") + return ret + + +def text_unintend(text): + text = text.lstrip("\n") + text_newlines = text.split("\n") + text_indent = len(text_newlines[0]) - len(text_newlines[0].lstrip(" \t")) + return "\n".join([t[text_indent:] for t in text_newlines]) + + +def options_as_rst(data): + ret = [] + ret.extend([ + ".. list-table::", + INDENT + ":widths: 2, 8", + "", + ]) + for c in data: + c_name = c["name"] + if isinstance(c_name, list): + c_name = ", ".join(["``%s``" % w for w in c["name"]]) + c_metavar = c["metavar"] + c_choices = c.get("choices") + + ret.extend([ + INDENT + "* - " + c_name + (" ``<%s>``" % c_metavar if c_metavar else ""), + INDENT + " - " + c["help"], + ]) + if c_choices: + ret.extend([ + "", + INDENT + " Possible choices: " + ", ".join(["``%s``" % w for w in c_choices]) + ]) + + # print(c.keys()) + ret.append("") + return ret + + +def subcommands_as_rst(data, commands, subcommands, level): + # import IPython; IPython.embed() + ret = [] + for c in data: + name = c["name"] + name_abbr = name.split(" ", 1)[0] + if (subcommands is None) or (name_abbr in subcommands): + commands_sub = commands + [name_abbr] + ret.extend(write_title(" ".join(commands_sub[1:]), title_chars[level])) + + # ret.extend([".. program:: " + " ".join(commands_sub), ""]) + + ret.extend([c["help"], ""]) + ret.extend(["::", "", INDENT + c["usage"], ""]) + ret.extend([text_unintend(c.get("description", "")), ""]) + + ls = c.get("args") + if ls: + ret.extend(write_title("Positional arguments:", title_chars[level + 1])) + ret.extend(options_as_rst(ls)) + + ls = c.get("options") + if ls: + ret.extend(write_title("Options:", title_chars[level + 1])) + # import IPython; IPython.embed() + ret.extend(options_as_rst(c["options"])) + + ls = c.get("children") + if ls: + ret.extend(write_title("Subcommands:", title_chars[level + 1])) + ret.extend(subcommands_as_rst(ls, commands_sub, None, level + 2)) + + # ret.extend(["", "----", ""]) + return ret + + +def write_argparse_rst(title_level): + program_root = "bam" + + parser = bam.cli.create_argparse() + + import sphinxarg.parser as sp + data = sp.parse_parser(parser) + + # import pprint + # pprint.pprint(data) + + with open(os.path.join(CURRENT_DIR, 'bam_cli_argparse.rst'), 'r', encoding='utf-8') as f: + main_doc_split = f.read().split("\n") + + for i, l in enumerate(main_doc_split): + l_strip = l.lstrip() + if l_strip.startswith(".. %%"): + # ".. %%(foo, bar)%%" --> ("foo", "bar") + subcommands = tuple([w.strip() for w in l_strip[3:].strip("%()").split(",")]) + l = subcommands_as_rst(data["children"], [program_root], subcommands, title_level) + main_doc_split[i] = "\n".join(l) + + return main_doc_split + + +def main(): + os.makedirs(os.path.join(CURRENT_DIR, "source", "reference"), exist_ok=True) + with open(os.path.join(CURRENT_DIR, "source", "reference", "index.rst"), 'w') as f: + ret = write_argparse_rst(3) + f.write("\n".join(ret)) + + +if __name__ == "__main__": + main() diff --git a/doc/bam_cli_argparse.rst b/doc/bam_cli_argparse.rst new file mode 100644 index 0000000..cfa7525 --- /dev/null +++ b/doc/bam_cli_argparse.rst @@ -0,0 +1,20 @@ +**************** +Bam Command Line +**************** + + +Standalone Subcommands +====================== + +These commands can run standalone. + +.. %%(pack,remap,deps)%% + + +Project Subcommands +=================== + +These commands relate to projects which use a BAM server. + +.. %%(init,create,checkout,update,commit,revert,status,list,deps)%% + diff --git a/doc/exts/sphinxarg/ext.py b/doc/exts/sphinxarg/ext.py index 464691b..e4dcc3c 100644 --- a/doc/exts/sphinxarg/ext.py +++ b/doc/exts/sphinxarg/ext.py @@ -9,6 +9,29 @@ from sphinx.util.nodes import nested_parse_with_titles from sphinxarg.parser import parse_parser, parser_navigate +def text_from_rst(text, is_rst=False): + if not text: + return [] + if not is_rst: + return [nodes.paragraph(text=text)] + else: + text = text.lstrip("\n") + text_newlines = text.split("\n") + text_indent = len(text_newlines[0]) - len(text_newlines[0].lstrip(" \t")) + text = "\n".join([t[text_indent:] for t in text_newlines]) + del text_newlines, text_indent + + import docutils.parsers.rst + parser = docutils.parsers.rst.Parser() + filepath = None + settings = docutils.frontend.OptionParser( + components=(docutils.parsers.rst.Parser,) + ).get_default_values() + document = docutils.utils.new_document(filepath, settings) + parser.parse(text, document) + return document.children + + def map_nested_definitions(nested_content): if nested_content is None: raise Exception('Nested content should be iterable, not null') @@ -91,7 +114,7 @@ def print_opt_list(data, nested_content): return nodes.option_list('', *items) if items else None -def print_command_args_and_opts(arg_list, opt_list, sub_list=None): +def print_command_args_and_opts(arg_list, opt_list, epilog_list=None, sub_list=None): items = [] if arg_list: items.append(nodes.definition_list_item( @@ -101,6 +124,8 @@ def print_command_args_and_opts(arg_list, opt_list, sub_list=None): items.append(nodes.definition_list_item( '', nodes.term(text='Options:'), nodes.definition('', opt_list))) + if epilog_list: + items.extend(epilog_list) if sub_list and len(sub_list): items.append(nodes.definition_list_item( '', nodes.term(text='Sub-commands:'), @@ -137,7 +162,9 @@ def print_subcommand_list(data, nested_content): my_def.append(print_command_args_and_opts( print_arg_list(child, nested_content), print_opt_list(child, nested_content), - print_subcommand_list(child, nested_content) + text_from_rst(child.get('description', ""), is_rst=True), + print_subcommand_list(child, nested_content), + )) items.append( nodes.definition_list_item( @@ -345,6 +372,7 @@ class ArgParseDirective(Directive): items.append(print_command_args_and_opts( print_arg_list(result, nested_content), print_opt_list(result, nested_content), + None, print_subcommand_list(result, nested_content) )) if 'epilog' in result: diff --git a/doc/exts/sphinxarg/parser.py b/doc/exts/sphinxarg/parser.py index 75cb8a5..2d6b2b3 100644 --- a/doc/exts/sphinxarg/parser.py +++ b/doc/exts/sphinxarg/parser.py @@ -116,7 +116,8 @@ def parse_parser(parser, data=None, **kwargs): option = { 'name': action.option_strings, 'default': action.default if show_defaults else '==SUPPRESS==', - 'help': action.help or '' + 'help': action.help or '', + 'metavar': action.metavar } if action.choices: option['choices'] = action.choices diff --git a/doc/source/conf.py b/doc/source/conf.py index 3437cc4..7207f06 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -33,14 +33,11 @@ extensions = [ 'sphinx.ext.todo', ] -if 1: - extensions += ['sphinxarg.ext'] - sys.path.extend([ - # to import 'bam.py' - os.path.join(os.path.dirname(__file__), "..", ".."), - # to access the 'sphinxarg' extension - os.path.abspath(os.path.join("..", "exts")) - ]) +# create bam_cli_argparse.rst +import os +import sys +sys.path.append(os.path.join(os.path.dirname(__file__), "..")) +__import__("bam_cli_argparse").main() # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst deleted file mode 100644 index a4cd7ef..0000000 --- a/doc/source/reference/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -Client Reference -################ - -Here is a reference of all the BAM cli commands. - -.. argparse:: - :module: bam.cli - :func: create_argparse - :prog: bam