-
-
Notifications
You must be signed in to change notification settings - Fork 116
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generate commands documentation #649
Comments
@coccoinomane thank you for the kind words, and for your sponsorship (many thanks). I am happy Cement has been of value to you. I like the idea... kind of like a complete Some thoughts: Without diving deep into the most complex piece of the framework (controllers), this can obviously be accomplished manually by creating a To make this a framework feature, I'd begin inspecting From within the
The above assumes you are using ArgparseController (default). The
With the above, it would be relatively trivial to iterate over controllers/parsers/etc in a template and output the above attributes. I think the biggest challenge will be logical ordering so that it reads "top down", especially with regards to controller stacking. Happy to help, and could try to set aside some time to dig in on this... I will say, out of all of Cement code base, controllers are a bit of deep dark black magic. |
Thank you so much for your suggestions @derks , and sorry for the late reply. I would like to make this a framework feature. I will try to implement it in the coming weeks, will keep you updated on my progress :-) Thanks again, and have a nice weekend! |
Did this feature ever get started? Seems pretty useful. |
Hi @brianmezini! I did not implement the feature on my side. I ended up copying and pasting from the Cheers, |
@coccoinomane Lol a man does what a man gotta do. I found the need for this feature at the end of my project, obviously to document the app so I ended up implementing a docs command that generates markdown formatted documentation from a Jinja2 template with the hints that @derks provided. Perhaps in the near future I could take a swing at implementing that in the cement cli. |
@brianmezini Wow, that seems pretty cool 💪 Why don't you create a draft pull request with the code? No problem if it does not work. It can be a starting point for the community to implement the feature in Cement. |
@coccoinomane that sounds like a solid next step. I have to polish my code a little bit first 😅 |
That sounds great, @brianmezini! Looking forward to help implementing it when the draft PR is out. |
@coccoinomane @derks so the way I implemented the documentation in my app is by creating a |
Command code: (pretty sure it can be done much better) @ex(
help="generate documentation in Markdown format",
arguments=[
(
["docs_file"],
{
"help": "File to generate documentation in. Defaults to DOCS.md.",
"nargs": "?",
"default": "DOCS.md"
}
)
]
)
def docs(self):
docs_file = self.app.pargs.docs_file
controllers: dict[str, Controller] = self._controllers_map
cmds = {}
for name, controller in controllers.items():
if controller._meta.label == "base":
continue
if controller._meta.stacked_type == "nested":
if controller._meta.stacked_on == "base":
cmds[controller._meta.label] = {"help": controller._meta.help}
for cmd in controller._collect_commands():
#print(cmd)
cmds[controller._meta.label][cmd["label"]] = {"help": cmd['parser_options']['help']}
cmds[controller._meta.label][cmd["label"]]["arguments"] = {"positional": [], "flags": []}
for arg in cmd["arguments"]:
# arg = (['-l', '--long-flag'], {'help': ""})
if arg[0][0].startswith("-"):
cmds[controller._meta.label][cmd["label"]]["arguments"]["flags"].append(
{
"name": ' | '.join(arg[0]),
"help": arg[1]['help']
}
)
else:
cmds[controller._meta.label][cmd["label"]]["arguments"]["positional"].append(
{
"name": ' | '.join(arg[0]),
"help": arg[1]['help']
}
)
else:
parent_controller = controllers[controller._meta.stacked_on]
if parent_controller._meta.label in cmds.keys():
cmds[parent_controller._meta.label][controller._meta.label] = {"help": controller._meta.help}
else:
cmds[parent_controller._meta.label] = {
"help": parent_controller._meta.help,
controller._meta.label: {
"help": controller._meta.help
}
}
for cmd in controller._collect_commands():
#print(cmd)
cmds[parent_controller._meta.label][controller._meta.label][cmd["label"]] = {"help": cmd['parser_options']['help']}
cmds[parent_controller._meta.label][controller._meta.label][cmd["label"]]["arguments"] = {"positional": [], "flags": []}
for arg in cmd["arguments"]:
# arg = (['-l', '--long-flag'], {'help': ""})
if arg[0][0].startswith("-"):
cmds[parent_controller._meta.label][controller._meta.label][cmd["label"]]["arguments"]["flags"].append(
{
"name": ' | '.join(arg[0]),
"help": arg[1]['help']
}
)
else:
cmds[parent_controller._meta.label][controller._meta.label][cmd["label"]]["arguments"]["positional"].append(
{
"name": ' | '.join(arg[0]),
"help": arg[1]['help']
}
)
elif controller._meta.stacked_type == "embedded":
parent_controller = controllers[controller._meta.stacked_on]
if controller._meta.stacked_on == "base":
for cmd in controller._collect_commands():
#print(cmd)
cmds[cmd["label"]] = {"help": cmd['parser_options']['help']}
cmds[cmd["label"]]["arguments"] = {"positional": [], "flags": []}
for arg in cmd["arguments"]:
# arg = (['-l', '--long-flag'], {'help': ""})
if arg[0][0].startswith("-"):
cmds[cmd["label"]]["arguments"]["flags"].append(
{
"name": ' | '.join(arg[0]),
"help": arg[1]['help']
}
)
else:
cmds[cmd["label"]]["arguments"]["positional"].append(
{
"name": ' | '.join(arg[0]),
"help": arg[1]['help']
}
)
elif parent_controller._meta.stacked_on == "base":
for cmd in controller._collect_commands():
#print(cmd)
cmds[cmd["label"]] = {"help": cmd['parser_options']['help']}
cmds[cmd["label"]]["arguments"] = {"positional": [], "flags": []}
for arg in cmd["arguments"]:
# arg = (['-l', '--long-flag'], {'help': ""})
if arg[0][0].startswith("-"):
cmds[cmd["label"]]["arguments"]["flags"].append(
{
"name": ' | '.join(arg[0]),
"help": arg[1]['help']
}
)
else:
cmds[cmd["label"]]["arguments"]["positional"].append(
{
"name": ' | '.join(arg[0]),
"help": arg[1]['help']
}
)
with open(docs_file, "w") as f:
self.app.render({"cmds": cmds}, "docs.j2", f)
self.app.log.info(f"Generated application documentation at {docs_file}.") Jinja2 template: {%- macro parse_commands(cmd_string, cmds, level) -%}
{% for cmd_name, cmd_conf in cmds.items() %}
{% if cmd_name != 'help' %}
{%- set cmd_string = cmd_string + " " + cmd_name %}
{%- set positional_args = [] %}
{%- set flag_args = [] %}
{{ '#' * level }} {{ cmd_name }}
{%- if 'arguments' in cmd_conf.keys() and cmd_conf['arguments'] != {} %}
{%- set args = cmd_conf['arguments'] %}
{%- if 'flags' in args.keys() and args['flags'] != [] %}
{%- set flag_args = args['flags'] %}
{%- set cmd_string = cmd_string + " [flags]" %}
{%- endif %}
{%- if 'positional' in args.keys() and args["positional"] != [] %}
{%- set positional_args = args['positional'] %}
{%- set cmd_string = cmd_string + " [arguments]" %}
{%- endif %}
**Usage:** `{{ cmd_string }}`
**Description:** {{ cmd_conf["help"] }}
{% if positional_args != [] %}
**Arguments:**
{%- for positional_arg in positional_args %}
- `{{ positional_arg['name'] }}` - {{ positional_arg["help"] }}
{%- endfor %}
{% endif %}
{%- if flag_args != [] %}
**Flags:**
{%- for flag_arg in flag_args %}
- `{{ flag_arg['name'] }}` - {{ flag_arg["help"] }}
{%- endfor %}
{% endif %}
{%- else %}
{%- set subcmd_string = cmd_string + " [subcommand]" %}
**Usage:** `{{ subcmd_string }}`
where `[subcommand]` is one of:
{%- for subcmd_name, subcmd_conf in cmds[cmd_name].items() %}
{%- if subcmd_name != 'help' %}
- [`{{ subcmd_name }}`](#{{subcmd_name}})
{%- endif %}
{%- endfor %}
**Description:** {{ cmd_conf["help"] }}
{{- parse_commands(cmd_string, cmds[cmd_name], level+1) -}}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- endmacro -%}
{%- set cmd_string = "<CLI_APP_NAME_HERE>" -%}
# Top level commands
**Usage:** `{{ cmd_string }} [command]`
where `[command]` is one of:
{%- for cmd_name, cmd_conf in cmds.items() %}
- [`{{ cmd_name }}`](#{{cmd_name}})
{%- endfor %}
{{ parse_commands(cmd_string, cmds, 2) }} |
That's great @brianmezini, thank you for sharing! This is a great start. I tried the code in my base controller. It worked fine! I just added some logging and fixed a small bug when an argument had no I could not make the jinja template work, will work on it. Will also do a PR as soon as I have some free time :-) Cheers, Revised code@ex(
help="Generate documentation in Markdown format",
arguments=[
(
["docs_file"],
{
"help": "File to generate documentation in. Defaults to DOCS.md.",
"nargs": "?",
"default": "DOCS.md",
},
)
],
)
def docs(self) -> None:
docs_file = self.app.pargs.docs_file
controllers: dict[str, Controller] = self._controllers_map
cmds = {}
for name, controller in controllers.items():
if controller._meta.label == "base":
continue
self.app.log.info(f"Processing controller {controller._meta.label}")
if controller._meta.stacked_type == "nested":
if controller._meta.stacked_on == "base":
cmds[controller._meta.label] = {"help": controller._meta.help}
for cmd in controller._collect_commands():
self.app.log.info(f" - command {cmd['label']}")
if cmd["hide"] == True:
self.app.log.info(f" \ hidden command - skipping")
cmds[controller._meta.label][cmd["label"]] = {
"help": cmd["parser_options"]["help"]
}
cmds[controller._meta.label][cmd["label"]]["arguments"] = {
"positional": [],
"flags": [],
}
for arg in cmd["arguments"]:
# arg = (['-l', '--long-flag'], {'help': ""})
help_string = arg[1].get("help", "")
if arg[0][0].startswith("-"):
cmds[controller._meta.label][cmd["label"]]["arguments"][
"flags"
].append(
{"name": " | ".join(arg[0]), "help": help_string}
)
else:
cmds[controller._meta.label][cmd["label"]]["arguments"][
"positional"
].append(
{"name": " | ".join(arg[0]), "help": help_string}
)
else:
parent_controller = controllers[controller._meta.stacked_on]
if parent_controller._meta.label in cmds.keys():
cmds[parent_controller._meta.label][controller._meta.label] = {
"help": controller._meta.help
}
else:
cmds[parent_controller._meta.label] = {
"help": parent_controller._meta.help,
controller._meta.label: {"help": controller._meta.help},
}
for cmd in controller._collect_commands():
# print(cmd)
cmds[parent_controller._meta.label][controller._meta.label][
cmd["label"]
] = {"help": cmd["parser_options"]["help"]}
cmds[parent_controller._meta.label][controller._meta.label][
cmd["label"]
]["arguments"] = {"positional": [], "flags": []}
for arg in cmd["arguments"]:
help_string = arg[1].get("help", "")
# arg = (['-l', '--long-flag'], {'help': ""})
if arg[0][0].startswith("-"):
cmds[parent_controller._meta.label][
controller._meta.label
][cmd["label"]]["arguments"]["flags"].append(
{"name": " | ".join(arg[0]), "help": help_string}
)
else:
cmds[parent_controller._meta.label][
controller._meta.label
][cmd["label"]]["arguments"]["positional"].append(
{"name": " | ".join(arg[0]), "help": help_string}
)
elif controller._meta.stacked_type == "embedded":
parent_controller = controllers[controller._meta.stacked_on]
if controller._meta.stacked_on == "base":
for cmd in controller._collect_commands():
# print(cmd)
cmds[cmd["label"]] = {"help": cmd["parser_options"]["help"]}
cmds[cmd["label"]]["arguments"] = {
"positional": [],
"flags": [],
}
for arg in cmd["arguments"]:
help_string = arg[1].get("help", "")
# arg = (['-l', '--long-flag'], {'help': ""})
if arg[0][0].startswith("-"):
cmds[cmd["label"]]["arguments"]["flags"].append(
{"name": " | ".join(arg[0]), "help": help_string}
)
else:
cmds[cmd["label"]]["arguments"]["positional"].append(
{"name": " | ".join(arg[0]), "help": help_string}
)
elif parent_controller._meta.stacked_on == "base":
for cmd in controller._collect_commands():
# print(cmd)
cmds[cmd["label"]] = {"help": cmd["parser_options"]["help"]}
cmds[cmd["label"]]["arguments"] = {
"positional": [],
"flags": [],
}
for arg in cmd["arguments"]:
help_string = arg[1].get("help", "")
# arg = (['-l', '--long-flag'], {'help': ""})
if arg[0][0].startswith("-"):
cmds[cmd["label"]]["arguments"]["flags"].append(
{"name": " | ".join(arg[0]), "help": help_string}
)
else:
cmds[cmd["label"]]["arguments"]["positional"].append(
{"name": " | ".join(arg[0]), "help": help_string}
)
with open(docs_file, "w") as f:
self.app.render(data={"cmds": cmds}, template="docs.j2", out=f)
self.app.log.info(f"Generated application documentation at {docs_file}.") |
Hello,
First of all, thank you so much @derks for developing and maintaning
cement
! I have been using it intensely for a web3 project of mine and I am loving it 💪That said, I was wondering if there was a way to print the full documentation of all commands. Something like
my-app --help
but that outputs all of the controllers and subcommands, including their descriptions and arguments, in a structured way.If this feature does not exist, I am happy to try and make a pull request.
Cheers,
Cocco
The text was updated successfully, but these errors were encountered: