mirror of
https://github.com/django-components/django-components.git
synced 2025-07-07 17:34:59 +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
|
||||
|
||||
🚨📢 **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.
|
||||
|
||||
- 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
|
||||
|
||||
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
|
||||
INSTALLED_APPS = [
|
||||
...,
|
||||
'django_components',
|
||||
]
|
||||
```
|
||||
```python
|
||||
INSTALLED_APPS = [
|
||||
...,
|
||||
'django_components',
|
||||
]
|
||||
```
|
||||
|
||||
Modify `TEMPLATES` section of settings.py as follows:
|
||||
- *Remove `'APP_DIRS': True,`*
|
||||
- add `loaders` to `OPTIONS` list and set it to following value:
|
||||
3. Ensure that `BASE_DIR` setting is defined in settings.py:
|
||||
|
||||
```python
|
||||
TEMPLATES = [
|
||||
{
|
||||
...,
|
||||
'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',
|
||||
]
|
||||
)],
|
||||
},
|
||||
},
|
||||
]
|
||||
```
|
||||
```py
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
```
|
||||
|
||||
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
|
||||
STATICFILES_DIRS = [
|
||||
...,
|
||||
os.path.join(BASE_DIR, "components"),
|
||||
]
|
||||
```
|
||||
```python
|
||||
TEMPLATES = [
|
||||
{
|
||||
...,
|
||||
'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
|
||||
|
||||
|
@ -623,7 +642,37 @@ If you're planning on passing an HTML string, check Django's use of [`format_htm
|
|||
|
||||
## 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.
|
||||
|
||||
|
@ -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()`
|
||||
- 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
|
||||
|
||||
|
@ -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
|
||||
|
||||
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]
|
||||
name = "django_components"
|
||||
version = "0.84"
|
||||
version = "0.85"
|
||||
requires-python = ">=3.8, <4.0"
|
||||
description = "A way to create simple reusable template components in Django."
|
||||
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
|
||||
from django.conf import settings
|
||||
from django.utils.module_loading import autodiscover_modules
|
||||
|
||||
from django_components.logger import logger
|
||||
from django_components.utils import search
|
||||
from django_components.autodiscover import autodiscover as autodiscover # NOQA
|
||||
from django_components.autodiscover import import_libraries as import_libraries # NOQA
|
||||
|
||||
if django.VERSION < (3, 2):
|
||||
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):
|
||||
name = "django_components"
|
||||
|
||||
# This is the code that gets run when user adds django_components
|
||||
# to Django's INSTALLED_APPS
|
||||
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.utils.safestring import SafeData
|
||||
|
||||
from django_components.autodiscover import get_dirs
|
||||
from django_components.logger import logger
|
||||
from django_components.utils import search
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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
|
||||
# component's template and media files
|
||||
components_dirs = search().searched_dirs
|
||||
components_dirs = get_dirs()
|
||||
|
||||
# Get the directory where the component class is defined
|
||||
try:
|
||||
|
|
|
@ -2,33 +2,33 @@
|
|||
Template loader that loads templates from each Django app's "components" directory.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import List, Set
|
||||
|
||||
from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.template.loaders.filesystem import Loader as FilesystemLoader
|
||||
|
||||
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):
|
||||
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
|
||||
if hasattr(settings, "STATICFILES_DIRS") and len(settings.STATICFILES_DIRS):
|
||||
if hasattr(settings, "STATICFILES_DIRS") and settings.STATICFILES_DIRS:
|
||||
component_dirs = settings.STATICFILES_DIRS
|
||||
else:
|
||||
component_dirs = ["components"]
|
||||
component_dirs = [settings.BASE_DIR / "components"]
|
||||
|
||||
logger.debug(
|
||||
"Template loader will search for valid template dirs from following options:\n"
|
||||
|
@ -37,6 +37,8 @@ class Loader(FilesystemLoader):
|
|||
|
||||
directories: Set[Path] = set()
|
||||
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:
|
||||
component_dir = component_dir[1]
|
||||
try:
|
||||
|
@ -47,47 +49,11 @@ class Loader(FilesystemLoader):
|
|||
f"See Django documentation. Got {type(component_dir)} : {component_dir}"
|
||||
)
|
||||
continue
|
||||
curr_directories: Set[Path] = set()
|
||||
|
||||
# For each dir in `settings.STATICFILES_DIRS`, we go over all Django apps
|
||||
# and, for each app, check if the STATICFILES_DIRS dir is within that app dir.
|
||||
# If so, we add the dir as a valid source.
|
||||
# The for loop is based on Django's `get_app_template_dirs`.
|
||||
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)
|
||||
if not Path(component_dir).is_absolute():
|
||||
raise ValueError(f"STATICFILES_DIRS must contain absolute paths, got '{component_dir}'")
|
||||
else:
|
||||
directories.add(Path(component_dir).resolve())
|
||||
|
||||
logger.debug(
|
||||
"Template loader matched following template dirs:\n" + "\n".join([f" - {str(d)}" for d in directories])
|
||||
|
|
|
@ -1,41 +1,4 @@
|
|||
import glob
|
||||
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)
|
||||
|
||||
from typing import Any, Callable, List
|
||||
|
||||
# Global counter to ensure that all IDs generated by `gen_id` WILL be unique
|
||||
_id = 0
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
from pathlib import Path
|
||||
from typing import Dict, Optional
|
||||
|
||||
import django
|
||||
from django.conf import settings
|
||||
|
||||
if not settings.configured:
|
||||
|
||||
def setup_test_config(components: Optional[Dict] = None):
|
||||
if settings.configured:
|
||||
return
|
||||
|
||||
settings.configure(
|
||||
BASE_DIR=Path(__file__).resolve().parent,
|
||||
INSTALLED_APPS=("django_components",),
|
||||
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"],
|
||||
DATABASES={
|
||||
"default": {
|
||||
|
|
|
@ -4,11 +4,10 @@ from django.utils.safestring import SafeString, mark_safe
|
|||
from django_components import component, types
|
||||
from django_components.attributes import append_attributes, attributes_to_string
|
||||
|
||||
# isort: off
|
||||
from .django_test_setup import * # NOQA
|
||||
from .django_test_setup import setup_test_config
|
||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||
|
||||
# isort: on
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
||||
|
||||
class AttributesToStringTest(BaseTestCase):
|
||||
|
|
|
@ -1,186 +1,164 @@
|
|||
from pathlib import Path
|
||||
from unittest import mock
|
||||
from unittest.mock import MagicMock, patch
|
||||
import os
|
||||
import sys
|
||||
from unittest import TestCase, mock
|
||||
|
||||
from django.template.engine import Engine
|
||||
from django.urls import include, path
|
||||
from django.conf import settings
|
||||
|
||||
# isort: off
|
||||
from .django_test_setup import settings
|
||||
from .testutils import BaseTestCase
|
||||
from django_components import component, component_registry
|
||||
from django_components.autodiscover import _filepath_to_python_module, autodiscover, import_libraries
|
||||
|
||||
# isort: on
|
||||
|
||||
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")),
|
||||
]
|
||||
from .django_test_setup import setup_test_config
|
||||
|
||||
|
||||
class TestAutodiscover(BaseTestCase):
|
||||
def setUp(self):
|
||||
settings.SETTINGS_MODULE = "tests.test_autodiscover" # noqa
|
||||
|
||||
# NOTE: This is different from BaseTestCase in testutils.py, because here we need
|
||||
# TestCase instead of SimpleTestCase.
|
||||
class _TestCase(TestCase):
|
||||
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?
|
||||
def test_autodiscover_with_components_as_views(self):
|
||||
all_components_before = component_registry.registry.all().copy()
|
||||
|
||||
class TestAutodiscover(_TestCase):
|
||||
def test_autodiscover(self):
|
||||
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:
|
||||
autodiscover()
|
||||
except component.AlreadyRegistered:
|
||||
modules = autodiscover(map_module=lambda p: "tests." + p)
|
||||
except component_registry.AlreadyRegistered:
|
||||
self.fail("Autodiscover should not raise AlreadyRegistered exception")
|
||||
|
||||
all_components_after = component_registry.registry.all().copy()
|
||||
imported_components_count = len(all_components_after) - len(all_components_before)
|
||||
self.assertEqual(imported_components_count, 3)
|
||||
self.assertIn("tests.components.single_file", modules)
|
||||
self.assertIn("tests.components.multi_file.multi_file", modules)
|
||||
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):
|
||||
def tearDown(self) -> None:
|
||||
del settings.SETTINGS_MODULE # noqa
|
||||
|
||||
def test_get_dirs(self):
|
||||
settings.SETTINGS_MODULE = "tests.test_autodiscover" # noqa
|
||||
current_engine = Engine.get_default()
|
||||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
self.assertEqual(
|
||||
sorted(dirs),
|
||||
sorted(
|
||||
[
|
||||
Path(__file__).parent.resolve() / "components",
|
||||
]
|
||||
),
|
||||
class TestImportLibraries(_TestCase):
|
||||
def test_import_libraries(self):
|
||||
# Prepare settings
|
||||
setup_test_config(
|
||||
{
|
||||
"autodiscover": False,
|
||||
}
|
||||
)
|
||||
settings.COMPONENTS["libraries"] = ["tests.components.single_file", "tests.components.multi_file.multi_file"]
|
||||
|
||||
def test_complex_settings_module(self):
|
||||
settings.SETTINGS_MODULE = "tests.test_structures.test_structure_1.config.settings" # noqa
|
||||
# Ensure we start with a clean state
|
||||
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()
|
||||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
self.assertEqual(
|
||||
sorted(dirs),
|
||||
sorted(
|
||||
[
|
||||
Path(__file__).parent.resolve() / "test_structures" / "test_structure_1" / "components",
|
||||
]
|
||||
),
|
||||
# Ensure that the modules are executed again after import
|
||||
if "tests.components.single_file" in sys.modules:
|
||||
del sys.modules["tests.components.single_file"]
|
||||
if "tests.components.multi_file.multi_file" in sys.modules:
|
||||
del sys.modules["tests.components.multi_file.multi_file"]
|
||||
|
||||
try:
|
||||
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):
|
||||
settings.SETTINGS_MODULE = "tests.test_structures.test_structure_2.project.settings.production" # noqa
|
||||
# Ensure we start with a clean state
|
||||
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()
|
||||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
self.assertEqual(
|
||||
sorted(dirs),
|
||||
sorted(
|
||||
[
|
||||
Path(__file__).parent.resolve()
|
||||
/ "test_structures"
|
||||
/ "test_structure_2"
|
||||
/ "project"
|
||||
/ "components",
|
||||
]
|
||||
),
|
||||
)
|
||||
# Ensure that the modules are executed again after import
|
||||
if "tests.components.single_file" in sys.modules:
|
||||
del sys.modules["tests.components.single_file"]
|
||||
if "tests.components.multi_file.multi_file" in sys.modules:
|
||||
del sys.modules["tests.components.multi_file.multi_file"]
|
||||
|
||||
def test_complex_settings_module_3(self):
|
||||
settings.SETTINGS_MODULE = "tests.test_structures.test_structure_3.project.settings.production" # noqa
|
||||
try:
|
||||
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()
|
||||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
expected = [
|
||||
Path(__file__).parent.resolve() / "test_structures" / "test_structure_3" / "components",
|
||||
Path(__file__).parent.resolve() / "test_structures" / "test_structure_3" / "project" / "components",
|
||||
]
|
||||
self.assertEqual(
|
||||
sorted(dirs),
|
||||
sorted(expected),
|
||||
)
|
||||
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"] = []
|
||||
|
||||
|
||||
class TestBaseDir(BaseTestCase):
|
||||
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):
|
||||
class TestFilepathToPythonModule(_TestCase):
|
||||
def test_prepares_path(self):
|
||||
base_path = str(settings.BASE_DIR)
|
||||
|
||||
the_path = os.path.join(base_path, "tests.py")
|
||||
self.assertEqual(
|
||||
_filepath_to_python_module(Path("tests.py")),
|
||||
_filepath_to_python_module(the_path),
|
||||
"tests",
|
||||
)
|
||||
|
||||
the_path = os.path.join(base_path, "tests/components/relative_file/relative_file.py")
|
||||
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",
|
||||
)
|
||||
|
||||
def test_handles_nonlinux_paths(self):
|
||||
base_path = str(settings.BASE_DIR).replace("/", "//")
|
||||
|
||||
with mock.patch("os.path.sep", new="//"):
|
||||
the_path = os.path.join(base_path, "tests.py")
|
||||
self.assertEqual(
|
||||
_filepath_to_python_module(Path("tests.py")),
|
||||
_filepath_to_python_module(the_path),
|
||||
"tests",
|
||||
)
|
||||
|
||||
the_path = os.path.join(base_path, "tests//components//relative_file//relative_file.py")
|
||||
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",
|
||||
)
|
||||
|
||||
base_path = str(settings.BASE_DIR).replace("//", "\\")
|
||||
with mock.patch("os.path.sep", new="\\"):
|
||||
the_path = os.path.join(base_path, "tests.py")
|
||||
self.assertEqual(
|
||||
_filepath_to_python_module(Path("tests.py")),
|
||||
_filepath_to_python_module(the_path),
|
||||
"tests",
|
||||
)
|
||||
|
||||
the_path = os.path.join(base_path, "tests\\components\\relative_file\\relative_file.py")
|
||||
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",
|
||||
)
|
||||
|
|
|
@ -7,19 +7,19 @@ from django.core.management import call_command
|
|||
from django.core.management.base import CommandError
|
||||
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):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
cls.temp_dir = tempfile.mkdtemp()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
rmtree(cls.temp_dir)
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
rmtree(self.temp_dir)
|
||||
|
||||
def test_default_file_names(self):
|
||||
component_name = "defaultcomponent"
|
||||
|
|
|
@ -9,15 +9,14 @@ from django.core.exceptions import ImproperlyConfigured
|
|||
from django.http import HttpResponse
|
||||
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.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 ParentComponent(component.Component):
|
||||
|
@ -55,11 +54,10 @@ class ComponentTest(BaseTestCase):
|
|||
context["unique_variable"] = new_variable
|
||||
return context
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
component.registry.register(name="parent_component", component=cls.ParentComponent)
|
||||
component.registry.register(name="variable_display", component=cls.VariableDisplay)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
component.registry.register(name="parent_component", component=self.ParentComponent)
|
||||
component.registry.register(name="variable_display", component=self.VariableDisplay)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_empty_component(self):
|
||||
|
|
|
@ -6,13 +6,12 @@ from django.template import Context, Template
|
|||
from django.test import Client
|
||||
from django.urls import path
|
||||
|
||||
# isort: off
|
||||
from .django_test_setup import * # noqa
|
||||
from django_components import component
|
||||
|
||||
from .django_test_setup import setup_test_config
|
||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||
|
||||
# isort: on
|
||||
|
||||
from django_components import component
|
||||
setup_test_config()
|
||||
|
||||
|
||||
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.safestring import mark_safe
|
||||
|
||||
# isort: off
|
||||
from .django_test_setup import * # NOQA
|
||||
from django_components import component, types
|
||||
|
||||
from .django_test_setup import setup_test_config
|
||||
from .testutils import BaseTestCase, autodiscover_with_cleanup
|
||||
|
||||
# isort: on
|
||||
|
||||
from django_components import component, types
|
||||
setup_test_config()
|
||||
|
||||
|
||||
class InlineComponentTest(BaseTestCase):
|
||||
|
@ -750,11 +749,10 @@ class MediaRelativePathTests(BaseTestCase):
|
|||
context["unique_variable"] = new_variable
|
||||
return context
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
component.registry.register(name="parent_component", component=cls.ParentComponent)
|
||||
component.registry.register(name="variable_display", component=cls.VariableDisplay)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
component.registry.register(name="parent_component", component=self.ParentComponent)
|
||||
component.registry.register(name="variable_display", component=self.VariableDisplay)
|
||||
|
||||
# Settings required for autodiscover to work
|
||||
@override_settings(
|
||||
|
@ -769,7 +767,7 @@ class MediaRelativePathTests(BaseTestCase):
|
|||
del sys.modules["tests.components.relative_file.relative_file"]
|
||||
|
||||
# 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:
|
||||
comps_to_remove = [
|
||||
comp_name
|
||||
|
@ -811,7 +809,7 @@ class MediaRelativePathTests(BaseTestCase):
|
|||
del sys.modules["tests.components.relative_file.relative_file"]
|
||||
|
||||
# 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")
|
||||
|
||||
template_str: types.django_html = """
|
||||
|
@ -851,7 +849,7 @@ class MediaRelativePathTests(BaseTestCase):
|
|||
del sys.modules["tests.components.relative_file_pathobj.relative_file_pathobj"]
|
||||
|
||||
# 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
|
||||
# error PathObj.__str__ is triggered.
|
||||
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_test_setup import * # NOQA
|
||||
from .django_test_setup import setup_test_config
|
||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||
|
||||
setup_test_config()
|
||||
|
||||
|
||||
#########################
|
||||
# COMPONENTS
|
||||
#########################
|
||||
|
@ -85,11 +88,10 @@ class ContextTests(BaseTestCase):
|
|||
def get_context_data(self):
|
||||
return {"shadowing_variable": "NOT SHADOWED"}
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
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"])
|
||||
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):
|
||||
return {"inner_parent_value": parent_value}
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
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)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
|
@ -323,9 +324,8 @@ class ParentArgsTests(BaseTestCase):
|
|||
|
||||
|
||||
class ContextCalledOnceTests(BaseTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
component.registry.register(name="incrementer", component=IncrementerComponent)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
|
@ -414,9 +414,8 @@ class ContextCalledOnceTests(BaseTestCase):
|
|||
|
||||
|
||||
class ComponentsCanAccessOuterContext(BaseTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
component.registry.register(name="simple_component", component=SimpleComponent)
|
||||
|
||||
# NOTE: Second arg in tuple is expected value.
|
||||
|
@ -442,9 +441,8 @@ class ComponentsCanAccessOuterContext(BaseTestCase):
|
|||
|
||||
|
||||
class IsolatedContextTests(BaseTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
component.registry.register(name="simple_component", component=SimpleComponent)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
|
@ -469,9 +467,8 @@ class IsolatedContextTests(BaseTestCase):
|
|||
|
||||
|
||||
class IsolatedContextSettingTests(BaseTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
component.registry.register(name="simple_component", component=SimpleComponent)
|
||||
|
||||
@parametrize_context_behavior(["isolated"])
|
||||
|
@ -534,10 +531,9 @@ class OuterContextPropertyTests(BaseTestCase):
|
|||
def get_context_data(self):
|
||||
return self.outer_context.flatten()
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
component.registry.register(name="outer_context_component", component=cls.OuterContextComponent)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
component.registry.register(name="outer_context_component", component=self.OuterContextComponent)
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
def test_outer_context_property_with_component(self):
|
||||
|
@ -609,20 +605,18 @@ class ContextVarsIsFilledTests(BaseTestCase):
|
|||
</div>
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
component.registry.register("is_filled_vars", cls.IsFilledVarsComponent)
|
||||
component.registry.register("conditional_slots", cls.ComponentWithConditionalSlots)
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
component.registry.register("is_filled_vars", self.IsFilledVarsComponent)
|
||||
component.registry.register("conditional_slots", self.ComponentWithConditionalSlots)
|
||||
component.registry.register(
|
||||
"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 tearDownClass(cls) -> None:
|
||||
super().tearDownClass()
|
||||
def tearDown(self) -> None:
|
||||
super().tearDown()
|
||||
component.registry.clear()
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
|
|
|
@ -7,9 +7,11 @@ from django.test import override_settings
|
|||
from django_components import component, types
|
||||
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
|
||||
|
||||
setup_test_config()
|
||||
|
||||
|
||||
class SimpleComponent(component.Component):
|
||||
template: types.django_html = """
|
||||
|
|
|
@ -2,7 +2,9 @@ import unittest
|
|||
|
||||
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):
|
||||
|
|
|
@ -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
|
||||
|
||||
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):
|
||||
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):
|
||||
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.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.component import safe_resolve_dict, safe_resolve_list
|
||||
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):
|
||||
def test_parses_args_kwargs(self):
|
||||
|
|
|
@ -4,13 +4,12 @@ from typing import Callable
|
|||
|
||||
from django.template import Context, Template
|
||||
|
||||
# isort: off
|
||||
from .django_test_setup import * # NOQA
|
||||
from django_components import component, types
|
||||
|
||||
from .django_test_setup import setup_test_config
|
||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||
|
||||
# isort: on
|
||||
|
||||
from django_components import component, types
|
||||
setup_test_config()
|
||||
|
||||
|
||||
class SlottedComponent(component.Component):
|
||||
|
@ -25,19 +24,16 @@ class SlottedComponent(component.Component):
|
|||
class TemplateInstrumentationTest(BaseTestCase):
|
||||
saved_render_method: Callable # Assigned during setup.
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
def tearDown(self):
|
||||
Template._render = self.saved_render_method
|
||||
|
||||
def setUp(self):
|
||||
"""Emulate Django test instrumentation for TestCase (see setup_test_environment)"""
|
||||
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
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
Template._render = cls.saved_render_method
|
||||
|
||||
def setUp(self):
|
||||
component.registry.clear()
|
||||
component.registry.register("test_component", SlottedComponent)
|
||||
|
||||
|
@ -100,9 +96,8 @@ class BlockCompatTests(BaseTestCase):
|
|||
component.registry.clear()
|
||||
super().setUp()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
component.registry.clear()
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
|
|
|
@ -2,15 +2,12 @@ import textwrap
|
|||
|
||||
from django.template import Context, Template, TemplateSyntaxError
|
||||
|
||||
# isort: off
|
||||
from .django_test_setup import * # NOQA
|
||||
from django_components import component, component_registry, types
|
||||
|
||||
from .django_test_setup import setup_test_config
|
||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||
|
||||
# isort: on
|
||||
|
||||
import django_components
|
||||
import django_components.component_registry
|
||||
from django_components import component, types
|
||||
setup_test_config()
|
||||
|
||||
|
||||
class SlottedComponent(component.Component):
|
||||
|
@ -77,7 +74,7 @@ class ComponentTemplateTagTest(BaseTestCase):
|
|||
"""
|
||||
|
||||
template = Template(simple_tag_template)
|
||||
with self.assertRaises(django_components.component_registry.NotRegistered):
|
||||
with self.assertRaises(component_registry.NotRegistered):
|
||||
template.render(Context({}))
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
|
@ -164,7 +161,7 @@ class ComponentTemplateTagTest(BaseTestCase):
|
|||
"""
|
||||
|
||||
template = Template(simple_tag_template)
|
||||
with self.assertRaises(django_components.component_registry.NotRegistered):
|
||||
with self.assertRaises(component_registry.NotRegistered):
|
||||
template.render(Context({}))
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
|
@ -357,14 +354,12 @@ class AggregateInputTests(BaseTestCase):
|
|||
|
||||
|
||||
class ComponentTemplateSyntaxErrorTests(BaseTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
component.registry.register("test", SlottedComponent)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
super().tearDownClass()
|
||||
def tearDown(self) -> None:
|
||||
super().tearDown()
|
||||
component.registry.clear()
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
from django.template import Context, Template, TemplateSyntaxError
|
||||
|
||||
# isort: off
|
||||
from .django_test_setup import * # NOQA
|
||||
from django_components import component, types
|
||||
|
||||
from .django_test_setup import setup_test_config
|
||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||
|
||||
# isort: on
|
||||
|
||||
from django_components import component, types
|
||||
setup_test_config()
|
||||
|
||||
|
||||
class ProvideTemplateTagTest(BaseTestCase):
|
||||
|
|
|
@ -2,13 +2,12 @@ from typing import Any, Dict, List, Optional
|
|||
|
||||
from django.template import Context, Template, TemplateSyntaxError
|
||||
|
||||
# isort: off
|
||||
from .django_test_setup import * # NOQA
|
||||
from django_components import component, types
|
||||
|
||||
from .django_test_setup import setup_test_config
|
||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||
|
||||
# isort: on
|
||||
|
||||
from django_components import component, types
|
||||
setup_test_config()
|
||||
|
||||
|
||||
class SlottedComponent(component.Component):
|
||||
|
@ -516,15 +515,13 @@ class SlottedTemplateRegressionTests(BaseTestCase):
|
|||
|
||||
|
||||
class SlotDefaultTests(BaseTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
component.registry.clear()
|
||||
component.registry.register("test", SlottedComponent)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
component.registry.clear()
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
|
@ -976,12 +973,11 @@ class DuplicateSlotTest(BaseTestCase):
|
|||
</div>
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
component.registry.register(name="duplicate_slot", component=cls.DuplicateSlotComponent)
|
||||
component.registry.register(name="duplicate_slot_nested", component=cls.DuplicateSlotNestedComponent)
|
||||
component.registry.register(name="calendar", component=cls.CalendarComponent)
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
component.registry.register(name="duplicate_slot", component=self.DuplicateSlotComponent)
|
||||
component.registry.register(name="duplicate_slot_nested", component=self.DuplicateSlotNestedComponent)
|
||||
component.registry.register(name="calendar", component=self.CalendarComponent)
|
||||
|
||||
# NOTE: Second arg is the input for the "name" component kwarg
|
||||
@parametrize_context_behavior(
|
||||
|
@ -1115,14 +1111,12 @@ class DuplicateSlotTest(BaseTestCase):
|
|||
|
||||
|
||||
class SlotFillTemplateSyntaxErrorTests(BaseTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
component.registry.register("test", SlottedComponent)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
super().tearDownClass()
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
component.registry.clear()
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
|
|
|
@ -4,15 +4,12 @@ from typing import Any, Dict, Optional
|
|||
|
||||
from django.template import Context, Template
|
||||
|
||||
# isort: off
|
||||
from .django_test_setup import * # NOQA
|
||||
from django_components import component, types
|
||||
|
||||
from .django_test_setup import setup_test_config
|
||||
from .testutils import BaseTestCase, parametrize_context_behavior
|
||||
|
||||
# isort: on
|
||||
|
||||
import django_components
|
||||
import django_components.component_registry
|
||||
from django_components import component, types
|
||||
setup_test_config()
|
||||
|
||||
|
||||
class SlottedComponent(component.Component):
|
||||
|
@ -179,15 +176,13 @@ class ConditionalSlotTests(BaseTestCase):
|
|||
def get_context_data(self, branch=None):
|
||||
return {"branch": branch}
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super().setUpClass()
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
component.registry.clear()
|
||||
component.registry.register("test", cls.ConditionalComponent)
|
||||
component.registry.register("test", self.ConditionalComponent)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
super().tearDownClass()
|
||||
def tearDown(self):
|
||||
super().tearDown()
|
||||
component.registry.clear()
|
||||
|
||||
@parametrize_context_behavior(["django", "isolated"])
|
||||
|
@ -250,7 +245,7 @@ class ConditionalSlotTests(BaseTestCase):
|
|||
class SlotIterationTest(BaseTestCase):
|
||||
"""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 = """
|
||||
{% load component_tags %}
|
||||
{% for object in objects %}
|
||||
|
@ -266,7 +261,7 @@ class SlotIterationTest(BaseTestCase):
|
|||
}
|
||||
|
||||
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.
|
||||
@parametrize_context_behavior(
|
||||
|
@ -625,17 +620,15 @@ class ComponentNestingTests(BaseTestCase):
|
|||
def get_context_data(self, items, *args, **kwargs) -> Dict[str, Any]:
|
||||
return {"items": items}
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
super().setUpClass()
|
||||
component.registry.register("dashboard", cls.DashboardComponent)
|
||||
component.registry.register("calendar", cls.CalendarComponent)
|
||||
component.registry.register("complex_child", cls.ComplexChildComponent)
|
||||
component.registry.register("complex_parent", cls.ComplexParentComponent)
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
component.registry.register("dashboard", self.DashboardComponent)
|
||||
component.registry.register("calendar", self.CalendarComponent)
|
||||
component.registry.register("complex_child", self.ComplexChildComponent)
|
||||
component.registry.register("complex_parent", self.ComplexParentComponent)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
super().tearDownClass()
|
||||
def tearDown(self) -> None:
|
||||
super().tearDown()
|
||||
component.registry.clear()
|
||||
|
||||
# NOTE: Second arg in tuple are expected names in nested fills. In "django" mode,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import contextlib
|
||||
import functools
|
||||
import sys
|
||||
from typing import Any, List, Tuple, Union
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
from unittest.mock import Mock
|
||||
|
||||
from django.template import Context, Node
|
||||
|
@ -9,8 +9,8 @@ from django.template.loader import engines
|
|||
from django.template.response import TemplateResponse
|
||||
from django.test import SimpleTestCase, override_settings
|
||||
|
||||
from django_components import autodiscover
|
||||
from django_components.app_settings import ContextBehavior
|
||||
from django_components.autodiscover import autodiscover
|
||||
from django_components.component_registry import registry
|
||||
from django_components.middleware import ComponentDependencyMiddleware
|
||||
|
||||
|
@ -20,14 +20,8 @@ middleware = ComponentDependencyMiddleware(get_response=lambda _: response_stash
|
|||
|
||||
|
||||
class BaseTestCase(SimpleTestCase):
|
||||
@classmethod
|
||||
def setUpClass(self) -> None:
|
||||
registry.clear()
|
||||
return super().setUpClass()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
super().tearDownClass()
|
||||
def tearDown(self) -> None:
|
||||
super().tearDown()
|
||||
registry.clear()
|
||||
|
||||
|
||||
|
@ -89,7 +83,7 @@ ContextBehStr = Union[ContextBehavior, str]
|
|||
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
|
||||
context_behavior settings set to given values.
|
||||
|
@ -157,7 +151,17 @@ def parametrize_context_behavior(cases: List[ContextBehParam]):
|
|||
else:
|
||||
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
|
||||
try:
|
||||
if case_has_data:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue