mirror of
https://github.com/django-components/django-components.git
synced 2025-07-07 17:34:59 +00:00
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:
parent
107284f474
commit
0f41a62592
11 changed files with 647 additions and 63 deletions
|
@ -4,14 +4,9 @@
|
|||
|
||||
### <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 -->
|
||||
> ⚠️ 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` is a modular and extensible UI framework for Django.
|
||||
|
||||
`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.
|
||||
|
||||
With `django-components` you can support Django projects small and large without leaving the Django ecosystem.
|
||||
|
|
|
@ -387,7 +387,8 @@ python manage.py components ext run my_ext hello
|
|||
|
||||
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
|
||||
- `hello` - is the command name
|
||||
|
||||
|
|
|
@ -9,13 +9,13 @@ that will be added by installing `django_components`:
|
|||
## ` components`
|
||||
|
||||
```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 ... %}'.
|
||||
- [`ext`](../commands#components-`ext`)
|
||||
- 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.
|
||||
|
||||
```bash
|
||||
python manage.py components list
|
||||
python manage.py components start <name>
|
||||
python manage.py components upgrade <name>
|
||||
python manage.py components ext list
|
||||
|
@ -51,9 +54,7 @@ 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
|
||||
usage: python manage.py components create [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose] [--dry-run] name
|
||||
|
||||
```
|
||||
|
||||
|
@ -214,7 +215,7 @@ python manage.py components ext run <extension> <command>
|
|||
## `components ext list`
|
||||
|
||||
```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`
|
||||
- show this help message and exit
|
||||
- `-v {0,1}`, `--verbosity {0,1}`
|
||||
- Verbosity level; 0=minimal output, 1=normal output
|
||||
- `--all`
|
||||
- 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:
|
||||
|
||||
```txt
|
||||
Installed extensions:
|
||||
name
|
||||
==============
|
||||
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:
|
||||
To specify which columns to show, use the `--columns` flag:
|
||||
|
||||
```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:
|
||||
|
@ -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).
|
||||
|
||||
|
||||
## `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`
|
||||
|
||||
```txt
|
||||
usage: upgradecomponent [-h] [--path PATH] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
|
||||
[--skip-checks]
|
||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||
|
||||
```
|
||||
|
||||
|
@ -397,10 +506,9 @@ Deprecated. Use `components upgrade` instead.
|
|||
## `startcomponent`
|
||||
|
||||
```txt
|
||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force]
|
||||
[--verbose] [--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS]
|
||||
[--pythonpath PYTHONPATH] [--traceback] [--no-color] [--force-color]
|
||||
[--skip-checks]
|
||||
usage: startcomponent [-h] [--path PATH] [--js JS] [--css CSS] [--template TEMPLATE] [--force] [--verbose]
|
||||
[--dry-run] [--version] [-v {0,1,2,3}] [--settings SETTINGS] [--pythonpath PYTHONPATH]
|
||||
[--traceback] [--no-color] [--force-color] [--skip-checks]
|
||||
name
|
||||
|
||||
```
|
||||
|
|
|
@ -88,6 +88,7 @@ exclude = [
|
|||
'build',
|
||||
]
|
||||
per-file-ignores = [
|
||||
'tests/test_command_list.py:E501',
|
||||
'tests/test_component_media.py:E501',
|
||||
'tests/test_dependency_rendering.py:E501',
|
||||
]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from django_components.commands.create import CreateCommand
|
||||
from django_components.commands.ext import ExtCommand
|
||||
from django_components.commands.list import ComponentListCommand
|
||||
from django_components.commands.upgrade import UpgradeCommand
|
||||
from django_components.util.command import ComponentCommand
|
||||
|
||||
|
@ -12,6 +13,7 @@ class ComponentsRootCommand(ComponentCommand):
|
|||
The entrypoint for the "components" commands.
|
||||
|
||||
```bash
|
||||
python manage.py components list
|
||||
python manage.py components start <name>
|
||||
python manage.py components upgrade <name>
|
||||
python manage.py components ext list
|
||||
|
@ -26,4 +28,5 @@ class ComponentsRootCommand(ComponentCommand):
|
|||
CreateCommand,
|
||||
UpgradeCommand,
|
||||
ExtCommand,
|
||||
ComponentListCommand,
|
||||
]
|
||||
|
|
|
@ -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.util.command import CommandArg, ComponentCommand
|
||||
|
||||
|
||||
class ExtListCommand(ComponentCommand):
|
||||
class ExtListCommand(ListCommand):
|
||||
"""
|
||||
List all extensions.
|
||||
|
||||
|
@ -15,16 +15,38 @@ class ExtListCommand(ComponentCommand):
|
|||
Prints the list of installed extensions:
|
||||
|
||||
```txt
|
||||
Installed extensions:
|
||||
name
|
||||
==============
|
||||
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:
|
||||
To specify which columns to show, use the `--columns` flag:
|
||||
|
||||
```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:
|
||||
|
@ -38,18 +60,15 @@ class ExtListCommand(ComponentCommand):
|
|||
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"),
|
||||
),
|
||||
]
|
||||
columns = ["name"]
|
||||
default_columns = ["name"]
|
||||
|
||||
def handle(self, *args: Any, **kwargs: Any) -> None:
|
||||
if kwargs["verbosity"] > 0:
|
||||
print("Installed extensions:")
|
||||
def get_data(self) -> List[Dict[str, Any]]:
|
||||
data: List[Dict[str, Any]] = []
|
||||
for extension in extensions.extensions:
|
||||
print(extension.name)
|
||||
data.append(
|
||||
{
|
||||
"name": extension.name,
|
||||
}
|
||||
)
|
||||
return data
|
||||
|
|
|
@ -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.
|
||||
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
|
||||
# or instance.subcommands
|
||||
return _gen_subcommands()
|
||||
|
|
222
src/django_components/commands/list.py
Normal file
222
src/django_components/commands/list.py
Normal 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
|
|
@ -1,6 +1,9 @@
|
|||
import re
|
||||
import sys
|
||||
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
|
||||
|
||||
|
@ -59,6 +62,31 @@ def get_import_path(cls_or_fn: Type[Any]) -> str:
|
|||
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:
|
||||
return val if val is not None else default
|
||||
|
||||
|
|
|
@ -91,40 +91,57 @@ class TestExtensionsCommand:
|
|||
|
||||
@djc_test
|
||||
class TestExtensionsListCommand:
|
||||
def test_list_command_default(self):
|
||||
def test_list_default_extensions(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
|
||||
assert output.strip() == "name\n====\nview"
|
||||
|
||||
@djc_test(
|
||||
components_settings={"extensions": [EmptyExtension, DummyExtension]},
|
||||
)
|
||||
def test_list_command_extra(self):
|
||||
def test_list_extra_extensions(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
|
||||
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()
|
||||
with patch("sys.stdout", new=out):
|
||||
call_command("components", "ext", "list", "-v", "0")
|
||||
call_command("components", "ext", "list", "--all")
|
||||
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
|
||||
|
|
190
tests/test_command_list.py
Normal file
190
tests/test_command_list.py
Normal 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)
|
Loading…
Add table
Add a link
Reference in a new issue