feat: allow extensions to add commands (#1017)

* feat: allow extensions to add commands

* refactor: fix tests

* refactor: more test fix

* refactor: more test fixes

* refactor: more linter fixes
This commit is contained in:
Juro Oravec 2025-03-16 12:03:16 +01:00 committed by GitHub
parent 3a139127cd
commit d3d2d0ab08
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 2320 additions and 397 deletions

View file

@ -4,6 +4,7 @@ Django-components functionality can be extended with "extensions". Extensions al
- Tap into lifecycle events, such as when a component is created, deleted, registered, or unregistered.
- Add new attributes and methods to the components under an extension-specific nested class.
- Define custom commands that can be executed via the Django management command interface.
## Setting up extensions
@ -328,3 +329,286 @@ class MyComponent(Component):
```
This will log the component name and color when the component is created, deleted, or rendered.
## Extension Commands
Extensions in django-components can define custom commands that can be executed via the Django management command interface. This allows for powerful automation and customization capabilities.
For example, if you have an extension that defines a command that prints "Hello world", you can run the command with:
```bash
python manage.py components ext run my_ext hello
```
Where:
- `python manage.py components ext run` - is the Django command run
- `my_ext` - is the extension name
- `hello` - is the command name
### Defining Commands
To define a command, subclass from [`ComponentCommand`](../../../reference/api#django_components.ComponentCommand).
This subclass should define:
- `name` - the command's name
- `help` - the command's help text
- `handle` - the logic to execute when the command is run
```python
from django_components import ComponentCommand, ComponentsExtension
class HelloCommand(ComponentCommand):
name = "hello"
help = "Say hello"
def handle(self, *args, **kwargs):
print("Hello, world!")
class MyExt(ComponentsExtension):
name = "my_ext"
commands = [HelloCommand]
```
### Defining Command Arguments and Options
Commands can accept positional arguments and options (e.g. `--foo`), which are defined using the
[`arguments`](../../../reference/api#django_components.ComponentCommand.arguments)
attribute of the [`ComponentCommand`](../../../reference/api#django_components.ComponentCommand) class.
The arguments are parsed with [`argparse`](https://docs.python.org/3/library/argparse.html)
into a dictionary of arguments and options. These are then available
as keyword arguments to the [`handle`](../../../reference/api#django_components.ComponentCommand.handle)
method of the command.
```python
from django_components import CommandArg, ComponentCommand, ComponentsExtension
class HelloCommand(ComponentCommand):
name = "hello"
help = "Say hello"
arguments = [
# Positional argument
CommandArg(
name_or_flags="name",
help="The name to say hello to",
),
# Optional argument
CommandArg(
name_or_flags=["--shout", "-s"],
action="store_true",
help="Shout the hello",
),
]
def handle(self, name: str, *args, **kwargs):
shout = kwargs.get("shout", False)
msg = f"Hello, {name}!"
if shout:
msg = msg.upper()
print(msg)
```
You can run the command with arguments and options:
```bash
python manage.py components ext run my_ext hello John --shout
>>> HELLO, JOHN!
```
!!! note
Command definitions are parsed with `argparse`, so you can use all the features of `argparse` to define your arguments and options.
See the [argparse documentation](https://docs.python.org/3/library/argparse.html) for more information.
django-components defines types as
[`CommandArg`](../../../reference/api#django_components.CommandArg),
[`CommandArgGroup`](../../../reference/api#django_components.CommandArgGroup),
[`CommandSubcommand`](../../../reference/api#django_components.CommandSubcommand),
and [`CommandParserInput`](../../../reference/api#django_components.CommandParserInput)
to help with type checking.
!!! note
If a command doesn't have the [`handle`](../../../reference/api#django_components.ComponentCommand.handle)
method defined, the command will print a help message and exit.
### Grouping Arguments
Arguments can be grouped using [`CommandArgGroup`](../../../reference/api#django_components.CommandArgGroup)
to provide better organization and help messages.
Read more on [argparse argument groups](https://docs.python.org/3/library/argparse.html#argument-groups).
```python
from django_components import CommandArg, CommandArgGroup, ComponentCommand, ComponentsExtension
class HelloCommand(ComponentCommand):
name = "hello"
help = "Say hello"
# Argument parsing is managed by `argparse`.
arguments = [
# Positional argument
CommandArg(
name_or_flags="name",
help="The name to say hello to",
),
# Optional argument
CommandArg(
name_or_flags=["--shout", "-s"],
action="store_true",
help="Shout the hello",
),
# When printing the command help message, `--bar` and `--baz`
# will be grouped under "group bar".
CommandArgGroup(
title="group bar",
description="Group description.",
arguments=[
CommandArg(
name_or_flags="--bar",
help="Bar description.",
),
CommandArg(
name_or_flags="--baz",
help="Baz description.",
),
],
),
]
def handle(self, name: str, *args, **kwargs):
shout = kwargs.get("shout", False)
msg = f"Hello, {name}!"
if shout:
msg = msg.upper()
print(msg)
```
### Subcommands
Extensions can define subcommands, allowing for more complex command structures.
Subcommands are defined similarly to root commands, as subclasses of
[`ComponentCommand`](../../../reference/api#django_components.ComponentCommand) class.
However, instead of defining the subcommands in the
[`commands`](../../../reference/api#django_components.ComponentExtension.commands)
attribute of the extension, you define them in the
[`subcommands`](../../../reference/api#django_components.ComponentCommand.subcommands)
attribute of the parent command:
```python
from django_components import CommandArg, CommandArgGroup, ComponentCommand, ComponentsExtension
class ChildCommand(ComponentCommand):
name = "child"
help = "Child command"
def handle(self, *args, **kwargs):
print("Child command")
class ParentCommand(ComponentCommand):
name = "parent"
help = "Parent command"
subcommands = [
ChildCommand,
]
def handle(self, *args, **kwargs):
print("Parent command")
```
In this example, we can run two commands.
Either the parent command:
```bash
python manage.py components ext run parent
>>> Parent command
```
Or the child command:
```bash
python manage.py components ext run parent child
>>> Child command
```
!!! warning
Subcommands are independent of the parent command. When a subcommand runs, the parent command is NOT executed.
As such, if you want to pass arguments to both the parent and child commands, e.g.:
```bash
python manage.py components ext run parent --foo child --bar
```
You should instead pass all the arguments to the subcommand:
```bash
python manage.py components ext run parent child --foo --bar
```
### Print command help
By default, all commands will print their help message when run with the `--help` / `-h` flag.
```bash
python manage.py components ext run my_ext --help
```
The help message prints out all the arguments and options available for the command, as well as any subcommands.
### Testing Commands
Commands can be tested using Django's [`call_command()`](https://docs.djangoproject.com/en/5.1/ref/django-admin/#running-management-commands-from-your-code)
function, which allows you to simulate running the command in tests.
```python
from django.core.management import call_command
call_command('components', 'ext', 'run', 'my_ext', 'hello', '--name', 'John')
```
To capture the output of the command, you can use the [`StringIO`](https://docs.python.org/3/library/io.html#io.StringIO)
module to redirect the output to a string:
```python
from io import StringIO
out = StringIO()
with patch("sys.stdout", new=out):
call_command('components', 'ext', 'run', 'my_ext', 'hello', '--name', 'John')
output = out.getvalue()
```
And to temporarily set the extensions, you can use the [`@djc_test`](../../../reference/testing_api#djc_test) decorator.
Thus, a full test example can then look like this:
```python
from io import StringIO
from unittest.mock import patch
from django.core.management import call_command
from django_components.testing import djc_test
@djc_test(
components_settings={
"extensions": [
"my_app.extensions.MyExtension",
],
},
)
def test_hello_command(self):
out = StringIO()
with patch("sys.stdout", new=out):
call_command('components', 'ext', 'run', 'my_ext', 'hello', '--name', 'John')
output = out.getvalue()
assert output == "Hello, John!\n"
```

View file

@ -7,6 +7,30 @@
options:
show_if_no_docstring: true
::: django_components.CommandArg
options:
show_if_no_docstring: true
::: django_components.CommandArgGroup
options:
show_if_no_docstring: true
::: django_components.CommandHandler
options:
show_if_no_docstring: true
::: django_components.CommandLiteralAction
options:
show_if_no_docstring: true
::: django_components.CommandParserInput
options:
show_if_no_docstring: true
::: django_components.CommandSubcommand
options:
show_if_no_docstring: true
::: django_components.Component
options:
show_if_no_docstring: true
@ -39,6 +63,10 @@
options:
show_if_no_docstring: true
::: django_components.ComponentCommand
options:
show_if_no_docstring: true
::: django_components.ComponentsSettings
options:
show_if_no_docstring: true

View file

@ -6,21 +6,361 @@ These are all the [Django management commands](https://docs.djangoproject.com/en
that will be added by installing `django_components`:
## `upgradecomponent`
## ` components`
```txt
usage: manage.py upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks]
usage: python manage.py components [-h] {create,upgrade,ext} ...
```
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/management/commands/upgradecomponent.py#L12" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/commands/components.py#L9" target="_blank">See source code</a>
Updates component and component_block tags to the new syntax
The entrypoint for the 'components' commands.
**Options:**
- `-h`, `--help`
- show this help message and exit
**Subcommands:**
- [`create`](../commands#components-`create`)
- Create a new django component.
- [`upgrade`](../commands#components-`upgrade`)
- Upgrade django components syntax from '{% component_block ... %}' to '{% component ... %}'.
- [`ext`](../commands#components-`ext`)
- Run extension commands.
The entrypoint for the "components" commands.
```bash
python manage.py components start <name>
python manage.py components upgrade <name>
python manage.py components ext list
python manage.py components ext run <extension> <command>
```
## `components create`
```txt
usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run] name
```
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/commands/create.py#L11" target="_blank">See source code</a>
Create a new django component.
**Positional Arguments:**
- `name`
- The name of the component to create. This is a required argument.
**Options:**
- `-h`, `--help`
- show this help message and exit
- `--path PATH`
- The path to the component's directory. This is an optional argument. If not provided, the command will use the `COMPONENTS.dirs` setting from your Django settings.
- `--js JS`
- The name of the JavaScript file. This is an optional argument. The default value is `script.js`.
- `--css CSS`
- The name of the CSS file. This is an optional argument. The default value is `style.css`.
- `--template TEMPLATE`
- The name of the template file. This is an optional argument. The default value is `template.html`.
- `--force`
- This option allows you to overwrite existing files if they exist. This is an optional argument.
- `--verbose`
- This option allows the command to print additional information during component creation. This is an optional argument.
- `--dry-run`
- This option allows you to simulate component creation without actually creating any files. This is an optional argument. The default value is `False`.
### Usage
To use the command, run the following command in your terminal:
```bash
python manage.py components create <name> --path <path> --js <js_filename> --css <css_filename> --template <template_filename> --force --verbose --dry-run
```
Replace `<name>`, `<path>`, `<js_filename>`, `<css_filename>`, and `<template_filename>` with your desired values.
### Examples
Here are some examples of how you can use the command:
**Creating a Component with Default Settings**
To create a component with the default settings, you only need to provide the name of the component:
```bash
python manage.py components create my_component
```
This will create a new component named `my_component` in the `components` directory of your Django project. The JavaScript, CSS, and template files will be named `script.js`, `style.css`, and `template.html`, respectively.
**Creating a Component with Custom Settings**
You can also create a component with custom settings by providing additional arguments:
```bash
python manage.py components create new_component --path my_components --js my_script.js --css my_style.css --template my_template.html
```
This will create a new component named `new_component` in the `my_components` directory. The JavaScript, CSS, and template files will be named `my_script.js`, `my_style.css`, and `my_template.html`, respectively.
**Overwriting an Existing Component**
If you want to overwrite an existing component, you can use the `--force` option:
```bash
python manage.py components create my_component --force
```
This will overwrite the existing `my_component` if it exists.
**Simulating Component Creation**
If you want to simulate the creation of a component without actually creating any files, you can use the `--dry-run` option:
```bash
python manage.py components create my_component --dry-run
```
This will simulate the creation of `my_component` without creating any files.
## `components upgrade`
```txt
usage: python manage.py components upgrade [-h] [--path PATH]
```
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/commands/upgrade.py#L13" target="_blank">See source code</a>
Upgrade django components syntax from '{% component_block ... %}' to '{% component ... %}'.
**Options:**
- `-h`, `--help`
- show this help message and exit
- `--path PATH`
- Path to search for components
## `components ext`
```txt
usage: python manage.py components ext [-h] {list,run} ...
```
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/commands/ext.py#L5" target="_blank">See source code</a>
Run extension commands.
**Options:**
- `-h`, `--help`
- show this help message and exit
**Subcommands:**
- [`list`](../commands#components-ext-`list`)
- List all extensions.
- [`run`](../commands#components-ext-`run`)
- Run a command added by an extension.
Run extension commands.
```bash
python manage.py components ext list
python manage.py components ext run <extension> <command>
```
## `components ext list`
```txt
usage: python manage.py components ext list [-h] [-v {0,1}]
```
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/commands/ext_list.py#L6" target="_blank">See source code</a>
List all extensions.
**Options:**
- `-h`, `--help`
- show this help message and exit
- `-v {0,1}`, `--verbosity {0,1}`
- Verbosity level; 0=minimal output, 1=normal output
List all extensions.
```bash
python manage.py components ext list
```
Prints the list of installed extensions:
```txt
Installed extensions:
view
my_extension
```
If you need to omit the title in order to programmatically post-process the output,
you can use the `--verbosity` (or `-v`) flag:
```bash
python manage.py components ext list -v 0
```
Which prints just:
```txt
view
my_extension
```
## `components ext run`
```txt
usage: python manage.py components ext run [-h]
```
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/commands/ext_run.py#L48" target="_blank">See source code</a>
Run a command added by an extension.
**Options:**
- `-h`, `--help`
- show this help message and exit
Run a command added by an [extension](../../concepts/advanced/extensions).
Each extension can add its own commands, which will be available to run with this command.
For example, if you define and install the following extension:
```python
from django_components ComponentCommand, ComponentsExtension
class HelloCommand(ComponentCommand):
name = "hello"
help = "Say hello"
def handle(self, *args, **kwargs):
print("Hello, world!")
class MyExt(ComponentsExtension):
name = "my_ext"
commands = [HelloCommand]
```
You can run the `hello` command with:
```bash
python manage.py components ext run my_ext hello
```
You can also define arguments for the command, which will be passed to the command's `handle` method.
```python
from django_components import CommandArg, ComponentCommand, ComponentsExtension
class HelloCommand(ComponentCommand):
name = "hello"
help = "Say hello"
arguments = [
CommandArg(name="name", help="The name to say hello to"),
CommandArg(name=["--shout", "-s"], action="store_true"),
]
def handle(self, name: str, *args, **kwargs):
shout = kwargs.get("shout", False)
msg = f"Hello, {name}!"
if shout:
msg = msg.upper()
print(msg)
```
You can run the command with:
```bash
python manage.py components ext run my_ext hello --name John --shout
```
!!! note
Command arguments and options are based on Python's `argparse` module.
For more information, see the [argparse documentation](https://docs.python.org/3/library/argparse.html).
## `upgradecomponent`
```txt
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
[--traceback] [--no-color] [--force-color] [--skip-checks]
```
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/management/commands/upgradecomponent.py#L83" target="_blank">See source code</a>
Deprecated. Use `components upgrade` instead.
**Options:**
@ -48,24 +388,26 @@ Updates component and component_block tags to the new syntax
**Deprecated**. Use [`components upgrade`](../commands#components-upgrade) instead.
## `startcomponent`
```txt
usage: manage.py startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
[--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
[--traceback] [--no-color] [--force-color] [--skip-checks]
name
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run]
[--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback]
[--no-color] [--force-color] [--skip-checks]
name
```
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/management/commands/startcomponent.py#L8" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/management/commands/startcomponent.py#L83" target="_blank">See source code</a>
Create a new django component.
Deprecated. Use `components create` instead.
**Positional Arguments:**
@ -110,58 +452,6 @@ Create a new django component.
### Management Command Usage
To use the command, run the following command in your terminal:
```bash
python manage.py startcomponent <name> --path <path> --js <js_filename> --css <css_filename> --template <template_filename> --force --verbose --dry-run
```
Replace `<name>`, `<path>`, `<js_filename>`, `<css_filename>`, and `<template_filename>` with your desired values.
### Management Command Examples
Here are some examples of how you can use the command:
#### Creating a Component with Default Settings
To create a component with the default settings, you only need to provide the name of the component:
```bash
python manage.py startcomponent my_component
```
This will create a new component named `my_component` in the `components` directory of your Django project. The JavaScript, CSS, and template files will be named `script.js`, `style.css`, and `template.html`, respectively.
#### Creating a Component with Custom Settings
You can also create a component with custom settings by providing additional arguments:
```bash
python manage.py startcomponent new_component --path my_components --js my_script.js --css my_style.css --template my_template.html
```
This will create a new component named `new_component` in the `my_components` directory. The JavaScript, CSS, and template files will be named `my_script.js`, `my_style.css`, and `my_template.html`, respectively.
#### Overwriting an Existing Component
If you want to overwrite an existing component, you can use the `--force` option:
```bash
python manage.py startcomponent my_component --force
```
This will overwrite the existing `my_component` if it exists.
#### Simulating Component Creation
If you want to simulate the creation of a component without actually creating any files, you can use the `--dry-run` option:
```bash
python manage.py startcomponent my_component --dry-run
```
This will simulate the creation of `my_component` without creating any files.
**Deprecated**. Use [`components create`](../commands#components-create) instead.

View file

@ -67,7 +67,7 @@ If you insert this tag multiple times, ALL JS scripts will be duplicately insert
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1568" target="_blank">See source code</a>
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/templatetags/component_tags.py#L1573" target="_blank">See source code</a>

View file

@ -42,14 +42,15 @@ from argparse import ArgumentParser
from importlib import import_module
from pathlib import Path
from textwrap import dedent
from typing import Any, Dict, List, NamedTuple, Optional, Sequence, Type, Union
from typing import Any, Dict, List, NamedTuple, Optional, Sequence, Tuple, Type, Union
from django.core.management.base import BaseCommand
from django.urls import URLPattern, URLResolver
from django_components import ComponentVars, TagFormatterABC
from django_components.component import Component
from django_components import Component, ComponentVars, ComponentCommand, TagFormatterABC
from django_components.commands.components import ComponentsRootCommand
from django_components.node import BaseNode
from django_components.util.command import setup_parser_from_command
from django_components.util.misc import get_import_path
# NOTE: This file is an entrypoint for the `gen-files` plugin in `mkdocs.yml`.
@ -462,13 +463,6 @@ def gen_reference_commands():
These are discovered by looking at the files defined inside `management/commands`.
"""
command_files = Path("./src/django_components/management/commands").glob("*.py")
command_modules = [
(p.stem, f"django_components.management.commands.{p.stem}")
for p in command_files
if not p.stem.startswith("_")
]
preface = "<!-- Autogenerated by reference.py -->\n\n"
preface += (root / "docs/templates/reference_commands.md").read_text()
out_file = root / "docs/reference/commands.md"
@ -477,13 +471,72 @@ def gen_reference_commands():
with out_file.open("w", encoding="utf-8") as f:
f.write(preface + "\n\n")
for cmd_name, cmd_path in command_modules:
cmd_module = import_module(cmd_path)
# Document all commands defined by django-components
# All our commands are scoped under `components` (e.g. `components create`, `components upgrade`, etc.)
# Furthermore, all subcommands are declared statically, so we can walk down the tree of subcommands.
commands_stack: List[Tuple[Type[ComponentCommand], Tuple[str, ...]]] = [(ComponentsRootCommand, ())]
while commands_stack:
cmd_def_cls, cmd_path = commands_stack.pop()
# NOTE: Argparse formats the help string, and so it uses `%%` to escape `%` characters.
# So we need to replace them with `%`
cmd_summary = cmd_def_cls.help.replace("%%", "%") if cmd_def_cls.help else ""
cmd_desc = dedent(cmd_def_cls.__doc__ or "")
cmd_name = " ".join(cmd_path) + " " + cmd_def_cls.name
cmd_parser = setup_parser_from_command(cmd_def_cls)
cmd_usage = cmd_parser.format_usage()
# NOTE: The generated usage shows only the command name, not the full path.
# So we need to add it manually.
#
# So this:
# `usage: ext run [-h]`
#
# becomes this:
# `usage: python manage.py components ext run [-h]`
cmd_usage = cmd_usage[:7] + "python manage.py " + " ".join(cmd_path) + " " + cmd_usage[7:]
formatted_args = _format_command_args(cmd_parser, cmd_path + (cmd_def_cls.name,))
# Add link to source code
module_abs_path = import_module(cmd_def_cls.__module__).__file__
module_rel_path = Path(module_abs_path).relative_to(Path.cwd()).as_posix() # type: ignore[arg-type]
obj_lineno = inspect.findsource(cmd_def_cls)[1]
source_code_link = _format_source_code_html(module_rel_path, obj_lineno)
# NOTE: For the commands we have to generate the markdown entries ourselves,
# instead of delegating to mkdocs, for two reasons:
# 1. All commands have to use the class name `Command` for Django to pick them up
# 2. The command name is actually defined by the file name.
f.write(
f"## `{cmd_name}`\n\n"
f"```txt\n{cmd_usage}\n```\n\n"
f"{source_code_link}\n\n"
f"{cmd_summary}\n\n"
f"{formatted_args}\n\n"
f"{cmd_desc}\n\n"
)
# Add subcommands
for subcmd_cls in reversed(cmd_def_cls.subcommands):
commands_stack.append((subcmd_cls, cmd_path + (cmd_def_cls.name,)))
# TODO_v1 - REMOVE - This this section as it only for legacy commands `startcomponent` and `upgradecomponent`
command_files = Path("./src/django_components/management/commands").glob("*.py")
command_modules = [
(p.stem, f"django_components.management.commands.{p.stem}")
for p in command_files
if not p.stem.startswith("_")
]
for cmd_name, cmd_import_path in command_modules:
# NOTE: `components` command is already documented in the non-legacy section
if cmd_name == "components":
continue
cmd_module = import_module(cmd_import_path)
cmd_cls: BaseCommand = cmd_module.Command
cmd_summary = cmd_cls.help
cmd_desc = dedent(cmd_cls.__doc__ or "")
cmd_parser: ArgumentParser = cmd_cls().create_parser("manage.py", cmd_name)
cmd_usage: str = cmd_parser.format_usage()
cmd_parser = cmd_cls().create_parser("manage.py", cmd_name)
cmd_usage = cmd_parser.format_usage()
formatted_args = _format_command_args(cmd_parser)
# Add link to source code
@ -850,6 +903,36 @@ def _gen_command_args(parser: ArgumentParser) -> str:
# {'desc': "Show program's version number and exit.",
# ```
def _parse_command_args(cmd_inputs: str) -> Dict[str, List[Dict]]:
# Replace
# ```
# subcommands:
# {create,upgrade,ext}
# create Create a new django component.
# upgrade Upgrade django components syntax from ...
# ext Run extension commands.
# ```
#
# Into:
# ```
# subcommands:
# create Create a new django component.
# upgrade Upgrade django components syntax from ...
# ext Run extension commands.
# ```
if "subcommands:" in cmd_inputs:
cmd_inputs = re.compile(r"subcommands:\n.*?\}", re.DOTALL).sub("subcommands:", cmd_inputs)
# Dedent the lines that contain subcommands from 4 spaces to 2 spaces
text_before_subcommands, text_after_subcommands = cmd_inputs.split("subcommands:\n")
lines_after_subcommands = text_after_subcommands.split("\n")
new_text_after_subcommands = ""
for line in lines_after_subcommands:
if line.startswith(" " * 4):
new_text_after_subcommands += line[2:] + "\n"
else:
new_text_after_subcommands += line + "\n"
cmd_inputs = text_before_subcommands + "subcommands:\n" + new_text_after_subcommands
section: Optional[str] = None
data: Dict[str, List[Dict]] = {}
@ -893,7 +976,7 @@ def _parse_command_args(cmd_inputs: str) -> Dict[str, List[Dict]]:
return data
def _format_command_args(cmd_parser: ArgumentParser):
def _format_command_args(cmd_parser: ArgumentParser, cmd_path: Optional[Sequence[str]] = None):
cmd_inputs: str = _gen_command_args(cmd_parser)
parsed_cmd_inputs = _parse_command_args(cmd_inputs)
@ -901,9 +984,15 @@ def _format_command_args(cmd_parser: ArgumentParser):
for section, args in parsed_cmd_inputs.items():
formatted_args += f"**{section.title()}:**\n\n"
for arg in args:
formatted_args += (
"- " + ", ".join([f"`{name}`" for name in arg["names"]]) + f"\n - {arg['desc']}" + "\n"
)
# Add link to the subcommand
if section == "subcommands":
name = "`" + arg["names"][0] + "`"
if cmd_path:
name = "[" + name + "](../commands#" + "-".join(cmd_path) + "-" + name + ")"
else:
name = ", ".join([f"`{name}`" for name in arg["names"]])
formatted_args += "- " + name + f"\n - {arg['desc']}" + "\n"
formatted_args += "\n"
return formatted_args