refactor: Prepare autodiscover and template loader for v1 (#533)

This commit is contained in:
Juro Oravec 2024-07-29 20:27:35 +02:00 committed by GitHub
parent b1bd430a07
commit 8cb88558f0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 643 additions and 552 deletions

167
README.md
View file

@ -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.

View file

@ -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"]

View file

@ -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

View file

@ -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()

View 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

View file

@ -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:

View file

@ -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])

View file

@ -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

View file

@ -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": {

View file

@ -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):

View file

@ -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",
)

View 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"

View file

@ -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):

View file

@ -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):

View file

@ -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")

View file

@ -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"])

View file

@ -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 = """

View file

@ -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):

View file

@ -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")

View 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()

View file

@ -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):

View file

@ -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"])

View file

@ -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"])

View file

@ -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):

View file

@ -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"])

View file

@ -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,

View file

@ -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: