mirror of
https://github.com/django-components/django-components.git
synced 2025-09-18 11:49:44 +00:00
refactor: replace isort, black and flake8 with ruff (#1346)
Some checks are pending
Docs - build & deploy / docs (push) Waiting to run
Run tests / build (ubuntu-latest, 3.10) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.11) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.12) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.13) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.8) (push) Waiting to run
Run tests / test_sampleproject (3.13) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.9) (push) Waiting to run
Run tests / build (windows-latest, 3.10) (push) Waiting to run
Run tests / build (windows-latest, 3.11) (push) Waiting to run
Run tests / build (windows-latest, 3.12) (push) Waiting to run
Run tests / build (windows-latest, 3.13) (push) Waiting to run
Run tests / build (windows-latest, 3.8) (push) Waiting to run
Run tests / build (windows-latest, 3.9) (push) Waiting to run
Run tests / test_docs (3.13) (push) Waiting to run
Some checks are pending
Docs - build & deploy / docs (push) Waiting to run
Run tests / build (ubuntu-latest, 3.10) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.11) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.12) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.13) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.8) (push) Waiting to run
Run tests / test_sampleproject (3.13) (push) Waiting to run
Run tests / build (ubuntu-latest, 3.9) (push) Waiting to run
Run tests / build (windows-latest, 3.10) (push) Waiting to run
Run tests / build (windows-latest, 3.11) (push) Waiting to run
Run tests / build (windows-latest, 3.12) (push) Waiting to run
Run tests / build (windows-latest, 3.13) (push) Waiting to run
Run tests / build (windows-latest, 3.8) (push) Waiting to run
Run tests / build (windows-latest, 3.9) (push) Waiting to run
Run tests / test_docs (3.13) (push) Waiting to run
This commit is contained in:
parent
5279fd372a
commit
f100cc1836
128 changed files with 3076 additions and 2599 deletions
17
.github/copilot-instructions.md
vendored
17
.github/copilot-instructions.md
vendored
|
@ -8,7 +8,7 @@ Always reference these instructions first and fallback to search or bash command
|
|||
|
||||
### Initial Setup
|
||||
- Install development dependencies:
|
||||
- `pip install -r requirements-dev.txt` -- installs all dev dependencies including pytest, black, flake8, etc.
|
||||
- `pip install -r requirements-dev.txt` -- installs all dev dependencies including pytest, ruff, etc.
|
||||
- `pip install -e .` -- install the package in development mode
|
||||
- Install Playwright for browser testing (optional, may timeout):
|
||||
- `playwright install chromium --with-deps` -- NEVER CANCEL: Can take 10+ minutes due to large download. Set timeout to 15+ minutes.
|
||||
|
@ -20,14 +20,11 @@ Always reference these instructions first and fallback to search or bash command
|
|||
- `python -m pytest tests/test_component.py` -- runs specific test file (~5 seconds)
|
||||
- `python -m pytest tests/test_templatetags*.py` -- runs template tag tests (~10 seconds, 349 tests)
|
||||
- Run linting and code quality checks:
|
||||
- `black --check src/django_components` -- check code formatting (~1 second)
|
||||
- `black src/django_components` -- format code
|
||||
- `isort --check-only --diff src/django_components` -- check import sorting (~1 second)
|
||||
- `flake8 .` -- run linting (~2 seconds)
|
||||
- `ruff check .` -- run linting, and import sorting (~2 seconds)
|
||||
- `ruff format .` -- format code
|
||||
- `mypy .` -- run type checking (~10 seconds, may show some errors in tests)
|
||||
- Use tox for comprehensive testing (requires network access):
|
||||
- `tox -e black` -- run black in isolated environment
|
||||
- `tox -e flake8` -- run flake8 in isolated environment
|
||||
- `tox -e ruff` -- run ruff in isolated environment
|
||||
- `tox` -- run full test matrix (multiple Python/Django versions). NEVER CANCEL: Takes 10-30 minutes.
|
||||
|
||||
### Sample Project Testing
|
||||
|
@ -52,7 +49,7 @@ The package provides custom Django management commands:
|
|||
|
||||
## Validation
|
||||
|
||||
- Always run linting before committing: `black src/django_components && isort src/django_components && flake8 .`
|
||||
- Always run linting before committing: `ruff check .`
|
||||
- Always run at least basic tests: `python -m pytest tests/test_component.py`
|
||||
- Test sample project functionality: Start the sample project and make a request to verify components render correctly
|
||||
- Check that imports work: `python -c "import django_components; print('OK')"`
|
||||
|
@ -80,7 +77,7 @@ The package provides custom Django management commands:
|
|||
- Tests run on Python 3.8-3.13 with Django 4.2-5.2
|
||||
- Includes Playwright browser testing (requires `playwright install chromium --with-deps`)
|
||||
- Documentation building uses mkdocs
|
||||
- Pre-commit hooks run black, isort, and flake8
|
||||
- Pre-commit hooks run ruff
|
||||
|
||||
### Time Expectations
|
||||
- Installing dependencies: 1-2 minutes
|
||||
|
@ -100,7 +97,7 @@ The package provides custom Django management commands:
|
|||
1. Install dependencies: `pip install -r requirements-dev.txt && pip install -e .`
|
||||
2. Make changes to source code in `src/django_components/`
|
||||
3. Run tests: `python -m pytest tests/test_component.py` (or specific test files)
|
||||
4. Run linting: `black src/django_components && isort src/django_components && flake8 .`
|
||||
4. Run linting: `ruff check .`
|
||||
5. Test sample project: `cd sampleproject && python manage.py runserver`
|
||||
6. Validate with curl: `curl http://127.0.0.1:8000/`
|
||||
7. Run broader tests before final commit: `python -m pytest tests/test_templatetags*.py`
|
||||
|
|
|
@ -1,14 +1,10 @@
|
|||
repos:
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.10.0
|
||||
hooks:
|
||||
- id: black
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 7.1.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-pyproject]
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.9
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff-check
|
||||
args: [ --fix ]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
|
|
|
@ -8,10 +8,8 @@ from typing import Literal
|
|||
|
||||
# Fix for for https://github.com/airspeed-velocity/asv_runner/pull/44
|
||||
import benchmarks.monkeypatch_asv # noqa: F401
|
||||
|
||||
from benchmarks.utils import benchmark, create_virtual_module
|
||||
|
||||
|
||||
DJC_VS_DJ_GROUP = "Components vs Django"
|
||||
DJC_ISOLATED_VS_NON_GROUP = "isolated vs django modes"
|
||||
OTHER_GROUP = "Other"
|
||||
|
@ -30,7 +28,7 @@ TemplatingTestType = Literal[
|
|||
def _get_templating_filepath(renderer: TemplatingRenderer, size: TemplatingTestSize) -> Path:
|
||||
if renderer == "none":
|
||||
raise ValueError("Cannot get filepath for renderer 'none'")
|
||||
elif renderer not in ["django", "django-components"]:
|
||||
if renderer not in ["django", "django-components"]:
|
||||
raise ValueError(f"Invalid renderer: {renderer}")
|
||||
|
||||
if size not in ("lg", "sm"):
|
||||
|
@ -43,11 +41,10 @@ def _get_templating_filepath(renderer: TemplatingRenderer, size: TemplatingTestS
|
|||
file_path = root / "tests" / "test_benchmark_django.py"
|
||||
else:
|
||||
file_path = root / "tests" / "test_benchmark_django_small.py"
|
||||
elif size == "lg":
|
||||
file_path = root / "tests" / "test_benchmark_djc.py"
|
||||
else:
|
||||
if size == "lg":
|
||||
file_path = root / "tests" / "test_benchmark_djc.py"
|
||||
else:
|
||||
file_path = root / "tests" / "test_benchmark_djc_small.py"
|
||||
file_path = root / "tests" / "test_benchmark_djc_small.py"
|
||||
|
||||
return file_path
|
||||
|
||||
|
@ -60,7 +57,7 @@ def _get_templating_script(
|
|||
) -> str:
|
||||
if renderer == "none":
|
||||
return ""
|
||||
elif renderer not in ["django", "django-components"]:
|
||||
if renderer not in ["django", "django-components"]:
|
||||
raise ValueError(f"Invalid renderer: {renderer}")
|
||||
|
||||
# At this point, we know the renderer is either "django" or "django-components"
|
||||
|
@ -119,7 +116,7 @@ def setup_templating_memory_benchmark(
|
|||
context_mode: DjcContextMode,
|
||||
imports_only: bool = False,
|
||||
):
|
||||
global do_render
|
||||
global do_render # noqa: PLW0603
|
||||
module = _get_templating_module(renderer, size, context_mode, imports_only)
|
||||
data = module.gen_render_data()
|
||||
render = module.render
|
||||
|
@ -145,16 +142,15 @@ def prepare_templating_benchmark(
|
|||
# If we're testing the startup time, then the setup is actually the tested code
|
||||
if test_type == "startup":
|
||||
return setup_script
|
||||
else:
|
||||
# Otherwise include also data generation as part of setup
|
||||
setup_script += "\n\n" "render_data = gen_render_data()\n"
|
||||
# Otherwise include also data generation as part of setup
|
||||
setup_script += "\n\nrender_data = gen_render_data()\n"
|
||||
|
||||
# Do the first render as part of setup if we're testing the subsequent renders
|
||||
if test_type == "subsequent":
|
||||
setup_script += "render(render_data)\n"
|
||||
# Do the first render as part of setup if we're testing the subsequent renders
|
||||
if test_type == "subsequent":
|
||||
setup_script += "render(render_data)\n"
|
||||
|
||||
benchmark_script = "render(render_data)\n"
|
||||
return benchmark_script, setup_script
|
||||
benchmark_script = "render(render_data)\n"
|
||||
return benchmark_script, setup_script
|
||||
|
||||
|
||||
# - Group: django-components vs django
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from typing import Any
|
||||
|
||||
from asv_runner.benchmarks.timeraw import TimerawBenchmark, _SeparateProcessTimer
|
||||
|
||||
|
||||
# Fix for https://github.com/airspeed-velocity/asv_runner/pull/44
|
||||
def _get_timer(self, *param):
|
||||
def _get_timer(self: Any, *param: Any) -> _SeparateProcessTimer:
|
||||
"""
|
||||
Returns a timer that runs the benchmark function in a separate process.
|
||||
|
||||
|
@ -16,7 +18,7 @@ def _get_timer(self, *param):
|
|||
"""
|
||||
if param:
|
||||
|
||||
def func():
|
||||
def func() -> Any:
|
||||
# ---------- OUR CHANGES: ADDED RETURN STATEMENT ----------
|
||||
return self.func(*param)
|
||||
# ---------- OUR CHANGES END ----------
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import os
|
||||
import sys
|
||||
from importlib.abc import Loader
|
||||
from importlib.util import spec_from_loader, module_from_spec
|
||||
from importlib.util import module_from_spec, spec_from_loader
|
||||
from types import ModuleType
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
|
||||
# NOTE: benchmark_name constraints:
|
||||
|
@ -20,35 +20,35 @@ def benchmark(
|
|||
number: Optional[int] = None,
|
||||
min_run_count: Optional[int] = None,
|
||||
include_in_quick_benchmark: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
def decorator(func):
|
||||
**kwargs: Any,
|
||||
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
||||
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||
# For pull requests, we want to run benchmarks only for a subset of tests,
|
||||
# because the full set of tests takes about 10 minutes to run (5 min per commit).
|
||||
# This is done by setting DJC_BENCHMARK_QUICK=1 in the environment.
|
||||
if os.getenv("DJC_BENCHMARK_QUICK") and not include_in_quick_benchmark:
|
||||
# By setting the benchmark name to something that does NOT start with
|
||||
# valid prefixes like `time_`, `mem_`, or `peakmem_`, this function will be ignored by asv.
|
||||
func.benchmark_name = "noop"
|
||||
func.benchmark_name = "noop" # type: ignore[attr-defined]
|
||||
return func
|
||||
|
||||
# "group_name" is our custom field, which we actually convert to asv's "benchmark_name"
|
||||
if group_name is not None:
|
||||
benchmark_name = f"{group_name}.{func.__name__}"
|
||||
func.benchmark_name = benchmark_name
|
||||
func.benchmark_name = benchmark_name # type: ignore[attr-defined]
|
||||
|
||||
# Also "params" is custom, so we normalize it to "params" and "param_names"
|
||||
if params is not None:
|
||||
func.params, func.param_names = list(params.values()), list(params.keys())
|
||||
func.params, func.param_names = list(params.values()), list(params.keys()) # type: ignore[attr-defined]
|
||||
|
||||
if pretty_name is not None:
|
||||
func.pretty_name = pretty_name
|
||||
func.pretty_name = pretty_name # type: ignore[attr-defined]
|
||||
if timeout is not None:
|
||||
func.timeout = timeout
|
||||
func.timeout = timeout # type: ignore[attr-defined]
|
||||
if number is not None:
|
||||
func.number = number
|
||||
func.number = number # type: ignore[attr-defined]
|
||||
if min_run_count is not None:
|
||||
func.min_run_count = min_run_count
|
||||
func.min_run_count = min_run_count # type: ignore[attr-defined]
|
||||
|
||||
# Additional, untyped kwargs
|
||||
for k, v in kwargs.items():
|
||||
|
@ -60,11 +60,11 @@ def benchmark(
|
|||
|
||||
|
||||
class VirtualModuleLoader(Loader):
|
||||
def __init__(self, code_string):
|
||||
def __init__(self, code_string: str) -> None:
|
||||
self.code_string = code_string
|
||||
|
||||
def exec_module(self, module):
|
||||
exec(self.code_string, module.__dict__)
|
||||
def exec_module(self, module: ModuleType) -> None:
|
||||
exec(self.code_string, module.__dict__) # noqa: S102
|
||||
|
||||
|
||||
def create_virtual_module(name: str, code_string: str, file_path: str) -> ModuleType:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
## Install locally and run the tests
|
||||
## Local installation
|
||||
|
||||
Start by forking the project by clicking the **Fork button** up in the right corner in the [GitHub](https://github.com/django-components/django-components).
|
||||
This makes a copy of the repository in your own name. Now you can clone this repository locally and start adding features:
|
||||
|
@ -20,6 +20,8 @@ You also have to install this local django-components version. Use `-e` for [edi
|
|||
pip install -e .
|
||||
```
|
||||
|
||||
## Running tests
|
||||
|
||||
Now you can run the tests to make sure everything works as expected:
|
||||
|
||||
```sh
|
||||
|
@ -47,15 +49,39 @@ tox -e py38
|
|||
|
||||
NOTE: See the available environments in `tox.ini`.
|
||||
|
||||
And to run only linters, use:
|
||||
## Linting and formatting
|
||||
|
||||
To check linting rules, run:
|
||||
|
||||
```sh
|
||||
tox -e mypy,flake8,isort,black
|
||||
ruff check .
|
||||
# Or to fix errors automatically:
|
||||
ruff check --fix .
|
||||
```
|
||||
|
||||
## Running Playwright tests
|
||||
To format the code, run:
|
||||
|
||||
We use [Playwright](https://playwright.dev/python/docs/intro) for end-to-end tests. You will therefore need to install Playwright to be able to run these tests.
|
||||
```sh
|
||||
ruff format --check .
|
||||
# Or to fix errors automatically:
|
||||
ruff format .
|
||||
```
|
||||
|
||||
To validate with Mypy, run:
|
||||
|
||||
```sh
|
||||
mypy .
|
||||
```
|
||||
|
||||
You can run these through `tox` as well:
|
||||
|
||||
```sh
|
||||
tox -e mypy,ruff
|
||||
```
|
||||
|
||||
## Playwright tests
|
||||
|
||||
We use [Playwright](https://playwright.dev/python/docs/intro) for end-to-end tests. You will need to install Playwright to run these tests.
|
||||
|
||||
Luckily, Playwright makes it very easy:
|
||||
|
||||
|
@ -64,13 +90,15 @@ pip install -r requirements-dev.txt
|
|||
playwright install chromium --with-deps
|
||||
```
|
||||
|
||||
After Playwright is ready, simply run the tests with `tox`:
|
||||
After Playwright is ready, run the tests the same way as before:
|
||||
|
||||
```sh
|
||||
tox
|
||||
pytest
|
||||
# Or for specific Python version
|
||||
tox -e py38
|
||||
```
|
||||
|
||||
## Developing against live Django app
|
||||
## Dev server
|
||||
|
||||
How do you check that your changes to django-components project will work in an actual Django project?
|
||||
|
||||
|
@ -96,9 +124,10 @@ Use the [sampleproject](https://github.com/django-components/django-components/t
|
|||
|
||||
!!! note
|
||||
|
||||
The path to the local version (in this case `..`) must point to the directory that has the `setup.py` file.
|
||||
The path to the local version (in this case `..`) must point to the directory that has the `pyproject.toml` file.
|
||||
|
||||
4. Start Django server:
|
||||
|
||||
4. Start Django server
|
||||
```sh
|
||||
python manage.py runserver
|
||||
```
|
||||
|
@ -116,7 +145,7 @@ django_components uses a bit of JS code to:
|
|||
|
||||
When you make changes to this JS code, you also need to compile it:
|
||||
|
||||
1. Make sure you are inside `src/django_components_js`:
|
||||
1. Navigate to `src/django_components_js`:
|
||||
|
||||
```sh
|
||||
cd src/django_components_js
|
||||
|
|
|
@ -91,7 +91,7 @@ MyTable.render(
|
|||
|
||||
## Live examples
|
||||
|
||||
For live interactive examples, [start our demo project](../../community/development.md#developing-against-live-django-app)
|
||||
For live interactive examples, [start our demo project](../../community/development.md#dev-server)
|
||||
(`sampleproject`).
|
||||
|
||||
Then navigate to these URLs:
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from pathlib import Path
|
||||
from typing import List, Optional, Type
|
||||
from typing import Any, List, Optional, Type
|
||||
|
||||
import griffe
|
||||
from mkdocs_util import get_mkdocstrings_plugin_handler_options, import_object, load_config
|
||||
|
@ -18,7 +18,7 @@ is_skip_docstring: bool = mkdocstrings_config.get("show_if_no_docstring", "false
|
|||
class RuntimeBasesExtension(griffe.Extension):
|
||||
"""Griffe extension that lists class bases."""
|
||||
|
||||
def on_class_instance(self, cls: griffe.Class, **kwargs) -> None:
|
||||
def on_class_instance(self, cls: griffe.Class, **_kwargs: Any) -> None:
|
||||
if is_skip_docstring and cls.docstring is None:
|
||||
return
|
||||
|
||||
|
@ -37,7 +37,7 @@ class RuntimeBasesExtension(griffe.Extension):
|
|||
class SourceCodeExtension(griffe.Extension):
|
||||
"""Griffe extension that adds link to the source code at the end of the docstring."""
|
||||
|
||||
def on_instance(self, obj: griffe.Object, **kwargs) -> None:
|
||||
def on_instance(self, obj: griffe.Object, **_kwargs: Any) -> None:
|
||||
if is_skip_docstring and obj.docstring is None:
|
||||
return
|
||||
|
||||
|
@ -46,7 +46,7 @@ class SourceCodeExtension(griffe.Extension):
|
|||
obj.docstring.value = html + obj.docstring.value
|
||||
|
||||
|
||||
def _format_source_code_html(relative_filepath: Path, lineno: Optional[int]):
|
||||
def _format_source_code_html(relative_filepath: Path, lineno: Optional[int]) -> str:
|
||||
# Remove trailing slash and whitespace
|
||||
repo_url = load_config()["repo_url"].strip("/ ")
|
||||
branch_path = f"tree/{SOURCE_CODE_GIT_BRANCH}"
|
||||
|
|
|
@ -9,7 +9,7 @@ from mkdocs_gen_files import Nav
|
|||
ROOT = pathlib.Path(__file__).parent.parent.parent
|
||||
|
||||
|
||||
def generate_release_notes():
|
||||
def generate_release_notes() -> None:
|
||||
"""
|
||||
Reads CHANGELOG.md, splits it into per-version pages,
|
||||
and generates an index page with links to all versions.
|
||||
|
@ -20,7 +20,7 @@ def generate_release_notes():
|
|||
# Create the output directory if it doesn't exist
|
||||
(ROOT / "docs" / releases_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(changelog_path, "r", encoding="utf-8") as f:
|
||||
with changelog_path.open("r", encoding="utf-8") as f:
|
||||
changelog_content = f.read()
|
||||
|
||||
# Split the changelog by version headers (e.g., "## vX.Y.Z")
|
||||
|
@ -62,7 +62,7 @@ def generate_release_notes():
|
|||
# Prepare title for navigation, e.g. "v0.140.0 (2024-09-11)"
|
||||
nav_title = version_title_full
|
||||
if date_str:
|
||||
parsed_date = datetime.strptime(date_str, "%d %b %Y")
|
||||
parsed_date = datetime.strptime(date_str, "%d %b %Y") # noqa: DTZ007
|
||||
formatted_date = parsed_date.strftime("%Y-%m-%d")
|
||||
nav_title += f" ({formatted_date})"
|
||||
|
||||
|
|
|
@ -3,22 +3,22 @@
|
|||
from functools import lru_cache
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import griffe
|
||||
import yaml # type: ignore[import-untyped]
|
||||
|
||||
|
||||
@lru_cache()
|
||||
@lru_cache
|
||||
def load_config() -> Dict:
|
||||
mkdocs_config_str = Path("mkdocs.yml").read_text()
|
||||
# NOTE: Use BaseLoader to avoid resolving tags like `!ENV`
|
||||
# See https://stackoverflow.com/questions/45966633/yaml-error-could-not-determine-a-constructor-for-the-tag
|
||||
mkdocs_config = yaml.load(mkdocs_config_str, yaml.BaseLoader)
|
||||
mkdocs_config = yaml.load(mkdocs_config_str, yaml.BaseLoader) # noqa: S506
|
||||
return mkdocs_config
|
||||
|
||||
|
||||
@lru_cache()
|
||||
@lru_cache
|
||||
def find_plugin(name: str) -> Optional[Dict]:
|
||||
config = load_config()
|
||||
plugins: List[Union[str, Dict[str, Dict]]] = config.get("plugins", [])
|
||||
|
@ -27,8 +27,8 @@ def find_plugin(name: str) -> Optional[Dict]:
|
|||
|
||||
for plugin in plugins:
|
||||
if isinstance(plugin, str):
|
||||
plugin = {plugin: {}}
|
||||
plugin_name, plugin_conf = list(plugin.items())[0]
|
||||
plugin = {plugin: {}} # noqa: PLW2901
|
||||
plugin_name, plugin_conf = next(iter(plugin.items()))
|
||||
if plugin_name == name:
|
||||
return plugin_conf
|
||||
|
||||
|
@ -43,7 +43,7 @@ def get_mkdocstrings_plugin_handler_options() -> Optional[Dict]:
|
|||
return plugin.get("handlers", {}).get("python", {}).get("options", {})
|
||||
|
||||
|
||||
def import_object(obj: griffe.Object):
|
||||
def import_object(obj: griffe.Object) -> Any:
|
||||
module = import_module(obj.module.path)
|
||||
runtime_obj = getattr(module, obj.name)
|
||||
return runtime_obj
|
||||
|
|
|
@ -42,19 +42,21 @@ from argparse import ArgumentParser
|
|||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import Any, Dict, List, NamedTuple, Optional, Sequence, Tuple, Type, Union
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, NamedTuple, Optional, Sequence, Tuple, Type, Union
|
||||
|
||||
import mkdocs_gen_files
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.urls import URLPattern, URLResolver
|
||||
|
||||
from django_components import Component, ComponentVars, ComponentCommand, TagFormatterABC
|
||||
from django_components import Component, ComponentCommand, ComponentVars, TagFormatterABC
|
||||
from django_components.commands.components import ComponentsRootCommand
|
||||
from django_components.node import BaseNode
|
||||
from django_components.util.command import setup_parser_from_command
|
||||
from django_components.util.misc import get_import_path
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
# NOTE: This file is an entrypoint for the `gen-files` plugin in `mkdocs.yml`.
|
||||
# However, `gen-files` plugin runs this file as a script, NOT as a module.
|
||||
# That means that:
|
||||
|
@ -71,7 +73,7 @@ from extensions import _format_source_code_html # noqa: E402
|
|||
root = Path(__file__).parent.parent.parent
|
||||
|
||||
|
||||
def gen_reference_api():
|
||||
def gen_reference_api() -> None:
|
||||
"""
|
||||
Generate documentation for the Python API of `django_components`.
|
||||
|
||||
|
@ -109,14 +111,14 @@ def gen_reference_api():
|
|||
# options:
|
||||
# show_if_no_docstring: true
|
||||
# ```
|
||||
f.write(f"::: {module.__name__}.{name}\n" f" options:\n" f" show_if_no_docstring: true\n")
|
||||
f.write(f"::: {module.__name__}.{name}\n options:\n show_if_no_docstring: true\n")
|
||||
|
||||
f.write("\n")
|
||||
|
||||
mkdocs_gen_files.set_edit_path(out_path, template_path)
|
||||
|
||||
|
||||
def gen_reference_testing_api():
|
||||
def gen_reference_testing_api() -> None:
|
||||
"""
|
||||
Generate documentation for the Python API of `django_components.testing`.
|
||||
|
||||
|
@ -142,17 +144,15 @@ def gen_reference_testing_api():
|
|||
# options:
|
||||
# show_if_no_docstring: true
|
||||
# ```
|
||||
f.write(f"::: {module.__name__}.{name}\n" f" options:\n" f" show_if_no_docstring: true\n")
|
||||
f.write(f"::: {module.__name__}.{name}\n options:\n show_if_no_docstring: true\n")
|
||||
|
||||
f.write("\n")
|
||||
|
||||
mkdocs_gen_files.set_edit_path(out_path, template_path)
|
||||
|
||||
|
||||
def gen_reference_exceptions():
|
||||
"""
|
||||
Generate documentation for the Exception classes included in the Python API of `django_components`.
|
||||
"""
|
||||
def gen_reference_exceptions() -> None:
|
||||
"""Generate documentation for the Exception classes included in the Python API of `django_components`."""
|
||||
module = import_module("django_components")
|
||||
|
||||
preface = "<!-- Autogenerated by reference.py -->\n\n"
|
||||
|
@ -178,14 +178,14 @@ def gen_reference_exceptions():
|
|||
# options:
|
||||
# show_if_no_docstring: true
|
||||
# ```
|
||||
f.write(f"::: {module.__name__}.{name}\n" f" options:\n" f" show_if_no_docstring: true\n")
|
||||
f.write(f"::: {module.__name__}.{name}\n options:\n show_if_no_docstring: true\n")
|
||||
|
||||
f.write("\n")
|
||||
|
||||
mkdocs_gen_files.set_edit_path(out_path, template_path)
|
||||
|
||||
|
||||
def gen_reference_components():
|
||||
def gen_reference_components() -> None:
|
||||
"""
|
||||
Generate documentation for the Component classes (AKA pre-defined components) included
|
||||
in the Python API of `django_components`.
|
||||
|
@ -200,7 +200,7 @@ def gen_reference_components():
|
|||
with mkdocs_gen_files.open(out_path, "w", encoding="utf-8") as f:
|
||||
f.write(preface + "\n\n")
|
||||
|
||||
for name, obj in inspect.getmembers(module):
|
||||
for _name, obj in inspect.getmembers(module):
|
||||
if not _is_component_cls(obj):
|
||||
continue
|
||||
|
||||
|
@ -236,7 +236,7 @@ def gen_reference_components():
|
|||
f" show_root_heading: true\n"
|
||||
f" show_signature: false\n"
|
||||
f" separate_signature: false\n"
|
||||
f" members: {members}\n"
|
||||
f" members: {members}\n",
|
||||
)
|
||||
|
||||
f.write("\n")
|
||||
|
@ -244,10 +244,8 @@ def gen_reference_components():
|
|||
mkdocs_gen_files.set_edit_path(out_path, template_path)
|
||||
|
||||
|
||||
def gen_reference_settings():
|
||||
"""
|
||||
Generate documentation for the settings of django-components, as defined by the `ComponentsSettings` class.
|
||||
"""
|
||||
def gen_reference_settings() -> None:
|
||||
"""Generate documentation for the settings of django-components, as defined by the `ComponentsSettings` class."""
|
||||
module = import_module("django_components.app_settings")
|
||||
|
||||
preface = "<!-- Autogenerated by reference.py -->\n\n"
|
||||
|
@ -293,7 +291,7 @@ def gen_reference_settings():
|
|||
f" show_symbol_type_heading: false\n"
|
||||
f" show_symbol_type_toc: false\n"
|
||||
f" show_if_no_docstring: true\n"
|
||||
f" show_labels: false\n"
|
||||
f" show_labels: false\n",
|
||||
)
|
||||
f.write("\n")
|
||||
|
||||
|
@ -301,7 +299,7 @@ def gen_reference_settings():
|
|||
|
||||
|
||||
# Get attributes / methods that are unique to the subclass
|
||||
def _get_unique_methods(base_class: Type, sub_class: Type):
|
||||
def _get_unique_methods(base_class: Type, sub_class: Type) -> List[str]:
|
||||
base_methods = set(dir(base_class))
|
||||
subclass_methods = set(dir(sub_class))
|
||||
unique_methods = subclass_methods - base_methods
|
||||
|
@ -332,25 +330,25 @@ def _gen_default_settings_section(app_settings_filepath: str) -> str:
|
|||
#
|
||||
# However, for the documentation, we need to remove those.
|
||||
dynamic_re = re.compile(r"Dynamic\(lambda\: (?P<code>.+)\)")
|
||||
cleaned_snippet_lines = []
|
||||
cleaned_snippet_lines: List[str] = []
|
||||
for line in defaults_snippet_lines:
|
||||
line = comment_re.split(line)[0].rstrip()
|
||||
line = dynamic_re.sub(
|
||||
curr_line = comment_re.split(line)[0].rstrip()
|
||||
curr_line = dynamic_re.sub(
|
||||
lambda m: m.group("code"),
|
||||
line,
|
||||
curr_line,
|
||||
)
|
||||
cleaned_snippet_lines.append(line)
|
||||
cleaned_snippet_lines.append(curr_line)
|
||||
clean_defaults_snippet = "\n".join(cleaned_snippet_lines)
|
||||
|
||||
return (
|
||||
"### Settings defaults\n\n"
|
||||
"Here's overview of all available settings and their defaults:\n\n"
|
||||
+ f"```py\n{clean_defaults_snippet}\n```"
|
||||
+ "\n\n"
|
||||
f"```py\n{clean_defaults_snippet}\n```"
|
||||
"\n\n"
|
||||
)
|
||||
|
||||
|
||||
def gen_reference_tagformatters():
|
||||
def gen_reference_tagformatters() -> None:
|
||||
"""
|
||||
Generate documentation for all pre-defined TagFormatters included
|
||||
in the Python API of `django_components`.
|
||||
|
@ -387,7 +385,7 @@ def gen_reference_tagformatters():
|
|||
formatted_instances = "\n".join(formatted_instances_lines)
|
||||
f.write("### Available tag formatters\n\n" + formatted_instances)
|
||||
|
||||
for name, obj in tag_formatter_classes.items():
|
||||
for obj in tag_formatter_classes.values():
|
||||
class_name = get_import_path(obj)
|
||||
|
||||
# Generate reference entry for each TagFormatter class.
|
||||
|
@ -408,7 +406,7 @@ def gen_reference_tagformatters():
|
|||
f" show_symbol_type_toc: false\n"
|
||||
f" show_if_no_docstring: true\n"
|
||||
f" show_labels: false\n"
|
||||
f" members: false\n"
|
||||
f" members: false\n",
|
||||
)
|
||||
|
||||
f.write("\n")
|
||||
|
@ -416,10 +414,8 @@ def gen_reference_tagformatters():
|
|||
mkdocs_gen_files.set_edit_path(out_path, template_path)
|
||||
|
||||
|
||||
def gen_reference_urls():
|
||||
"""
|
||||
Generate documentation for all URLs (`urlpattern` entries) defined by django-components.
|
||||
"""
|
||||
def gen_reference_urls() -> None:
|
||||
"""Generate documentation for all URLs (`urlpattern` entries) defined by django-components."""
|
||||
module = import_module("django_components.urls")
|
||||
|
||||
preface = "<!-- Autogenerated by reference.py -->\n\n"
|
||||
|
@ -437,7 +433,7 @@ def gen_reference_urls():
|
|||
f.write("\n".join([f"- `{url_path}`\n" for url_path in all_urls]))
|
||||
|
||||
|
||||
def gen_reference_commands():
|
||||
def gen_reference_commands() -> None:
|
||||
"""
|
||||
Generate documentation for all Django admin commands defined by django-components.
|
||||
|
||||
|
@ -474,7 +470,7 @@ def gen_reference_commands():
|
|||
# becomes this:
|
||||
# `usage: python manage.py components ext run [-h]`
|
||||
cmd_usage = cmd_usage[:7] + "python manage.py " + " ".join(cmd_path) + " " + cmd_usage[7:]
|
||||
formatted_args = _format_command_args(cmd_parser, cmd_path + (cmd_def_cls.name,))
|
||||
formatted_args = _format_command_args(cmd_parser, (*cmd_path, cmd_def_cls.name))
|
||||
|
||||
# Add link to source code
|
||||
module_abs_path = import_module(cmd_def_cls.__module__).__file__
|
||||
|
@ -483,7 +479,7 @@ def gen_reference_commands():
|
|||
# NOTE: Raises `OSError` if the file is not found.
|
||||
try:
|
||||
obj_lineno = inspect.findsource(cmd_def_cls)[1]
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001
|
||||
obj_lineno = None
|
||||
|
||||
source_code_link = _format_source_code_html(module_rel_path, obj_lineno)
|
||||
|
@ -498,12 +494,12 @@ def gen_reference_commands():
|
|||
f"{source_code_link}\n\n"
|
||||
f"{cmd_summary}\n\n"
|
||||
f"{formatted_args}\n\n"
|
||||
f"{cmd_desc}\n\n"
|
||||
f"{cmd_desc}\n\n",
|
||||
)
|
||||
|
||||
# Add subcommands
|
||||
for subcmd_cls in reversed(cmd_def_cls.subcommands):
|
||||
commands_stack.append((subcmd_cls, cmd_path + (cmd_def_cls.name,)))
|
||||
commands_stack.append((subcmd_cls, (*cmd_path, cmd_def_cls.name)))
|
||||
|
||||
# TODO_v1 - REMOVE - This this section as it only for legacy commands `startcomponent` and `upgradecomponent`
|
||||
command_files = Path("./src/django_components/management/commands").glob("*.py")
|
||||
|
@ -540,13 +536,13 @@ def gen_reference_commands():
|
|||
f"{source_code_link}\n\n"
|
||||
f"{cmd_summary}\n\n"
|
||||
f"{formatted_args}\n\n"
|
||||
f"{cmd_desc}\n\n"
|
||||
f"{cmd_desc}\n\n",
|
||||
)
|
||||
|
||||
mkdocs_gen_files.set_edit_path(out_path, template_path)
|
||||
|
||||
|
||||
def gen_reference_template_tags():
|
||||
def gen_reference_template_tags() -> None:
|
||||
"""
|
||||
Generate documentation for all Django template tags defined by django-components,
|
||||
like `{% slot %}`, `{% component %}`, etc.
|
||||
|
@ -573,7 +569,7 @@ def gen_reference_template_tags():
|
|||
f.write(
|
||||
f"All following template tags are defined in\n\n"
|
||||
f"`{mod_path}`\n\n"
|
||||
f"Import as\n```django\n{{% load {mod_name} %}}\n```\n\n"
|
||||
f"Import as\n```django\n{{% load {mod_name} %}}\n```\n\n",
|
||||
)
|
||||
|
||||
for _, obj in inspect.getmembers(tags_module):
|
||||
|
@ -597,19 +593,21 @@ def gen_reference_template_tags():
|
|||
# {% component [arg, ...] **kwargs [only] %}
|
||||
# {% endcomponent %}
|
||||
# ```
|
||||
# fmt: off
|
||||
f.write(
|
||||
f"## {name}\n\n"
|
||||
f"```django\n"
|
||||
f"{tag_signature}\n"
|
||||
f"```\n\n"
|
||||
f"{source_code_link}\n\n"
|
||||
f"{docstring}\n\n"
|
||||
f"{docstring}\n\n",
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
mkdocs_gen_files.set_edit_path(out_path, template_path)
|
||||
|
||||
|
||||
def gen_reference_template_variables():
|
||||
def gen_reference_template_variables() -> None:
|
||||
"""
|
||||
Generate documentation for all variables that are available inside the component templates
|
||||
under the `{{ component_vars }}` variable, as defined by `ComponentVars`.
|
||||
|
@ -628,10 +626,8 @@ def gen_reference_template_variables():
|
|||
mkdocs_gen_files.set_edit_path(out_path, template_path)
|
||||
|
||||
|
||||
def gen_reference_extension_hooks():
|
||||
"""
|
||||
Generate documentation for the hooks that are available to the extensions.
|
||||
"""
|
||||
def gen_reference_extension_hooks() -> None:
|
||||
"""Generate documentation for the hooks that are available to the extensions."""
|
||||
module = import_module("django_components.extension")
|
||||
|
||||
preface = "<!-- Autogenerated by reference.py -->\n\n"
|
||||
|
@ -691,7 +687,7 @@ def gen_reference_extension_hooks():
|
|||
f" show_symbol_type_heading: false\n"
|
||||
f" show_symbol_type_toc: false\n"
|
||||
f" show_if_no_docstring: true\n"
|
||||
f" show_labels: false\n"
|
||||
f" show_labels: false\n",
|
||||
)
|
||||
f.write("\n")
|
||||
f.write(available_data)
|
||||
|
@ -714,7 +710,7 @@ def gen_reference_extension_hooks():
|
|||
f"::: {module.__name__}.{name}\n"
|
||||
f" options:\n"
|
||||
f" heading_level: 3\n"
|
||||
f" show_if_no_docstring: true\n"
|
||||
f" show_if_no_docstring: true\n",
|
||||
)
|
||||
|
||||
f.write("\n")
|
||||
|
@ -722,10 +718,8 @@ def gen_reference_extension_hooks():
|
|||
mkdocs_gen_files.set_edit_path(out_path, template_path)
|
||||
|
||||
|
||||
def gen_reference_extension_commands():
|
||||
"""
|
||||
Generate documentation for the objects related to defining extension commands.
|
||||
"""
|
||||
def gen_reference_extension_commands() -> None:
|
||||
"""Generate documentation for the objects related to defining extension commands."""
|
||||
module = import_module("django_components")
|
||||
|
||||
preface = "<!-- Autogenerated by reference.py -->\n\n"
|
||||
|
@ -753,7 +747,7 @@ def gen_reference_extension_commands():
|
|||
f"::: {module.__name__}.{name}\n"
|
||||
f" options:\n"
|
||||
f" heading_level: 3\n"
|
||||
f" show_if_no_docstring: true\n"
|
||||
f" show_if_no_docstring: true\n",
|
||||
)
|
||||
|
||||
f.write("\n")
|
||||
|
@ -761,10 +755,8 @@ def gen_reference_extension_commands():
|
|||
mkdocs_gen_files.set_edit_path(out_path, template_path)
|
||||
|
||||
|
||||
def gen_reference_extension_urls():
|
||||
"""
|
||||
Generate documentation for the objects related to defining extension URLs.
|
||||
"""
|
||||
def gen_reference_extension_urls() -> None:
|
||||
"""Generate documentation for the objects related to defining extension URLs."""
|
||||
module = import_module("django_components")
|
||||
|
||||
preface = "<!-- Autogenerated by reference.py -->\n\n"
|
||||
|
@ -792,7 +784,7 @@ def gen_reference_extension_urls():
|
|||
f"::: {module.__name__}.{name}\n"
|
||||
f" options:\n"
|
||||
f" heading_level: 3\n"
|
||||
f" show_if_no_docstring: true\n"
|
||||
f" show_if_no_docstring: true\n",
|
||||
)
|
||||
|
||||
f.write("\n")
|
||||
|
@ -855,7 +847,7 @@ def _extract_property_docstrings(cls: Type) -> Dict[str, str]:
|
|||
and that the body is indented with 4 spaces.
|
||||
"""
|
||||
lines, start_line_index = inspect.getsourcelines(cls)
|
||||
attrs_lines = []
|
||||
attrs_lines: List[str] = []
|
||||
ignore = True
|
||||
for line in lines:
|
||||
if ignore:
|
||||
|
@ -863,10 +855,9 @@ def _extract_property_docstrings(cls: Type) -> Dict[str, str]:
|
|||
ignore = False
|
||||
continue
|
||||
# Ignore comments
|
||||
elif line.strip().startswith("#"):
|
||||
if line.strip().startswith("#"):
|
||||
continue
|
||||
else:
|
||||
attrs_lines.append(line)
|
||||
attrs_lines.append(line)
|
||||
|
||||
attrs_docstrings = {}
|
||||
curr_attr = None
|
||||
|
@ -886,7 +877,7 @@ def _extract_property_docstrings(cls: Type) -> Dict[str, str]:
|
|||
attrs_docstrings[curr_attr] = ""
|
||||
state = "before_attr_docstring"
|
||||
elif state == "before_attr_docstring":
|
||||
if not is_one_indent or not (line.startswith("'''") or line.startswith('"""')):
|
||||
if not is_one_indent or not line.startswith(("'''", '"""')):
|
||||
continue
|
||||
# Found start of docstring
|
||||
docstring_delimiter = line[0:3]
|
||||
|
@ -909,7 +900,7 @@ def _extract_property_docstrings(cls: Type) -> Dict[str, str]:
|
|||
|
||||
# NOTE: Unlike other references, the API of Signals is not yet codified (AKA source of truth defined
|
||||
# as Python code). Instead, we manually list all signals that are sent by django-components.
|
||||
def gen_reference_signals():
|
||||
def gen_reference_signals() -> None:
|
||||
"""
|
||||
Generate documentation for all [Django Signals](https://docs.djangoproject.com/en/5.2/ref/signals) that are
|
||||
send by or during the use of django-components.
|
||||
|
@ -925,7 +916,7 @@ def gen_reference_signals():
|
|||
mkdocs_gen_files.set_edit_path(out_path, template_path)
|
||||
|
||||
|
||||
def _list_urls(urlpatterns: Sequence[Union[URLPattern, URLResolver]], prefix=""):
|
||||
def _list_urls(urlpatterns: Sequence[Union[URLPattern, URLResolver]], prefix: str = "") -> List[str]:
|
||||
"""Recursively extract all URLs and their associated views from Django's urlpatterns"""
|
||||
urls: List[str] = []
|
||||
|
||||
|
@ -1077,7 +1068,7 @@ def _parse_command_args(cmd_inputs: str) -> Dict[str, List[Dict]]:
|
|||
return data
|
||||
|
||||
|
||||
def _format_command_args(cmd_parser: ArgumentParser, cmd_path: Optional[Sequence[str]] = None):
|
||||
def _format_command_args(cmd_parser: ArgumentParser, cmd_path: Optional[Sequence[str]] = None) -> str:
|
||||
cmd_inputs: str = _gen_command_args(cmd_parser)
|
||||
parsed_cmd_inputs = _parse_command_args(cmd_inputs)
|
||||
|
||||
|
@ -1131,9 +1122,8 @@ def _is_extension_url_api(obj: Any) -> bool:
|
|||
return inspect.isclass(obj) and getattr(obj, "_extension_url_api", False)
|
||||
|
||||
|
||||
def gen_reference():
|
||||
def gen_reference() -> None:
|
||||
"""The entrypoint to generate all the reference documentation."""
|
||||
|
||||
# Set up Django settings so we can import `extensions`
|
||||
if not settings.configured:
|
||||
settings.configure(
|
||||
|
|
145
pyproject.toml
145
pyproject.toml
|
@ -10,8 +10,8 @@ description = "A way to create simple reusable template components in Django."
|
|||
keywords = ["django", "components", "css", "js", "html"]
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
{name = "Emil Stenström", email = "emil@emilstenstrom.se"},
|
||||
{name = "Juro Oravec", email = "juraj.oravec.josefson@gmail.com"},
|
||||
{ name = "Emil Stenström", email = "emil@emilstenstrom.se" },
|
||||
{ name = "Juro Oravec", email = "juraj.oravec.josefson@gmail.com" },
|
||||
]
|
||||
classifiers = [
|
||||
"Framework :: Django",
|
||||
|
@ -33,7 +33,7 @@ dependencies = [
|
|||
'djc-core-html-parser>=1.0.2',
|
||||
'typing-extensions>=4.12.2',
|
||||
]
|
||||
license = {text = "MIT"}
|
||||
license = { text = "MIT" }
|
||||
|
||||
# See https://docs.pypi.org/project_metadata/#icons
|
||||
[project.urls]
|
||||
|
@ -68,39 +68,117 @@ exclude = '''
|
|||
)/
|
||||
'''
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
line_length = 119
|
||||
multi_line_output = 3
|
||||
include_trailing_comma = "True"
|
||||
known_first_party = "django_components"
|
||||
|
||||
[tool.flake8]
|
||||
ignore = ['E302', 'W503']
|
||||
max-line-length = 119
|
||||
[tool.ruff]
|
||||
line-length = 119
|
||||
src = ["src", "tests"]
|
||||
exclude = [
|
||||
'migrations',
|
||||
'__pycache__',
|
||||
'manage.py',
|
||||
'settings.py',
|
||||
'env',
|
||||
'.env',
|
||||
'.venv',
|
||||
'.tox',
|
||||
'build',
|
||||
"migrations",
|
||||
"manage.py",
|
||||
"settings.py",
|
||||
"env",
|
||||
".env",
|
||||
# From mypy
|
||||
"test_structures",
|
||||
]
|
||||
per-file-ignores = [
|
||||
'tests/test_command_list.py:E501',
|
||||
'tests/test_component_media.py:E501',
|
||||
'tests/test_dependency_rendering.py:E501',
|
||||
|
||||
# See https://docs.astral.sh/ruff/linter/#rule-selection
|
||||
[tool.ruff.lint]
|
||||
select = ["ALL"]
|
||||
ignore = [
|
||||
# Annotations
|
||||
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed in `*args`
|
||||
# Docstring
|
||||
"D100", # Missing docstring in public module
|
||||
"D101", # Missing docstring in public class
|
||||
"D102", # Missing docstring in public method
|
||||
"D103", # Missing docstring in public function
|
||||
"D104", # Missing docstring in public package
|
||||
"D105", # Missing docstring in magic method
|
||||
"D106", # Missing docstring in public nested class
|
||||
"D107", # Missing docstring in `__init__`
|
||||
"D203", # Incorrect blank line before class
|
||||
"D205", # 1 blank line required between summary line and description
|
||||
"D212", # Multi-line docstring summary should start at the first line
|
||||
"D400", # First line should end with a period
|
||||
"D401", # First line of docstring should be in imperative mood
|
||||
"D404", # First word of the docstring should not be "This"
|
||||
"D412", # No blank lines allowed between a section header and its content ("Examples")
|
||||
"D415", # First line should end with a period, question mark, or exclamation point
|
||||
# Exceptions
|
||||
"EM101", # Exception must not use a string literal, assign to variable first
|
||||
"EM102", # Exception must not use an f-string literal, assign to variable first
|
||||
# `TODO` comments
|
||||
"FIX002", # Line contains TODO, consider resolving the issue
|
||||
"TD002", # Missing author in TODO; try: `# TODO(<author_name>): ...` or `# TODO @<author_name>: ...`
|
||||
"TD003", # Missing issue link for this TODO
|
||||
"TD004", # Missing colon in TODO
|
||||
# Code
|
||||
"C901", # `test_result_interception` is too complex (36 > 10)
|
||||
"COM812", # missing-trailing-comma (NOTE: Already handled by formatter)
|
||||
"ERA001", # Found commented-out code (NOTE: Too many false positives)
|
||||
"INP001", # File `...` is part of an implicit namespace package. Add an `__init__.py`.
|
||||
"PLR0915", # Too many statements (64 > 50)
|
||||
"PLR0911", # Too many return statements (7 > 6)
|
||||
"PLR0912", # Too many branches (31 > 12)
|
||||
"PLR0913", # Too many arguments in function definition (6 > 5)
|
||||
"PLR2004", # Magic value used in comparison, consider replacing `123` with a constant variable
|
||||
"RET504", # Unnecessary assignment to `collected` before `return` statement
|
||||
"S308", # Use of `mark_safe` may expose cross-site scripting vulnerabilities
|
||||
"S603", # `subprocess` call: check for execution of untrusted input
|
||||
"SIM108", # Use ternary operator `...` instead of `if`-`else`-block
|
||||
"SIM117", # Use a single `with` statement with multiple contexts instead of nested `with` statements
|
||||
"SLF001", # Private member accessed: `_registry`
|
||||
"TRY300", # Consider moving this statement to an `else` block
|
||||
|
||||
# TODO: Following could be useful to start using, but might require more changes.
|
||||
"C420", # Unnecessary dict comprehension for iterable; use `dict.fromkeys` instead
|
||||
"PERF401", # Use `list.extend` to create a transformed list
|
||||
"PERF203", # `try`-`except` within a loop incurs performance overhead
|
||||
"FBT001", # Boolean-typed positional argument in function definition
|
||||
"FBT002", # Boolean default positional argument in function definition
|
||||
"TRY003", # Avoid specifying long messages outside the exception class
|
||||
# TODO - Enable FA100 once we drop support for Python 3.8
|
||||
"FA100", # Add `from __future__ import annotations` to simplify `typing.Optional`
|
||||
# TODO_V1 - Rename error to suffix with `Error` before v1?
|
||||
"N818", # Exception name `NotRegistered` should be named with an Error suffix
|
||||
]
|
||||
|
||||
[tool.ruff.lint.isort]
|
||||
known-first-party = ["django_components"]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"tests/*" = [
|
||||
"ARG002", # Unused method argument: `components_settings`
|
||||
"ANN", # Annotations are not needed for tests
|
||||
"N806", # Variable `SimpleComponent` in function should be lowercase
|
||||
"PLC0415", # `import` should be at the top-level of a file
|
||||
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
"S101", # Use of `assert` detected
|
||||
"TRY002", # Create your own exception
|
||||
]
|
||||
"benchmarks/*" = [
|
||||
"ARG002", # Unused method argument: `components_settings`
|
||||
"ANN", # Annotations are not needed for tests
|
||||
"N806", # Variable `SimpleComponent` in function should be lowercase
|
||||
"PLC0415", # `import` should be at the top-level of a file
|
||||
"RUF012", # Mutable class attributes should be annotated with `typing.ClassVar`
|
||||
"S101", # Use of `assert` detected
|
||||
"TRY002", # Create your own exception
|
||||
]
|
||||
"sampleproject/*" = [
|
||||
"ARG002", # Unused method argument
|
||||
"ANN", # Annotations are not needed for tests
|
||||
"T201", # `print` found
|
||||
"DTZ", # `datetime` found
|
||||
]
|
||||
|
||||
|
||||
[tool.mypy]
|
||||
check_untyped_defs = true
|
||||
ignore_missing_imports = true
|
||||
exclude = [
|
||||
'test_structures',
|
||||
'build',
|
||||
"test_structures",
|
||||
"build",
|
||||
]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
|
@ -110,14 +188,14 @@ disallow_untyped_defs = true
|
|||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = [
|
||||
"tests"
|
||||
"tests",
|
||||
]
|
||||
asyncio_mode = "auto"
|
||||
|
||||
[tool.hatch.env]
|
||||
requires = [
|
||||
"hatch-mkdocs",
|
||||
"hatch-pip-compile"
|
||||
"hatch-pip-compile",
|
||||
]
|
||||
|
||||
[tool.hatch.envs.default]
|
||||
|
@ -126,11 +204,8 @@ dependencies = [
|
|||
"djc-core-html-parser",
|
||||
"tox",
|
||||
"pytest",
|
||||
"flake8",
|
||||
"flake8-pyproject",
|
||||
"isort",
|
||||
"ruff",
|
||||
"pre-commit",
|
||||
"black",
|
||||
"mypy",
|
||||
]
|
||||
type = "pip-compile"
|
||||
|
@ -141,9 +216,7 @@ type = "pip-compile"
|
|||
lock-filename = "requirements-docs.txt"
|
||||
detached = false
|
||||
# Dependencies are fetched automatically from the mkdocs.yml file with hatch-mkdocs
|
||||
# We only add black for formatting code in the docs
|
||||
dependencies = [
|
||||
"black",
|
||||
"pygments",
|
||||
"pygments-djc",
|
||||
"mkdocs-awesome-nav",
|
||||
|
|
|
@ -6,11 +6,8 @@ pytest
|
|||
pytest-asyncio
|
||||
pytest-django
|
||||
syrupy
|
||||
flake8
|
||||
flake8-pyproject
|
||||
isort
|
||||
ruff
|
||||
pre-commit
|
||||
black
|
||||
mypy
|
||||
playwright
|
||||
requests
|
||||
|
|
|
@ -4,33 +4,29 @@
|
|||
#
|
||||
# pip-compile requirements-dev.in
|
||||
#
|
||||
asgiref==3.8.1
|
||||
asgiref==3.9.1
|
||||
# via django
|
||||
asv==0.6.4
|
||||
# via -r requirements-dev.in
|
||||
asv-runner==0.2.1
|
||||
# via asv
|
||||
black==25.1.0
|
||||
# via -r requirements-dev.in
|
||||
build==1.2.2.post1
|
||||
build==1.3.0
|
||||
# via asv
|
||||
cachetools==5.5.2
|
||||
cachetools==6.1.0
|
||||
# via tox
|
||||
certifi==2025.1.31
|
||||
certifi==2025.8.3
|
||||
# via requests
|
||||
cfgv==3.4.0
|
||||
# via pre-commit
|
||||
chardet==5.2.0
|
||||
# via tox
|
||||
charset-normalizer==3.4.1
|
||||
charset-normalizer==3.4.3
|
||||
# via requests
|
||||
click==8.1.8
|
||||
# via black
|
||||
colorama==0.4.6
|
||||
# via tox
|
||||
distlib==0.3.9
|
||||
distlib==0.4.0
|
||||
# via virtualenv
|
||||
django==4.2.23
|
||||
django==5.2.5
|
||||
# via
|
||||
# -r requirements-dev.in
|
||||
# django-template-partials
|
||||
|
@ -38,47 +34,31 @@ django-template-partials==25.1
|
|||
# via -r requirements-dev.in
|
||||
djc-core-html-parser==1.0.2
|
||||
# via -r requirements-dev.in
|
||||
exceptiongroup==1.2.2
|
||||
# via pytest
|
||||
filelock==3.16.1
|
||||
filelock==3.19.1
|
||||
# via
|
||||
# tox
|
||||
# virtualenv
|
||||
flake8==7.3.0
|
||||
# via
|
||||
# -r requirements-dev.in
|
||||
# flake8-pyproject
|
||||
flake8-pyproject==1.2.3
|
||||
# via -r requirements-dev.in
|
||||
greenlet==3.1.1
|
||||
greenlet==3.2.4
|
||||
# via playwright
|
||||
identify==2.6.8
|
||||
identify==2.6.13
|
||||
# via pre-commit
|
||||
idna==3.10
|
||||
# via requests
|
||||
importlib-metadata==8.5.0
|
||||
# via
|
||||
# asv-runner
|
||||
# build
|
||||
iniconfig==2.0.0
|
||||
importlib-metadata==8.7.0
|
||||
# via asv-runner
|
||||
iniconfig==2.1.0
|
||||
# via pytest
|
||||
isort==6.0.1
|
||||
# via -r requirements-dev.in
|
||||
json5==0.10.0
|
||||
json5==0.12.1
|
||||
# via asv
|
||||
mccabe==0.7.0
|
||||
# via flake8
|
||||
mypy==1.17.1
|
||||
# via -r requirements-dev.in
|
||||
mypy-extensions==1.0.0
|
||||
mypy-extensions==1.1.0
|
||||
# via
|
||||
# black
|
||||
# mypy
|
||||
nodeenv==1.9.1
|
||||
# via pre-commit
|
||||
packaging==24.2
|
||||
packaging==25.0
|
||||
# via
|
||||
# black
|
||||
# build
|
||||
# pyproject-api
|
||||
# pytest
|
||||
|
@ -86,46 +66,41 @@ packaging==24.2
|
|||
pathspec==0.12.1
|
||||
# via
|
||||
# -r requirements-dev.in
|
||||
# black
|
||||
# mypy
|
||||
platformdirs==4.3.6
|
||||
platformdirs==4.3.8
|
||||
# via
|
||||
# black
|
||||
# tox
|
||||
# virtualenv
|
||||
playwright==1.48.0
|
||||
playwright==1.54.0
|
||||
# via -r requirements-dev.in
|
||||
pluggy==1.5.0
|
||||
pluggy==1.6.0
|
||||
# via
|
||||
# pytest
|
||||
# tox
|
||||
pre-commit==4.3.0
|
||||
# via -r requirements-dev.in
|
||||
pycodestyle==2.14.0
|
||||
# via flake8
|
||||
pyee==12.0.0
|
||||
pyee==13.0.0
|
||||
# via playwright
|
||||
pyflakes==3.4.0
|
||||
# via flake8
|
||||
pygments==2.19.1
|
||||
pygments==2.19.2
|
||||
# via
|
||||
# -r requirements-dev.in
|
||||
# pygments-djc
|
||||
# pytest
|
||||
pygments-djc==1.0.1
|
||||
# via -r requirements-dev.in
|
||||
pympler==1.1
|
||||
# via asv
|
||||
pyproject-api==1.8.0
|
||||
pyproject-api==1.9.1
|
||||
# via tox
|
||||
pyproject-hooks==1.2.0
|
||||
# via build
|
||||
pytest==8.3.5
|
||||
pytest==8.4.1
|
||||
# via
|
||||
# -r requirements-dev.in
|
||||
# pytest-asyncio
|
||||
# pytest-django
|
||||
# syrupy
|
||||
pytest-asyncio==0.24.0
|
||||
pytest-asyncio==1.1.0
|
||||
# via -r requirements-dev.in
|
||||
pytest-django==4.11.1
|
||||
# via -r requirements-dev.in
|
||||
|
@ -133,7 +108,9 @@ pyyaml==6.0.2
|
|||
# via
|
||||
# asv
|
||||
# pre-commit
|
||||
requests==2.32.3
|
||||
requests==2.32.4
|
||||
# via -r requirements-dev.in
|
||||
ruff==0.12.9
|
||||
# via -r requirements-dev.in
|
||||
sqlparse==0.5.3
|
||||
# via django
|
||||
|
@ -141,30 +118,16 @@ syrupy==4.9.1
|
|||
# via -r requirements-dev.in
|
||||
tabulate==0.9.0
|
||||
# via asv
|
||||
tomli==2.2.1
|
||||
# via
|
||||
# asv
|
||||
# black
|
||||
# build
|
||||
# flake8-pyproject
|
||||
# mypy
|
||||
# pyproject-api
|
||||
# pytest
|
||||
# tox
|
||||
tox==4.25.0
|
||||
tox==4.28.4
|
||||
# via -r requirements-dev.in
|
||||
types-requests==2.32.0.20241016
|
||||
types-requests==2.32.4.20250809
|
||||
# via -r requirements-dev.in
|
||||
typing-extensions==4.13.2
|
||||
typing-extensions==4.14.1
|
||||
# via
|
||||
# -r requirements-dev.in
|
||||
# asgiref
|
||||
# black
|
||||
# mypy
|
||||
# pyee
|
||||
# tox
|
||||
# virtualenv
|
||||
urllib3==2.2.3
|
||||
urllib3==2.5.0
|
||||
# via
|
||||
# requests
|
||||
# types-requests
|
||||
|
@ -174,7 +137,7 @@ virtualenv==20.34.0
|
|||
# asv
|
||||
# pre-commit
|
||||
# tox
|
||||
whitenoise==6.7.0
|
||||
whitenoise==6.9.0
|
||||
# via -r requirements-dev.in
|
||||
zipp==3.20.2
|
||||
zipp==3.23.0
|
||||
# via importlib-metadata
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
# - mkdocstrings
|
||||
# - mkdocstrings-python
|
||||
# - pymdown-extensions
|
||||
# - black
|
||||
# - pygments
|
||||
# - pygments-djc
|
||||
# - django>=4.2
|
||||
|
@ -30,8 +29,6 @@ babel==2.17.0
|
|||
# via
|
||||
# mkdocs-git-revision-date-localized-plugin
|
||||
# mkdocs-material
|
||||
black==25.1.0
|
||||
# via hatch.envs.docs
|
||||
bracex==2.6
|
||||
# via wcmatch
|
||||
cairocffi==1.7.1
|
||||
|
@ -46,7 +43,6 @@ charset-normalizer==3.4.3
|
|||
# via requests
|
||||
click==8.1.8
|
||||
# via
|
||||
# black
|
||||
# mkdocs
|
||||
colorama==0.4.6
|
||||
# via
|
||||
|
@ -154,17 +150,13 @@ mkdocstrings==0.30.0
|
|||
# mkdocstrings-python
|
||||
mkdocstrings-python==1.17.0
|
||||
# via hatch.envs.docs
|
||||
mypy-extensions==1.1.0
|
||||
# via black
|
||||
packaging==25.0
|
||||
# via
|
||||
# black
|
||||
# mkdocs
|
||||
paginate==0.5.7
|
||||
# via mkdocs-material
|
||||
pathspec==0.12.1
|
||||
# via
|
||||
# black
|
||||
# mkdocs
|
||||
pillow==11.3.0
|
||||
# via
|
||||
|
@ -172,7 +164,6 @@ pillow==11.3.0
|
|||
# mkdocs-material
|
||||
platformdirs==4.3.8
|
||||
# via
|
||||
# black
|
||||
# mkdocs-get-deps
|
||||
pycparser==2.22
|
||||
# via cffi
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from calendarapp.views import calendar
|
||||
from django.urls import path
|
||||
|
||||
from calendarapp.views import calendar
|
||||
|
||||
urlpatterns = [
|
||||
path("", calendar, name="calendar"),
|
||||
]
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
from django_components import Component, register, types
|
||||
|
||||
|
||||
|
@ -26,7 +28,7 @@ class Greeting(Component):
|
|||
return {"name": kwargs["name"]}
|
||||
|
||||
class View:
|
||||
def get(self, request, *args, **kwargs):
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
slots = {"message": "Hello, world!"}
|
||||
return Greeting.render_to_response(
|
||||
request=request,
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
from typing import NamedTuple
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
from django_components import Component, register
|
||||
|
||||
|
@ -24,7 +26,7 @@ class CalendarNested(Component):
|
|||
}
|
||||
|
||||
class View:
|
||||
def get(self, request, *args, **kwargs):
|
||||
def get(self, request: HttpRequest, *_args: Any, **_kwargs: Any) -> HttpResponse:
|
||||
return CalendarNested.render_to_response(
|
||||
request=request,
|
||||
kwargs={
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import time
|
||||
from typing import NamedTuple
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
|
||||
from django_components import Component, register, types
|
||||
|
||||
|
||||
|
@ -26,7 +28,7 @@ class Recursive(Component):
|
|||
return {"depth": kwargs.depth + 1}
|
||||
|
||||
class View:
|
||||
def get(self, request):
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
time_before = time.time()
|
||||
output = Recursive.render_to_response(
|
||||
request=request,
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
from django.urls import path
|
||||
|
||||
from components.calendar.calendar import Calendar, CalendarRelative
|
||||
from components.fragment import FragAlpine, FragJs, FragmentBaseAlpine, FragmentBaseHtmx, FragmentBaseJs
|
||||
from components.greeting import Greeting
|
||||
from components.nested.calendar.calendar import CalendarNested
|
||||
from components.recursive import Recursive
|
||||
from django.urls import path
|
||||
|
||||
urlpatterns = [
|
||||
path("greeting/", Greeting.as_view(), name="greeting"),
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# ruff: noqa: T201, S310
|
||||
import re
|
||||
import textwrap
|
||||
from collections import defaultdict
|
||||
|
@ -8,21 +9,21 @@ Version = Tuple[int, ...]
|
|||
VersionMapping = Dict[Version, List[Version]]
|
||||
|
||||
|
||||
def cut_by_content(content: str, cut_from: str, cut_to: str):
|
||||
def cut_by_content(content: str, cut_from: str, cut_to: str) -> str:
|
||||
return content.split(cut_from)[1].split(cut_to)[0]
|
||||
|
||||
|
||||
def keys_from_content(content: str):
|
||||
def keys_from_content(content: str) -> List[str]:
|
||||
return re.findall(r"<td><p>(.*?)</p></td>", content)
|
||||
|
||||
|
||||
def get_python_supported_version(url: str) -> list[Version]:
|
||||
def get_python_supported_version(url: str) -> List[Version]:
|
||||
with request.urlopen(url) as response:
|
||||
response_content = response.read()
|
||||
|
||||
content = response_content.decode("utf-8")
|
||||
|
||||
def parse_supported_versions(content: str) -> list[Version]:
|
||||
def parse_supported_versions(content: str) -> List[Version]:
|
||||
content = cut_by_content(
|
||||
content,
|
||||
'<section id="supported-versions">',
|
||||
|
@ -37,13 +38,13 @@ def get_python_supported_version(url: str) -> list[Version]:
|
|||
return parse_supported_versions(content)
|
||||
|
||||
|
||||
def get_django_to_pythoon_versions(url: str):
|
||||
def get_django_to_python_versions(url: str) -> VersionMapping:
|
||||
with request.urlopen(url) as response:
|
||||
response_content = response.read()
|
||||
|
||||
content = response_content.decode("utf-8")
|
||||
|
||||
def parse_supported_versions(content):
|
||||
def parse_supported_versions(content: str) -> VersionMapping:
|
||||
content = cut_by_content(
|
||||
content,
|
||||
'<span id="what-python-version-can-i-use-with-django">',
|
||||
|
@ -92,7 +93,7 @@ def get_django_supported_versions(url: str) -> List[Tuple[int, ...]]:
|
|||
return versions
|
||||
|
||||
|
||||
def get_latest_version(url: str):
|
||||
def get_latest_version(url: str) -> Version:
|
||||
with request.urlopen(url) as response:
|
||||
response_content = response.read()
|
||||
|
||||
|
@ -101,11 +102,11 @@ def get_latest_version(url: str):
|
|||
return version_to_tuple(version_string)
|
||||
|
||||
|
||||
def version_to_tuple(version_string: str):
|
||||
def version_to_tuple(version_string: str) -> Version:
|
||||
return tuple(int(num) for num in version_string.split("."))
|
||||
|
||||
|
||||
def build_python_to_django(django_to_python: VersionMapping, latest_version: Version):
|
||||
def build_python_to_django(django_to_python: VersionMapping, latest_version: Version) -> VersionMapping:
|
||||
python_to_django: VersionMapping = defaultdict(list)
|
||||
for django_version, python_versions in django_to_python.items():
|
||||
for python_version in python_versions:
|
||||
|
@ -116,11 +117,11 @@ def build_python_to_django(django_to_python: VersionMapping, latest_version: Ver
|
|||
return python_to_django
|
||||
|
||||
|
||||
def env_format(version_tuple, divider=""):
|
||||
def env_format(version_tuple: Version, divider: str = "") -> str:
|
||||
return divider.join(str(num) for num in version_tuple)
|
||||
|
||||
|
||||
def build_tox_envlist(python_to_django: VersionMapping):
|
||||
def build_tox_envlist(python_to_django: VersionMapping) -> str:
|
||||
lines_data = [
|
||||
(
|
||||
env_format(python_version),
|
||||
|
@ -129,11 +130,11 @@ def build_tox_envlist(python_to_django: VersionMapping):
|
|||
for python_version, django_versions in python_to_django.items()
|
||||
]
|
||||
lines = [f"py{a}-django{{{b}}}" for a, b in lines_data]
|
||||
version_lines = "\n".join([version for version in lines])
|
||||
version_lines = "\n".join(version for version in lines)
|
||||
return "envlist = \n" + textwrap.indent(version_lines, prefix=" ")
|
||||
|
||||
|
||||
def build_gh_actions_envlist(python_to_django: VersionMapping):
|
||||
def build_gh_actions_envlist(python_to_django: VersionMapping) -> str:
|
||||
lines_data = [
|
||||
(
|
||||
env_format(python_version, divider="."),
|
||||
|
@ -143,11 +144,11 @@ def build_gh_actions_envlist(python_to_django: VersionMapping):
|
|||
for python_version, django_versions in python_to_django.items()
|
||||
]
|
||||
lines = [f"{a}: py{b}-django{{{c}}}" for a, b, c in lines_data]
|
||||
version_lines = "\n".join([version for version in lines])
|
||||
version_lines = "\n".join(version for version in lines)
|
||||
return "python = \n" + textwrap.indent(version_lines, prefix=" ")
|
||||
|
||||
|
||||
def build_deps_envlist(python_to_django: VersionMapping):
|
||||
def build_deps_envlist(python_to_django: VersionMapping) -> str:
|
||||
all_django_versions = set()
|
||||
for django_versions in python_to_django.values():
|
||||
for django_version in django_versions:
|
||||
|
@ -165,7 +166,7 @@ def build_deps_envlist(python_to_django: VersionMapping):
|
|||
return "deps = \n" + textwrap.indent("\n".join(lines), prefix=" ")
|
||||
|
||||
|
||||
def build_pypi_classifiers(python_to_django: VersionMapping):
|
||||
def build_pypi_classifiers(python_to_django: VersionMapping) -> str:
|
||||
classifiers = []
|
||||
|
||||
all_python_versions = python_to_django.keys()
|
||||
|
@ -183,14 +184,14 @@ def build_pypi_classifiers(python_to_django: VersionMapping):
|
|||
return textwrap.indent("classifiers=[\n", prefix=" " * 4) + textwrap.indent("\n".join(classifiers), prefix=" " * 8)
|
||||
|
||||
|
||||
def build_readme(python_to_django: VersionMapping):
|
||||
def build_readme(python_to_django: VersionMapping) -> str:
|
||||
print(
|
||||
textwrap.dedent(
|
||||
"""\
|
||||
| Python version | Django version |
|
||||
|----------------|--------------------------|
|
||||
""".rstrip()
|
||||
)
|
||||
""".rstrip(),
|
||||
),
|
||||
)
|
||||
lines_data = [
|
||||
(
|
||||
|
@ -200,24 +201,25 @@ def build_readme(python_to_django: VersionMapping):
|
|||
for python_version, django_versions in python_to_django.items()
|
||||
]
|
||||
lines = [f"| {a: <14} | {b: <24} |" for a, b in lines_data]
|
||||
version_lines = "\n".join([version for version in lines])
|
||||
version_lines = "\n".join(version for version in lines)
|
||||
return version_lines
|
||||
|
||||
|
||||
def build_pyenv(python_to_django: VersionMapping):
|
||||
def build_pyenv(python_to_django: VersionMapping) -> str:
|
||||
lines = []
|
||||
all_python_versions = python_to_django.keys()
|
||||
for python_version in all_python_versions:
|
||||
lines.append(f'pyenv install -s {env_format(python_version, divider=".")}')
|
||||
lines.append(f"pyenv install -s {env_format(python_version, divider='.')}")
|
||||
|
||||
lines.append(f'pyenv local {" ".join(env_format(version, divider=".") for version in all_python_versions)}')
|
||||
versions_str = " ".join(env_format(version, divider=".") for version in all_python_versions)
|
||||
lines.append(f"pyenv local {versions_str}")
|
||||
|
||||
lines.append("tox -p")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def build_ci_python_versions(python_to_django: Dict[str, str]):
|
||||
def build_ci_python_versions(python_to_django: VersionMapping) -> str:
|
||||
# Outputs python-version, like: ['3.8', '3.9', '3.10', '3.11', '3.12']
|
||||
lines = [
|
||||
f"'{env_format(python_version, divider='.')}'" for python_version, django_versions in python_to_django.items()
|
||||
|
@ -226,13 +228,13 @@ def build_ci_python_versions(python_to_django: Dict[str, str]):
|
|||
return lines_formatted
|
||||
|
||||
|
||||
def filter_dict(d: Dict, filter_fn: Callable[[Any], bool]):
|
||||
def filter_dict(d: Dict, filter_fn: Callable[[Any], bool]) -> Dict:
|
||||
return dict(filter(filter_fn, d.items()))
|
||||
|
||||
|
||||
def main():
|
||||
def main() -> None:
|
||||
active_python = get_python_supported_version("https://devguide.python.org/versions/")
|
||||
django_to_python = get_django_to_pythoon_versions("https://docs.djangoproject.com/en/dev/faq/install/")
|
||||
django_to_python = get_django_to_python_versions("https://docs.djangoproject.com/en/dev/faq/install/")
|
||||
django_supported_versions = get_django_supported_versions("https://www.djangoproject.com/download/")
|
||||
latest_version = get_latest_version("https://www.djangoproject.com/download/")
|
||||
|
||||
|
|
|
@ -35,19 +35,22 @@ Configuration:
|
|||
See the code for more details and examples.
|
||||
"""
|
||||
|
||||
# ruff: noqa: T201,BLE001,PTH118
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
import sys
|
||||
import time
|
||||
from collections import defaultdict, deque
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import DefaultDict, Deque, Dict, List, Tuple, Union
|
||||
from typing import DefaultDict, Deque, Dict, List, Literal, Optional, Tuple, Union
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
import pathspec
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from django_components.util.misc import format_as_ascii_table
|
||||
|
||||
|
@ -77,7 +80,7 @@ IGNORED_PATHS = [
|
|||
IGNORE_DOMAINS = [
|
||||
"127.0.0.1",
|
||||
"localhost",
|
||||
"0.0.0.0",
|
||||
"0.0.0.0", # noqa: S104
|
||||
"example.com",
|
||||
]
|
||||
|
||||
|
@ -112,9 +115,35 @@ URL_VALIDATOR_REGEX = re.compile(
|
|||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Link:
|
||||
file: str
|
||||
lineno: int
|
||||
url: str
|
||||
base_url: str # The URL without the fragment
|
||||
fragment: Optional[str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class LinkRewrite:
|
||||
link: Link
|
||||
new_url: str
|
||||
mapping_key: Union[str, re.Pattern]
|
||||
|
||||
|
||||
@dataclass
|
||||
class LinkError:
|
||||
link: Link
|
||||
error_type: Literal["ERROR_FRAGMENT", "ERROR_HTTP", "ERROR_INVALID", "ERROR_OTHER"]
|
||||
error_details: str
|
||||
|
||||
|
||||
FetchedResults = Dict[str, Union[requests.Response, Exception, Literal["SKIPPED", "INVALID_URL"]]]
|
||||
|
||||
|
||||
def is_binary_file(filepath: Path) -> bool:
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
with filepath.open("rb") as f:
|
||||
chunk = f.read(1024)
|
||||
if b"\0" in chunk:
|
||||
return True
|
||||
|
@ -127,7 +156,7 @@ def load_gitignore(root: Path) -> pathspec.PathSpec:
|
|||
gitignore = root / ".gitignore"
|
||||
patterns = []
|
||||
if gitignore.exists():
|
||||
with open(gitignore) as f:
|
||||
with gitignore.open() as f:
|
||||
patterns = f.read().splitlines()
|
||||
# Add additional ignored paths
|
||||
patterns += IGNORED_PATHS
|
||||
|
@ -153,29 +182,33 @@ def find_files(root: Path, spec: pathspec.PathSpec) -> List[Path]:
|
|||
|
||||
|
||||
# Extract URLs from a file
|
||||
def extract_urls_from_file(filepath: Path) -> List[Tuple[str, int, str, str]]:
|
||||
urls = []
|
||||
def extract_links_from_file(filepath: Path) -> List[Link]:
|
||||
urls: List[Link] = []
|
||||
try:
|
||||
with open(filepath, encoding="utf-8", errors="replace") as f:
|
||||
with filepath.open(encoding="utf-8", errors="replace") as f:
|
||||
for i, line in enumerate(f, 1):
|
||||
for match in URL_REGEX.finditer(line):
|
||||
url = match.group(0)
|
||||
urls.append((str(filepath), i, line.rstrip(), url))
|
||||
if "#" in url:
|
||||
base_url, fragment = url.split("#", 1)
|
||||
else:
|
||||
base_url, fragment = url, None
|
||||
urls.append(Link(file=str(filepath), lineno=i, url=url, base_url=base_url, fragment=fragment))
|
||||
except Exception as e:
|
||||
print(f"[WARN] Could not read {filepath}: {e}", file=sys.stderr)
|
||||
return urls
|
||||
|
||||
|
||||
def get_base_url(url: str) -> str:
|
||||
"""Return the URL without the fragment."""
|
||||
return url.split("#", 1)[0]
|
||||
|
||||
|
||||
def pick_next_url(domains, domain_to_urls, last_request_time):
|
||||
"""
|
||||
Pick the next (domain, url) to fetch, respecting REQUEST_DELAY per domain.
|
||||
Returns (domain, url) or None if all are on cooldown or empty.
|
||||
"""
|
||||
# We validate the links by fetching them, reaching the (potentially 3rd party) servers.
|
||||
# This can be slow, because servers am have rate limiting policies.
|
||||
# So we group the URLs by domain - URLs pointing to different domains can be
|
||||
# fetched in parallel. This way we can spread the load over the domains, and avoid hitting the rate limits.
|
||||
# This function picks the next URL to fetch, respecting the cooldown.
|
||||
def pick_next_url(
|
||||
domains: List[str],
|
||||
domain_to_urls: Dict[str, Deque[str]],
|
||||
last_request_time: Dict[str, float],
|
||||
) -> Optional[Tuple[str, str]]:
|
||||
now = time.time()
|
||||
for domain in domains:
|
||||
if not domain_to_urls[domain]:
|
||||
|
@ -187,16 +220,23 @@ def pick_next_url(domains, domain_to_urls, last_request_time):
|
|||
return None
|
||||
|
||||
|
||||
def validate_urls(all_urls):
|
||||
def fetch_urls(links: List[Link]) -> FetchedResults:
|
||||
"""
|
||||
For each unique base URL, make a GET request (with caching).
|
||||
For each unique URL, make a GET request (with caching).
|
||||
Print progress for each request (including cache hits).
|
||||
If a URL is invalid, print a warning and skip fetching.
|
||||
Skip URLs whose netloc matches IGNORE_DOMAINS.
|
||||
Use round-robin scheduling per domain, with cooldown.
|
||||
"""
|
||||
url_cache: Dict[str, Union[requests.Response, Exception, str]] = {}
|
||||
unique_base_urls = sorted(set(get_base_url(url) for _, _, _, url in all_urls))
|
||||
all_url_results: FetchedResults = {}
|
||||
unique_base_urls = set()
|
||||
base_urls_with_fragments = set()
|
||||
for link in links:
|
||||
unique_base_urls.add(link.base_url)
|
||||
if link.fragment:
|
||||
base_urls_with_fragments.add(link.base_url)
|
||||
|
||||
base_urls = sorted(unique_base_urls) # Ensure consistency
|
||||
|
||||
# NOTE: Originally we fetched the URLs one after another. But the issue with this was that
|
||||
# there is a few large domains like Github, MDN, Djagno docs, etc. And there's a lot of URLs
|
||||
|
@ -208,10 +248,10 @@ def validate_urls(all_urls):
|
|||
|
||||
# Group URLs by domain
|
||||
domain_to_urls: DefaultDict[str, Deque[str]] = defaultdict(deque)
|
||||
for url in unique_base_urls:
|
||||
for url in base_urls:
|
||||
parsed = urlparse(url)
|
||||
if parsed.hostname and any(parsed.hostname == d for d in IGNORE_DOMAINS):
|
||||
url_cache[url] = "SKIPPED"
|
||||
all_url_results[url] = "SKIPPED"
|
||||
continue
|
||||
domain_to_urls[parsed.netloc].append(url)
|
||||
|
||||
|
@ -236,37 +276,83 @@ def validate_urls(all_urls):
|
|||
domain, url = pick
|
||||
|
||||
# Classify and fetch
|
||||
if url in url_cache:
|
||||
if url in all_url_results:
|
||||
print(f"[done {done_count + 1}/{total_urls}] {url} (cache hit)")
|
||||
done_count += 1
|
||||
continue
|
||||
if not URL_VALIDATOR_REGEX.match(url):
|
||||
url_cache[url] = "INVALID_URL"
|
||||
all_url_results[url] = "INVALID_URL"
|
||||
print(f"[done {done_count + 1}/{total_urls}] {url} WARNING: Invalid URL format, not fetched.")
|
||||
done_count += 1
|
||||
continue
|
||||
|
||||
print(f"[done {done_count + 1}/{total_urls}] {url} ...", end=" ")
|
||||
method = "GET" if url in base_urls_with_fragments else "HEAD"
|
||||
print(f"[done {done_count + 1}/{total_urls}] {method:<4} {url} ...", end=" ")
|
||||
try:
|
||||
resp = requests.get(
|
||||
url, timeout=REQUEST_TIMEOUT, headers={"User-Agent": "django-components-link-checker/0.1"}
|
||||
)
|
||||
url_cache[url] = resp
|
||||
# If there is at least one URL that specifies a fragment in the URL,
|
||||
# we will fetch the full HTML with GET.
|
||||
# But if there isn't any, we can simply send HEAD request instead.
|
||||
if method == "GET":
|
||||
resp = requests.get(
|
||||
url,
|
||||
allow_redirects=True,
|
||||
timeout=REQUEST_TIMEOUT,
|
||||
headers={"User-Agent": "django-components-link-checker/0.1"},
|
||||
)
|
||||
else:
|
||||
resp = requests.head(
|
||||
url,
|
||||
allow_redirects=True,
|
||||
timeout=REQUEST_TIMEOUT,
|
||||
headers={"User-Agent": "django-components-link-checker/0.1"},
|
||||
)
|
||||
all_url_results[url] = resp
|
||||
print(f"{resp.status_code}")
|
||||
except Exception as err:
|
||||
url_cache[url] = err
|
||||
all_url_results[url] = err
|
||||
print(f"ERROR: {err}")
|
||||
|
||||
last_request_time[domain] = time.time()
|
||||
done_count += 1
|
||||
return url_cache
|
||||
return all_url_results
|
||||
|
||||
|
||||
def check_fragment_in_html(html: str, fragment: str) -> bool:
|
||||
"""Return True if id=fragment exists in the HTML."""
|
||||
print(f"Checking fragment {fragment} in HTML...")
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
return bool(soup.find(id=fragment))
|
||||
def rewrite_links(links: List[Link], files: List[Path], dry_run: bool) -> None:
|
||||
# Group by file for efficient rewriting
|
||||
file_to_lines: Dict[str, List[str]] = {}
|
||||
for filepath in files:
|
||||
try:
|
||||
with filepath.open(encoding="utf-8", errors="replace") as f:
|
||||
file_to_lines[str(filepath)] = f.readlines()
|
||||
except Exception as e:
|
||||
print(f"[WARN] Could not read {filepath}: {e}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
rewrites: List[LinkRewrite] = []
|
||||
for link in links:
|
||||
new_url, mapping_key = rewrite_url(link.url)
|
||||
if not new_url or new_url == link.url or mapping_key is None:
|
||||
continue
|
||||
|
||||
# Rewrite in memory, so we can have dry-run mode
|
||||
lines = file_to_lines[link.file]
|
||||
idx = link.lineno - 1
|
||||
old_line = lines[idx]
|
||||
new_line = old_line.replace(link.url, new_url)
|
||||
if old_line != new_line:
|
||||
lines[idx] = new_line
|
||||
rewrites.append(LinkRewrite(link=link, new_url=new_url, mapping_key=mapping_key))
|
||||
|
||||
# Write back or dry-run
|
||||
if dry_run:
|
||||
for rewrite in rewrites:
|
||||
print(f"[DRY-RUN] {rewrite.link.file}#{rewrite.link.lineno}: {rewrite.link.url} -> {rewrite.new_url}")
|
||||
else:
|
||||
for rewrite in rewrites:
|
||||
# Write only once per file
|
||||
lines = file_to_lines[rewrite.link.file]
|
||||
Path(rewrite.link.file).write_text("".join(lines), encoding="utf-8")
|
||||
print(f"[REWRITE] {rewrite.link.file}#{rewrite.link.lineno}: {rewrite.link.url} -> {rewrite.new_url}")
|
||||
|
||||
|
||||
def rewrite_url(url: str) -> Union[Tuple[None, None], Tuple[str, Union[str, re.Pattern]]]:
|
||||
|
@ -279,16 +365,82 @@ def rewrite_url(url: str) -> Union[Tuple[None, None], Tuple[str, Union[str, re.P
|
|||
if key.search(url):
|
||||
return key.sub(repl, url), key
|
||||
else:
|
||||
raise ValueError(f"Invalid key type: {type(key)}")
|
||||
raise TypeError(f"Invalid key type: {type(key)}")
|
||||
return None, None
|
||||
|
||||
|
||||
def output_summary(errors: List[Tuple[str, int, str, str, str]], output: str):
|
||||
def check_links_for_errors(all_urls: List[Link], all_url_results: FetchedResults) -> List[LinkError]:
|
||||
errors: List[LinkError] = []
|
||||
for link in all_urls:
|
||||
cache_val = all_url_results.get(link.base_url)
|
||||
|
||||
if cache_val == "SKIPPED":
|
||||
continue
|
||||
|
||||
if cache_val == "INVALID_URL":
|
||||
link_error = LinkError(link=link, error_type="ERROR_INVALID", error_details="Invalid URL format")
|
||||
errors.append(link_error)
|
||||
continue
|
||||
|
||||
if isinstance(cache_val, Exception):
|
||||
link_error = LinkError(link=link, error_type="ERROR_OTHER", error_details=str(cache_val))
|
||||
errors.append(link_error)
|
||||
continue
|
||||
|
||||
if isinstance(cache_val, requests.Response):
|
||||
# Error response
|
||||
if hasattr(cache_val, "status_code") and getattr(cache_val, "status_code", 0) != 200:
|
||||
link_error = LinkError(
|
||||
link=link,
|
||||
error_type="ERROR_HTTP",
|
||||
error_details=f"Status {getattr(cache_val, 'status_code', '?')}",
|
||||
)
|
||||
errors.append(link_error)
|
||||
continue
|
||||
|
||||
# Success response
|
||||
if cache_val and hasattr(cache_val, "text") and link.fragment:
|
||||
content_type = cache_val.headers.get("Content-Type", "")
|
||||
if "html" not in content_type:
|
||||
# The specified URL does NOT point to an HTML page, so the fragment is not valid.
|
||||
link_error = LinkError(link=link, error_type="ERROR_FRAGMENT", error_details="Not HTML content")
|
||||
errors.append(link_error)
|
||||
continue
|
||||
|
||||
fragment_in_html = check_fragment_in_html(cache_val.text, link.fragment)
|
||||
if not fragment_in_html:
|
||||
# The specified URL points to an HTML page, but the fragment is not valid.
|
||||
link_error = LinkError(
|
||||
link=link,
|
||||
error_type="ERROR_FRAGMENT",
|
||||
error_details=f"Fragment '#{link.fragment}' not found",
|
||||
)
|
||||
errors.append(link_error)
|
||||
continue
|
||||
|
||||
else:
|
||||
raise TypeError(f"Unknown cache value type: {type(cache_val)}")
|
||||
return errors
|
||||
|
||||
|
||||
def check_fragment_in_html(html: str, fragment: str) -> bool:
|
||||
"""Return True if id=fragment exists in the HTML."""
|
||||
print(f"Checking fragment {fragment} in HTML...")
|
||||
soup = BeautifulSoup(html, "html.parser")
|
||||
return bool(soup.find(id=fragment))
|
||||
|
||||
|
||||
def output_summary(errors: List[LinkError], output: Optional[str]) -> None:
|
||||
# Format the errors into a table
|
||||
headers = ["Type", "Details", "File", "URL"]
|
||||
data = [
|
||||
{"File": file + "#" + str(lineno), "Type": errtype, "URL": url, "Details": details}
|
||||
for file, lineno, errtype, url, details in errors
|
||||
{
|
||||
"File": link_error.link.file + "#" + str(link_error.link.lineno),
|
||||
"Type": link_error.error_type,
|
||||
"URL": link_error.link.url,
|
||||
"Details": link_error.error_details,
|
||||
}
|
||||
for link_error in errors
|
||||
]
|
||||
table = format_as_ascii_table(data, headers, include_headers=True)
|
||||
|
||||
|
@ -300,106 +452,59 @@ def output_summary(errors: List[Tuple[str, int, str, str, str]], output: str):
|
|||
print(table + "\n")
|
||||
|
||||
|
||||
# TODO: Run this as a test in CI?
|
||||
# NOTE: At v0.140 there was ~800 URL instances total, ~300 unique URLs, and the script took 4 min.
|
||||
def main():
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="Validate links and fragments in the codebase.")
|
||||
parser.add_argument(
|
||||
"-o", "--output", type=str, help="Output summary table to file (suppress stdout except errors)"
|
||||
"-o",
|
||||
"--output",
|
||||
type=str,
|
||||
help="Output summary table to file (suppress stdout except errors)",
|
||||
)
|
||||
parser.add_argument("--rewrite", action="store_true", help="Rewrite URLs using URL_REWRITE_MAP and update files")
|
||||
parser.add_argument(
|
||||
"--dry-run", action="store_true", help="Show what would be changed by --rewrite, but do not write files"
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Show what would be changed by --rewrite, but do not write files",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
return parser.parse_args()
|
||||
|
||||
root = Path(os.getcwd())
|
||||
|
||||
# TODO: Run this as a test in CI?
|
||||
# NOTE: At v0.140 there was ~800 URL instances total, ~300 unique URLs, and the script took 4 min.
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
|
||||
# Find all relevant files
|
||||
root = Path.cwd()
|
||||
spec = load_gitignore(root)
|
||||
|
||||
files = find_files(root, spec)
|
||||
print(f"Scanning {len(files)} files...")
|
||||
|
||||
all_urls: List[Tuple[str, int, str, str]] = []
|
||||
for f in files:
|
||||
if is_binary_file(f):
|
||||
# Find links in those files
|
||||
all_links: List[Link] = []
|
||||
for filepath in files:
|
||||
if is_binary_file(filepath):
|
||||
continue
|
||||
all_urls.extend(extract_urls_from_file(f))
|
||||
all_links.extend(extract_links_from_file(filepath))
|
||||
|
||||
# HTTP request and caching step
|
||||
url_cache = validate_urls(all_urls)
|
||||
|
||||
# --- URL rewriting logic ---
|
||||
# Rewrite links in those files if requested
|
||||
if args.rewrite:
|
||||
# Group by file for efficient rewriting
|
||||
file_to_lines: Dict[str, List[str]] = {}
|
||||
for f in files:
|
||||
try:
|
||||
with open(f, encoding="utf-8", errors="replace") as fh:
|
||||
file_to_lines[str(f)] = fh.readlines()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
rewrites = []
|
||||
for file, lineno, line, url in all_urls:
|
||||
new_url, mapping_key = rewrite_url(url)
|
||||
if not new_url or new_url == url:
|
||||
continue
|
||||
|
||||
# Rewrite in memory, so we can have dry-run mode
|
||||
lines = file_to_lines[file]
|
||||
idx = lineno - 1
|
||||
old_line = lines[idx]
|
||||
new_line = old_line.replace(url, new_url)
|
||||
if old_line != new_line:
|
||||
lines[idx] = new_line
|
||||
rewrites.append((file, lineno, url, new_url, mapping_key))
|
||||
|
||||
# Write back or dry-run
|
||||
if args.dry_run:
|
||||
for file, lineno, old, new, _ in rewrites:
|
||||
print(f"[DRY-RUN] {file}#{lineno}: {old} -> {new}")
|
||||
else:
|
||||
for file, _, _, _, _ in rewrites:
|
||||
# Write only once per file
|
||||
lines = file_to_lines[file]
|
||||
Path(file).write_text("".join(lines), encoding="utf-8")
|
||||
for file, lineno, old, new, _ in rewrites:
|
||||
print(f"[REWRITE] {file}#{lineno}: {old} -> {new}")
|
||||
|
||||
rewrite_links(all_links, files, dry_run=args.dry_run)
|
||||
return # After rewriting, skip error reporting
|
||||
|
||||
# --- Categorize the results / errors ---
|
||||
errors = []
|
||||
for file, lineno, line, url in all_urls:
|
||||
base_url = get_base_url(url)
|
||||
fragment = url.split("#", 1)[1] if "#" in url else None
|
||||
cache_val = url_cache.get(base_url)
|
||||
|
||||
if cache_val == "SKIPPED":
|
||||
continue
|
||||
elif cache_val == "INVALID_URL":
|
||||
errors.append((file, lineno, "INVALID", url, "Invalid URL format"))
|
||||
continue
|
||||
elif isinstance(cache_val, Exception):
|
||||
errors.append((file, lineno, "ERROR", url, str(cache_val)))
|
||||
continue
|
||||
elif hasattr(cache_val, "status_code") and getattr(cache_val, "status_code", 0) != 200:
|
||||
errors.append((file, lineno, "ERROR_HTTP", url, f"Status {getattr(cache_val, 'status_code', '?')}"))
|
||||
continue
|
||||
elif fragment and hasattr(cache_val, "text"):
|
||||
content_type = cache_val.headers.get("Content-Type", "")
|
||||
if "html" not in content_type:
|
||||
errors.append((file, lineno, "ERROR_FRAGMENT", url, "Not HTML content"))
|
||||
continue
|
||||
if not check_fragment_in_html(cache_val.text, fragment):
|
||||
errors.append((file, lineno, "ERROR_FRAGMENT", url, f"Fragment '#{fragment}' not found"))
|
||||
# Otherwise proceed to validation of the URLs and fragments
|
||||
# by first fetching the HTTP requests.
|
||||
all_url_results = fetch_urls(all_links)
|
||||
|
||||
# After everything's fetched, check for errors.
|
||||
errors = check_links_for_errors(all_links, all_url_results)
|
||||
if not errors:
|
||||
print("\nAll links and fragments are valid!")
|
||||
return
|
||||
|
||||
# Format the errors into a table
|
||||
output_summary(errors, args.output)
|
||||
output_summary(errors, args.output or None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
@ -76,7 +76,7 @@ from django_components.tag_formatter import (
|
|||
component_shorthand_formatter,
|
||||
)
|
||||
from django_components.template import cached_template
|
||||
import django_components.types as types
|
||||
import django_components.types as types # noqa: PLR0402
|
||||
from django_components.util.loader import ComponentFileEntry, get_component_dirs, get_component_files
|
||||
from django_components.util.routing import URLRoute, URLRouteHandler
|
||||
from django_components.util.types import Empty
|
||||
|
@ -85,12 +85,8 @@ from django_components.util.types import Empty
|
|||
|
||||
|
||||
__all__ = [
|
||||
"all_components",
|
||||
"all_registries",
|
||||
"AlreadyRegistered",
|
||||
"autodiscover",
|
||||
"BaseNode",
|
||||
"cached_template",
|
||||
"CommandArg",
|
||||
"CommandArgGroup",
|
||||
"CommandHandler",
|
||||
|
@ -113,8 +109,6 @@ __all__ = [
|
|||
"ComponentVars",
|
||||
"ComponentView",
|
||||
"ComponentsSettings",
|
||||
"component_formatter",
|
||||
"component_shorthand_formatter",
|
||||
"ContextBehavior",
|
||||
"Default",
|
||||
"DependenciesStrategy",
|
||||
|
@ -122,13 +116,6 @@ __all__ = [
|
|||
"Empty",
|
||||
"ExtensionComponentConfig",
|
||||
"FillNode",
|
||||
"format_attributes",
|
||||
"get_component_by_class_id",
|
||||
"get_component_dirs",
|
||||
"get_component_files",
|
||||
"get_component_url",
|
||||
"import_libraries",
|
||||
"merge_attributes",
|
||||
"NotRegistered",
|
||||
"OnComponentClassCreatedContext",
|
||||
"OnComponentClassDeletedContext",
|
||||
|
@ -140,10 +127,7 @@ __all__ = [
|
|||
"OnRegistryDeletedContext",
|
||||
"OnRenderGenerator",
|
||||
"ProvideNode",
|
||||
"register",
|
||||
"registry",
|
||||
"RegistrySettings",
|
||||
"render_dependencies",
|
||||
"ShorthandComponentFormatter",
|
||||
"Slot",
|
||||
"SlotContent",
|
||||
|
@ -157,8 +141,24 @@ __all__ = [
|
|||
"TagFormatterABC",
|
||||
"TagProtectedError",
|
||||
"TagResult",
|
||||
"template_tag",
|
||||
"types",
|
||||
"URLRoute",
|
||||
"URLRouteHandler",
|
||||
"all_components",
|
||||
"all_registries",
|
||||
"autodiscover",
|
||||
"cached_template",
|
||||
"component_formatter",
|
||||
"component_shorthand_formatter",
|
||||
"format_attributes",
|
||||
"get_component_by_class_id",
|
||||
"get_component_dirs",
|
||||
"get_component_files",
|
||||
"get_component_url",
|
||||
"import_libraries",
|
||||
"merge_attributes",
|
||||
"register",
|
||||
"registry",
|
||||
"render_dependencies",
|
||||
"template_tag",
|
||||
"types",
|
||||
]
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# ruff: noqa: N802, PLC0415
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
@ -440,7 +441,7 @@ class ComponentsSettings(NamedTuple):
|
|||
reload_on_template_change: Optional[bool] = None
|
||||
"""Deprecated. Use
|
||||
[`COMPONENTS.reload_on_file_change`](./settings.md#django_components.app_settings.ComponentsSettings.reload_on_file_change)
|
||||
instead.""" # noqa: E501
|
||||
instead."""
|
||||
|
||||
reload_on_file_change: Optional[bool] = None
|
||||
"""
|
||||
|
@ -515,7 +516,7 @@ class ComponentsSettings(NamedTuple):
|
|||
forbidden_static_files: Optional[List[Union[str, re.Pattern]]] = None
|
||||
"""Deprecated. Use
|
||||
[`COMPONENTS.static_files_forbidden`](./settings.md#django_components.app_settings.ComponentsSettings.static_files_forbidden)
|
||||
instead.""" # noqa: E501
|
||||
instead."""
|
||||
|
||||
static_files_forbidden: Optional[List[Union[str, re.Pattern]]] = None
|
||||
"""
|
||||
|
@ -693,7 +694,7 @@ class Dynamic(Generic[T]):
|
|||
# for `COMPONENTS.dirs`, we do it lazily.
|
||||
# NOTE 2: We show the defaults in the documentation, together with the comments
|
||||
# (except for the `Dynamic` instances and comments like `type: ignore`).
|
||||
# So `fmt: off` turns off Black formatting and `snippet:defaults` allows
|
||||
# So `fmt: off` turns off Black/Ruff formatting and `snippet:defaults` allows
|
||||
# us to extract the snippet from the file.
|
||||
#
|
||||
# fmt: off
|
||||
|
@ -757,7 +758,7 @@ class InternalSettings:
|
|||
|
||||
# For DIRS setting, we use a getter for the default value, because the default value
|
||||
# uses Django settings, which may not yet be initialized at the time these settings are generated.
|
||||
dirs_default_fn = cast(Dynamic[Sequence[Union[str, Tuple[str, str]]]], defaults.dirs)
|
||||
dirs_default_fn = cast("Dynamic[Sequence[Union[str, Tuple[str, str]]]]", defaults.dirs)
|
||||
dirs_default = dirs_default_fn.getter()
|
||||
|
||||
self._settings = ComponentsSettings(
|
||||
|
@ -766,11 +767,13 @@ class InternalSettings:
|
|||
dirs=default(components_settings.dirs, dirs_default),
|
||||
app_dirs=default(components_settings.app_dirs, defaults.app_dirs),
|
||||
debug_highlight_components=default(
|
||||
components_settings.debug_highlight_components, defaults.debug_highlight_components
|
||||
components_settings.debug_highlight_components,
|
||||
defaults.debug_highlight_components,
|
||||
),
|
||||
debug_highlight_slots=default(components_settings.debug_highlight_slots, defaults.debug_highlight_slots),
|
||||
dynamic_component_name=default(
|
||||
components_settings.dynamic_component_name, defaults.dynamic_component_name
|
||||
components_settings.dynamic_component_name,
|
||||
defaults.dynamic_component_name,
|
||||
),
|
||||
libraries=default(components_settings.libraries, defaults.libraries),
|
||||
# NOTE: Internally we store the extensions as a list of instances, but the user
|
||||
|
@ -789,11 +792,12 @@ class InternalSettings:
|
|||
def _get_settings(self) -> ComponentsSettings:
|
||||
if self._settings is None:
|
||||
self._load_settings()
|
||||
return cast(ComponentsSettings, self._settings)
|
||||
return cast("ComponentsSettings", self._settings)
|
||||
|
||||
def _prepare_extensions(self, new_settings: ComponentsSettings) -> List["ComponentExtension"]:
|
||||
extensions: Sequence[Union[Type["ComponentExtension"], str]] = default(
|
||||
new_settings.extensions, cast(List[str], defaults.extensions)
|
||||
extensions: Sequence[Union[Type[ComponentExtension], str]] = default(
|
||||
new_settings.extensions,
|
||||
cast("List[str]", defaults.extensions),
|
||||
)
|
||||
|
||||
# Prepend built-in extensions
|
||||
|
@ -804,7 +808,7 @@ class InternalSettings:
|
|||
from django_components.extensions.view import ViewExtension
|
||||
|
||||
extensions = cast(
|
||||
List[Type["ComponentExtension"]],
|
||||
"List[Type[ComponentExtension]]",
|
||||
[
|
||||
CacheExtension,
|
||||
DefaultsExtension,
|
||||
|
@ -815,12 +819,12 @@ class InternalSettings:
|
|||
) + list(extensions)
|
||||
|
||||
# Extensions may be passed in either as classes or import strings.
|
||||
extension_instances: List["ComponentExtension"] = []
|
||||
extension_instances: List[ComponentExtension] = []
|
||||
for extension in extensions:
|
||||
if isinstance(extension, str):
|
||||
import_path, class_name = extension.rsplit(".", 1)
|
||||
extension_module = import_module(import_path)
|
||||
extension = cast(Type["ComponentExtension"], getattr(extension_module, class_name))
|
||||
extension = cast("Type[ComponentExtension]", getattr(extension_module, class_name)) # noqa: PLW2901
|
||||
|
||||
if isinstance(extension, type):
|
||||
extension_instance = extension()
|
||||
|
@ -837,7 +841,7 @@ class InternalSettings:
|
|||
if val is None:
|
||||
val = new_settings.reload_on_template_change
|
||||
|
||||
return default(val, cast(bool, defaults.reload_on_file_change))
|
||||
return default(val, cast("bool", defaults.reload_on_file_change))
|
||||
|
||||
def _prepare_static_files_forbidden(self, new_settings: ComponentsSettings) -> List[Union[str, re.Pattern]]:
|
||||
val = new_settings.static_files_forbidden
|
||||
|
@ -845,18 +849,18 @@ class InternalSettings:
|
|||
if val is None:
|
||||
val = new_settings.forbidden_static_files
|
||||
|
||||
return default(val, cast(List[Union[str, re.Pattern]], defaults.static_files_forbidden))
|
||||
return default(val, cast("List[Union[str, re.Pattern]]", defaults.static_files_forbidden))
|
||||
|
||||
def _prepare_context_behavior(self, new_settings: ComponentsSettings) -> Literal["django", "isolated"]:
|
||||
raw_value = cast(
|
||||
Literal["django", "isolated"],
|
||||
"Literal['django', 'isolated']",
|
||||
default(new_settings.context_behavior, defaults.context_behavior),
|
||||
)
|
||||
try:
|
||||
ContextBehavior(raw_value)
|
||||
except ValueError:
|
||||
except ValueError as err:
|
||||
valid_values = [behavior.value for behavior in ContextBehavior]
|
||||
raise ValueError(f"Invalid context behavior: {raw_value}. Valid options are {valid_values}")
|
||||
raise ValueError(f"Invalid context behavior: {raw_value}. Valid options are {valid_values}") from err
|
||||
|
||||
return raw_value
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# ruff: noqa: PLC0415
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
@ -76,7 +77,7 @@ def _watch_component_files_for_autoreload() -> None:
|
|||
|
||||
component_dirs = set(get_component_dirs())
|
||||
|
||||
def template_changed(sender: Any, file_path: Path, **kwargs: Any) -> None:
|
||||
def template_changed(sender: Any, file_path: Path, **kwargs: Any) -> None: # noqa: ARG001
|
||||
# Reload dev server if any of the files within `COMPONENTS.dirs` or `COMPONENTS.app_dirs` changed
|
||||
for dir_path in file_path.parents:
|
||||
if dir_path in component_dirs:
|
||||
|
|
|
@ -70,11 +70,11 @@ class HtmlAttrsNode(BaseNode):
|
|||
|
||||
tag = "html_attrs"
|
||||
end_tag = None # inline-only
|
||||
allowed_flags = []
|
||||
allowed_flags = ()
|
||||
|
||||
def render(
|
||||
self,
|
||||
context: Context,
|
||||
context: Context, # noqa: ARG002
|
||||
attrs: Optional[Dict] = None,
|
||||
defaults: Optional[Dict] = None,
|
||||
**kwargs: Any,
|
||||
|
@ -269,7 +269,7 @@ def normalize_class(value: ClassValue) -> str:
|
|||
res: Dict[str, bool] = {}
|
||||
if isinstance(value, str):
|
||||
return value.strip()
|
||||
elif isinstance(value, (list, tuple)):
|
||||
if isinstance(value, (list, tuple)):
|
||||
# List items may be strings, dicts, or other lists/tuples
|
||||
for item in value:
|
||||
# NOTE: One difference from Vue is that if a class is given multiple times,
|
||||
|
@ -287,7 +287,7 @@ def normalize_class(value: ClassValue) -> str:
|
|||
# `{"class": True, "extra": True}` will result in `class="class extra"`
|
||||
res = value
|
||||
else:
|
||||
raise ValueError(f"Invalid class value: {value}")
|
||||
raise TypeError(f"Invalid class value: {value}")
|
||||
|
||||
res_str = ""
|
||||
for key, val in res.items():
|
||||
|
@ -313,7 +313,7 @@ def _normalize_class(value: ClassValue) -> Dict[str, bool]:
|
|||
elif isinstance(value, dict):
|
||||
res = value
|
||||
else:
|
||||
raise ValueError(f"Invalid class value: {value}")
|
||||
raise TypeError(f"Invalid class value: {value}")
|
||||
return res
|
||||
|
||||
|
||||
|
@ -360,7 +360,7 @@ def normalize_style(value: StyleValue) -> str:
|
|||
res: StyleDict = {}
|
||||
if isinstance(value, str):
|
||||
return value.strip()
|
||||
elif isinstance(value, (list, tuple)):
|
||||
if isinstance(value, (list, tuple)):
|
||||
# List items may be strings, dicts, or other lists/tuples
|
||||
for item in value:
|
||||
normalized = _normalize_style(item)
|
||||
|
@ -369,7 +369,7 @@ def normalize_style(value: StyleValue) -> str:
|
|||
# Remove entries with `None` value
|
||||
res = _normalize_style(value)
|
||||
else:
|
||||
raise ValueError(f"Invalid style value: {value}")
|
||||
raise TypeError(f"Invalid style value: {value}")
|
||||
|
||||
# By the time we get here, all `None` values have been removed.
|
||||
# If the final dict has `None` or `False` values, they are removed, so those
|
||||
|
@ -398,7 +398,7 @@ def _normalize_style(value: StyleValue) -> StyleDict:
|
|||
if val is not None:
|
||||
res[key] = val
|
||||
else:
|
||||
raise ValueError(f"Invalid style value: {value}")
|
||||
raise TypeError(f"Invalid style value: {value}")
|
||||
return res
|
||||
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ def autodiscover(
|
|||
|
||||
modules = get_component_files(".py")
|
||||
```
|
||||
|
||||
"""
|
||||
modules = get_component_files(".py")
|
||||
logger.debug(f"Autodiscover found {len(modules)} files in component directories.")
|
||||
|
@ -80,8 +81,9 @@ def import_libraries(
|
|||
|
||||
import_libraries(lambda path: path.replace("tests.", "myapp."))
|
||||
```
|
||||
|
||||
"""
|
||||
from django_components.app_settings import app_settings
|
||||
from django_components.app_settings import app_settings # noqa: PLC0415
|
||||
|
||||
return _import_modules(app_settings.LIBRARIES, map_module)
|
||||
|
||||
|
@ -93,7 +95,7 @@ def _import_modules(
|
|||
imported_modules: List[str] = []
|
||||
for module_name in modules:
|
||||
if map_module:
|
||||
module_name = map_module(module_name)
|
||||
module_name = map_module(module_name) # noqa: PLW2901
|
||||
|
||||
# This imports the file and runs it's code. So if the file defines any
|
||||
# django components, they will be registered.
|
||||
|
|
|
@ -20,7 +20,7 @@ component_media_cache: Optional[BaseCache] = None
|
|||
|
||||
# TODO_V1 - Remove, won't be needed once we remove `get_template_string()`, `get_template_name()`, `get_template()`
|
||||
def get_template_cache() -> LRUCache:
|
||||
global template_cache
|
||||
global template_cache # noqa: PLW0603
|
||||
if template_cache is None:
|
||||
template_cache = LRUCache(maxsize=app_settings.TEMPLATE_CACHE_SIZE)
|
||||
|
||||
|
@ -32,7 +32,7 @@ def get_component_media_cache() -> BaseCache:
|
|||
return caches[app_settings.CACHE]
|
||||
|
||||
# If no cache is set, use a local memory cache.
|
||||
global component_media_cache
|
||||
global component_media_cache # noqa: PLW0603
|
||||
if component_media_cache is None:
|
||||
component_media_cache = LocMemCache(
|
||||
"django-components-media",
|
||||
|
|
|
@ -24,9 +24,9 @@ class ComponentsRootCommand(ComponentCommand):
|
|||
name = "components"
|
||||
help = "The entrypoint for the 'components' commands."
|
||||
|
||||
subcommands = [
|
||||
subcommands = (
|
||||
CreateCommand,
|
||||
UpgradeCommand,
|
||||
ExtCommand,
|
||||
ComponentListCommand,
|
||||
]
|
||||
)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
|
||||
|
@ -69,7 +69,7 @@ class CreateCommand(ComponentCommand):
|
|||
name = "create"
|
||||
help = "Create a new django component."
|
||||
|
||||
arguments = [
|
||||
arguments = (
|
||||
CommandArg(
|
||||
name_or_flags="name",
|
||||
help="The name of the component to create. This is a required argument.",
|
||||
|
@ -118,9 +118,9 @@ class CreateCommand(ComponentCommand):
|
|||
),
|
||||
action="store_true",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def handle(self, *args: Any, **kwargs: Any) -> None:
|
||||
def handle(self, *_args: Any, **kwargs: Any) -> None:
|
||||
name = kwargs["name"]
|
||||
|
||||
if not name:
|
||||
|
@ -138,16 +138,16 @@ class CreateCommand(ComponentCommand):
|
|||
dry_run = kwargs["dry_run"]
|
||||
|
||||
if path:
|
||||
component_path = os.path.join(path, name)
|
||||
component_path = Path(path) / name
|
||||
elif base_dir:
|
||||
component_path = os.path.join(base_dir, "components", name)
|
||||
component_path = Path(base_dir) / "components" / name
|
||||
else:
|
||||
raise CommandError("You must specify a path or set BASE_DIR in your django settings")
|
||||
|
||||
if os.path.exists(component_path):
|
||||
if component_path.exists():
|
||||
if not force:
|
||||
raise CommandError(
|
||||
f'The component "{name}" already exists at {component_path}. Use --force to overwrite.'
|
||||
f'The component "{name}" already exists at {component_path}. Use --force to overwrite.',
|
||||
)
|
||||
|
||||
if verbose:
|
||||
|
@ -158,29 +158,32 @@ class CreateCommand(ComponentCommand):
|
|||
sys.stdout.write(style_warning(msg) + "\n")
|
||||
|
||||
if not dry_run:
|
||||
os.makedirs(component_path, exist_ok=force)
|
||||
component_path.mkdir(parents=True, exist_ok=force)
|
||||
|
||||
with open(os.path.join(component_path, js_filename), "w") as f:
|
||||
js_path = component_path / js_filename
|
||||
with js_path.open("w") as f:
|
||||
script_content = dedent(
|
||||
f"""
|
||||
window.addEventListener('load', (event) => {{
|
||||
console.log("{name} component is fully loaded");
|
||||
}});
|
||||
"""
|
||||
""",
|
||||
)
|
||||
f.write(script_content.strip())
|
||||
|
||||
with open(os.path.join(component_path, css_filename), "w") as f:
|
||||
css_path = component_path / css_filename
|
||||
with css_path.open("w") as f:
|
||||
style_content = dedent(
|
||||
f"""
|
||||
.component-{name} {{
|
||||
background: red;
|
||||
}}
|
||||
"""
|
||||
""",
|
||||
)
|
||||
f.write(style_content.strip())
|
||||
|
||||
with open(os.path.join(component_path, template_filename), "w") as f:
|
||||
template_path = component_path / template_filename
|
||||
with template_path.open("w") as f:
|
||||
template_content = dedent(
|
||||
f"""
|
||||
<div class="component-{name}">
|
||||
|
@ -188,11 +191,12 @@ class CreateCommand(ComponentCommand):
|
|||
<br>
|
||||
This is {{ param }} context value.
|
||||
</div>
|
||||
"""
|
||||
""",
|
||||
)
|
||||
f.write(template_content.strip())
|
||||
|
||||
with open(os.path.join(component_path, f"{name}.py"), "w") as f:
|
||||
py_path = component_path / f"{name}.py"
|
||||
with py_path.open("w") as f:
|
||||
py_content = dedent(
|
||||
f"""
|
||||
from django_components import Component, register
|
||||
|
@ -213,7 +217,7 @@ class CreateCommand(ComponentCommand):
|
|||
return {{
|
||||
"param": kwargs.param,
|
||||
}}
|
||||
"""
|
||||
""",
|
||||
)
|
||||
f.write(py_content.strip())
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ class ExtCommand(ComponentCommand):
|
|||
name = "ext"
|
||||
help = "Run extension commands."
|
||||
|
||||
subcommands = [
|
||||
subcommands = (
|
||||
ExtListCommand,
|
||||
ExtRunCommand,
|
||||
]
|
||||
)
|
||||
|
|
|
@ -60,8 +60,8 @@ class ExtListCommand(ListCommand):
|
|||
name = "list"
|
||||
help = "List all extensions."
|
||||
|
||||
columns = ["name"]
|
||||
default_columns = ["name"]
|
||||
columns = ("name",)
|
||||
default_columns = ("name",)
|
||||
|
||||
def get_data(self) -> List[Dict[str, Any]]:
|
||||
data: List[Dict[str, Any]] = []
|
||||
|
@ -69,6 +69,6 @@ class ExtListCommand(ListCommand):
|
|||
data.append(
|
||||
{
|
||||
"name": extension.name,
|
||||
}
|
||||
},
|
||||
)
|
||||
return data
|
||||
|
|
|
@ -22,7 +22,7 @@ def _gen_subcommands() -> List[Type[ComponentCommand]]:
|
|||
if not extension.commands:
|
||||
continue
|
||||
|
||||
ExtCommand = type(
|
||||
ExtCommand = type( # noqa: N806
|
||||
"ExtRunSubcommand_" + extension.name,
|
||||
(ComponentCommand,),
|
||||
{
|
||||
|
@ -113,4 +113,4 @@ class ExtRunCommand(ComponentCommand):
|
|||
|
||||
name = "run"
|
||||
help = "Run a command added by an extension."
|
||||
subcommands = SubcommandsDescriptor() # type: ignore
|
||||
subcommands = SubcommandsDescriptor() # type: ignore[assignment]
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import os
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
# ruff: noqa: T201
|
||||
from pathlib import Path
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple, Type, Union
|
||||
|
||||
from django_components.component import all_components
|
||||
from django_components.util.command import CommandArg, ComponentCommand
|
||||
|
@ -48,8 +49,8 @@ class ListCommand(ComponentCommand):
|
|||
# SUBCLASS API
|
||||
####################
|
||||
|
||||
columns: List[str]
|
||||
default_columns: List[str]
|
||||
columns: ClassVar[Union[List[str], Tuple[str, ...], Set[str]]]
|
||||
default_columns: ClassVar[Union[List[str], Tuple[str, ...], Set[str]]]
|
||||
|
||||
def get_data(self) -> List[Dict[str, Any]]:
|
||||
return []
|
||||
|
@ -60,7 +61,7 @@ class ListCommand(ComponentCommand):
|
|||
|
||||
arguments = ListArgumentsDescriptor() # type: ignore[assignment]
|
||||
|
||||
def handle(self, *args: Any, **kwargs: Any) -> None:
|
||||
def handle(self, *_args: Any, **kwargs: Any) -> None:
|
||||
"""
|
||||
This runs when the "list" command is called. This handler delegates to subclasses
|
||||
to define how to get the data with the `get_data` method and formats the results
|
||||
|
@ -146,8 +147,8 @@ class ComponentListCommand(ListCommand):
|
|||
name = "list"
|
||||
help = "List all components created in this project."
|
||||
|
||||
columns = ["name", "full_name", "path"]
|
||||
default_columns = ["full_name", "path"]
|
||||
columns = ("name", "full_name", "path")
|
||||
default_columns = ("full_name", "path")
|
||||
|
||||
def get_data(self) -> List[Dict[str, Any]]:
|
||||
components = all_components()
|
||||
|
@ -158,13 +159,13 @@ class ComponentListCommand(ListCommand):
|
|||
|
||||
# Make paths relative to CWD
|
||||
if module_file_path:
|
||||
module_file_path = os.path.relpath(module_file_path, os.getcwd())
|
||||
module_file_path = str(Path(module_file_path).relative_to(Path.cwd()))
|
||||
|
||||
data.append(
|
||||
{
|
||||
"name": component.__name__,
|
||||
"full_name": full_name,
|
||||
"path": module_file_path,
|
||||
}
|
||||
},
|
||||
)
|
||||
return data
|
||||
|
|
|
@ -3,9 +3,7 @@ from django_components.commands.create import CreateCommand
|
|||
|
||||
# TODO_REMOVE_IN_V1 - Superseded by `components create`
|
||||
class StartComponentCommand(CreateCommand):
|
||||
"""
|
||||
**Deprecated**. Use [`components create`](../commands#components-create) instead.
|
||||
"""
|
||||
"""**Deprecated**. Use [`components create`](../commands#components-create) instead."""
|
||||
|
||||
name = "startcomponent"
|
||||
help = "Deprecated. Use `components create` instead."
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
# ruff: noqa: T201
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, List
|
||||
|
||||
from django.conf import settings
|
||||
from django.template.engine import Engine
|
||||
|
@ -15,14 +16,14 @@ class UpgradeCommand(ComponentCommand):
|
|||
name = "upgrade"
|
||||
help = "Upgrade django components syntax from '{%% component_block ... %%}' to '{%% component ... %%}'."
|
||||
|
||||
arguments = [
|
||||
arguments = (
|
||||
CommandArg(
|
||||
name_or_flags="--path",
|
||||
help="Path to search for components",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
def handle(self, *_args: Any, **options: Any) -> None:
|
||||
current_engine = Engine.get_default()
|
||||
loader = DjcLoader(current_engine)
|
||||
dirs = loader.get_dirs(include_apps=False)
|
||||
|
@ -33,7 +34,7 @@ class UpgradeCommand(ComponentCommand):
|
|||
if options["path"]:
|
||||
dirs = [options["path"]]
|
||||
|
||||
all_files = []
|
||||
all_files: List[Path] = []
|
||||
|
||||
for dir_path in dirs:
|
||||
print(f"Searching for components in {dir_path}...")
|
||||
|
@ -41,11 +42,11 @@ class UpgradeCommand(ComponentCommand):
|
|||
for file in files:
|
||||
if not file.endswith((".html", ".py")):
|
||||
continue
|
||||
file_path = os.path.join(root, file)
|
||||
file_path = Path(root) / file
|
||||
all_files.append(file_path)
|
||||
|
||||
for file_path in all_files:
|
||||
with open(file_path, "r+", encoding="utf-8") as f:
|
||||
with file_path.open("r+", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
content_with_closed_components, step0_count = re.subn(
|
||||
r'({%\s*component\s*"(\w+?)"(.*?)%})(?!.*?{%\s*endcomponent\s*%})',
|
||||
|
|
|
@ -3,9 +3,7 @@ from django_components.commands.upgrade import UpgradeCommand
|
|||
|
||||
# TODO_REMOVE_IN_V1 - No longer needed?
|
||||
class UpgradeComponentCommand(UpgradeCommand):
|
||||
"""
|
||||
**Deprecated**. Use [`components upgrade`](../commands#components-upgrade) instead.
|
||||
"""
|
||||
"""**Deprecated**. Use [`components upgrade`](../commands#components-upgrade) instead."""
|
||||
|
||||
name = "upgradecomponent"
|
||||
help = "Deprecated. Use `components upgrade` instead."
|
||||
|
|
|
@ -32,7 +32,7 @@ DJANGO_COMMAND_ARGS = [
|
|||
default=1,
|
||||
type=int,
|
||||
choices=[0, 1, 2, 3],
|
||||
help=("Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, " "3=very verbose output"),
|
||||
help=("Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output"),
|
||||
),
|
||||
CommandArg(
|
||||
"--settings",
|
||||
|
@ -44,7 +44,7 @@ DJANGO_COMMAND_ARGS = [
|
|||
),
|
||||
CommandArg(
|
||||
"--pythonpath",
|
||||
help=("A directory to add to the Python path, e.g. " '"/home/djangoprojects/myproject".'),
|
||||
help=('A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".'),
|
||||
),
|
||||
CommandArg(
|
||||
"--traceback",
|
||||
|
@ -93,7 +93,7 @@ def load_as_django_command(command: Type[ComponentCommand]) -> Type[DjangoComman
|
|||
def __init__(self) -> None:
|
||||
self._command = command()
|
||||
|
||||
def create_parser(self, prog_name: str, subcommand: str, **kwargs: Any) -> ArgumentParser:
|
||||
def create_parser(self, *_args: Any, **_kwargs: Any) -> ArgumentParser:
|
||||
parser = setup_parser_from_command(command)
|
||||
for arg in DJANGO_COMMAND_ARGS:
|
||||
_setup_command_arg(parser, arg.asdict())
|
||||
|
@ -104,13 +104,13 @@ def load_as_django_command(command: Type[ComponentCommand]) -> Type[DjangoComman
|
|||
# this is where we forward the args to the command handler.
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
# Case: (Sub)command matched and it HAS handler
|
||||
resolved_command: Optional[ComponentCommand] = options.get("_command", None)
|
||||
resolved_command: Optional[ComponentCommand] = options.get("_command")
|
||||
if resolved_command and resolved_command.handle:
|
||||
resolved_command.handle(*args, **options)
|
||||
return
|
||||
|
||||
# Case: (Sub)command matched and it DOES NOT have handler (e.g. subcommand used for routing)
|
||||
cmd_parser: Optional[ArgumentParser] = options.get("_parser", None)
|
||||
cmd_parser: Optional[ArgumentParser] = options.get("_parser")
|
||||
if cmd_parser:
|
||||
cmd_parser.print_help()
|
||||
return
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
# ruff: noqa: ARG002, N804, N805
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from inspect import signature
|
||||
from types import MethodType
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Callable,
|
||||
ClassVar,
|
||||
|
@ -25,7 +27,6 @@ from django.template.base import NodeList, Parser, Template, Token
|
|||
from django.template.context import Context, RequestContext
|
||||
from django.template.loader_tags import BLOCK_CONTEXT_KEY, BlockContext
|
||||
from django.test.signals import template_rendered
|
||||
from django.views import View
|
||||
|
||||
from django_components.app_settings import ContextBehavior
|
||||
from django_components.component_media import ComponentMediaInput, ComponentMediaMeta
|
||||
|
@ -40,11 +41,9 @@ from django_components.dependencies import (
|
|||
cache_component_js,
|
||||
cache_component_js_vars,
|
||||
insert_component_dependencies_comment,
|
||||
)
|
||||
from django_components.dependencies import render_dependencies as _render_dependencies
|
||||
from django_components.dependencies import (
|
||||
set_component_attrs_for_js_and_css,
|
||||
)
|
||||
from django_components.dependencies import render_dependencies as _render_dependencies
|
||||
from django_components.extension import (
|
||||
OnComponentClassCreatedContext,
|
||||
OnComponentClassDeletedContext,
|
||||
|
@ -85,14 +84,17 @@ from django_components.util.weakref import cached_ref
|
|||
|
||||
# TODO_REMOVE_IN_V1 - Users should use top-level import instead
|
||||
# isort: off
|
||||
from django_components.component_registry import AlreadyRegistered as AlreadyRegistered # NOQA
|
||||
from django_components.component_registry import ComponentRegistry as ComponentRegistry # NOQA
|
||||
from django_components.component_registry import NotRegistered as NotRegistered # NOQA
|
||||
from django_components.component_registry import register as register # NOQA
|
||||
from django_components.component_registry import registry as registry # NOQA
|
||||
from django_components.component_registry import AlreadyRegistered as AlreadyRegistered # noqa: PLC0414
|
||||
from django_components.component_registry import ComponentRegistry as ComponentRegistry # noqa: PLC0414,F811
|
||||
from django_components.component_registry import NotRegistered as NotRegistered # noqa: PLC0414
|
||||
from django_components.component_registry import register as register # noqa: PLC0414
|
||||
from django_components.component_registry import registry as registry # noqa: PLC0414
|
||||
|
||||
# isort: on
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django.views import View
|
||||
|
||||
COMP_ONLY_FLAG = "only"
|
||||
|
||||
|
||||
|
@ -160,7 +162,7 @@ ALL_COMPONENTS: AllComponents = []
|
|||
|
||||
def all_components() -> List[Type["Component"]]:
|
||||
"""Get a list of all created [`Component`](../api#django_components.Component) classes."""
|
||||
components: List[Type["Component"]] = []
|
||||
components: List[Type[Component]] = []
|
||||
for comp_ref in ALL_COMPONENTS:
|
||||
comp = comp_ref()
|
||||
if comp is not None:
|
||||
|
@ -473,13 +475,13 @@ class ComponentMeta(ComponentMediaMeta):
|
|||
attrs["template_file"] = attrs.pop("template_name")
|
||||
attrs["template_name"] = ComponentTemplateNameDescriptor()
|
||||
|
||||
cls = cast(Type["Component"], super().__new__(mcs, name, bases, attrs))
|
||||
cls = cast("Type[Component]", super().__new__(mcs, name, bases, attrs))
|
||||
|
||||
# If the component defined `template_file`, then associate this Component class
|
||||
# with that template file path.
|
||||
# This way, when we will be instantiating `Template` in order to load the Component's template,
|
||||
# and its template_name matches this path, then we know that the template belongs to this Component class.
|
||||
if "template_file" in attrs and attrs["template_file"]:
|
||||
if attrs.get("template_file"):
|
||||
cache_component_template_file(cls)
|
||||
|
||||
# TODO_V1 - Remove. This is only for backwards compatibility with v0.139 and earlier,
|
||||
|
@ -493,7 +495,7 @@ class ComponentMeta(ComponentMediaMeta):
|
|||
context: Context,
|
||||
template: Template,
|
||||
result: str,
|
||||
error: Optional[Exception],
|
||||
_error: Optional[Exception],
|
||||
) -> Optional[SlotResult]:
|
||||
return orig_on_render_after(self, context, template, result) # type: ignore[call-arg]
|
||||
|
||||
|
@ -507,7 +509,7 @@ class ComponentMeta(ComponentMediaMeta):
|
|||
if not extensions:
|
||||
return
|
||||
|
||||
comp_cls = cast(Type["Component"], cls)
|
||||
comp_cls = cast("Type[Component]", cls)
|
||||
extensions.on_component_class_deleted(OnComponentClassDeletedContext(comp_cls))
|
||||
|
||||
|
||||
|
@ -826,6 +828,7 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
Returns:
|
||||
Optional[str]: The filepath to the template.
|
||||
|
||||
"""
|
||||
return None
|
||||
|
||||
|
@ -915,11 +918,12 @@ class Component(metaclass=ComponentMeta):
|
|||
Returns:
|
||||
Optional[Union[str, Template]]: The inlined Django template string or\
|
||||
a [`Template`](https://docs.djangoproject.com/en/5.1/topics/templates/#template) instance.
|
||||
|
||||
"""
|
||||
return None
|
||||
|
||||
# TODO_V2 - Remove this in v2
|
||||
def get_context_data(self, *args: Any, **kwargs: Any) -> Optional[Mapping]:
|
||||
def get_context_data(self, *_args: Any, **_kwargs: Any) -> Optional[Mapping]:
|
||||
"""
|
||||
DEPRECATED: Use [`get_template_data()`](../api#django_components.Component.get_template_data) instead.
|
||||
Will be removed in v2.
|
||||
|
@ -1788,7 +1792,7 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
media_class = MyMediaClass
|
||||
```
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
Media: ClassVar[Optional[Type[ComponentMediaInput]]] = None
|
||||
"""
|
||||
|
@ -1832,7 +1836,7 @@ class Component(metaclass=ComponentMeta):
|
|||
"print": ["path/to/style2.css"],
|
||||
}
|
||||
```
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
response_class: ClassVar[Type[HttpResponse]] = HttpResponse
|
||||
"""
|
||||
|
@ -1911,8 +1915,8 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
Since this hook is called for every component, this means that the template would be modified
|
||||
every time a component is rendered.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_render(self, context: Context, template: Optional[Template]) -> Union[SlotResult, OnRenderGenerator, None]:
|
||||
"""
|
||||
|
@ -2026,11 +2030,14 @@ class Component(metaclass=ComponentMeta):
|
|||
"""
|
||||
if template is None:
|
||||
return None
|
||||
else:
|
||||
return template.render(context)
|
||||
return template.render(context)
|
||||
|
||||
def on_render_after(
|
||||
self, context: Context, template: Optional[Template], result: Optional[str], error: Optional[Exception]
|
||||
self,
|
||||
context: Context,
|
||||
template: Optional[Template],
|
||||
result: Optional[str],
|
||||
error: Optional[Exception],
|
||||
) -> Optional[SlotResult]:
|
||||
"""
|
||||
Hook that runs when the component was fully rendered,
|
||||
|
@ -2099,7 +2106,6 @@ class Component(metaclass=ComponentMeta):
|
|||
print(f"Error: {error}")
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
# #####################################
|
||||
# BUILT-IN EXTENSIONS
|
||||
|
@ -2225,7 +2231,7 @@ class Component(metaclass=ComponentMeta):
|
|||
self,
|
||||
registered_name: Optional[str] = None,
|
||||
outer_context: Optional[Context] = None,
|
||||
registry: Optional[ComponentRegistry] = None, # noqa F811
|
||||
registry: Optional[ComponentRegistry] = None, # noqa: F811
|
||||
context: Optional[Context] = None,
|
||||
args: Optional[Any] = None,
|
||||
kwargs: Optional[Any] = None,
|
||||
|
@ -2233,8 +2239,8 @@ class Component(metaclass=ComponentMeta):
|
|||
deps_strategy: Optional[DependenciesStrategy] = None,
|
||||
request: Optional[HttpRequest] = None,
|
||||
node: Optional["ComponentNode"] = None,
|
||||
id: Optional[str] = None,
|
||||
):
|
||||
id: Optional[str] = None, # noqa: A002
|
||||
) -> None:
|
||||
# TODO_v1 - Remove this whole block in v1. This is for backwards compatibility with pre-v0.140
|
||||
# where one could do:
|
||||
# `MyComp("my_comp").render(kwargs={"a": 1})`.
|
||||
|
@ -2272,10 +2278,10 @@ class Component(metaclass=ComponentMeta):
|
|||
},
|
||||
)
|
||||
|
||||
self.render_to_response = MethodType(primed_render_to_response, self) # type: ignore
|
||||
self.render = MethodType(primed_render, self) # type: ignore
|
||||
self.render_to_response = MethodType(primed_render_to_response, self) # type: ignore[method-assign]
|
||||
self.render = MethodType(primed_render, self) # type: ignore[method-assign]
|
||||
|
||||
deps_strategy = cast(DependenciesStrategy, default(deps_strategy, "document"))
|
||||
deps_strategy = cast("DependenciesStrategy", default(deps_strategy, "document"))
|
||||
|
||||
self.id = default(id, _gen_component_id, factory=True) # type: ignore[arg-type]
|
||||
self.name = _get_component_name(self.__class__, registered_name)
|
||||
|
@ -2293,9 +2299,9 @@ class Component(metaclass=ComponentMeta):
|
|||
self.input = ComponentInput(
|
||||
context=self.context,
|
||||
# NOTE: Convert args / kwargs / slots to plain lists / dicts
|
||||
args=cast(List, args if isinstance(self.args, list) else list(self.args)),
|
||||
kwargs=cast(Dict, kwargs if isinstance(self.kwargs, dict) else to_dict(self.kwargs)),
|
||||
slots=cast(Dict, slots if isinstance(self.slots, dict) else to_dict(self.slots)),
|
||||
args=cast("List", args if isinstance(self.args, list) else list(self.args)),
|
||||
kwargs=cast("Dict", kwargs if isinstance(self.kwargs, dict) else to_dict(self.kwargs)),
|
||||
slots=cast("Dict", slots if isinstance(self.slots, dict) else to_dict(self.slots)),
|
||||
deps_strategy=deps_strategy,
|
||||
# TODO_v1 - Remove, superseded by `deps_strategy`
|
||||
type=deps_strategy,
|
||||
|
@ -2804,7 +2810,7 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
[`{{ component_vars.is_filled.slot_name }}`](../template_vars#django_components.component.ComponentVars.is_filled)
|
||||
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
request: Optional[HttpRequest]
|
||||
"""
|
||||
|
@ -2882,8 +2888,7 @@ class Component(metaclass=ComponentMeta):
|
|||
|
||||
if request is None:
|
||||
return {}
|
||||
else:
|
||||
return gen_context_processors_data(self.context, request)
|
||||
return gen_context_processors_data(self.context, request)
|
||||
|
||||
# #####################################
|
||||
# MISC
|
||||
|
@ -2950,7 +2955,7 @@ class Component(metaclass=ComponentMeta):
|
|||
def outer_view(request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
# `view` is a built-in extension defined in `extensions.view`. It subclasses
|
||||
# from Django's `View` class, and adds the `component` attribute to it.
|
||||
view_cls = cast(View, cls.View) # type: ignore[attr-defined]
|
||||
view_cls = cast("View", cls.View) # type: ignore[attr-defined]
|
||||
|
||||
# TODO_v1 - Remove `component` and use only `component_cls` instead.
|
||||
inner_view = view_cls.as_view(**initkwargs, component=cls(), component_cls=cls)
|
||||
|
@ -2971,13 +2976,13 @@ class Component(metaclass=ComponentMeta):
|
|||
slots: Optional[Any] = None,
|
||||
deps_strategy: DependenciesStrategy = "document",
|
||||
# TODO_v1 - Remove, superseded by `deps_strategy`
|
||||
type: Optional[DependenciesStrategy] = None,
|
||||
type: Optional[DependenciesStrategy] = None, # noqa: A002
|
||||
# TODO_v1 - Remove, superseded by `deps_strategy="ignore"`
|
||||
render_dependencies: bool = True,
|
||||
request: Optional[HttpRequest] = None,
|
||||
outer_context: Optional[Context] = None,
|
||||
# TODO_v2 - Remove `registered_name` and `registry`
|
||||
registry: Optional[ComponentRegistry] = None,
|
||||
registry: Optional[ComponentRegistry] = None, # noqa: F811
|
||||
registered_name: Optional[str] = None,
|
||||
node: Optional["ComponentNode"] = None,
|
||||
**response_kwargs: Any,
|
||||
|
@ -3061,13 +3066,13 @@ class Component(metaclass=ComponentMeta):
|
|||
slots: Optional[Any] = None,
|
||||
deps_strategy: DependenciesStrategy = "document",
|
||||
# TODO_v1 - Remove, superseded by `deps_strategy`
|
||||
type: Optional[DependenciesStrategy] = None,
|
||||
type: Optional[DependenciesStrategy] = None, # noqa: A002
|
||||
# TODO_v1 - Remove, superseded by `deps_strategy="ignore"`
|
||||
render_dependencies: bool = True,
|
||||
request: Optional[HttpRequest] = None,
|
||||
outer_context: Optional[Context] = None,
|
||||
# TODO_v2 - Remove `registered_name` and `registry`
|
||||
registry: Optional[ComponentRegistry] = None,
|
||||
registry: Optional[ComponentRegistry] = None, # noqa: F811
|
||||
registered_name: Optional[str] = None,
|
||||
node: Optional["ComponentNode"] = None,
|
||||
) -> str:
|
||||
|
@ -3253,13 +3258,12 @@ class Component(metaclass=ComponentMeta):
|
|||
)
|
||||
```
|
||||
""" # noqa: E501
|
||||
|
||||
# TODO_v1 - Remove, superseded by `deps_strategy`
|
||||
if type is not None:
|
||||
if deps_strategy != "document":
|
||||
raise ValueError(
|
||||
"Component.render() received both `type` and `deps_strategy` arguments. "
|
||||
"Only one should be given. The `type` argument is deprecated. Use `deps_strategy` instead."
|
||||
"Only one should be given. The `type` argument is deprecated. Use `deps_strategy` instead.",
|
||||
)
|
||||
deps_strategy = type
|
||||
|
||||
|
@ -3293,7 +3297,7 @@ class Component(metaclass=ComponentMeta):
|
|||
request: Optional[HttpRequest] = None,
|
||||
outer_context: Optional[Context] = None,
|
||||
# TODO_v2 - Remove `registered_name` and `registry`
|
||||
registry: Optional[ComponentRegistry] = None,
|
||||
registry: Optional[ComponentRegistry] = None, # noqa: F811
|
||||
registered_name: Optional[str] = None,
|
||||
node: Optional["ComponentNode"] = None,
|
||||
) -> str:
|
||||
|
@ -3326,7 +3330,7 @@ class Component(metaclass=ComponentMeta):
|
|||
request: Optional[HttpRequest] = None,
|
||||
outer_context: Optional[Context] = None,
|
||||
# TODO_v2 - Remove `registered_name` and `registry`
|
||||
registry: Optional[ComponentRegistry] = None,
|
||||
registry: Optional[ComponentRegistry] = None, # noqa: F811
|
||||
registered_name: Optional[str] = None,
|
||||
node: Optional["ComponentNode"] = None,
|
||||
) -> str:
|
||||
|
@ -3394,7 +3398,7 @@ class Component(metaclass=ComponentMeta):
|
|||
kwargs=kwargs_dict,
|
||||
slots=slots_dict,
|
||||
context=context,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# The component rendering was short-circuited by an extension, skipping
|
||||
|
@ -3415,7 +3419,7 @@ class Component(metaclass=ComponentMeta):
|
|||
# Required for compatibility with Django's {% extends %} tag
|
||||
# See https://github.com/django-components/django-components/pull/859
|
||||
context.render_context.push( # type: ignore[union-attr]
|
||||
{BLOCK_CONTEXT_KEY: context.render_context.get(BLOCK_CONTEXT_KEY, BlockContext())} # type: ignore
|
||||
{BLOCK_CONTEXT_KEY: context.render_context.get(BLOCK_CONTEXT_KEY, BlockContext())}, # type: ignore[union-attr]
|
||||
)
|
||||
|
||||
# We pass down the components the info about the component's parent.
|
||||
|
@ -3485,7 +3489,7 @@ class Component(metaclass=ComponentMeta):
|
|||
template_data=template_data,
|
||||
js_data=js_data,
|
||||
css_data=css_data,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# Cache component's JS and CSS scripts, in case they have been evicted from the cache.
|
||||
|
@ -3549,7 +3553,7 @@ class Component(metaclass=ComponentMeta):
|
|||
# `{% if variable > 8 and component_vars.is_filled.header %}`
|
||||
is_filled=component.is_filled,
|
||||
),
|
||||
}
|
||||
},
|
||||
):
|
||||
# Make a "snapshot" of the context as it was at the time of the render call.
|
||||
#
|
||||
|
@ -3600,7 +3604,7 @@ class Component(metaclass=ComponentMeta):
|
|||
if maybe_output is not None:
|
||||
html = maybe_output
|
||||
error = None
|
||||
except Exception as new_error:
|
||||
except Exception as new_error: # noqa: BLE001
|
||||
error = new_error
|
||||
html = None
|
||||
|
||||
|
@ -3619,7 +3623,7 @@ class Component(metaclass=ComponentMeta):
|
|||
component_id=render_id,
|
||||
result=html,
|
||||
error=error,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
|
@ -3769,7 +3773,7 @@ class Component(metaclass=ComponentMeta):
|
|||
if legacy_template_data and new_template_data:
|
||||
raise RuntimeError(
|
||||
f"Component {self.name} has both `get_context_data()` and `get_template_data()` methods. "
|
||||
"Please remove one of them."
|
||||
"Please remove one of them.",
|
||||
)
|
||||
template_data = new_template_data or legacy_template_data
|
||||
|
||||
|
@ -3896,13 +3900,13 @@ class ComponentNode(BaseNode):
|
|||
|
||||
tag = "component"
|
||||
end_tag = "endcomponent"
|
||||
allowed_flags = [COMP_ONLY_FLAG]
|
||||
allowed_flags = (COMP_ONLY_FLAG,)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
# ComponentNode inputs
|
||||
name: str,
|
||||
registry: ComponentRegistry, # noqa F811
|
||||
registry: ComponentRegistry, # noqa: F811
|
||||
# BaseNode inputs
|
||||
params: List[TagAttr],
|
||||
flags: Optional[Dict[str, bool]] = None,
|
||||
|
@ -3930,7 +3934,7 @@ class ComponentNode(BaseNode):
|
|||
cls,
|
||||
parser: Parser,
|
||||
token: Token,
|
||||
registry: ComponentRegistry, # noqa F811
|
||||
registry: ComponentRegistry, # noqa: F811
|
||||
name: str,
|
||||
start_tag: str,
|
||||
end_tag: str,
|
||||
|
@ -3952,11 +3956,11 @@ class ComponentNode(BaseNode):
|
|||
|
||||
if cached_registry is not registry:
|
||||
raise RuntimeError(
|
||||
f"Detected two Components from different registries using the same start tag '{start_tag}'"
|
||||
f"Detected two Components from different registries using the same start tag '{start_tag}'",
|
||||
)
|
||||
elif cached_subcls.end_tag != end_tag:
|
||||
if cached_subcls.end_tag != end_tag:
|
||||
raise RuntimeError(
|
||||
f"Detected two Components using the same start tag '{start_tag}' but with different end tags"
|
||||
f"Detected two Components using the same start tag '{start_tag}' but with different end tags",
|
||||
)
|
||||
|
||||
# Call `BaseNode.parse()` as if with the context of subcls.
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# ruff: noqa: PTH100, PTH118, PTH120, PTH207
|
||||
import glob
|
||||
import os
|
||||
import sys
|
||||
|
@ -254,7 +255,7 @@ class ComponentMediaInput(Protocol):
|
|||
|
||||
print(MyComponent.media._js) # ["script.js", "other1.js", "other2.js"]
|
||||
```
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -294,7 +295,7 @@ class ComponentMedia:
|
|||
if (inlined_val is not UNSET and file_val is not UNSET) and not (inlined_val is None and file_val is None):
|
||||
raise ImproperlyConfigured(
|
||||
f"Received non-empty value from both '{inlined_attr}' and '{file_attr}' in"
|
||||
f" Component {self.comp_cls.__name__}. Only one of the two must be set."
|
||||
f" Component {self.comp_cls.__name__}. Only one of the two must be set.",
|
||||
)
|
||||
# Make a copy of the original state, so we can reset it in tests
|
||||
self._original = copy(self)
|
||||
|
@ -338,7 +339,7 @@ class ComponentMediaMeta(type):
|
|||
_normalize_media(attrs["Media"])
|
||||
|
||||
cls = super().__new__(mcs, name, bases, attrs)
|
||||
comp_cls = cast(Type["Component"], cls)
|
||||
comp_cls = cast("Type[Component]", cls)
|
||||
|
||||
_setup_lazy_media_resolve(comp_cls, attrs)
|
||||
|
||||
|
@ -358,9 +359,9 @@ class ComponentMediaMeta(type):
|
|||
if name in COMP_MEDIA_LAZY_ATTRS:
|
||||
comp_media: Optional[ComponentMedia] = getattr(cls, "_component_media", None)
|
||||
if comp_media is not None and comp_media.resolved:
|
||||
print(
|
||||
print( # noqa: T201
|
||||
f"WARNING: Setting attribute '{name}' on component '{cls.__name__}' after the media files were"
|
||||
" already resolved. This may lead to unexpected behavior."
|
||||
" already resolved. This may lead to unexpected behavior.",
|
||||
)
|
||||
|
||||
# NOTE: When a metaclass specifies a `__setattr__` method, this overrides the normal behavior of
|
||||
|
@ -393,8 +394,7 @@ def _setup_lazy_media_resolve(comp_cls: Type["Component"], attrs: Dict[str, Any]
|
|||
def get_comp_media_attr(attr: str) -> Any:
|
||||
if attr == "media":
|
||||
return _get_comp_cls_media(comp_cls)
|
||||
else:
|
||||
return _get_comp_cls_attr(comp_cls, attr)
|
||||
return _get_comp_cls_attr(comp_cls, attr)
|
||||
|
||||
# Because of the lazy resolution, we want to know when the user tries to access the media attributes.
|
||||
# And because these fields are class attributes, we can't use `@property` decorator.
|
||||
|
@ -432,27 +432,26 @@ def _get_comp_cls_attr(comp_cls: Type["Component"], attr: str) -> Any:
|
|||
# For each of the pairs of inlined_content + file (e.g. `js` + `js_file`), if at least one of the two
|
||||
# is defined, we interpret it such that this (sub)class has overridden what was set by the parent class(es),
|
||||
# and we won't search further up the MRO.
|
||||
def resolve_pair(inline_attr: str, file_attr: str) -> Any:
|
||||
inline_attr_empty = getattr(comp_media, inline_attr, UNSET) is UNSET
|
||||
file_attr_empty = getattr(comp_media, file_attr, UNSET) is UNSET
|
||||
def is_pair_empty(inline_attr: str, file_attr: str) -> bool:
|
||||
inline_attr_empty = getattr(comp_media, inline_attr, UNSET) is UNSET # noqa: B023
|
||||
file_attr_empty = getattr(comp_media, file_attr, UNSET) is UNSET # noqa: B023
|
||||
|
||||
is_pair_empty = inline_attr_empty and file_attr_empty
|
||||
if is_pair_empty:
|
||||
return UNSET
|
||||
else:
|
||||
return value
|
||||
return inline_attr_empty and file_attr_empty
|
||||
|
||||
if attr in ("js", "js_file"):
|
||||
value = resolve_pair("js", "js_file")
|
||||
is_empty_pair = is_pair_empty("js", "js_file")
|
||||
elif attr in ("css", "css_file"):
|
||||
value = resolve_pair("css", "css_file")
|
||||
is_empty_pair = is_pair_empty("css", "css_file")
|
||||
elif attr in ("template", "template_file"):
|
||||
value = resolve_pair("template", "template_file")
|
||||
is_empty_pair = is_pair_empty("template", "template_file")
|
||||
else:
|
||||
is_empty_pair = False
|
||||
|
||||
value = UNSET if is_empty_pair else value
|
||||
|
||||
if value is UNSET:
|
||||
continue
|
||||
else:
|
||||
return value
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
|
@ -509,7 +508,7 @@ def _get_comp_cls_media(comp_cls: Type["Component"]) -> Any:
|
|||
# pass
|
||||
# ```
|
||||
media_input = getattr(curr_cls, "Media", UNSET)
|
||||
default_extend = True if media_input is not None else False
|
||||
default_extend = media_input is not None
|
||||
media_extend = getattr(media_input, "extend", default_extend)
|
||||
|
||||
# This ensures the same behavior as Django's Media class, where:
|
||||
|
@ -520,7 +519,7 @@ def _get_comp_cls_media(comp_cls: Type["Component"]) -> Any:
|
|||
if media_extend is True:
|
||||
bases = curr_cls.__bases__
|
||||
elif media_extend is False:
|
||||
bases = tuple()
|
||||
bases = ()
|
||||
else:
|
||||
bases = media_extend
|
||||
|
||||
|
@ -716,17 +715,17 @@ def _normalize_media(media: Type[ComponentMediaInput]) -> None:
|
|||
for media_type, path_or_list in media.css.items():
|
||||
# {"all": "style.css"}
|
||||
if _is_media_filepath(path_or_list):
|
||||
media.css[media_type] = [path_or_list] # type: ignore
|
||||
media.css[media_type] = [path_or_list] # type: ignore[misc]
|
||||
# {"all": ["style.css"]}
|
||||
else:
|
||||
media.css[media_type] = path_or_list # type: ignore
|
||||
media.css[media_type] = path_or_list # type: ignore[misc]
|
||||
else:
|
||||
raise ValueError(f"Media.css must be str, list, or dict, got {type(media.css)}")
|
||||
|
||||
if hasattr(media, "js") and media.js:
|
||||
# Allow: class Media: js = "script.js"
|
||||
if _is_media_filepath(media.js):
|
||||
media.js = [media.js] # type: ignore
|
||||
media.js = [media.js] # type: ignore[misc]
|
||||
# Allow: class Media: js = ["script.js"]
|
||||
else:
|
||||
# JS is already a list, no action needed
|
||||
|
@ -759,29 +758,31 @@ def _map_media_filepaths(media: Type[ComponentMediaInput], map_fn: Callable[[Seq
|
|||
|
||||
|
||||
def _is_media_filepath(filepath: Any) -> bool:
|
||||
# Case callable
|
||||
if callable(filepath):
|
||||
return True
|
||||
|
||||
# Case SafeString
|
||||
if isinstance(filepath, SafeData) or hasattr(filepath, "__html__"):
|
||||
return True
|
||||
|
||||
elif isinstance(filepath, (Path, os.PathLike)) or hasattr(filepath, "__fspath__"):
|
||||
# Case PathLike
|
||||
if isinstance(filepath, (Path, os.PathLike)) or hasattr(filepath, "__fspath__"):
|
||||
return True
|
||||
|
||||
# Case bytes
|
||||
if isinstance(filepath, bytes):
|
||||
return True
|
||||
|
||||
if isinstance(filepath, str):
|
||||
return True
|
||||
|
||||
return False
|
||||
# Case str
|
||||
return isinstance(filepath, str)
|
||||
|
||||
|
||||
def _normalize_media_filepath(filepaths: Sequence[ComponentMediaInputPath]) -> List[Union[str, SafeData]]:
|
||||
normalized: List[Union[str, SafeData]] = []
|
||||
for filepath in filepaths:
|
||||
if callable(filepath):
|
||||
filepath = filepath()
|
||||
filepath = filepath() # noqa: PLW2901
|
||||
|
||||
if isinstance(filepath, SafeData) or hasattr(filepath, "__html__"):
|
||||
normalized.append(filepath)
|
||||
|
@ -789,10 +790,10 @@ def _normalize_media_filepath(filepaths: Sequence[ComponentMediaInputPath]) -> L
|
|||
|
||||
if isinstance(filepath, (Path, os.PathLike)) or hasattr(filepath, "__fspath__"):
|
||||
# In case of Windows OS, convert to forward slashes
|
||||
filepath = Path(filepath.__fspath__()).as_posix()
|
||||
filepath = Path(filepath.__fspath__()).as_posix() # noqa: PLW2901
|
||||
|
||||
if isinstance(filepath, bytes):
|
||||
filepath = filepath.decode("utf-8")
|
||||
filepath = filepath.decode("utf-8") # noqa: PLW2901
|
||||
|
||||
if isinstance(filepath, str):
|
||||
normalized.append(filepath)
|
||||
|
@ -800,14 +801,16 @@ def _normalize_media_filepath(filepaths: Sequence[ComponentMediaInputPath]) -> L
|
|||
|
||||
raise ValueError(
|
||||
f"Unknown filepath {filepath} of type {type(filepath)}. Must be str, bytes, PathLike, SafeString,"
|
||||
" or a function that returns one of the former"
|
||||
" or a function that returns one of the former",
|
||||
)
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
def _resolve_component_relative_files(
|
||||
comp_cls: Type["Component"], comp_media: ComponentMedia, comp_dirs: List[Path]
|
||||
comp_cls: Type["Component"],
|
||||
comp_media: ComponentMedia,
|
||||
comp_dirs: List[Path],
|
||||
) -> None:
|
||||
"""
|
||||
Check if component's HTML, JS and CSS files refer to files in the same directory
|
||||
|
@ -825,7 +828,8 @@ def _resolve_component_relative_files(
|
|||
if is_set(comp_media.template_file) or is_set(comp_media.js_file) or is_set(comp_media.css_file):
|
||||
will_resolve_files = True
|
||||
elif not will_resolve_files and is_set(comp_media.Media):
|
||||
if getattr(comp_media.Media, "css", None) or getattr(comp_media.Media, "js", None):
|
||||
has_media_files = getattr(comp_media.Media, "css", None) or getattr(comp_media.Media, "js", None)
|
||||
if has_media_files:
|
||||
will_resolve_files = True
|
||||
|
||||
if not will_resolve_files:
|
||||
|
@ -837,7 +841,7 @@ def _resolve_component_relative_files(
|
|||
if not module_file_path:
|
||||
logger.debug(
|
||||
f"Could not resolve the path to the file for component '{component_name}'."
|
||||
" Paths for HTML, JS or CSS templates will NOT be resolved relative to the component file."
|
||||
" Paths for HTML, JS or CSS templates will NOT be resolved relative to the component file.",
|
||||
)
|
||||
return
|
||||
|
||||
|
@ -851,7 +855,7 @@ def _resolve_component_relative_files(
|
|||
f"No component directory found for component '{component_name}' in {module_file_path}"
|
||||
" If this component defines HTML, JS or CSS templates relatively to the component file,"
|
||||
" then check that the component's directory is accessible from one of the paths"
|
||||
" specified in the Django's 'COMPONENTS.dirs' settings."
|
||||
" specified in the Django's 'COMPONENTS.dirs' settings.",
|
||||
)
|
||||
return
|
||||
|
||||
|
@ -876,12 +880,11 @@ def _resolve_component_relative_files(
|
|||
# NOTE: It's important to use `repr`, so we don't trigger __str__ on SafeStrings
|
||||
if has_matched:
|
||||
logger.debug(
|
||||
f"Interpreting file '{repr(filepath)}' of component '{module_name}'" " relatively to component file"
|
||||
f"Interpreting file '{filepath!r}' of component '{module_name}' relatively to component file",
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f"Interpreting file '{repr(filepath)}' of component '{module_name}'"
|
||||
" relatively to components directory"
|
||||
f"Interpreting file '{filepath!r}' of component '{module_name}' relatively to components directory",
|
||||
)
|
||||
|
||||
return resolved_filepaths
|
||||
|
@ -904,18 +907,18 @@ def _resolve_component_relative_files(
|
|||
|
||||
# Check if template name is a local file or not
|
||||
if is_set(comp_media.template_file):
|
||||
comp_media.template_file = resolve_relative_media_file(comp_media.template_file, False)[0]
|
||||
comp_media.template_file = resolve_relative_media_file(comp_media.template_file, allow_glob=False)[0]
|
||||
if is_set(comp_media.js_file):
|
||||
comp_media.js_file = resolve_relative_media_file(comp_media.js_file, False)[0]
|
||||
comp_media.js_file = resolve_relative_media_file(comp_media.js_file, allow_glob=False)[0]
|
||||
if is_set(comp_media.css_file):
|
||||
comp_media.css_file = resolve_relative_media_file(comp_media.css_file, False)[0]
|
||||
comp_media.css_file = resolve_relative_media_file(comp_media.css_file, allow_glob=False)[0]
|
||||
|
||||
if is_set(comp_media.Media):
|
||||
_map_media_filepaths(
|
||||
comp_media.Media,
|
||||
# Media files can be defined as a glob patterns that match multiple files.
|
||||
# Thus, flatten the list of lists returned by `resolve_relative_media_file`.
|
||||
lambda filepaths: flatten(resolve_relative_media_file(f, True) for f in filepaths),
|
||||
lambda filepaths: flatten(resolve_relative_media_file(f, allow_glob=True) for f in filepaths),
|
||||
)
|
||||
|
||||
# Go over the JS / CSS media files again, but this time, if there are still any globs,
|
||||
|
@ -925,7 +928,7 @@ def _resolve_component_relative_files(
|
|||
comp_media.Media,
|
||||
# Media files can be defined as a glob patterns that match multiple files.
|
||||
# Thus, flatten the list of lists returned by `resolve_static_media_file`.
|
||||
lambda filepaths: flatten(resolve_static_media_file(f, True) for f in filepaths),
|
||||
lambda filepaths: flatten(resolve_static_media_file(f, allow_glob=True) for f in filepaths),
|
||||
)
|
||||
|
||||
|
||||
|
@ -957,12 +960,11 @@ def resolve_media_file(
|
|||
if allow_glob and is_glob(filepath_abs_or_glob):
|
||||
# Since globs are matched against the files, then we know that these files exist.
|
||||
matched_abs_filepaths = glob.glob(filepath_abs_or_glob)
|
||||
# But if we were given non-glob file path, then we need to check if it exists.
|
||||
elif Path(filepath_abs_or_glob).exists():
|
||||
matched_abs_filepaths = [filepath_abs_or_glob]
|
||||
else:
|
||||
# But if we were given non-glob file path, then we need to check if it exists.
|
||||
if Path(filepath_abs_or_glob).exists():
|
||||
matched_abs_filepaths = [filepath_abs_or_glob]
|
||||
else:
|
||||
matched_abs_filepaths = []
|
||||
matched_abs_filepaths = []
|
||||
|
||||
# If there are no matches, return the original filepath
|
||||
if not matched_abs_filepaths:
|
||||
|
@ -1082,7 +1084,7 @@ def _get_asset(
|
|||
if asset_content is not UNSET and asset_file is not UNSET:
|
||||
raise ValueError(
|
||||
f"Received both '{inlined_attr}' and '{file_attr}' in Component {comp_cls.__qualname__}."
|
||||
" Only one of the two must be set."
|
||||
" Only one of the two must be set.",
|
||||
)
|
||||
|
||||
# At this point we can tell that only EITHER `asset_content` OR `asset_file` is set.
|
||||
|
@ -1108,7 +1110,7 @@ def _get_asset(
|
|||
if asset_file is None:
|
||||
return None, None
|
||||
|
||||
asset_file = cast(str, asset_file)
|
||||
asset_file = cast("str", asset_file)
|
||||
|
||||
if inlined_attr == "template":
|
||||
# NOTE: `load_component_template()` applies `on_template_loaded()` and `on_template_compiled()` hooks.
|
||||
|
@ -1139,14 +1141,14 @@ def _get_asset(
|
|||
OnJsLoadedContext(
|
||||
component_cls=comp_cls,
|
||||
content=content,
|
||||
)
|
||||
),
|
||||
)
|
||||
elif inlined_attr == "css":
|
||||
content = extensions.on_css_loaded(
|
||||
OnCssLoadedContext(
|
||||
component_cls=comp_cls,
|
||||
content=content,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
return content, None
|
||||
|
|
|
@ -38,8 +38,6 @@ class AlreadyRegistered(Exception):
|
|||
[ComponentRegistry](./api.md#django_components.ComponentRegistry).
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NotRegistered(Exception):
|
||||
"""
|
||||
|
@ -48,8 +46,6 @@ class NotRegistered(Exception):
|
|||
[ComponentRegistry](./api.md#django_components.ComponentRegistry).
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
# Why do we store the tags with the components?
|
||||
#
|
||||
|
@ -146,10 +142,8 @@ ALL_REGISTRIES: AllRegistries = []
|
|||
|
||||
|
||||
def all_registries() -> List["ComponentRegistry"]:
|
||||
"""
|
||||
Get a list of all created [`ComponentRegistry`](./api.md#django_components.ComponentRegistry) instances.
|
||||
"""
|
||||
registries: List["ComponentRegistry"] = []
|
||||
"""Get a list of all created [`ComponentRegistry`](./api.md#django_components.ComponentRegistry) instances."""
|
||||
registries: List[ComponentRegistry] = []
|
||||
for reg_ref in ALL_REGISTRIES:
|
||||
reg = reg_ref()
|
||||
if reg is not None:
|
||||
|
@ -238,6 +232,7 @@ class ComponentRegistry:
|
|||
{% component "button" %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
@ -255,7 +250,7 @@ class ComponentRegistry:
|
|||
extensions.on_registry_created(
|
||||
OnRegistryCreatedContext(
|
||||
registry=self,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
def __del__(self) -> None:
|
||||
|
@ -266,7 +261,7 @@ class ComponentRegistry:
|
|||
extensions.on_registry_deleted(
|
||||
OnRegistryDeletedContext(
|
||||
registry=self,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# Unregister all components when the registry is deleted
|
||||
|
@ -288,7 +283,7 @@ class ComponentRegistry:
|
|||
if self._library is not None:
|
||||
lib = self._library
|
||||
else:
|
||||
from django_components.templatetags.component_tags import register as tag_library
|
||||
from django_components.templatetags.component_tags import register as tag_library # noqa: PLC0415
|
||||
|
||||
# For the default library, we want to protect our template tags from
|
||||
# being overriden.
|
||||
|
@ -301,9 +296,7 @@ class ComponentRegistry:
|
|||
|
||||
@property
|
||||
def settings(self) -> InternalRegistrySettings:
|
||||
"""
|
||||
[Registry settings](./api.md#django_components.RegistrySettings) configured for this registry.
|
||||
"""
|
||||
"""[Registry settings](./api.md#django_components.RegistrySettings) configured for this registry."""
|
||||
# NOTE: We allow the settings to be given as a getter function
|
||||
# so the settings can respond to changes.
|
||||
if callable(self._settings):
|
||||
|
@ -348,10 +341,11 @@ class ComponentRegistry:
|
|||
```python
|
||||
registry.register("button", ButtonComponent)
|
||||
```
|
||||
|
||||
"""
|
||||
existing_component = self._registry.get(name)
|
||||
if existing_component and existing_component.cls.class_id != component.class_id:
|
||||
raise AlreadyRegistered('The component "%s" has already been registered' % name)
|
||||
raise AlreadyRegistered(f'The component "{name}" has already been registered')
|
||||
|
||||
entry = self._register_to_library(name, component)
|
||||
|
||||
|
@ -372,7 +366,7 @@ class ComponentRegistry:
|
|||
registry=self,
|
||||
name=name,
|
||||
component_cls=entry.cls,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
def unregister(self, name: str) -> None:
|
||||
|
@ -398,6 +392,7 @@ class ComponentRegistry:
|
|||
# Then unregister
|
||||
registry.unregister("button")
|
||||
```
|
||||
|
||||
"""
|
||||
# Validate
|
||||
self.get(name)
|
||||
|
@ -420,10 +415,9 @@ class ComponentRegistry:
|
|||
|
||||
# Only unregister a tag if it's NOT protected
|
||||
is_protected = is_tag_protected(self.library, tag)
|
||||
if not is_protected:
|
||||
# Unregister the tag from library if this was the last component using this tag
|
||||
if is_tag_empty and tag in self.library.tags:
|
||||
self.library.tags.pop(tag, None)
|
||||
# Unregister the tag from library if this was the last component using this tag
|
||||
if not is_protected and is_tag_empty and tag in self.library.tags:
|
||||
self.library.tags.pop(tag, None)
|
||||
|
||||
entry = self._registry[name]
|
||||
del self._registry[name]
|
||||
|
@ -433,7 +427,7 @@ class ComponentRegistry:
|
|||
registry=self,
|
||||
name=name,
|
||||
component_cls=entry.cls,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
def get(self, name: str) -> Type["Component"]:
|
||||
|
@ -461,9 +455,10 @@ class ComponentRegistry:
|
|||
registry.get("button")
|
||||
# > ButtonComponent
|
||||
```
|
||||
|
||||
"""
|
||||
if name not in self._registry:
|
||||
raise NotRegistered('The component "%s" is not registered' % name)
|
||||
raise NotRegistered(f'The component "{name}" is not registered')
|
||||
|
||||
return self._registry[name].cls
|
||||
|
||||
|
@ -487,6 +482,7 @@ class ComponentRegistry:
|
|||
registry.has("button")
|
||||
# > True
|
||||
```
|
||||
|
||||
"""
|
||||
return name in self._registry
|
||||
|
||||
|
@ -510,6 +506,7 @@ class ComponentRegistry:
|
|||
# > "card": CardComponent,
|
||||
# > }
|
||||
```
|
||||
|
||||
"""
|
||||
comps = {key: entry.cls for key, entry in self._registry.items()}
|
||||
return comps
|
||||
|
@ -530,6 +527,7 @@ class ComponentRegistry:
|
|||
registry.all()
|
||||
# > {}
|
||||
```
|
||||
|
||||
"""
|
||||
all_comp_names = list(self._registry.keys())
|
||||
for comp_name in all_comp_names:
|
||||
|
@ -544,7 +542,7 @@ class ComponentRegistry:
|
|||
component: Type["Component"],
|
||||
) -> ComponentRegistryEntry:
|
||||
# Lazily import to avoid circular dependencies
|
||||
from django_components.component import ComponentNode
|
||||
from django_components.component import ComponentNode # noqa: PLC0415
|
||||
|
||||
registry = self
|
||||
|
||||
|
@ -613,7 +611,10 @@ registry.clear()
|
|||
_the_registry = registry
|
||||
|
||||
|
||||
def register(name: str, registry: Optional[ComponentRegistry] = None) -> Callable[
|
||||
def register(
|
||||
name: str,
|
||||
registry: Optional[ComponentRegistry] = None,
|
||||
) -> Callable[
|
||||
[Type[TComponent]],
|
||||
Type[TComponent],
|
||||
]:
|
||||
|
@ -656,6 +657,7 @@ def register(name: str, registry: Optional[ComponentRegistry] = None) -> Callabl
|
|||
class MyComponent(Component):
|
||||
...
|
||||
```
|
||||
|
||||
"""
|
||||
if registry is None:
|
||||
registry = _the_registry
|
||||
|
|
|
@ -95,6 +95,7 @@ class DynamicComponent(Component):
|
|||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
_is_dynamic_component = True
|
||||
|
@ -105,8 +106,8 @@ class DynamicComponent(Component):
|
|||
# will know that it's a child of this component.
|
||||
def on_render(
|
||||
self,
|
||||
context: Context,
|
||||
template: Optional[Template],
|
||||
context: Context, # noqa: ARG002
|
||||
template: Optional[Template], # noqa: ARG002
|
||||
) -> str:
|
||||
# Make a copy of kwargs so we pass to the child only the kwargs that are
|
||||
# actually used by the child component.
|
||||
|
@ -146,23 +147,22 @@ class DynamicComponent(Component):
|
|||
if inspect.isclass(comp_name_or_class):
|
||||
component_cls = comp_name_or_class
|
||||
else:
|
||||
component_cls = cast(Type[Component], comp_name_or_class.__class__)
|
||||
component_cls = cast("Type[Component]", comp_name_or_class.__class__)
|
||||
|
||||
elif registry:
|
||||
component_cls = registry.get(comp_name_or_class)
|
||||
else:
|
||||
if registry:
|
||||
component_cls = registry.get(comp_name_or_class)
|
||||
else:
|
||||
# Search all registries for the first match
|
||||
for reg_ref in ALL_REGISTRIES:
|
||||
reg = reg_ref()
|
||||
if not reg:
|
||||
continue
|
||||
# Search all registries for the first match
|
||||
for reg_ref in ALL_REGISTRIES:
|
||||
reg = reg_ref()
|
||||
if not reg:
|
||||
continue
|
||||
|
||||
try:
|
||||
component_cls = reg.get(comp_name_or_class)
|
||||
break
|
||||
except NotRegistered:
|
||||
continue
|
||||
try:
|
||||
component_cls = reg.get(comp_name_or_class)
|
||||
break
|
||||
except NotRegistered:
|
||||
continue
|
||||
|
||||
# Raise if none found
|
||||
if not component_cls:
|
||||
|
|
|
@ -70,8 +70,7 @@ def _gen_cache_key(
|
|||
) -> str:
|
||||
if input_hash:
|
||||
return f"__components:{comp_cls_id}:{script_type}:{input_hash}"
|
||||
else:
|
||||
return f"__components:{comp_cls_id}:{script_type}"
|
||||
return f"__components:{comp_cls_id}:{script_type}"
|
||||
|
||||
|
||||
def _is_script_in_cache(
|
||||
|
@ -94,7 +93,6 @@ def _cache_script(
|
|||
Given a component and it's inlined JS or CSS, store the JS/CSS in a cache,
|
||||
so it can be retrieved via URL endpoint.
|
||||
"""
|
||||
|
||||
# E.g. `__components:MyButton:js:df7c6d10`
|
||||
if script_type in ("js", "css"):
|
||||
cache_key = _gen_cache_key(comp_cls.class_id, script_type, input_hash)
|
||||
|
@ -114,10 +112,10 @@ def cache_component_js(comp_cls: Type["Component"], force: bool) -> None:
|
|||
times, this JS is loaded only once.
|
||||
"""
|
||||
if not comp_cls.js or not is_nonempty_str(comp_cls.js):
|
||||
return None
|
||||
return
|
||||
|
||||
if not force and _is_script_in_cache(comp_cls, "js", None):
|
||||
return None
|
||||
return
|
||||
|
||||
_cache_script(
|
||||
comp_cls=comp_cls,
|
||||
|
@ -147,7 +145,7 @@ def cache_component_js_vars(comp_cls: Type["Component"], js_vars: Mapping) -> Op
|
|||
|
||||
# The hash for the file that holds the JS variables is derived from the variables themselves.
|
||||
json_data = json.dumps(js_vars)
|
||||
input_hash = md5(json_data.encode()).hexdigest()[0:6]
|
||||
input_hash = md5(json_data.encode()).hexdigest()[0:6] # noqa: S324
|
||||
|
||||
# Generate and cache a JS script that contains the JS variables.
|
||||
if not _is_script_in_cache(comp_cls, "js", input_hash):
|
||||
|
@ -165,7 +163,7 @@ def wrap_component_js(comp_cls: Type["Component"], content: str) -> str:
|
|||
if "</script" in content:
|
||||
raise RuntimeError(
|
||||
f"Content of `Component.js` for component '{comp_cls.__name__}' contains '</script>' end tag. "
|
||||
"This is not allowed, as it would break the HTML."
|
||||
"This is not allowed, as it would break the HTML.",
|
||||
)
|
||||
return f"<script>{content}</script>"
|
||||
|
||||
|
@ -177,10 +175,10 @@ def cache_component_css(comp_cls: Type["Component"], force: bool) -> None:
|
|||
times, this CSS is loaded only once.
|
||||
"""
|
||||
if not comp_cls.css or not is_nonempty_str(comp_cls.css):
|
||||
return None
|
||||
return
|
||||
|
||||
if not force and _is_script_in_cache(comp_cls, "css", None):
|
||||
return None
|
||||
return
|
||||
|
||||
_cache_script(
|
||||
comp_cls=comp_cls,
|
||||
|
@ -200,7 +198,7 @@ def cache_component_css_vars(comp_cls: Type["Component"], css_vars: Mapping) ->
|
|||
|
||||
# The hash for the file that holds the CSS variables is derived from the variables themselves.
|
||||
json_data = json.dumps(css_vars)
|
||||
input_hash = md5(json_data.encode()).hexdigest()[0:6]
|
||||
input_hash = md5(json_data.encode()).hexdigest()[0:6] # noqa: S324
|
||||
|
||||
# Generate and cache a CSS stylesheet that contains the CSS variables.
|
||||
if not _is_script_in_cache(comp_cls, "css", input_hash):
|
||||
|
@ -218,7 +216,7 @@ def wrap_component_css(comp_cls: Type["Component"], content: str) -> str:
|
|||
if "</style" in content:
|
||||
raise RuntimeError(
|
||||
f"Content of `Component.css` for component '{comp_cls.__name__}' contains '</style>' end tag. "
|
||||
"This is not allowed, as it would break the HTML."
|
||||
"This is not allowed, as it would break the HTML.",
|
||||
)
|
||||
return f"<style>{content}</style>"
|
||||
|
||||
|
@ -347,10 +345,10 @@ COMPONENT_COMMENT_REGEX = re.compile(rb"<!--\s+_RENDERED\s+(?P<data>[\w\-,/]+?)\
|
|||
# - js - Cache key for the JS data from `get_js_data()`
|
||||
# - css - Cache key for the CSS data from `get_css_data()`
|
||||
SCRIPT_NAME_REGEX = re.compile(
|
||||
rb"^(?P<comp_cls_id>[\w\-\./]+?),(?P<id>[\w]+?),(?P<js>[0-9a-f]*?),(?P<css>[0-9a-f]*?)$"
|
||||
rb"^(?P<comp_cls_id>[\w\-\./]+?),(?P<id>[\w]+?),(?P<js>[0-9a-f]*?),(?P<css>[0-9a-f]*?)$",
|
||||
)
|
||||
# E.g. `data-djc-id-ca1b2c3`
|
||||
MAYBE_COMP_ID = r'(?: data-djc-id-\w{{{COMP_ID_LENGTH}}}="")?'.format(COMP_ID_LENGTH=COMP_ID_LENGTH)
|
||||
MAYBE_COMP_ID = r'(?: data-djc-id-\w{{{COMP_ID_LENGTH}}}="")?'.format(COMP_ID_LENGTH=COMP_ID_LENGTH) # noqa: UP032
|
||||
# E.g. `data-djc-css-99914b`
|
||||
MAYBE_COMP_CSS_ID = r'(?: data-djc-css-\w{6}="")?'
|
||||
|
||||
|
@ -358,7 +356,7 @@ PLACEHOLDER_REGEX = re.compile(
|
|||
r"{css_placeholder}|{js_placeholder}".format(
|
||||
css_placeholder=f'<link name="{CSS_PLACEHOLDER_NAME}"{MAYBE_COMP_CSS_ID}{MAYBE_COMP_ID}/?>',
|
||||
js_placeholder=f'<script name="{JS_PLACEHOLDER_NAME}"{MAYBE_COMP_CSS_ID}{MAYBE_COMP_ID}></script>',
|
||||
).encode()
|
||||
).encode(),
|
||||
)
|
||||
|
||||
|
||||
|
@ -400,10 +398,11 @@ def render_dependencies(content: TContent, strategy: DependenciesStrategy = "doc
|
|||
|
||||
return HttpResponse(processed_html)
|
||||
```
|
||||
|
||||
"""
|
||||
if strategy not in DEPS_STRATEGIES:
|
||||
raise ValueError(f"Invalid strategy '{strategy}'")
|
||||
elif strategy == "ignore":
|
||||
if strategy == "ignore":
|
||||
return content
|
||||
|
||||
is_safestring = isinstance(content, SafeString)
|
||||
|
@ -411,7 +410,7 @@ def render_dependencies(content: TContent, strategy: DependenciesStrategy = "doc
|
|||
if isinstance(content, str):
|
||||
content_ = content.encode()
|
||||
else:
|
||||
content_ = cast(bytes, content)
|
||||
content_ = cast("bytes", content)
|
||||
|
||||
content_, js_dependencies, css_dependencies = _process_dep_declarations(content_, strategy)
|
||||
|
||||
|
@ -438,7 +437,7 @@ def render_dependencies(content: TContent, strategy: DependenciesStrategy = "doc
|
|||
else:
|
||||
raise RuntimeError(
|
||||
"Unexpected error: Regex for component dependencies processing"
|
||||
f" matched unknown string '{match[0].decode()}'"
|
||||
f" matched unknown string '{match[0].decode()}'",
|
||||
)
|
||||
return replacement
|
||||
|
||||
|
@ -469,7 +468,7 @@ def render_dependencies(content: TContent, strategy: DependenciesStrategy = "doc
|
|||
# Return the same type as we were given
|
||||
output = content_.decode() if isinstance(content, str) else content_
|
||||
output = mark_safe(output) if is_safestring else output
|
||||
return cast(TContent, output)
|
||||
return cast("TContent", output)
|
||||
|
||||
|
||||
# Renamed so we can access use this function where there's kwarg of the same name
|
||||
|
@ -531,7 +530,7 @@ def _process_dep_declarations(content: bytes, strategy: DependenciesStrategy) ->
|
|||
`<!-- _RENDERED table_10bac31,123,a92ef298,bd002c3 -->`
|
||||
"""
|
||||
# Extract all matched instances of `<!-- _RENDERED ... -->` while also removing them from the text
|
||||
all_parts: List[bytes] = list()
|
||||
all_parts: List[bytes] = []
|
||||
|
||||
def on_replace_match(match: "re.Match[bytes]") -> bytes:
|
||||
all_parts.append(match.group("data"))
|
||||
|
@ -595,7 +594,7 @@ def _process_dep_declarations(content: bytes, strategy: DependenciesStrategy) ->
|
|||
) = _prepare_tags_and_urls(comp_data, strategy)
|
||||
|
||||
def get_component_media(comp_cls_id: str) -> Media:
|
||||
from django_components.component import get_component_by_class_id
|
||||
from django_components.component import get_component_by_class_id # noqa: PLC0415
|
||||
|
||||
comp_cls = get_component_by_class_id(comp_cls_id)
|
||||
return comp_cls.media
|
||||
|
@ -639,7 +638,7 @@ def _process_dep_declarations(content: bytes, strategy: DependenciesStrategy) ->
|
|||
# to avoid a flash of unstyled content. In such case, the "CSS to load" is actually already
|
||||
# loaded, so we have to mark those scripts as loaded in the dependency manager.
|
||||
*(media_css_urls if strategy == "document" else []),
|
||||
]
|
||||
],
|
||||
)
|
||||
loaded_js_urls = sorted(
|
||||
[
|
||||
|
@ -649,7 +648,7 @@ def _process_dep_declarations(content: bytes, strategy: DependenciesStrategy) ->
|
|||
# so the scripts are executed at proper order. In such case, the "JS to load" is actually already
|
||||
# loaded, so we have to mark those scripts as loaded in the dependency manager.
|
||||
*(media_js_urls if strategy == "document" else []),
|
||||
]
|
||||
],
|
||||
)
|
||||
|
||||
# NOTE: No exec script for the "simple" mode, as that one is NOT using the dependency manager
|
||||
|
@ -686,7 +685,7 @@ def _process_dep_declarations(content: bytes, strategy: DependenciesStrategy) ->
|
|||
final_script_tags = "".join(
|
||||
[
|
||||
# JS by us
|
||||
*[tag for tag in core_script_tags],
|
||||
*core_script_tags,
|
||||
# Make calls to the JS dependency manager
|
||||
# Loads JS from `Media.js` and `Component.js` if fragment
|
||||
*([exec_script] if exec_script else []),
|
||||
|
@ -696,10 +695,10 @@ def _process_dep_declarations(content: bytes, strategy: DependenciesStrategy) ->
|
|||
# we only mark those scripts as loaded.
|
||||
*(media_js_tags if strategy in ("document", "simple", "prepend", "append") else []),
|
||||
# JS variables
|
||||
*[tag for tag in js_variables_tags],
|
||||
*js_variables_tags,
|
||||
# JS from `Component.js` (if not fragment)
|
||||
*[tag for tag in component_js_tags],
|
||||
]
|
||||
*component_js_tags,
|
||||
],
|
||||
)
|
||||
|
||||
final_css_tags = "".join(
|
||||
|
@ -707,14 +706,14 @@ def _process_dep_declarations(content: bytes, strategy: DependenciesStrategy) ->
|
|||
# CSS by us
|
||||
# <NONE>
|
||||
# CSS from `Component.css` (if not fragment)
|
||||
*[tag for tag in component_css_tags],
|
||||
*component_css_tags,
|
||||
# CSS variables
|
||||
*[tag for tag in css_variables_tags],
|
||||
*css_variables_tags,
|
||||
# CSS from `Media.css` (plus from `Component.css` if fragment)
|
||||
# NOTE: Similarly to JS, the initial CSS is loaded outside of the dependency
|
||||
# manager, and only marked as loaded, to avoid a flash of unstyled content.
|
||||
*[tag for tag in media_css_tags],
|
||||
]
|
||||
*media_css_tags,
|
||||
],
|
||||
)
|
||||
|
||||
return (content, final_script_tags.encode("utf-8"), final_css_tags.encode("utf-8"))
|
||||
|
@ -748,10 +747,10 @@ def _postprocess_media_tags(
|
|||
raise RuntimeError(
|
||||
f"One of entries for `Component.Media.{script_type}` media is missing a "
|
||||
f"value for attribute '{attr}'. If there is content inlined inside the `<{attr}>` tags, "
|
||||
f"you must move the content to a `.{script_type}` file and reference it via '{attr}'.\nGot:\n{tag}"
|
||||
f"you must move the content to a `.{script_type}` file and reference it via '{attr}'.\nGot:\n{tag}",
|
||||
)
|
||||
|
||||
url = cast(str, maybe_url)
|
||||
url = cast("str", maybe_url)
|
||||
|
||||
# Skip duplicates
|
||||
if url in tags_by_url:
|
||||
|
@ -770,7 +769,7 @@ def _prepare_tags_and_urls(
|
|||
data: List[Tuple[str, ScriptType, Optional[str]]],
|
||||
strategy: DependenciesStrategy,
|
||||
) -> Tuple[List[str], List[str], List[str], List[str], List[str], List[str]]:
|
||||
from django_components.component import get_component_by_class_id
|
||||
from django_components.component import get_component_by_class_id # noqa: PLC0415
|
||||
|
||||
# JS / CSS that we should insert into the HTML
|
||||
inlined_js_tags: List[str] = []
|
||||
|
@ -859,7 +858,7 @@ def get_script_tag(
|
|||
content = get_script_content(script_type, comp_cls, input_hash)
|
||||
if content is None:
|
||||
raise RuntimeError(
|
||||
f"Could not find {script_type.upper()} for component '{comp_cls.__name__}' (id: {comp_cls.class_id})"
|
||||
f"Could not find {script_type.upper()} for component '{comp_cls.__name__}' (id: {comp_cls.class_id})",
|
||||
)
|
||||
|
||||
if script_type == "js":
|
||||
|
@ -979,8 +978,8 @@ def _insert_js_css_to_default_locations(
|
|||
|
||||
if did_modify_html:
|
||||
return updated_html
|
||||
else:
|
||||
return None # No changes made
|
||||
|
||||
return None # No changes made
|
||||
|
||||
|
||||
#########################################################
|
||||
|
@ -1006,7 +1005,7 @@ def cached_script_view(
|
|||
script_type: ScriptType,
|
||||
input_hash: Optional[str] = None,
|
||||
) -> HttpResponse:
|
||||
from django_components.component import get_component_by_class_id
|
||||
from django_components.component import get_component_by_class_id # noqa: PLC0415
|
||||
|
||||
if req.method != "GET":
|
||||
return HttpResponseNotAllowed(["GET"])
|
||||
|
@ -1036,15 +1035,15 @@ urlpatterns = [
|
|||
#########################################################
|
||||
|
||||
|
||||
def _component_dependencies(type: Literal["js", "css"]) -> SafeString:
|
||||
def _component_dependencies(dep_type: Literal["js", "css"]) -> SafeString:
|
||||
"""Marks location where CSS link and JS script tags should be rendered."""
|
||||
if type == "css":
|
||||
if dep_type == "css":
|
||||
placeholder = CSS_DEPENDENCY_PLACEHOLDER
|
||||
elif type == "js":
|
||||
elif dep_type == "js":
|
||||
placeholder = JS_DEPENDENCY_PLACEHOLDER
|
||||
else:
|
||||
raise TemplateSyntaxError(
|
||||
f"Unknown dependency type in {{% component_dependencies %}}. Must be one of 'css' or 'js', got {type}"
|
||||
f"Unknown dependency type in {{% component_dependencies %}}. Must be one of 'css' or 'js', got {dep_type}",
|
||||
)
|
||||
|
||||
return mark_safe(placeholder)
|
||||
|
@ -1066,9 +1065,9 @@ class ComponentCssDependenciesNode(BaseNode):
|
|||
|
||||
tag = "component_css_dependencies"
|
||||
end_tag = None # inline-only
|
||||
allowed_flags = []
|
||||
allowed_flags = ()
|
||||
|
||||
def render(self, context: Context) -> str:
|
||||
def render(self, context: Context) -> str: # noqa: ARG002
|
||||
return _component_dependencies("css")
|
||||
|
||||
|
||||
|
@ -1088,7 +1087,7 @@ class ComponentJsDependenciesNode(BaseNode):
|
|||
|
||||
tag = "component_js_dependencies"
|
||||
end_tag = None # inline-only
|
||||
allowed_flags = []
|
||||
allowed_flags = ()
|
||||
|
||||
def render(self, context: Context) -> str:
|
||||
def render(self, context: Context) -> str: # noqa: ARG002
|
||||
return _component_dependencies("js")
|
||||
|
|
|
@ -81,18 +81,18 @@ class DynamicFilterExpression:
|
|||
# to avoid it being stringified
|
||||
if isinstance(node, VariableNode):
|
||||
return node.filter_expression.resolve(context)
|
||||
else:
|
||||
# For any other tags `{% %}`, we're at a mercy of the authors, and
|
||||
# we don't know if the result comes out stringified or not.
|
||||
return node.render(context)
|
||||
else:
|
||||
# Lastly, if there's multiple nodes, we render it to a string
|
||||
#
|
||||
# NOTE: When rendering a NodeList, it expects that each node is a string.
|
||||
# However, we want to support tags that return non-string results, so we can pass
|
||||
# them as inputs to components. So we wrap the nodes in `StringifiedNode`
|
||||
nodelist = NodeList(StringifiedNode(node) for node in self.nodelist)
|
||||
return nodelist.render(context)
|
||||
|
||||
# For any other tags `{% %}`, we're at a mercy of the authors, and
|
||||
# we don't know if the result comes out stringified or not.
|
||||
return node.render(context)
|
||||
|
||||
# Lastly, if there's multiple nodes, we render it to a string
|
||||
#
|
||||
# NOTE: When rendering a NodeList, it expects that each node is a string.
|
||||
# However, we want to support tags that return non-string results, so we can pass
|
||||
# them as inputs to components. So we wrap the nodes in `StringifiedNode`
|
||||
nodelist = NodeList(StringifiedNode(node) for node in self.nodelist)
|
||||
return nodelist.render(context)
|
||||
|
||||
|
||||
class StringifiedNode(Node):
|
||||
|
@ -127,23 +127,20 @@ DYNAMIC_EXPR_RE = re.compile(
|
|||
comment_tag=r"(?:\{#.*?#\})",
|
||||
start_quote=r"(?P<quote>['\"])", # NOTE: Capture group so we check for the same quote at the end
|
||||
end_quote=r"(?P=quote)",
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def is_dynamic_expression(value: Any) -> bool:
|
||||
# NOTE: Currently dynamic expression need at least 6 characters
|
||||
# for the opening and closing tags, and quotes, e.g. `"`, `{%`, `%}` in `" some text {% ... %}"`
|
||||
MIN_EXPR_LEN = 6
|
||||
MIN_EXPR_LEN = 6 # noqa: N806
|
||||
|
||||
if not isinstance(value, str) or not value or len(value) < MIN_EXPR_LEN:
|
||||
return False
|
||||
|
||||
# Is not wrapped in quotes, or does not contain any tags
|
||||
if not DYNAMIC_EXPR_RE.match(value):
|
||||
return False
|
||||
|
||||
return True
|
||||
return bool(DYNAMIC_EXPR_RE.match(value))
|
||||
|
||||
|
||||
# TODO - Move this out into a plugin?
|
||||
|
@ -200,8 +197,9 @@ def process_aggregate_kwargs(params: List["TagParam"]) -> List["TagParam"]:
|
|||
This provides sufficient flexiblity to make it easy for component users to provide
|
||||
"fallthrough attributes", and sufficiently easy for component authors to process
|
||||
that input while still being able to provide their own keys.
|
||||
|
||||
"""
|
||||
from django_components.util.template_tag import TagParam
|
||||
from django_components.util.template_tag import TagParam # noqa: PLC0415
|
||||
|
||||
_check_kwargs_for_agg_conflict(params)
|
||||
|
||||
|
@ -233,7 +231,7 @@ def process_aggregate_kwargs(params: List["TagParam"]) -> List["TagParam"]:
|
|||
if key in seen_keys:
|
||||
raise TemplateSyntaxError(
|
||||
f"Received argument '{key}' both as a regular input ({key}=...)"
|
||||
f" and as an aggregate dict ('{key}:key=...'). Must be only one of the two"
|
||||
f" and as an aggregate dict ('{key}:key=...'). Must be only one of the two",
|
||||
)
|
||||
processed_params.append(TagParam(key=key, value=val))
|
||||
|
||||
|
@ -256,7 +254,7 @@ def _check_kwargs_for_agg_conflict(params: List["TagParam"]) -> None:
|
|||
): # fmt: skip
|
||||
raise TemplateSyntaxError(
|
||||
f"Received argument '{param.key}' both as a regular input ({param.key}=...)"
|
||||
f" and as an aggregate dict ('{param.key}:key=...'). Must be only one of the two"
|
||||
f" and as an aggregate dict ('{param.key}:key=...'). Must be only one of the two",
|
||||
)
|
||||
|
||||
if is_agg_kwarg:
|
||||
|
|
|
@ -555,7 +555,6 @@ class ComponentExtension(metaclass=ExtensionMeta):
|
|||
ctx.component_cls.my_attr = "my_value"
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_component_class_deleted(self, ctx: OnComponentClassDeletedContext) -> None:
|
||||
"""
|
||||
|
@ -577,7 +576,6 @@ class ComponentExtension(metaclass=ExtensionMeta):
|
|||
self.cache.pop(ctx.component_cls, None)
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_registry_created(self, ctx: OnRegistryCreatedContext) -> None:
|
||||
"""
|
||||
|
@ -599,7 +597,6 @@ class ComponentExtension(metaclass=ExtensionMeta):
|
|||
ctx.registry.my_attr = "my_value"
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_registry_deleted(self, ctx: OnRegistryDeletedContext) -> None:
|
||||
"""
|
||||
|
@ -621,7 +618,6 @@ class ComponentExtension(metaclass=ExtensionMeta):
|
|||
self.cache.pop(ctx.registry, None)
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_component_registered(self, ctx: OnComponentRegisteredContext) -> None:
|
||||
"""
|
||||
|
@ -641,7 +637,6 @@ class ComponentExtension(metaclass=ExtensionMeta):
|
|||
print(f"Component {ctx.component_cls} registered to {ctx.registry} as '{ctx.name}'")
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_component_unregistered(self, ctx: OnComponentUnregisteredContext) -> None:
|
||||
"""
|
||||
|
@ -661,7 +656,6 @@ class ComponentExtension(metaclass=ExtensionMeta):
|
|||
print(f"Component {ctx.component_cls} unregistered from {ctx.registry} as '{ctx.name}'")
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
###########################
|
||||
# Component render hooks
|
||||
|
@ -712,7 +706,6 @@ class ComponentExtension(metaclass=ExtensionMeta):
|
|||
[`Component.slots`](./api.md#django_components.Component.slots)
|
||||
are plain `list` / `dict` objects.
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_component_data(self, ctx: OnComponentDataContext) -> None:
|
||||
"""
|
||||
|
@ -739,7 +732,6 @@ class ComponentExtension(metaclass=ExtensionMeta):
|
|||
ctx.template_data["my_template_var"] = "my_value"
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_component_rendered(self, ctx: OnComponentRenderedContext) -> Optional[str]:
|
||||
"""
|
||||
|
@ -797,7 +789,6 @@ class ComponentExtension(metaclass=ExtensionMeta):
|
|||
print(f"Result: {ctx.result}")
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
##########################
|
||||
# Template / JS / CSS hooks
|
||||
|
@ -826,7 +817,6 @@ class ComponentExtension(metaclass=ExtensionMeta):
|
|||
return ctx.content.replace("Hello", "Hi")
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_template_compiled(self, ctx: OnTemplateCompiledContext) -> None:
|
||||
"""
|
||||
|
@ -849,7 +839,6 @@ class ComponentExtension(metaclass=ExtensionMeta):
|
|||
print(f"Template origin: {ctx.template.origin.name}")
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_css_loaded(self, ctx: OnCssLoadedContext) -> Optional[str]:
|
||||
"""
|
||||
|
@ -874,7 +863,6 @@ class ComponentExtension(metaclass=ExtensionMeta):
|
|||
return ctx.content.replace("Hello", "Hi")
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
def on_js_loaded(self, ctx: OnJsLoadedContext) -> Optional[str]:
|
||||
"""
|
||||
|
@ -899,7 +887,6 @@ class ComponentExtension(metaclass=ExtensionMeta):
|
|||
return ctx.content.replace("Hello", "Hi")
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
##########################
|
||||
# Tags lifecycle hooks
|
||||
|
@ -944,7 +931,6 @@ class ComponentExtension(metaclass=ExtensionMeta):
|
|||
print(f"Slot owner: {slot_owner}")
|
||||
```
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
# Decorator to store events in `ExtensionManager._events` when django_components is not yet initialized.
|
||||
|
@ -955,7 +941,7 @@ def store_events(func: TCallable) -> TCallable:
|
|||
def wrapper(self: "ExtensionManager", ctx: Any) -> Any:
|
||||
if not self._initialized:
|
||||
self._events.append((fn_name, ctx))
|
||||
return
|
||||
return None
|
||||
|
||||
return func(self, ctx)
|
||||
|
||||
|
@ -1051,13 +1037,13 @@ class ExtensionManager:
|
|||
extension_defaults = all_extensions_defaults.get(extension.name, None)
|
||||
if extension_defaults:
|
||||
# Create dummy class that holds the extension defaults
|
||||
defaults_class = type(f"{ext_class_name}Defaults", tuple(), extension_defaults.copy())
|
||||
defaults_class = type(f"{ext_class_name}Defaults", (), extension_defaults.copy())
|
||||
bases_list.insert(0, defaults_class)
|
||||
|
||||
if component_ext_subclass:
|
||||
bases_list.insert(0, component_ext_subclass)
|
||||
|
||||
bases: tuple[Type, ...] = tuple(bases_list)
|
||||
bases: Tuple[Type, ...] = tuple(bases_list)
|
||||
|
||||
# Allow component-level extension class to access the owner `Component` class that via
|
||||
# `component_cls`.
|
||||
|
@ -1118,7 +1104,7 @@ class ExtensionManager:
|
|||
urls: List[URLResolver] = []
|
||||
seen_names: Set[str] = set()
|
||||
|
||||
from django_components import Component
|
||||
from django_components import Component # noqa: PLC0415
|
||||
|
||||
for extension in self.extensions:
|
||||
# Ensure that the extension name won't conflict with existing Component class API
|
||||
|
@ -1297,7 +1283,7 @@ class ExtensionManager:
|
|||
for extension in self.extensions:
|
||||
try:
|
||||
result = extension.on_component_rendered(ctx)
|
||||
except Exception as error:
|
||||
except Exception as error: # noqa: BLE001
|
||||
# Error from `on_component_rendered()` - clear HTML and set error
|
||||
ctx = ctx._replace(result=None, error=error)
|
||||
else:
|
||||
|
|
|
@ -120,7 +120,7 @@ class ComponentCache(ExtensionComponentConfig):
|
|||
if self.include_slots:
|
||||
cache_key += ":" + self.hash_slots(slots)
|
||||
cache_key = self.component._class_hash + ":" + cache_key
|
||||
cache_key = CACHE_KEY_PREFIX + md5(cache_key.encode()).hexdigest()
|
||||
cache_key = CACHE_KEY_PREFIX + md5(cache_key.encode()).hexdigest() # noqa: S324
|
||||
return cache_key
|
||||
|
||||
def hash(self, args: List, kwargs: Dict) -> str:
|
||||
|
@ -141,10 +141,10 @@ class ComponentCache(ExtensionComponentConfig):
|
|||
hash_parts = []
|
||||
for key, slot in sorted_items:
|
||||
if callable(slot.contents):
|
||||
raise ValueError(
|
||||
raise TypeError(
|
||||
f"Cannot hash slot '{key}' of component '{self.component.name}' - Slot functions are unhashable."
|
||||
" Instead define the slot as a string or `{% fill %}` tag, or disable slot caching"
|
||||
" with `Cache.include_slots=False`."
|
||||
" with `Cache.include_slots=False`.",
|
||||
)
|
||||
hash_parts.append(f"{key}-{slot.contents}")
|
||||
return ",".join(hash_parts)
|
||||
|
@ -175,8 +175,8 @@ class CacheExtension(ComponentExtension):
|
|||
|
||||
ComponentConfig = ComponentCache
|
||||
|
||||
def __init__(self, *args: Any, **kwargs: Any):
|
||||
self.render_id_to_cache_key: dict[str, str] = {}
|
||||
def __init__(self, *_args: Any, **_kwargs: Any) -> None:
|
||||
self.render_id_to_cache_key: Dict[str, str] = {}
|
||||
|
||||
def on_component_input(self, ctx: OnComponentInputContext) -> Optional[Any]:
|
||||
cache_instance = ctx.component.cache
|
||||
|
@ -196,7 +196,7 @@ class CacheExtension(ComponentExtension):
|
|||
def on_component_rendered(self, ctx: OnComponentRenderedContext) -> None:
|
||||
cache_instance = ctx.component.cache
|
||||
if not cache_instance.enabled:
|
||||
return None
|
||||
return
|
||||
|
||||
if ctx.error is not None:
|
||||
return
|
||||
|
|
|
@ -21,14 +21,14 @@ COLORS = {
|
|||
}
|
||||
|
||||
|
||||
def apply_component_highlight(type: Literal["component", "slot"], output: str, name: str) -> str:
|
||||
def apply_component_highlight(highlight_type: Literal["component", "slot"], output: str, name: str) -> str:
|
||||
"""
|
||||
Wrap HTML (string) in a div with a border and a highlight color.
|
||||
|
||||
This is part of the component / slot highlighting feature. User can toggle on
|
||||
to see the component / slot boundaries.
|
||||
"""
|
||||
color = COLORS[type]
|
||||
color = COLORS[highlight_type]
|
||||
|
||||
# Because the component / slot name is set via styling as a `::before` pseudo-element,
|
||||
# we need to generate a unique ID for each component / slot to avoid conflicts.
|
||||
|
@ -36,13 +36,13 @@ def apply_component_highlight(type: Literal["component", "slot"], output: str, n
|
|||
|
||||
output = f"""
|
||||
<style>
|
||||
.{type}-highlight-{highlight_id}::before {{
|
||||
.{highlight_type}-highlight-{highlight_id}::before {{
|
||||
content: "{name}: ";
|
||||
font-weight: bold;
|
||||
color: {color.text_color};
|
||||
}}
|
||||
</style>
|
||||
<div class="{type}-highlight-{highlight_id}" style="border: 1px solid {color.border_color}">
|
||||
<div class="{highlight_type}-highlight-{highlight_id}" style="border: 1px solid {color.border_color}">
|
||||
{output}
|
||||
</div>
|
||||
"""
|
||||
|
|
|
@ -145,8 +145,6 @@ class ComponentDefaults(ExtensionComponentConfig):
|
|||
```
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DefaultsExtension(ComponentExtension):
|
||||
"""
|
||||
|
|
|
@ -27,7 +27,7 @@ else:
|
|||
|
||||
|
||||
class ViewFn(Protocol):
|
||||
def __call__(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Any: ... # noqa: E704
|
||||
def __call__(self, request: HttpRequest, *args: Any, **kwargs: Any) -> Any: ...
|
||||
|
||||
|
||||
def _get_component_route_name(component: Union[Type["Component"], "Component"]) -> str:
|
||||
|
@ -143,7 +143,7 @@ class ComponentView(ExtensionComponentConfig, View):
|
|||
```
|
||||
"""
|
||||
|
||||
component_cls = cast(Type["Component"], None)
|
||||
component_cls = cast("Type[Component]", None)
|
||||
"""
|
||||
The parent component class.
|
||||
|
||||
|
@ -220,28 +220,28 @@ class ComponentView(ExtensionComponentConfig, View):
|
|||
# `return self.component_cls.render_to_response(request, *args, **kwargs)` or similar
|
||||
# or raise NotImplementedError.
|
||||
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return getattr(self.component_cls(), "get")(request, *args, **kwargs)
|
||||
return self.component_cls().get(request, *args, **kwargs) # type: ignore[attr-defined]
|
||||
|
||||
def post(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return getattr(self.component_cls(), "post")(request, *args, **kwargs)
|
||||
return self.component_cls().post(request, *args, **kwargs) # type: ignore[attr-defined]
|
||||
|
||||
def put(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return getattr(self.component_cls(), "put")(request, *args, **kwargs)
|
||||
return self.component_cls().put(request, *args, **kwargs) # type: ignore[attr-defined]
|
||||
|
||||
def patch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return getattr(self.component_cls(), "patch")(request, *args, **kwargs)
|
||||
return self.component_cls().patch(request, *args, **kwargs) # type: ignore[attr-defined]
|
||||
|
||||
def delete(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return getattr(self.component_cls(), "delete")(request, *args, **kwargs)
|
||||
return self.component_cls().delete(request, *args, **kwargs) # type: ignore[attr-defined]
|
||||
|
||||
def head(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return getattr(self.component_cls(), "head")(request, *args, **kwargs)
|
||||
return self.component_cls().head(request, *args, **kwargs) # type: ignore[attr-defined]
|
||||
|
||||
def options(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return getattr(self.component_cls(), "options")(request, *args, **kwargs)
|
||||
return self.component_cls().options(request, *args, **kwargs) # type: ignore[attr-defined]
|
||||
|
||||
def trace(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
return getattr(self.component_cls(), "trace")(request, *args, **kwargs)
|
||||
return self.component_cls().trace(request, *args, **kwargs) # type: ignore[attr-defined]
|
||||
|
||||
|
||||
class ViewExtension(ComponentExtension):
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
|
||||
|
||||
from django import VERSION as DJANGO_VERSION
|
||||
from django.contrib.staticfiles.finders import BaseFinder
|
||||
from django.contrib.staticfiles.utils import get_files
|
||||
from django.core.checks import CheckMessage, Error, Warning
|
||||
from django.core import checks
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.utils._os import safe_join
|
||||
|
||||
|
@ -34,7 +35,7 @@ class ComponentsFileSystemFinder(BaseFinder):
|
|||
- If `COMPONENTS.dirs` is not set, defaults to `settings.BASE_DIR / "components"`
|
||||
"""
|
||||
|
||||
def __init__(self, app_names: Any = None, *args: Any, **kwargs: Any) -> None:
|
||||
def __init__(self, app_names: Any = None, *args: Any, **kwargs: Any) -> None: # noqa: ARG002
|
||||
component_dirs = [str(p) for p in get_component_dirs()]
|
||||
|
||||
# NOTE: The rest of the __init__ is the same as `django.contrib.staticfiles.finders.FileSystemFinder`,
|
||||
|
@ -47,7 +48,7 @@ class ComponentsFileSystemFinder(BaseFinder):
|
|||
self.storages: Dict[str, FileSystemStorage] = {}
|
||||
for root in component_dirs:
|
||||
if isinstance(root, (list, tuple)):
|
||||
prefix, root = root
|
||||
prefix, root = root # noqa: PLW2901
|
||||
else:
|
||||
prefix = ""
|
||||
if (prefix, root) not in self.locations:
|
||||
|
@ -60,41 +61,39 @@ class ComponentsFileSystemFinder(BaseFinder):
|
|||
super().__init__(*args, **kwargs)
|
||||
|
||||
# NOTE: Based on `FileSystemFinder.check`
|
||||
def check(self, **kwargs: Any) -> List[CheckMessage]:
|
||||
errors: List[CheckMessage] = []
|
||||
def check(self, **_kwargs: Any) -> List[checks.CheckMessage]:
|
||||
errors: List[checks.CheckMessage] = []
|
||||
if not isinstance(app_settings.DIRS, (list, tuple)):
|
||||
errors.append(
|
||||
Error(
|
||||
checks.Error(
|
||||
"The COMPONENTS.dirs setting is not a tuple or list.",
|
||||
hint="Perhaps you forgot a trailing comma?",
|
||||
id="components.E001",
|
||||
)
|
||||
),
|
||||
)
|
||||
return errors
|
||||
for root in app_settings.DIRS:
|
||||
if isinstance(root, (list, tuple)):
|
||||
prefix, root = root
|
||||
prefix, root = root # noqa: PLW2901
|
||||
if prefix.endswith("/"):
|
||||
errors.append(
|
||||
Error(
|
||||
"The prefix %r in the COMPONENTS.dirs setting must not end with a slash." % prefix,
|
||||
checks.Error(
|
||||
f"The prefix {prefix!r} in the COMPONENTS.dirs setting must not end with a slash.",
|
||||
id="staticfiles.E003",
|
||||
)
|
||||
),
|
||||
)
|
||||
elif not os.path.isdir(root):
|
||||
elif not Path(root).is_dir():
|
||||
errors.append(
|
||||
Warning(
|
||||
checks.Warning(
|
||||
f"The directory '{root}' in the COMPONENTS.dirs setting does not exist.",
|
||||
id="components.W004",
|
||||
)
|
||||
),
|
||||
)
|
||||
return errors
|
||||
|
||||
# NOTE: Same as `FileSystemFinder.find`
|
||||
def find(self, path: str, **kwargs: Any) -> Union[List[str], str]:
|
||||
"""
|
||||
Look for files in the extra locations as defined in COMPONENTS.dirs.
|
||||
"""
|
||||
"""Look for files in the extra locations as defined in COMPONENTS.dirs."""
|
||||
# Handle deprecated `all` parameter:
|
||||
# - In Django 5.2, the `all` parameter was deprecated in favour of `find_all`.
|
||||
# - Between Django 5.2 (inclusive) and 6.1 (exclusive), the `all` parameter was still
|
||||
|
@ -104,7 +103,7 @@ class ComponentsFileSystemFinder(BaseFinder):
|
|||
# See https://github.com/django/django/blob/5.2/django/contrib/staticfiles/finders.py#L58C9-L58C37
|
||||
# And https://github.com/django-components/django-components/issues/1119
|
||||
if DJANGO_VERSION >= (5, 2) and DJANGO_VERSION < (6, 1):
|
||||
find_all = self._check_deprecated_find_param(**kwargs) # type: ignore
|
||||
find_all = self._check_deprecated_find_param(**kwargs)
|
||||
elif DJANGO_VERSION >= (6, 1):
|
||||
find_all = kwargs.get("find_all", False)
|
||||
else:
|
||||
|
@ -128,28 +127,26 @@ class ComponentsFileSystemFinder(BaseFinder):
|
|||
absolute path (or ``None`` if no match).
|
||||
"""
|
||||
if prefix:
|
||||
prefix = "%s%s" % (prefix, os.sep)
|
||||
prefix = f"{prefix}{os.sep}"
|
||||
if not path.startswith(prefix):
|
||||
return None
|
||||
path = path.removeprefix(prefix)
|
||||
path = safe_join(root, path)
|
||||
|
||||
if os.path.exists(path) and self._is_path_valid(path):
|
||||
if Path(path).exists() and self._is_path_valid(path):
|
||||
return path
|
||||
return None
|
||||
|
||||
# `Finder.list` is called from `collectstatic` command,
|
||||
# see https://github.com/django/django/blob/bc9b6251e0b54c3b5520e3c66578041cc17e4a28/django/contrib/staticfiles/management/commands/collectstatic.py#L126C23-L126C30 # noqa E501
|
||||
# see https://github.com/django/django/blob/bc9b6251e0b54c3b5520e3c66578041cc17e4a28/django/contrib/staticfiles/management/commands/collectstatic.py#L126C23-L126C30
|
||||
#
|
||||
# NOTE: This is same as `FileSystemFinder.list`, but we exclude Python/HTML files
|
||||
# NOTE 2: Yield can be annotated as Iterable, see https://stackoverflow.com/questions/38419654
|
||||
def list(self, ignore_patterns: List[str]) -> Iterable[Tuple[str, FileSystemStorage]]:
|
||||
"""
|
||||
List all files in all locations.
|
||||
"""
|
||||
for prefix, root in self.locations:
|
||||
"""List all files in all locations."""
|
||||
for _prefix, root in self.locations:
|
||||
# Skip nonexistent directories.
|
||||
if os.path.isdir(root):
|
||||
if Path(root).is_dir():
|
||||
storage = self.storages[root]
|
||||
for path in get_files(storage, ignore_patterns):
|
||||
if self._is_path_valid(path):
|
||||
|
|
|
@ -31,9 +31,7 @@ class TagProtectedError(Exception):
|
|||
|
||||
Thus, this exception is raised when a component is attempted to be registered under
|
||||
a forbidden name, such that it would overwrite one of django_component's own template tags.
|
||||
""" # noqa: E501
|
||||
|
||||
pass
|
||||
"""
|
||||
|
||||
|
||||
PROTECTED_TAGS = [
|
||||
|
@ -57,9 +55,8 @@ def register_tag(
|
|||
) -> None:
|
||||
# Register inline tag
|
||||
if is_tag_protected(library, tag):
|
||||
raise TagProtectedError('Cannot register tag "%s", this tag name is protected' % tag)
|
||||
else:
|
||||
library.tag(tag, tag_fn)
|
||||
raise TagProtectedError(f'Cannot register tag "{tag}", this tag name is protected')
|
||||
library.tag(tag, tag_fn)
|
||||
|
||||
|
||||
def mark_protected_tags(lib: Library, tags: Optional[List[str]] = None) -> None:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import functools
|
||||
import inspect
|
||||
import keyword
|
||||
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, List, Optional, Tuple, Type, cast
|
||||
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Dict, Iterable, List, Optional, Tuple, Type, cast
|
||||
|
||||
from django.template import Context, Library
|
||||
from django.template.base import Node, NodeList, Parser, Token
|
||||
|
@ -50,10 +50,10 @@ class NodeMeta(type):
|
|||
bases: Tuple[Type, ...],
|
||||
attrs: Dict[str, Any],
|
||||
) -> Type["BaseNode"]:
|
||||
cls = cast(Type["BaseNode"], super().__new__(mcs, name, bases, attrs))
|
||||
cls = cast("Type[BaseNode]", super().__new__(mcs, name, bases, attrs))
|
||||
|
||||
# Ignore the `BaseNode` class itself
|
||||
if attrs.get("__module__", None) == "django_components.node":
|
||||
if attrs.get("__module__") == "django_components.node":
|
||||
return cls
|
||||
|
||||
if not hasattr(cls, "tag"):
|
||||
|
@ -195,8 +195,8 @@ class NodeMeta(type):
|
|||
|
||||
# Wrap cls.render() so we resolve the args and kwargs and pass them to the
|
||||
# actual render method.
|
||||
cls.render = wrapper_render # type: ignore
|
||||
cls.render._djc_wrapped = True # type: ignore
|
||||
cls.render = wrapper_render # type: ignore[assignment]
|
||||
cls.render._djc_wrapped = True # type: ignore[attr-defined]
|
||||
|
||||
return cls
|
||||
|
||||
|
@ -210,8 +210,7 @@ class BaseNode(Node, metaclass=NodeMeta):
|
|||
1. It declares how a particular template tag should be parsed - By setting the
|
||||
[`tag`](../api#django_components.BaseNode.tag),
|
||||
[`end_tag`](../api#django_components.BaseNode.end_tag),
|
||||
and [`allowed_flags`](../api#django_components.BaseNode.allowed_flags)
|
||||
attributes:
|
||||
and [`allowed_flags`](../api#django_components.BaseNode.allowed_flags) attributes:
|
||||
|
||||
```python
|
||||
class SlotNode(BaseNode):
|
||||
|
@ -306,7 +305,7 @@ class BaseNode(Node, metaclass=NodeMeta):
|
|||
```
|
||||
"""
|
||||
|
||||
allowed_flags: ClassVar[Optional[List[str]]] = None
|
||||
allowed_flags: ClassVar[Optional[Iterable[str]]] = None
|
||||
"""
|
||||
The list of all *possible* flags for this tag.
|
||||
|
||||
|
@ -328,7 +327,7 @@ class BaseNode(Node, metaclass=NodeMeta):
|
|||
```
|
||||
"""
|
||||
|
||||
def render(self, context: Context, *args: Any, **kwargs: Any) -> str:
|
||||
def render(self, context: Context, *_args: Any, **_kwargs: Any) -> str:
|
||||
"""
|
||||
Render the node. This method is meant to be overridden by subclasses.
|
||||
|
||||
|
@ -491,7 +490,7 @@ class BaseNode(Node, metaclass=NodeMeta):
|
|||
contents: Optional[str] = None,
|
||||
template_name: Optional[str] = None,
|
||||
template_component: Optional[Type["Component"]] = None,
|
||||
):
|
||||
) -> None:
|
||||
self.params = params
|
||||
self.flags = flags or {flag: False for flag in self.allowed_flags or []}
|
||||
self.nodelist = nodelist or NodeList()
|
||||
|
@ -501,10 +500,7 @@ class BaseNode(Node, metaclass=NodeMeta):
|
|||
self.template_component = template_component
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"<{self.__class__.__name__}: {self.node_id}. Contents: {repr(self.nodelist)}."
|
||||
f" Flags: {self.active_flags}>"
|
||||
)
|
||||
return f"<{self.__class__.__name__}: {self.node_id}. Contents: {self.contents}. Flags: {self.active_flags}>"
|
||||
|
||||
@property
|
||||
def active_flags(self) -> List[str]:
|
||||
|
@ -541,7 +537,7 @@ class BaseNode(Node, metaclass=NodeMeta):
|
|||
To register the tag, you can use [`BaseNode.register()`](../api#django_components.BaseNode.register).
|
||||
"""
|
||||
# NOTE: Avoids circular import
|
||||
from django_components.template import get_component_from_origin
|
||||
from django_components.template import get_component_from_origin # noqa: PLC0415
|
||||
|
||||
tag_id = gen_id()
|
||||
tag = parse_template_tag(cls.tag, cls.end_tag, cls.allowed_flags, parser, token)
|
||||
|
@ -650,7 +646,7 @@ def template_tag(
|
|||
{
|
||||
"tag": tag,
|
||||
"end_tag": end_tag,
|
||||
"allowed_flags": allowed_flags or [],
|
||||
"allowed_flags": allowed_flags or (),
|
||||
"render": fn,
|
||||
},
|
||||
)
|
||||
|
|
|
@ -77,10 +77,10 @@ component_renderer_cache: Dict[str, Tuple[ComponentRenderer, str]] = {}
|
|||
child_component_attrs: Dict[str, List[str]] = {}
|
||||
|
||||
nested_comp_pattern = re.compile(
|
||||
r'<template [^>]*?djc-render-id="\w{{{COMP_ID_LENGTH}}}"[^>]*?></template>'.format(COMP_ID_LENGTH=COMP_ID_LENGTH)
|
||||
r'<template [^>]*?djc-render-id="\w{{{COMP_ID_LENGTH}}}"[^>]*?></template>'.format(COMP_ID_LENGTH=COMP_ID_LENGTH), # noqa: UP032
|
||||
)
|
||||
render_id_pattern = re.compile(
|
||||
r'djc-render-id="(?P<render_id>\w{{{COMP_ID_LENGTH}}})"'.format(COMP_ID_LENGTH=COMP_ID_LENGTH)
|
||||
r'djc-render-id="(?P<render_id>\w{{{COMP_ID_LENGTH}}})"'.format(COMP_ID_LENGTH=COMP_ID_LENGTH), # noqa: UP032
|
||||
)
|
||||
|
||||
|
||||
|
@ -135,7 +135,8 @@ def component_post_render(
|
|||
component_name: str,
|
||||
parent_id: Optional[str],
|
||||
on_component_rendered_callbacks: Dict[
|
||||
str, Callable[[Optional[str], Optional[Exception]], OnComponentRenderedResult]
|
||||
str,
|
||||
Callable[[Optional[str], Optional[Exception]], OnComponentRenderedResult],
|
||||
],
|
||||
on_html_rendered: Callable[[str], str],
|
||||
) -> str:
|
||||
|
@ -345,11 +346,11 @@ def component_post_render(
|
|||
continue
|
||||
|
||||
# Skip parts of errored components
|
||||
elif curr_item.parent_id in ignored_ids:
|
||||
if curr_item.parent_id in ignored_ids:
|
||||
continue
|
||||
|
||||
# Process text parts
|
||||
elif isinstance(curr_item, TextPart):
|
||||
if isinstance(curr_item, TextPart):
|
||||
parent_html_parts = get_html_parts(curr_item.parent_id)
|
||||
parent_html_parts.append(curr_item.text)
|
||||
|
||||
|
@ -388,7 +389,7 @@ def component_post_render(
|
|||
# - Rendering of component's template
|
||||
#
|
||||
# In all cases, we want to mark the component as errored, and let the parent handle it.
|
||||
except Exception as err:
|
||||
except Exception as err: # noqa: BLE001
|
||||
handle_error(component_id=component_id, error=err)
|
||||
continue
|
||||
|
||||
|
@ -416,7 +417,7 @@ def component_post_render(
|
|||
last_index = 0
|
||||
parts_to_process: List[Union[TextPart, ComponentPart]] = []
|
||||
for match in nested_comp_pattern.finditer(comp_content):
|
||||
part_before_component = comp_content[last_index : match.start()] # noqa: E203
|
||||
part_before_component = comp_content[last_index : match.start()]
|
||||
last_index = match.end()
|
||||
comp_part = match[0]
|
||||
|
||||
|
@ -490,7 +491,7 @@ def _call_generator_before_callback(
|
|||
|
||||
# Catch if `Component.on_render()` raises an exception, in which case this becomes
|
||||
# the new error.
|
||||
except Exception as new_error:
|
||||
except Exception as new_error: # noqa: BLE001
|
||||
error = new_error
|
||||
html = None
|
||||
# This raises if `StopIteration` was not raised, which may be if `Component.on_render()`
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
This module contains optimizations for the `{% provide %}` feature.
|
||||
"""
|
||||
"""This module contains optimizations for the `{% provide %}` feature."""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from typing import Dict, Generator, NamedTuple, Set
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from collections import namedtuple
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, NamedTuple, Optional
|
||||
|
||||
from django.template import Context, TemplateSyntaxError
|
||||
from django.utils.safestring import SafeString
|
||||
|
@ -85,7 +84,7 @@ class ProvideNode(BaseNode):
|
|||
|
||||
tag = "provide"
|
||||
end_tag = "endprovide"
|
||||
allowed_flags = []
|
||||
allowed_flags = ()
|
||||
|
||||
def render(self, context: Context, name: str, **kwargs: Any) -> SafeString:
|
||||
# NOTE: The "provided" kwargs are meant to be shared privately, meaning that components
|
||||
|
@ -130,7 +129,7 @@ def get_injected_context_var(
|
|||
raise KeyError(
|
||||
f"Component '{component_name}' tried to inject a variable '{key}' before it was provided."
|
||||
f" To fix this, make sure that at least one ancestor of component '{component_name}' has"
|
||||
f" the variable '{key}' in their 'provide' attribute."
|
||||
f" the variable '{key}' in their 'provide' attribute.",
|
||||
)
|
||||
|
||||
|
||||
|
@ -148,17 +147,18 @@ def set_provided_context_var(
|
|||
# within template.
|
||||
if not key:
|
||||
raise TemplateSyntaxError(
|
||||
"Provide tag received an empty string. Key must be non-empty and a valid identifier."
|
||||
"Provide tag received an empty string. Key must be non-empty and a valid identifier.",
|
||||
)
|
||||
if not key.isidentifier():
|
||||
raise TemplateSyntaxError(
|
||||
"Provide tag received a non-identifier string. Key must be non-empty and a valid identifier."
|
||||
"Provide tag received a non-identifier string. Key must be non-empty and a valid identifier.",
|
||||
)
|
||||
|
||||
# We turn the kwargs into a NamedTuple so that the object that's "provided"
|
||||
# is immutable. This ensures that the data returned from `inject` will always
|
||||
# have all the keys that were passed to the `provide` tag.
|
||||
tuple_cls = namedtuple("DepInject", provided_kwargs.keys()) # type: ignore[misc]
|
||||
fields = [(field, Any) for field in provided_kwargs]
|
||||
tuple_cls = NamedTuple("DepInject", fields) # type: ignore[misc]
|
||||
payload = tuple_cls(**provided_kwargs)
|
||||
|
||||
# Instead of storing the provided data on the Context object, we store it
|
||||
|
|
|
@ -174,9 +174,10 @@ class SlotFunc(Protocol, Generic[TSlotData]):
|
|||
},
|
||||
)
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
def __call__(self, ctx: SlotContext[TSlotData]) -> SlotResult: ... # noqa E704
|
||||
def __call__(self, ctx: SlotContext[TSlotData]) -> SlotResult: ...
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -238,7 +239,7 @@ class Slot(Generic[TSlotData]):
|
|||
|
||||
Read more about [Slot contents](../../concepts/fundamentals/slots#slot-contents).
|
||||
"""
|
||||
content_func: SlotFunc[TSlotData] = cast(SlotFunc[TSlotData], None)
|
||||
content_func: SlotFunc[TSlotData] = cast("SlotFunc[TSlotData]", None) # noqa: RUF009
|
||||
"""
|
||||
The actual slot function.
|
||||
|
||||
|
@ -319,7 +320,7 @@ class Slot(Generic[TSlotData]):
|
|||
# Raise if Slot received another Slot instance as `contents`,
|
||||
# because this leads to ambiguity about how to handle the metadata.
|
||||
if isinstance(self.contents, Slot):
|
||||
raise ValueError("Slot received another Slot instance as `contents`")
|
||||
raise TypeError("Slot received another Slot instance as `contents`")
|
||||
|
||||
if self.content_func is None:
|
||||
self.contents, new_nodelist, self.content_func = self._resolve_contents(self.contents)
|
||||
|
@ -327,7 +328,7 @@ class Slot(Generic[TSlotData]):
|
|||
self.nodelist = new_nodelist
|
||||
|
||||
if not callable(self.content_func):
|
||||
raise ValueError(f"Slot 'content_func' must be a callable, got: {self.content_func}")
|
||||
raise TypeError(f"Slot 'content_func' must be a callable, got: {self.content_func}")
|
||||
|
||||
# Allow to treat the instances as functions
|
||||
def __call__(
|
||||
|
@ -463,9 +464,9 @@ class SlotFallback:
|
|||
def slot_function(self, ctx: SlotContext):
|
||||
return f"Hello, {ctx.fallback}!"
|
||||
```
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
def __init__(self, slot: "SlotNode", context: Context):
|
||||
def __init__(self, slot: "SlotNode", context: Context) -> None:
|
||||
self._slot = slot
|
||||
self._context = context
|
||||
|
||||
|
@ -486,12 +487,10 @@ name_escape_re = re.compile(r"[^\w]")
|
|||
|
||||
# TODO_v1 - Remove, superseded by `Component.slots` and `component_vars.slots`
|
||||
class SlotIsFilled(dict):
|
||||
"""
|
||||
Dictionary that returns `True` if the slot is filled (key is found), `False` otherwise.
|
||||
"""
|
||||
"""Dictionary that returns `True` if the slot is filled (key is found), `False` otherwise."""
|
||||
|
||||
def __init__(self, fills: Dict, *args: Any, **kwargs: Any) -> None:
|
||||
escaped_fill_names = {self._escape_slot_name(fill_name): True for fill_name in fills.keys()}
|
||||
escaped_fill_names = {self._escape_slot_name(fill_name): True for fill_name in fills}
|
||||
super().__init__(escaped_fill_names, *args, **kwargs)
|
||||
|
||||
def __missing__(self, key: Any) -> bool:
|
||||
|
@ -641,7 +640,7 @@ class SlotNode(BaseNode):
|
|||
|
||||
tag = "slot"
|
||||
end_tag = "endslot"
|
||||
allowed_flags = [SLOT_DEFAULT_FLAG, SLOT_REQUIRED_FLAG]
|
||||
allowed_flags = (SLOT_DEFAULT_FLAG, SLOT_REQUIRED_FLAG)
|
||||
|
||||
# NOTE:
|
||||
# In the current implementation, the slots are resolved only at the render time.
|
||||
|
@ -675,7 +674,7 @@ class SlotNode(BaseNode):
|
|||
raise TemplateSyntaxError(
|
||||
"Encountered a SlotNode outside of a Component context. "
|
||||
"Make sure that all {% slot %} tags are nested within {% component %} tags.\n"
|
||||
f"SlotNode: {self.__repr__()}"
|
||||
f"SlotNode: {self.__repr__()}",
|
||||
)
|
||||
|
||||
# Component info
|
||||
|
@ -715,7 +714,7 @@ class SlotNode(BaseNode):
|
|||
"Only one component slot may be marked as 'default', "
|
||||
f"found '{default_slot_name}' and '{slot_name}'. "
|
||||
f"To fix, check template '{component_ctx.template_name}' "
|
||||
f"of component '{component_name}'."
|
||||
f"of component '{component_name}'.",
|
||||
)
|
||||
|
||||
if default_slot_name is None:
|
||||
|
@ -730,7 +729,7 @@ class SlotNode(BaseNode):
|
|||
):
|
||||
raise TemplateSyntaxError(
|
||||
f"Slot '{slot_name}' of component '{component_name}' was filled twice: "
|
||||
"once explicitly and once implicitly as 'default'."
|
||||
"once explicitly and once implicitly as 'default'.",
|
||||
)
|
||||
|
||||
# If slot is marked as 'default', we use the name 'default' for the fill,
|
||||
|
@ -798,7 +797,8 @@ class SlotNode(BaseNode):
|
|||
# To achieve that, we first find the left-most `hui3q2` (index 2), and then find the `ax3c89`
|
||||
# in the list of dicts before it (index 1).
|
||||
curr_index = get_index(
|
||||
context.dicts, lambda d: _COMPONENT_CONTEXT_KEY in d and d[_COMPONENT_CONTEXT_KEY] == component_id
|
||||
context.dicts,
|
||||
lambda d: _COMPONENT_CONTEXT_KEY in d and d[_COMPONENT_CONTEXT_KEY] == component_id,
|
||||
)
|
||||
parent_index = get_last_index(context.dicts[:curr_index], lambda d: _COMPONENT_CONTEXT_KEY in d)
|
||||
|
||||
|
@ -808,7 +808,8 @@ class SlotNode(BaseNode):
|
|||
# Looking left finds nothing. In this case, look for the first component layer to the right.
|
||||
if parent_index is None and curr_index + 1 < len(context.dicts):
|
||||
parent_index = get_index(
|
||||
context.dicts[curr_index + 1 :], lambda d: _COMPONENT_CONTEXT_KEY in d # noqa: E203
|
||||
context.dicts[curr_index + 1 :],
|
||||
lambda d: _COMPONENT_CONTEXT_KEY in d,
|
||||
)
|
||||
if parent_index is not None:
|
||||
parent_index = parent_index + curr_index + 1
|
||||
|
@ -914,7 +915,7 @@ class SlotNode(BaseNode):
|
|||
# {% endprovide %}
|
||||
for key, value in context.flatten().items():
|
||||
if key.startswith(_INJECT_CONTEXT_KEY_PREFIX):
|
||||
extra_context[key] = value
|
||||
extra_context[key] = value # noqa: PERF403
|
||||
|
||||
fallback = SlotFallback(self, context)
|
||||
|
||||
|
@ -982,10 +983,9 @@ class SlotNode(BaseNode):
|
|||
registry_settings = component.registry.settings
|
||||
if registry_settings.context_behavior == ContextBehavior.DJANGO:
|
||||
return context
|
||||
elif registry_settings.context_behavior == ContextBehavior.ISOLATED:
|
||||
if registry_settings.context_behavior == ContextBehavior.ISOLATED:
|
||||
return outer_context if outer_context is not None else Context()
|
||||
else:
|
||||
raise ValueError(f"Unknown value for context_behavior: '{registry_settings.context_behavior}'")
|
||||
raise ValueError(f"Unknown value for context_behavior: '{registry_settings.context_behavior}'")
|
||||
|
||||
|
||||
class FillNode(BaseNode):
|
||||
|
@ -1138,7 +1138,7 @@ class FillNode(BaseNode):
|
|||
|
||||
tag = "fill"
|
||||
end_tag = "endfill"
|
||||
allowed_flags = []
|
||||
allowed_flags = ()
|
||||
|
||||
def render(
|
||||
self,
|
||||
|
@ -1155,15 +1155,15 @@ class FillNode(BaseNode):
|
|||
if fallback is not None and default is not None:
|
||||
raise TemplateSyntaxError(
|
||||
f"Fill tag received both 'default' and '{FILL_FALLBACK_KWARG}' kwargs. "
|
||||
f"Use '{FILL_FALLBACK_KWARG}' instead."
|
||||
f"Use '{FILL_FALLBACK_KWARG}' instead.",
|
||||
)
|
||||
elif fallback is None and default is not None:
|
||||
if fallback is None and default is not None:
|
||||
fallback = default
|
||||
|
||||
if not _is_extracting_fill(context):
|
||||
raise TemplateSyntaxError(
|
||||
"FillNode.render() (AKA {% fill ... %} block) cannot be rendered outside of a Component context. "
|
||||
"Make sure that the {% fill %} tags are nested within {% component %} tags."
|
||||
"Make sure that the {% fill %} tags are nested within {% component %} tags.",
|
||||
)
|
||||
|
||||
# Validate inputs
|
||||
|
@ -1175,31 +1175,31 @@ class FillNode(BaseNode):
|
|||
raise TemplateSyntaxError(f"Fill tag '{FILL_DATA_KWARG}' kwarg must resolve to a string, got {data}")
|
||||
if not is_identifier(data):
|
||||
raise RuntimeError(
|
||||
f"Fill tag kwarg '{FILL_DATA_KWARG}' does not resolve to a valid Python identifier, got '{data}'"
|
||||
f"Fill tag kwarg '{FILL_DATA_KWARG}' does not resolve to a valid Python identifier, got '{data}'",
|
||||
)
|
||||
|
||||
if fallback is not None:
|
||||
if not isinstance(fallback, str):
|
||||
raise TemplateSyntaxError(
|
||||
f"Fill tag '{FILL_FALLBACK_KWARG}' kwarg must resolve to a string, got {fallback}"
|
||||
f"Fill tag '{FILL_FALLBACK_KWARG}' kwarg must resolve to a string, got {fallback}",
|
||||
)
|
||||
if not is_identifier(fallback):
|
||||
raise RuntimeError(
|
||||
f"Fill tag kwarg '{FILL_FALLBACK_KWARG}' does not resolve to a valid Python identifier,"
|
||||
f" got '{fallback}'"
|
||||
f" got '{fallback}'",
|
||||
)
|
||||
|
||||
# data and fallback cannot be bound to the same variable
|
||||
if data and fallback and data == fallback:
|
||||
raise RuntimeError(
|
||||
f"Fill '{name}' received the same string for slot fallback ({FILL_FALLBACK_KWARG}=...)"
|
||||
f" and slot data ({FILL_DATA_KWARG}=...)"
|
||||
f" and slot data ({FILL_DATA_KWARG}=...)",
|
||||
)
|
||||
|
||||
if body is not None and self.contents:
|
||||
raise TemplateSyntaxError(
|
||||
f"Fill '{name}' received content both through '{FILL_BODY_KWARG}' kwarg and '{{% fill %}}' body. "
|
||||
f"Use only one method."
|
||||
f"Use only one method.",
|
||||
)
|
||||
|
||||
fill_data = FillWithData(
|
||||
|
@ -1229,7 +1229,7 @@ class FillNode(BaseNode):
|
|||
if captured_fills is None:
|
||||
raise RuntimeError(
|
||||
"FillNode.render() (AKA {% fill ... %} block) cannot be rendered outside of a Component context. "
|
||||
"Make sure that the {% fill %} tags are nested within {% component %} tags."
|
||||
"Make sure that the {% fill %} tags are nested within {% component %} tags.",
|
||||
)
|
||||
|
||||
# To allow using variables which were defined within the template and to which
|
||||
|
@ -1282,9 +1282,9 @@ class FillNode(BaseNode):
|
|||
# ]
|
||||
for layer in context.dicts:
|
||||
if "forloop" in layer:
|
||||
layer = layer.copy()
|
||||
layer["forloop"] = layer["forloop"].copy()
|
||||
data.extra_context.update(layer)
|
||||
layer_copy = layer.copy()
|
||||
layer_copy["forloop"] = layer_copy["forloop"].copy()
|
||||
data.extra_context.update(layer_copy)
|
||||
|
||||
captured_fills.append(data)
|
||||
|
||||
|
@ -1472,11 +1472,11 @@ def _extract_fill_content(
|
|||
if not captured_fills:
|
||||
return False
|
||||
|
||||
elif content:
|
||||
if content:
|
||||
raise TemplateSyntaxError(
|
||||
f"Illegal content passed to component '{component_name}'. "
|
||||
"Explicit 'fill' tags cannot occur alongside other text. "
|
||||
"The component body rendered content: {content}"
|
||||
"The component body rendered content: {content}",
|
||||
)
|
||||
|
||||
# Check for any duplicates
|
||||
|
@ -1485,7 +1485,7 @@ def _extract_fill_content(
|
|||
if fill.name in seen_names:
|
||||
raise TemplateSyntaxError(
|
||||
f"Multiple fill tags cannot target the same slot name in component '{component_name}': "
|
||||
f"Detected duplicate fill tag name '{fill.name}'."
|
||||
f"Detected duplicate fill tag name '{fill.name}'.",
|
||||
)
|
||||
seen_names.add(fill.name)
|
||||
|
||||
|
@ -1550,7 +1550,7 @@ def normalize_slot_fills(
|
|||
if content is None:
|
||||
continue
|
||||
# Case: Content is a string / non-slot / non-callable
|
||||
elif not callable(content):
|
||||
if not callable(content):
|
||||
# NOTE: `Slot.content_func` and `Slot.nodelist` will be set in `Slot.__init__()`
|
||||
slot: Slot = Slot(contents=content, component_name=component_name, slot_name=slot_name)
|
||||
# Case: Content is a callable, so either a plain function or a `Slot` instance.
|
||||
|
@ -1573,17 +1573,15 @@ def _nodelist_to_slot(
|
|||
fill_node: Optional[Union[FillNode, "ComponentNode"]] = None,
|
||||
extra: Optional[Dict[str, Any]] = None,
|
||||
) -> Slot:
|
||||
if data_var:
|
||||
if not data_var.isidentifier():
|
||||
raise TemplateSyntaxError(
|
||||
f"Slot data alias in fill '{slot_name}' must be a valid identifier. Got '{data_var}'"
|
||||
)
|
||||
if data_var and not data_var.isidentifier():
|
||||
raise TemplateSyntaxError(
|
||||
f"Slot data alias in fill '{slot_name}' must be a valid identifier. Got '{data_var}'",
|
||||
)
|
||||
|
||||
if fallback_var:
|
||||
if not fallback_var.isidentifier():
|
||||
raise TemplateSyntaxError(
|
||||
f"Slot fallback alias in fill '{slot_name}' must be a valid identifier. Got '{fallback_var}'"
|
||||
)
|
||||
if fallback_var and not fallback_var.isidentifier():
|
||||
raise TemplateSyntaxError(
|
||||
f"Slot fallback alias in fill '{slot_name}' must be a valid identifier. Got '{fallback_var}'",
|
||||
)
|
||||
|
||||
# We use Template.render() to render the nodelist, so that Django correctly sets up
|
||||
# and binds the context.
|
||||
|
@ -1655,7 +1653,7 @@ def _nodelist_to_slot(
|
|||
return rendered
|
||||
|
||||
return Slot(
|
||||
content_func=cast(SlotFunc, render_func),
|
||||
content_func=cast("SlotFunc", render_func),
|
||||
component_name=component_name,
|
||||
slot_name=slot_name,
|
||||
nodelist=nodelist,
|
||||
|
|
|
@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
|||
|
||||
# Require the start / end tags to contain NO spaces and only these characters
|
||||
TAG_CHARS = r"\w\-\:\@\.\#/"
|
||||
TAG_RE = re.compile(r"^[{chars}]+$".format(chars=TAG_CHARS))
|
||||
TAG_RE = re.compile(rf"^[{TAG_CHARS}]+$")
|
||||
|
||||
|
||||
class TagResult(NamedTuple):
|
||||
|
@ -106,6 +106,7 @@ class TagFormatterABC(abc.ABC):
|
|||
|
||||
Returns:
|
||||
str: The formatted start tag.
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
|
@ -119,6 +120,7 @@ class TagFormatterABC(abc.ABC):
|
|||
|
||||
Returns:
|
||||
str: The formatted end tag.
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
|
@ -131,7 +133,7 @@ class TagFormatterABC(abc.ABC):
|
|||
which is a tuple of `(component_name, remaining_tokens)`.
|
||||
|
||||
Args:
|
||||
tokens [List(str]): List of tokens passed to the component tag.
|
||||
tokens (List[str]): List of tokens passed to the component tag.
|
||||
|
||||
Returns:
|
||||
TagResult: Parsed component name and remaining tokens.
|
||||
|
@ -160,16 +162,15 @@ class TagFormatterABC(abc.ABC):
|
|||
```python
|
||||
TagResult('my_comp', ['key=val', 'key2=val2'])
|
||||
```
|
||||
|
||||
"""
|
||||
...
|
||||
|
||||
|
||||
class InternalTagFormatter:
|
||||
"""
|
||||
Internal wrapper around user-provided TagFormatters, so that we validate the outputs.
|
||||
"""
|
||||
"""Internal wrapper around user-provided TagFormatters, so that we validate the outputs."""
|
||||
|
||||
def __init__(self, tag_formatter: TagFormatterABC):
|
||||
def __init__(self, tag_formatter: TagFormatterABC) -> None:
|
||||
self.tag_formatter = tag_formatter
|
||||
|
||||
def start_tag(self, name: str) -> str:
|
||||
|
@ -192,13 +193,13 @@ class InternalTagFormatter:
|
|||
if not tag:
|
||||
raise ValueError(
|
||||
f"{self.tag_formatter.__class__.__name__} returned an invalid tag for {tag_type}: '{tag}'."
|
||||
f" Tag cannot be empty"
|
||||
f" Tag cannot be empty",
|
||||
)
|
||||
|
||||
if not TAG_RE.match(tag):
|
||||
raise ValueError(
|
||||
f"{self.tag_formatter.__class__.__name__} returned an invalid tag for {tag_type}: '{tag}'."
|
||||
f" Tag must contain only following chars: {TAG_CHARS}"
|
||||
f" Tag must contain only following chars: {TAG_CHARS}",
|
||||
)
|
||||
|
||||
|
||||
|
@ -222,13 +223,13 @@ class ComponentFormatter(TagFormatterABC):
|
|||
```
|
||||
"""
|
||||
|
||||
def __init__(self, tag: str):
|
||||
def __init__(self, tag: str) -> None:
|
||||
self.tag = tag
|
||||
|
||||
def start_tag(self, name: str) -> str:
|
||||
def start_tag(self, _name: str) -> str:
|
||||
return self.tag
|
||||
|
||||
def end_tag(self, name: str) -> str:
|
||||
def end_tag(self, _name: str) -> str:
|
||||
return f"end{self.tag}"
|
||||
|
||||
def parse(self, tokens: List[str]) -> TagResult:
|
||||
|
@ -238,10 +239,7 @@ class ComponentFormatter(TagFormatterABC):
|
|||
raise TemplateSyntaxError(f"{self.__class__.__name__}: Component tag did not receive tag name")
|
||||
|
||||
# If the first arg is a kwarg, then clearly the component name is not set.
|
||||
if "=" in args[0]:
|
||||
comp_name = None
|
||||
else:
|
||||
comp_name = args.pop(0)
|
||||
comp_name = None if "=" in args[0] else args.pop(0)
|
||||
|
||||
if not comp_name:
|
||||
raise TemplateSyntaxError("Component name must be a non-empty quoted string, e.g. 'my_comp'")
|
||||
|
|
|
@ -60,7 +60,8 @@ def cached_template(
|
|||
engine=...
|
||||
)
|
||||
```
|
||||
""" # noqa: E501
|
||||
|
||||
"""
|
||||
template_cache = get_template_cache()
|
||||
|
||||
template_cls = template_cls or Template
|
||||
|
@ -103,7 +104,7 @@ def prepare_component_template(
|
|||
"Django-components received a Template instance which was not patched."
|
||||
"If you are using Django's Template class, check if you added django-components"
|
||||
"to INSTALLED_APPS. If you are using a custom template class, then you need to"
|
||||
"manually patch the class."
|
||||
"manually patch the class.",
|
||||
)
|
||||
|
||||
with _maybe_bind_template(context, template):
|
||||
|
@ -223,7 +224,7 @@ def _get_component_template(component: "Component") -> Optional[Template]:
|
|||
|
||||
# TODO_V1 - Remove `get_template_string()` in v1
|
||||
if hasattr(component, "get_template_string"):
|
||||
template_string_getter = getattr(component, "get_template_string")
|
||||
template_string_getter = component.get_template_string
|
||||
template_body_from_getter = template_string_getter(component.context)
|
||||
else:
|
||||
template_body_from_getter = None
|
||||
|
@ -244,8 +245,7 @@ def _get_component_template(component: "Component") -> Optional[Template]:
|
|||
sources_with_values = [k for k, v in template_sources.items() if v is not None]
|
||||
if len(sources_with_values) > 1:
|
||||
raise ImproperlyConfigured(
|
||||
f"Component template was set multiple times in Component {component.name}."
|
||||
f"Sources: {sources_with_values}"
|
||||
f"Component template was set multiple times in Component {component.name}. Sources: {sources_with_values}",
|
||||
)
|
||||
|
||||
# Load the template based on the source
|
||||
|
@ -281,7 +281,7 @@ def _get_component_template(component: "Component") -> Optional[Template]:
|
|||
if template is not None:
|
||||
return template
|
||||
# Create the template from the string
|
||||
elif template_string is not None:
|
||||
if template_string is not None:
|
||||
return _create_template_from_string(component.__class__, template_string)
|
||||
|
||||
# Otherwise, Component has no template - this is valid, as it may be instead rendered
|
||||
|
@ -384,7 +384,12 @@ def cache_component_template_file(component_cls: Type["Component"]) -> None:
|
|||
return
|
||||
|
||||
# NOTE: Avoids circular import
|
||||
from django_components.component_media import ComponentMedia, Unset, _resolve_component_relative_files, is_set
|
||||
from django_components.component_media import ( # noqa: PLC0415
|
||||
ComponentMedia,
|
||||
Unset,
|
||||
_resolve_component_relative_files,
|
||||
is_set,
|
||||
)
|
||||
|
||||
# If we access the `Component.template_file` attribute, then this triggers media resolution if it was not done yet.
|
||||
# The problem is that this also causes the loading of the Template, if Component has defined `template_file`.
|
||||
|
@ -422,12 +427,12 @@ def get_component_by_template_file(template_file: str) -> Optional[Type["Compone
|
|||
#
|
||||
# So at this point we want to call `cache_component_template_file()` for all Components for which
|
||||
# we skipped it earlier.
|
||||
global component_template_file_cache_initialized
|
||||
global component_template_file_cache_initialized # noqa: PLW0603
|
||||
if not component_template_file_cache_initialized:
|
||||
component_template_file_cache_initialized = True
|
||||
|
||||
# NOTE: Avoids circular import
|
||||
from django_components.component import all_components
|
||||
from django_components.component import all_components # noqa: PLC0415
|
||||
|
||||
components = all_components()
|
||||
for component in components:
|
||||
|
@ -462,10 +467,10 @@ def get_component_by_template_file(template_file: str) -> Optional[Type["Compone
|
|||
|
||||
# NOTE: Used by `@djc_test` to reset the component template file cache
|
||||
def _reset_component_template_file_cache() -> None:
|
||||
global component_template_file_cache
|
||||
global component_template_file_cache # noqa: PLW0603
|
||||
component_template_file_cache = {}
|
||||
|
||||
global component_template_file_cache_initialized
|
||||
global component_template_file_cache_initialized # noqa: PLW0603
|
||||
component_template_file_cache_initialized = False
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
"""
|
||||
Template loader that loads templates from each Django app's "components" directory.
|
||||
"""
|
||||
"""Template loader that loads templates from each Django app's "components" directory."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
|
|
@ -10,7 +10,7 @@ urlpatterns = [
|
|||
[
|
||||
*dependencies_urlpatterns,
|
||||
*extension_urlpatterns,
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
|
|
|
@ -7,17 +7,17 @@ T = TypeVar("T")
|
|||
class CacheNode(Generic[T]):
|
||||
"""A node in the doubly linked list."""
|
||||
|
||||
def __init__(self, key: Hashable, value: T):
|
||||
def __init__(self, key: Hashable, value: T) -> None:
|
||||
self.key = key
|
||||
self.value = value
|
||||
self.prev: Optional["CacheNode"] = None
|
||||
self.next: Optional["CacheNode"] = None
|
||||
self.prev: Optional[CacheNode] = None
|
||||
self.next: Optional[CacheNode] = None
|
||||
|
||||
|
||||
class LRUCache(Generic[T]):
|
||||
"""A simple LRU Cache implementation."""
|
||||
|
||||
def __init__(self, maxsize: Optional[int] = None):
|
||||
def __init__(self, maxsize: Optional[int] = None) -> None:
|
||||
"""
|
||||
Initialize the LRU cache.
|
||||
|
||||
|
@ -26,8 +26,8 @@ class LRUCache(Generic[T]):
|
|||
self.maxsize = maxsize
|
||||
self.cache: Dict[Hashable, CacheNode[T]] = {} # Maps keys to nodes in the doubly linked list
|
||||
# Dummy head and tail nodes to simplify operations
|
||||
self.head = CacheNode[T]("", cast(T, None)) # Most recently used
|
||||
self.tail = CacheNode[T]("", cast(T, None)) # Least recently used
|
||||
self.head = CacheNode[T]("", cast("T", None)) # Most recently used
|
||||
self.tail = CacheNode[T]("", cast("T", None)) # Least recently used
|
||||
self.head.next = self.tail
|
||||
self.tail.prev = self.head
|
||||
|
||||
|
@ -44,8 +44,8 @@ class LRUCache(Generic[T]):
|
|||
self._remove(node)
|
||||
self._add_to_front(node)
|
||||
return node.value
|
||||
else:
|
||||
return None # Key not found
|
||||
|
||||
return None # Key not found
|
||||
|
||||
def has(self, key: Hashable) -> bool:
|
||||
"""
|
||||
|
|
|
@ -35,7 +35,15 @@ def mark_extension_command_api(obj: TClass) -> TClass:
|
|||
#############################
|
||||
|
||||
CommandLiteralAction = Literal[
|
||||
"append", "append_const", "count", "extend", "store", "store_const", "store_true", "store_false", "version"
|
||||
"append",
|
||||
"append_const",
|
||||
"count",
|
||||
"extend",
|
||||
"store",
|
||||
"store_const",
|
||||
"store_true",
|
||||
"store_false",
|
||||
"version",
|
||||
]
|
||||
"""
|
||||
The basic type of action to be taken when this argument is encountered at the command line.
|
||||
|
@ -43,7 +51,7 @@ The basic type of action to be taken when this argument is encountered at the co
|
|||
This is a subset of the values for `action` in
|
||||
[`ArgumentParser.add_argument()`](https://docs.python.org/3/library/argparse.html#the-add-argument-method).
|
||||
"""
|
||||
mark_extension_command_api(CommandLiteralAction) # type: ignore
|
||||
mark_extension_command_api(CommandLiteralAction) # type: ignore[type-var]
|
||||
|
||||
|
||||
@mark_extension_command_api
|
||||
|
@ -54,7 +62,7 @@ class CommandArg:
|
|||
|
||||
Fields on this class correspond to the arguments for
|
||||
[`ArgumentParser.add_argument()`](https://docs.python.org/3/library/argparse.html#the-add-argument-method)
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
name_or_flags: Union[str, Sequence[str]]
|
||||
"""Either a name or a list of option strings, e.g. 'foo' or '-f', '--foo'."""
|
||||
|
@ -111,7 +119,7 @@ class CommandArgGroup:
|
|||
|
||||
Fields on this class correspond to the arguments for
|
||||
[`ArgumentParser.add_argument_group()`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument_group)
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
title: Optional[str] = None
|
||||
"""
|
||||
|
@ -137,7 +145,7 @@ class CommandSubcommand:
|
|||
|
||||
Fields on this class correspond to the arguments for
|
||||
[`ArgumentParser.add_subparsers.add_parser()`](https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_subparsers)
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
title: Optional[str] = None
|
||||
"""
|
||||
|
@ -208,7 +216,7 @@ class CommandParserInput:
|
|||
formatter_class: Optional[Type["_FormatterClass"]] = None
|
||||
"""A class for customizing the help output"""
|
||||
prefix_chars: Optional[str] = None
|
||||
"""The set of characters that prefix optional arguments (default: ‘-‘)"""
|
||||
"""The set of characters that prefix optional arguments (default: `-`)"""
|
||||
fromfile_prefix_chars: Optional[str] = None
|
||||
"""The set of characters that prefix files from which additional arguments should be read (default: `None`)"""
|
||||
argument_default: Optional[Any] = None
|
||||
|
@ -234,7 +242,7 @@ class CommandParserInput:
|
|||
|
||||
@mark_extension_command_api
|
||||
class CommandHandler(Protocol):
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> None: ... # noqa: E704
|
||||
def __call__(self, *args: Any, **kwargs: Any) -> None: ...
|
||||
|
||||
|
||||
@mark_extension_command_api
|
||||
|
@ -367,7 +375,7 @@ def setup_parser_from_command(command: Type[ComponentCommand]) -> ArgumentParser
|
|||
# Recursively setup the parser and its subcommands
|
||||
def _setup_parser_from_command(
|
||||
parser: ArgumentParser,
|
||||
command: Union[Type[ComponentCommand], Type[ComponentCommand]],
|
||||
command: Type[ComponentCommand],
|
||||
) -> ArgumentParser:
|
||||
# Attach the command to the data returned by `parser.parse_args()`, so we know
|
||||
# which command was matched.
|
||||
|
@ -383,9 +391,9 @@ def _setup_parser_from_command(
|
|||
# NOTE: Seems that dataclass's `asdict()` calls `asdict()` also on the
|
||||
# nested dataclass fields. Thus we need to apply `_remove_none_values()`
|
||||
# to the nested dataclass fields.
|
||||
group_arg = _remove_none_values(group_arg)
|
||||
cleaned_group_arg = _remove_none_values(group_arg)
|
||||
|
||||
_setup_command_arg(arg_group, group_arg)
|
||||
_setup_command_arg(arg_group, cleaned_group_arg)
|
||||
else:
|
||||
_setup_command_arg(parser, arg.asdict())
|
||||
|
||||
|
@ -421,11 +429,7 @@ def _setup_command_arg(parser: Union[ArgumentParser, "_ArgumentGroup"], arg: dic
|
|||
|
||||
|
||||
def _remove_none_values(data: dict) -> dict:
|
||||
new_data = {}
|
||||
for key, val in data.items():
|
||||
if val is not None:
|
||||
new_data[key] = val
|
||||
return new_data
|
||||
return {key: val for key, val in data.items() if val is not None}
|
||||
|
||||
|
||||
def style_success(message: str) -> str:
|
||||
|
|
|
@ -20,8 +20,6 @@ else:
|
|||
class CopiedDict(dict):
|
||||
"""Dict subclass to identify dictionaries that have been copied with `snapshot_context`"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def snapshot_context(context: Context) -> Context:
|
||||
"""
|
||||
|
@ -151,7 +149,7 @@ def gen_context_processors_data(context: BaseContext, request: HttpRequest) -> D
|
|||
try:
|
||||
processors_data.update(data)
|
||||
except TypeError as e:
|
||||
raise TypeError(f"Context processor {processor.__qualname__} didn't return a " "dictionary.") from e
|
||||
raise TypeError(f"Context processor {processor.__qualname__} didn't return a dictionary.") from e
|
||||
|
||||
context_processors_data[request] = processors_data
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ def monkeypatch_template_init(template_cls: Type[Template]) -> None:
|
|||
|
||||
# NOTE: Function signature of Template.__init__ hasn't changed in 11 years, so we can safely patch it.
|
||||
# See https://github.com/django/django/blame/main/django/template/base.py#L139
|
||||
def __init__(
|
||||
def __init__( # noqa: N807
|
||||
self: Template,
|
||||
template_string: Any,
|
||||
origin: Optional[Origin] = None,
|
||||
|
@ -38,7 +38,7 @@ def monkeypatch_template_init(template_cls: Type[Template]) -> None:
|
|||
**kwargs: Any,
|
||||
) -> None:
|
||||
# NOTE: Avoids circular import
|
||||
from django_components.template import (
|
||||
from django_components.template import ( # noqa: PLC0415
|
||||
get_component_by_template_file,
|
||||
get_component_from_origin,
|
||||
set_component_to_origin,
|
||||
|
@ -70,7 +70,7 @@ def monkeypatch_template_init(template_cls: Type[Template]) -> None:
|
|||
content=template_string,
|
||||
origin=origin,
|
||||
name=name,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# Calling original `Template.__init__` should also compile the template into a Nodelist
|
||||
|
@ -82,7 +82,7 @@ def monkeypatch_template_init(template_cls: Type[Template]) -> None:
|
|||
OnTemplateCompiledContext(
|
||||
component_cls=component_cls,
|
||||
template=self,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
template_cls.__init__ = __init__
|
||||
|
@ -129,7 +129,7 @@ def monkeypatch_template_compile_nodelist(template_cls: Type[Template]) -> None:
|
|||
return nodelist
|
||||
except Exception as e:
|
||||
if self.engine.debug:
|
||||
e.template_debug = self.get_exception_info(e, e.token) # type: ignore
|
||||
e.template_debug = self.get_exception_info(e, e.token) # type: ignore[attr-defined]
|
||||
raise
|
||||
|
||||
template_cls.compile_nodelist = _compile_nodelist
|
||||
|
@ -162,7 +162,7 @@ def monkeypatch_template_render(template_cls: Type[Template]) -> None:
|
|||
|
||||
# NOTE: This implementation is based on Django v5.1.3)
|
||||
def _template_render(self: Template, context: Context, *args: Any, **kwargs: Any) -> str:
|
||||
"Display stage -- can be called many times"
|
||||
"""Display stage -- can be called many times"""
|
||||
# We parametrized `isolated_context`, which was `True` in the original method.
|
||||
if COMPONENT_IS_NESTED_KEY not in context:
|
||||
isolated_context = True
|
||||
|
@ -254,7 +254,7 @@ def monkeypatch_template_proxy_cls() -> None:
|
|||
# Patch TemplateProxy if template_partials is installed
|
||||
# See https://github.com/django-components/django-components/issues/1323#issuecomment-3164224042
|
||||
try:
|
||||
from template_partials.templatetags.partials import TemplateProxy
|
||||
from template_partials.templatetags.partials import TemplateProxy # noqa: PLC0415
|
||||
except ImportError:
|
||||
# template_partials is in INSTALLED_APPS but not actually installed
|
||||
# This is fine, just skip the patching
|
||||
|
@ -270,7 +270,7 @@ def monkeypatch_template_proxy_cls() -> None:
|
|||
def monkeypatch_template_proxy_render(template_proxy_cls: Type[Any]) -> None:
|
||||
# NOTE: TemplateProxy.render() is same logic as Template.render(), just duplicated.
|
||||
# So we can instead reuse Template.render()
|
||||
def _template_proxy_render(self: Any, context: Context, *args: Any, **kwargs: Any) -> str:
|
||||
def _template_proxy_render(self: Any, context: Context, *_args: Any, **_kwargs: Any) -> str:
|
||||
return Template.render(self, context)
|
||||
|
||||
template_proxy_cls.render = _template_proxy_render
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import glob
|
||||
import os
|
||||
from pathlib import Path, PurePosixPath, PureWindowsPath
|
||||
from typing import List, NamedTuple, Optional, Set, Union
|
||||
|
@ -41,6 +40,7 @@ def get_component_dirs(include_apps: bool = True) -> List[Path]:
|
|||
|
||||
- The paths in [`COMPONENTS.dirs`](../settings#django_components.app_settings.ComponentsSettings.dirs)
|
||||
must be absolute paths.
|
||||
|
||||
"""
|
||||
# Allow to configure from settings which dirs should be checked for components
|
||||
component_dirs = app_settings.DIRS
|
||||
|
@ -56,9 +56,7 @@ def get_component_dirs(include_apps: bool = True) -> List[Path]:
|
|||
is_component_dirs_set = raw_dirs_value is not None
|
||||
is_legacy_paths = (
|
||||
# Use value of `STATICFILES_DIRS` ONLY if `COMPONENT.dirs` not set
|
||||
not is_component_dirs_set
|
||||
and hasattr(settings, "STATICFILES_DIRS")
|
||||
and settings.STATICFILES_DIRS
|
||||
not is_component_dirs_set and getattr(settings, "STATICFILES_DIRS", None)
|
||||
)
|
||||
if is_legacy_paths:
|
||||
# NOTE: For STATICFILES_DIRS, we use the defaults even for empty list.
|
||||
|
@ -70,7 +68,7 @@ def get_component_dirs(include_apps: bool = True) -> List[Path]:
|
|||
|
||||
logger.debug(
|
||||
"get_component_dirs will search for valid dirs from following options:\n"
|
||||
+ "\n".join([f" - {str(d)}" for d in component_dirs])
|
||||
+ "\n".join([f" - {d!s}" for d in component_dirs]),
|
||||
)
|
||||
|
||||
# Add `[app]/[APP_DIR]` to the directories. This is, by default `[app]/components`
|
||||
|
@ -89,23 +87,22 @@ def get_component_dirs(include_apps: bool = True) -> List[Path]:
|
|||
# Consider tuples for STATICFILES_DIRS (See #489)
|
||||
# See https://docs.djangoproject.com/en/5.2/ref/settings/#prefixes-optional
|
||||
if isinstance(component_dir, (tuple, list)):
|
||||
component_dir = component_dir[1]
|
||||
component_dir = component_dir[1] # noqa: PLW2901
|
||||
try:
|
||||
Path(component_dir)
|
||||
except TypeError:
|
||||
logger.warning(
|
||||
f"{source} expected str, bytes or os.PathLike object, or tuple/list of length 2. "
|
||||
f"See Django documentation for STATICFILES_DIRS. Got {type(component_dir)} : {component_dir}"
|
||||
f"See Django documentation for STATICFILES_DIRS. Got {type(component_dir)} : {component_dir}",
|
||||
)
|
||||
continue
|
||||
|
||||
if not Path(component_dir).is_absolute():
|
||||
raise ValueError(f"{source} must contain absolute paths, got '{component_dir}'")
|
||||
else:
|
||||
directories.add(Path(component_dir).resolve())
|
||||
directories.add(Path(component_dir).resolve())
|
||||
|
||||
logger.debug(
|
||||
"get_component_dirs matched following template dirs:\n" + "\n".join([f" - {str(d)}" for d in directories])
|
||||
"get_component_dirs matched following template dirs:\n" + "\n".join([f" - {d!s}" for d in directories]),
|
||||
)
|
||||
return list(directories)
|
||||
|
||||
|
@ -143,6 +140,7 @@ def get_component_files(suffix: Optional[str] = None) -> List[ComponentFileEntry
|
|||
|
||||
modules = get_component_files(".py")
|
||||
```
|
||||
|
||||
"""
|
||||
search_glob = f"**/*{suffix}" if suffix else "**/*"
|
||||
|
||||
|
@ -150,10 +148,10 @@ def get_component_files(suffix: Optional[str] = None) -> List[ComponentFileEntry
|
|||
component_filepaths = _search_dirs(dirs, search_glob)
|
||||
|
||||
if hasattr(settings, "BASE_DIR") and settings.BASE_DIR:
|
||||
project_root = str(settings.BASE_DIR)
|
||||
project_root = 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__))
|
||||
project_root = Path(__name__).parent.resolve()
|
||||
|
||||
# NOTE: We handle dirs from `COMPONENTS.dirs` and from individual apps separately.
|
||||
modules: List[ComponentFileEntry] = []
|
||||
|
@ -212,6 +210,7 @@ def _filepath_to_python_module(
|
|||
- 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`
|
||||
|
||||
"""
|
||||
path_cls = PureWindowsPath if os.name == "nt" else PurePosixPath
|
||||
|
||||
|
@ -234,8 +233,7 @@ def _search_dirs(dirs: List[Path], search_glob: str) -> List[Path]:
|
|||
"""
|
||||
matched_files: List[Path] = []
|
||||
for directory in dirs:
|
||||
for path_str in glob.iglob(str(Path(directory) / search_glob), recursive=True):
|
||||
path = Path(path_str)
|
||||
for path in Path(directory).rglob(search_glob):
|
||||
# Skip any subdirectory or file (under the top-level directory) that starts with an underscore
|
||||
rel_dir_parts = list(path.relative_to(directory).parts)
|
||||
name_part = rel_dir_parts.pop()
|
||||
|
|
|
@ -11,7 +11,7 @@ actual_trace_level_num = -1
|
|||
def setup_logging() -> None:
|
||||
# Check if "TRACE" level was already defined. And if so, use its log level.
|
||||
# See https://docs.python.org/3/howto/logging.html#custom-levels
|
||||
global actual_trace_level_num
|
||||
global actual_trace_level_num # noqa: PLW0603
|
||||
log_levels = _get_log_levels()
|
||||
|
||||
if "TRACE" in log_levels:
|
||||
|
@ -25,8 +25,7 @@ def _get_log_levels() -> Dict[str, int]:
|
|||
# Use official API if possible
|
||||
if sys.version_info >= (3, 11):
|
||||
return logging.getLevelNamesMapping()
|
||||
else:
|
||||
return logging._nameToLevel.copy()
|
||||
return logging._nameToLevel.copy()
|
||||
|
||||
|
||||
def trace(message: str, *args: Any, **kwargs: Any) -> None:
|
||||
|
@ -54,6 +53,7 @@ def trace(message: str, *args: Any, **kwargs: Any) -> None:
|
|||
},
|
||||
}
|
||||
```
|
||||
|
||||
"""
|
||||
if actual_trace_level_num == -1:
|
||||
setup_logging()
|
||||
|
@ -96,26 +96,10 @@ def trace_component_msg(
|
|||
|
||||
`"RENDER_SLOT COMPONENT 'component_name' SLOT: 'slot_name' FILLS: 'fill_name' PATH: Root > Child > Grandchild "`
|
||||
"""
|
||||
|
||||
if component_id:
|
||||
component_id_str = f"ID {component_id}"
|
||||
else:
|
||||
component_id_str = ""
|
||||
|
||||
if slot_name:
|
||||
slot_name_str = f"SLOT: '{slot_name}'"
|
||||
else:
|
||||
slot_name_str = ""
|
||||
|
||||
if component_path:
|
||||
component_path_str = "PATH: " + " > ".join(component_path)
|
||||
else:
|
||||
component_path_str = ""
|
||||
|
||||
if slot_fills:
|
||||
slot_fills_str = "FILLS: " + ", ".join(slot_fills.keys())
|
||||
else:
|
||||
slot_fills_str = ""
|
||||
component_id_str = f"ID {component_id}" if component_id else ""
|
||||
slot_name_str = f"SLOT: '{slot_name}'" if slot_name else ""
|
||||
component_path_str = "PATH: " + " > ".join(component_path) if component_path else ""
|
||||
slot_fills_str = "FILLS: " + ", ".join(slot_fills.keys()) if slot_fills else ""
|
||||
|
||||
full_msg = f"{action} COMPONENT: '{component_name}' {component_id_str} {slot_name_str} {slot_fills_str} {component_path_str} {extra}" # noqa: E501
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ from hashlib import md5
|
|||
from importlib import import_module
|
||||
from itertools import chain
|
||||
from types import ModuleType
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Tuple, Type, TypeVar, Union, cast
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type, TypeVar, Union, cast
|
||||
from urllib import parse
|
||||
|
||||
from django_components.constants import UID_LENGTH
|
||||
|
@ -43,9 +43,7 @@ def snake_to_pascal(name: str) -> str:
|
|||
def is_identifier(value: Any) -> bool:
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
if not value.isidentifier():
|
||||
return False
|
||||
return True
|
||||
return value.isidentifier()
|
||||
|
||||
|
||||
def any_regex_match(string: str, patterns: List[re.Pattern]) -> bool:
|
||||
|
@ -58,9 +56,7 @@ def no_regex_match(string: str, patterns: List[re.Pattern]) -> bool:
|
|||
|
||||
# See https://stackoverflow.com/a/2020083/9788634
|
||||
def get_import_path(cls_or_fn: Type[Any]) -> str:
|
||||
"""
|
||||
Get the full import path for a class or a function, e.g. `"path.to.MyClass"`
|
||||
"""
|
||||
"""Get the full import path for a class or a function, e.g. `"path.to.MyClass"`"""
|
||||
module = cls_or_fn.__module__
|
||||
if module == "builtins":
|
||||
return cls_or_fn.__qualname__ # avoid outputs like 'builtins.str'
|
||||
|
@ -79,7 +75,7 @@ def get_module_info(
|
|||
else:
|
||||
try:
|
||||
module = import_module(module_name)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001
|
||||
module = None
|
||||
else:
|
||||
module = None
|
||||
|
@ -96,9 +92,9 @@ def default(val: Optional[T], default: Union[U, Callable[[], U], Type[T]], facto
|
|||
if val is not None:
|
||||
return val
|
||||
if factory:
|
||||
default_func = cast(Callable[[], U], default)
|
||||
default_func = cast("Callable[[], U]", default)
|
||||
return default_func()
|
||||
return cast(U, default)
|
||||
return cast("U", default)
|
||||
|
||||
|
||||
def get_index(lst: List, key: Callable[[Any], bool]) -> Optional[int]:
|
||||
|
@ -124,7 +120,7 @@ def is_nonempty_str(txt: Optional[str]) -> bool:
|
|||
# Convert Component class to something like `TableComp_a91d03`
|
||||
def hash_comp_cls(comp_cls: Type["Component"]) -> str:
|
||||
full_name = get_import_path(comp_cls)
|
||||
name_hash = md5(full_name.encode()).hexdigest()[0:6]
|
||||
name_hash = md5(full_name.encode()).hexdigest()[0:6] # noqa: S324
|
||||
return comp_cls.__name__ + "_" + name_hash
|
||||
|
||||
|
||||
|
@ -148,9 +144,9 @@ def to_dict(data: Any) -> dict:
|
|||
"""
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
elif hasattr(data, "_asdict"): # Case: NamedTuple
|
||||
if hasattr(data, "_asdict"): # Case: NamedTuple
|
||||
return data._asdict()
|
||||
elif is_dataclass(data): # Case: dataclass
|
||||
if is_dataclass(data): # Case: dataclass
|
||||
return asdict(data) # type: ignore[arg-type]
|
||||
|
||||
return dict(data)
|
||||
|
@ -176,7 +172,11 @@ def format_url(url: str, query: Optional[Dict] = None, fragment: Optional[str] =
|
|||
return parse.urlunsplit(parts._replace(query=encoded_qs, fragment=fragment_enc))
|
||||
|
||||
|
||||
def format_as_ascii_table(data: List[Dict[str, Any]], headers: List[str], include_headers: bool = True) -> str:
|
||||
def format_as_ascii_table(
|
||||
data: List[Dict[str, Any]],
|
||||
headers: Union[List[str], Tuple[str, ...], Set[str]],
|
||||
include_headers: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Format a list of dictionaries as an ASCII table.
|
||||
|
||||
|
@ -201,6 +201,7 @@ def format_as_ascii_table(data: List[Dict[str, Any]], headers: List[str], includ
|
|||
ProjectDashboard project.components.dashboard.ProjectDashboard ./project/components/dashboard
|
||||
ProjectDashboardAction project.components.dashboard_action.ProjectDashboardAction ./project/components/dashboard_action
|
||||
```
|
||||
|
||||
""" # noqa: E501
|
||||
# Calculate the width of each column
|
||||
column_widths = {header: len(header) for header in headers}
|
||||
|
@ -221,8 +222,5 @@ def format_as_ascii_table(data: List[Dict[str, Any]], headers: List[str], includ
|
|||
data_rows.append(data_row)
|
||||
|
||||
# Combine all parts into the final table
|
||||
if include_headers:
|
||||
table = "\n".join([header_row, separator] + data_rows)
|
||||
else:
|
||||
table = "\n".join(data_rows)
|
||||
table = "\n".join([header_row, separator, *data_rows]) if include_headers else "\n".join(data_rows)
|
||||
return table
|
||||
|
|
|
@ -13,17 +13,16 @@ def generate(alphabet: str, size: int) -> str:
|
|||
mask = 1
|
||||
if alphabet_len > 1:
|
||||
mask = (2 << int(log(alphabet_len - 1) / log(2))) - 1
|
||||
step = int(ceil(1.6 * mask * size / alphabet_len))
|
||||
step = int(ceil(1.6 * mask * size / alphabet_len)) # noqa: RUF046
|
||||
|
||||
id = ""
|
||||
id_str = ""
|
||||
while True:
|
||||
random_bytes = bytearray(urandom(step))
|
||||
|
||||
for i in range(step):
|
||||
random_byte = random_bytes[i] & mask
|
||||
if random_byte < alphabet_len:
|
||||
if alphabet[random_byte]:
|
||||
id += alphabet[random_byte]
|
||||
if random_byte < alphabet_len and alphabet[random_byte]:
|
||||
id_str += alphabet[random_byte]
|
||||
|
||||
if len(id) == size:
|
||||
return id
|
||||
if len(id_str) == size:
|
||||
return id_str
|
||||
|
|
|
@ -15,7 +15,7 @@ def mark_extension_url_api(obj: TClass) -> TClass:
|
|||
class URLRouteHandler(Protocol):
|
||||
"""Framework-agnostic 'view' function for routes"""
|
||||
|
||||
def __call__(self, request: Any, *args: Any, **kwargs: Any) -> Any: ... # noqa: E704
|
||||
def __call__(self, request: Any, *args: Any, **kwargs: Any) -> Any: ...
|
||||
|
||||
|
||||
@mark_extension_url_api
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# ruff: noqa: S105
|
||||
"""
|
||||
Parser for Django template tags.
|
||||
|
||||
|
@ -180,7 +181,7 @@ class TagValuePart:
|
|||
# Create a hash based on the attributes that define object equality
|
||||
return hash((self.value, self.quoted, self.spread, self.translation, self.filter))
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, TagValuePart):
|
||||
return False
|
||||
return (
|
||||
|
@ -231,16 +232,15 @@ class TagValueStruct:
|
|||
def render_value(value: Union[TagValue, TagValueStruct]) -> str:
|
||||
if isinstance(value, TagValue):
|
||||
return value.serialize()
|
||||
else:
|
||||
return value.serialize()
|
||||
return value.serialize()
|
||||
|
||||
if self.type == "simple":
|
||||
value = self.entries[0]
|
||||
return render_value(value)
|
||||
elif self.type == "list":
|
||||
if self.type == "list":
|
||||
prefix = self.spread or ""
|
||||
return prefix + "[" + ", ".join([render_value(entry) for entry in self.entries]) + "]"
|
||||
elif self.type == "dict":
|
||||
if self.type == "dict":
|
||||
prefix = self.spread or ""
|
||||
dict_pairs = []
|
||||
dict_pair: List[str] = []
|
||||
|
@ -255,18 +255,19 @@ class TagValueStruct:
|
|||
dict_pairs.append(rendered)
|
||||
else:
|
||||
dict_pair.append(rendered)
|
||||
elif entry.is_spread:
|
||||
if dict_pair:
|
||||
raise TemplateSyntaxError("Malformed dict: spread operator cannot be used as a dict key")
|
||||
dict_pairs.append(rendered)
|
||||
else:
|
||||
if entry.is_spread:
|
||||
if dict_pair:
|
||||
raise TemplateSyntaxError("Malformed dict: spread operator cannot be used as a dict key")
|
||||
dict_pairs.append(rendered)
|
||||
else:
|
||||
dict_pair.append(rendered)
|
||||
dict_pair.append(rendered)
|
||||
if len(dict_pair) == 2:
|
||||
dict_pairs.append(": ".join(dict_pair))
|
||||
dict_pair = []
|
||||
return prefix + "{" + ", ".join(dict_pairs) + "}"
|
||||
|
||||
raise ValueError(f"Invalid type: {self.type}")
|
||||
|
||||
# When we want to render the TagValueStruct, which may contain nested lists and dicts,
|
||||
# we need to find all leaf nodes (the "simple" types) and compile them to FilterExpression.
|
||||
#
|
||||
|
@ -308,7 +309,7 @@ class TagValueStruct:
|
|||
raise TemplateSyntaxError("Malformed tag: simple value is not a TagValue")
|
||||
return value.resolve(context)
|
||||
|
||||
elif self.type == "list":
|
||||
if self.type == "list":
|
||||
resolved_list: List[Any] = []
|
||||
for entry in self.entries:
|
||||
resolved = entry.resolve(context)
|
||||
|
@ -325,7 +326,7 @@ class TagValueStruct:
|
|||
resolved_list.append(resolved)
|
||||
return resolved_list
|
||||
|
||||
elif self.type == "dict":
|
||||
if self.type == "dict":
|
||||
resolved_dict: Dict = {}
|
||||
dict_pair: List = []
|
||||
|
||||
|
@ -336,14 +337,14 @@ class TagValueStruct:
|
|||
if isinstance(entry, TagValueStruct) and entry.spread:
|
||||
if dict_pair:
|
||||
raise TemplateSyntaxError(
|
||||
"Malformed dict: spread operator cannot be used on the position of a dict value"
|
||||
"Malformed dict: spread operator cannot be used on the position of a dict value",
|
||||
)
|
||||
# Case: Spreading a literal dict: { **{"key": val2} }
|
||||
resolved_dict.update(resolved)
|
||||
elif isinstance(entry, TagValue) and entry.is_spread:
|
||||
if dict_pair:
|
||||
raise TemplateSyntaxError(
|
||||
"Malformed dict: spread operator cannot be used on the position of a dict value"
|
||||
"Malformed dict: spread operator cannot be used on the position of a dict value",
|
||||
)
|
||||
# Case: Spreading a variable: { **val }
|
||||
resolved_dict.update(resolved)
|
||||
|
@ -358,6 +359,8 @@ class TagValueStruct:
|
|||
dict_pair = []
|
||||
return resolved_dict
|
||||
|
||||
raise ValueError(f"Invalid type: {self.type}")
|
||||
|
||||
|
||||
def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
||||
"""
|
||||
|
@ -447,7 +450,7 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
return False
|
||||
|
||||
def taken_n(n: int) -> str:
|
||||
result = text[index : index + n] # noqa: E203
|
||||
result = text[index : index + n]
|
||||
add_token(result)
|
||||
return result
|
||||
|
||||
|
@ -506,7 +509,7 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
if is_next_token(["..."]):
|
||||
if curr_struct.type != "simple":
|
||||
raise TemplateSyntaxError(
|
||||
f"Spread syntax '...' found in {curr_struct.type}. It must be used on tag attributes only"
|
||||
f"Spread syntax '...' found in {curr_struct.type}. It must be used on tag attributes only",
|
||||
)
|
||||
spread_token = "..."
|
||||
elif is_next_token(["**"]):
|
||||
|
@ -529,7 +532,7 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
if curr_struct.type == "simple" and key is not None:
|
||||
raise TemplateSyntaxError("Spread syntax '...' cannot follow a key ('key=...attrs')")
|
||||
|
||||
taken_n(len(cast(str, spread_token))) # ... or * or **
|
||||
taken_n(len(cast("str", spread_token))) # ... or * or **
|
||||
# Allow whitespace between spread and the variable, but only for the Python-like syntax
|
||||
# (lists and dicts). E.g.:
|
||||
# `{% component key=[ * spread ] %}` or `{% component key={ ** spread } %}`
|
||||
|
@ -539,9 +542,8 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
# `{% component key=val ...spread key2=val2 %}`
|
||||
if spread_token != "...":
|
||||
take_while(TAG_WHITESPACE)
|
||||
else:
|
||||
if is_next_token(TAG_WHITESPACE) or is_at_end():
|
||||
raise TemplateSyntaxError("Spread syntax '...' is missing a value")
|
||||
elif is_next_token(TAG_WHITESPACE) or is_at_end():
|
||||
raise TemplateSyntaxError("Spread syntax '...' is missing a value")
|
||||
return spread_token
|
||||
|
||||
# Parse attributes
|
||||
|
@ -586,9 +588,8 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
# Manage state with regards to lists and dictionaries
|
||||
if is_next_token(["[", "...[", "*[", "**["]):
|
||||
spread_token = extract_spread_token(curr_value, None)
|
||||
if spread_token is not None:
|
||||
if curr_value.type == "simple" and key is not None:
|
||||
raise TemplateSyntaxError("Spread syntax '...' cannot follow a key ('key=...attrs')")
|
||||
if spread_token is not None and curr_value.type == "simple" and key is not None:
|
||||
raise TemplateSyntaxError("Spread syntax '...' cannot follow a key ('key=...attrs')")
|
||||
# NOTE: The `...`, `**`, `*` are "taken" in `extract_spread_token()`
|
||||
taken_n(1) # [
|
||||
struct = TagValueStruct(type="list", entries=[], spread=spread_token, meta={}, parser=parser)
|
||||
|
@ -596,7 +597,7 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
stack.append(struct)
|
||||
continue
|
||||
|
||||
elif is_next_token(["]"]):
|
||||
if is_next_token(["]"]):
|
||||
if curr_value.type != "list":
|
||||
raise TemplateSyntaxError("Unexpected closing bracket")
|
||||
taken_n(1) # ]
|
||||
|
@ -606,11 +607,10 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
stack.pop()
|
||||
continue
|
||||
|
||||
elif is_next_token(["{", "...{", "*{", "**{"]):
|
||||
if is_next_token(["{", "...{", "*{", "**{"]):
|
||||
spread_token = extract_spread_token(curr_value, None)
|
||||
if spread_token is not None:
|
||||
if curr_value.type == "simple" and key is not None:
|
||||
raise TemplateSyntaxError("Spread syntax '...' cannot follow a key ('key=...attrs')")
|
||||
if spread_token is not None and curr_value.type == "simple" and key is not None:
|
||||
raise TemplateSyntaxError("Spread syntax '...' cannot follow a key ('key=...attrs')")
|
||||
# NOTE: The `...`, `**`, `*` are "taken" in `extract_spread_token()`
|
||||
taken_n(1) # {
|
||||
|
||||
|
@ -630,7 +630,7 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
stack.append(struct)
|
||||
continue
|
||||
|
||||
elif is_next_token(["}"]):
|
||||
if is_next_token(["}"]):
|
||||
if curr_value.type != "dict":
|
||||
raise TemplateSyntaxError("Unexpected closing bracket")
|
||||
|
||||
|
@ -643,37 +643,33 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
# Case: `{ "key": **{"key2": val2} }`
|
||||
if dict_pair:
|
||||
raise TemplateSyntaxError(
|
||||
"Spread syntax cannot be used in place of a dictionary value"
|
||||
"Spread syntax cannot be used in place of a dictionary value",
|
||||
)
|
||||
# Case: `{ **{"key": val2} }`
|
||||
continue
|
||||
else:
|
||||
# Case: `{ {"key": val2}: value }`
|
||||
if not dict_pair:
|
||||
val_type = "Dictionary" if curr_value.type == "dict" else "List"
|
||||
raise TemplateSyntaxError(f"{val_type} cannot be used as a dictionary key")
|
||||
# Case: `{ "key": {"key2": val2} }`
|
||||
else:
|
||||
pass
|
||||
# Case: `{ {"key": val2}: value }`
|
||||
if not dict_pair:
|
||||
val_type = "Dictionary" if curr_value.type == "dict" else "List"
|
||||
raise TemplateSyntaxError(f"{val_type} cannot be used as a dictionary key")
|
||||
# Case: `{ "key": {"key2": val2} }`
|
||||
dict_pair.append(entry)
|
||||
if len(dict_pair) == 2:
|
||||
dict_pair = []
|
||||
# Spread is fine when on its own, but cannot be used after a dict key
|
||||
elif entry.is_spread:
|
||||
# Case: `{ "key": **my_attrs }`
|
||||
if dict_pair:
|
||||
raise TemplateSyntaxError(
|
||||
"Spread syntax cannot be used in place of a dictionary value",
|
||||
)
|
||||
# Case: `{ **my_attrs }`
|
||||
continue
|
||||
# Non-spread value can be both key and value.
|
||||
else:
|
||||
# Spread is fine when on its own, but cannot be used after a dict key
|
||||
if entry.is_spread:
|
||||
# Case: `{ "key": **my_attrs }`
|
||||
if dict_pair:
|
||||
raise TemplateSyntaxError(
|
||||
"Spread syntax cannot be used in place of a dictionary value"
|
||||
)
|
||||
# Case: `{ **my_attrs }`
|
||||
continue
|
||||
# Non-spread value can be both key and value.
|
||||
else:
|
||||
# Cases: `{ my_attrs: "value" }` or `{ "key": my_attrs }`
|
||||
dict_pair.append(entry)
|
||||
if len(dict_pair) == 2:
|
||||
dict_pair = []
|
||||
# Cases: `{ my_attrs: "value" }` or `{ "key": my_attrs }`
|
||||
dict_pair.append(entry)
|
||||
if len(dict_pair) == 2:
|
||||
dict_pair = []
|
||||
# If, at the end, there an unmatched key-value pair, raise an error
|
||||
if dict_pair:
|
||||
raise TemplateSyntaxError("Dictionary key is missing a value")
|
||||
|
@ -687,7 +683,7 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
stack.pop()
|
||||
continue
|
||||
|
||||
elif is_next_token([","]):
|
||||
if is_next_token([","]):
|
||||
if curr_value.type not in ("list", "dict"):
|
||||
raise TemplateSyntaxError("Unexpected comma")
|
||||
taken_n(1) # ,
|
||||
|
@ -698,7 +694,7 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
# NOTE: Altho `:` is used also in filter syntax, the "value" part
|
||||
# that the filter is part of is parsed as a whole block. So if we got
|
||||
# here, we know we're NOT in filter.
|
||||
elif is_next_token([":"]):
|
||||
if is_next_token([":"]):
|
||||
if curr_value.type != "dict":
|
||||
raise TemplateSyntaxError("Unexpected colon")
|
||||
if not curr_value.meta["expects_key"]:
|
||||
|
@ -707,13 +703,11 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
curr_value.meta["expects_key"] = False
|
||||
continue
|
||||
|
||||
else:
|
||||
# Allow only 1 top-level plain value, similar to JSON
|
||||
if curr_value.type == "simple":
|
||||
stack.pop()
|
||||
else:
|
||||
if is_at_end():
|
||||
raise TemplateSyntaxError("Unexpected end of text")
|
||||
# Allow only 1 top-level plain value, similar to JSON
|
||||
if curr_value.type == "simple":
|
||||
stack.pop()
|
||||
elif is_at_end():
|
||||
raise TemplateSyntaxError("Unexpected end of text")
|
||||
|
||||
# Once we got here, we know that next token is NOT a list nor dict.
|
||||
# So we can now parse the value.
|
||||
|
@ -731,9 +725,8 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
if is_at_end():
|
||||
if is_first_part:
|
||||
raise TemplateSyntaxError("Unexpected end of text")
|
||||
else:
|
||||
end_of_value = True
|
||||
continue
|
||||
end_of_value = True
|
||||
continue
|
||||
|
||||
# In this case we've reached the end of a filter sequence
|
||||
# e.g. image: `height="20"|lower key1=value1`
|
||||
|
@ -744,7 +737,7 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
continue
|
||||
|
||||
# Catch cases like `|filter` or `:arg`, which should be `var|filter` or `filter:arg`
|
||||
elif is_first_part and is_next_token(TAG_FILTER):
|
||||
if is_first_part and is_next_token(TAG_FILTER):
|
||||
raise TemplateSyntaxError("Filter is missing a value")
|
||||
|
||||
# Get past the filter tokens like `|` or `:`, until the next value part.
|
||||
|
@ -800,7 +793,7 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
elif curr_value.type == "list":
|
||||
terminal_tokens = (",", "]")
|
||||
else:
|
||||
terminal_tokens = tuple()
|
||||
terminal_tokens = ()
|
||||
|
||||
# Parse the value
|
||||
#
|
||||
|
@ -860,7 +853,7 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
spread=spread_token,
|
||||
translation=is_translation,
|
||||
filter=filter_token,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
# Here we're done with the value (+ a sequence of filters)
|
||||
|
@ -878,19 +871,18 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
# Validation for `{"key": **spread }`
|
||||
if not curr_value.meta["expects_key"]:
|
||||
raise TemplateSyntaxError(
|
||||
"Got spread syntax on the position of a value inside a dictionary key-value pair"
|
||||
"Got spread syntax on the position of a value inside a dictionary key-value pair",
|
||||
)
|
||||
|
||||
# Validation for `{**spread: value }`
|
||||
take_while(TAG_WHITESPACE)
|
||||
if is_next_token([":"]):
|
||||
raise TemplateSyntaxError("Spread syntax cannot be used in place of a dictionary key")
|
||||
else:
|
||||
# Validation for `{"key", value }`
|
||||
if curr_value.meta["expects_key"]:
|
||||
take_while(TAG_WHITESPACE)
|
||||
if not is_next_token([":"]):
|
||||
raise TemplateSyntaxError("Dictionary key is missing a value")
|
||||
# Validation for `{"key", value }`
|
||||
elif curr_value.meta["expects_key"]:
|
||||
take_while(TAG_WHITESPACE)
|
||||
if not is_next_token([":"]):
|
||||
raise TemplateSyntaxError("Dictionary key is missing a value")
|
||||
|
||||
# And at this point, we have the full representation of the tag value,
|
||||
# including any lists or dictionaries (even nested). E.g.
|
||||
|
@ -916,7 +908,7 @@ def parse_tag(text: str, parser: Optional[Parser]) -> Tuple[str, List[TagAttr]]:
|
|||
key=key,
|
||||
start_index=start_index,
|
||||
value=total_value,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
return normalized, attrs
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
"""
|
||||
r"""
|
||||
Parser for Django template.
|
||||
|
||||
The parser reads a template file (usually HTML, but not necessarily), which may contain
|
||||
|
@ -83,8 +83,7 @@ def parse_template(text: str) -> List[Token]:
|
|||
if token.token_type == TokenType.BLOCK and ("'" in token.contents or '"' in token.contents):
|
||||
broken_token = token
|
||||
break
|
||||
else:
|
||||
resolved_tokens.append(token)
|
||||
resolved_tokens.append(token)
|
||||
|
||||
# If we found a broken token, we switch to our slow parser
|
||||
if broken_token is not None:
|
||||
|
@ -110,8 +109,8 @@ def _detailed_tag_parser(text: str, lineno: int, start_index: int) -> Token:
|
|||
result_content: List[str] = []
|
||||
|
||||
# Pre-compute common substrings
|
||||
QUOTE_CHARS = ("'", '"')
|
||||
QUOTE_OR_PERCENT = (*QUOTE_CHARS, "%")
|
||||
QUOTE_CHARS = ("'", '"') # noqa: N806
|
||||
QUOTE_OR_PERCENT = (*QUOTE_CHARS, "%") # noqa: N806
|
||||
|
||||
def take_char() -> str:
|
||||
nonlocal index
|
||||
|
@ -192,11 +191,10 @@ def _detailed_tag_parser(text: str, lineno: int, start_index: int) -> Token:
|
|||
take_char() # %
|
||||
take_char() # }
|
||||
break
|
||||
else:
|
||||
# False alarm, just a string
|
||||
content = take_until_any(QUOTE_CHARS)
|
||||
result_content.append(content)
|
||||
continue
|
||||
# False alarm, just a string
|
||||
content = take_until_any(QUOTE_CHARS)
|
||||
result_content.append(content)
|
||||
continue
|
||||
|
||||
# Take regular content until we hit a quote or potential closing tag
|
||||
content = take_until_any(QUOTE_OR_PERCENT)
|
||||
|
|
|
@ -41,7 +41,8 @@ def validate_params(
|
|||
args, kwargs = _validate_params_with_signature(validation_signature, params, extra_kwargs)
|
||||
return args, kwargs
|
||||
except TypeError as e:
|
||||
raise TypeError(f"Invalid parameters for tag '{tag}': {str(e)}") from None
|
||||
err_msg = str(e)
|
||||
raise TypeError(f"Invalid parameters for tag '{tag}': {err_msg}") from None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -88,7 +89,7 @@ def resolve_params(
|
|||
resolved_params.append(TagParam(key=None, value=value))
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Cannot spread non-iterable value: '{param.value.serialize()}' resolved to {resolved}"
|
||||
f"Cannot spread non-iterable value: '{param.value.serialize()}' resolved to {resolved}",
|
||||
)
|
||||
else:
|
||||
resolved_params.append(TagParam(key=param.key, value=resolved))
|
||||
|
@ -110,7 +111,7 @@ class ParsedTag(NamedTuple):
|
|||
def parse_template_tag(
|
||||
tag: str,
|
||||
end_tag: Optional[str],
|
||||
allowed_flags: Optional[List[str]],
|
||||
allowed_flags: Optional[Iterable[str]],
|
||||
parser: Parser,
|
||||
token: Token,
|
||||
) -> ParsedTag:
|
||||
|
@ -138,7 +139,8 @@ def parse_template_tag(
|
|||
else:
|
||||
is_inline = not end_tag
|
||||
|
||||
raw_params, flags = _extract_flags(tag_name, attrs, allowed_flags or [])
|
||||
allowed_flags_set = set(allowed_flags) if allowed_flags else set()
|
||||
raw_params, flags = _extract_flags(tag_name, attrs, allowed_flags_set)
|
||||
|
||||
def _parse_tag_body(parser: Parser, end_tag: str, inline: bool) -> Tuple[NodeList, Optional[str]]:
|
||||
if inline:
|
||||
|
@ -188,8 +190,7 @@ def _extract_contents_until(parser: Parser, until_blocks: List[str]) -> str:
|
|||
contents.append("{% " + token.contents + " %}")
|
||||
if command in until_blocks:
|
||||
return "".join(contents)
|
||||
else:
|
||||
contents.append("{% " + token.contents + " %}")
|
||||
contents.append("{% " + token.contents + " %}")
|
||||
elif token_type == 3: # TokenType.COMMENT
|
||||
contents.append("{# " + token.contents + " #}")
|
||||
else:
|
||||
|
@ -205,7 +206,9 @@ def _extract_contents_until(parser: Parser, until_blocks: List[str]) -> str:
|
|||
|
||||
|
||||
def _extract_flags(
|
||||
tag_name: str, attrs: List[TagAttr], allowed_flags: List[str]
|
||||
tag_name: str,
|
||||
attrs: List[TagAttr],
|
||||
allowed_flags: Set[str],
|
||||
) -> Tuple[List[TagAttr], Dict[str, bool]]:
|
||||
found_flags = set()
|
||||
remaining_attrs = []
|
||||
|
@ -386,13 +389,12 @@ def _validate_params_with_signature(
|
|||
if signature_param.kind in (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD):
|
||||
if signature_param.default == inspect.Parameter.empty:
|
||||
raise TypeError(f"missing a required argument: '{param_name}'")
|
||||
elif len(validated_args) <= next_positional_index:
|
||||
if len(validated_args) <= next_positional_index:
|
||||
validated_kwargs[param_name] = signature_param.default
|
||||
elif signature_param.kind == inspect.Parameter.KEYWORD_ONLY:
|
||||
if signature_param.default == inspect.Parameter.empty:
|
||||
raise TypeError(f"missing a required argument: '{param_name}'")
|
||||
else:
|
||||
validated_kwargs[param_name] = signature_param.default
|
||||
validated_kwargs[param_name] = signature_param.default
|
||||
|
||||
# Return args and kwargs
|
||||
return validated_args, validated_kwargs
|
||||
|
@ -492,13 +494,12 @@ def _validate_params_with_code(
|
|||
if i < positional_count: # Positional parameter
|
||||
if i < required_positional:
|
||||
raise TypeError(f"missing a required argument: '{param_name}'")
|
||||
elif len(validated_args) <= i:
|
||||
if len(validated_args) <= i:
|
||||
default_index = i - required_positional
|
||||
validated_kwargs[param_name] = defaults[default_index]
|
||||
elif i < positional_count + kwonly_count: # Keyword-only parameter
|
||||
if param_name not in kwdefaults:
|
||||
raise TypeError(f"missing a required argument: '{param_name}'")
|
||||
else:
|
||||
validated_kwargs[param_name] = kwdefaults[param_name]
|
||||
validated_kwargs[param_name] = kwdefaults[param_name]
|
||||
|
||||
return tuple(validated_args), validated_kwargs
|
||||
|
|
|
@ -2,7 +2,7 @@ import gc
|
|||
import inspect
|
||||
import sys
|
||||
from functools import wraps
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Type, Union
|
||||
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Optional, Sequence, Set, Tuple, Type, Union
|
||||
from unittest.mock import patch
|
||||
from weakref import ReferenceType
|
||||
|
||||
|
@ -14,12 +14,14 @@ from django.template.loaders.base import Loader
|
|||
from django.test import override_settings
|
||||
|
||||
from django_components.component import ALL_COMPONENTS, Component, component_node_subclasses_by_name
|
||||
from django_components.component_media import ComponentMedia
|
||||
from django_components.component_registry import ALL_REGISTRIES, ComponentRegistry
|
||||
from django_components.extension import extensions
|
||||
from django_components.perfutil.provide import provide_cache
|
||||
from django_components.template import _reset_component_template_file_cache, loading_components
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component_media import ComponentMedia
|
||||
|
||||
# NOTE: `ReferenceType` is NOT a generic pre-3.9
|
||||
if sys.version_info >= (3, 9):
|
||||
RegistryRef = ReferenceType[ComponentRegistry]
|
||||
|
@ -50,7 +52,7 @@ class GenIdPatcher:
|
|||
# Random number so that the generated IDs are "hex-looking", e.g. a1bc3d
|
||||
self._gen_id_count = 10599485
|
||||
|
||||
def mock_gen_id(*args: Any, **kwargs: Any) -> str:
|
||||
def mock_gen_id(*_args: Any, **_kwargs: Any) -> str:
|
||||
self._gen_id_count += 1
|
||||
return hex(self._gen_id_count)[2:]
|
||||
|
||||
|
@ -67,7 +69,7 @@ class GenIdPatcher:
|
|||
|
||||
class CsrfTokenPatcher:
|
||||
def __init__(self) -> None:
|
||||
self._csrf_token = "predictabletoken"
|
||||
self._csrf_token = "predictabletoken" # noqa: S105
|
||||
self._csrf_token_patch: Any = None
|
||||
|
||||
def start(self) -> None:
|
||||
|
@ -200,8 +202,7 @@ def djc_test(
|
|||
- `(param_names, param_values)` or
|
||||
- `(param_names, param_values, ids)`
|
||||
|
||||
Example:
|
||||
|
||||
Example:
|
||||
```py
|
||||
from django_components.testing import djc_test
|
||||
|
||||
|
@ -267,6 +268,7 @@ def djc_test(
|
|||
3. The parametrized `components_settings` override the fields on the `components_settings` kwarg.
|
||||
|
||||
Priority: `components_settings` (parametrized) > `components_settings` > `django_settings["COMPONENTS"]` > `django.conf.settings.COMPONENTS`
|
||||
|
||||
""" # noqa: E501
|
||||
|
||||
def decorator(func: Callable) -> Callable:
|
||||
|
@ -327,13 +329,13 @@ def djc_test(
|
|||
# Make a copy of `ALL_COMPONENTS` and `ALL_REGISTRIES` as they were before the test.
|
||||
# Since the tests require Django to be configured, this should contain any
|
||||
# components that were registered with autodiscovery / at `AppConfig.ready()`.
|
||||
_ALL_COMPONENTS = ALL_COMPONENTS.copy()
|
||||
_ALL_REGISTRIES_COPIES: RegistriesCopies = []
|
||||
_all_components = ALL_COMPONENTS.copy()
|
||||
_all_registries_copies: RegistriesCopies = []
|
||||
for reg_ref in ALL_REGISTRIES:
|
||||
reg = reg_ref()
|
||||
if not reg:
|
||||
continue
|
||||
_ALL_REGISTRIES_COPIES.append((reg_ref, list(reg._registry.keys())))
|
||||
_all_registries_copies.append((reg_ref, list(reg._registry.keys())))
|
||||
|
||||
# Prepare global state
|
||||
_setup_djc_global_state(gen_id_patcher, csrf_token_patcher)
|
||||
|
@ -342,8 +344,8 @@ def djc_test(
|
|||
_clear_djc_global_state(
|
||||
gen_id_patcher,
|
||||
csrf_token_patcher,
|
||||
_ALL_COMPONENTS, # type: ignore[arg-type]
|
||||
_ALL_REGISTRIES_COPIES,
|
||||
_all_components, # type: ignore[arg-type]
|
||||
_all_registries_copies,
|
||||
gc_collect,
|
||||
)
|
||||
|
||||
|
@ -388,7 +390,7 @@ def djc_test(
|
|||
|
||||
# NOTE: Lazily import pytest, so user can still run tests with plain `unittest`
|
||||
# if they choose not to use parametrization.
|
||||
import pytest
|
||||
import pytest # noqa: PLC0415
|
||||
|
||||
wrapper = pytest.mark.parametrize(param_names, values, ids=ids)(wrapper)
|
||||
|
||||
|
@ -428,14 +430,14 @@ def _setup_djc_global_state(
|
|||
# Declare that the code is running in test mode - this is used
|
||||
# by the import / autodiscover mechanism to clean up loaded modules
|
||||
# between tests.
|
||||
global IS_TESTING
|
||||
global IS_TESTING # noqa: PLW0603
|
||||
IS_TESTING = True
|
||||
|
||||
gen_id_patcher.start()
|
||||
csrf_token_patcher.start()
|
||||
|
||||
# Re-load the settings, so that the test-specific settings overrides are applied
|
||||
from django_components.app_settings import app_settings
|
||||
from django_components.app_settings import app_settings # noqa: PLC0415
|
||||
|
||||
app_settings._load_settings()
|
||||
extensions._initialized = False
|
||||
|
@ -465,7 +467,7 @@ def _clear_djc_global_state(
|
|||
loader.reset()
|
||||
|
||||
# NOTE: There are 1-2 tests which check Templates, so we need to clear the cache
|
||||
from django_components.cache import component_media_cache, template_cache
|
||||
from django_components.cache import component_media_cache, template_cache # noqa: PLC0415
|
||||
|
||||
if template_cache:
|
||||
template_cache.clear()
|
||||
|
@ -505,7 +507,7 @@ def _clear_djc_global_state(
|
|||
del ALL_COMPONENTS[reverse_index]
|
||||
|
||||
# Remove registries that were created during the test
|
||||
initial_registries_set: Set[RegistryRef] = set([reg_ref for reg_ref, init_keys in initial_registries_copies])
|
||||
initial_registries_set: Set[RegistryRef] = {reg_ref for reg_ref, init_keys in initial_registries_copies}
|
||||
for index in range(len(ALL_REGISTRIES)):
|
||||
registry_ref = ALL_REGISTRIES[len(ALL_REGISTRIES) - index - 1]
|
||||
is_ref_deleted = registry_ref() is None
|
||||
|
@ -532,7 +534,7 @@ def _clear_djc_global_state(
|
|||
|
||||
# Delete autoimported modules from memory, so the module
|
||||
# is executed also the next time one of the tests calls `autodiscover`.
|
||||
from django_components.autodiscovery import LOADED_MODULES
|
||||
from django_components.autodiscovery import LOADED_MODULES # noqa: PLC0415
|
||||
|
||||
for mod in LOADED_MODULES:
|
||||
sys.modules.pop(mod, None)
|
||||
|
@ -556,5 +558,5 @@ def _clear_djc_global_state(
|
|||
if gc_collect:
|
||||
gc.collect()
|
||||
|
||||
global IS_TESTING
|
||||
global IS_TESTING # noqa: PLW0603
|
||||
IS_TESTING = False
|
||||
|
|
|
@ -26,5 +26,3 @@ class Empty(NamedTuple):
|
|||
|
||||
Read more about [Typing and validation](../../concepts/fundamentals/typing_and_validation).
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
|
@ -11,7 +11,7 @@ T = TypeVar("T")
|
|||
if sys.version_info >= (3, 9):
|
||||
|
||||
@overload # type: ignore[misc]
|
||||
def cached_ref(obj: T) -> ReferenceType[T]: ... # noqa: E704
|
||||
def cached_ref(obj: T) -> ReferenceType[T]: ...
|
||||
|
||||
|
||||
def cached_ref(obj: Any) -> ReferenceType:
|
||||
|
|
|
@ -17,10 +17,10 @@ def compile_js_files_to_file(
|
|||
file_paths: Sequence[Union[Path, str]],
|
||||
out_file: Union[Path, str],
|
||||
esbuild_args: Optional[List[str]] = None,
|
||||
):
|
||||
) -> None:
|
||||
# Find Esbuild binary
|
||||
bin_name = "esbuild.cmd" if os.name == "nt" else "esbuild"
|
||||
esbuild_path = Path(os.getcwd()) / "node_modules" / ".bin" / bin_name
|
||||
esbuild_path = Path.cwd() / "node_modules" / ".bin" / bin_name
|
||||
|
||||
# E.g. `esbuild js_file1.ts js_file2.ts js_file3.ts --bundle --minify --outfile=here.js`
|
||||
esbuild_cmd = [
|
||||
|
@ -39,12 +39,12 @@ def compile_js_files_to_file(
|
|||
# - This script should be called from within django_components_js` dir!
|
||||
# - Also you need to have esbuild installed. If not yet, run:
|
||||
# `npm install -D esbuild`
|
||||
def build():
|
||||
def build() -> None:
|
||||
entrypoint = "./src/index.ts"
|
||||
out_file = Path("../django_components/static/django_components/django_components.min.js")
|
||||
|
||||
# Prepare output dir
|
||||
os.makedirs(out_file.parent, exist_ok=True)
|
||||
out_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Compile JS
|
||||
compile_js_files_to_file(file_paths=[entrypoint], out_file=out_file)
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
from django_components import Component
|
||||
|
||||
|
||||
|
|
|
@ -20,8 +20,7 @@ class PathObj:
|
|||
|
||||
if self.static_path.endswith(".js"):
|
||||
return format_html('<script type="module" src="{}"></script>', static(self.static_path))
|
||||
else:
|
||||
return format_html('<link href="{}" rel="stylesheet">', static(self.static_path))
|
||||
return format_html('<link href="{}" rel="stylesheet">', static(self.static_path))
|
||||
|
||||
|
||||
@register("relative_file_pathobj_component")
|
||||
|
|
|
@ -20,7 +20,7 @@ from testserver.views import (
|
|||
urlpatterns = [
|
||||
path("", include("django_components.urls")),
|
||||
# Empty response with status 200 to notify other systems when the server has started
|
||||
path("poll/", lambda *args, **kwargs: HttpResponse("")),
|
||||
path("poll/", lambda *_args, **_kwargs: HttpResponse("")),
|
||||
# Test views
|
||||
path("single/", single_component_view, name="single"),
|
||||
path("multi/", multiple_components_view, name="multi"),
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from django.http import HttpResponse
|
||||
from django.template import Context, Template
|
||||
from testserver.components import FragComp, FragMedia
|
||||
|
||||
from django_components import types
|
||||
if TYPE_CHECKING:
|
||||
from django_components import types
|
||||
|
||||
|
||||
def single_component_view(request):
|
||||
def single_component_view(_request):
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<!DOCTYPE html>
|
||||
|
@ -27,7 +30,7 @@ def single_component_view(request):
|
|||
return HttpResponse(rendered)
|
||||
|
||||
|
||||
def multiple_components_view(request):
|
||||
def multiple_components_view(_request):
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<!DOCTYPE html>
|
||||
|
@ -50,7 +53,7 @@ def multiple_components_view(request):
|
|||
return HttpResponse(rendered)
|
||||
|
||||
|
||||
def check_js_order_in_js_view(request):
|
||||
def check_js_order_in_js_view(_request):
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<!DOCTYPE html>
|
||||
|
@ -74,7 +77,7 @@ def check_js_order_in_js_view(request):
|
|||
return HttpResponse(rendered)
|
||||
|
||||
|
||||
def check_js_order_in_media_view(request):
|
||||
def check_js_order_in_media_view(_request):
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<!DOCTYPE html>
|
||||
|
@ -98,7 +101,7 @@ def check_js_order_in_media_view(request):
|
|||
return HttpResponse(rendered)
|
||||
|
||||
|
||||
def check_js_order_vars_not_available_before_view(request):
|
||||
def check_js_order_vars_not_available_before_view(_request):
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<!DOCTYPE html>
|
||||
|
@ -170,8 +173,8 @@ def fragment_base_js_view(request):
|
|||
Context(
|
||||
{
|
||||
"frag": frag,
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
return HttpResponse(rendered)
|
||||
|
||||
|
@ -283,13 +286,12 @@ def fragment_view(request):
|
|||
fragment_type = request.GET["frag"]
|
||||
if fragment_type == "comp":
|
||||
return FragComp.render_to_response(deps_strategy="fragment")
|
||||
elif fragment_type == "media":
|
||||
if fragment_type == "media":
|
||||
return FragMedia.render_to_response(deps_strategy="fragment")
|
||||
else:
|
||||
raise ValueError("Invalid fragment type")
|
||||
raise ValueError("Invalid fragment type")
|
||||
|
||||
|
||||
def alpine_in_head_view(request):
|
||||
def alpine_in_head_view(_request):
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<!DOCTYPE html>
|
||||
|
@ -309,7 +311,7 @@ def alpine_in_head_view(request):
|
|||
return HttpResponse(rendered)
|
||||
|
||||
|
||||
def alpine_in_body_view(request):
|
||||
def alpine_in_body_view(_request):
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<!DOCTYPE html>
|
||||
|
@ -330,7 +332,7 @@ def alpine_in_body_view(request):
|
|||
|
||||
|
||||
# Same as before, but Alpine component defined in Component.js
|
||||
def alpine_in_body_view_2(request):
|
||||
def alpine_in_body_view_2(_request):
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<!DOCTYPE html>
|
||||
|
@ -350,7 +352,7 @@ def alpine_in_body_view_2(request):
|
|||
return HttpResponse(rendered)
|
||||
|
||||
|
||||
def alpine_in_body_vars_not_available_before_view(request):
|
||||
def alpine_in_body_vars_not_available_before_view(_request):
|
||||
template_str: types.django_html = """
|
||||
{% load component_tags %}
|
||||
<!DOCTYPE html>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# ruff: noqa: T201
|
||||
|
||||
import functools
|
||||
import subprocess
|
||||
import sys
|
||||
|
@ -54,7 +56,7 @@ def run_django_dev_server():
|
|||
start_time = time.time()
|
||||
while time.time() - start_time < 30: # timeout after 30 seconds
|
||||
try:
|
||||
response = requests.get(f"http://127.0.0.1:{TEST_SERVER_PORT}/poll")
|
||||
response = requests.get(f"http://127.0.0.1:{TEST_SERVER_PORT}/poll") # noqa: S113
|
||||
if response.status_code == 200:
|
||||
print("Django dev server is up and running.")
|
||||
break
|
||||
|
|
|
@ -23,7 +23,9 @@ class TestFormatAttributes:
|
|||
assert format_attributes({"class": "foo", "style": "color: red;"}) == 'class="foo" style="color: red;"'
|
||||
|
||||
def test_escapes_special_characters(self):
|
||||
assert format_attributes({"x-on:click": "bar", "@click": "'baz'"}) == 'x-on:click="bar" @click="'baz'"' # noqa: E501
|
||||
assert (
|
||||
format_attributes({"x-on:click": "bar", "@click": "'baz'"}) == 'x-on:click="bar" @click="'baz'"'
|
||||
)
|
||||
|
||||
def test_does_not_escape_special_characters_if_safe_string(self):
|
||||
assert format_attributes({"foo": mark_safe("'bar'")}) == "foo=\"'bar'\""
|
||||
|
@ -51,7 +53,7 @@ class TestMergeAttributes:
|
|||
assert merge_attributes({"class": "foo", "id": "bar"}, {"class": "baz"}) == {
|
||||
"class": "foo baz",
|
||||
"id": "bar",
|
||||
} # noqa: E501
|
||||
}
|
||||
|
||||
def test_merge_with_empty_dict(self):
|
||||
assert merge_attributes({}, {"foo": "bar"}) == {"foo": "bar"}
|
||||
|
@ -70,7 +72,7 @@ class TestMergeAttributes:
|
|||
"tuna3",
|
||||
{"baz": True, "baz2": False, "tuna": False, "tuna2": True, "tuna3": None},
|
||||
["extra", {"extra2": False, "baz2": True, "tuna": True, "tuna2": False}],
|
||||
]
|
||||
],
|
||||
},
|
||||
) == {"class": "foo bar tuna baz baz2 extra"}
|
||||
|
||||
|
@ -82,7 +84,7 @@ class TestMergeAttributes:
|
|||
"background-color: blue;",
|
||||
{"background-color": "green", "color": None, "width": False},
|
||||
["position: absolute", {"height": "12px"}],
|
||||
]
|
||||
],
|
||||
},
|
||||
) == {"style": "color: red; height: 12px; background-color: green; position: absolute;"}
|
||||
|
||||
|
@ -142,7 +144,7 @@ class TestHtmlAttrs:
|
|||
<div {% html_attrs attrs defaults class="added_class" class="another-class" data-id=123 %}>
|
||||
content
|
||||
</div>
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
|
@ -170,7 +172,7 @@ class TestHtmlAttrs:
|
|||
<div {% html_attrs attrs defaults class %}>
|
||||
content
|
||||
</div>
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
|
@ -183,7 +185,9 @@ class TestHtmlAttrs:
|
|||
|
||||
with pytest.raises(
|
||||
TypeError,
|
||||
match=re.escape("Invalid parameters for tag 'html_attrs': takes 2 positional argument(s) but more were given"), # noqa: E501
|
||||
match=re.escape(
|
||||
"Invalid parameters for tag 'html_attrs': takes 2 positional argument(s) but more were given",
|
||||
),
|
||||
):
|
||||
template.render(Context({"class_var": "padding-top-8"}))
|
||||
|
||||
|
@ -251,7 +255,7 @@ class TestHtmlAttrs:
|
|||
<div {% html_attrs ...props class="another-class" %}>
|
||||
content
|
||||
</div>
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {
|
||||
|
@ -298,7 +302,7 @@ class TestHtmlAttrs:
|
|||
<div class="added_class another-class from_agg_key" data-djc-id-ca1bc3f data-id="123" type="submit">
|
||||
content
|
||||
</div>
|
||||
""", # noqa: E501
|
||||
""",
|
||||
)
|
||||
assert "override-me" not in rendered
|
||||
|
||||
|
@ -344,7 +348,7 @@ class TestHtmlAttrs:
|
|||
%}>
|
||||
content
|
||||
</div>
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"attrs": kwargs["attrs"]}
|
||||
|
@ -389,7 +393,7 @@ class TestHtmlAttrs:
|
|||
<div {% html_attrs attrs class="added_class" class="another-class" data-id=123 %}>
|
||||
content
|
||||
</div>
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"attrs": kwargs["attrs"]}
|
||||
|
@ -419,7 +423,7 @@ class TestHtmlAttrs:
|
|||
<div {% html_attrs class="added_class" class="another-class" data-id=123 %}>
|
||||
content
|
||||
</div>
|
||||
""" # noqa: E501
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"attrs": kwargs["attrs"]}
|
||||
|
|
|
@ -46,8 +46,8 @@ class TestAutodiscover:
|
|||
class TestImportLibraries:
|
||||
@djc_test(
|
||||
components_settings={
|
||||
"libraries": ["tests.components.single_file", "tests.components.multi_file.multi_file"]
|
||||
}
|
||||
"libraries": ["tests.components.single_file", "tests.components.multi_file.multi_file"],
|
||||
},
|
||||
)
|
||||
def test_import_libraries(self):
|
||||
all_components = registry.all().copy()
|
||||
|
@ -74,8 +74,8 @@ class TestImportLibraries:
|
|||
|
||||
@djc_test(
|
||||
components_settings={
|
||||
"libraries": ["components.single_file", "components.multi_file.multi_file"]
|
||||
}
|
||||
"libraries": ["components.single_file", "components.multi_file.multi_file"],
|
||||
},
|
||||
)
|
||||
def test_import_libraries_map_modules(self):
|
||||
all_components = registry.all().copy()
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
import difflib
|
||||
import json
|
||||
from dataclasses import MISSING, dataclass, field
|
||||
from datetime import date, datetime, timedelta
|
||||
from dataclasses import dataclass, field, MISSING
|
||||
from enum import Enum
|
||||
from inspect import signature
|
||||
from pathlib import Path
|
||||
|
@ -30,16 +30,16 @@ from typing import (
|
|||
import django
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||
from django.http import HttpRequest
|
||||
from django.middleware import csrf
|
||||
from django.template import Context, Template
|
||||
from django.template.defaultfilters import title
|
||||
from django.template.defaulttags import register as default_library
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.timezone import now
|
||||
from django.contrib.humanize.templatetags.humanize import naturaltime
|
||||
from django.template.defaulttags import register as default_library
|
||||
|
||||
from django_components import types, registry
|
||||
from django_components import registry, types
|
||||
|
||||
# DO NOT REMOVE - See https://github.com/django-components/django-components/pull/999
|
||||
# ----------- IMPORTS END ------------ #
|
||||
|
@ -61,9 +61,9 @@ if not settings.configured:
|
|||
"OPTIONS": {
|
||||
"builtins": [
|
||||
"django_components.templatetags.component_tags",
|
||||
]
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
COMPONENTS={
|
||||
"autodiscover": False,
|
||||
|
@ -74,9 +74,9 @@ if not settings.configured:
|
|||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": ":memory:",
|
||||
}
|
||||
},
|
||||
},
|
||||
SECRET_KEY="secret",
|
||||
SECRET_KEY="secret", # noqa: S106
|
||||
ROOT_URLCONF="django_components.urls",
|
||||
)
|
||||
django.setup()
|
||||
|
@ -91,19 +91,21 @@ else:
|
|||
|
||||
templates_cache: Dict[int, Template] = {}
|
||||
|
||||
|
||||
def lazy_load_template(template: str) -> Template:
|
||||
template_hash = hash(template)
|
||||
if template_hash in templates_cache:
|
||||
return templates_cache[template_hash]
|
||||
else:
|
||||
template_instance = Template(template)
|
||||
templates_cache[template_hash] = template_instance
|
||||
return template_instance
|
||||
template_instance = Template(template)
|
||||
templates_cache[template_hash] = template_instance
|
||||
return template_instance
|
||||
|
||||
|
||||
#####################################
|
||||
# RENDER ENTRYPOINT
|
||||
#####################################
|
||||
|
||||
|
||||
def gen_render_data():
|
||||
data = load_project_data_from_json(data_json)
|
||||
|
||||
|
@ -118,7 +120,7 @@ def gen_render_data():
|
|||
"text": "Test bookmark",
|
||||
"url": "http://localhost:8000/bookmarks/9/create",
|
||||
"attachment": None,
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
request = HttpRequest()
|
||||
|
@ -140,7 +142,7 @@ def render(data):
|
|||
# Render
|
||||
result = project_page(
|
||||
Context(),
|
||||
ProjectPageData(**data)
|
||||
ProjectPageData(**data),
|
||||
)
|
||||
|
||||
return result
|
||||
|
@ -669,7 +671,7 @@ data_json = """
|
|||
|
||||
def load_project_data_from_json(contents: str) -> dict:
|
||||
"""
|
||||
Loads project data from JSON and resolves references between objects.
|
||||
Load project data from JSON and resolves references between objects.
|
||||
Returns the data with all resolvable references replaced with actual object references.
|
||||
"""
|
||||
data = json.loads(contents)
|
||||
|
@ -1003,7 +1005,7 @@ TAG_TYPE_META = MappingProxyType(
|
|||
),
|
||||
),
|
||||
TagResourceType.PROJECT_OUTPUT: TagTypeMeta(
|
||||
allowed_values=tuple(),
|
||||
allowed_values=(),
|
||||
),
|
||||
TagResourceType.PROJECT_OUTPUT_ATTACHMENT: TagTypeMeta(
|
||||
allowed_values=(
|
||||
|
@ -1024,7 +1026,7 @@ TAG_TYPE_META = MappingProxyType(
|
|||
TagResourceType.PROJECT_TEMPLATE: TagTypeMeta(
|
||||
allowed_values=("Tag 21",),
|
||||
),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
@ -1091,7 +1093,7 @@ PROJECT_PHASES_META = MappingProxyType(
|
|||
ProjectOutputDef(title="Lorem ipsum 14"),
|
||||
],
|
||||
),
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
#####################################
|
||||
|
@ -1156,7 +1158,7 @@ _secondary_btn_styling = "ring-1 ring-inset"
|
|||
theme = Theme(
|
||||
default=ThemeStylingVariant(
|
||||
primary=ThemeStylingUnit(
|
||||
color="bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 transition"
|
||||
color="bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 transition",
|
||||
),
|
||||
primary_disabled=ThemeStylingUnit(color="bg-blue-300 text-blue-50 focus-visible:outline-blue-600 transition"),
|
||||
secondary=ThemeStylingUnit(
|
||||
|
@ -1277,8 +1279,7 @@ def format_timestamp(timestamp: datetime):
|
|||
"""
|
||||
if now() - timestamp > timedelta(days=7):
|
||||
return timestamp.strftime("%b %-d, %Y")
|
||||
else:
|
||||
return naturaltime(timestamp)
|
||||
return naturaltime(timestamp)
|
||||
|
||||
|
||||
def group_by(
|
||||
|
@ -1400,17 +1401,16 @@ def serialize_to_js(obj):
|
|||
items.append(f"{key}: {serialized_value}")
|
||||
return f"{{ {', '.join(items)} }}"
|
||||
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
if isinstance(obj, (list, tuple)):
|
||||
# If the object is a list, recursively serialize each item
|
||||
serialized_items = [serialize_to_js(item) for item in obj]
|
||||
return f"[{', '.join(serialized_items)}]"
|
||||
|
||||
elif isinstance(obj, str):
|
||||
if isinstance(obj, str):
|
||||
return obj
|
||||
|
||||
else:
|
||||
# For other types (int, float, etc.), just return the string representation
|
||||
return str(obj)
|
||||
# For other types (int, float, etc.), just return the string representation
|
||||
return str(obj)
|
||||
|
||||
|
||||
#####################################
|
||||
|
@ -1481,7 +1481,7 @@ def button(context: Context, data: ButtonData):
|
|||
"attrs": all_attrs,
|
||||
"is_link": is_link,
|
||||
"slot_content": data.slot_content,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(button_template_str).render(context)
|
||||
|
||||
|
@ -1615,7 +1615,7 @@ def menu(context: Context, data: MenuData):
|
|||
{
|
||||
"x-show": model,
|
||||
"x-cloak": "",
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
menu_list_data = MenuListData(
|
||||
|
@ -1633,7 +1633,7 @@ def menu(context: Context, data: MenuData):
|
|||
"attrs": data.attrs,
|
||||
"menu_list_data": menu_list_data,
|
||||
"slot_activator": data.slot_activator,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(menu_template_str).render(context)
|
||||
|
||||
|
@ -1738,7 +1738,7 @@ def menu_list(context: Context, data: MenuListData):
|
|||
{
|
||||
"item_groups": item_groups,
|
||||
"attrs": data.attrs,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(menu_list_template_str).render(context)
|
||||
|
||||
|
@ -1800,7 +1800,7 @@ class TableCell:
|
|||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.colspan, int) or self.colspan < 1:
|
||||
raise ValueError("TableCell.colspan must be a non-negative integer." f" Instead got {self.colspan}")
|
||||
raise ValueError(f"TableCell.colspan must be a non-negative integer. Instead got {self.colspan}")
|
||||
|
||||
|
||||
NULL_CELL = TableCell("")
|
||||
|
@ -1976,7 +1976,7 @@ class TableData(NamedTuple):
|
|||
|
||||
@registry.library.simple_tag(takes_context=True)
|
||||
def table(context: Context, data: TableData):
|
||||
rows_to_render = [tuple([row, prepare_row_headers(row, data.headers)]) for row in data.rows]
|
||||
rows_to_render = [(row, prepare_row_headers(row, data.headers)) for row in data.rows]
|
||||
|
||||
with context.push(
|
||||
{
|
||||
|
@ -1984,7 +1984,7 @@ def table(context: Context, data: TableData):
|
|||
"rows_to_render": rows_to_render,
|
||||
"NULL_CELL": NULL_CELL,
|
||||
"attrs": data.attrs,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(table_template_str).render(context)
|
||||
|
||||
|
@ -2080,7 +2080,7 @@ def icon(context: Context, data: IconData):
|
|||
"attrs": data.attrs,
|
||||
"heroicon_data": heroicon_data,
|
||||
"slot_content": data.slot_content,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(icon_template_str).render(context)
|
||||
|
||||
|
@ -2097,9 +2097,9 @@ ICONS = {
|
|||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round",
|
||||
"d": "M4.26 10.147a60.438 60.438 0 0 0-.491 6.347A48.62 48.62 0 0 1 12 20.904a48.62 48.62 0 0 1 8.232-4.41 60.46 60.46 0 0 0-.491-6.347m-15.482 0a50.636 50.636 0 0 0-2.658-.813A59.906 59.906 0 0 1 12 3.493a59.903 59.903 0 0 1 10.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.717 50.717 0 0 1 12 13.489a50.702 50.702 0 0 1 7.74-3.342M6.75 15a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm0 0v-3.675A55.378 55.378 0 0 1 12 8.443m-7.007 11.55A5.981 5.981 0 0 0 6.75 15.75v-1.5", # noqa: E501
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
@ -2113,9 +2113,8 @@ class ComponentDefaults(metaclass=ComponentDefaultsMeta):
|
|||
def __post_init__(self) -> None:
|
||||
fields = self.__class__.__dataclass_fields__ # type: ignore[attr-defined]
|
||||
for field_name, dataclass_field in fields.items():
|
||||
if dataclass_field.default is not MISSING:
|
||||
if getattr(self, field_name) is None:
|
||||
setattr(self, field_name, dataclass_field.default)
|
||||
if dataclass_field.default is not MISSING and getattr(self, field_name) is None:
|
||||
setattr(self, field_name, dataclass_field.default)
|
||||
|
||||
|
||||
class IconDefaults(ComponentDefaults):
|
||||
|
@ -2195,7 +2194,7 @@ def heroicon(context: Context, data: HeroIconData):
|
|||
"icon_paths": icon_paths,
|
||||
"default_attrs": default_attrs,
|
||||
"attrs": kwargs.attrs,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(heroicon_template_str).render(context)
|
||||
|
||||
|
@ -2295,10 +2294,11 @@ def expansion_panel(context: Context, data: ExpansionPanelData):
|
|||
"expand_icon_data": expand_icon_data,
|
||||
"slot_header": data.slot_header,
|
||||
"slot_content": data.slot_content,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(expansion_panel_template_str).render(context)
|
||||
|
||||
|
||||
#####################################
|
||||
# PROJECT_PAGE
|
||||
#####################################
|
||||
|
@ -2363,7 +2363,7 @@ def project_page(context: Context, data: ProjectPageData):
|
|||
ListItem(
|
||||
value=title,
|
||||
link=f"/projects/{data.project['id']}/phases/{phase['phase_template']['type']}",
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
project_page_tabs = [
|
||||
|
@ -2378,7 +2378,7 @@ def project_page(context: Context, data: ProjectPageData):
|
|||
contacts=data.contacts,
|
||||
status_updates=data.status_updates,
|
||||
editable=data.user_is_project_owner,
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
TabItemData(
|
||||
|
@ -2390,7 +2390,7 @@ def project_page(context: Context, data: ProjectPageData):
|
|||
notes=data.notes_1,
|
||||
comments_by_notes=data.comments_by_notes_1, # type: ignore[arg-type]
|
||||
editable=data.user_is_project_member,
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
TabItemData(
|
||||
|
@ -2402,7 +2402,7 @@ def project_page(context: Context, data: ProjectPageData):
|
|||
notes=data.notes_2,
|
||||
comments_by_notes=data.comments_by_notes_2, # type: ignore[arg-type]
|
||||
editable=data.user_is_project_member,
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
TabItemData(
|
||||
|
@ -2414,7 +2414,7 @@ def project_page(context: Context, data: ProjectPageData):
|
|||
notes=data.notes_3,
|
||||
comments_by_notes=data.comments_by_notes_3, # type: ignore[arg-type]
|
||||
editable=data.user_is_project_member,
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
TabItemData(
|
||||
|
@ -2426,7 +2426,7 @@ def project_page(context: Context, data: ProjectPageData):
|
|||
outputs=data.outputs,
|
||||
editable=data.user_is_project_member,
|
||||
phase_titles=data.phase_titles,
|
||||
)
|
||||
),
|
||||
),
|
||||
),
|
||||
]
|
||||
|
@ -2435,7 +2435,7 @@ def project_page(context: Context, data: ProjectPageData):
|
|||
with tabs_context.push(
|
||||
{
|
||||
"project_page_tabs": project_page_tabs,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(project_page_tabs_template_str).render(tabs_context)
|
||||
|
||||
|
@ -2444,12 +2444,12 @@ def project_page(context: Context, data: ProjectPageData):
|
|||
ListData(
|
||||
items=rendered_phases,
|
||||
item_attrs={"class": "py-5"},
|
||||
)
|
||||
),
|
||||
)
|
||||
with context.push(
|
||||
{
|
||||
"project": data.project,
|
||||
}
|
||||
},
|
||||
):
|
||||
header_content = lazy_load_template(project_page_header_template_str).render(context)
|
||||
|
||||
|
@ -2464,6 +2464,7 @@ def project_page(context: Context, data: ProjectPageData):
|
|||
|
||||
return project_layout_tabbed(context, layout_tabbed_data)
|
||||
|
||||
|
||||
#####################################
|
||||
# PROJECT_LAYOUT_TABBED
|
||||
#####################################
|
||||
|
@ -2569,7 +2570,7 @@ def project_layout_tabbed(context: Context, data: ProjectLayoutTabbedData):
|
|||
breadcrumbs_content = breadcrumbs_tag(context, BreadcrumbsData(items=prefixed_breadcrumbs))
|
||||
bookmarks_content = bookmarks_tag(
|
||||
context,
|
||||
BookmarksData(bookmarks=data.layout_data.bookmarks, project_id=data.layout_data.project["id"])
|
||||
BookmarksData(bookmarks=data.layout_data.bookmarks, project_id=data.layout_data.project["id"]),
|
||||
)
|
||||
|
||||
content_tabs_static_data = TabsStaticData(
|
||||
|
@ -2600,7 +2601,7 @@ def project_layout_tabbed(context: Context, data: ProjectLayoutTabbedData):
|
|||
"slot_left_panel": data.slot_left_panel,
|
||||
"slot_header": data.slot_header,
|
||||
"slot_tabs": data.slot_tabs,
|
||||
}
|
||||
},
|
||||
):
|
||||
layout_content = lazy_load_template(project_layout_tabbed_content_template_str).render(context)
|
||||
|
||||
|
@ -2695,7 +2696,7 @@ def layout(context: Context, data: LayoutData):
|
|||
"sidebar_data": sidebar_data,
|
||||
"slot_header": data.slot_header,
|
||||
"slot_content": data.slot_content,
|
||||
}
|
||||
},
|
||||
):
|
||||
layout_base_content = lazy_load_template(layout_base_content_template_str).render(provided_context)
|
||||
|
||||
|
@ -2745,7 +2746,7 @@ def layout(context: Context, data: LayoutData):
|
|||
with provided_context.push(
|
||||
{
|
||||
"base_data": base_data,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template("{% base base_data %}").render(provided_context)
|
||||
|
||||
|
@ -2757,7 +2758,7 @@ def layout(context: Context, data: LayoutData):
|
|||
with context.push(
|
||||
{
|
||||
"render_context_provider_data": render_context_provider_data,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(layout_template_str).render(context)
|
||||
|
||||
|
@ -3108,7 +3109,7 @@ def base(context: Context, data: BaseData) -> str:
|
|||
"slot_css": data.slot_css,
|
||||
"slot_js": data.slot_js,
|
||||
"slot_content": data.slot_content,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(base_template_str).render(context)
|
||||
|
||||
|
@ -3330,7 +3331,7 @@ def sidebar(context: Context, data: SidebarData):
|
|||
"faq_icon_data": faq_icon_data,
|
||||
# Slots
|
||||
"slot_content": data.slot_content,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(sidebar_template_str).render(context)
|
||||
|
||||
|
@ -3391,7 +3392,7 @@ def navbar(context: Context, data: NavbarData):
|
|||
{
|
||||
"sidebar_toggle_icon_data": sidebar_toggle_icon_data,
|
||||
"attrs": data.attrs,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(navbar_template_str).render(context)
|
||||
|
||||
|
@ -3621,7 +3622,7 @@ def dialog(context: Context, data: DialogData):
|
|||
"slot_title": data.slot_title,
|
||||
"slot_content": data.slot_content,
|
||||
"slot_append": data.slot_append,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(dialog_template_str).render(context)
|
||||
|
||||
|
@ -3871,7 +3872,7 @@ def tags(context: Context, data: TagsData):
|
|||
"remove_button_data": remove_button_data,
|
||||
"add_tag_button_data": add_tag_button_data,
|
||||
"slot_title": slot_title,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(tags_template_str).render(context)
|
||||
|
||||
|
@ -4062,7 +4063,7 @@ def form(context: Context, data: FormData):
|
|||
"slot_actions_append": data.slot_actions_append,
|
||||
"slot_form": data.slot_form,
|
||||
"slot_below_form": data.slot_below_form,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(form_template_str).render(context)
|
||||
|
||||
|
@ -4074,9 +4075,7 @@ def form(context: Context, data: FormData):
|
|||
|
||||
@dataclass(frozen=True)
|
||||
class Breadcrumb:
|
||||
"""
|
||||
Single breadcrumb item used with the `breadcrumb` components.
|
||||
"""
|
||||
"""Single breadcrumb item used with the `breadcrumb` components."""
|
||||
|
||||
value: Any
|
||||
"""Value of the menu item to render."""
|
||||
|
@ -4160,7 +4159,7 @@ def breadcrumbs(context: Context, data: BreadcrumbsData):
|
|||
{
|
||||
"items": data.items,
|
||||
"attrs": data.attrs,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(breadcrumbs_template_str).render(context)
|
||||
|
||||
|
@ -4383,7 +4382,7 @@ def bookmarks(context: Context, data: BookmarksData):
|
|||
"bookmarks_icon_data": bookmarks_icon_data,
|
||||
"add_new_bookmark_icon_data": add_new_bookmark_icon_data,
|
||||
"context_menu_data": context_menu_data,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(bookmarks_template_str).render(context)
|
||||
|
||||
|
@ -4485,7 +4484,7 @@ def bookmark(context: Context, data: BookmarkData):
|
|||
"bookmark": data.bookmark._asdict(),
|
||||
"js": data.js,
|
||||
"bookmark_icon_data": bookmark_icon_data,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(bookmark_template_str).render(context)
|
||||
|
||||
|
@ -4562,7 +4561,7 @@ def list_tag(context: Context, data: ListData):
|
|||
"attrs": data.attrs,
|
||||
"item_attrs": data.item_attrs,
|
||||
"slot_empty": data.slot_empty,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(list_template_str).render(context)
|
||||
|
||||
|
@ -4728,7 +4727,7 @@ def tabs_impl(context: Context, data: TabsImplData):
|
|||
"content_attrs": data.content_attrs,
|
||||
"tabs_data": {"name": data.name},
|
||||
"theme": theme,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(tabs_impl_template_str).render(context)
|
||||
|
||||
|
@ -4743,6 +4742,11 @@ class TabsData(NamedTuple):
|
|||
slot_content: Optional[CallableSlot] = None
|
||||
|
||||
|
||||
class ProvidedData(NamedTuple):
|
||||
tabs: List[TabEntry]
|
||||
enabled: bool
|
||||
|
||||
|
||||
# This is an "API" component, meaning that it's designed to process
|
||||
# user input provided as nested components. But after the input is
|
||||
# processed, it delegates to an internal "implementation" component
|
||||
|
@ -4752,7 +4756,6 @@ def tabs(context: Context, data: TabsData):
|
|||
if not data.slot_content:
|
||||
return ""
|
||||
|
||||
ProvidedData = NamedTuple("ProvidedData", [("tabs", List[TabEntry]), ("enabled", bool)])
|
||||
collected_tabs: List[TabEntry] = []
|
||||
provided_data = ProvidedData(tabs=collected_tabs, enabled=True)
|
||||
|
||||
|
@ -4792,7 +4795,7 @@ def tab_item(context, data: TabItemData):
|
|||
raise RuntimeError(
|
||||
"Component 'tab_item' was called with no parent Tabs component. "
|
||||
"Either wrap 'tab_item' in Tabs component, or check if the component "
|
||||
"is not a descendant of another instance of 'tab_item'"
|
||||
"is not a descendant of another instance of 'tab_item'",
|
||||
)
|
||||
parent_tabs = tab_ctx.tabs
|
||||
|
||||
|
@ -4801,7 +4804,7 @@ def tab_item(context, data: TabItemData):
|
|||
"header": data.header,
|
||||
"disabled": data.disabled,
|
||||
"content": mark_safe(data.slot_content or "").strip(),
|
||||
}
|
||||
},
|
||||
)
|
||||
return ""
|
||||
|
||||
|
@ -4877,7 +4880,7 @@ def tabs_static(context: Context, data: TabsStaticData):
|
|||
"hide_body": data.hide_body,
|
||||
"selected_content": selected_content,
|
||||
"theme": theme,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(tabs_static_template_str).render(context)
|
||||
|
||||
|
@ -5055,7 +5058,7 @@ def project_info(context: Context, data: ProjectInfoData) -> str:
|
|||
"edit_project_button_data": edit_project_button_data,
|
||||
"edit_team_button_data": edit_team_button_data,
|
||||
"edit_contacts_button_data": edit_contacts_button_data,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(project_info_template_str).render(context)
|
||||
|
||||
|
@ -5126,11 +5129,7 @@ project_notes_template_str: types.django_html = """
|
|||
|
||||
def _make_comments_data(note: ProjectNote, comment: ProjectNoteComment):
|
||||
modified_time_str = format_timestamp(datetime.fromisoformat(comment["modified"]))
|
||||
formatted_modified_by = (
|
||||
modified_time_str
|
||||
+ " "
|
||||
+ comment['modified_by']['name']
|
||||
)
|
||||
formatted_modified_by = modified_time_str + " " + comment["modified_by"]["name"]
|
||||
|
||||
edit_comment_icon_data = IconData(
|
||||
name="pencil-square",
|
||||
|
@ -5174,7 +5173,7 @@ def _make_notes_data(
|
|||
"edit_note_icon_data": edit_note_icon_data,
|
||||
"comments": comments_data,
|
||||
"create_comment_button_data": create_comment_button_data,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
return notes_data
|
||||
|
@ -5201,7 +5200,7 @@ def project_notes(context: Context, data: ProjectNotesData) -> str:
|
|||
"create_note_button_data": create_note_button_data,
|
||||
"notes_data": notes_data,
|
||||
"editable": data.editable,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(project_notes_template_str).render(context)
|
||||
|
||||
|
@ -5271,9 +5270,11 @@ def project_outputs_summary(context: Context, data: ProjectOutputsSummaryData) -
|
|||
{
|
||||
"outputs_data": outputs_data,
|
||||
"outputs": data.outputs,
|
||||
}
|
||||
},
|
||||
):
|
||||
expansion_panel_content = lazy_load_template(outputs_summary_expansion_content_template_str).render(context) # noqa: E501
|
||||
expansion_panel_content = lazy_load_template(outputs_summary_expansion_content_template_str).render(
|
||||
context,
|
||||
)
|
||||
|
||||
expansion_panel_data = ExpansionPanelData(
|
||||
open=has_outputs,
|
||||
|
@ -5291,7 +5292,7 @@ def project_outputs_summary(context: Context, data: ProjectOutputsSummaryData) -
|
|||
with context.push(
|
||||
{
|
||||
"groups": groups,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(project_outputs_summary_template_str).render(context)
|
||||
|
||||
|
@ -5333,11 +5334,7 @@ project_status_updates_template_str: types.django_html = """
|
|||
|
||||
def _make_status_update_data(status_update: ProjectStatusUpdate):
|
||||
modified_time_str = format_timestamp(datetime.fromisoformat(status_update["modified"]))
|
||||
formatted_modified_by = (
|
||||
modified_time_str
|
||||
+ " "
|
||||
+ status_update['modified_by']['name']
|
||||
)
|
||||
formatted_modified_by = modified_time_str + " " + status_update["modified_by"]["name"]
|
||||
|
||||
return {
|
||||
"timestamp": formatted_modified_by,
|
||||
|
@ -5381,7 +5378,7 @@ def project_status_updates(context: Context, data: ProjectStatusUpdatesData) ->
|
|||
"updates_data": updates_data,
|
||||
"editable": data.editable,
|
||||
"add_status_button_data": add_status_button_data,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(project_status_updates_template_str).render(context)
|
||||
|
||||
|
@ -5510,17 +5507,14 @@ def project_users(context: Context, data: ProjectUsersData) -> str:
|
|||
"name": TableCell(user["name"]),
|
||||
"role": TableCell(role["name"]),
|
||||
"delete": delete_action,
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
submit_url = f"/submit/{data.project_id}/role/create"
|
||||
project_url = f"/project/{data.project_id}"
|
||||
|
||||
if data.available_roles:
|
||||
available_role_choices = [(role, role) for role in data.available_roles]
|
||||
else:
|
||||
available_role_choices = []
|
||||
available_role_choices = [(role, role) for role in data.available_roles] if data.available_roles else []
|
||||
|
||||
if data.available_users:
|
||||
available_user_choices = [(str(user["id"]), user["name"]) for user in data.available_users]
|
||||
|
@ -5554,7 +5548,7 @@ def project_users(context: Context, data: ProjectUsersData) -> str:
|
|||
with context.push(
|
||||
{
|
||||
"delete_icon_data": delete_icon_data,
|
||||
}
|
||||
},
|
||||
):
|
||||
user_dialog_title = lazy_load_template(user_dialog_title_template_str).render(context)
|
||||
|
||||
|
@ -5585,7 +5579,7 @@ def project_users(context: Context, data: ProjectUsersData) -> str:
|
|||
"set_role_button_data": set_role_button_data,
|
||||
"cancel_button_data": cancel_button_data,
|
||||
"dialog_data": dialog_data,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(project_users_template_str).render(context)
|
||||
|
||||
|
@ -5636,7 +5630,7 @@ def project_user_action(context: Context, data: ProjectUserActionData) -> str:
|
|||
{
|
||||
"role": role_data,
|
||||
"delete_icon_data": delete_icon_data,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(project_user_action_template_str).render(context)
|
||||
|
||||
|
@ -5692,7 +5686,7 @@ def project_outputs(context: Context, data: ProjectOutputsData) -> str:
|
|||
url=attachment[0]["url"],
|
||||
text=attachment[0]["text"],
|
||||
tags=attachment[1],
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
update_output_url = "/update"
|
||||
|
@ -5713,10 +5707,10 @@ def project_outputs(context: Context, data: ProjectOutputsData) -> str:
|
|||
}
|
||||
for d in attachments
|
||||
],
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
has_missing_deps = any([not output["completed"] for output, _ in dependencies])
|
||||
has_missing_deps = any(not output["completed"] for output, _ in dependencies)
|
||||
|
||||
output_badge_data = ProjectOutputBadgeData(
|
||||
completed=output["completed"],
|
||||
|
@ -5741,9 +5735,11 @@ def project_outputs(context: Context, data: ProjectOutputsData) -> str:
|
|||
{
|
||||
"dependencies_data": [ProjectOutputDependencyData(dependency=dep) for dep in deps],
|
||||
"output_form_data": output_form_data,
|
||||
}
|
||||
},
|
||||
):
|
||||
output_expansion_panel_content = lazy_load_template(output_expansion_panel_content_template_str).render(context) # noqa: E501
|
||||
output_expansion_panel_content = lazy_load_template(output_expansion_panel_content_template_str).render(
|
||||
context,
|
||||
)
|
||||
|
||||
expansion_panel_data = ExpansionPanelData(
|
||||
panel_id=output["id"], # type: ignore[arg-type]
|
||||
|
@ -5752,7 +5748,7 @@ def project_outputs(context: Context, data: ProjectOutputsData) -> str:
|
|||
header_attrs={"class": "flex align-center justify-between"},
|
||||
slot_header=f"""
|
||||
<div>
|
||||
{output['name']}
|
||||
{output["name"]}
|
||||
</div>
|
||||
""",
|
||||
slot_content=output_expansion_panel_content,
|
||||
|
@ -5763,13 +5759,13 @@ def project_outputs(context: Context, data: ProjectOutputsData) -> str:
|
|||
output_data,
|
||||
output_badge_data,
|
||||
expansion_panel_data,
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
with context.push(
|
||||
{
|
||||
"outputs_data": outputs_data,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(project_outputs_template_str).render(context)
|
||||
|
||||
|
@ -5833,7 +5829,7 @@ def project_output_badge(context: Context, data: ProjectOutputBadgeData):
|
|||
"theme": theme,
|
||||
"missing_icon_data": missing_icon_data,
|
||||
"completed_icon_data": completed_icon_data,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(project_output_badge_template_str).render(context)
|
||||
|
||||
|
@ -5952,7 +5948,7 @@ def project_output_dependency(context: Context, data: ProjectOutputDependencyDat
|
|||
"warning_icon_data": warning_icon_data,
|
||||
"missing_button_data": missing_button_data,
|
||||
"parent_attachments_data": parent_attachments_data,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(project_output_dependency_template_str).render(context)
|
||||
|
||||
|
@ -6131,7 +6127,7 @@ def project_output_attachments(context: Context, data: ProjectOutputAttachmentsD
|
|||
"edit_button_data": edit_button_data,
|
||||
"remove_button_data": remove_button_data,
|
||||
"tags_data": tags_data,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(project_output_attachments_template_str).render(context)
|
||||
|
||||
|
@ -6400,7 +6396,7 @@ def project_output_form(context: Context, data: ProjectOutputFormData):
|
|||
"project_output_attachments_data": project_output_attachments_data,
|
||||
"save_button_data": save_button_data,
|
||||
"add_attachment_button_data": add_attachment_button_data,
|
||||
}
|
||||
},
|
||||
):
|
||||
form_content = lazy_load_template(form_content_template_str).render(context)
|
||||
|
||||
|
@ -6414,10 +6410,11 @@ def project_output_form(context: Context, data: ProjectOutputFormData):
|
|||
{
|
||||
"form_data": form_data,
|
||||
"alpine_attachments": [d._asdict() for d in data.data.attachments],
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(project_output_form_template_str).render(context)
|
||||
|
||||
|
||||
#####################################
|
||||
#
|
||||
# IMPLEMENTATION END
|
||||
|
@ -6432,6 +6429,7 @@ def project_output_form(context: Context, data: ProjectOutputFormData):
|
|||
|
||||
from django_components.testing import djc_test # noqa: E402
|
||||
|
||||
|
||||
@djc_test
|
||||
def test_render(snapshot):
|
||||
data = gen_render_data()
|
||||
|
|
|
@ -32,9 +32,9 @@ if not settings.configured:
|
|||
"OPTIONS": {
|
||||
"builtins": [
|
||||
"django_components.templatetags.component_tags",
|
||||
]
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
COMPONENTS={
|
||||
"autodiscover": False,
|
||||
|
@ -45,9 +45,9 @@ if not settings.configured:
|
|||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": ":memory:",
|
||||
}
|
||||
},
|
||||
},
|
||||
SECRET_KEY="secret",
|
||||
SECRET_KEY="secret", # noqa: S106
|
||||
ROOT_URLCONF="django_components.urls",
|
||||
)
|
||||
django.setup()
|
||||
|
@ -67,10 +67,9 @@ def lazy_load_template(template: str) -> Template:
|
|||
template_hash = hash(template)
|
||||
if template_hash in templates_cache:
|
||||
return templates_cache[template_hash]
|
||||
else:
|
||||
template_instance = Template(template)
|
||||
templates_cache[template_hash] = template_instance
|
||||
return template_instance
|
||||
template_instance = Template(template)
|
||||
templates_cache[template_hash] = template_instance
|
||||
return template_instance
|
||||
|
||||
|
||||
#####################################
|
||||
|
@ -150,7 +149,7 @@ _secondary_btn_styling = "ring-1 ring-inset"
|
|||
theme = Theme(
|
||||
default=ThemeStylingVariant(
|
||||
primary=ThemeStylingUnit(
|
||||
color="bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 transition"
|
||||
color="bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 transition",
|
||||
),
|
||||
primary_disabled=ThemeStylingUnit(color="bg-blue-300 text-blue-50 focus-visible:outline-blue-600 transition"),
|
||||
secondary=ThemeStylingUnit(
|
||||
|
@ -314,7 +313,7 @@ def button(context: Context, data: ButtonData):
|
|||
"attrs": all_attrs,
|
||||
"is_link": is_link,
|
||||
"slot_content": data.slot_content,
|
||||
}
|
||||
},
|
||||
):
|
||||
return lazy_load_template(button_template_str).render(context)
|
||||
|
||||
|
@ -333,6 +332,7 @@ def button(context: Context, data: ButtonData):
|
|||
|
||||
from django_components.testing import djc_test # noqa: E402
|
||||
|
||||
|
||||
@djc_test
|
||||
def test_render(snapshot):
|
||||
data = gen_render_data()
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -32,9 +32,9 @@ if not settings.configured:
|
|||
"OPTIONS": {
|
||||
"builtins": [
|
||||
"django_components.templatetags.component_tags",
|
||||
]
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
COMPONENTS={
|
||||
"autodiscover": False,
|
||||
|
@ -45,9 +45,9 @@ if not settings.configured:
|
|||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": ":memory:",
|
||||
}
|
||||
},
|
||||
},
|
||||
SECRET_KEY="secret",
|
||||
SECRET_KEY="secret", # noqa: S106
|
||||
ROOT_URLCONF="django_components.urls",
|
||||
)
|
||||
|
||||
|
@ -68,10 +68,10 @@ def lazy_load_template(template: str) -> Template:
|
|||
template_hash = hash(template)
|
||||
if template_hash in templates_cache:
|
||||
return templates_cache[template_hash]
|
||||
else:
|
||||
template_instance = Template(template)
|
||||
templates_cache[template_hash] = template_instance
|
||||
return template_instance
|
||||
|
||||
template_instance = Template(template)
|
||||
templates_cache[template_hash] = template_instance
|
||||
return template_instance
|
||||
|
||||
|
||||
#####################################
|
||||
|
@ -156,7 +156,7 @@ _secondary_btn_styling = "ring-1 ring-inset"
|
|||
theme = Theme(
|
||||
default=ThemeStylingVariant(
|
||||
primary=ThemeStylingUnit(
|
||||
color="bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 transition"
|
||||
color="bg-blue-600 text-white hover:bg-blue-500 focus-visible:outline-blue-600 transition",
|
||||
),
|
||||
primary_disabled=ThemeStylingUnit(color="bg-blue-300 text-blue-50 focus-visible:outline-blue-600 transition"),
|
||||
secondary=ThemeStylingUnit(
|
||||
|
@ -268,7 +268,7 @@ class Button(Component):
|
|||
disabled: Optional[bool] = False,
|
||||
variant: Union["ThemeVariant", Literal["plain"]] = "primary",
|
||||
color: Union["ThemeColor", str] = "default",
|
||||
type: Optional[str] = "button",
|
||||
type: Optional[str] = "button", # noqa: A002
|
||||
attrs: Optional[dict] = None,
|
||||
):
|
||||
common_css = (
|
||||
|
@ -336,6 +336,7 @@ class Button(Component):
|
|||
|
||||
from django_components.testing import djc_test # noqa: E402
|
||||
|
||||
|
||||
@djc_test
|
||||
def test_render(snapshot):
|
||||
data = gen_render_data()
|
||||
|
|
|
@ -125,8 +125,14 @@ class TestComponentMediaCache:
|
|||
assert not test_cache.has_key(f"__components:{TestSimpleComponent.class_id}:css")
|
||||
|
||||
# Check that we cache `Component.js` / `Component.css`
|
||||
assert test_cache.get(f"__components:{TestMediaNoVarsComponent.class_id}:js").strip() == "console.log('Hello from JS');" # noqa: E501
|
||||
assert test_cache.get(f"__components:{TestMediaNoVarsComponent.class_id}:css").strip() == ".novars-component { color: blue; }" # noqa: E501
|
||||
assert (
|
||||
test_cache.get(f"__components:{TestMediaNoVarsComponent.class_id}:js").strip()
|
||||
== "console.log('Hello from JS');"
|
||||
)
|
||||
assert (
|
||||
test_cache.get(f"__components:{TestMediaNoVarsComponent.class_id}:css").strip()
|
||||
== ".novars-component { color: blue; }"
|
||||
)
|
||||
|
||||
# Render the components to trigger caching of JS/CSS variables from `get_js_data` / `get_css_data`
|
||||
TestMediaAndVarsComponent.render()
|
||||
|
@ -138,4 +144,4 @@ class TestComponentMediaCache:
|
|||
|
||||
# TODO - Update once JS and CSS vars are enabled
|
||||
assert test_cache.get(f"__components:{TestMediaAndVarsComponent.class_id}:js:{js_vars_hash}").strip() == ""
|
||||
assert test_cache.get(f"__components:{TestMediaAndVarsComponent.class_id}:css:{css_vars_hash}").strip() == "" # noqa: E501
|
||||
assert test_cache.get(f"__components:{TestMediaAndVarsComponent.class_id}:css:{css_vars_hash}").strip() == ""
|
||||
|
|
|
@ -2,7 +2,9 @@ from io import StringIO
|
|||
from unittest.mock import patch
|
||||
|
||||
from django.core.management import call_command
|
||||
|
||||
from django_components.testing import djc_test
|
||||
|
||||
from .testutils import setup_test_config
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import os
|
||||
import tempfile
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from shutil import rmtree
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
|
||||
from django_components.testing import djc_test
|
||||
|
||||
from .testutils import setup_test_config
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
@ -22,12 +24,12 @@ class TestCreateComponentCommand:
|
|||
call_command("components", "create", component_name, "--path", temp_dir)
|
||||
|
||||
expected_files = [
|
||||
os.path.join(temp_dir, component_name, "script.js"),
|
||||
os.path.join(temp_dir, component_name, "style.css"),
|
||||
os.path.join(temp_dir, component_name, "template.html"),
|
||||
Path(temp_dir) / component_name / "script.js",
|
||||
Path(temp_dir) / component_name / "style.css",
|
||||
Path(temp_dir) / component_name / "template.html",
|
||||
]
|
||||
for file_path in expected_files:
|
||||
assert os.path.exists(file_path)
|
||||
assert file_path.exists()
|
||||
|
||||
rmtree(temp_dir)
|
||||
|
||||
|
@ -50,14 +52,14 @@ class TestCreateComponentCommand:
|
|||
)
|
||||
|
||||
expected_files = [
|
||||
os.path.join(temp_dir, component_name, "test.js"),
|
||||
os.path.join(temp_dir, component_name, "test.css"),
|
||||
os.path.join(temp_dir, component_name, "test.html"),
|
||||
os.path.join(temp_dir, component_name, f"{component_name}.py"),
|
||||
Path(temp_dir) / component_name / "test.js",
|
||||
Path(temp_dir) / component_name / "test.css",
|
||||
Path(temp_dir) / component_name / "test.html",
|
||||
Path(temp_dir) / component_name / f"{component_name}.py",
|
||||
]
|
||||
|
||||
for file_path in expected_files:
|
||||
assert os.path.exists(file_path), f"File {file_path} was not created"
|
||||
assert file_path.exists(), f"File {file_path} was not created"
|
||||
|
||||
rmtree(temp_dir)
|
||||
|
||||
|
@ -74,8 +76,8 @@ class TestCreateComponentCommand:
|
|||
"--dry-run",
|
||||
)
|
||||
|
||||
component_path = os.path.join(temp_dir, component_name)
|
||||
assert not os.path.exists(component_path)
|
||||
component_path = Path(temp_dir) / component_name
|
||||
assert not component_path.exists()
|
||||
|
||||
rmtree(temp_dir)
|
||||
|
||||
|
@ -83,10 +85,11 @@ class TestCreateComponentCommand:
|
|||
temp_dir = tempfile.mkdtemp()
|
||||
|
||||
component_name = "existingcomponent"
|
||||
component_path = os.path.join(temp_dir, component_name)
|
||||
os.makedirs(component_path)
|
||||
component_path = Path(temp_dir) / component_name
|
||||
component_path.mkdir(parents=True)
|
||||
|
||||
with open(os.path.join(component_path, f"{component_name}.py"), "w") as f:
|
||||
filepath = component_path / f"{component_name}.py"
|
||||
with filepath.open("w", encoding="utf-8") as f:
|
||||
f.write("hello world")
|
||||
|
||||
call_command(
|
||||
|
@ -98,7 +101,8 @@ class TestCreateComponentCommand:
|
|||
"--force",
|
||||
)
|
||||
|
||||
with open(os.path.join(component_path, f"{component_name}.py"), "r") as f:
|
||||
filepath = component_path / f"{component_name}.py"
|
||||
with filepath.open("r", encoding="utf-8") as f:
|
||||
assert "hello world" not in f.read()
|
||||
|
||||
rmtree(temp_dir)
|
||||
|
@ -107,8 +111,8 @@ class TestCreateComponentCommand:
|
|||
temp_dir = tempfile.mkdtemp()
|
||||
|
||||
component_name = "existingcomponent_2"
|
||||
component_path = os.path.join(temp_dir, component_name)
|
||||
os.makedirs(component_path)
|
||||
component_path = Path(temp_dir) / component_name
|
||||
component_path.mkdir(parents=True)
|
||||
|
||||
with pytest.raises(CommandError):
|
||||
call_command("components", "create", component_name, "--path", temp_dir)
|
||||
|
@ -143,11 +147,11 @@ class TestCreateComponentCommand:
|
|||
call_command("startcomponent", component_name, "--path", temp_dir)
|
||||
|
||||
expected_files = [
|
||||
os.path.join(temp_dir, component_name, "script.js"),
|
||||
os.path.join(temp_dir, component_name, "style.css"),
|
||||
os.path.join(temp_dir, component_name, "template.html"),
|
||||
Path(temp_dir) / component_name / "script.js",
|
||||
Path(temp_dir) / component_name / "style.css",
|
||||
Path(temp_dir) / component_name / "template.html",
|
||||
]
|
||||
for file_path in expected_files:
|
||||
assert os.path.exists(file_path)
|
||||
assert file_path.exists()
|
||||
|
||||
rmtree(temp_dir)
|
||||
|
|
|
@ -5,8 +5,10 @@ from textwrap import dedent
|
|||
from unittest.mock import patch
|
||||
|
||||
from django.core.management import call_command
|
||||
|
||||
from django_components import CommandArg, CommandArgGroup, ComponentCommand, ComponentExtension
|
||||
from django_components.testing import djc_test
|
||||
|
||||
from .testutils import setup_test_config
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
@ -51,7 +53,7 @@ class DummyCommand(ComponentCommand):
|
|||
kwargs.pop("_command")
|
||||
kwargs.pop("_parser")
|
||||
sorted_kwargs = dict(sorted(kwargs.items()))
|
||||
print(f"DummyCommand.handle: args={args}, kwargs={sorted_kwargs}")
|
||||
print(f"DummyCommand.handle: args={args}, kwargs={sorted_kwargs}") # noqa: T201
|
||||
|
||||
|
||||
class DummyExtension(ComponentExtension):
|
||||
|
@ -85,7 +87,7 @@ class TestExtensionsCommand:
|
|||
{{list,run}}
|
||||
list List all extensions.
|
||||
run Run a command added by an extension.
|
||||
"""
|
||||
""",
|
||||
).lstrip()
|
||||
)
|
||||
|
||||
|
@ -221,7 +223,7 @@ class TestExtensionsRunCommand:
|
|||
subcommands:
|
||||
{{dummy}}
|
||||
dummy Run commands added by the 'dummy' extension.
|
||||
"""
|
||||
""",
|
||||
).lstrip()
|
||||
)
|
||||
|
||||
|
@ -248,7 +250,7 @@ class TestExtensionsRunCommand:
|
|||
subcommands:
|
||||
{{dummy_cmd}}
|
||||
dummy_cmd Dummy command description.
|
||||
"""
|
||||
""",
|
||||
).lstrip()
|
||||
)
|
||||
|
||||
|
@ -275,7 +277,7 @@ class TestExtensionsRunCommand:
|
|||
subcommands:
|
||||
{{dummy_cmd}}
|
||||
dummy_cmd Dummy command description.
|
||||
"""
|
||||
""",
|
||||
).lstrip()
|
||||
)
|
||||
|
||||
|
@ -294,7 +296,7 @@ class TestExtensionsRunCommand:
|
|||
== dedent(
|
||||
"""
|
||||
DummyCommand.handle: args=(), kwargs={'bar': None, 'baz': None, 'foo': None, 'force_color': False, 'no_color': False, 'pythonpath': None, 'settings': None, 'skip_checks': True, 'traceback': False, 'verbosity': 1}
|
||||
""" # noqa: E501
|
||||
""", # noqa: E501
|
||||
).lstrip()
|
||||
)
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# ruff: noqa: E501
|
||||
import re
|
||||
from io import StringIO
|
||||
from unittest.mock import patch
|
||||
|
@ -6,6 +7,7 @@ from django.core.management import call_command
|
|||
|
||||
from django_components import Component
|
||||
from django_components.testing import djc_test
|
||||
|
||||
from .testutils import setup_test_config
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
@ -41,7 +43,7 @@ class TestComponentListCommand:
|
|||
# Check first line of output
|
||||
assert re.compile(
|
||||
# full_name path
|
||||
r"full_name\s+path\s+"
|
||||
r"full_name\s+path\s+",
|
||||
).search(output.strip().split("\n")[0])
|
||||
|
||||
# Check that the output contains the built-in component
|
||||
|
@ -49,17 +51,17 @@ class TestComponentListCommand:
|
|||
# django_components.components.dynamic.DynamicComponent src/django_components/components/dynamic.py
|
||||
# or
|
||||
# django_components.components.dynamic.DynamicComponent .tox/py311/lib/python3.11/site-packages/django_components/components/dynamic.py
|
||||
r"django_components\.components\.dynamic\.DynamicComponent\s+[\w/\\.-]+django_components{SLASH}components{SLASH}dynamic\.py".format(
|
||||
SLASH=SLASH
|
||||
)
|
||||
r"django_components\.components\.dynamic\.DynamicComponent\s+[\w/\\.-]+django_components{SLASH}components{SLASH}dynamic\.py".format( # noqa: UP032
|
||||
SLASH=SLASH,
|
||||
),
|
||||
).search(output)
|
||||
|
||||
# Check that the output contains the test component
|
||||
assert re.compile(
|
||||
# tests.test_command_list.TestComponentListCommand.test_list_default.<locals>.TestComponent tests/test_command_list.py
|
||||
r"tests\.test_command_list\.TestComponentListCommand\.test_list_default\.<locals>\.TestComponent\s+tests{SLASH}test_command_list\.py".format(
|
||||
SLASH=SLASH
|
||||
)
|
||||
r"tests\.test_command_list\.TestComponentListCommand\.test_list_default\.<locals>\.TestComponent\s+tests{SLASH}test_command_list\.py".format( # noqa: UP032
|
||||
SLASH=SLASH,
|
||||
),
|
||||
).search(output)
|
||||
|
||||
def test_list_all(self):
|
||||
|
@ -86,7 +88,7 @@ class TestComponentListCommand:
|
|||
# Check first line of output
|
||||
assert re.compile(
|
||||
# name full_name path
|
||||
r"name\s+full_name\s+path\s+"
|
||||
r"name\s+full_name\s+path\s+",
|
||||
).search(output.strip().split("\n")[0])
|
||||
|
||||
# Check that the output contains the built-in component
|
||||
|
@ -94,17 +96,17 @@ class TestComponentListCommand:
|
|||
# DynamicComponent django_components.components.dynamic.DynamicComponent src/django_components/components/dynamic.py
|
||||
# or
|
||||
# DynamicComponent django_components.components.dynamic.DynamicComponent .tox/py311/lib/python3.11/site-packages/django_components/components/dynamic.py
|
||||
r"DynamicComponent\s+django_components\.components\.dynamic\.DynamicComponent\s+[\w/\\.-]+django_components{SLASH}components{SLASH}dynamic\.py".format(
|
||||
SLASH=SLASH
|
||||
)
|
||||
r"DynamicComponent\s+django_components\.components\.dynamic\.DynamicComponent\s+[\w/\\.-]+django_components{SLASH}components{SLASH}dynamic\.py".format( # noqa: UP032
|
||||
SLASH=SLASH,
|
||||
),
|
||||
).search(output)
|
||||
|
||||
# Check that the output contains the test component
|
||||
assert re.compile(
|
||||
# TestComponent tests.test_command_list.TestComponentListCommand.test_list_all.<locals>.TestComponent tests/test_command_list.py
|
||||
r"TestComponent\s+tests\.test_command_list\.TestComponentListCommand\.test_list_all\.<locals>\.TestComponent\s+tests{SLASH}test_command_list\.py".format(
|
||||
SLASH=SLASH
|
||||
)
|
||||
r"TestComponent\s+tests\.test_command_list\.TestComponentListCommand\.test_list_all\.<locals>\.TestComponent\s+tests{SLASH}test_command_list\.py".format( # noqa: UP032
|
||||
SLASH=SLASH,
|
||||
),
|
||||
).search(output)
|
||||
|
||||
def test_list_specific_columns(self):
|
||||
|
@ -131,19 +133,19 @@ class TestComponentListCommand:
|
|||
# Check first line of output
|
||||
assert re.compile(
|
||||
# name full_name
|
||||
r"name\s+full_name"
|
||||
r"name\s+full_name",
|
||||
).search(output.strip().split("\n")[0])
|
||||
|
||||
# Check that the output contains the built-in component
|
||||
assert re.compile(
|
||||
# DynamicComponent django_components.components.dynamic.DynamicComponent
|
||||
r"DynamicComponent\s+django_components\.components\.dynamic\.DynamicComponent"
|
||||
r"DynamicComponent\s+django_components\.components\.dynamic\.DynamicComponent",
|
||||
).search(output)
|
||||
|
||||
# Check that the output contains the test component
|
||||
assert re.compile(
|
||||
# TestComponent tests.test_command_list.TestComponentListCommand.test_list_specific_columns.<locals>.TestComponent
|
||||
r"TestComponent\s+tests\.test_command_list\.TestComponentListCommand\.test_list_specific_columns\.<locals>\.TestComponent"
|
||||
r"TestComponent\s+tests\.test_command_list\.TestComponentListCommand\.test_list_specific_columns\.<locals>\.TestComponent",
|
||||
).search(output)
|
||||
|
||||
def test_list_simple(self):
|
||||
|
@ -166,25 +168,28 @@ class TestComponentListCommand:
|
|||
# tests.test_command_list.TestComponentListCommand.test_list_simple.<locals>.TestComponent tests/test_command_list.py
|
||||
|
||||
# Check first line of output is omitted
|
||||
assert re.compile(
|
||||
# full_name path
|
||||
r"full_name\s+path\s+"
|
||||
).search(output.strip().split("\n")[0]) is None
|
||||
assert (
|
||||
re.compile(
|
||||
# full_name path
|
||||
r"full_name\s+path\s+",
|
||||
).search(output.strip().split("\n")[0])
|
||||
is None
|
||||
)
|
||||
|
||||
# Check that the output contains the built-in component
|
||||
assert re.compile(
|
||||
# django_components.components.dynamic.DynamicComponent src/django_components/components/dynamic.py
|
||||
# or
|
||||
# django_components.components.dynamic.DynamicComponent .tox/py311/lib/python3.11/site-packages/django_components/components/dynamic.py
|
||||
r"django_components\.components\.dynamic\.DynamicComponent\s+[\w/\\.-]+django_components{SLASH}components{SLASH}dynamic\.py".format(
|
||||
SLASH=SLASH
|
||||
)
|
||||
r"django_components\.components\.dynamic\.DynamicComponent\s+[\w/\\.-]+django_components{SLASH}components{SLASH}dynamic\.py".format( # noqa: UP032
|
||||
SLASH=SLASH,
|
||||
),
|
||||
).search(output)
|
||||
|
||||
# Check that the output contains the test component
|
||||
assert re.compile(
|
||||
# tests.test_command_list.TestComponentListCommand.test_list_simple.<locals>.TestComponent tests/test_command_list.py
|
||||
r"tests\.test_command_list\.TestComponentListCommand\.test_list_simple\.<locals>\.TestComponent\s+tests{SLASH}test_command_list\.py".format(
|
||||
SLASH=SLASH
|
||||
)
|
||||
r"tests\.test_command_list\.TestComponentListCommand\.test_list_simple\.<locals>\.TestComponent\s+tests{SLASH}test_command_list\.py".format( # noqa: UP032
|
||||
SLASH=SLASH,
|
||||
),
|
||||
).search(output)
|
||||
|
|
|
@ -29,9 +29,9 @@ from django_components import (
|
|||
types,
|
||||
)
|
||||
from django_components.template import _get_component_template
|
||||
from django_components.testing import djc_test
|
||||
from django_components.urls import urlpatterns as dc_urlpatterns
|
||||
|
||||
from django_components.testing import djc_test
|
||||
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
@ -44,11 +44,11 @@ class CustomClient(Client):
|
|||
|
||||
if urlpatterns:
|
||||
urls_module = types.ModuleType("urls")
|
||||
urls_module.urlpatterns = urlpatterns + dc_urlpatterns # type: ignore
|
||||
urls_module.urlpatterns = urlpatterns + dc_urlpatterns # type: ignore[attr-defined]
|
||||
settings.ROOT_URLCONF = urls_module
|
||||
else:
|
||||
settings.ROOT_URLCONF = __name__
|
||||
settings.SECRET_KEY = "secret" # noqa
|
||||
settings.SECRET_KEY = "secret" # noqa: S105
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
|
@ -294,6 +294,7 @@ class TestComponentLegacyApi:
|
|||
""",
|
||||
)
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestComponent:
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
|
@ -365,19 +366,21 @@ class TestComponent:
|
|||
"builtins": [
|
||||
"django_components.templatetags.component_tags",
|
||||
],
|
||||
'loaders': [
|
||||
('django.template.loaders.cached.Loader', [
|
||||
|
||||
# Default Django loader
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
# Including this is the same as APP_DIRS=True
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
# Components loader
|
||||
'django_components.template_loader.Loader',
|
||||
]),
|
||||
"loaders": [
|
||||
(
|
||||
"django.template.loaders.cached.Loader",
|
||||
[
|
||||
# Default Django loader
|
||||
"django.template.loaders.filesystem.Loader",
|
||||
# Including this is the same as APP_DIRS=True
|
||||
"django.template.loaders.app_directories.Loader",
|
||||
# Components loader
|
||||
"django_components.template_loader.Loader",
|
||||
],
|
||||
),
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
@ -398,8 +401,8 @@ class TestComponent:
|
|||
"variable": kwargs.get("variable", None),
|
||||
}
|
||||
|
||||
SimpleComponent1.template # Triggers template loading
|
||||
SimpleComponent2.template # Triggers template loading
|
||||
_ = SimpleComponent1.template # Triggers template loading
|
||||
_ = SimpleComponent2.template # Triggers template loading
|
||||
|
||||
# Both components have their own Template instance, but they point to the same template file.
|
||||
assert isinstance(SimpleComponent1._template, Template)
|
||||
|
@ -692,7 +695,7 @@ class TestComponentRenderAPI:
|
|||
|
||||
rendered = Outer.render()
|
||||
|
||||
assert rendered == 'hello'
|
||||
assert rendered == "hello"
|
||||
|
||||
assert isinstance(comp, TestComponent)
|
||||
|
||||
|
@ -706,9 +709,9 @@ class TestComponentRenderAPI:
|
|||
assert comp.node.template_component == Outer
|
||||
|
||||
if os.name == "nt":
|
||||
assert comp.node.template_name.endswith("tests\\test_component.py::Outer") # type: ignore
|
||||
assert comp.node.template_name.endswith("tests\\test_component.py::Outer") # type: ignore[union-attr]
|
||||
else:
|
||||
assert comp.node.template_name.endswith("tests/test_component.py::Outer") # type: ignore
|
||||
assert comp.node.template_name.endswith("tests/test_component.py::Outer") # type: ignore[union-attr]
|
||||
|
||||
def test_metadata__python(self):
|
||||
comp: Any = None
|
||||
|
@ -734,7 +737,7 @@ class TestComponentRenderAPI:
|
|||
registered_name="test",
|
||||
)
|
||||
|
||||
assert rendered == 'hello'
|
||||
assert rendered == "hello"
|
||||
|
||||
assert isinstance(comp, TestComponent)
|
||||
|
||||
|
@ -1463,7 +1466,7 @@ class TestComponentRender:
|
|||
TypeError,
|
||||
match=re.escape(
|
||||
"An error occured while rendering components Root > parent > provider > provider(slot:content) > broken:\n" # noqa: E501
|
||||
"tuple indices must be integers or slices, not str"
|
||||
"tuple indices must be integers or slices, not str",
|
||||
),
|
||||
):
|
||||
Root.render()
|
||||
|
@ -1810,40 +1813,41 @@ class TestComponentHook:
|
|||
parametrize=(
|
||||
["template", "action", "method"],
|
||||
[
|
||||
# on_render - return None
|
||||
["simple", "return_none", "on_render"],
|
||||
["broken", "return_none", "on_render"],
|
||||
[None, "return_none", "on_render"],
|
||||
|
||||
# on_render_after - return None
|
||||
["simple", "return_none", "on_render_after"],
|
||||
["broken", "return_none", "on_render_after"],
|
||||
[None, "return_none", "on_render_after"],
|
||||
|
||||
# on_render - no return
|
||||
["simple", "no_return", "on_render"],
|
||||
["broken", "no_return", "on_render"],
|
||||
[None, "no_return", "on_render"],
|
||||
|
||||
# on_render_after - no return
|
||||
["simple", "no_return", "on_render_after"],
|
||||
["broken", "no_return", "on_render_after"],
|
||||
[None, "no_return", "on_render_after"],
|
||||
|
||||
# on_render - raise error
|
||||
["simple", "raise_error", "on_render"],
|
||||
["broken", "raise_error", "on_render"],
|
||||
[None, "raise_error", "on_render"],
|
||||
|
||||
# on_render_after - raise error
|
||||
["simple", "raise_error", "on_render_after"],
|
||||
["broken", "raise_error", "on_render_after"],
|
||||
[None, "raise_error", "on_render_after"],
|
||||
|
||||
# on_render - return html
|
||||
["simple", "return_html", "on_render"],
|
||||
["broken", "return_html", "on_render"],
|
||||
[None, "return_html", "on_render"],
|
||||
|
||||
# on_render_after - return html
|
||||
["simple", "return_html", "on_render_after"],
|
||||
["broken", "return_html", "on_render_after"],
|
||||
[None, "return_html", "on_render_after"],
|
||||
],
|
||||
None
|
||||
)
|
||||
None,
|
||||
),
|
||||
)
|
||||
def test_result_interception(
|
||||
self,
|
||||
|
@ -1863,11 +1867,13 @@ class TestComponentHook:
|
|||
|
||||
# Set template
|
||||
if template is None:
|
||||
class Inner(Inner): # type: ignore
|
||||
|
||||
class Inner(Inner): # type: ignore # noqa: PGH003
|
||||
template = None
|
||||
|
||||
elif template == "broken":
|
||||
class Inner(Inner): # type: ignore
|
||||
|
||||
class Inner(Inner): # type: ignore # noqa: PGH003
|
||||
template = "{% component 'broken' / %}"
|
||||
|
||||
elif template == "simple":
|
||||
|
@ -1876,16 +1882,18 @@ class TestComponentHook:
|
|||
# Set `on_render` behavior
|
||||
if method == "on_render":
|
||||
if action == "return_none":
|
||||
class Inner(Inner): # type: ignore
|
||||
|
||||
class Inner(Inner): # type: ignore # noqa: PGH003
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
if template is None:
|
||||
yield None
|
||||
else:
|
||||
html, error = yield template.render(context)
|
||||
return None
|
||||
return None # noqa: PLR1711
|
||||
|
||||
elif action == "no_return":
|
||||
class Inner(Inner): # type: ignore
|
||||
|
||||
class Inner(Inner): # type: ignore # noqa: PGH003
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
if template is None:
|
||||
yield None
|
||||
|
@ -1893,7 +1901,8 @@ class TestComponentHook:
|
|||
html, error = yield template.render(context)
|
||||
|
||||
elif action == "raise_error":
|
||||
class Inner(Inner): # type: ignore
|
||||
|
||||
class Inner(Inner): # type: ignore # noqa: PGH003
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
if template is None:
|
||||
yield None
|
||||
|
@ -1902,37 +1911,68 @@ class TestComponentHook:
|
|||
raise ValueError("ERROR_FROM_ON_RENDER")
|
||||
|
||||
elif action == "return_html":
|
||||
class Inner(Inner): # type: ignore
|
||||
|
||||
class Inner(Inner): # type: ignore # noqa: PGH003
|
||||
def on_render(self, context: Context, template: Optional[Template]):
|
||||
if template is None:
|
||||
yield None
|
||||
else:
|
||||
html, error = yield template.render(context)
|
||||
return "HTML_FROM_ON_RENDER"
|
||||
|
||||
else:
|
||||
raise pytest.fail(f"Unknown action: {action}")
|
||||
|
||||
# Set `on_render_after` behavior
|
||||
elif method == "on_render_after":
|
||||
if action == "return_none":
|
||||
class Inner(Inner): # type: ignore
|
||||
def on_render_after(self, context: Context, template: Template, html: Optional[str], error: Optional[Exception]): # noqa: E501
|
||||
|
||||
class Inner(Inner): # type: ignore # noqa: PGH003
|
||||
def on_render_after(
|
||||
self,
|
||||
context: Context,
|
||||
template: Template,
|
||||
html: Optional[str],
|
||||
error: Optional[Exception],
|
||||
):
|
||||
return None
|
||||
|
||||
elif action == "no_return":
|
||||
class Inner(Inner): # type: ignore
|
||||
def on_render_after(self, context: Context, template: Template, html: Optional[str], error: Optional[Exception]): # noqa: E501
|
||||
|
||||
class Inner(Inner): # type: ignore # noqa: PGH003
|
||||
def on_render_after(
|
||||
self,
|
||||
context: Context,
|
||||
template: Template,
|
||||
html: Optional[str],
|
||||
error: Optional[Exception],
|
||||
):
|
||||
pass
|
||||
|
||||
elif action == "raise_error":
|
||||
class Inner(Inner): # type: ignore
|
||||
def on_render_after(self, context: Context, template: Template, html: Optional[str], error: Optional[Exception]): # noqa: E501
|
||||
|
||||
class Inner(Inner): # type: ignore # noqa: PGH003
|
||||
def on_render_after(
|
||||
self,
|
||||
context: Context,
|
||||
template: Template,
|
||||
html: Optional[str],
|
||||
error: Optional[Exception],
|
||||
):
|
||||
raise ValueError("ERROR_FROM_ON_RENDER")
|
||||
|
||||
elif action == "return_html":
|
||||
class Inner(Inner): # type: ignore
|
||||
def on_render_after(self, context: Context, template: Template, html: Optional[str], error: Optional[Exception]): # noqa: E501
|
||||
|
||||
class Inner(Inner): # type: ignore # noqa: PGH003
|
||||
def on_render_after(
|
||||
self,
|
||||
context: Context,
|
||||
template: Template,
|
||||
html: Optional[str],
|
||||
error: Optional[Exception],
|
||||
):
|
||||
return "HTML_FROM_ON_RENDER"
|
||||
|
||||
else:
|
||||
raise pytest.fail(f"Unknown action: {action}")
|
||||
else:
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import re
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from django.core.cache import caches
|
||||
from django.template import Template
|
||||
from django.template.context import Context
|
||||
import pytest
|
||||
|
||||
from django_components import Component, register
|
||||
from django_components.testing import djc_test
|
||||
|
@ -209,7 +209,6 @@ class TestComponentCache:
|
|||
assert component.cache.get_entry(expected_key) == "<!-- _RENDERED TestComponent_28880f,ca1bc3f,, -->Hello"
|
||||
|
||||
def test_cached_component_inside_include(self):
|
||||
|
||||
@register("test_component")
|
||||
class TestComponent(Component):
|
||||
template = "Hello"
|
||||
|
@ -223,7 +222,7 @@ class TestComponentCache:
|
|||
{% block content %}
|
||||
THIS_IS_IN_ACTUAL_TEMPLATE_SO_SHOULD_NOT_BE_OVERRIDDEN
|
||||
{% endblock %}
|
||||
"""
|
||||
""",
|
||||
)
|
||||
|
||||
result = template.render(Context({}))
|
||||
|
@ -251,7 +250,7 @@ class TestComponentCache:
|
|||
{% component "test_component" input="cake" %}
|
||||
ONE
|
||||
{% endcomponent %}
|
||||
"""
|
||||
""",
|
||||
).render(Context({}))
|
||||
|
||||
Template(
|
||||
|
@ -259,7 +258,7 @@ class TestComponentCache:
|
|||
{% component "test_component" input="cake" %}
|
||||
ONE
|
||||
{% endcomponent %}
|
||||
"""
|
||||
""",
|
||||
).render(Context({}))
|
||||
|
||||
# Check if the cache entry is set
|
||||
|
@ -277,7 +276,7 @@ class TestComponentCache:
|
|||
{% component "test_component" input="cake" %}
|
||||
TWO
|
||||
{% endcomponent %}
|
||||
"""
|
||||
""",
|
||||
).render(Context({}))
|
||||
|
||||
assert len(cache._cache) == 2
|
||||
|
@ -339,12 +338,12 @@ class TestComponentCache:
|
|||
return {"input": kwargs["input"]}
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
TypeError,
|
||||
match=re.escape(
|
||||
"Cannot hash slot 'content' of component 'TestComponent' - Slot functions are unhashable."
|
||||
"Cannot hash slot 'content' of component 'TestComponent' - Slot functions are unhashable.",
|
||||
),
|
||||
):
|
||||
TestComponent.render(
|
||||
kwargs={"input": "cake"},
|
||||
slots={"content": lambda ctx: "ONE"},
|
||||
slots={"content": lambda _ctx: "ONE"},
|
||||
)
|
||||
|
|
|
@ -3,8 +3,8 @@ from dataclasses import field
|
|||
from django.template import Context
|
||||
|
||||
from django_components import Component, Default
|
||||
|
||||
from django_components.testing import djc_test
|
||||
|
||||
from .testutils import setup_test_config
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
|
|
@ -2,8 +2,9 @@ from django.template import Context, Template
|
|||
from pytest_django.asserts import assertHTMLEqual
|
||||
|
||||
from django_components import Component, register, types
|
||||
from django_components.extensions.debug_highlight import apply_component_highlight, COLORS
|
||||
from django_components.extensions.debug_highlight import COLORS, apply_component_highlight
|
||||
from django_components.testing import djc_test
|
||||
|
||||
from .testutils import setup_test_config
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
@ -87,7 +88,7 @@ class TestComponentHighlight:
|
|||
"extensions_defaults": {
|
||||
"debug_highlight": {"highlight_components": True},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
def test_component_highlight_extension(self):
|
||||
template = _prepare_template()
|
||||
|
@ -232,7 +233,7 @@ class TestComponentHighlight:
|
|||
"extensions_defaults": {
|
||||
"debug_highlight": {"highlight_slots": True},
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
def test_slot_highlight_extension(self):
|
||||
template = _prepare_template()
|
||||
|
@ -399,12 +400,14 @@ class TestComponentHighlight:
|
|||
highlight_components = True
|
||||
highlight_slots = True
|
||||
|
||||
template = Template("""
|
||||
template = Template(
|
||||
"""
|
||||
{% load component_tags %}
|
||||
{% component "inner" %}
|
||||
{{ content }}
|
||||
{% endcomponent %}
|
||||
""")
|
||||
""",
|
||||
)
|
||||
rendered = template.render(Context({"content": "Hello, world!"}))
|
||||
|
||||
expected = """
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
# ruff: noqa: E501
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from textwrap import dedent
|
||||
from typing import Optional
|
||||
from typing import List, Optional
|
||||
|
||||
import pytest
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
|
@ -15,8 +16,8 @@ from django.utils.safestring import mark_safe
|
|||
from pytest_django.asserts import assertHTMLEqual, assertInHTML
|
||||
|
||||
from django_components import Component, autodiscover, registry, render_dependencies, types
|
||||
|
||||
from django_components.testing import djc_test
|
||||
|
||||
from .testutils import setup_test_config
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
@ -34,7 +35,7 @@ class TestMainMedia:
|
|||
{% component_js_dependencies %}
|
||||
{% component_css_dependencies %}
|
||||
<div class='html-css-only'>Content</div>
|
||||
"""
|
||||
""",
|
||||
)
|
||||
css = ".html-css-only { color: blue; }"
|
||||
js = "console.log('HTML and JS only');"
|
||||
|
@ -64,7 +65,7 @@ class TestMainMedia:
|
|||
{% component_js_dependencies %}
|
||||
{% component_css_dependencies %}
|
||||
<div class='html-css-only'>Content</div>
|
||||
"""
|
||||
""",
|
||||
)
|
||||
assert TestComponent.css == ".html-css-only { color: blue; }"
|
||||
assert TestComponent.js == "console.log('HTML and JS only');"
|
||||
|
@ -75,9 +76,9 @@ class TestMainMedia:
|
|||
@djc_test(
|
||||
django_settings={
|
||||
"STATICFILES_DIRS": [
|
||||
os.path.join(Path(__file__).resolve().parent, "static_root"),
|
||||
Path(__file__).resolve().parent / "static_root",
|
||||
],
|
||||
}
|
||||
},
|
||||
)
|
||||
def test_html_js_css_filepath_rel_to_component(self):
|
||||
from tests.test_app.components.app_lvl_comp.app_lvl_comp import AppLvlCompComponent
|
||||
|
@ -96,7 +97,7 @@ class TestMainMedia:
|
|||
{% component_js_dependencies %}
|
||||
{% component_css_dependencies %}
|
||||
{% component "test" variable="test" / %}
|
||||
"""
|
||||
""",
|
||||
).render(Context())
|
||||
|
||||
assertInHTML(
|
||||
|
@ -135,9 +136,9 @@ class TestMainMedia:
|
|||
@djc_test(
|
||||
django_settings={
|
||||
"STATICFILES_DIRS": [
|
||||
os.path.join(Path(__file__).resolve().parent, "static_root"),
|
||||
Path(__file__).resolve().parent / "static_root",
|
||||
],
|
||||
}
|
||||
},
|
||||
)
|
||||
def test_html_js_css_filepath_from_static(self):
|
||||
class TestComponent(Component):
|
||||
|
@ -165,7 +166,7 @@ class TestMainMedia:
|
|||
{% component_js_dependencies %}
|
||||
{% component_css_dependencies %}
|
||||
{% component "test" variable="test" / %}
|
||||
"""
|
||||
""",
|
||||
).render(Context())
|
||||
|
||||
assert 'Variable: <strong data-djc-id-ca1bc41="">test</strong>' in rendered
|
||||
|
@ -180,12 +181,14 @@ class TestMainMedia:
|
|||
|
||||
# Check that the HTML / JS / CSS can be accessed on the component class
|
||||
assert TestComponent.template == "Variable: <strong>{{ variable }}</strong>\n"
|
||||
# fmt: off
|
||||
assert TestComponent.css == (
|
||||
"/* Used in `MainMediaTest` tests in `test_component_media.py` */\n"
|
||||
".html-css-only {\n"
|
||||
" color: blue;\n"
|
||||
"}"
|
||||
)
|
||||
# fmt: on
|
||||
assert TestComponent.js == (
|
||||
'/* Used in `MainMediaTest` tests in `test_component_media.py` */\nconsole.log("HTML and JS only");\n'
|
||||
)
|
||||
|
@ -193,9 +196,9 @@ class TestMainMedia:
|
|||
@djc_test(
|
||||
django_settings={
|
||||
"STATICFILES_DIRS": [
|
||||
os.path.join(Path(__file__).resolve().parent, "static_root"),
|
||||
Path(__file__).resolve().parent / "static_root",
|
||||
],
|
||||
}
|
||||
},
|
||||
)
|
||||
def test_html_js_css_filepath_lazy_loaded(self):
|
||||
from tests.test_app.components.app_lvl_comp.app_lvl_comp import AppLvlCompComponent
|
||||
|
@ -216,7 +219,7 @@ class TestMainMedia:
|
|||
# # Access the property to load the CSS
|
||||
# _ = TestComponent.css
|
||||
|
||||
assert AppLvlCompComponent._component_media.css == (".html-css-only {\n" " color: blue;\n" "}\n") # type: ignore[attr-defined]
|
||||
assert AppLvlCompComponent._component_media.css == (".html-css-only {\n color: blue;\n}\n") # type: ignore[attr-defined]
|
||||
assert AppLvlCompComponent._component_media.css_file == "app_lvl_comp/app_lvl_comp.css" # type: ignore[attr-defined]
|
||||
|
||||
# Also check JS and HTML while we're at it
|
||||
|
@ -341,7 +344,7 @@ class TestComponentMedia:
|
|||
def test_media_custom_render_js(self):
|
||||
class MyMedia(Media):
|
||||
def render_js(self):
|
||||
tags: list[str] = []
|
||||
tags: List[str] = []
|
||||
for path in self._js: # type: ignore[attr-defined]
|
||||
abs_path = self.absolute_path(path) # type: ignore[attr-defined]
|
||||
tags.append(f'<script defer src="{abs_path}"></script>')
|
||||
|
@ -367,7 +370,7 @@ class TestComponentMedia:
|
|||
def test_media_custom_render_css(self):
|
||||
class MyMedia(Media):
|
||||
def render_css(self):
|
||||
tags: list[str] = []
|
||||
tags: List[str] = []
|
||||
media = sorted(self._css) # type: ignore[attr-defined]
|
||||
for medium in media:
|
||||
for path in self._css[medium]: # type: ignore[attr-defined]
|
||||
|
@ -399,7 +402,7 @@ class TestComponentMedia:
|
|||
@djc_test(
|
||||
django_settings={
|
||||
"INSTALLED_APPS": ("django_components", "tests"),
|
||||
}
|
||||
},
|
||||
)
|
||||
def test_glob_pattern_relative_to_component(self):
|
||||
from tests.components.glob.glob import GlobComponent
|
||||
|
@ -414,7 +417,7 @@ class TestComponentMedia:
|
|||
@djc_test(
|
||||
django_settings={
|
||||
"INSTALLED_APPS": ("django_components", "tests"),
|
||||
}
|
||||
},
|
||||
)
|
||||
def test_glob_pattern_relative_to_root_dir(self):
|
||||
from tests.components.glob.glob import GlobComponentRootDir
|
||||
|
@ -429,7 +432,7 @@ class TestComponentMedia:
|
|||
@djc_test(
|
||||
django_settings={
|
||||
"INSTALLED_APPS": ("django_components", "tests"),
|
||||
}
|
||||
},
|
||||
)
|
||||
def test_non_globs_not_modified(self):
|
||||
from tests.components.glob.glob import NonGlobComponentRootDir
|
||||
|
@ -442,7 +445,7 @@ class TestComponentMedia:
|
|||
@djc_test(
|
||||
django_settings={
|
||||
"INSTALLED_APPS": ("django_components", "tests"),
|
||||
}
|
||||
},
|
||||
)
|
||||
def test_non_globs_not_modified_nonexist(self):
|
||||
from tests.components.glob.glob import NonGlobNonexistComponentRootDir
|
||||
|
@ -464,14 +467,17 @@ class TestComponentMedia:
|
|||
assertInHTML('<link href="/path/to/style.css" media="all" rel="stylesheet">', rendered)
|
||||
|
||||
assertInHTML(
|
||||
'<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered
|
||||
'<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>',
|
||||
rendered,
|
||||
)
|
||||
assertInHTML(
|
||||
'<script src="http://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered
|
||||
'<script src="http://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>',
|
||||
rendered,
|
||||
)
|
||||
# `://` is escaped because Django's `Media.absolute_path()` doesn't consider `://` a valid URL
|
||||
assertInHTML(
|
||||
'<script src="%3A//cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>', rendered
|
||||
'<script src="%3A//cdnjs.cloudflare.com/ajax/libs/Chart.js/3.0.2/chart.min.js"></script>',
|
||||
rendered,
|
||||
)
|
||||
assertInHTML('<script src="/path/to/script.js"></script>', rendered)
|
||||
|
||||
|
@ -604,7 +610,7 @@ class TestMediaPathAsObject:
|
|||
"""
|
||||
|
||||
class MyStr(str):
|
||||
pass
|
||||
__slots__ = ()
|
||||
|
||||
class SimpleComponent(Component):
|
||||
template = """
|
||||
|
@ -718,7 +724,7 @@ class TestMediaPathAsObject:
|
|||
@djc_test(
|
||||
django_settings={
|
||||
"STATIC_URL": "static/",
|
||||
}
|
||||
},
|
||||
)
|
||||
def test_works_with_static(self):
|
||||
"""Test that all the different ways of defining media files works with Django's staticfiles"""
|
||||
|
@ -770,20 +776,20 @@ class TestMediaStaticfiles:
|
|||
# NOTE: We don't need STATICFILES_DIRS, because we don't run collectstatic
|
||||
# See https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STATICFILES_DIRS
|
||||
"STATIC_URL": "static/",
|
||||
"STATIC_ROOT": os.path.join(Path(__file__).resolve().parent, "static_root"),
|
||||
"STATIC_ROOT": Path(__file__).resolve().parent / "static_root",
|
||||
# `django.contrib.staticfiles` MUST be installed for staticfiles resolution to work.
|
||||
"INSTALLED_APPS": [
|
||||
"django.contrib.staticfiles",
|
||||
"django_components",
|
||||
],
|
||||
}
|
||||
},
|
||||
)
|
||||
def test_default_static_files_storage(self):
|
||||
"""Test integration with Django's staticfiles app"""
|
||||
|
||||
class MyMedia(Media):
|
||||
def render_js(self):
|
||||
tags: list[str] = []
|
||||
tags: List[str] = []
|
||||
for path in self._js: # type: ignore[attr-defined]
|
||||
abs_path = self.absolute_path(path) # type: ignore[attr-defined]
|
||||
tags.append(f'<script defer src="{abs_path}"></script>')
|
||||
|
@ -818,7 +824,7 @@ class TestMediaStaticfiles:
|
|||
# NOTE: We don't need STATICFILES_DIRS, because we don't run collectstatic
|
||||
# See https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STATICFILES_DIRS
|
||||
"STATIC_URL": "static/",
|
||||
"STATIC_ROOT": os.path.join(Path(__file__).resolve().parent, "static_root"),
|
||||
"STATIC_ROOT": Path(__file__).resolve().parent / "static_root",
|
||||
# NOTE: STATICFILES_STORAGE is deprecated since 5.1, use STORAGES instead
|
||||
# See https://docs.djangoproject.com/en/5.2/ref/settings/#storages
|
||||
"STORAGES": {
|
||||
|
@ -836,14 +842,14 @@ class TestMediaStaticfiles:
|
|||
"django.contrib.staticfiles",
|
||||
"django_components",
|
||||
],
|
||||
}
|
||||
},
|
||||
)
|
||||
def test_manifest_static_files_storage(self):
|
||||
"""Test integration with Django's staticfiles app and ManifestStaticFilesStorage"""
|
||||
|
||||
class MyMedia(Media):
|
||||
def render_js(self):
|
||||
tags: list[str] = []
|
||||
tags: List[str] = []
|
||||
for path in self._js: # type: ignore[attr-defined]
|
||||
abs_path = self.absolute_path(path) # type: ignore[attr-defined]
|
||||
tags.append(f'<script defer src="{abs_path}"></script>')
|
||||
|
@ -889,7 +895,7 @@ class TestMediaRelativePath:
|
|||
{% endcomponent %}
|
||||
{% endslot %}
|
||||
</div>
|
||||
""" # noqa
|
||||
"""
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"shadowing_variable": "NOT SHADOWED"}
|
||||
|
@ -921,7 +927,7 @@ class TestMediaRelativePath:
|
|||
"STATICFILES_DIRS": [
|
||||
Path(__file__).resolve().parent / "components",
|
||||
],
|
||||
}
|
||||
},
|
||||
)
|
||||
def test_component_with_relative_media_paths(self):
|
||||
registry.register(name="parent_component", component=self._gen_parent_component())
|
||||
|
@ -973,7 +979,7 @@ class TestMediaRelativePath:
|
|||
"STATICFILES_DIRS": [
|
||||
Path(__file__).resolve().parent / "components",
|
||||
],
|
||||
}
|
||||
},
|
||||
)
|
||||
def test_component_with_relative_media_paths_as_subcomponent(self):
|
||||
registry.register(name="parent_component", component=self._gen_parent_component())
|
||||
|
@ -1010,7 +1016,7 @@ class TestMediaRelativePath:
|
|||
"STATICFILES_DIRS": [
|
||||
Path(__file__).resolve().parent / "components",
|
||||
],
|
||||
}
|
||||
},
|
||||
)
|
||||
def test_component_with_relative_media_does_not_trigger_safestring_path_at__new__(self):
|
||||
"""
|
||||
|
@ -1036,8 +1042,8 @@ class TestMediaRelativePath:
|
|||
# Mark the PathObj instances of 'relative_file_pathobj_component' so they won't raise
|
||||
# error if PathObj.__str__ is triggered.
|
||||
CompCls = registry.get("relative_file_pathobj_component")
|
||||
CompCls.Media.js[0].throw_on_calling_str = False # type: ignore
|
||||
CompCls.Media.css["all"][0].throw_on_calling_str = False # type: ignore
|
||||
CompCls.Media.js[0].throw_on_calling_str = False # type: ignore # noqa: PGH003
|
||||
CompCls.Media.css["all"][0].throw_on_calling_str = False # type: ignore # noqa: PGH003
|
||||
|
||||
rendered = CompCls.render(kwargs={"variable": "abc"})
|
||||
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import NamedTuple, Optional
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
import pytest
|
||||
from django.template import Context
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
from django_components import Component, Empty, Slot, SlotInput
|
||||
|
||||
from django_components.testing import djc_test
|
||||
|
||||
from .testutils import setup_test_config
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
@ -76,7 +76,7 @@ class TestComponentTyping:
|
|||
kwargs=Button.Kwargs(name="name", age=123),
|
||||
slots=Button.Slots(
|
||||
header="HEADER",
|
||||
footer=Slot(lambda ctx: "FOOTER"),
|
||||
footer=Slot(lambda _ctx: "FOOTER"),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -124,7 +124,7 @@ class TestComponentTyping:
|
|||
kwargs={"name": "name", "age": 123},
|
||||
slots={
|
||||
"header": "HEADER",
|
||||
"footer": Slot(lambda ctx: "FOOTER"),
|
||||
"footer": Slot(lambda _ctx: "FOOTER"),
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -206,7 +206,7 @@ class TestComponentTyping:
|
|||
kwargs={"name": "name", "age": 123},
|
||||
slots={
|
||||
"header": "HEADER",
|
||||
"footer": Slot(lambda ctx: "FOOTER"),
|
||||
"footer": Slot(lambda _ctx: "FOOTER"),
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -314,7 +314,7 @@ class TestComponentTyping:
|
|||
kwargs={"name": "name", "age": 123},
|
||||
slots={
|
||||
"header": "HEADER",
|
||||
"footer": Slot(lambda ctx: "FOOTER"),
|
||||
"footer": Slot(lambda _ctx: "FOOTER"),
|
||||
},
|
||||
)
|
||||
|
||||
|
@ -397,7 +397,7 @@ class TestComponentTyping:
|
|||
kwargs=Button.Kwargs(name="name", age=123),
|
||||
slots=Button.Slots(
|
||||
header="HEADER",
|
||||
footer=Slot(lambda ctx: "FOOTER"),
|
||||
footer=Slot(lambda _ctx: "FOOTER"),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -412,7 +412,7 @@ class TestComponentTyping:
|
|||
kwargs=Button.Kwargs(name="name", age=123),
|
||||
slots=Button.Slots(
|
||||
header="HEADER",
|
||||
footer=Slot(lambda ctx: "FOOTER"),
|
||||
footer=Slot(lambda _ctx: "FOOTER"),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -427,7 +427,7 @@ class TestComponentTyping:
|
|||
kwargs=Button.Kwargs(age=123), # type: ignore[call-arg]
|
||||
slots=Button.Slots(
|
||||
header="HEADER",
|
||||
footer=Slot(lambda ctx: "FOOTER"),
|
||||
footer=Slot(lambda _ctx: "FOOTER"),
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -438,7 +438,7 @@ class TestComponentTyping:
|
|||
args=Button.Args(arg1="arg1", arg2="arg2"),
|
||||
kwargs=Button.Kwargs(name="name", age=123),
|
||||
slots=Button.Slots( # type: ignore[typeddict-item]
|
||||
footer=Slot(lambda ctx: "FOOTER"), # Missing header
|
||||
footer=Slot(lambda _ctx: "FOOTER"), # Missing header
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
@ -4,16 +4,18 @@ import pytest
|
|||
from django.conf import settings
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.template import Context, Template
|
||||
from django.test import Client, SimpleTestCase
|
||||
from django.test import Client
|
||||
from django.urls import path
|
||||
from pytest_django.asserts import assertInHTML
|
||||
|
||||
from django_components import Component, ComponentView, get_component_url, register, types
|
||||
from django_components.testing import djc_test
|
||||
from django_components.urls import urlpatterns as dc_urlpatterns
|
||||
from django_components.util.misc import format_url
|
||||
|
||||
from django_components.testing import djc_test
|
||||
from .testutils import setup_test_config
|
||||
|
||||
|
||||
# DO NOT REMOVE!
|
||||
#
|
||||
# This is intentionally defined before `setup_test_config()` in order to test that
|
||||
|
@ -40,16 +42,16 @@ class CustomClient(Client):
|
|||
|
||||
if urlpatterns:
|
||||
urls_module = types.ModuleType("urls")
|
||||
urls_module.urlpatterns = urlpatterns + dc_urlpatterns # type: ignore
|
||||
urls_module.urlpatterns = urlpatterns + dc_urlpatterns # type: ignore[attr-defined]
|
||||
settings.ROOT_URLCONF = urls_module
|
||||
else:
|
||||
settings.ROOT_URLCONF = __name__
|
||||
settings.SECRET_KEY = "secret" # noqa
|
||||
settings.SECRET_KEY = "secret" # noqa: S105
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
@djc_test
|
||||
class TestComponentAsView(SimpleTestCase):
|
||||
class TestComponentAsView:
|
||||
def test_render_component_from_template(self):
|
||||
@register("testcomponent")
|
||||
class MockComponentRequest(Component):
|
||||
|
@ -64,19 +66,19 @@ class TestComponentAsView(SimpleTestCase):
|
|||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"variable": kwargs["variable"]}
|
||||
|
||||
def render_template_view(request):
|
||||
def render_template_view(_request):
|
||||
template = Template(
|
||||
"""
|
||||
{% load component_tags %}
|
||||
{% component "testcomponent" variable="TEMPLATE" %}{% endcomponent %}
|
||||
"""
|
||||
""",
|
||||
)
|
||||
return HttpResponse(template.render(Context({})))
|
||||
|
||||
client = CustomClient(urlpatterns=[path("test_template/", render_template_view)])
|
||||
response = client.get("/test_template/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertInHTML(
|
||||
assert response.status_code == 200
|
||||
assertInHTML(
|
||||
'<input type="text" name="variable" value="TEMPLATE">',
|
||||
response.content.decode(),
|
||||
)
|
||||
|
@ -100,8 +102,8 @@ class TestComponentAsView(SimpleTestCase):
|
|||
|
||||
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
|
||||
response = client.get("/test/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertInHTML(
|
||||
assert response.status_code == 200
|
||||
assertInHTML(
|
||||
'<input type="text" name="variable" value="GET">',
|
||||
response.content.decode(),
|
||||
)
|
||||
|
@ -124,8 +126,8 @@ class TestComponentAsView(SimpleTestCase):
|
|||
|
||||
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
|
||||
response = client.get("/test/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertInHTML(
|
||||
assert response.status_code == 200
|
||||
assertInHTML(
|
||||
'<input type="text" name="variable" value="GET">',
|
||||
response.content.decode(),
|
||||
)
|
||||
|
@ -150,8 +152,8 @@ class TestComponentAsView(SimpleTestCase):
|
|||
|
||||
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
|
||||
response = client.post("/test/", {"variable": "POST"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertInHTML(
|
||||
assert response.status_code == 200
|
||||
assertInHTML(
|
||||
'<input type="text" name="variable" value="POST">',
|
||||
response.content.decode(),
|
||||
)
|
||||
|
@ -175,8 +177,8 @@ class TestComponentAsView(SimpleTestCase):
|
|||
|
||||
client = CustomClient(urlpatterns=[path("test/", MockComponentRequest.as_view())])
|
||||
response = client.post("/test/", {"variable": "POST"})
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertInHTML(
|
||||
assert response.status_code == 200
|
||||
assertInHTML(
|
||||
'<input type="text" name="variable" value="POST">',
|
||||
response.content.decode(),
|
||||
)
|
||||
|
@ -198,8 +200,8 @@ class TestComponentAsView(SimpleTestCase):
|
|||
view = MockComponentRequest.as_view()
|
||||
client = CustomClient(urlpatterns=[path("test/", view)])
|
||||
response = client.get("/test/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertInHTML(
|
||||
assert response.status_code == 200
|
||||
assertInHTML(
|
||||
'<input type="text" name="variable" value="MockComponentRequest">',
|
||||
response.content.decode(),
|
||||
)
|
||||
|
@ -225,15 +227,9 @@ class TestComponentAsView(SimpleTestCase):
|
|||
|
||||
client = CustomClient(urlpatterns=[path("test_slot/", MockComponentSlot.as_view())])
|
||||
response = client.get("/test_slot/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(
|
||||
b"Hey, I'm Bob",
|
||||
response.content,
|
||||
)
|
||||
self.assertIn(
|
||||
b"Nice to meet you, Bob",
|
||||
response.content,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert b"Hey, I'm Bob" in response.content
|
||||
assert b"Nice to meet you, Bob" in response.content
|
||||
|
||||
def test_replace_slot_in_view_with_insecure_content(self):
|
||||
class MockInsecureComponentSlot(Component):
|
||||
|
@ -253,11 +249,8 @@ class TestComponentAsView(SimpleTestCase):
|
|||
|
||||
client = CustomClient(urlpatterns=[path("test_slot_insecure/", MockInsecureComponentSlot.as_view())])
|
||||
response = client.get("/test_slot_insecure/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn(
|
||||
b"<script>",
|
||||
response.content,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert b"<script>" not in response.content
|
||||
|
||||
def test_replace_context_in_view(self):
|
||||
class TestComponent(Component):
|
||||
|
@ -273,11 +266,8 @@ class TestComponentAsView(SimpleTestCase):
|
|||
|
||||
client = CustomClient(urlpatterns=[path("test_context_django/", TestComponent.as_view())])
|
||||
response = client.get("/test_context_django/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(
|
||||
b"Hey, I'm Bob",
|
||||
response.content,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert b"Hey, I'm Bob" in response.content
|
||||
|
||||
def test_replace_context_in_view_with_insecure_content(self):
|
||||
class MockInsecureComponentContext(Component):
|
||||
|
@ -293,11 +283,8 @@ class TestComponentAsView(SimpleTestCase):
|
|||
|
||||
client = CustomClient(urlpatterns=[path("test_context_insecure/", MockInsecureComponentContext.as_view())])
|
||||
response = client.get("/test_context_insecure/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertNotIn(
|
||||
b"<script>",
|
||||
response.content,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert b"<script>" not in response.content
|
||||
|
||||
def test_component_url(self):
|
||||
class TestComponent(Component):
|
||||
|
@ -315,13 +302,16 @@ class TestComponentAsView(SimpleTestCase):
|
|||
|
||||
# Check that the query and fragment are correctly escaped
|
||||
component_url3 = get_component_url(TestComponent, query={"f'oo": "b ar&ba'z"}, fragment='q u"x')
|
||||
assert component_url3 == f"/components/ext/view/components/{TestComponent.class_id}/?f%27oo=b+ar%26ba%27z#q%20u%22x" # noqa: E501
|
||||
assert (
|
||||
component_url3
|
||||
== f"/components/ext/view/components/{TestComponent.class_id}/?f%27oo=b+ar%26ba%27z#q%20u%22x"
|
||||
)
|
||||
|
||||
# Merges query params from original URL
|
||||
component_url4 = format_url(
|
||||
"/components/ext/view/components/123?foo=123&bar=456#abc",
|
||||
query={"foo": "new", "baz": "new2"},
|
||||
fragment='xyz',
|
||||
fragment="xyz",
|
||||
)
|
||||
assert component_url4 == "/components/ext/view/components/123?foo=new&bar=456&baz=new2#xyz"
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@ from django.template import Context, RequestContext, Template
|
|||
from pytest_django.asserts import assertHTMLEqual, assertInHTML
|
||||
|
||||
from django_components import Component, register, registry, types
|
||||
from django_components.testing import djc_test
|
||||
from django_components.util.misc import gen_id
|
||||
|
||||
from django_components.testing import djc_test
|
||||
from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config
|
||||
|
||||
setup_test_config({"autodiscover": False})
|
||||
|
@ -15,7 +15,7 @@ setup_test_config({"autodiscover": False})
|
|||
|
||||
# Context processor that generates a unique ID. This is used to test that the context
|
||||
# processor is generated only once, as each time this is called, it should generate a different ID.
|
||||
def dummy_context_processor(request):
|
||||
def dummy_context_processor(request): # noqa: ARG001
|
||||
return {"dummy": gen_id()}
|
||||
|
||||
|
||||
|
@ -94,7 +94,7 @@ def gen_parent_component():
|
|||
{% endcomponent %}
|
||||
{% endslot %}
|
||||
</div>
|
||||
""" # noqa
|
||||
""" # noqa: E501
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"shadowing_variable": "NOT SHADOWED"}
|
||||
|
@ -118,7 +118,7 @@ def gen_parent_component_with_args():
|
|||
{% endcomponent %}
|
||||
{% endslot %}
|
||||
</div>
|
||||
""" # noqa
|
||||
""" # noqa: E501
|
||||
|
||||
def get_template_data(self, args, kwargs, slots, context):
|
||||
return {"inner_parent_value": kwargs["parent_value"]}
|
||||
|
@ -135,7 +135,8 @@ def gen_parent_component_with_args():
|
|||
class TestContext:
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_nested_component_context_shadows_parent_with_unfilled_slots_and_component_tag(
|
||||
self, components_settings,
|
||||
self,
|
||||
components_settings,
|
||||
):
|
||||
registry.register(name="variable_display", component=gen_variable_display_component())
|
||||
registry.register(name="parent_component", component=gen_parent_component())
|
||||
|
@ -153,7 +154,8 @@ class TestContext:
|
|||
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_nested_component_instances_have_unique_context_with_unfilled_slots_and_component_tag(
|
||||
self, components_settings,
|
||||
self,
|
||||
components_settings,
|
||||
):
|
||||
registry.register(name="variable_display", component=gen_variable_display_component())
|
||||
registry.register(name="parent_component", component=gen_parent_component())
|
||||
|
@ -184,7 +186,7 @@ class TestContext:
|
|||
{% endcomponent %}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
""" # NOQA
|
||||
""" # noqa: E501
|
||||
template = Template(template_str)
|
||||
rendered = template.render(Context())
|
||||
|
||||
|
@ -205,7 +207,7 @@ class TestContext:
|
|||
{% endcomponent %}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
""" # NOQA
|
||||
""" # noqa: E501
|
||||
template = Template(template_str)
|
||||
rendered = template.render(Context())
|
||||
|
||||
|
@ -214,7 +216,8 @@ class TestContext:
|
|||
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_nested_component_context_shadows_outer_context_with_unfilled_slots_and_component_tag(
|
||||
self, components_settings,
|
||||
self,
|
||||
components_settings,
|
||||
):
|
||||
registry.register(name="variable_display", component=gen_variable_display_component())
|
||||
registry.register(name="parent_component", component=gen_parent_component())
|
||||
|
@ -232,7 +235,8 @@ class TestContext:
|
|||
|
||||
@djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR)
|
||||
def test_nested_component_context_shadows_outer_context_with_filled_slots(
|
||||
self, components_settings,
|
||||
self,
|
||||
components_settings,
|
||||
):
|
||||
registry.register(name="variable_display", component=gen_variable_display_component())
|
||||
registry.register(name="parent_component", component=gen_parent_component())
|
||||
|
@ -245,7 +249,7 @@ class TestContext:
|
|||
{% endcomponent %}
|
||||
{% endfill %}
|
||||
{% endcomponent %}
|
||||
""" # NOQA
|
||||
""" # noqa: E501
|
||||
template = Template(template_str)
|
||||
rendered = template.render(Context({"shadowing_variable": "NOT SHADOWED"}))
|
||||
|
||||
|
@ -324,7 +328,7 @@ class TestParentArgs:
|
|||
[{"context_behavior": "isolated"}, "passed_in", ""],
|
||||
],
|
||||
["django", "isolated"],
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_parent_args_available_in_slots(self, components_settings, first_val, second_val):
|
||||
registry.register(name="incrementer", component=gen_incrementer_component())
|
||||
|
@ -473,7 +477,7 @@ class TestComponentsCanAccessOuterContext:
|
|||
[{"context_behavior": "isolated"}, ""],
|
||||
],
|
||||
["django", "isolated"],
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_simple_component_can_use_outer_context(self, components_settings, expected_value):
|
||||
registry.register(name="simple_component", component=gen_simple_component())
|
||||
|
@ -890,22 +894,24 @@ class TestContextProcessors:
|
|||
assert list(context_processors_data.keys()) == ["csrf_token"] # type: ignore[union-attr]
|
||||
assert inner_request == request
|
||||
|
||||
@djc_test(django_settings={
|
||||
"TEMPLATES": [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": ["tests/templates/", "tests/components/"],
|
||||
"OPTIONS": {
|
||||
"builtins": [
|
||||
"django_components.templatetags.component_tags",
|
||||
],
|
||||
"context_processors": [
|
||||
"tests.test_context.dummy_context_processor",
|
||||
],
|
||||
@djc_test(
|
||||
django_settings={
|
||||
"TEMPLATES": [
|
||||
{
|
||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||
"DIRS": ["tests/templates/", "tests/components/"],
|
||||
"OPTIONS": {
|
||||
"builtins": [
|
||||
"django_components.templatetags.component_tags",
|
||||
],
|
||||
"context_processors": [
|
||||
"tests.test_context.dummy_context_processor",
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
})
|
||||
],
|
||||
},
|
||||
)
|
||||
def test_data_generated_only_once(self):
|
||||
context_processors_data: Optional[Dict] = None
|
||||
context_processors_data_child: Optional[Dict] = None
|
||||
|
@ -932,8 +938,8 @@ class TestContextProcessors:
|
|||
request = HttpRequest()
|
||||
TestParentComponent.render(request=request)
|
||||
|
||||
parent_data = cast(dict, context_processors_data)
|
||||
child_data = cast(dict, context_processors_data_child)
|
||||
parent_data = cast("dict", context_processors_data)
|
||||
child_data = cast("dict", context_processors_data_child)
|
||||
|
||||
# Check that the context processors data is reused across the components with
|
||||
# the same request.
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue