mirror of
https://github.com/django-components/django-components.git
synced 2025-08-28 01:44:05 +00:00
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:
parent
3a139127cd
commit
d3d2d0ab08
28 changed files with 2320 additions and 397 deletions
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -6,8 +6,19 @@
|
||||||
|
|
||||||
- Support for extensions (plugins) for django-components!
|
- Support for extensions (plugins) for django-components!
|
||||||
|
|
||||||
|
- Hook into lifecycle events of django-components
|
||||||
|
- Pre-/post-process component inputs, outputs, and templates
|
||||||
|
- Add extra methods or attributes to Components
|
||||||
|
- Add custom CLI commands to django-components
|
||||||
|
|
||||||
Read more on [Extensions](https://django-components.github.io/django-components/0.131/concepts/advanced/extensions/).
|
Read more on [Extensions](https://django-components.github.io/django-components/0.131/concepts/advanced/extensions/).
|
||||||
|
|
||||||
|
- New CLI commands:
|
||||||
|
- `components create <name>` - Create a new component (supersedes `startcomponent`)
|
||||||
|
- `components upgrade <name>` - Upgrade a component (supersedes `upgradecomponent`)
|
||||||
|
- `components ext list` - List all extensions
|
||||||
|
- `components ext run <extension> <command>` - Run a command added by an extension
|
||||||
|
|
||||||
- `@djc_test` decorator for writing tests that involve Components.
|
- `@djc_test` decorator for writing tests that involve Components.
|
||||||
|
|
||||||
- The decorator manages global state, ensuring that tests don't leak.
|
- The decorator manages global state, ensuring that tests don't leak.
|
||||||
|
@ -16,6 +27,12 @@
|
||||||
|
|
||||||
See the API reference for [`@djc_test`](https://django-components.github.io/django-components/0.131/reference/testing_api/#djc_test) for more details.
|
See the API reference for [`@djc_test`](https://django-components.github.io/django-components/0.131/reference/testing_api/#djc_test) for more details.
|
||||||
|
|
||||||
|
#### Refactor
|
||||||
|
|
||||||
|
- The `startcomponent` and `upgradecomponent` commands are deprecated, and will be removed in v1.
|
||||||
|
|
||||||
|
Instead, use `components start <name>` and `components upgrade`.
|
||||||
|
|
||||||
#### Internal
|
#### Internal
|
||||||
|
|
||||||
- Settings are now loaded only once, and thus are considered immutable once loaded. Previously,
|
- Settings are now loaded only once, and thus are considered immutable once loaded. Previously,
|
||||||
|
|
|
@ -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.
|
- 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.
|
- 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
|
## 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.
|
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"
|
||||||
|
```
|
||||||
|
|
|
@ -7,6 +7,30 @@
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
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
|
::: django_components.Component
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
@ -39,6 +63,10 @@
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
||||||
|
::: django_components.ComponentCommand
|
||||||
|
options:
|
||||||
|
show_if_no_docstring: true
|
||||||
|
|
||||||
::: django_components.ComponentsSettings
|
::: django_components.ComponentsSettings
|
||||||
options:
|
options:
|
||||||
show_if_no_docstring: true
|
show_if_no_docstring: true
|
||||||
|
|
|
@ -6,21 +6,361 @@ These are all the [Django management commands](https://docs.djangoproject.com/en
|
||||||
that will be added by installing `django_components`:
|
that will be added by installing `django_components`:
|
||||||
|
|
||||||
|
|
||||||
## `upgradecomponent`
|
## ` components`
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
usage: manage.py upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
usage: python manage.py components [-h] {create,upgrade,ext} ...
|
||||||
[--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#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:**
|
**Options:**
|
||||||
|
|
||||||
|
@ -48,24 +388,26 @@ Updates component and component_block tags to the new syntax
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Deprecated**. Use [`components upgrade`](../commands#components-upgrade) instead.
|
||||||
|
|
||||||
|
|
||||||
## `startcomponent`
|
## `startcomponent`
|
||||||
|
|
||||||
```txt
|
```txt
|
||||||
usage: manage.py startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
|
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run]
|
||||||
[--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
[--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH] [--traceback]
|
||||||
[--traceback] [--no-color] [--force-color] [--skip-checks]
|
[--no-color] [--force-color] [--skip-checks]
|
||||||
name
|
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:**
|
**Positional Arguments:**
|
||||||
|
|
||||||
|
@ -110,58 +452,6 @@ Create a new django component.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Management Command Usage
|
**Deprecated**. Use [`components create`](../commands#components-create) instead.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -42,14 +42,15 @@ from argparse import ArgumentParser
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from textwrap import dedent
|
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.core.management.base import BaseCommand
|
||||||
from django.urls import URLPattern, URLResolver
|
from django.urls import URLPattern, URLResolver
|
||||||
|
|
||||||
from django_components import ComponentVars, TagFormatterABC
|
from django_components import Component, ComponentVars, ComponentCommand, TagFormatterABC
|
||||||
from django_components.component import Component
|
from django_components.commands.components import ComponentsRootCommand
|
||||||
from django_components.node import BaseNode
|
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
|
from django_components.util.misc import get_import_path
|
||||||
|
|
||||||
# NOTE: This file is an entrypoint for the `gen-files` plugin in `mkdocs.yml`.
|
# 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`.
|
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 = "<!-- Autogenerated by reference.py -->\n\n"
|
||||||
preface += (root / "docs/templates/reference_commands.md").read_text()
|
preface += (root / "docs/templates/reference_commands.md").read_text()
|
||||||
out_file = root / "docs/reference/commands.md"
|
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:
|
with out_file.open("w", encoding="utf-8") as f:
|
||||||
f.write(preface + "\n\n")
|
f.write(preface + "\n\n")
|
||||||
|
|
||||||
for cmd_name, cmd_path in command_modules:
|
# Document all commands defined by django-components
|
||||||
cmd_module = import_module(cmd_path)
|
# 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_cls: BaseCommand = cmd_module.Command
|
||||||
cmd_summary = cmd_cls.help
|
cmd_summary = cmd_cls.help
|
||||||
cmd_desc = dedent(cmd_cls.__doc__ or "")
|
cmd_desc = dedent(cmd_cls.__doc__ or "")
|
||||||
cmd_parser: ArgumentParser = cmd_cls().create_parser("manage.py", cmd_name)
|
cmd_parser = cmd_cls().create_parser("manage.py", cmd_name)
|
||||||
cmd_usage: str = cmd_parser.format_usage()
|
cmd_usage = cmd_parser.format_usage()
|
||||||
formatted_args = _format_command_args(cmd_parser)
|
formatted_args = _format_command_args(cmd_parser)
|
||||||
|
|
||||||
# Add link to source code
|
# Add link to source code
|
||||||
|
@ -850,6 +903,36 @@ def _gen_command_args(parser: ArgumentParser) -> str:
|
||||||
# {'desc': "Show program's version number and exit.",
|
# {'desc': "Show program's version number and exit.",
|
||||||
# ```
|
# ```
|
||||||
def _parse_command_args(cmd_inputs: str) -> Dict[str, List[Dict]]:
|
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
|
section: Optional[str] = None
|
||||||
data: Dict[str, List[Dict]] = {}
|
data: Dict[str, List[Dict]] = {}
|
||||||
|
|
||||||
|
@ -893,7 +976,7 @@ def _parse_command_args(cmd_inputs: str) -> Dict[str, List[Dict]]:
|
||||||
return data
|
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)
|
cmd_inputs: str = _gen_command_args(cmd_parser)
|
||||||
parsed_cmd_inputs = _parse_command_args(cmd_inputs)
|
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():
|
for section, args in parsed_cmd_inputs.items():
|
||||||
formatted_args += f"**{section.title()}:**\n\n"
|
formatted_args += f"**{section.title()}:**\n\n"
|
||||||
for arg in args:
|
for arg in args:
|
||||||
formatted_args += (
|
# Add link to the subcommand
|
||||||
"- " + ", ".join([f"`{name}`" for name in arg["names"]]) + f"\n - {arg['desc']}" + "\n"
|
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"
|
formatted_args += "\n"
|
||||||
|
|
||||||
return formatted_args
|
return formatted_args
|
||||||
|
|
|
@ -6,6 +6,15 @@
|
||||||
# isort: off
|
# isort: off
|
||||||
from django_components.app_settings import ContextBehavior, ComponentsSettings
|
from django_components.app_settings import ContextBehavior, ComponentsSettings
|
||||||
from django_components.autodiscovery import autodiscover, import_libraries
|
from django_components.autodiscovery import autodiscover, import_libraries
|
||||||
|
from django_components.util.command import (
|
||||||
|
CommandArg,
|
||||||
|
CommandArgGroup,
|
||||||
|
CommandHandler,
|
||||||
|
CommandLiteralAction,
|
||||||
|
CommandParserInput,
|
||||||
|
CommandSubcommand,
|
||||||
|
ComponentCommand,
|
||||||
|
)
|
||||||
from django_components.component import Component, ComponentVars
|
from django_components.component import Component, ComponentVars
|
||||||
from django_components.component_media import ComponentMediaInput, ComponentMediaInputPath
|
from django_components.component_media import ComponentMediaInput, ComponentMediaInputPath
|
||||||
from django_components.component_registry import (
|
from django_components.component_registry import (
|
||||||
|
@ -52,9 +61,15 @@ from django_components.util.types import EmptyTuple, EmptyDict
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"AlreadyRegistered",
|
"AlreadyRegistered",
|
||||||
"autodiscover",
|
"autodiscover",
|
||||||
"cached_template",
|
|
||||||
"BaseNode",
|
"BaseNode",
|
||||||
"ContextBehavior",
|
"cached_template",
|
||||||
|
"CommandArg",
|
||||||
|
"CommandArgGroup",
|
||||||
|
"CommandHandler",
|
||||||
|
"CommandLiteralAction",
|
||||||
|
"CommandParserInput",
|
||||||
|
"CommandSubcommand",
|
||||||
|
"ComponentCommand",
|
||||||
"ComponentsSettings",
|
"ComponentsSettings",
|
||||||
"Component",
|
"Component",
|
||||||
"ComponentExtension",
|
"ComponentExtension",
|
||||||
|
@ -67,6 +82,7 @@ __all__ = [
|
||||||
"ComponentView",
|
"ComponentView",
|
||||||
"component_formatter",
|
"component_formatter",
|
||||||
"component_shorthand_formatter",
|
"component_shorthand_formatter",
|
||||||
|
"ContextBehavior",
|
||||||
"DynamicComponent",
|
"DynamicComponent",
|
||||||
"EmptyTuple",
|
"EmptyTuple",
|
||||||
"EmptyDict",
|
"EmptyDict",
|
||||||
|
|
0
src/django_components/commands/__init__.py
Normal file
0
src/django_components/commands/__init__.py
Normal file
29
src/django_components/commands/components.py
Normal file
29
src/django_components/commands/components.py
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
from django_components.commands.create import CreateCommand
|
||||||
|
from django_components.commands.ext import ExtCommand
|
||||||
|
from django_components.commands.upgrade import UpgradeCommand
|
||||||
|
from django_components.util.command import ComponentCommand
|
||||||
|
|
||||||
|
|
||||||
|
# TODO_V3 - This command should be called standalone as "components":
|
||||||
|
# `python -m components start <name>`
|
||||||
|
# `components start <name>`
|
||||||
|
class ComponentsRootCommand(ComponentCommand):
|
||||||
|
"""
|
||||||
|
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>
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "components"
|
||||||
|
help = "The entrypoint for the 'components' commands."
|
||||||
|
|
||||||
|
subcommands = [
|
||||||
|
CreateCommand,
|
||||||
|
UpgradeCommand,
|
||||||
|
ExtCommand,
|
||||||
|
]
|
220
src/django_components/commands/create.py
Normal file
220
src/django_components/commands/create.py
Normal file
|
@ -0,0 +1,220 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from textwrap import dedent
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import CommandError
|
||||||
|
|
||||||
|
from django_components.util.command import CommandArg, ComponentCommand, style_success, style_warning
|
||||||
|
|
||||||
|
|
||||||
|
class CreateCommand(ComponentCommand):
|
||||||
|
"""
|
||||||
|
### 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.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
name = "create"
|
||||||
|
help = "Create a new django component."
|
||||||
|
|
||||||
|
arguments = [
|
||||||
|
CommandArg(
|
||||||
|
name_or_flags="name",
|
||||||
|
help="The name of the component to create. This is a required argument.",
|
||||||
|
),
|
||||||
|
CommandArg(
|
||||||
|
name_or_flags="--path",
|
||||||
|
help=(
|
||||||
|
"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."
|
||||||
|
),
|
||||||
|
default=None,
|
||||||
|
),
|
||||||
|
CommandArg(
|
||||||
|
name_or_flags="--js",
|
||||||
|
help="The name of the JavaScript file. This is an optional argument. The default value is `script.js`.",
|
||||||
|
default="script.js",
|
||||||
|
),
|
||||||
|
CommandArg(
|
||||||
|
name_or_flags="--css",
|
||||||
|
help="The name of the CSS file. This is an optional argument. The default value is `style.css`.",
|
||||||
|
default="style.css",
|
||||||
|
),
|
||||||
|
CommandArg(
|
||||||
|
name_or_flags="--template",
|
||||||
|
help="The name of the template file. This is an optional argument. The default value is `template.html`.",
|
||||||
|
default="template.html",
|
||||||
|
),
|
||||||
|
CommandArg(
|
||||||
|
name_or_flags="--force",
|
||||||
|
help="This option allows you to overwrite existing files if they exist. This is an optional argument.",
|
||||||
|
action="store_true",
|
||||||
|
),
|
||||||
|
CommandArg(
|
||||||
|
name_or_flags="--verbose",
|
||||||
|
help=(
|
||||||
|
"This option allows the command to print additional information during component creation. "
|
||||||
|
"This is an optional argument."
|
||||||
|
),
|
||||||
|
action="store_true",
|
||||||
|
),
|
||||||
|
CommandArg(
|
||||||
|
name_or_flags="--dry-run",
|
||||||
|
help=(
|
||||||
|
"This option allows you to simulate component creation without actually creating any files. "
|
||||||
|
"This is an optional argument. The default value is `False`."
|
||||||
|
),
|
||||||
|
action="store_true",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def handle(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
name = kwargs["name"]
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
raise CommandError("You must specify a component name")
|
||||||
|
|
||||||
|
# TODO_V3 - BASE_DIR should be taken from Components' settings
|
||||||
|
base_dir = getattr(settings, "BASE_DIR", None)
|
||||||
|
|
||||||
|
path = kwargs["path"]
|
||||||
|
js_filename = kwargs["js"]
|
||||||
|
css_filename = kwargs["css"]
|
||||||
|
template_filename = kwargs["template"]
|
||||||
|
force = kwargs["force"]
|
||||||
|
verbose = kwargs["verbose"]
|
||||||
|
dry_run = kwargs["dry_run"]
|
||||||
|
|
||||||
|
if path:
|
||||||
|
component_path = os.path.join(path, name)
|
||||||
|
elif base_dir:
|
||||||
|
component_path = os.path.join(base_dir, "components", name)
|
||||||
|
else:
|
||||||
|
raise CommandError("You must specify a path or set BASE_DIR in your django settings")
|
||||||
|
|
||||||
|
if os.path.exists(component_path):
|
||||||
|
if not force:
|
||||||
|
raise CommandError(
|
||||||
|
f'The component "{name}" already exists at {component_path}. Use --force to overwrite.'
|
||||||
|
)
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
msg = f'The component "{name}" already exists at {component_path}. Overwriting...'
|
||||||
|
else:
|
||||||
|
msg = f'The component "{name}" already exists. Overwriting...'
|
||||||
|
|
||||||
|
sys.stdout.write(style_warning(msg) + "\n")
|
||||||
|
|
||||||
|
if not dry_run:
|
||||||
|
os.makedirs(component_path, exist_ok=force)
|
||||||
|
|
||||||
|
with open(os.path.join(component_path, js_filename), "w") as f:
|
||||||
|
script_content = dedent(
|
||||||
|
f"""
|
||||||
|
window.addEventListener('load', (event) => {{
|
||||||
|
console.log("{name} component is fully loaded");
|
||||||
|
}});
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
f.write(script_content.strip())
|
||||||
|
|
||||||
|
with open(os.path.join(component_path, css_filename), "w") as f:
|
||||||
|
style_content = dedent(
|
||||||
|
f"""
|
||||||
|
.component-{name} {{
|
||||||
|
background: red;
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
f.write(style_content.strip())
|
||||||
|
|
||||||
|
with open(os.path.join(component_path, template_filename), "w") as f:
|
||||||
|
template_content = dedent(
|
||||||
|
f"""
|
||||||
|
<div class="component-{name}">
|
||||||
|
Hello from {name} component!
|
||||||
|
<br>
|
||||||
|
This is {{ param }} context value.
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
f.write(template_content.strip())
|
||||||
|
|
||||||
|
with open(os.path.join(component_path, f"{name}.py"), "w") as f:
|
||||||
|
py_content = dedent(
|
||||||
|
f"""
|
||||||
|
from django_components import Component, register
|
||||||
|
|
||||||
|
@register("{name}")
|
||||||
|
class {name.capitalize()}(Component):
|
||||||
|
template_file = "{name}/{template_filename}"
|
||||||
|
|
||||||
|
def get_context_data(self, value):
|
||||||
|
return {{
|
||||||
|
"param": "sample value",
|
||||||
|
}}
|
||||||
|
|
||||||
|
class Media:
|
||||||
|
css = "{name}/{css_filename}"
|
||||||
|
js = "{name}/{js_filename}"
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
f.write(py_content.strip())
|
||||||
|
|
||||||
|
if verbose:
|
||||||
|
msg = f"Successfully created {name} component at {component_path}"
|
||||||
|
else:
|
||||||
|
msg = f"Successfully created {name} component"
|
||||||
|
sys.stdout.write(style_success(msg) + "\n")
|
22
src/django_components/commands/ext.py
Normal file
22
src/django_components/commands/ext.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
from django_components.commands.ext_list import ExtListCommand
|
||||||
|
from django_components.commands.ext_run import ExtRunCommand
|
||||||
|
from django_components.util.command import ComponentCommand
|
||||||
|
|
||||||
|
|
||||||
|
class ExtCommand(ComponentCommand):
|
||||||
|
"""
|
||||||
|
Run extension commands.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python manage.py components ext list
|
||||||
|
python manage.py components ext run <extension> <command>
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "ext"
|
||||||
|
help = "Run extension commands."
|
||||||
|
|
||||||
|
subcommands = [
|
||||||
|
ExtListCommand,
|
||||||
|
ExtRunCommand,
|
||||||
|
]
|
55
src/django_components/commands/ext_list.py
Normal file
55
src/django_components/commands/ext_list.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django_components.extension import extensions
|
||||||
|
from django_components.util.command import CommandArg, ComponentCommand
|
||||||
|
|
||||||
|
|
||||||
|
class ExtListCommand(ComponentCommand):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "list"
|
||||||
|
help = "List all extensions."
|
||||||
|
|
||||||
|
arguments = [
|
||||||
|
CommandArg(
|
||||||
|
["-v", "--verbosity"],
|
||||||
|
default=1,
|
||||||
|
type=int,
|
||||||
|
choices=[0, 1],
|
||||||
|
help=("Verbosity level; 0=minimal output, 1=normal output"),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def handle(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
if kwargs["verbosity"] > 0:
|
||||||
|
print("Installed extensions:")
|
||||||
|
for extension in extensions.extensions:
|
||||||
|
print(extension.name)
|
113
src/django_components/commands/ext_run.py
Normal file
113
src/django_components/commands/ext_run.py
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
from typing import Any, List, Optional, Type
|
||||||
|
|
||||||
|
from django_components.extension import extensions
|
||||||
|
from django_components.util.command import ComponentCommand
|
||||||
|
|
||||||
|
|
||||||
|
# Scope the extension-specific commands to the extension name, so users call these commands like:
|
||||||
|
# `python manage.py components ext run <extension> <command>`
|
||||||
|
#
|
||||||
|
# We achieve that by creating temporary `ComponentCommand` subclasses for each extension. E.g.
|
||||||
|
# ```python
|
||||||
|
# class ExtCommand(ComponentCommand):
|
||||||
|
# name = "my_ext"
|
||||||
|
# help = "Run commands added by the 'my_ext' extension."
|
||||||
|
# subcommands = [
|
||||||
|
# ...commands
|
||||||
|
# ]
|
||||||
|
# ```
|
||||||
|
def _gen_subcommands() -> List[Type[ComponentCommand]]:
|
||||||
|
commands: List[Type[ComponentCommand]] = []
|
||||||
|
for extension in extensions.extensions:
|
||||||
|
ExtCommand = type(
|
||||||
|
"ExtCommand",
|
||||||
|
(ComponentCommand,),
|
||||||
|
{
|
||||||
|
"name": extension.name,
|
||||||
|
"help": f"Run commands added by the '{extension.name}' extension.",
|
||||||
|
"subcommands": extension.commands,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
commands.append(ExtCommand)
|
||||||
|
|
||||||
|
return commands
|
||||||
|
|
||||||
|
|
||||||
|
# This descriptor generates the list of subcommands of the `components ext run` command dynamically when accessed.
|
||||||
|
# This is because this list depends on the settings and extensions. In tests, the settings and available extensions
|
||||||
|
# may change between each test case. So we have to ensure we access the latest settings when accessing this property.
|
||||||
|
#
|
||||||
|
# NOTE: This is possible, because Django sets up the project and settings BEFORE the commands are loaded.
|
||||||
|
class SubcommandsDescriptor:
|
||||||
|
def __get__(self, obj: Any, objtype: Optional[Type] = None) -> List[Type[ComponentCommand]]:
|
||||||
|
# This will be called when accessing ExtRunCommand.subcommands
|
||||||
|
# or instance.subcommands
|
||||||
|
return _gen_subcommands()
|
||||||
|
|
||||||
|
|
||||||
|
class ExtRunCommand(ComponentCommand):
|
||||||
|
"""
|
||||||
|
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).
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "run"
|
||||||
|
help = "Run a command added by an extension."
|
||||||
|
subcommands = SubcommandsDescriptor() # type: ignore
|
11
src/django_components/commands/startcomponent.py
Normal file
11
src/django_components/commands/startcomponent.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from django_components.commands.create import CreateCommand
|
||||||
|
|
||||||
|
|
||||||
|
# TODO_REMOVE_IN_V1 - Superseded by `components create`
|
||||||
|
class StartComponentCommand(CreateCommand):
|
||||||
|
"""
|
||||||
|
**Deprecated**. Use [`components create`](../commands#components-create) instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "startcomponent"
|
||||||
|
help = "Deprecated. Use `components create` instead."
|
79
src/django_components/commands/upgrade.py
Normal file
79
src/django_components/commands/upgrade.py
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.template.engine import Engine
|
||||||
|
|
||||||
|
from django_components.template_loader import Loader
|
||||||
|
from django_components.util.command import CommandArg, ComponentCommand
|
||||||
|
|
||||||
|
|
||||||
|
# TODO_V1 - Remove, no longer needed?
|
||||||
|
class UpgradeCommand(ComponentCommand):
|
||||||
|
name = "upgrade"
|
||||||
|
help = "Upgrade django components syntax from '{%% component_block ... %%}' to '{%% component ... %%}'."
|
||||||
|
|
||||||
|
arguments = [
|
||||||
|
CommandArg(
|
||||||
|
name_or_flags="--path",
|
||||||
|
help="Path to search for components",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
def handle(self, *args: Any, **options: Any) -> None:
|
||||||
|
current_engine = Engine.get_default()
|
||||||
|
loader = Loader(current_engine)
|
||||||
|
dirs = loader.get_dirs(include_apps=False)
|
||||||
|
|
||||||
|
if settings.BASE_DIR:
|
||||||
|
dirs.append(Path(settings.BASE_DIR) / "templates")
|
||||||
|
|
||||||
|
if options["path"]:
|
||||||
|
dirs = [options["path"]]
|
||||||
|
|
||||||
|
all_files = []
|
||||||
|
|
||||||
|
for dir_path in dirs:
|
||||||
|
print(f"Searching for components in {dir_path}...")
|
||||||
|
for root, _, files in os.walk(dir_path):
|
||||||
|
for file in files:
|
||||||
|
if not file.endswith((".html", ".py")):
|
||||||
|
continue
|
||||||
|
file_path = os.path.join(root, file)
|
||||||
|
all_files.append(file_path)
|
||||||
|
|
||||||
|
for file_path in all_files:
|
||||||
|
with open(file_path, "r+", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
content_with_closed_components, step0_count = re.subn(
|
||||||
|
r'({%\s*component\s*"(\w+?)"(.*?)%})(?!.*?{%\s*endcomponent\s*%})',
|
||||||
|
r"\1{% endcomponent %}",
|
||||||
|
content,
|
||||||
|
flags=re.DOTALL,
|
||||||
|
)
|
||||||
|
updated_content, step1_count_opening = re.subn(
|
||||||
|
r'{%\s*component_block\s*"(\w+?)"\s*(.*?)%}',
|
||||||
|
r'{% component "\1" \2%}',
|
||||||
|
content_with_closed_components,
|
||||||
|
flags=re.DOTALL,
|
||||||
|
)
|
||||||
|
updated_content, step2_count_closing = re.subn(
|
||||||
|
r'{%\s*endcomponent_block\s*"(\w+?)"\s*%}',
|
||||||
|
r"{% endcomponent %}",
|
||||||
|
updated_content,
|
||||||
|
flags=re.DOTALL,
|
||||||
|
)
|
||||||
|
updated_content, step2_count_closing_no_name = re.subn(
|
||||||
|
r"{%\s*endcomponent_block\s*%}",
|
||||||
|
r"{% endcomponent %}",
|
||||||
|
updated_content,
|
||||||
|
flags=re.DOTALL,
|
||||||
|
)
|
||||||
|
total_updates = step0_count + step1_count_opening + step2_count_closing + step2_count_closing_no_name
|
||||||
|
if total_updates > 0:
|
||||||
|
f.seek(0)
|
||||||
|
f.write(updated_content)
|
||||||
|
f.truncate()
|
||||||
|
print(f"Updated {file_path}: {total_updates} changes made")
|
11
src/django_components/commands/upgradecomponent.py
Normal file
11
src/django_components/commands/upgradecomponent.py
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
from django_components.commands.upgrade import UpgradeCommand
|
||||||
|
|
||||||
|
|
||||||
|
# TODO_REMOVE_IN_V1 - No longer needed?
|
||||||
|
class UpgradeComponentCommand(UpgradeCommand):
|
||||||
|
"""
|
||||||
|
**Deprecated**. Use [`components upgrade`](../commands#components-upgrade) instead.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "upgradecomponent"
|
||||||
|
help = "Deprecated. Use `components upgrade` instead."
|
0
src/django_components/compat/__init__.py
Normal file
0
src/django_components/compat/__init__.py
Normal file
116
src/django_components/compat/django.py
Normal file
116
src/django_components/compat/django.py
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
from typing import Any, Optional, Type
|
||||||
|
|
||||||
|
import django
|
||||||
|
from django.core.management.base import BaseCommand as DjangoCommand
|
||||||
|
|
||||||
|
from django_components.util.command import (
|
||||||
|
CommandArg,
|
||||||
|
ComponentCommand,
|
||||||
|
_setup_command_arg,
|
||||||
|
setup_parser_from_command,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Django command arguments added to all commands
|
||||||
|
# NOTE: Many of these MUST be present for the command to work with Django
|
||||||
|
DJANGO_COMMAND_ARGS = [
|
||||||
|
CommandArg(
|
||||||
|
"--version",
|
||||||
|
action="version",
|
||||||
|
version=django.get_version(),
|
||||||
|
help="Show program's version number and exit.",
|
||||||
|
),
|
||||||
|
CommandArg(
|
||||||
|
["-v", "--verbosity"],
|
||||||
|
default=1,
|
||||||
|
type=int,
|
||||||
|
choices=[0, 1, 2, 3],
|
||||||
|
help=("Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, " "3=very verbose output"),
|
||||||
|
),
|
||||||
|
CommandArg(
|
||||||
|
"--settings",
|
||||||
|
help=(
|
||||||
|
"The Python path to a settings module, e.g. "
|
||||||
|
'"myproject.settings.main". If this isn\'t provided, the '
|
||||||
|
"DJANGO_SETTINGS_MODULE environment variable will be used."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CommandArg(
|
||||||
|
"--pythonpath",
|
||||||
|
help=("A directory to add to the Python path, e.g. " '"/home/djangoprojects/myproject".'),
|
||||||
|
),
|
||||||
|
CommandArg(
|
||||||
|
"--traceback",
|
||||||
|
action="store_true",
|
||||||
|
help="Raise on CommandError exceptions.",
|
||||||
|
),
|
||||||
|
CommandArg(
|
||||||
|
"--no-color",
|
||||||
|
action="store_true",
|
||||||
|
help="Don't colorize the command output.",
|
||||||
|
),
|
||||||
|
CommandArg(
|
||||||
|
"--force-color",
|
||||||
|
action="store_true",
|
||||||
|
help="Force colorization of the command output.",
|
||||||
|
),
|
||||||
|
CommandArg(
|
||||||
|
"--skip-checks",
|
||||||
|
action="store_true",
|
||||||
|
help="Skip system checks.",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def load_as_django_command(command: Type[ComponentCommand]) -> Type[DjangoCommand]:
|
||||||
|
"""
|
||||||
|
Create a Django `Command` class from a `ComponentCommand` class.
|
||||||
|
|
||||||
|
The created class can be used as a Django command by placing it in
|
||||||
|
the `management/commands/` module.
|
||||||
|
|
||||||
|
```python
|
||||||
|
# management/commands/mycommand.py
|
||||||
|
from django_components.compat.django import load_as_django_command
|
||||||
|
from myapp.commands.mycommand import MyCommand
|
||||||
|
|
||||||
|
# NOTE: MUST be assigned to a variable named `Command`
|
||||||
|
Command = load_as_django_command(MyCommand)
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Define a Command class that delegates to our command_class
|
||||||
|
class Command(DjangoCommand):
|
||||||
|
help = command.help
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._command = command()
|
||||||
|
|
||||||
|
def create_parser(self, prog_name: str, subcommand: str, **kwargs: Any) -> ArgumentParser:
|
||||||
|
parser = setup_parser_from_command(command)
|
||||||
|
for arg in DJANGO_COMMAND_ARGS:
|
||||||
|
_setup_command_arg(parser, arg.asdict())
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
# This is the entrypoint for the command - After argparser has resolved the args,
|
||||||
|
# this is where we forward the args to the command handler.
|
||||||
|
def handle(self, *args: Any, **options: Any) -> None:
|
||||||
|
# Case: (Sub)command matched and it HAS handler
|
||||||
|
resolved_command: Optional[ComponentCommand] = options.get("_command", None)
|
||||||
|
if resolved_command and resolved_command.handle:
|
||||||
|
resolved_command.handle(*args, **options)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Case: (Sub)command matched and it DOES NOT have handler (e.g. subcommand used for routing)
|
||||||
|
cmd_parser: Optional[ArgumentParser] = options.get("_parser", None)
|
||||||
|
if cmd_parser:
|
||||||
|
cmd_parser.print_help()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Case: (Sub)command did not match - Print help for the main command
|
||||||
|
self.print_help(self._command.name, "")
|
||||||
|
|
||||||
|
Command.__doc__ = command.__doc__
|
||||||
|
|
||||||
|
return Command
|
|
@ -891,17 +891,24 @@ class Component(
|
||||||
"""
|
"""
|
||||||
Shortcut for calling `Component.View.as_view` and passing component instance to it.
|
Shortcut for calling `Component.View.as_view` and passing component instance to it.
|
||||||
"""
|
"""
|
||||||
# This method may be called as class method or as instance method.
|
|
||||||
# If called as class method, create a new instance.
|
|
||||||
if isinstance(cls, Component):
|
|
||||||
comp: Component = cls
|
|
||||||
else:
|
|
||||||
comp = cls()
|
|
||||||
|
|
||||||
# `view` is a built-in extension defined in `extensions.view`. It subclasses
|
# NOTE: `Component.View` may not be available at the time that URLs are being
|
||||||
# from Django's `View` class, and adds the `component` attribute to it.
|
# defined. So we return a view that calls `View.as_view()` only once it's actually called.
|
||||||
view_inst = cast(View, comp.view) # type: ignore[attr-defined]
|
def outer_view(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||||
return view_inst.__class__.as_view(**initkwargs, component=comp)
|
# This method may be called as class method or as instance method.
|
||||||
|
# If called as class method, create a new instance.
|
||||||
|
if isinstance(cls, Component):
|
||||||
|
comp: Component = cls
|
||||||
|
else:
|
||||||
|
comp = cls()
|
||||||
|
|
||||||
|
# `view` is a built-in extension defined in `extensions.view`. It subclasses
|
||||||
|
# from Django's `View` class, and adds the `component` attribute to it.
|
||||||
|
view_cls = cast(View, cls.View) # type: ignore[attr-defined]
|
||||||
|
inner_view = view_cls.as_view(**initkwargs, component=comp)
|
||||||
|
return inner_view(request, *args, **kwargs)
|
||||||
|
|
||||||
|
return outer_view
|
||||||
|
|
||||||
# #####################################
|
# #####################################
|
||||||
# RENDERING
|
# RENDERING
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# NOTE: Components exported here are documented in
|
# NOTE: Components exported here are documented in the API reference
|
||||||
from django_components.components.dynamic import DynamicComponent
|
from django_components.components.dynamic import DynamicComponent
|
||||||
|
|
||||||
__all__ = ["DynamicComponent"]
|
__all__ = ["DynamicComponent"]
|
||||||
|
|
|
@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, List, NamedTuple, Tuple,
|
||||||
from django.template import Context
|
from django.template import Context
|
||||||
|
|
||||||
from django_components.app_settings import app_settings
|
from django_components.app_settings import app_settings
|
||||||
|
from django_components.util.command import ComponentCommand
|
||||||
from django_components.util.misc import snake_to_pascal
|
from django_components.util.misc import snake_to_pascal
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
@ -184,6 +185,65 @@ class ComponentExtension:
|
||||||
This setting decides what the extension class will inherit from.
|
This setting decides what the extension class will inherit from.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
commands: List[Type[ComponentCommand]] = []
|
||||||
|
"""
|
||||||
|
List of commands that can be run by the extension.
|
||||||
|
|
||||||
|
These commands will be available to the user as `components ext run <extension> <command>`.
|
||||||
|
|
||||||
|
Commands are defined as subclasses of [`ComponentCommand`](../api#django_components.ComponentCommand).
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
This example defines an extension with a command that prints "Hello world". To run the command,
|
||||||
|
the user would run `components ext run hello_world hello`.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from django_components import ComponentCommand, ComponentExtension, CommandArg, CommandArgGroup
|
||||||
|
|
||||||
|
class HelloWorldCommand(ComponentCommand):
|
||||||
|
name = "hello"
|
||||||
|
help = "Hello world command."
|
||||||
|
|
||||||
|
# Allow to pass flags `--foo`, `--bar` and `--baz`.
|
||||||
|
# Argument parsing is managed by `argparse`.
|
||||||
|
arguments = [
|
||||||
|
CommandArg(
|
||||||
|
name_or_flags="--foo",
|
||||||
|
help="Foo description.",
|
||||||
|
),
|
||||||
|
# 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.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Callback that receives the parsed arguments and options.
|
||||||
|
def handle(self, *args, **kwargs):
|
||||||
|
print(f"HelloWorldCommand.handle: args={args}, kwargs={kwargs}")
|
||||||
|
|
||||||
|
# Associate the command with the extension
|
||||||
|
class HelloWorldExtension(ComponentExtension):
|
||||||
|
name = "hello_world"
|
||||||
|
|
||||||
|
commands = [
|
||||||
|
HelloWorldCommand,
|
||||||
|
]
|
||||||
|
```
|
||||||
|
"""
|
||||||
|
|
||||||
def __init_subclass__(cls) -> None:
|
def __init_subclass__(cls) -> None:
|
||||||
if not cls.name.isidentifier():
|
if not cls.name.isidentifier():
|
||||||
raise ValueError(f"Extension name must be a valid Python identifier, got {cls.name}")
|
raise ValueError(f"Extension name must be a valid Python identifier, got {cls.name}")
|
||||||
|
@ -529,6 +589,19 @@ class ExtensionManager:
|
||||||
getattr(self, hook)(data)
|
getattr(self, hook)(data)
|
||||||
self._events = []
|
self._events = []
|
||||||
|
|
||||||
|
def get_extension(self, name: str) -> ComponentExtension:
|
||||||
|
for extension in self.extensions:
|
||||||
|
if extension.name == name:
|
||||||
|
return extension
|
||||||
|
raise ValueError(f"Extension {name} not found")
|
||||||
|
|
||||||
|
def get_extension_command(self, name: str, command_name: str) -> Type[ComponentCommand]:
|
||||||
|
extension = self.get_extension(name)
|
||||||
|
for command in extension.commands:
|
||||||
|
if command.name == command_name:
|
||||||
|
return command
|
||||||
|
raise ValueError(f"Command {command_name} not found in extension {name}")
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
# Component lifecycle hooks
|
# Component lifecycle hooks
|
||||||
#############################
|
#############################
|
||||||
|
|
5
src/django_components/management/commands/components.py
Normal file
5
src/django_components/management/commands/components.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
from django_components.commands.components import ComponentsRootCommand
|
||||||
|
from django_components.compat.django import load_as_django_command
|
||||||
|
|
||||||
|
# TODO_V3
|
||||||
|
Command = load_as_django_command(ComponentsRootCommand)
|
|
@ -1,219 +1,5 @@
|
||||||
import os
|
from django_components.commands.startcomponent import StartComponentCommand
|
||||||
from textwrap import dedent
|
from django_components.compat.django import load_as_django_command
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.conf import settings
|
# TODO_V3
|
||||||
from django.core.management.base import BaseCommand, CommandError, CommandParser
|
Command = load_as_django_command(StartComponentCommand)
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
"""
|
|
||||||
### 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.
|
|
||||||
""" # noqa: E501
|
|
||||||
|
|
||||||
help = "Create a new django component."
|
|
||||||
|
|
||||||
def add_arguments(self, parser: CommandParser) -> None:
|
|
||||||
parser.add_argument(
|
|
||||||
"name",
|
|
||||||
type=str,
|
|
||||||
help="The name of the component to create. This is a required argument.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--path",
|
|
||||||
type=str,
|
|
||||||
help=(
|
|
||||||
"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."
|
|
||||||
),
|
|
||||||
default=None,
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--js",
|
|
||||||
type=str,
|
|
||||||
help="The name of the JavaScript file. This is an optional argument. The default value is `script.js`.",
|
|
||||||
default="script.js",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--css",
|
|
||||||
type=str,
|
|
||||||
help="The name of the CSS file. This is an optional argument. The default value is `style.css`.",
|
|
||||||
default="style.css",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--template",
|
|
||||||
type=str,
|
|
||||||
help="The name of the template file. This is an optional argument. The default value is `template.html`.",
|
|
||||||
default="template.html",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--force",
|
|
||||||
action="store_true",
|
|
||||||
help="This option allows you to overwrite existing files if they exist. This is an optional argument.",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--verbose",
|
|
||||||
action="store_true",
|
|
||||||
help=(
|
|
||||||
"This option allows the command to print additional information during component "
|
|
||||||
"creation. This is an optional argument."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--dry-run",
|
|
||||||
action="store_true",
|
|
||||||
help=(
|
|
||||||
"This option allows you to simulate component creation without actually creating any files. "
|
|
||||||
"This is an optional argument. The default value is `False`."
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def handle(self, *args: Any, **kwargs: Any) -> None:
|
|
||||||
name = kwargs["name"]
|
|
||||||
|
|
||||||
if name:
|
|
||||||
path = kwargs["path"]
|
|
||||||
js_filename = kwargs["js"]
|
|
||||||
css_filename = kwargs["css"]
|
|
||||||
template_filename = kwargs["template"]
|
|
||||||
base_dir = getattr(settings, "BASE_DIR", None)
|
|
||||||
force = kwargs["force"]
|
|
||||||
verbose = kwargs["verbose"]
|
|
||||||
dry_run = kwargs["dry_run"]
|
|
||||||
|
|
||||||
if path:
|
|
||||||
component_path = os.path.join(path, name)
|
|
||||||
elif base_dir:
|
|
||||||
component_path = os.path.join(base_dir, "components", name)
|
|
||||||
else:
|
|
||||||
raise CommandError("You must specify a path or set BASE_DIR in your django settings")
|
|
||||||
|
|
||||||
if os.path.exists(component_path):
|
|
||||||
if force:
|
|
||||||
if verbose:
|
|
||||||
self.stdout.write(
|
|
||||||
self.style.WARNING(
|
|
||||||
f'The component "{name}" already exists at {component_path}. Overwriting...'
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
self.stdout.write(self.style.WARNING(f'The component "{name}" already exists. Overwriting...'))
|
|
||||||
else:
|
|
||||||
raise CommandError(
|
|
||||||
f'The component "{name}" already exists at {component_path}. Use --force to overwrite.'
|
|
||||||
)
|
|
||||||
|
|
||||||
if not dry_run:
|
|
||||||
os.makedirs(component_path, exist_ok=force)
|
|
||||||
|
|
||||||
with open(os.path.join(component_path, js_filename), "w") as f:
|
|
||||||
script_content = dedent(
|
|
||||||
f"""
|
|
||||||
window.addEventListener('load', (event) => {{
|
|
||||||
console.log("{name} component is fully loaded");
|
|
||||||
}});
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
f.write(script_content.strip())
|
|
||||||
|
|
||||||
with open(os.path.join(component_path, css_filename), "w") as f:
|
|
||||||
style_content = dedent(
|
|
||||||
f"""
|
|
||||||
.component-{name} {{
|
|
||||||
background: red;
|
|
||||||
}}
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
f.write(style_content.strip())
|
|
||||||
|
|
||||||
with open(os.path.join(component_path, template_filename), "w") as f:
|
|
||||||
template_content = dedent(
|
|
||||||
f"""
|
|
||||||
<div class="component-{name}">
|
|
||||||
Hello from {name} component!
|
|
||||||
<br>
|
|
||||||
This is {{ param }} context value.
|
|
||||||
</div>
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
f.write(template_content.strip())
|
|
||||||
|
|
||||||
with open(os.path.join(component_path, f"{name}.py"), "w") as f:
|
|
||||||
py_content = dedent(
|
|
||||||
f"""
|
|
||||||
from django_components import Component, register
|
|
||||||
|
|
||||||
@register("{name}")
|
|
||||||
class {name.capitalize()}(Component):
|
|
||||||
template_file = "{name}/{template_filename}"
|
|
||||||
|
|
||||||
def get_context_data(self, value):
|
|
||||||
return {{
|
|
||||||
"param": "sample value",
|
|
||||||
}}
|
|
||||||
|
|
||||||
class Media:
|
|
||||||
css = "{name}/{css_filename}"
|
|
||||||
js = "{name}/{js_filename}"
|
|
||||||
"""
|
|
||||||
)
|
|
||||||
f.write(py_content.strip())
|
|
||||||
|
|
||||||
if verbose:
|
|
||||||
self.stdout.write(self.style.SUCCESS(f"Successfully created {name} component at {component_path}"))
|
|
||||||
else:
|
|
||||||
self.stdout.write(self.style.SUCCESS(f"Successfully created {name} component"))
|
|
||||||
else:
|
|
||||||
raise CommandError("You must specify a component name")
|
|
||||||
|
|
|
@ -1,69 +1,5 @@
|
||||||
import os
|
from django_components.commands.upgradecomponent import UpgradeComponentCommand
|
||||||
import re
|
from django_components.compat.django import load_as_django_command
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from django.conf import settings
|
# TODO_V3
|
||||||
from django.core.management.base import BaseCommand, CommandParser
|
Command = load_as_django_command(UpgradeComponentCommand)
|
||||||
from django.template.engine import Engine
|
|
||||||
|
|
||||||
from django_components.template_loader import Loader
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = "Updates component and component_block tags to the new syntax"
|
|
||||||
|
|
||||||
def add_arguments(self, parser: CommandParser) -> None:
|
|
||||||
parser.add_argument("--path", type=str, help="Path to search for components")
|
|
||||||
|
|
||||||
def handle(self, *args: Any, **options: Any) -> None:
|
|
||||||
current_engine = Engine.get_default()
|
|
||||||
loader = Loader(current_engine)
|
|
||||||
dirs = loader.get_dirs(include_apps=False)
|
|
||||||
|
|
||||||
if settings.BASE_DIR:
|
|
||||||
dirs.append(Path(settings.BASE_DIR) / "templates")
|
|
||||||
|
|
||||||
if options["path"]:
|
|
||||||
dirs = [options["path"]]
|
|
||||||
|
|
||||||
for dir_path in dirs:
|
|
||||||
self.stdout.write(f"Searching for components in {dir_path}...")
|
|
||||||
for root, _, files in os.walk(dir_path):
|
|
||||||
for file in files:
|
|
||||||
if file.endswith((".html", ".py")):
|
|
||||||
file_path = os.path.join(root, file)
|
|
||||||
with open(file_path, "r+", encoding="utf-8") as f:
|
|
||||||
content = f.read()
|
|
||||||
content_with_closed_components, step0_count = re.subn(
|
|
||||||
r'({%\s*component\s*"(\w+?)"(.*?)%})(?!.*?{%\s*endcomponent\s*%})',
|
|
||||||
r"\1{% endcomponent %}",
|
|
||||||
content,
|
|
||||||
flags=re.DOTALL,
|
|
||||||
)
|
|
||||||
updated_content, step1_count_opening = re.subn(
|
|
||||||
r'{%\s*component_block\s*"(\w+?)"\s*(.*?)%}',
|
|
||||||
r'{% component "\1" \2%}',
|
|
||||||
content_with_closed_components,
|
|
||||||
flags=re.DOTALL,
|
|
||||||
)
|
|
||||||
updated_content, step2_count_closing = re.subn(
|
|
||||||
r'{%\s*endcomponent_block\s*"(\w+?)"\s*%}',
|
|
||||||
r"{% endcomponent %}",
|
|
||||||
updated_content,
|
|
||||||
flags=re.DOTALL,
|
|
||||||
)
|
|
||||||
updated_content, step2_count_closing_no_name = re.subn(
|
|
||||||
r"{%\s*endcomponent_block\s*%}",
|
|
||||||
r"{% endcomponent %}",
|
|
||||||
updated_content,
|
|
||||||
flags=re.DOTALL,
|
|
||||||
)
|
|
||||||
total_updates = (
|
|
||||||
step0_count + step1_count_opening + step2_count_closing + step2_count_closing_no_name
|
|
||||||
)
|
|
||||||
if total_updates > 0:
|
|
||||||
f.seek(0)
|
|
||||||
f.write(updated_content)
|
|
||||||
f.truncate()
|
|
||||||
self.stdout.write(f"Updated {file_path}: {total_updates} changes made")
|
|
||||||
|
|
407
src/django_components/util/command.py
Normal file
407
src/django_components/util/command.py
Normal file
|
@ -0,0 +1,407 @@
|
||||||
|
import sys
|
||||||
|
from argparse import Action, ArgumentParser
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Protocol, Sequence, Type, Union
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from argparse import _ArgumentGroup, _FormatterClass
|
||||||
|
|
||||||
|
#############################
|
||||||
|
# Argparse typing
|
||||||
|
#############################
|
||||||
|
|
||||||
|
CommandLiteralAction = Literal[
|
||||||
|
"append", "append_const", "count", "extend", "store", "store_const", "store_true", "store_false", "version"
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
The basic type of action to be taken when this argument is encountered at the command line.
|
||||||
|
|
||||||
|
This is a subset of the values for `action` in
|
||||||
|
[`ArgumentParser.add_argument()`](https://docs.python.org/3/library/argparse.html#the-add-argument-method).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CommandArg:
|
||||||
|
"""
|
||||||
|
Define a single positional argument or an option for a command.
|
||||||
|
|
||||||
|
Fields on this class correspond to the arguments for
|
||||||
|
[`ArgumentParser.add_argument()`](https://docs.python.org/3/library/argparse.html#the-add-argument-method)
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
name_or_flags: Union[str, Sequence[str]]
|
||||||
|
"""Either a name or a list of option strings, e.g. 'foo' or '-f', '--foo'."""
|
||||||
|
action: Optional[Union[CommandLiteralAction, Action]] = None
|
||||||
|
"""The basic type of action to be taken when this argument is encountered at the command line."""
|
||||||
|
nargs: Optional[Union[int, Literal["*", "+", "?"]]] = None
|
||||||
|
"""The number of command-line arguments that should be consumed."""
|
||||||
|
const: Any = None
|
||||||
|
"""A constant value required by some action and nargs selections."""
|
||||||
|
default: Any = None
|
||||||
|
"""
|
||||||
|
The value produced if the argument is absent from the command line and if it is absent from the namespace object.
|
||||||
|
"""
|
||||||
|
type: Optional[Union[Type, Callable[[str], Any]]] = None
|
||||||
|
"""The type to which the command-line argument should be converted."""
|
||||||
|
choices: Optional[Sequence[Any]] = None
|
||||||
|
"""A sequence of the allowable values for the argument."""
|
||||||
|
required: Optional[bool] = None
|
||||||
|
"""Whether or not the command-line option may be omitted (optionals only)."""
|
||||||
|
help: Optional[str] = None
|
||||||
|
"""A brief description of what the argument does."""
|
||||||
|
metavar: Optional[str] = None
|
||||||
|
"""A name for the argument in usage messages."""
|
||||||
|
dest: Optional[str] = None
|
||||||
|
"""The name of the attribute to be added to the object returned by parse_args()."""
|
||||||
|
version: Optional[str] = None
|
||||||
|
"""
|
||||||
|
The version string to be added to the object returned by parse_args().
|
||||||
|
|
||||||
|
MUST be used with `action='version'`.
|
||||||
|
|
||||||
|
See https://docs.python.org/3/library/argparse.html#action
|
||||||
|
"""
|
||||||
|
|
||||||
|
# NOTE: Support for deprecated was added in Python 3.13
|
||||||
|
# See https://docs.python.org/3/library/argparse.html#deprecated
|
||||||
|
deprecated: Optional[bool] = None
|
||||||
|
"""
|
||||||
|
Whether or not use of the argument is deprecated.
|
||||||
|
|
||||||
|
NOTE: This is supported only in Python 3.13+
|
||||||
|
"""
|
||||||
|
|
||||||
|
def asdict(self) -> dict:
|
||||||
|
"""Convert the dataclass to a dictionary, stripping out fields with `None` values"""
|
||||||
|
return _remove_none_values(asdict(self))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CommandArgGroup:
|
||||||
|
"""
|
||||||
|
Define a group of arguments for a command.
|
||||||
|
|
||||||
|
Fields on this class correspond to the arguments for
|
||||||
|
[`ArgumentParser.add_argument_group()`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument_group)
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
title: Optional[str] = None
|
||||||
|
"""
|
||||||
|
Title for the argument group in help output; by default “positional arguments” if description is provided,
|
||||||
|
otherwise uses title for positional arguments.
|
||||||
|
"""
|
||||||
|
description: Optional[str] = None
|
||||||
|
"""
|
||||||
|
Description for the argument group in help output, by default None
|
||||||
|
"""
|
||||||
|
arguments: Sequence[CommandArg] = ()
|
||||||
|
|
||||||
|
def asdict(self) -> dict:
|
||||||
|
"""Convert the dataclass to a dictionary, stripping out fields with `None` values"""
|
||||||
|
return _remove_none_values(asdict(self))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CommandSubcommand:
|
||||||
|
"""
|
||||||
|
Define a subcommand for a command.
|
||||||
|
|
||||||
|
Fields on this class correspond to the arguments for
|
||||||
|
[`ArgumentParser.add_subparsers.add_parser()`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_subparsers)
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
title: Optional[str] = None
|
||||||
|
"""
|
||||||
|
Title for the sub-parser group in help output; by default “subcommands” if description is provided,
|
||||||
|
otherwise uses title for positional arguments.
|
||||||
|
"""
|
||||||
|
description: Optional[str] = None
|
||||||
|
"""
|
||||||
|
Description for the sub-parser group in help output, by default `None`.
|
||||||
|
"""
|
||||||
|
prog: Optional[str] = None
|
||||||
|
"""
|
||||||
|
Usage information that will be displayed with sub-command help, by default the name of the program
|
||||||
|
and any positional arguments before the subparser argument.
|
||||||
|
"""
|
||||||
|
parser_class: Optional[Type[ArgumentParser]] = None
|
||||||
|
"""
|
||||||
|
Class which will be used to create sub-parser instances, by default the class of
|
||||||
|
the current parser (e.g. `ArgumentParser`).
|
||||||
|
"""
|
||||||
|
action: Optional[Union[CommandLiteralAction, Action]] = None
|
||||||
|
"""
|
||||||
|
The basic type of action to be taken when this argument is encountered at the command line.
|
||||||
|
"""
|
||||||
|
dest: Optional[str] = None
|
||||||
|
"""
|
||||||
|
Name of the attribute under which sub-command name will be stored; by default `None`
|
||||||
|
and no value is stored.
|
||||||
|
"""
|
||||||
|
required: Optional[bool] = None
|
||||||
|
"""
|
||||||
|
Whether or not a subcommand must be provided, by default `False` (added in 3.7)
|
||||||
|
"""
|
||||||
|
help: Optional[str] = None
|
||||||
|
"""
|
||||||
|
Help for sub-parser group in help output, by default `None`.
|
||||||
|
"""
|
||||||
|
metavar: Optional[str] = None
|
||||||
|
"""
|
||||||
|
String presenting available subcommands in help; by default it is None and
|
||||||
|
presents subcommands in form `{cmd1, cmd2, ..}`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def asdict(self) -> dict:
|
||||||
|
"""Convert the dataclass to a dictionary, stripping out fields with `None` values"""
|
||||||
|
return _remove_none_values(asdict(self))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class CommandParserInput:
|
||||||
|
"""
|
||||||
|
Typing for the input to the
|
||||||
|
[`ArgumentParser`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser)
|
||||||
|
constructor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
prog: Optional[str] = None
|
||||||
|
"""The name of the program (default: `os.path.basename(sys.argv[0])`)"""
|
||||||
|
usage: Optional[str] = None
|
||||||
|
"""The string describing the program usage (default: generated from arguments added to parser)"""
|
||||||
|
description: Optional[str] = None
|
||||||
|
"""Text to display before the argument help (by default, no text)"""
|
||||||
|
epilog: Optional[str] = None
|
||||||
|
"""Text to display after the argument help (by default, no text)"""
|
||||||
|
parents: Optional[Sequence[ArgumentParser]] = None
|
||||||
|
"""A list of ArgumentParser objects whose arguments should also be included"""
|
||||||
|
formatter_class: Optional[Type["_FormatterClass"]] = None
|
||||||
|
"""A class for customizing the help output"""
|
||||||
|
prefix_chars: Optional[str] = None
|
||||||
|
"""The set of characters that prefix optional arguments (default: ‘-‘)"""
|
||||||
|
fromfile_prefix_chars: Optional[str] = None
|
||||||
|
"""The set of characters that prefix files from which additional arguments should be read (default: `None`)"""
|
||||||
|
argument_default: Optional[Any] = None
|
||||||
|
"""The global default value for arguments (default: `None`)"""
|
||||||
|
conflict_handler: Optional[str] = None
|
||||||
|
"""The strategy for resolving conflicting optionals (usually unnecessary)"""
|
||||||
|
add_help: Optional[bool] = None
|
||||||
|
"""Add a -h/--help option to the parser (default: `True`)"""
|
||||||
|
allow_abbrev: Optional[bool] = None
|
||||||
|
"""Allows long options to be abbreviated if the abbreviation is unambiguous. (default: `True`)"""
|
||||||
|
exit_on_error: Optional[bool] = None
|
||||||
|
"""Determines whether or not ArgumentParser exits with error info when an error occurs. (default: `True`)"""
|
||||||
|
|
||||||
|
def asdict(self) -> dict:
|
||||||
|
"""Convert the dataclass to a dictionary, stripping out fields with `None` values"""
|
||||||
|
return _remove_none_values(asdict(self))
|
||||||
|
|
||||||
|
|
||||||
|
#############################
|
||||||
|
# Command logic
|
||||||
|
#############################
|
||||||
|
|
||||||
|
|
||||||
|
class CommandHandler(Protocol):
|
||||||
|
def __call__(self, *args: Any, **kwargs: Any) -> None: ... # noqa: E704
|
||||||
|
|
||||||
|
|
||||||
|
class ComponentCommand:
|
||||||
|
"""
|
||||||
|
Definition of a CLI command.
|
||||||
|
|
||||||
|
This class is based on Python's [`argparse`](https://docs.python.org/3/library/argparse.html)
|
||||||
|
module and Django's [`BaseCommand`](https://docs.djangoproject.com/en/5.1/howto/custom-management-commands/)
|
||||||
|
class. `ComponentCommand` allows you to define:
|
||||||
|
|
||||||
|
- Command name, description, and help text
|
||||||
|
- Arguments and options (e.g. `--name John`)
|
||||||
|
- Group arguments (see [argparse groups](https://docs.python.org/3/library/argparse.html#argument-groups))
|
||||||
|
- Subcommands (e.g. `components ext run my_ext hello`)
|
||||||
|
- Handler behavior
|
||||||
|
|
||||||
|
Each extension can add its own commands, which will be available to run with `components ext run`.
|
||||||
|
|
||||||
|
Extensions use the `ComponentCommand` class to define their commands.
|
||||||
|
|
||||||
|
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).
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
"""The name of the command - this is what is used to call the command"""
|
||||||
|
help: Optional[str] = None
|
||||||
|
"""The help text for the command"""
|
||||||
|
arguments: Sequence[Union[CommandArg, CommandArgGroup]] = ()
|
||||||
|
"""argparse arguments for the command"""
|
||||||
|
subcommands: Sequence[Type["ComponentCommand"]] = ()
|
||||||
|
"""Subcommands for the command"""
|
||||||
|
|
||||||
|
handle: Optional[CommandHandler] = None
|
||||||
|
"""
|
||||||
|
The function that is called when the command is run. If `None`, the command will
|
||||||
|
print the help message.
|
||||||
|
"""
|
||||||
|
|
||||||
|
parser_input: Optional[CommandParserInput] = None
|
||||||
|
"""
|
||||||
|
The input to use when creating the `ArgumentParser` for this command. If `None`,
|
||||||
|
the default values will be used.
|
||||||
|
"""
|
||||||
|
subparser_input: Optional[CommandSubcommand] = None
|
||||||
|
"""
|
||||||
|
The input to use when this command is a subcommand installed with `add_subparser()`.
|
||||||
|
If `None`, the default values will be used.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def setup_parser_from_command(command: Type[ComponentCommand]) -> ArgumentParser:
|
||||||
|
"""
|
||||||
|
Create an `ArgumentParser` instance from a `ComponentCommand`.
|
||||||
|
|
||||||
|
The `ArgumentParser` will:
|
||||||
|
|
||||||
|
- Set help message and command name
|
||||||
|
- Add arguments to the parser
|
||||||
|
- Add subcommands recursively
|
||||||
|
"""
|
||||||
|
parser_kwargs = {}
|
||||||
|
if hasattr(command, "parser_input") and command.parser_input:
|
||||||
|
parser_kwargs = command.parser_input.asdict()
|
||||||
|
|
||||||
|
# NOTE: Command name is always present
|
||||||
|
parser_kwargs["prog"] = command.name
|
||||||
|
if command.help is not None:
|
||||||
|
parser_kwargs["description"] = command.help
|
||||||
|
|
||||||
|
parser = ArgumentParser(**parser_kwargs)
|
||||||
|
|
||||||
|
_setup_parser_from_command(parser, command)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
# Recursively setup the parser and its subcommands
|
||||||
|
def _setup_parser_from_command(
|
||||||
|
parser: ArgumentParser,
|
||||||
|
command: Union[Type[ComponentCommand], Type[ComponentCommand]],
|
||||||
|
) -> ArgumentParser:
|
||||||
|
# Attach the command to the data returned by `parser.parse_args()`, so we know
|
||||||
|
# which command was matched.
|
||||||
|
parser.set_defaults(_command=command(), _parser=parser)
|
||||||
|
|
||||||
|
# Apply arguments to the parser. Arguments may be defined as a group.
|
||||||
|
for arg in command.arguments:
|
||||||
|
if isinstance(arg, CommandArgGroup):
|
||||||
|
group_data = arg.asdict()
|
||||||
|
group_args: List[Dict] = group_data.pop("arguments")
|
||||||
|
arg_group = parser.add_argument_group(**group_data)
|
||||||
|
for group_arg in group_args:
|
||||||
|
# NOTE: Seems that dataclass's `asdict()` calls `asdict()` also on the
|
||||||
|
# nested dataclass fields. Thus we need to apply `_remove_none_values()`
|
||||||
|
# to the nested dataclass fields.
|
||||||
|
group_arg = _remove_none_values(group_arg)
|
||||||
|
|
||||||
|
_setup_command_arg(arg_group, group_arg)
|
||||||
|
else:
|
||||||
|
_setup_command_arg(parser, arg.asdict())
|
||||||
|
|
||||||
|
# Add subcommands to the parser
|
||||||
|
if command.subcommands:
|
||||||
|
subparsers = parser.add_subparsers(title="subcommands")
|
||||||
|
for subcommand in command.subcommands:
|
||||||
|
subparser_data: Dict[str, Any] = {}
|
||||||
|
if getattr(subcommand, "subparser_input", None) and subcommand.subparser_input:
|
||||||
|
subparser_data = subcommand.subparser_input.asdict()
|
||||||
|
|
||||||
|
subparser_data["name"] = subcommand.name
|
||||||
|
if subcommand.help:
|
||||||
|
subparser_data["help"] = subcommand.help
|
||||||
|
subparser_data["description"] = subcommand.help
|
||||||
|
|
||||||
|
subparser = subparsers.add_parser(**subparser_data)
|
||||||
|
_setup_parser_from_command(subparser, subcommand)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def _setup_command_arg(parser: Union[ArgumentParser, "_ArgumentGroup"], arg: dict) -> None:
|
||||||
|
# NOTE: Support for deprecated was added in Python 3.13
|
||||||
|
# See https://docs.python.org/3/library/argparse.html#deprecated
|
||||||
|
if sys.version_info < (3, 13) and "deprecated" in arg:
|
||||||
|
raise ValueError("'deprecated' command argument requires Python 3.13+")
|
||||||
|
|
||||||
|
name_or_flags = arg.pop("name_or_flags")
|
||||||
|
if isinstance(name_or_flags, str):
|
||||||
|
name_or_flags = [name_or_flags]
|
||||||
|
parser.add_argument(*name_or_flags, **arg)
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_none_values(data: dict) -> dict:
|
||||||
|
new_data = {}
|
||||||
|
for key, val in data.items():
|
||||||
|
if val is not None:
|
||||||
|
new_data[key] = val
|
||||||
|
return new_data
|
||||||
|
|
||||||
|
|
||||||
|
def style_success(message: str) -> str:
|
||||||
|
"""Style the message with green text"""
|
||||||
|
return f"\033[92m{message}\033[0m" # Green text
|
||||||
|
|
||||||
|
|
||||||
|
def style_warning(message: str) -> str:
|
||||||
|
"""Style the message with yellow text"""
|
||||||
|
return f"\033[93m{message}\033[0m" # Yellow text
|
66
tests/test_command_components.py
Normal file
66
tests/test_command_components.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
from io import StringIO
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django_components.testing import djc_test
|
||||||
|
from .testutils import setup_test_config
|
||||||
|
|
||||||
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
|
@djc_test
|
||||||
|
class TestComponentCommand:
|
||||||
|
def test_root_command(self):
|
||||||
|
out = StringIO()
|
||||||
|
with patch("sys.stdout", new=out):
|
||||||
|
call_command("components")
|
||||||
|
output = out.getvalue()
|
||||||
|
|
||||||
|
# NOTE: The full output is different in CI and locally, because of different whitespace wrapping
|
||||||
|
# (probably due to different terminal widths). So we check only for parts of the output.
|
||||||
|
#
|
||||||
|
# The full expected output is:
|
||||||
|
# ```
|
||||||
|
# usage: components [-h] [--version] [-v {{0,1,2,3}}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
||||||
|
# [--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||||
|
# {{create,upgrade,ext}} ...
|
||||||
|
#
|
||||||
|
# The entrypoint for the 'components' commands.
|
||||||
|
#
|
||||||
|
# optional arguments:
|
||||||
|
# -h, --help show this help message and exit
|
||||||
|
# --version Show program's version number and exit.
|
||||||
|
# -v {{0,1,2,3}}, --verbosity {{0,1,2,3}}
|
||||||
|
# Verbosity level; 0=minimal output, 1=normal output, 2=verbose output,
|
||||||
|
# 3=very verbose output
|
||||||
|
# --settings SETTINGS The Python path to a settings module, e.g. "myproject.settings.main". If this
|
||||||
|
# isn't provided, the DJANGO_SETTINGS_MODULE environment variable will be used.
|
||||||
|
# --pythonpath PYTHONPATH
|
||||||
|
# A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".
|
||||||
|
# --traceback Raise on CommandError exceptions.
|
||||||
|
# --no-color Don't colorize the command output.
|
||||||
|
# --force-color Force colorization of the command output.
|
||||||
|
# --skip-checks Skip system checks.
|
||||||
|
#
|
||||||
|
# subcommands:
|
||||||
|
# {{create,upgrade,ext}}
|
||||||
|
# create Create a new django component.
|
||||||
|
# upgrade Upgrade django components syntax from '{{% component_block ... %}}' to
|
||||||
|
# '{{% component ... %}}'.
|
||||||
|
# ext Run extension commands.
|
||||||
|
# ```
|
||||||
|
|
||||||
|
assert "usage: components" in output
|
||||||
|
assert "The entrypoint for the 'components' commands." in output
|
||||||
|
assert "-h, --help show this help message and exit" in output
|
||||||
|
assert "--version Show program's version number and exit." in output
|
||||||
|
assert "-v {0,1,2,3}" in output
|
||||||
|
assert "--settings SETTINGS The Python path to a settings module" in output
|
||||||
|
assert "--pythonpath PYTHONPATH" in output
|
||||||
|
assert "--traceback Raise on CommandError exceptions." in output
|
||||||
|
assert "--no-color Don't colorize the command output." in output
|
||||||
|
assert "--force-color Force colorization of the command output." in output
|
||||||
|
assert "--skip-checks Skip system checks." in output
|
||||||
|
assert "create Create a new django component." in output
|
||||||
|
assert "upgrade Upgrade django components syntax" in output
|
||||||
|
assert "ext Run extension commands." in output
|
|
@ -2,15 +2,15 @@ import os
|
||||||
import tempfile
|
import tempfile
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from shutil import rmtree
|
from shutil import rmtree
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
|
|
||||||
from django_components.testing import djc_test
|
from django_components.testing import djc_test
|
||||||
from .testutils import setup_test_config
|
from .testutils import setup_test_config
|
||||||
|
|
||||||
setup_test_config()
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
@djc_test
|
@djc_test
|
||||||
|
@ -19,7 +19,7 @@ class TestCreateComponentCommand:
|
||||||
temp_dir = tempfile.mkdtemp()
|
temp_dir = tempfile.mkdtemp()
|
||||||
|
|
||||||
component_name = "defaultcomponent"
|
component_name = "defaultcomponent"
|
||||||
call_command("startcomponent", component_name, "--path", temp_dir)
|
call_command("components", "create", component_name, "--path", temp_dir)
|
||||||
|
|
||||||
expected_files = [
|
expected_files = [
|
||||||
os.path.join(temp_dir, component_name, "script.js"),
|
os.path.join(temp_dir, component_name, "script.js"),
|
||||||
|
@ -36,7 +36,8 @@ class TestCreateComponentCommand:
|
||||||
|
|
||||||
component_name = "testcomponent"
|
component_name = "testcomponent"
|
||||||
call_command(
|
call_command(
|
||||||
"startcomponent",
|
"components",
|
||||||
|
"create",
|
||||||
component_name,
|
component_name,
|
||||||
"--path",
|
"--path",
|
||||||
temp_dir,
|
temp_dir,
|
||||||
|
@ -65,7 +66,8 @@ class TestCreateComponentCommand:
|
||||||
|
|
||||||
component_name = "dryruncomponent"
|
component_name = "dryruncomponent"
|
||||||
call_command(
|
call_command(
|
||||||
"startcomponent",
|
"components",
|
||||||
|
"create",
|
||||||
component_name,
|
component_name,
|
||||||
"--path",
|
"--path",
|
||||||
temp_dir,
|
temp_dir,
|
||||||
|
@ -88,7 +90,8 @@ class TestCreateComponentCommand:
|
||||||
f.write("hello world")
|
f.write("hello world")
|
||||||
|
|
||||||
call_command(
|
call_command(
|
||||||
"startcomponent",
|
"components",
|
||||||
|
"create",
|
||||||
component_name,
|
component_name,
|
||||||
"--path",
|
"--path",
|
||||||
temp_dir,
|
temp_dir,
|
||||||
|
@ -108,7 +111,7 @@ class TestCreateComponentCommand:
|
||||||
os.makedirs(component_path)
|
os.makedirs(component_path)
|
||||||
|
|
||||||
with pytest.raises(CommandError):
|
with pytest.raises(CommandError):
|
||||||
call_command("startcomponent", component_name, "--path", temp_dir)
|
call_command("components", "create", component_name, "--path", temp_dir)
|
||||||
|
|
||||||
rmtree(temp_dir)
|
rmtree(temp_dir)
|
||||||
|
|
||||||
|
@ -117,15 +120,34 @@ class TestCreateComponentCommand:
|
||||||
|
|
||||||
component_name = "verbosecomponent"
|
component_name = "verbosecomponent"
|
||||||
out = StringIO()
|
out = StringIO()
|
||||||
call_command(
|
with patch("sys.stdout", new=out):
|
||||||
"startcomponent",
|
call_command(
|
||||||
component_name,
|
"components",
|
||||||
"--path",
|
"create",
|
||||||
temp_dir,
|
component_name,
|
||||||
"--verbose",
|
"--path",
|
||||||
stdout=out,
|
temp_dir,
|
||||||
)
|
"--verbose",
|
||||||
|
stdout=out,
|
||||||
|
)
|
||||||
output = out.getvalue()
|
output = out.getvalue()
|
||||||
assert "component at" in output
|
assert "component at" in output
|
||||||
|
|
||||||
rmtree(temp_dir)
|
rmtree(temp_dir)
|
||||||
|
|
||||||
|
# TODO_V1 - REMOVE - deprecated
|
||||||
|
def test_startcomponent(self):
|
||||||
|
temp_dir = tempfile.mkdtemp()
|
||||||
|
|
||||||
|
component_name = "defaultcomponent"
|
||||||
|
call_command("startcomponent", component_name, "--path", temp_dir)
|
||||||
|
|
||||||
|
expected_files = [
|
||||||
|
os.path.join(temp_dir, component_name, "script.js"),
|
||||||
|
os.path.join(temp_dir, component_name, "style.css"),
|
||||||
|
os.path.join(temp_dir, component_name, "template.html"),
|
||||||
|
]
|
||||||
|
for file_path in expected_files:
|
||||||
|
assert os.path.exists(file_path)
|
||||||
|
|
||||||
|
rmtree(temp_dir)
|
241
tests/test_command_ext.py
Normal file
241
tests/test_command_ext.py
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
import sys
|
||||||
|
from io import StringIO
|
||||||
|
from textwrap import dedent
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from django.core.management import call_command
|
||||||
|
from django_components import CommandArg, CommandArgGroup, ComponentCommand, ComponentExtension
|
||||||
|
from django_components.testing import djc_test
|
||||||
|
from .testutils import setup_test_config
|
||||||
|
|
||||||
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
# NOTE: Argparse changed how the optional args are displayed in Python 3.11+
|
||||||
|
if sys.version_info >= (3, 10):
|
||||||
|
OPTIONS_TITLE = "options"
|
||||||
|
else:
|
||||||
|
OPTIONS_TITLE = "optional arguments"
|
||||||
|
|
||||||
|
|
||||||
|
class EmptyExtension(ComponentExtension):
|
||||||
|
name = "empty"
|
||||||
|
|
||||||
|
|
||||||
|
class DummyCommand(ComponentCommand):
|
||||||
|
name = "dummy_cmd"
|
||||||
|
help = "Dummy command description."
|
||||||
|
|
||||||
|
arguments = [
|
||||||
|
CommandArg(
|
||||||
|
name_or_flags="--foo",
|
||||||
|
help="Foo description.",
|
||||||
|
),
|
||||||
|
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, *args, **kwargs):
|
||||||
|
kwargs.pop("_command")
|
||||||
|
kwargs.pop("_parser")
|
||||||
|
sorted_kwargs = dict(sorted(kwargs.items()))
|
||||||
|
print(f"DummyCommand.handle: args={args}, kwargs={sorted_kwargs}")
|
||||||
|
|
||||||
|
|
||||||
|
class DummyExtension(ComponentExtension):
|
||||||
|
name = "dummy"
|
||||||
|
|
||||||
|
commands = [
|
||||||
|
DummyCommand,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@djc_test
|
||||||
|
class TestExtensionsCommand:
|
||||||
|
def test_root_command(self):
|
||||||
|
out = StringIO()
|
||||||
|
with patch("sys.stdout", new=out):
|
||||||
|
call_command("components", "ext")
|
||||||
|
output = out.getvalue()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
output
|
||||||
|
== dedent(
|
||||||
|
f"""
|
||||||
|
usage: components ext [-h] {{list,run}} ...
|
||||||
|
|
||||||
|
Run extension commands.
|
||||||
|
|
||||||
|
{OPTIONS_TITLE}:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
|
||||||
|
subcommands:
|
||||||
|
{{list,run}}
|
||||||
|
list List all extensions.
|
||||||
|
run Run a command added by an extension.
|
||||||
|
"""
|
||||||
|
).lstrip()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@djc_test
|
||||||
|
class TestExtensionsListCommand:
|
||||||
|
def test_list_command_default(self):
|
||||||
|
out = StringIO()
|
||||||
|
with patch("sys.stdout", new=out):
|
||||||
|
call_command("components", "ext", "list")
|
||||||
|
output = out.getvalue()
|
||||||
|
|
||||||
|
assert "Installed extensions:\nview\n" == output
|
||||||
|
|
||||||
|
# Check that it omits the title when verbose is 0
|
||||||
|
out = StringIO()
|
||||||
|
with patch("sys.stdout", new=out):
|
||||||
|
call_command("components", "ext", "list", "-v", "0")
|
||||||
|
output = out.getvalue()
|
||||||
|
|
||||||
|
assert "view\n" == output
|
||||||
|
|
||||||
|
@djc_test(
|
||||||
|
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||||
|
)
|
||||||
|
def test_list_command_extra(self):
|
||||||
|
out = StringIO()
|
||||||
|
with patch("sys.stdout", new=out):
|
||||||
|
call_command("components", "ext", "list")
|
||||||
|
output = out.getvalue()
|
||||||
|
|
||||||
|
assert "Installed extensions:\nview\nempty\ndummy\n" == output
|
||||||
|
|
||||||
|
# Check that it omits the title when verbose is 0
|
||||||
|
out = StringIO()
|
||||||
|
with patch("sys.stdout", new=out):
|
||||||
|
call_command("components", "ext", "list", "-v", "0")
|
||||||
|
output = out.getvalue()
|
||||||
|
|
||||||
|
assert "view\nempty\ndummy\n" == output
|
||||||
|
|
||||||
|
|
||||||
|
@djc_test
|
||||||
|
class TestExtensionsRunCommand:
|
||||||
|
@djc_test(
|
||||||
|
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||||
|
)
|
||||||
|
def test_run_command_root(self):
|
||||||
|
out = StringIO()
|
||||||
|
with patch("sys.stdout", new=out):
|
||||||
|
call_command("components", "ext", "run")
|
||||||
|
output = out.getvalue()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
output
|
||||||
|
== dedent(
|
||||||
|
f"""
|
||||||
|
usage: components ext run [-h] {{view,empty,dummy}} ...
|
||||||
|
|
||||||
|
Run a command added by an extension.
|
||||||
|
|
||||||
|
{OPTIONS_TITLE}:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
|
||||||
|
subcommands:
|
||||||
|
{{view,empty,dummy}}
|
||||||
|
view Run commands added by the 'view' extension.
|
||||||
|
empty Run commands added by the 'empty' extension.
|
||||||
|
dummy Run commands added by the 'dummy' extension.
|
||||||
|
"""
|
||||||
|
).lstrip()
|
||||||
|
)
|
||||||
|
|
||||||
|
@djc_test(
|
||||||
|
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||||
|
)
|
||||||
|
def test_run_command_ext_empty(self):
|
||||||
|
out = StringIO()
|
||||||
|
with patch("sys.stdout", new=out):
|
||||||
|
call_command("components", "ext", "run", "empty")
|
||||||
|
output = out.getvalue()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
output
|
||||||
|
== dedent(
|
||||||
|
f"""
|
||||||
|
usage: components ext run empty [-h]
|
||||||
|
|
||||||
|
Run commands added by the 'empty' extension.
|
||||||
|
|
||||||
|
{OPTIONS_TITLE}:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
"""
|
||||||
|
).lstrip()
|
||||||
|
)
|
||||||
|
|
||||||
|
@djc_test(
|
||||||
|
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||||
|
)
|
||||||
|
def test_run_command_ext_with_commands(self):
|
||||||
|
out = StringIO()
|
||||||
|
with patch("sys.stdout", new=out):
|
||||||
|
call_command("components", "ext", "run", "dummy")
|
||||||
|
output = out.getvalue()
|
||||||
|
|
||||||
|
assert (
|
||||||
|
output
|
||||||
|
== dedent(
|
||||||
|
f"""
|
||||||
|
usage: components ext run dummy [-h] {{dummy_cmd}} ...
|
||||||
|
|
||||||
|
Run commands added by the 'dummy' extension.
|
||||||
|
|
||||||
|
{OPTIONS_TITLE}:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
|
||||||
|
subcommands:
|
||||||
|
{{dummy_cmd}}
|
||||||
|
dummy_cmd Dummy command description.
|
||||||
|
"""
|
||||||
|
).lstrip()
|
||||||
|
)
|
||||||
|
|
||||||
|
@djc_test(
|
||||||
|
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||||
|
)
|
||||||
|
def test_run_command_ext_command(self):
|
||||||
|
out = StringIO()
|
||||||
|
with patch("sys.stdout", new=out):
|
||||||
|
call_command("components", "ext", "run", "dummy", "dummy_cmd")
|
||||||
|
output = out.getvalue()
|
||||||
|
|
||||||
|
# NOTE: The dummy command prints out the kwargs, which is what we check for here
|
||||||
|
assert (
|
||||||
|
output
|
||||||
|
== dedent(
|
||||||
|
"""
|
||||||
|
DummyCommand.handle: args=(), kwargs={'bar': None, 'baz': None, 'foo': None, 'force_color': False, 'no_color': False, 'pythonpath': None, 'settings': None, 'skip_checks': True, 'traceback': False, 'verbosity': 1}
|
||||||
|
""" # noqa: E501
|
||||||
|
).lstrip()
|
||||||
|
)
|
||||||
|
|
||||||
|
@djc_test(
|
||||||
|
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||||
|
)
|
||||||
|
def test_prints_error_if_command_not_found(self):
|
||||||
|
out = StringIO()
|
||||||
|
with patch("sys.stderr", new=out):
|
||||||
|
try:
|
||||||
|
call_command("components", "ext", "run", "dummy", "dummy_cmd_not_found")
|
||||||
|
except SystemExit:
|
||||||
|
output = out.getvalue()
|
||||||
|
|
||||||
|
assert "invalid choice: 'dummy_cmd_not_found'" in output
|
Loading…
Add table
Add a link
Reference in a new issue