feat: add a command to list all components (#1041)

* feat: add a command to list all components

* refactor: fix tests

* refactor: fix linter errors

* refactor: fix the tests for tests running within tox

* temp: print out test outputs

* refactor: fix tests for windows

* refactor: remove escape from slash?

* refactor: fixes to regex

* refactor: remove print statements

* docs: update API reference
This commit is contained in:
Juro Oravec 2025-03-19 09:38:25 +01:00 committed by GitHub
parent 107284f474
commit 0f41a62592
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 647 additions and 63 deletions

View file

@ -4,14 +4,9 @@
### <table><td>[Read the full documentation](https://django-components.github.io/django-components/latest/)</td></table> ### <table><td>[Read the full documentation](https://django-components.github.io/django-components/latest/)</td></table>
<!-- TODO - Remove this banner after a month(?), so March 2025 --> `django-components` is a modular and extensible UI framework for Django.
> ⚠️ Attention ⚠️ - We migrated from `EmilStenstrom/django-components` to `django-components/django-components`.
>
> **Repo name and documentation URL changed. Package name remains the same.**
>
> Report any broken links links in [#922](https://github.com/django-components/django-components/issues/922).
`django-components` combines Django's templating system with the modularity seen It combines Django's templating system with the modularity seen
in modern frontend frameworks like Vue or React. in modern frontend frameworks like Vue or React.
With `django-components` you can support Django projects small and large without leaving the Django ecosystem. With `django-components` you can support Django projects small and large without leaving the Django ecosystem.

View file

@ -387,7 +387,8 @@ python manage.py components ext run my_ext hello
Where: Where:
- `python manage.py components ext run` - is the Django command run - `python manage.py components` - is the Django entrypoint
- `ext run` - is the subcommand to run extension commands
- `my_ext` - is the extension name - `my_ext` - is the extension name
- `hello` - is the command name - `hello` - is the command name

View file

@ -9,13 +9,13 @@ that will be added by installing `django_components`:
## ` components` ## ` components`
```txt ```txt
usage: python manage.py components [-h] {create,upgrade,ext} ... usage: python manage.py components [-h] {create,upgrade,ext,list} ...
``` ```
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/commands/components.py#L9" target="_blank">See source code</a> <a href="https://github.com/django-components/django-components/tree/master/src/django_components/commands/components.py#L10" target="_blank">See source code</a>
@ -34,6 +34,8 @@ The entrypoint for the 'components' commands.
- Upgrade django components syntax from '{% component_block ... %}' to '{% component ... %}'. - Upgrade django components syntax from '{% component_block ... %}' to '{% component ... %}'.
- [`ext`](../commands#components-`ext`) - [`ext`](../commands#components-`ext`)
- Run extension commands. - Run extension commands.
- [`list`](../commands#components-`list`)
- List all components created in this project.
@ -41,6 +43,7 @@ The entrypoint for the 'components' commands.
The entrypoint for the "components" commands. The entrypoint for the "components" commands.
```bash ```bash
python manage.py components list
python manage.py components start <name> python manage.py components start <name>
python manage.py components upgrade <name> python manage.py components upgrade <name>
python manage.py components ext list python manage.py components ext list
@ -51,9 +54,7 @@ python manage.py components ext run <extension> <command>
## `components create` ## `components create`
```txt ```txt
usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run] name
[--dry-run]
name
``` ```
@ -214,7 +215,7 @@ python manage.py components ext run <extension> <command>
## `components ext list` ## `components ext list`
```txt ```txt
usage: python manage.py components ext list [-h] [-v {0,1}] usage: python manage.py components ext list [-h] [--all] [--columns COLUMNS] [-s]
``` ```
@ -230,8 +231,12 @@ List all extensions.
- `-h`, `--help` - `-h`, `--help`
- show this help message and exit - show this help message and exit
- `-v {0,1}`, `--verbosity {0,1}` - `--all`
- Verbosity level; 0=minimal output, 1=normal output - Show all columns. Same as `--columns name`.
- `--columns COLUMNS`
- Comma-separated list of columns to show. Available columns: name. Defaults to `--columns name`.
- `-s`, `--simple`
- Only show table data, without headers. Use this option for generating machine-readable output.
@ -245,16 +250,38 @@ python manage.py components ext list
Prints the list of installed extensions: Prints the list of installed extensions:
```txt ```txt
Installed extensions: name
==============
view view
my_extension my_extension
``` ```
If you need to omit the title in order to programmatically post-process the output, To specify which columns to show, use the `--columns` flag:
you can use the `--verbosity` (or `-v`) flag:
```bash ```bash
python manage.py components ext list -v 0 python manage.py components ext list --columns name
```
Which prints:
```txt
name
==============
view
my_extension
```
To print out all columns, use the `--all` flag:
```bash
python manage.py components ext list --all
```
If you need to omit the title in order to programmatically post-process the output,
you can use the `--simple` (or `-s`) flag:
```bash
python manage.py components ext list --simple
``` ```
Which prints just: Which prints just:
@ -348,12 +375,94 @@ python manage.py components ext run my_ext hello --name John --shout
For more information, see the [argparse documentation](https://docs.python.org/3/library/argparse.html). For more information, see the [argparse documentation](https://docs.python.org/3/library/argparse.html).
## `components list`
```txt
usage: python manage.py components list [-h] [--all] [--columns COLUMNS] [-s]
```
<a href="https://github.com/django-components/django-components/tree/master/src/django_components/commands/list.py#L136" target="_blank">See source code</a>
List all components created in this project.
**Options:**
- `-h`, `--help`
- show this help message and exit
- `--all`
- Show all columns. Same as `--columns name,full_name,path`.
- `--columns COLUMNS`
- Comma-separated list of columns to show. Available columns: name, full_name, path. Defaults to `--columns full_name,path`.
- `-s`, `--simple`
- Only show table data, without headers. Use this option for generating machine-readable output.
List all components.
```bash
python manage.py components list
```
Prints the list of available components:
```txt
full_name path
==================================================================================================
project.pages.project.ProjectPage ./project/pages/project
project.components.dashboard.ProjectDashboard ./project/components/dashboard
project.components.dashboard_action.ProjectDashboardAction ./project/components/dashboard_action
```
To specify which columns to show, use the `--columns` flag:
```bash
python manage.py components list --columns name,full_name,path
```
Which prints:
```txt
name full_name path
==================================================================================================
ProjectPage project.pages.project.ProjectPage ./project/pages/project
ProjectDashboard project.components.dashboard.ProjectDashboard ./project/components/dashboard
ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAction ./project/components/dashboard_action
```
To print out all columns, use the `--all` flag:
```bash
python manage.py components list --all
```
If you need to omit the title in order to programmatically post-process the output,
you can use the `--simple` (or `-s`) flag:
```bash
python manage.py components list --simple
```
Which prints just:
```txt
ProjectPage project.pages.project.ProjectPage ./project/pages/project
ProjectDashboard project.components.dashboard.ProjectDashboard ./project/components/dashboard
ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAction ./project/components/dashboard_action
```
## `upgradecomponent` ## `upgradecomponent`
```txt ```txt
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS] usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks]
[--skip-checks]
``` ```
@ -397,10 +506,9 @@ Deprecated. Use `components upgrade` instead.
## `startcomponent` ## `startcomponent`
```txt ```txt
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
[--verbose] [--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--traceback] [--no-color] [--force-color] [--skip-checks]
[--skip-checks]
name name
``` ```

View file

@ -88,6 +88,7 @@ exclude = [
'build', 'build',
] ]
per-file-ignores = [ per-file-ignores = [
'tests/test_command_list.py:E501',
'tests/test_component_media.py:E501', 'tests/test_component_media.py:E501',
'tests/test_dependency_rendering.py:E501', 'tests/test_dependency_rendering.py:E501',
] ]

View file

@ -1,5 +1,6 @@
from django_components.commands.create import CreateCommand from django_components.commands.create import CreateCommand
from django_components.commands.ext import ExtCommand from django_components.commands.ext import ExtCommand
from django_components.commands.list import ComponentListCommand
from django_components.commands.upgrade import UpgradeCommand from django_components.commands.upgrade import UpgradeCommand
from django_components.util.command import ComponentCommand from django_components.util.command import ComponentCommand
@ -12,6 +13,7 @@ class ComponentsRootCommand(ComponentCommand):
The entrypoint for the "components" commands. The entrypoint for the "components" commands.
```bash ```bash
python manage.py components list
python manage.py components start <name> python manage.py components start <name>
python manage.py components upgrade <name> python manage.py components upgrade <name>
python manage.py components ext list python manage.py components ext list
@ -26,4 +28,5 @@ class ComponentsRootCommand(ComponentCommand):
CreateCommand, CreateCommand,
UpgradeCommand, UpgradeCommand,
ExtCommand, ExtCommand,
ComponentListCommand,
] ]

View file

@ -1,10 +1,10 @@
from typing import Any from typing import Any, Dict, List
from django_components.commands.list import ListCommand
from django_components.extension import extensions from django_components.extension import extensions
from django_components.util.command import CommandArg, ComponentCommand
class ExtListCommand(ComponentCommand): class ExtListCommand(ListCommand):
""" """
List all extensions. List all extensions.
@ -15,16 +15,38 @@ class ExtListCommand(ComponentCommand):
Prints the list of installed extensions: Prints the list of installed extensions:
```txt ```txt
Installed extensions: name
==============
view view
my_extension my_extension
``` ```
If you need to omit the title in order to programmatically post-process the output, To specify which columns to show, use the `--columns` flag:
you can use the `--verbosity` (or `-v`) flag:
```bash ```bash
python manage.py components ext list -v 0 python manage.py components ext list --columns name
```
Which prints:
```txt
name
==============
view
my_extension
```
To print out all columns, use the `--all` flag:
```bash
python manage.py components ext list --all
```
If you need to omit the title in order to programmatically post-process the output,
you can use the `--simple` (or `-s`) flag:
```bash
python manage.py components ext list --simple
``` ```
Which prints just: Which prints just:
@ -38,18 +60,15 @@ class ExtListCommand(ComponentCommand):
name = "list" name = "list"
help = "List all extensions." help = "List all extensions."
arguments = [ columns = ["name"]
CommandArg( default_columns = ["name"]
["-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: def get_data(self) -> List[Dict[str, Any]]:
if kwargs["verbosity"] > 0: data: List[Dict[str, Any]] = []
print("Installed extensions:")
for extension in extensions.extensions: for extension in extensions.extensions:
print(extension.name) data.append(
{
"name": extension.name,
}
)
return data

View file

@ -40,7 +40,7 @@ def _gen_subcommands() -> List[Type[ComponentCommand]]:
# #
# NOTE: This is possible, because Django sets up the project and settings BEFORE the commands are loaded. # NOTE: This is possible, because Django sets up the project and settings BEFORE the commands are loaded.
class SubcommandsDescriptor: class SubcommandsDescriptor:
def __get__(self, obj: Any, objtype: Optional[Type] = None) -> List[Type[ComponentCommand]]: def __get__(self, obj: Optional[Any], objtype: Type) -> List[Type[ComponentCommand]]:
# This will be called when accessing ExtRunCommand.subcommands # This will be called when accessing ExtRunCommand.subcommands
# or instance.subcommands # or instance.subcommands
return _gen_subcommands() return _gen_subcommands()

View file

@ -0,0 +1,222 @@
import os
from typing import Any, Dict, List, Optional, Type
from django_components.component import all_components
from django_components.util.command import CommandArg, ComponentCommand
from django_components.util.misc import get_import_path, get_module_info
# This descriptor generates the list of command arguments (e.g. `--all`), such that
# their descriptions are dynamically generated based on the Command class.
class ListArgumentsDescriptor:
# This will be called when accessing `ListCommand.arguments`
def __get__(self, obj: Optional["ListCommand"], cls: Type["ListCommand"]) -> List[CommandArg]:
command = obj or cls
all_cols = command.columns
default_cols = command.default_columns
all_cols_input = ",".join(all_cols)
all_cols_readable = ", ".join(all_cols)
default_cols_input = ",".join(default_cols)
return [
CommandArg(
["--all"],
action="store_true",
# Format as e.g. `--columns name,full_name,path`
help=f"Show all columns. Same as `--columns {all_cols_input}`.",
),
CommandArg(
["--columns"],
help=(
f"Comma-separated list of columns to show. Available columns: {all_cols_readable}. "
f"Defaults to `--columns {default_cols_input}`."
),
),
CommandArg(
["-s", "--simple"],
action="store_true",
help="Only show table data, without headers. Use this option for generating machine-readable output.",
),
]
# Common base class for all `list` commands, so they all have the same arguments
# and same formatting.
class ListCommand(ComponentCommand):
####################
# SUBCLASS API
####################
columns: List[str]
default_columns: List[str]
def get_data(self) -> List[Dict[str, Any]]:
return []
####################
# INTERNAL
####################
arguments = ListArgumentsDescriptor() # type: ignore[assignment]
def handle(self, *args: Any, **kwargs: Any) -> None:
"""
This runs when the "list" command is called. This handler delegates to subclasses
to define how to get the data with the `get_data` method and formats the results
as an ASCII table.
"""
if kwargs["all"] and kwargs["columns"]:
raise ValueError("Cannot use --all and --columns together.")
if kwargs["all"]:
columns = self.columns
elif kwargs["columns"]:
columns = kwargs["columns"].split(",")
for column in columns:
if column not in self.columns:
raise ValueError(f"Invalid column: {column}")
else:
# Default columns
columns = self.default_columns
data = self.get_data()
include_headers = not kwargs.get("simple", False)
table = format_as_ascii_table(data, columns, include_headers=include_headers)
print(table)
def format_as_ascii_table(data: List[Dict[str, Any]], headers: List[str], include_headers: bool = True) -> str:
"""
Format a list of dictionaries as an ASCII table.
Example:
```python
data = [
{"name": "ProjectPage", "full_name": "project.pages.project.ProjectPage", "path": "./project/pages/project"},
{"name": "ProjectDashboard", "full_name": "project.components.dashboard.ProjectDashboard", "path": "./project/components/dashboard"},
{"name": "ProjectDashboardAction", "full_name": "project.components.dashboard_action.ProjectDashboardAction", "path": "./project/components/dashboard_action"},
]
headers = ["name", "full_name", "path"]
print(format_as_ascii_table(data, headers))
```
Which prints:
```txt
name full_name path
==================================================================================================
ProjectPage project.pages.project.ProjectPage ./project/pages/project
ProjectDashboard project.components.dashboard.ProjectDashboard ./project/components/dashboard
ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAction ./project/components/dashboard_action
```
""" # noqa: E501
# Calculate the width of each column
column_widths = {header: len(header) for header in headers}
for row in data:
for header in headers:
row_value = str(row.get(header, ""))
column_widths[header] = max(column_widths[header], len(row_value))
# Create the header row
header_row = " ".join(f"{header:<{column_widths[header]}}" for header in headers)
separator = "=" * len(header_row)
# Create the data rows
data_rows = []
for row in data:
row_values = [str(row.get(header, "")) for header in headers]
data_row = " ".join(f"{value:<{column_widths[header]}}" for value, header in zip(row_values, headers))
data_rows.append(data_row)
# Combine all parts into the final table
if include_headers:
table = "\n".join([header_row, separator] + data_rows)
else:
table = "\n".join(data_rows)
return table
class ComponentListCommand(ListCommand):
"""
List all components.
```bash
python manage.py components list
```
Prints the list of available components:
```txt
full_name path
==================================================================================================
project.pages.project.ProjectPage ./project/pages/project
project.components.dashboard.ProjectDashboard ./project/components/dashboard
project.components.dashboard_action.ProjectDashboardAction ./project/components/dashboard_action
```
To specify which columns to show, use the `--columns` flag:
```bash
python manage.py components list --columns name,full_name,path
```
Which prints:
```txt
name full_name path
==================================================================================================
ProjectPage project.pages.project.ProjectPage ./project/pages/project
ProjectDashboard project.components.dashboard.ProjectDashboard ./project/components/dashboard
ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAction ./project/components/dashboard_action
```
To print out all columns, use the `--all` flag:
```bash
python manage.py components list --all
```
If you need to omit the title in order to programmatically post-process the output,
you can use the `--simple` (or `-s`) flag:
```bash
python manage.py components list --simple
```
Which prints just:
```txt
ProjectPage project.pages.project.ProjectPage ./project/pages/project
ProjectDashboard project.components.dashboard.ProjectDashboard ./project/components/dashboard
ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAction ./project/components/dashboard_action
```
""" # noqa: E501
name = "list"
help = "List all components created in this project."
columns = ["name", "full_name", "path"]
default_columns = ["full_name", "path"]
def get_data(self) -> List[Dict[str, Any]]:
components = all_components()
data: List[Dict[str, Any]] = []
for component in components:
full_name = get_import_path(component)
module, module_name, module_file_path = get_module_info(component)
# Make paths relative to CWD
if module_file_path:
module_file_path = os.path.relpath(module_file_path, os.getcwd())
data.append(
{
"name": component.__name__,
"full_name": full_name,
"path": module_file_path,
}
)
return data

View file

@ -1,6 +1,9 @@
import re import re
import sys
from hashlib import md5 from hashlib import md5
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Type, TypeVar from importlib import import_module
from types import ModuleType
from typing import TYPE_CHECKING, Any, Callable, List, Optional, Tuple, Type, TypeVar, Union
from django_components.util.nanoid import generate from django_components.util.nanoid import generate
@ -59,6 +62,31 @@ def get_import_path(cls_or_fn: Type[Any]) -> str:
return module + "." + cls_or_fn.__qualname__ return module + "." + cls_or_fn.__qualname__
def get_module_info(
cls_or_fn: Union[Type[Any], Callable[..., Any]],
) -> Tuple[Optional[ModuleType], Optional[str], Optional[str]]:
"""Get the module, module name and module file path where the class or function is defined."""
module_name: Optional[str] = getattr(cls_or_fn, "__module__", None)
if module_name:
if module_name in sys.modules:
module = sys.modules[module_name]
else:
try:
module = import_module(module_name)
except Exception:
module = None
else:
module = None
if module:
module_file_path: Optional[str] = getattr(module, "__file__", None)
else:
module_file_path = None
return module, module_name, module_file_path
def default(val: Optional[T], default: T) -> T: def default(val: Optional[T], default: T) -> T:
return val if val is not None else default return val if val is not None else default

View file

@ -91,40 +91,57 @@ class TestExtensionsCommand:
@djc_test @djc_test
class TestExtensionsListCommand: class TestExtensionsListCommand:
def test_list_command_default(self): def test_list_default_extensions(self):
out = StringIO() out = StringIO()
with patch("sys.stdout", new=out): with patch("sys.stdout", new=out):
call_command("components", "ext", "list") call_command("components", "ext", "list")
output = out.getvalue() output = out.getvalue()
assert "Installed extensions:\nview\n" == output assert output.strip() == "name\n====\nview"
# 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( @djc_test(
components_settings={"extensions": [EmptyExtension, DummyExtension]}, components_settings={"extensions": [EmptyExtension, DummyExtension]},
) )
def test_list_command_extra(self): def test_list_extra_extensions(self):
out = StringIO() out = StringIO()
with patch("sys.stdout", new=out): with patch("sys.stdout", new=out):
call_command("components", "ext", "list") call_command("components", "ext", "list")
output = out.getvalue() output = out.getvalue()
assert "Installed extensions:\nview\nempty\ndummy\n" == output assert output.strip() == "name \n=====\nview \nempty\ndummy"
# Check that it omits the title when verbose is 0 @djc_test(
components_settings={"extensions": [EmptyExtension, DummyExtension]},
)
def test_list_all(self):
out = StringIO() out = StringIO()
with patch("sys.stdout", new=out): with patch("sys.stdout", new=out):
call_command("components", "ext", "list", "-v", "0") call_command("components", "ext", "list", "--all")
output = out.getvalue() output = out.getvalue()
assert "view\nempty\ndummy\n" == output assert output.strip() == "name \n=====\nview \nempty\ndummy"
@djc_test(
components_settings={"extensions": [EmptyExtension, DummyExtension]},
)
def test_list_specific_columns(self):
out = StringIO()
with patch("sys.stdout", new=out):
call_command("components", "ext", "list", "--columns", "name")
output = out.getvalue()
assert output.strip() == "name \n=====\nview \nempty\ndummy"
@djc_test(
components_settings={"extensions": [EmptyExtension, DummyExtension]},
)
def test_list_simple(self):
out = StringIO()
with patch("sys.stdout", new=out):
call_command("components", "ext", "list", "--simple")
output = out.getvalue()
assert output.strip() == "view \nempty\ndummy"
@djc_test @djc_test

190
tests/test_command_list.py Normal file
View file

@ -0,0 +1,190 @@
import re
from io import StringIO
from unittest.mock import patch
from django.core.management import call_command
from django_components import Component
from django_components.testing import djc_test
from .testutils import setup_test_config
setup_test_config({"autodiscover": False})
# Either back or forward slash
SLASH = r"[\\/]"
@djc_test
class TestComponentListCommand:
def test_list_default(self):
class TestComponent(Component):
template = ""
out = StringIO()
with patch("sys.stdout", new=out):
call_command("components", "list")
output = out.getvalue()
# NOTE: When we run all tests, the output is different, as other test files define components
# outside of the `@djc_test` decorator, and thus they leak into the output. Since this affects also
# the formatting (how much whitespace there is), regex is used to check for the headers and the expected
# components.
#
# The output should look like this:
#
# full_name path
# ======================================================================================================================================
# django_components.components.dynamic.DynamicComponent src/django_components/components/dynamic.py
# tests.test_command_list.TestComponentListCommand.test_list_default.<locals>.TestComponent tests/test_command_list.py
# Check first line of output
assert re.compile(
# full_name path
r"full_name\s+path\s+"
).search(output.strip().split("\n")[0])
# Check that the output contains the built-in component
assert re.compile(
# django_components.components.dynamic.DynamicComponent src/django_components/components/dynamic.py
# or
# django_components.components.dynamic.DynamicComponent .tox/py311/lib/python3.11/site-packages/django_components/components/dynamic.py
r"django_components\.components\.dynamic\.DynamicComponent\s+[\w/\\.-]+django_components{SLASH}components{SLASH}dynamic\.py".format(
SLASH=SLASH
)
).search(output)
# Check that the output contains the test component
assert re.compile(
# tests.test_command_list.TestComponentListCommand.test_list_default.<locals>.TestComponent tests/test_command_list.py
r"tests\.test_command_list\.TestComponentListCommand\.test_list_default\.<locals>\.TestComponent\s+tests{SLASH}test_command_list\.py".format(
SLASH=SLASH
)
).search(output)
def test_list_all(self):
class TestComponent(Component):
template = ""
out = StringIO()
with patch("sys.stdout", new=out):
call_command("components", "list", "--all")
output = out.getvalue()
# NOTE: When we run all tests, the output is different, as other test files define components
# outside of the `@djc_test` decorator, and thus they leak into the output. Since this affects also
# the formatting (how much whitespace there is), regex is used to check for the headers and the expected
# components.
#
# The output should look like this:
#
# name full_name path
# ====================================================================================================================================================
# DynamicComponent django_components.components.dynamic.DynamicComponent src/django_components/components/dynamic.py
# TestComponent tests.test_command_list.TestComponentListCommand.test_list_all.<locals>.TestComponent tests/test_command_list.py
# Check first line of output
assert re.compile(
# name full_name path
r"name\s+full_name\s+path\s+"
).search(output.strip().split("\n")[0])
# Check that the output contains the built-in component
assert re.compile(
# DynamicComponent django_components.components.dynamic.DynamicComponent src/django_components/components/dynamic.py
# or
# DynamicComponent django_components.components.dynamic.DynamicComponent .tox/py311/lib/python3.11/site-packages/django_components/components/dynamic.py
r"DynamicComponent\s+django_components\.components\.dynamic\.DynamicComponent\s+[\w/\\.-]+django_components{SLASH}components{SLASH}dynamic\.py".format(
SLASH=SLASH
)
).search(output)
# Check that the output contains the test component
assert re.compile(
# TestComponent tests.test_command_list.TestComponentListCommand.test_list_all.<locals>.TestComponent tests/test_command_list.py
r"TestComponent\s+tests\.test_command_list\.TestComponentListCommand\.test_list_all\.<locals>\.TestComponent\s+tests{SLASH}test_command_list\.py".format(
SLASH=SLASH
)
).search(output)
def test_list_specific_columns(self):
class TestComponent(Component):
template = ""
out = StringIO()
with patch("sys.stdout", new=out):
call_command("components", "list", "--columns", "name,full_name")
output = out.getvalue()
# NOTE: When we run all tests, the output is different, as other test files define components
# outside of the `@djc_test` decorator, and thus they leak into the output. Since this affects also
# the formatting (how much whitespace there is), regex is used to check for the headers and the expected
# components.
#
# The output should look like this:
#
# name full_name
# ====================================================================================================================
# DynamicComponent django_components.components.dynamic.DynamicComponent
# TestComponent tests.test_command_list.TestComponentListCommand.test_list_specific_columns.<locals>.TestComponent
# Check first line of output
assert re.compile(
# name full_name
r"name\s+full_name"
).search(output.strip().split("\n")[0])
# Check that the output contains the built-in component
assert re.compile(
# DynamicComponent django_components.components.dynamic.DynamicComponent
r"DynamicComponent\s+django_components\.components\.dynamic\.DynamicComponent"
).search(output)
# Check that the output contains the test component
assert re.compile(
# TestComponent tests.test_command_list.TestComponentListCommand.test_list_specific_columns.<locals>.TestComponent
r"TestComponent\s+tests\.test_command_list\.TestComponentListCommand\.test_list_specific_columns\.<locals>\.TestComponent"
).search(output)
def test_list_simple(self):
class TestComponent(Component):
template = ""
out = StringIO()
with patch("sys.stdout", new=out):
call_command("components", "list", "--simple")
output = out.getvalue()
# NOTE: When we run all tests, the output is different, as other test files define components
# outside of the `@djc_test` decorator, and thus they leak into the output. Since this affects also
# the formatting (how much whitespace there is), regex is used to check for the headers and the expected
# components.
#
# The output should look like this:
#
# django_components.components.dynamic.DynamicComponent src/django_components/components/dynamic.py
# tests.test_command_list.TestComponentListCommand.test_list_simple.<locals>.TestComponent tests/test_command_list.py
# Check first line of output is omitted
assert re.compile(
# full_name path
r"full_name\s+path\s+"
).search(output.strip().split("\n")[0]) is None
# Check that the output contains the built-in component
assert re.compile(
# django_components.components.dynamic.DynamicComponent src/django_components/components/dynamic.py
# or
# django_components.components.dynamic.DynamicComponent .tox/py311/lib/python3.11/site-packages/django_components/components/dynamic.py
r"django_components\.components\.dynamic\.DynamicComponent\s+[\w/\\.-]+django_components{SLASH}components{SLASH}dynamic\.py".format(
SLASH=SLASH
)
).search(output)
# Check that the output contains the test component
assert re.compile(
# tests.test_command_list.TestComponentListCommand.test_list_simple.<locals>.TestComponent tests/test_command_list.py
r"tests\.test_command_list\.TestComponentListCommand\.test_list_simple\.<locals>\.TestComponent\s+tests{SLASH}test_command_list\.py".format(
SLASH=SLASH
)
).search(output)