mirror of
https://github.com/django-components/django-components.git
synced 2025-08-31 11:17:21 +00:00
refactor: Prepare autodiscover and template loader for v1 (#533)
This commit is contained in:
parent
b1bd430a07
commit
8cb88558f0
32 changed files with 643 additions and 552 deletions
167
README.md
167
README.md
|
@ -46,6 +46,14 @@ And this is what gets rendered (plus the CSS and Javascript you've specified):
|
||||||
|
|
||||||
## Release notes
|
## Release notes
|
||||||
|
|
||||||
|
🚨📢 **Version 0.85** Autodiscovery module resolution changed. Following undocumented behavior was removed:
|
||||||
|
- Previously, autodiscovery also imported any `[app]/components.py` files, and used `SETTINGS_MODULE` to search for component dirs.
|
||||||
|
- To migrate from:
|
||||||
|
- `[app]/components.py` - Define each module in `COMPONENTS.libraries` setting,
|
||||||
|
or import each module inside the `AppConfig.ready()` hook in respective `apps.py` files.
|
||||||
|
- `SETTINGS_MODULE` - Define component dirs using `STATICFILES_DIRS`
|
||||||
|
- Previously, autodiscovery handled relative files in `STATICFILES_DIRS`. To align with Django, `STATICFILES_DIRS` now must be full paths ([Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#std-setting-STATICFILES_DIRS)).
|
||||||
|
|
||||||
🚨📢 **Version 0.81** Aligned the `render_to_response` method with the (now public) `render` method of `Component` class. Moreover, slots passed to these can now be rendered also as functions.
|
🚨📢 **Version 0.81** Aligned the `render_to_response` method with the (now public) `render` method of `Component` class. Moreover, slots passed to these can now be rendered also as functions.
|
||||||
|
|
||||||
- BREAKING CHANGE: The order of arguments to `render_to_response` has changed.
|
- BREAKING CHANGE: The order of arguments to `render_to_response` has changed.
|
||||||
|
@ -135,51 +143,62 @@ For a step-by-step guide on deploying production server with static files,
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Install the app into your environment:
|
1. Install the app into your environment:
|
||||||
|
|
||||||
> `pip install django_components`
|
> `pip install django_components`
|
||||||
|
|
||||||
Then add the app into INSTALLED_APPS in settings.py
|
2. Then add the app into `INSTALLED_APPS` in settings.py
|
||||||
|
|
||||||
```python
|
```python
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
...,
|
...,
|
||||||
'django_components',
|
'django_components',
|
||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
Modify `TEMPLATES` section of settings.py as follows:
|
3. Ensure that `BASE_DIR` setting is defined in settings.py:
|
||||||
- *Remove `'APP_DIRS': True,`*
|
|
||||||
- add `loaders` to `OPTIONS` list and set it to following value:
|
|
||||||
|
|
||||||
```python
|
```py
|
||||||
TEMPLATES = [
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
{
|
```
|
||||||
...,
|
|
||||||
'OPTIONS': {
|
|
||||||
'context_processors': [
|
|
||||||
...
|
|
||||||
],
|
|
||||||
'loaders':[(
|
|
||||||
'django.template.loaders.cached.Loader', [
|
|
||||||
'django.template.loaders.filesystem.Loader',
|
|
||||||
'django.template.loaders.app_directories.Loader',
|
|
||||||
'django_components.template_loader.Loader',
|
|
||||||
]
|
|
||||||
)],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
Modify STATICFILES_DIRS (or add it if you don't have it) so django can find your static JS and CSS files:
|
4. Modify `TEMPLATES` section of settings.py as follows:
|
||||||
|
- *Remove `'APP_DIRS': True,`*
|
||||||
|
- Add `loaders` to `OPTIONS` list and set it to following value:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
STATICFILES_DIRS = [
|
TEMPLATES = [
|
||||||
...,
|
{
|
||||||
os.path.join(BASE_DIR, "components"),
|
...,
|
||||||
]
|
'OPTIONS': {
|
||||||
```
|
'context_processors': [
|
||||||
|
...
|
||||||
|
],
|
||||||
|
'loaders':[(
|
||||||
|
'django.template.loaders.cached.Loader', [
|
||||||
|
'django.template.loaders.filesystem.Loader',
|
||||||
|
'django.template.loaders.app_directories.Loader',
|
||||||
|
'django_components.template_loader.Loader',
|
||||||
|
]
|
||||||
|
)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
5. Modify `STATICFILES_DIRS` (or add it if you don't have it) so django can find your static JS and CSS files:
|
||||||
|
|
||||||
|
```python
|
||||||
|
STATICFILES_DIRS = [
|
||||||
|
...,
|
||||||
|
os.path.join(BASE_DIR, "components"),
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
If `STATICFILES_DIRS` is omitted or empty, django-components will by default look for
|
||||||
|
`{BASE_DIR}/components`
|
||||||
|
|
||||||
|
NOTE: The paths in `STATICFILES_DIRS` must be full paths. [See Django docs](https://docs.djangoproject.com/en/5.0/ref/settings/#staticfiles-dirs).
|
||||||
|
|
||||||
### Optional
|
### Optional
|
||||||
|
|
||||||
|
@ -623,7 +642,37 @@ If you're planning on passing an HTML string, check Django's use of [`format_htm
|
||||||
|
|
||||||
## Autodiscovery
|
## Autodiscovery
|
||||||
|
|
||||||
By default, the Python files in the `components` app are auto-imported in order to auto-register the components (e.g. `components/button/button.py`).
|
Every component that you want to use in the template with the `{% component %}` tag needs to be registered with the ComponentRegistry. Normally, we use the `@component.register` decorator for that:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django_components import component
|
||||||
|
|
||||||
|
@component.register("calendar")
|
||||||
|
class Calendar(component.Component):
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
But for the component to be registered, the code needs to be executed - the file needs to be imported as a module.
|
||||||
|
|
||||||
|
One way to do that is by importing all your components in `apps.py`:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django.apps import AppConfig
|
||||||
|
|
||||||
|
class MyAppConfig(AppConfig):
|
||||||
|
name = "my_app"
|
||||||
|
|
||||||
|
def ready(self) -> None:
|
||||||
|
from components.card.card import Card
|
||||||
|
from components.list.list import List
|
||||||
|
from components.menu.menu import Menu
|
||||||
|
from components.button.button import Button
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
However, there's a simpler way!
|
||||||
|
|
||||||
|
By default, the Python files in the `STATICFILES_DIRS` directories are auto-imported in order to auto-register the components.
|
||||||
|
|
||||||
Autodiscovery occurs when Django is loaded, during the `ready` hook of the `apps.py` file.
|
Autodiscovery occurs when Django is loaded, during the `ready` hook of the `apps.py` file.
|
||||||
|
|
||||||
|
@ -633,7 +682,17 @@ If you are using autodiscovery, keep a few points in mind:
|
||||||
- Components inside the auto-imported files still need to be registered with `@component.register()`
|
- Components inside the auto-imported files still need to be registered with `@component.register()`
|
||||||
- Auto-imported component files must be valid Python modules, they must use suffix `.py`, and module name should follow [PEP-8](https://peps.python.org/pep-0008/#package-and-module-names).
|
- Auto-imported component files must be valid Python modules, they must use suffix `.py`, and module name should follow [PEP-8](https://peps.python.org/pep-0008/#package-and-module-names).
|
||||||
|
|
||||||
Autodiscovery can be disabled via in the [settings](#disable-autodiscovery).
|
Autodiscovery can be disabled in the [settings](#disable-autodiscovery).
|
||||||
|
|
||||||
|
### Manually trigger autodiscovery
|
||||||
|
|
||||||
|
Autodiscovery can be also triggered manually as a function call. This is useful if you want to run autodiscovery at a custom point of the lifecycle:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django_components import autodiscover
|
||||||
|
|
||||||
|
autodiscover()
|
||||||
|
```
|
||||||
|
|
||||||
## Using slots in templates
|
## Using slots in templates
|
||||||
|
|
||||||
|
@ -1959,6 +2018,34 @@ COMPONENTS = {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Where `mysite/components/forms.py` may look like this:
|
||||||
|
|
||||||
|
```py
|
||||||
|
@component.register("form_simple")
|
||||||
|
class FormSimple(component.Component):
|
||||||
|
template = """
|
||||||
|
<form>
|
||||||
|
...
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
|
||||||
|
@component.register("form_other")
|
||||||
|
class FormOther(component.Component):
|
||||||
|
template = """
|
||||||
|
<form>
|
||||||
|
...
|
||||||
|
</form>
|
||||||
|
"""
|
||||||
|
```
|
||||||
|
|
||||||
|
In the rare cases when you need to manually trigger the import of libraries, you can use the `import_libraries` function:
|
||||||
|
|
||||||
|
```py
|
||||||
|
from django_components import import_libraries
|
||||||
|
|
||||||
|
import_libraries()
|
||||||
|
```
|
||||||
|
|
||||||
### Disable autodiscovery
|
### Disable autodiscovery
|
||||||
|
|
||||||
If you specify all the component locations with the setting above and have a lot of apps, you can (very) slightly speed things up by disabling autodiscovery.
|
If you specify all the component locations with the setting above and have a lot of apps, you can (very) slightly speed things up by disabling autodiscovery.
|
||||||
|
|
|
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "django_components"
|
name = "django_components"
|
||||||
version = "0.84"
|
version = "0.85"
|
||||||
requires-python = ">=3.8, <4.0"
|
requires-python = ">=3.8, <4.0"
|
||||||
description = "A way to create simple reusable template components in Django."
|
description = "A way to create simple reusable template components in Django."
|
||||||
keywords = ["django", "components", "css", "js", "html"]
|
keywords = ["django", "components", "css", "js", "html"]
|
||||||
|
|
|
@ -1,76 +1,7 @@
|
||||||
import importlib
|
|
||||||
import importlib.util
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Callable, List, Optional
|
|
||||||
|
|
||||||
import django
|
import django
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.module_loading import autodiscover_modules
|
|
||||||
|
|
||||||
from django_components.logger import logger
|
from django_components.autodiscover import autodiscover as autodiscover # NOQA
|
||||||
from django_components.utils import search
|
from django_components.autodiscover import import_libraries as import_libraries # NOQA
|
||||||
|
|
||||||
if django.VERSION < (3, 2):
|
if django.VERSION < (3, 2):
|
||||||
default_app_config = "django_components.apps.ComponentsConfig"
|
default_app_config = "django_components.apps.ComponentsConfig"
|
||||||
|
|
||||||
|
|
||||||
def autodiscover(map_import_paths: Optional[Callable[[str], str]] = None) -> List[str]:
|
|
||||||
"""
|
|
||||||
Search for component files and import them. Returns a list of module
|
|
||||||
paths of imported files.
|
|
||||||
"""
|
|
||||||
from django_components.app_settings import app_settings
|
|
||||||
|
|
||||||
imported_modules: List[str] = []
|
|
||||||
|
|
||||||
if app_settings.AUTODISCOVER:
|
|
||||||
# Autodetect a components.py file in each app directory
|
|
||||||
autodiscover_modules("components")
|
|
||||||
|
|
||||||
# Autodetect a <component>.py file in a components dir
|
|
||||||
component_filepaths = search(search_glob="**/*.py").matched_files
|
|
||||||
logger.debug(f"Autodiscover found {len(component_filepaths)} files in component directories.")
|
|
||||||
|
|
||||||
for path in component_filepaths:
|
|
||||||
module_name = _filepath_to_python_module(path)
|
|
||||||
if map_import_paths:
|
|
||||||
module_name = map_import_paths(module_name)
|
|
||||||
|
|
||||||
# This imports the file and runs it's code. So if the file defines any
|
|
||||||
# django components, they will be registered.
|
|
||||||
logger.debug(f'Importing module "{module_name}" (derived from path "{path}")')
|
|
||||||
importlib.import_module(module_name)
|
|
||||||
imported_modules.append(module_name)
|
|
||||||
|
|
||||||
for path_lib in app_settings.LIBRARIES:
|
|
||||||
importlib.import_module(path_lib)
|
|
||||||
|
|
||||||
return imported_modules
|
|
||||||
|
|
||||||
|
|
||||||
def _filepath_to_python_module(file_path: Path) -> str:
|
|
||||||
"""
|
|
||||||
Derive python import path from the filesystem path.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
- If project root is `/path/to/project`
|
|
||||||
- And file_path is `/path/to/project/app/components/mycomp.py`
|
|
||||||
- Then the path relative to project root is `app/components/mycomp.py`
|
|
||||||
- Which we then turn into python import path `app.components.mycomp`
|
|
||||||
"""
|
|
||||||
if hasattr(settings, "BASE_DIR"):
|
|
||||||
project_root = str(settings.BASE_DIR)
|
|
||||||
else:
|
|
||||||
# Fallback for getting the root dir, see https://stackoverflow.com/a/16413955/9788634
|
|
||||||
project_root = os.path.abspath(os.path.dirname(__name__))
|
|
||||||
|
|
||||||
rel_path = os.path.relpath(file_path, start=project_root)
|
|
||||||
rel_path_without_suffix = str(Path(rel_path).with_suffix(""))
|
|
||||||
|
|
||||||
# NOTE: Path normalizes paths to use `/` as separator, while os.path
|
|
||||||
# uses `os.path.sep`.
|
|
||||||
sep = os.path.sep if os.path.sep in rel_path_without_suffix else "/"
|
|
||||||
module_name = rel_path_without_suffix.replace(sep, ".")
|
|
||||||
|
|
||||||
return module_name
|
|
||||||
|
|
|
@ -4,5 +4,14 @@ from django.apps import AppConfig
|
||||||
class ComponentsConfig(AppConfig):
|
class ComponentsConfig(AppConfig):
|
||||||
name = "django_components"
|
name = "django_components"
|
||||||
|
|
||||||
|
# This is the code that gets run when user adds django_components
|
||||||
|
# to Django's INSTALLED_APPS
|
||||||
def ready(self) -> None:
|
def ready(self) -> None:
|
||||||
self.module.autodiscover()
|
from django_components.app_settings import app_settings
|
||||||
|
from django_components.autodiscover import autodiscover, import_libraries
|
||||||
|
|
||||||
|
# Import modules set in `COMPONENTS.libraries` setting
|
||||||
|
import_libraries()
|
||||||
|
|
||||||
|
if app_settings.AUTODISCOVER:
|
||||||
|
autodiscover()
|
||||||
|
|
115
src/django_components/autodiscover.py
Normal file
115
src/django_components/autodiscover.py
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
import glob
|
||||||
|
import importlib
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable, List, Optional, Union
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.template.engine import Engine
|
||||||
|
|
||||||
|
from django_components.logger import logger
|
||||||
|
from django_components.template_loader import Loader
|
||||||
|
|
||||||
|
|
||||||
|
def autodiscover(
|
||||||
|
map_module: Optional[Callable[[str], str]] = None,
|
||||||
|
) -> List[str]:
|
||||||
|
"""
|
||||||
|
Search for component files and import them. Returns a list of module
|
||||||
|
paths of imported files.
|
||||||
|
|
||||||
|
Autodiscover searches in the locations as defined by `Loader.get_dirs`.
|
||||||
|
|
||||||
|
You can map the module paths with `map_module` function. This serves
|
||||||
|
as an escape hatch for when you need to use this function in tests.
|
||||||
|
"""
|
||||||
|
dirs = get_dirs()
|
||||||
|
component_filepaths = search_dirs(dirs, "**/*.py")
|
||||||
|
logger.debug(f"Autodiscover found {len(component_filepaths)} files in component directories.")
|
||||||
|
|
||||||
|
modules = [_filepath_to_python_module(filepath) for filepath in component_filepaths]
|
||||||
|
return _import_modules(modules, map_module)
|
||||||
|
|
||||||
|
|
||||||
|
def import_libraries(
|
||||||
|
map_module: Optional[Callable[[str], str]] = None,
|
||||||
|
) -> List[str]:
|
||||||
|
"""
|
||||||
|
Import modules set in `COMPONENTS.libraries` setting.
|
||||||
|
|
||||||
|
You can map the module paths with `map_module` function. This serves
|
||||||
|
as an escape hatch for when you need to use this function in tests.
|
||||||
|
"""
|
||||||
|
from django_components.app_settings import app_settings
|
||||||
|
|
||||||
|
return _import_modules(app_settings.LIBRARIES, map_module)
|
||||||
|
|
||||||
|
|
||||||
|
def _import_modules(
|
||||||
|
modules: List[str],
|
||||||
|
map_module: Optional[Callable[[str], str]] = None,
|
||||||
|
) -> List[str]:
|
||||||
|
imported_modules: List[str] = []
|
||||||
|
for module_name in modules:
|
||||||
|
if map_module:
|
||||||
|
module_name = map_module(module_name)
|
||||||
|
|
||||||
|
# This imports the file and runs it's code. So if the file defines any
|
||||||
|
# django components, they will be registered.
|
||||||
|
logger.debug(f'Importing module "{module_name}"')
|
||||||
|
importlib.import_module(module_name)
|
||||||
|
imported_modules.append(module_name)
|
||||||
|
return imported_modules
|
||||||
|
|
||||||
|
|
||||||
|
def _filepath_to_python_module(file_path: Union[Path, str]) -> str:
|
||||||
|
"""
|
||||||
|
Derive python import path from the filesystem path.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
- If project root is `/path/to/project`
|
||||||
|
- And file_path is `/path/to/project/app/components/mycomp.py`
|
||||||
|
- Then the path relative to project root is `app/components/mycomp.py`
|
||||||
|
- Which we then turn into python import path `app.components.mycomp`
|
||||||
|
"""
|
||||||
|
if hasattr(settings, "BASE_DIR") and settings.BASE_DIR:
|
||||||
|
project_root = str(settings.BASE_DIR)
|
||||||
|
else:
|
||||||
|
# Fallback for getting the root dir, see https://stackoverflow.com/a/16413955/9788634
|
||||||
|
project_root = os.path.abspath(os.path.dirname(__name__))
|
||||||
|
|
||||||
|
rel_path = os.path.relpath(file_path, start=project_root)
|
||||||
|
rel_path_without_suffix = str(Path(rel_path).with_suffix(""))
|
||||||
|
|
||||||
|
# NOTE: `Path` normalizes paths to use `/` as separator, while `os.path`
|
||||||
|
# uses `os.path.sep`.
|
||||||
|
sep = os.path.sep if os.path.sep in rel_path_without_suffix else "/"
|
||||||
|
module_name = rel_path_without_suffix.replace(sep, ".")
|
||||||
|
|
||||||
|
return module_name
|
||||||
|
|
||||||
|
|
||||||
|
def get_dirs(engine: Optional[Engine] = None) -> List[Path]:
|
||||||
|
"""
|
||||||
|
Helper for using django_component's FilesystemLoader class to obtain a list
|
||||||
|
of directories where component python files may be defined.
|
||||||
|
"""
|
||||||
|
current_engine = engine
|
||||||
|
if current_engine is None:
|
||||||
|
current_engine = Engine.get_default()
|
||||||
|
|
||||||
|
loader = Loader(current_engine)
|
||||||
|
return loader.get_dirs()
|
||||||
|
|
||||||
|
|
||||||
|
def search_dirs(dirs: List[Path], search_glob: str) -> List[Path]:
|
||||||
|
"""
|
||||||
|
Search the directories for the given glob pattern. Glob search results are returned
|
||||||
|
as a flattened list.
|
||||||
|
"""
|
||||||
|
matched_files: List[Path] = []
|
||||||
|
for directory in dirs:
|
||||||
|
for path in glob.iglob(str(Path(directory) / search_glob), recursive=True):
|
||||||
|
matched_files.append(Path(path))
|
||||||
|
|
||||||
|
return matched_files
|
|
@ -6,8 +6,8 @@ from typing import TYPE_CHECKING, Any, Callable, Dict, List, MutableMapping, Opt
|
||||||
from django.forms.widgets import Media, MediaDefiningClass
|
from django.forms.widgets import Media, MediaDefiningClass
|
||||||
from django.utils.safestring import SafeData
|
from django.utils.safestring import SafeData
|
||||||
|
|
||||||
|
from django_components.autodiscover import get_dirs
|
||||||
from django_components.logger import logger
|
from django_components.logger import logger
|
||||||
from django_components.utils import search
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from django_components.component import Component
|
from django_components.component import Component
|
||||||
|
@ -273,7 +273,7 @@ def _resolve_component_relative_files(attrs: MutableMapping) -> None:
|
||||||
|
|
||||||
# Prepare all possible directories we need to check when searching for
|
# Prepare all possible directories we need to check when searching for
|
||||||
# component's template and media files
|
# component's template and media files
|
||||||
components_dirs = search().searched_dirs
|
components_dirs = get_dirs()
|
||||||
|
|
||||||
# Get the directory where the component class is defined
|
# Get the directory where the component class is defined
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -2,33 +2,33 @@
|
||||||
Template loader that loads templates from each Django app's "components" directory.
|
Template loader that loads templates from each Django app's "components" directory.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Set
|
from typing import List, Set
|
||||||
|
|
||||||
from django.apps import apps
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template.loaders.filesystem import Loader as FilesystemLoader
|
from django.template.loaders.filesystem import Loader as FilesystemLoader
|
||||||
|
|
||||||
from django_components.logger import logger
|
from django_components.logger import logger
|
||||||
|
|
||||||
|
|
||||||
# Same as `Path.is_relative_to`, defined as standalone function because `Path.is_relative_to`
|
|
||||||
# is marked for deprecation.
|
|
||||||
def path_is_relative_to(child_path: str, parent_path: str) -> bool:
|
|
||||||
# If the relative path doesn't start with `..`, then child is descendant of parent
|
|
||||||
# See https://stackoverflow.com/a/7288073/9788634
|
|
||||||
rel_path = os.path.relpath(child_path, parent_path)
|
|
||||||
return not rel_path.startswith("..")
|
|
||||||
|
|
||||||
|
|
||||||
class Loader(FilesystemLoader):
|
class Loader(FilesystemLoader):
|
||||||
def get_dirs(self) -> List[Path]:
|
def get_dirs(self) -> List[Path]:
|
||||||
|
"""
|
||||||
|
Prepare directories that may contain component files:
|
||||||
|
|
||||||
|
Searches for dirs set in `STATICFILES_DIRS` settings. If none set, defaults to searching
|
||||||
|
for a "components" app. The dirs in `STATICFILES_DIRS` must be absolute paths.
|
||||||
|
|
||||||
|
Paths are accepted only if they resolve to a directory.
|
||||||
|
E.g. `/path/to/django_project/my_app/components/`.
|
||||||
|
|
||||||
|
If `STATICFILES_DIRS` is not set or empty, then `BASE_DIR` is required.
|
||||||
|
"""
|
||||||
# Allow to configure from settings which dirs should be checked for components
|
# Allow to configure from settings which dirs should be checked for components
|
||||||
if hasattr(settings, "STATICFILES_DIRS") and len(settings.STATICFILES_DIRS):
|
if hasattr(settings, "STATICFILES_DIRS") and settings.STATICFILES_DIRS:
|
||||||
component_dirs = settings.STATICFILES_DIRS
|
component_dirs = settings.STATICFILES_DIRS
|
||||||
else:
|
else:
|
||||||
component_dirs = ["components"]
|
component_dirs = [settings.BASE_DIR / "components"]
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Template loader will search for valid template dirs from following options:\n"
|
"Template loader will search for valid template dirs from following options:\n"
|
||||||
|
@ -37,6 +37,8 @@ class Loader(FilesystemLoader):
|
||||||
|
|
||||||
directories: Set[Path] = set()
|
directories: Set[Path] = set()
|
||||||
for component_dir in component_dirs:
|
for component_dir in component_dirs:
|
||||||
|
# Consider tuples for STATICFILES_DIRS (See #489)
|
||||||
|
# See https://docs.djangoproject.com/en/5.0/ref/settings/#prefixes-optional
|
||||||
if isinstance(component_dir, (tuple, list)) and len(component_dir) == 2:
|
if isinstance(component_dir, (tuple, list)) and len(component_dir) == 2:
|
||||||
component_dir = component_dir[1]
|
component_dir = component_dir[1]
|
||||||
try:
|
try:
|
||||||
|
@ -47,47 +49,11 @@ class Loader(FilesystemLoader):
|
||||||
f"See Django documentation. Got {type(component_dir)} : {component_dir}"
|
f"See Django documentation. Got {type(component_dir)} : {component_dir}"
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
curr_directories: Set[Path] = set()
|
|
||||||
|
|
||||||
# For each dir in `settings.STATICFILES_DIRS`, we go over all Django apps
|
if not Path(component_dir).is_absolute():
|
||||||
# and, for each app, check if the STATICFILES_DIRS dir is within that app dir.
|
raise ValueError(f"STATICFILES_DIRS must contain absolute paths, got '{component_dir}'")
|
||||||
# If so, we add the dir as a valid source.
|
else:
|
||||||
# The for loop is based on Django's `get_app_template_dirs`.
|
directories.add(Path(component_dir).resolve())
|
||||||
for app_config in apps.get_app_configs():
|
|
||||||
if not app_config.path:
|
|
||||||
continue
|
|
||||||
if not Path(component_dir).is_dir():
|
|
||||||
continue
|
|
||||||
|
|
||||||
if path_is_relative_to(component_dir, app_config.path):
|
|
||||||
curr_directories.add(Path(component_dir).resolve())
|
|
||||||
|
|
||||||
if hasattr(settings, "BASE_DIR"):
|
|
||||||
path = (Path(settings.BASE_DIR) / component_dir).resolve()
|
|
||||||
if path.is_dir():
|
|
||||||
curr_directories.add(path)
|
|
||||||
|
|
||||||
# Add the directory that holds the settings file
|
|
||||||
if settings.SETTINGS_MODULE:
|
|
||||||
module_parts = settings.SETTINGS_MODULE.split(".")
|
|
||||||
module_path = Path(*module_parts)
|
|
||||||
|
|
||||||
# - If len() == 2, then path to settings file is <app_name>/<settings_file>.py
|
|
||||||
# - If len() > 2, then we assume that settings file is in a settings directory,
|
|
||||||
# e.g. <app_name>/settings/<settings_file>.py
|
|
||||||
if len(module_parts) > 2:
|
|
||||||
module_path = Path(*module_parts[:-1])
|
|
||||||
|
|
||||||
# Get the paths for the nested settings dir like `<app_name>/settings`,
|
|
||||||
# or for non-nested dir like `<app_name>`
|
|
||||||
#
|
|
||||||
# NOTE: Use list() for < Python 3.9
|
|
||||||
for parent in list(module_path.parents)[:2]:
|
|
||||||
path = (parent / component_dir).resolve()
|
|
||||||
if path.is_dir():
|
|
||||||
curr_directories.add(path)
|
|
||||||
|
|
||||||
directories.update(curr_directories)
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Template loader matched following template dirs:\n" + "\n".join([f" - {str(d)}" for d in directories])
|
"Template loader matched following template dirs:\n" + "\n".join([f" - {str(d)}" for d in directories])
|
||||||
|
|
|
@ -1,41 +1,4 @@
|
||||||
import glob
|
from typing import Any, Callable, List
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any, Callable, List, NamedTuple, Optional
|
|
||||||
|
|
||||||
from django.template.engine import Engine
|
|
||||||
|
|
||||||
from django_components.template_loader import Loader
|
|
||||||
|
|
||||||
|
|
||||||
class SearchResult(NamedTuple):
|
|
||||||
searched_dirs: List[Path]
|
|
||||||
matched_files: List[Path]
|
|
||||||
|
|
||||||
|
|
||||||
def search(search_glob: Optional[str] = None, engine: Optional[Engine] = None) -> SearchResult:
|
|
||||||
"""
|
|
||||||
Search for directories that may contain components.
|
|
||||||
|
|
||||||
If `search_glob` is given, the directories are searched for said glob pattern,
|
|
||||||
and glob search results are returned as a flattened list.
|
|
||||||
"""
|
|
||||||
current_engine = engine
|
|
||||||
if current_engine is None:
|
|
||||||
current_engine = Engine.get_default()
|
|
||||||
|
|
||||||
loader = Loader(current_engine)
|
|
||||||
dirs = loader.get_dirs()
|
|
||||||
|
|
||||||
if search_glob is None:
|
|
||||||
return SearchResult(searched_dirs=dirs, matched_files=[])
|
|
||||||
|
|
||||||
component_filenames: List[Path] = []
|
|
||||||
for directory in dirs:
|
|
||||||
for path in glob.iglob(str(Path(directory) / search_glob), recursive=True):
|
|
||||||
component_filenames.append(Path(path))
|
|
||||||
|
|
||||||
return SearchResult(searched_dirs=dirs, matched_files=component_filenames)
|
|
||||||
|
|
||||||
|
|
||||||
# Global counter to ensure that all IDs generated by `gen_id` WILL be unique
|
# Global counter to ensure that all IDs generated by `gen_id` WILL be unique
|
||||||
_id = 0
|
_id = 0
|
||||||
|
|
|
@ -1,8 +1,16 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict, Optional
|
||||||
|
|
||||||
import django
|
import django
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
if not settings.configured:
|
|
||||||
|
def setup_test_config(components: Optional[Dict] = None):
|
||||||
|
if settings.configured:
|
||||||
|
return
|
||||||
|
|
||||||
settings.configure(
|
settings.configure(
|
||||||
|
BASE_DIR=Path(__file__).resolve().parent,
|
||||||
INSTALLED_APPS=("django_components",),
|
INSTALLED_APPS=("django_components",),
|
||||||
TEMPLATES=[
|
TEMPLATES=[
|
||||||
{
|
{
|
||||||
|
@ -13,7 +21,10 @@ if not settings.configured:
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
COMPONENTS={"template_cache_size": 128},
|
COMPONENTS={
|
||||||
|
"template_cache_size": 128,
|
||||||
|
**(components or {}),
|
||||||
|
},
|
||||||
MIDDLEWARE=["django_components.middleware.ComponentDependencyMiddleware"],
|
MIDDLEWARE=["django_components.middleware.ComponentDependencyMiddleware"],
|
||||||
DATABASES={
|
DATABASES={
|
||||||
"default": {
|
"default": {
|
||||||
|
|
|
@ -4,11 +4,10 @@ from django.utils.safestring import SafeString, mark_safe
|
||||||
from django_components import component, types
|
from django_components import component, types
|
||||||
from django_components.attributes import append_attributes, attributes_to_string
|
from django_components.attributes import append_attributes, attributes_to_string
|
||||||
|
|
||||||
# isort: off
|
from .django_test_setup import setup_test_config
|
||||||
from .django_test_setup import * # NOQA
|
|
||||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||||
|
|
||||||
# isort: on
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
class AttributesToStringTest(BaseTestCase):
|
class AttributesToStringTest(BaseTestCase):
|
||||||
|
|
|
@ -1,186 +1,164 @@
|
||||||
from pathlib import Path
|
import os
|
||||||
from unittest import mock
|
import sys
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest import TestCase, mock
|
||||||
|
|
||||||
from django.template.engine import Engine
|
from django.conf import settings
|
||||||
from django.urls import include, path
|
|
||||||
|
|
||||||
# isort: off
|
from django_components import component, component_registry
|
||||||
from .django_test_setup import settings
|
from django_components.autodiscover import _filepath_to_python_module, autodiscover, import_libraries
|
||||||
from .testutils import BaseTestCase
|
|
||||||
|
|
||||||
# isort: on
|
from .django_test_setup import setup_test_config
|
||||||
|
|
||||||
from django_components import _filepath_to_python_module, autodiscover, component, component_registry
|
|
||||||
from django_components.template_loader import Loader
|
|
||||||
|
|
||||||
urlpatterns = [
|
|
||||||
path("", include("tests.components.urls")),
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class TestAutodiscover(BaseTestCase):
|
# NOTE: This is different from BaseTestCase in testutils.py, because here we need
|
||||||
def setUp(self):
|
# TestCase instead of SimpleTestCase.
|
||||||
settings.SETTINGS_MODULE = "tests.test_autodiscover" # noqa
|
class _TestCase(TestCase):
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
def tearDown(self) -> None:
|
||||||
del settings.SETTINGS_MODULE # noqa
|
super().tearDown()
|
||||||
|
component.registry.clear()
|
||||||
|
|
||||||
# TODO: As part of this test, check that `autoimport()` imports the components
|
|
||||||
# from the `tests/components` dir?
|
class TestAutodiscover(_TestCase):
|
||||||
def test_autodiscover_with_components_as_views(self):
|
def test_autodiscover(self):
|
||||||
all_components_before = component_registry.registry.all().copy()
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
all_components = component.registry.all().copy()
|
||||||
|
self.assertNotIn("single_file_component", all_components)
|
||||||
|
self.assertNotIn("multi_file_component", all_components)
|
||||||
|
self.assertNotIn("relative_file_component", all_components)
|
||||||
|
self.assertNotIn("relative_file_pathobj_component", all_components)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
autodiscover()
|
modules = autodiscover(map_module=lambda p: "tests." + p)
|
||||||
except component.AlreadyRegistered:
|
except component_registry.AlreadyRegistered:
|
||||||
self.fail("Autodiscover should not raise AlreadyRegistered exception")
|
self.fail("Autodiscover should not raise AlreadyRegistered exception")
|
||||||
|
|
||||||
all_components_after = component_registry.registry.all().copy()
|
self.assertIn("tests.components.single_file", modules)
|
||||||
imported_components_count = len(all_components_after) - len(all_components_before)
|
self.assertIn("tests.components.multi_file.multi_file", modules)
|
||||||
self.assertEqual(imported_components_count, 3)
|
self.assertIn("tests.components.relative_file_pathobj.relative_file_pathobj", modules)
|
||||||
|
self.assertIn("tests.components.relative_file.relative_file", modules)
|
||||||
|
|
||||||
|
all_components = component.registry.all().copy()
|
||||||
|
self.assertIn("single_file_component", all_components)
|
||||||
|
self.assertIn("multi_file_component", all_components)
|
||||||
|
self.assertIn("relative_file_component", all_components)
|
||||||
|
self.assertIn("relative_file_pathobj_component", all_components)
|
||||||
|
|
||||||
|
|
||||||
class TestLoaderSettingsModule(BaseTestCase):
|
class TestImportLibraries(_TestCase):
|
||||||
def tearDown(self) -> None:
|
def test_import_libraries(self):
|
||||||
del settings.SETTINGS_MODULE # noqa
|
# Prepare settings
|
||||||
|
setup_test_config(
|
||||||
def test_get_dirs(self):
|
{
|
||||||
settings.SETTINGS_MODULE = "tests.test_autodiscover" # noqa
|
"autodiscover": False,
|
||||||
current_engine = Engine.get_default()
|
}
|
||||||
loader = Loader(current_engine)
|
|
||||||
dirs = loader.get_dirs()
|
|
||||||
self.assertEqual(
|
|
||||||
sorted(dirs),
|
|
||||||
sorted(
|
|
||||||
[
|
|
||||||
Path(__file__).parent.resolve() / "components",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
settings.COMPONENTS["libraries"] = ["tests.components.single_file", "tests.components.multi_file.multi_file"]
|
||||||
|
|
||||||
def test_complex_settings_module(self):
|
# Ensure we start with a clean state
|
||||||
settings.SETTINGS_MODULE = "tests.test_structures.test_structure_1.config.settings" # noqa
|
component.registry.clear()
|
||||||
|
all_components = component.registry.all().copy()
|
||||||
|
self.assertNotIn("single_file_component", all_components)
|
||||||
|
self.assertNotIn("multi_file_component", all_components)
|
||||||
|
|
||||||
current_engine = Engine.get_default()
|
# Ensure that the modules are executed again after import
|
||||||
loader = Loader(current_engine)
|
if "tests.components.single_file" in sys.modules:
|
||||||
dirs = loader.get_dirs()
|
del sys.modules["tests.components.single_file"]
|
||||||
self.assertEqual(
|
if "tests.components.multi_file.multi_file" in sys.modules:
|
||||||
sorted(dirs),
|
del sys.modules["tests.components.multi_file.multi_file"]
|
||||||
sorted(
|
|
||||||
[
|
try:
|
||||||
Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" / "components",
|
modules = import_libraries()
|
||||||
]
|
except component_registry.AlreadyRegistered:
|
||||||
),
|
self.fail("Autodiscover should not raise AlreadyRegistered exception")
|
||||||
|
|
||||||
|
self.assertIn("tests.components.single_file", modules)
|
||||||
|
self.assertIn("tests.components.multi_file.multi_file", modules)
|
||||||
|
|
||||||
|
all_components = component.registry.all().copy()
|
||||||
|
self.assertIn("single_file_component", all_components)
|
||||||
|
self.assertIn("multi_file_component", all_components)
|
||||||
|
|
||||||
|
settings.COMPONENTS["libraries"] = []
|
||||||
|
|
||||||
|
def test_import_libraries_map_modules(self):
|
||||||
|
# Prepare settings
|
||||||
|
setup_test_config(
|
||||||
|
{
|
||||||
|
"autodiscover": False,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
settings.COMPONENTS["libraries"] = ["components.single_file", "components.multi_file.multi_file"]
|
||||||
|
|
||||||
def test_complex_settings_module_2(self):
|
# Ensure we start with a clean state
|
||||||
settings.SETTINGS_MODULE = "tests.test_structures.test_structure_2.project.settings.production" # noqa
|
component.registry.clear()
|
||||||
|
all_components = component.registry.all().copy()
|
||||||
|
self.assertNotIn("single_file_component", all_components)
|
||||||
|
self.assertNotIn("multi_file_component", all_components)
|
||||||
|
|
||||||
current_engine = Engine.get_default()
|
# Ensure that the modules are executed again after import
|
||||||
loader = Loader(current_engine)
|
if "tests.components.single_file" in sys.modules:
|
||||||
dirs = loader.get_dirs()
|
del sys.modules["tests.components.single_file"]
|
||||||
self.assertEqual(
|
if "tests.components.multi_file.multi_file" in sys.modules:
|
||||||
sorted(dirs),
|
del sys.modules["tests.components.multi_file.multi_file"]
|
||||||
sorted(
|
|
||||||
[
|
|
||||||
Path(__file__).parent.resolve()
|
|
||||||
/ "test_structures"
|
|
||||||
/ "test_structure_2"
|
|
||||||
/ "project"
|
|
||||||
/ "components",
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_complex_settings_module_3(self):
|
try:
|
||||||
settings.SETTINGS_MODULE = "tests.test_structures.test_structure_3.project.settings.production" # noqa
|
modules = import_libraries(map_module=lambda p: "tests." + p)
|
||||||
|
except component_registry.AlreadyRegistered:
|
||||||
|
self.fail("Autodiscover should not raise AlreadyRegistered exception")
|
||||||
|
|
||||||
current_engine = Engine.get_default()
|
self.assertIn("tests.components.single_file", modules)
|
||||||
loader = Loader(current_engine)
|
self.assertIn("tests.components.multi_file.multi_file", modules)
|
||||||
dirs = loader.get_dirs()
|
|
||||||
expected = [
|
all_components = component.registry.all().copy()
|
||||||
Path(__file__).parent.resolve() / "test_structures" / "test_structure_3" / "components",
|
self.assertIn("single_file_component", all_components)
|
||||||
Path(__file__).parent.resolve() / "test_structures" / "test_structure_3" / "project" / "components",
|
self.assertIn("multi_file_component", all_components)
|
||||||
]
|
|
||||||
self.assertEqual(
|
settings.COMPONENTS["libraries"] = []
|
||||||
sorted(dirs),
|
|
||||||
sorted(expected),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestBaseDir(BaseTestCase):
|
class TestFilepathToPythonModule(_TestCase):
|
||||||
def setUp(self):
|
|
||||||
settings.BASE_DIR = Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" # noqa
|
|
||||||
settings.SETTINGS_MODULE = "tests_fake.test_autodiscover_fake" # noqa
|
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
del settings.BASE_DIR # noqa
|
|
||||||
del settings.SETTINGS_MODULE # noqa
|
|
||||||
|
|
||||||
def test_base_dir(self):
|
|
||||||
current_engine = Engine.get_default()
|
|
||||||
loader = Loader(current_engine)
|
|
||||||
dirs = loader.get_dirs()
|
|
||||||
expected = [
|
|
||||||
Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" / "components",
|
|
||||||
]
|
|
||||||
self.assertEqual(sorted(dirs), sorted(expected))
|
|
||||||
|
|
||||||
|
|
||||||
class TestStaticFilesDirs(BaseTestCase):
|
|
||||||
def setUp(self):
|
|
||||||
settings.STATICFILES_DIRS = [
|
|
||||||
"components",
|
|
||||||
("with_alias", "components"),
|
|
||||||
("too_many", "items", "components"),
|
|
||||||
("with_not_str_alias", 3),
|
|
||||||
] # noqa
|
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
del settings.STATICFILES_DIRS # noqa
|
|
||||||
|
|
||||||
@patch("django_components.template_loader.logger.warning")
|
|
||||||
def test_static_files_dirs(self, mock_warning: MagicMock):
|
|
||||||
mock_warning.reset_mock()
|
|
||||||
current_engine = Engine.get_default()
|
|
||||||
Loader(current_engine).get_dirs()
|
|
||||||
|
|
||||||
warn_inputs = [warn.args[0] for warn in mock_warning.call_args_list]
|
|
||||||
assert "Got <class 'tuple'> : ('too_many', 'items', 'components')" in warn_inputs[0]
|
|
||||||
assert "Got <class 'int'> : 3" in warn_inputs[1]
|
|
||||||
|
|
||||||
|
|
||||||
class TestFilepathToPythonModule(BaseTestCase):
|
|
||||||
def test_prepares_path(self):
|
def test_prepares_path(self):
|
||||||
|
base_path = str(settings.BASE_DIR)
|
||||||
|
|
||||||
|
the_path = os.path.join(base_path, "tests.py")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
_filepath_to_python_module(Path("tests.py")),
|
_filepath_to_python_module(the_path),
|
||||||
"tests",
|
"tests",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
the_path = os.path.join(base_path, "tests/components/relative_file/relative_file.py")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
_filepath_to_python_module(Path("tests/components/relative_file/relative_file.py")),
|
_filepath_to_python_module(the_path),
|
||||||
"tests.components.relative_file.relative_file",
|
"tests.components.relative_file.relative_file",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_handles_nonlinux_paths(self):
|
def test_handles_nonlinux_paths(self):
|
||||||
|
base_path = str(settings.BASE_DIR).replace("/", "//")
|
||||||
|
|
||||||
with mock.patch("os.path.sep", new="//"):
|
with mock.patch("os.path.sep", new="//"):
|
||||||
|
the_path = os.path.join(base_path, "tests.py")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
_filepath_to_python_module(Path("tests.py")),
|
_filepath_to_python_module(the_path),
|
||||||
"tests",
|
"tests",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
the_path = os.path.join(base_path, "tests//components//relative_file//relative_file.py")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
_filepath_to_python_module(Path("tests//components//relative_file//relative_file.py")),
|
_filepath_to_python_module(the_path),
|
||||||
"tests.components.relative_file.relative_file",
|
"tests.components.relative_file.relative_file",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
base_path = str(settings.BASE_DIR).replace("//", "\\")
|
||||||
with mock.patch("os.path.sep", new="\\"):
|
with mock.patch("os.path.sep", new="\\"):
|
||||||
|
the_path = os.path.join(base_path, "tests.py")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
_filepath_to_python_module(Path("tests.py")),
|
_filepath_to_python_module(the_path),
|
||||||
"tests",
|
"tests",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
the_path = os.path.join(base_path, "tests\\components\\relative_file\\relative_file.py")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
_filepath_to_python_module(Path("tests\\components\\relative_file\\relative_file.py")),
|
_filepath_to_python_module(the_path),
|
||||||
"tests.components.relative_file.relative_file",
|
"tests.components.relative_file.relative_file",
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,19 +7,19 @@ from django.core.management import call_command
|
||||||
from django.core.management.base import CommandError
|
from django.core.management.base import CommandError
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from .django_test_setup import * # NOQA
|
from .django_test_setup import setup_test_config
|
||||||
|
|
||||||
|
setup_test_config()
|
||||||
|
|
||||||
|
|
||||||
class CreateComponentCommandTest(TestCase):
|
class CreateComponentCommandTest(TestCase):
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
self.temp_dir = tempfile.mkdtemp()
|
||||||
cls.temp_dir = tempfile.mkdtemp()
|
|
||||||
|
|
||||||
@classmethod
|
def tearDown(self):
|
||||||
def tearDownClass(cls):
|
super().tearDown()
|
||||||
super().tearDownClass()
|
rmtree(self.temp_dir)
|
||||||
rmtree(cls.temp_dir)
|
|
||||||
|
|
||||||
def test_default_file_names(self):
|
def test_default_file_names(self):
|
||||||
component_name = "defaultcomponent"
|
component_name = "defaultcomponent"
|
||||||
|
|
|
@ -9,15 +9,14 @@ from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.template import Context, Template, TemplateSyntaxError
|
from django.template import Context, Template, TemplateSyntaxError
|
||||||
|
|
||||||
# isort: off
|
|
||||||
from .django_test_setup import * # NOQA
|
|
||||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
|
||||||
|
|
||||||
# isort: on
|
|
||||||
|
|
||||||
from django_components import component, types
|
from django_components import component, types
|
||||||
from django_components.slots import SlotRef
|
from django_components.slots import SlotRef
|
||||||
|
|
||||||
|
from .django_test_setup import setup_test_config
|
||||||
|
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||||
|
|
||||||
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
class ComponentTest(BaseTestCase):
|
class ComponentTest(BaseTestCase):
|
||||||
class ParentComponent(component.Component):
|
class ParentComponent(component.Component):
|
||||||
|
@ -55,11 +54,10 @@ class ComponentTest(BaseTestCase):
|
||||||
context["unique_variable"] = new_variable
|
context["unique_variable"] = new_variable
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
component.registry.register(name="parent_component", component=self.ParentComponent)
|
||||||
component.registry.register(name="parent_component", component=cls.ParentComponent)
|
component.registry.register(name="variable_display", component=self.VariableDisplay)
|
||||||
component.registry.register(name="variable_display", component=cls.VariableDisplay)
|
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_empty_component(self):
|
def test_empty_component(self):
|
||||||
|
|
|
@ -6,13 +6,12 @@ from django.template import Context, Template
|
||||||
from django.test import Client
|
from django.test import Client
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
# isort: off
|
from django_components import component
|
||||||
from .django_test_setup import * # noqa
|
|
||||||
|
from .django_test_setup import setup_test_config
|
||||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||||
|
|
||||||
# isort: on
|
setup_test_config()
|
||||||
|
|
||||||
from django_components import component
|
|
||||||
|
|
||||||
|
|
||||||
class CustomClient(Client):
|
class CustomClient(Client):
|
||||||
|
|
|
@ -9,13 +9,12 @@ from django.test import override_settings
|
||||||
from django.utils.html import format_html, html_safe
|
from django.utils.html import format_html, html_safe
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
|
||||||
# isort: off
|
from django_components import component, types
|
||||||
from .django_test_setup import * # NOQA
|
|
||||||
|
from .django_test_setup import setup_test_config
|
||||||
from .testutils import BaseTestCase, autodiscover_with_cleanup
|
from .testutils import BaseTestCase, autodiscover_with_cleanup
|
||||||
|
|
||||||
# isort: on
|
setup_test_config()
|
||||||
|
|
||||||
from django_components import component, types
|
|
||||||
|
|
||||||
|
|
||||||
class InlineComponentTest(BaseTestCase):
|
class InlineComponentTest(BaseTestCase):
|
||||||
|
@ -750,11 +749,10 @@ class MediaRelativePathTests(BaseTestCase):
|
||||||
context["unique_variable"] = new_variable
|
context["unique_variable"] = new_variable
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
component.registry.register(name="parent_component", component=self.ParentComponent)
|
||||||
component.registry.register(name="parent_component", component=cls.ParentComponent)
|
component.registry.register(name="variable_display", component=self.VariableDisplay)
|
||||||
component.registry.register(name="variable_display", component=cls.VariableDisplay)
|
|
||||||
|
|
||||||
# Settings required for autodiscover to work
|
# Settings required for autodiscover to work
|
||||||
@override_settings(
|
@override_settings(
|
||||||
|
@ -769,7 +767,7 @@ class MediaRelativePathTests(BaseTestCase):
|
||||||
del sys.modules["tests.components.relative_file.relative_file"]
|
del sys.modules["tests.components.relative_file.relative_file"]
|
||||||
|
|
||||||
# Fix the paths, since the "components" dir is nested
|
# Fix the paths, since the "components" dir is nested
|
||||||
with autodiscover_with_cleanup(map_import_paths=lambda p: f"tests.{p}"):
|
with autodiscover_with_cleanup(map_module=lambda p: f"tests.{p}"):
|
||||||
# Make sure that only relevant components are registered:
|
# Make sure that only relevant components are registered:
|
||||||
comps_to_remove = [
|
comps_to_remove = [
|
||||||
comp_name
|
comp_name
|
||||||
|
@ -811,7 +809,7 @@ class MediaRelativePathTests(BaseTestCase):
|
||||||
del sys.modules["tests.components.relative_file.relative_file"]
|
del sys.modules["tests.components.relative_file.relative_file"]
|
||||||
|
|
||||||
# Fix the paths, since the "components" dir is nested
|
# Fix the paths, since the "components" dir is nested
|
||||||
with autodiscover_with_cleanup(map_import_paths=lambda p: f"tests.{p}"):
|
with autodiscover_with_cleanup(map_module=lambda p: f"tests.{p}"):
|
||||||
component.registry.unregister("relative_file_pathobj_component")
|
component.registry.unregister("relative_file_pathobj_component")
|
||||||
|
|
||||||
template_str: types.django_html = """
|
template_str: types.django_html = """
|
||||||
|
@ -851,7 +849,7 @@ class MediaRelativePathTests(BaseTestCase):
|
||||||
del sys.modules["tests.components.relative_file_pathobj.relative_file_pathobj"]
|
del sys.modules["tests.components.relative_file_pathobj.relative_file_pathobj"]
|
||||||
|
|
||||||
# Fix the paths, since the "components" dir is nested
|
# Fix the paths, since the "components" dir is nested
|
||||||
with autodiscover_with_cleanup(map_import_paths=lambda p: f"tests.{p}"):
|
with autodiscover_with_cleanup(map_module=lambda p: f"tests.{p}"):
|
||||||
# Mark the PathObj instances of 'relative_file_pathobj_component' so they won raise
|
# Mark the PathObj instances of 'relative_file_pathobj_component' so they won raise
|
||||||
# error PathObj.__str__ is triggered.
|
# error PathObj.__str__ is triggered.
|
||||||
CompCls = component.registry.get("relative_file_pathobj_component")
|
CompCls = component.registry.get("relative_file_pathobj_component")
|
||||||
|
|
|
@ -2,9 +2,12 @@ from django.template import Context, Template
|
||||||
|
|
||||||
from django_components import component, types
|
from django_components import component, types
|
||||||
|
|
||||||
from .django_test_setup import * # NOQA
|
from .django_test_setup import setup_test_config
|
||||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||||
|
|
||||||
|
setup_test_config()
|
||||||
|
|
||||||
|
|
||||||
#########################
|
#########################
|
||||||
# COMPONENTS
|
# COMPONENTS
|
||||||
#########################
|
#########################
|
||||||
|
@ -85,11 +88,10 @@ class ContextTests(BaseTestCase):
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
return {"shadowing_variable": "NOT SHADOWED"}
|
return {"shadowing_variable": "NOT SHADOWED"}
|
||||||
|
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
|
||||||
component.registry.register(name="variable_display", component=VariableDisplay)
|
component.registry.register(name="variable_display", component=VariableDisplay)
|
||||||
component.registry.register(name="parent_component", component=cls.ParentComponent)
|
component.registry.register(name="parent_component", component=self.ParentComponent)
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag(
|
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag(
|
||||||
|
@ -239,11 +241,10 @@ class ParentArgsTests(BaseTestCase):
|
||||||
def get_context_data(self, parent_value):
|
def get_context_data(self, parent_value):
|
||||||
return {"inner_parent_value": parent_value}
|
return {"inner_parent_value": parent_value}
|
||||||
|
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
|
||||||
component.registry.register(name="incrementer", component=IncrementerComponent)
|
component.registry.register(name="incrementer", component=IncrementerComponent)
|
||||||
component.registry.register(name="parent_with_args", component=cls.ParentComponentWithArgs)
|
component.registry.register(name="parent_with_args", component=self.ParentComponentWithArgs)
|
||||||
component.registry.register(name="variable_display", component=VariableDisplay)
|
component.registry.register(name="variable_display", component=VariableDisplay)
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
@ -323,9 +324,8 @@ class ParentArgsTests(BaseTestCase):
|
||||||
|
|
||||||
|
|
||||||
class ContextCalledOnceTests(BaseTestCase):
|
class ContextCalledOnceTests(BaseTestCase):
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
|
||||||
component.registry.register(name="incrementer", component=IncrementerComponent)
|
component.registry.register(name="incrementer", component=IncrementerComponent)
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
@ -414,9 +414,8 @@ class ContextCalledOnceTests(BaseTestCase):
|
||||||
|
|
||||||
|
|
||||||
class ComponentsCanAccessOuterContext(BaseTestCase):
|
class ComponentsCanAccessOuterContext(BaseTestCase):
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
|
||||||
component.registry.register(name="simple_component", component=SimpleComponent)
|
component.registry.register(name="simple_component", component=SimpleComponent)
|
||||||
|
|
||||||
# NOTE: Second arg in tuple is expected value.
|
# NOTE: Second arg in tuple is expected value.
|
||||||
|
@ -442,9 +441,8 @@ class ComponentsCanAccessOuterContext(BaseTestCase):
|
||||||
|
|
||||||
|
|
||||||
class IsolatedContextTests(BaseTestCase):
|
class IsolatedContextTests(BaseTestCase):
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
|
||||||
component.registry.register(name="simple_component", component=SimpleComponent)
|
component.registry.register(name="simple_component", component=SimpleComponent)
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
@ -469,9 +467,8 @@ class IsolatedContextTests(BaseTestCase):
|
||||||
|
|
||||||
|
|
||||||
class IsolatedContextSettingTests(BaseTestCase):
|
class IsolatedContextSettingTests(BaseTestCase):
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
|
||||||
component.registry.register(name="simple_component", component=SimpleComponent)
|
component.registry.register(name="simple_component", component=SimpleComponent)
|
||||||
|
|
||||||
@parametrize_context_behavior(["isolated"])
|
@parametrize_context_behavior(["isolated"])
|
||||||
|
@ -534,10 +531,9 @@ class OuterContextPropertyTests(BaseTestCase):
|
||||||
def get_context_data(self):
|
def get_context_data(self):
|
||||||
return self.outer_context.flatten()
|
return self.outer_context.flatten()
|
||||||
|
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
component.registry.register(name="outer_context_component", component=self.OuterContextComponent)
|
||||||
component.registry.register(name="outer_context_component", component=cls.OuterContextComponent)
|
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
def test_outer_context_property_with_component(self):
|
def test_outer_context_property_with_component(self):
|
||||||
|
@ -609,20 +605,18 @@ class ContextVarsIsFilledTests(BaseTestCase):
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
def setUp(self) -> None:
|
||||||
def setUpClass(cls) -> None:
|
super().setUp()
|
||||||
super().setUpClass()
|
component.registry.register("is_filled_vars", self.IsFilledVarsComponent)
|
||||||
component.registry.register("is_filled_vars", cls.IsFilledVarsComponent)
|
component.registry.register("conditional_slots", self.ComponentWithConditionalSlots)
|
||||||
component.registry.register("conditional_slots", cls.ComponentWithConditionalSlots)
|
|
||||||
component.registry.register(
|
component.registry.register(
|
||||||
"complex_conditional_slots",
|
"complex_conditional_slots",
|
||||||
cls.ComponentWithComplexConditionalSlots,
|
self.ComponentWithComplexConditionalSlots,
|
||||||
)
|
)
|
||||||
component.registry.register("negated_conditional_slot", cls.ComponentWithNegatedConditionalSlot)
|
component.registry.register("negated_conditional_slot", self.ComponentWithNegatedConditionalSlot)
|
||||||
|
|
||||||
@classmethod
|
def tearDown(self) -> None:
|
||||||
def tearDownClass(cls) -> None:
|
super().tearDown()
|
||||||
super().tearDownClass()
|
|
||||||
component.registry.clear()
|
component.registry.clear()
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
|
|
@ -7,9 +7,11 @@ from django.test import override_settings
|
||||||
from django_components import component, types
|
from django_components import component, types
|
||||||
from django_components.middleware import ComponentDependencyMiddleware
|
from django_components.middleware import ComponentDependencyMiddleware
|
||||||
|
|
||||||
from .django_test_setup import * # NOQA
|
from .django_test_setup import setup_test_config
|
||||||
from .testutils import BaseTestCase, create_and_process_template_response
|
from .testutils import BaseTestCase, create_and_process_template_response
|
||||||
|
|
||||||
|
setup_test_config()
|
||||||
|
|
||||||
|
|
||||||
class SimpleComponent(component.Component):
|
class SimpleComponent(component.Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
|
|
|
@ -2,7 +2,9 @@ import unittest
|
||||||
|
|
||||||
from django_components import component
|
from django_components import component
|
||||||
|
|
||||||
from .django_test_setup import * # NOQA
|
from .django_test_setup import setup_test_config
|
||||||
|
|
||||||
|
setup_test_config()
|
||||||
|
|
||||||
|
|
||||||
class MockComponent(component.Component):
|
class MockComponent(component.Component):
|
||||||
|
|
|
@ -1,35 +1,19 @@
|
||||||
from django.conf import settings
|
from django.test import override_settings
|
||||||
|
|
||||||
from .django_test_setup import * # NOQA
|
from django_components.app_settings import app_settings
|
||||||
|
|
||||||
|
from .django_test_setup import setup_test_config
|
||||||
from .testutils import BaseTestCase
|
from .testutils import BaseTestCase
|
||||||
|
|
||||||
|
setup_test_config()
|
||||||
|
|
||||||
class ValidateWrongContextBehaviorValueTestCase(BaseTestCase):
|
|
||||||
def setUp(self) -> None:
|
|
||||||
settings.COMPONENTS["context_behavior"] = "invalid_value"
|
|
||||||
return super().setUp()
|
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
del settings.COMPONENTS["context_behavior"]
|
|
||||||
return super().tearDown()
|
|
||||||
|
|
||||||
|
class SettingsTestCase(BaseTestCase):
|
||||||
|
@override_settings(COMPONENTS={"context_behavior": "isolated"})
|
||||||
def test_valid_context_behavior(self):
|
def test_valid_context_behavior(self):
|
||||||
from django_components.app_settings import app_settings
|
self.assertEqual(app_settings.CONTEXT_BEHAVIOR, "isolated")
|
||||||
|
|
||||||
|
@override_settings(COMPONENTS={"context_behavior": "invalid_value"})
|
||||||
|
def test_raises_on_invalid_context_behavior(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
app_settings.CONTEXT_BEHAVIOR
|
app_settings.CONTEXT_BEHAVIOR
|
||||||
|
|
||||||
|
|
||||||
class ValidateCorrectContextBehaviorValueTestCase(BaseTestCase):
|
|
||||||
def setUp(self) -> None:
|
|
||||||
settings.COMPONENTS["context_behavior"] = "isolated"
|
|
||||||
return super().setUp()
|
|
||||||
|
|
||||||
def tearDown(self) -> None:
|
|
||||||
del settings.COMPONENTS["context_behavior"]
|
|
||||||
return super().tearDown()
|
|
||||||
|
|
||||||
def test_valid_context_behavior(self):
|
|
||||||
from django_components.app_settings import app_settings
|
|
||||||
|
|
||||||
self.assertEqual(app_settings.CONTEXT_BEHAVIOR, "isolated")
|
|
||||||
|
|
76
tests/test_template_loader.py
Normal file
76
tests/test_template_loader.py
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from django.template.engine import Engine
|
||||||
|
from django.test import override_settings
|
||||||
|
|
||||||
|
from django_components.template_loader import Loader
|
||||||
|
|
||||||
|
from .django_test_setup import setup_test_config
|
||||||
|
from .testutils import BaseTestCase
|
||||||
|
|
||||||
|
setup_test_config({"autodiscover": False})
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
BASE_DIR=Path(__file__).parent.resolve(),
|
||||||
|
)
|
||||||
|
class TemplateLoaderTest(BaseTestCase):
|
||||||
|
def test_get_dirs__base_dir(self):
|
||||||
|
current_engine = Engine.get_default()
|
||||||
|
loader = Loader(current_engine)
|
||||||
|
dirs = loader.get_dirs()
|
||||||
|
self.assertEqual(
|
||||||
|
sorted(dirs),
|
||||||
|
sorted(
|
||||||
|
[
|
||||||
|
Path(__file__).parent.resolve() / "components",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
BASE_DIR=Path(__file__).parent.resolve() / "test_structures" / "test_structure_1", # noqa
|
||||||
|
)
|
||||||
|
def test_get_dirs__base_dir__complex(self):
|
||||||
|
current_engine = Engine.get_default()
|
||||||
|
loader = Loader(current_engine)
|
||||||
|
dirs = loader.get_dirs()
|
||||||
|
expected = [
|
||||||
|
Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" / "components",
|
||||||
|
]
|
||||||
|
self.assertEqual(sorted(dirs), sorted(expected))
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
STATICFILES_DIRS=[
|
||||||
|
Path(__file__).parent.resolve() / "components",
|
||||||
|
("with_alias", Path(__file__).parent.resolve() / "components"),
|
||||||
|
("too_many", "items", Path(__file__).parent.resolve() / "components"),
|
||||||
|
("with_not_str_alias", 3),
|
||||||
|
] # noqa
|
||||||
|
)
|
||||||
|
@patch("django_components.template_loader.logger.warning")
|
||||||
|
def test_get_dirs__staticfiles_dirs(self, mock_warning: MagicMock):
|
||||||
|
mock_warning.reset_mock()
|
||||||
|
current_engine = Engine.get_default()
|
||||||
|
Loader(current_engine).get_dirs()
|
||||||
|
|
||||||
|
comps_path = Path(__file__).parent.resolve() / "components"
|
||||||
|
|
||||||
|
warn_inputs = [warn.args[0] for warn in mock_warning.call_args_list]
|
||||||
|
assert f"Got <class 'tuple'> : ('too_many', 'items', {repr(comps_path)})" in warn_inputs[0]
|
||||||
|
assert "Got <class 'int'> : 3" in warn_inputs[1]
|
||||||
|
|
||||||
|
@override_settings(STATICFILES_DIRS=["components"])
|
||||||
|
def test_get_dirs__staticfiles_dirs__raises_on_relative_path_1(self):
|
||||||
|
current_engine = Engine.get_default()
|
||||||
|
loader = Loader(current_engine)
|
||||||
|
with self.assertRaisesMessage(ValueError, "STATICFILES_DIRS must contain absolute paths"):
|
||||||
|
loader.get_dirs()
|
||||||
|
|
||||||
|
@override_settings(STATICFILES_DIRS=[("with_alias", "components")])
|
||||||
|
def test_get_dirs__staticfiles_dirs__raises_on_relative_path_2(self):
|
||||||
|
current_engine = Engine.get_default()
|
||||||
|
loader = Loader(current_engine)
|
||||||
|
with self.assertRaisesMessage(ValueError, "STATICFILES_DIRS must contain absolute paths"):
|
||||||
|
loader.get_dirs()
|
|
@ -1,16 +1,15 @@
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
from django.template.base import Parser
|
from django.template.base import Parser
|
||||||
|
|
||||||
# isort: off
|
|
||||||
from .django_test_setup import * # NOQA
|
|
||||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
|
||||||
|
|
||||||
# isort: on
|
|
||||||
|
|
||||||
from django_components import component, types
|
from django_components import component, types
|
||||||
from django_components.component import safe_resolve_dict, safe_resolve_list
|
from django_components.component import safe_resolve_dict, safe_resolve_list
|
||||||
from django_components.templatetags.component_tags import _parse_component_with_args
|
from django_components.templatetags.component_tags import _parse_component_with_args
|
||||||
|
|
||||||
|
from .django_test_setup import setup_test_config
|
||||||
|
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||||
|
|
||||||
|
setup_test_config()
|
||||||
|
|
||||||
|
|
||||||
class ParserTest(BaseTestCase):
|
class ParserTest(BaseTestCase):
|
||||||
def test_parses_args_kwargs(self):
|
def test_parses_args_kwargs(self):
|
||||||
|
|
|
@ -4,13 +4,12 @@ from typing import Callable
|
||||||
|
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
|
|
||||||
# isort: off
|
from django_components import component, types
|
||||||
from .django_test_setup import * # NOQA
|
|
||||||
|
from .django_test_setup import setup_test_config
|
||||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||||
|
|
||||||
# isort: on
|
setup_test_config()
|
||||||
|
|
||||||
from django_components import component, types
|
|
||||||
|
|
||||||
|
|
||||||
class SlottedComponent(component.Component):
|
class SlottedComponent(component.Component):
|
||||||
|
@ -25,19 +24,16 @@ class SlottedComponent(component.Component):
|
||||||
class TemplateInstrumentationTest(BaseTestCase):
|
class TemplateInstrumentationTest(BaseTestCase):
|
||||||
saved_render_method: Callable # Assigned during setup.
|
saved_render_method: Callable # Assigned during setup.
|
||||||
|
|
||||||
@classmethod
|
def tearDown(self):
|
||||||
def setUpClass(cls):
|
Template._render = self.saved_render_method
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
"""Emulate Django test instrumentation for TestCase (see setup_test_environment)"""
|
"""Emulate Django test instrumentation for TestCase (see setup_test_environment)"""
|
||||||
from django.test.utils import instrumented_test_render
|
from django.test.utils import instrumented_test_render
|
||||||
|
|
||||||
cls.saved_render_method = Template._render
|
self.saved_render_method = Template._render
|
||||||
Template._render = instrumented_test_render
|
Template._render = instrumented_test_render
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(cls):
|
|
||||||
Template._render = cls.saved_render_method
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
component.registry.clear()
|
component.registry.clear()
|
||||||
component.registry.register("test_component", SlottedComponent)
|
component.registry.register("test_component", SlottedComponent)
|
||||||
|
|
||||||
|
@ -100,9 +96,8 @@ class BlockCompatTests(BaseTestCase):
|
||||||
component.registry.clear()
|
component.registry.clear()
|
||||||
super().setUp()
|
super().setUp()
|
||||||
|
|
||||||
@classmethod
|
def tearDown(self):
|
||||||
def tearDownClass(cls):
|
super().tearDown()
|
||||||
super().tearDownClass()
|
|
||||||
component.registry.clear()
|
component.registry.clear()
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
|
|
@ -2,15 +2,12 @@ import textwrap
|
||||||
|
|
||||||
from django.template import Context, Template, TemplateSyntaxError
|
from django.template import Context, Template, TemplateSyntaxError
|
||||||
|
|
||||||
# isort: off
|
from django_components import component, component_registry, types
|
||||||
from .django_test_setup import * # NOQA
|
|
||||||
|
from .django_test_setup import setup_test_config
|
||||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||||
|
|
||||||
# isort: on
|
setup_test_config()
|
||||||
|
|
||||||
import django_components
|
|
||||||
import django_components.component_registry
|
|
||||||
from django_components import component, types
|
|
||||||
|
|
||||||
|
|
||||||
class SlottedComponent(component.Component):
|
class SlottedComponent(component.Component):
|
||||||
|
@ -77,7 +74,7 @@ class ComponentTemplateTagTest(BaseTestCase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
template = Template(simple_tag_template)
|
template = Template(simple_tag_template)
|
||||||
with self.assertRaises(django_components.component_registry.NotRegistered):
|
with self.assertRaises(component_registry.NotRegistered):
|
||||||
template.render(Context({}))
|
template.render(Context({}))
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
@ -164,7 +161,7 @@ class ComponentTemplateTagTest(BaseTestCase):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
template = Template(simple_tag_template)
|
template = Template(simple_tag_template)
|
||||||
with self.assertRaises(django_components.component_registry.NotRegistered):
|
with self.assertRaises(component_registry.NotRegistered):
|
||||||
template.render(Context({}))
|
template.render(Context({}))
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
@ -357,14 +354,12 @@ class AggregateInputTests(BaseTestCase):
|
||||||
|
|
||||||
|
|
||||||
class ComponentTemplateSyntaxErrorTests(BaseTestCase):
|
class ComponentTemplateSyntaxErrorTests(BaseTestCase):
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
|
||||||
component.registry.register("test", SlottedComponent)
|
component.registry.register("test", SlottedComponent)
|
||||||
|
|
||||||
@classmethod
|
def tearDown(self) -> None:
|
||||||
def tearDownClass(cls) -> None:
|
super().tearDown()
|
||||||
super().tearDownClass()
|
|
||||||
component.registry.clear()
|
component.registry.clear()
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
from django.template import Context, Template, TemplateSyntaxError
|
from django.template import Context, Template, TemplateSyntaxError
|
||||||
|
|
||||||
# isort: off
|
from django_components import component, types
|
||||||
from .django_test_setup import * # NOQA
|
|
||||||
|
from .django_test_setup import setup_test_config
|
||||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||||
|
|
||||||
# isort: on
|
setup_test_config()
|
||||||
|
|
||||||
from django_components import component, types
|
|
||||||
|
|
||||||
|
|
||||||
class ProvideTemplateTagTest(BaseTestCase):
|
class ProvideTemplateTagTest(BaseTestCase):
|
||||||
|
|
|
@ -2,13 +2,12 @@ from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
from django.template import Context, Template, TemplateSyntaxError
|
from django.template import Context, Template, TemplateSyntaxError
|
||||||
|
|
||||||
# isort: off
|
from django_components import component, types
|
||||||
from .django_test_setup import * # NOQA
|
|
||||||
|
from .django_test_setup import setup_test_config
|
||||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||||
|
|
||||||
# isort: on
|
setup_test_config()
|
||||||
|
|
||||||
from django_components import component, types
|
|
||||||
|
|
||||||
|
|
||||||
class SlottedComponent(component.Component):
|
class SlottedComponent(component.Component):
|
||||||
|
@ -516,15 +515,13 @@ class SlottedTemplateRegressionTests(BaseTestCase):
|
||||||
|
|
||||||
|
|
||||||
class SlotDefaultTests(BaseTestCase):
|
class SlotDefaultTests(BaseTestCase):
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
|
||||||
component.registry.clear()
|
component.registry.clear()
|
||||||
component.registry.register("test", SlottedComponent)
|
component.registry.register("test", SlottedComponent)
|
||||||
|
|
||||||
@classmethod
|
def tearDown(self):
|
||||||
def tearDownClass(cls):
|
super().tearDown()
|
||||||
super().tearDownClass()
|
|
||||||
component.registry.clear()
|
component.registry.clear()
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
@ -976,12 +973,11 @@ class DuplicateSlotTest(BaseTestCase):
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
component.registry.register(name="duplicate_slot", component=self.DuplicateSlotComponent)
|
||||||
component.registry.register(name="duplicate_slot", component=cls.DuplicateSlotComponent)
|
component.registry.register(name="duplicate_slot_nested", component=self.DuplicateSlotNestedComponent)
|
||||||
component.registry.register(name="duplicate_slot_nested", component=cls.DuplicateSlotNestedComponent)
|
component.registry.register(name="calendar", component=self.CalendarComponent)
|
||||||
component.registry.register(name="calendar", component=cls.CalendarComponent)
|
|
||||||
|
|
||||||
# NOTE: Second arg is the input for the "name" component kwarg
|
# NOTE: Second arg is the input for the "name" component kwarg
|
||||||
@parametrize_context_behavior(
|
@parametrize_context_behavior(
|
||||||
|
@ -1115,14 +1111,12 @@ class DuplicateSlotTest(BaseTestCase):
|
||||||
|
|
||||||
|
|
||||||
class SlotFillTemplateSyntaxErrorTests(BaseTestCase):
|
class SlotFillTemplateSyntaxErrorTests(BaseTestCase):
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
|
||||||
component.registry.register("test", SlottedComponent)
|
component.registry.register("test", SlottedComponent)
|
||||||
|
|
||||||
@classmethod
|
def tearDown(self):
|
||||||
def tearDownClass(cls) -> None:
|
super().tearDown()
|
||||||
super().tearDownClass()
|
|
||||||
component.registry.clear()
|
component.registry.clear()
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
|
|
@ -4,15 +4,12 @@ from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
|
|
||||||
# isort: off
|
from django_components import component, types
|
||||||
from .django_test_setup import * # NOQA
|
|
||||||
|
from .django_test_setup import setup_test_config
|
||||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||||
|
|
||||||
# isort: on
|
setup_test_config()
|
||||||
|
|
||||||
import django_components
|
|
||||||
import django_components.component_registry
|
|
||||||
from django_components import component, types
|
|
||||||
|
|
||||||
|
|
||||||
class SlottedComponent(component.Component):
|
class SlottedComponent(component.Component):
|
||||||
|
@ -179,15 +176,13 @@ class ConditionalSlotTests(BaseTestCase):
|
||||||
def get_context_data(self, branch=None):
|
def get_context_data(self, branch=None):
|
||||||
return {"branch": branch}
|
return {"branch": branch}
|
||||||
|
|
||||||
@classmethod
|
def setUp(self):
|
||||||
def setUpClass(cls):
|
super().setUp()
|
||||||
super().setUpClass()
|
|
||||||
component.registry.clear()
|
component.registry.clear()
|
||||||
component.registry.register("test", cls.ConditionalComponent)
|
component.registry.register("test", self.ConditionalComponent)
|
||||||
|
|
||||||
@classmethod
|
def tearDown(self):
|
||||||
def tearDownClass(cls):
|
super().tearDown()
|
||||||
super().tearDownClass()
|
|
||||||
component.registry.clear()
|
component.registry.clear()
|
||||||
|
|
||||||
@parametrize_context_behavior(["django", "isolated"])
|
@parametrize_context_behavior(["django", "isolated"])
|
||||||
|
@ -250,7 +245,7 @@ class ConditionalSlotTests(BaseTestCase):
|
||||||
class SlotIterationTest(BaseTestCase):
|
class SlotIterationTest(BaseTestCase):
|
||||||
"""Tests a behaviour of {% fill .. %} tag which is inside a template {% for .. %} loop."""
|
"""Tests a behaviour of {% fill .. %} tag which is inside a template {% for .. %} loop."""
|
||||||
|
|
||||||
class ComponentSimpleSlotInALoop(django_components.component.Component):
|
class ComponentSimpleSlotInALoop(component.Component):
|
||||||
template: types.django_html = """
|
template: types.django_html = """
|
||||||
{% load component_tags %}
|
{% load component_tags %}
|
||||||
{% for object in objects %}
|
{% for object in objects %}
|
||||||
|
@ -266,7 +261,7 @@ class SlotIterationTest(BaseTestCase):
|
||||||
}
|
}
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
django_components.component.registry.clear()
|
component.registry.clear()
|
||||||
|
|
||||||
# NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak.
|
# NOTE: Second arg in tuple is expected result. In isolated mode, loops should NOT leak.
|
||||||
@parametrize_context_behavior(
|
@parametrize_context_behavior(
|
||||||
|
@ -625,17 +620,15 @@ class ComponentNestingTests(BaseTestCase):
|
||||||
def get_context_data(self, items, *args, **kwargs) -> Dict[str, Any]:
|
def get_context_data(self, items, *args, **kwargs) -> Dict[str, Any]:
|
||||||
return {"items": items}
|
return {"items": items}
|
||||||
|
|
||||||
@classmethod
|
def setUp(self) -> None:
|
||||||
def setUpClass(cls) -> None:
|
super().setUp()
|
||||||
super().setUpClass()
|
component.registry.register("dashboard", self.DashboardComponent)
|
||||||
component.registry.register("dashboard", cls.DashboardComponent)
|
component.registry.register("calendar", self.CalendarComponent)
|
||||||
component.registry.register("calendar", cls.CalendarComponent)
|
component.registry.register("complex_child", self.ComplexChildComponent)
|
||||||
component.registry.register("complex_child", cls.ComplexChildComponent)
|
component.registry.register("complex_parent", self.ComplexParentComponent)
|
||||||
component.registry.register("complex_parent", cls.ComplexParentComponent)
|
|
||||||
|
|
||||||
@classmethod
|
def tearDown(self) -> None:
|
||||||
def tearDownClass(cls) -> None:
|
super().tearDown()
|
||||||
super().tearDownClass()
|
|
||||||
component.registry.clear()
|
component.registry.clear()
|
||||||
|
|
||||||
# NOTE: Second arg in tuple are expected names in nested fills. In "django" mode,
|
# NOTE: Second arg in tuple are expected names in nested fills. In "django" mode,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import contextlib
|
import contextlib
|
||||||
import functools
|
import functools
|
||||||
import sys
|
import sys
|
||||||
from typing import Any, List, Tuple, Union
|
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from django.template import Context, Node
|
from django.template import Context, Node
|
||||||
|
@ -9,8 +9,8 @@ from django.template.loader import engines
|
||||||
from django.template.response import TemplateResponse
|
from django.template.response import TemplateResponse
|
||||||
from django.test import SimpleTestCase, override_settings
|
from django.test import SimpleTestCase, override_settings
|
||||||
|
|
||||||
from django_components import autodiscover
|
|
||||||
from django_components.app_settings import ContextBehavior
|
from django_components.app_settings import ContextBehavior
|
||||||
|
from django_components.autodiscover import autodiscover
|
||||||
from django_components.component_registry import registry
|
from django_components.component_registry import registry
|
||||||
from django_components.middleware import ComponentDependencyMiddleware
|
from django_components.middleware import ComponentDependencyMiddleware
|
||||||
|
|
||||||
|
@ -20,14 +20,8 @@ middleware = ComponentDependencyMiddleware(get_response=lambda _: response_stash
|
||||||
|
|
||||||
|
|
||||||
class BaseTestCase(SimpleTestCase):
|
class BaseTestCase(SimpleTestCase):
|
||||||
@classmethod
|
def tearDown(self) -> None:
|
||||||
def setUpClass(self) -> None:
|
super().tearDown()
|
||||||
registry.clear()
|
|
||||||
return super().setUpClass()
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def tearDownClass(cls) -> None:
|
|
||||||
super().tearDownClass()
|
|
||||||
registry.clear()
|
registry.clear()
|
||||||
|
|
||||||
|
|
||||||
|
@ -89,7 +83,7 @@ ContextBehStr = Union[ContextBehavior, str]
|
||||||
ContextBehParam = Union[ContextBehStr, Tuple[ContextBehStr, Any]]
|
ContextBehParam = Union[ContextBehStr, Tuple[ContextBehStr, Any]]
|
||||||
|
|
||||||
|
|
||||||
def parametrize_context_behavior(cases: List[ContextBehParam]):
|
def parametrize_context_behavior(cases: List[ContextBehParam], settings: Optional[Dict] = None):
|
||||||
"""
|
"""
|
||||||
Use this decorator to run a test function with django_component's
|
Use this decorator to run a test function with django_component's
|
||||||
context_behavior settings set to given values.
|
context_behavior settings set to given values.
|
||||||
|
@ -157,7 +151,17 @@ def parametrize_context_behavior(cases: List[ContextBehParam]):
|
||||||
else:
|
else:
|
||||||
context_beh, fixture = case
|
context_beh, fixture = case
|
||||||
|
|
||||||
with override_settings(COMPONENTS={"context_behavior": context_beh}):
|
# Set `COMPONENTS={"context_behavior": context_beh}`, but do so carefully,
|
||||||
|
# so we override only that single setting, and so that we operate on copies
|
||||||
|
# to avoid spilling settings across the test cases
|
||||||
|
merged_settings = {} if not settings else settings.copy()
|
||||||
|
if "COMPONENTS" in merged_settings:
|
||||||
|
merged_settings["COMPONENTS"] = merged_settings["COMPONENTS"].copy()
|
||||||
|
else:
|
||||||
|
merged_settings["COMPONENTS"] = {}
|
||||||
|
merged_settings["COMPONENTS"]["context_behavior"] = context_beh
|
||||||
|
|
||||||
|
with override_settings(**merged_settings):
|
||||||
# Call the test function with the fixture as an argument
|
# Call the test function with the fixture as an argument
|
||||||
try:
|
try:
|
||||||
if case_has_data:
|
if case_has_data:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue