From f100cc183600ee34b034ef706721e214c6306d90 Mon Sep 17 00:00:00 2001 From: Juro Oravec Date: Wed, 10 Sep 2025 14:06:53 +0200 Subject: [PATCH] refactor: replace isort, black and flake8 with ruff (#1346) --- .github/copilot-instructions.md | 17 +- .pre-commit-config.yaml | 22 +- benchmarks/benchmark_templating.py | 30 +- benchmarks/monkeypatch_asv.py | 6 +- benchmarks/utils.py | 30 +- docs/community/development.md | 51 +- docs/concepts/advanced/html_fragments.md | 2 +- docs/scripts/extensions.py | 8 +- docs/scripts/gen_release_notes.py | 6 +- docs/scripts/mkdocs_util.py | 14 +- docs/scripts/reference.py | 132 ++- pyproject.toml | 145 ++- requirements-dev.in | 5 +- requirements-dev.txt | 105 +-- requirements-docs.txt | 9 - sampleproject/calendarapp/urls.py | 3 +- sampleproject/components/greeting.py | 4 +- .../components/nested/calendar/calendar.py | 6 +- sampleproject/components/recursive.py | 4 +- sampleproject/components/urls.py | 3 +- scripts/supported_versions.py | 56 +- scripts/validate_links.py | 347 +++++--- src/django_components/__init__.py | 38 +- src/django_components/app_settings.py | 38 +- src/django_components/apps.py | 3 +- src/django_components/attributes.py | 16 +- src/django_components/autodiscovery.py | 6 +- src/django_components/cache.py | 4 +- src/django_components/commands/components.py | 4 +- src/django_components/commands/create.py | 38 +- src/django_components/commands/ext.py | 4 +- src/django_components/commands/ext_list.py | 6 +- src/django_components/commands/ext_run.py | 4 +- src/django_components/commands/list.py | 19 +- .../commands/startcomponent.py | 4 +- src/django_components/commands/upgrade.py | 15 +- .../commands/upgradecomponent.py | 4 +- src/django_components/compat/django.py | 10 +- src/django_components/component.py | 116 +-- src/django_components/component_media.py | 112 +-- src/django_components/component_registry.py | 50 +- src/django_components/components/dynamic.py | 32 +- src/django_components/dependencies.py | 89 +- src/django_components/expression.py | 40 +- src/django_components/extension.py | 24 +- src/django_components/extensions/cache.py | 12 +- .../extensions/debug_highlight.py | 8 +- src/django_components/extensions/defaults.py | 2 - src/django_components/extensions/view.py | 20 +- src/django_components/finders.py | 49 +- src/django_components/library.py | 9 +- src/django_components/node.py | 28 +- src/django_components/perfutil/component.py | 17 +- src/django_components/perfutil/provide.py | 4 +- src/django_components/provide.py | 14 +- src/django_components/slots.py | 94 +- src/django_components/tag_formatter.py | 28 +- src/django_components/template.py | 27 +- src/django_components/template_loader.py | 4 +- src/django_components/urls.py | 2 +- src/django_components/util/cache.py | 16 +- src/django_components/util/command.py | 34 +- src/django_components/util/context.py | 4 +- .../util/django_monkeypatch.py | 16 +- src/django_components/util/loader.py | 26 +- src/django_components/util/logger.py | 30 +- src/django_components/util/misc.py | 34 +- src/django_components/util/nanoid.py | 13 +- src/django_components/util/routing.py | 2 +- src/django_components/util/tag_parser.py | 146 ++- src/django_components/util/template_parser.py | 18 +- src/django_components/util/template_tag.py | 27 +- src/django_components/util/testing.py | 38 +- src/django_components/util/types.py | 2 - src/django_components/util/weakref.py | 2 +- src/django_components_js/build.py | 8 +- tests/components/glob/glob.py | 1 - .../relative_file_pathobj.py | 3 +- tests/e2e/testserver/testserver/urls.py | 2 +- tests/e2e/testserver/testserver/views.py | 32 +- tests/e2e/utils.py | 4 +- tests/test_attributes.py | 28 +- tests/test_autodiscover.py | 8 +- tests/test_benchmark_django.py | 224 +++-- tests/test_benchmark_django_small.py | 20 +- tests/test_benchmark_djc.py | 483 +++++----- tests/test_benchmark_djc_small.py | 21 +- tests/test_cache.py | 12 +- tests/test_command_components.py | 2 + tests/test_command_create.py | 48 +- tests/test_command_ext.py | 14 +- tests/test_command_list.py | 59 +- tests/test_component.py | 130 ++- tests/test_component_cache.py | 17 +- tests/test_component_defaults.py | 2 +- tests/test_component_highlight.py | 13 +- tests/test_component_media.py | 78 +- tests/test_component_typing.py | 20 +- tests/test_component_view.py | 80 +- tests/test_context.py | 66 +- tests/test_dependencies.py | 26 +- tests/test_dependency_manager.py | 18 +- tests/test_dependency_rendering.py | 8 +- tests/test_dependency_rendering_e2e.py | 167 ++-- tests/test_expression.py | 76 +- tests/test_extension.py | 62 +- tests/test_finders.py | 76 +- tests/test_html_parser.py | 4 +- tests/test_integration_template_partials.py | 7 +- tests/test_loader.py | 53 +- tests/test_node.py | 200 +++-- tests/test_registry.py | 4 +- tests/test_settings.py | 6 +- tests/test_signals.py | 6 +- tests/test_slots.py | 38 +- tests/test_tag_formatter.py | 43 +- tests/test_tag_parser.py | 840 +++++++++++------- tests/test_template.py | 2 +- tests/test_template_parser.py | 8 +- tests/test_templatetags.py | 3 +- tests/test_templatetags_component.py | 19 +- tests/test_templatetags_extends.py | 8 +- tests/test_templatetags_provide.py | 13 +- tests/test_templatetags_slot_fill.py | 109 ++- tests/test_templatetags_templating.py | 57 +- tests/test_utils.py | 1 + tests/testutils.py | 16 +- tox.ini | 23 +- 128 files changed, 3076 insertions(+), 2599 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 09fc626f..7b876413 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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` diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18541677..267d207c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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 diff --git a/benchmarks/benchmark_templating.py b/benchmarks/benchmark_templating.py index 779c70aa..f7f7539e 100644 --- a/benchmarks/benchmark_templating.py +++ b/benchmarks/benchmark_templating.py @@ -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 diff --git a/benchmarks/monkeypatch_asv.py b/benchmarks/monkeypatch_asv.py index 23003311..a891ddab 100644 --- a/benchmarks/monkeypatch_asv.py +++ b/benchmarks/monkeypatch_asv.py @@ -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 ---------- diff --git a/benchmarks/utils.py b/benchmarks/utils.py index eb160cb0..3437afaf 100644 --- a/benchmarks/utils.py +++ b/benchmarks/utils.py @@ -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: diff --git a/docs/community/development.md b/docs/community/development.md index 833b5839..afb512bd 100644 --- a/docs/community/development.md +++ b/docs/community/development.md @@ -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 diff --git a/docs/concepts/advanced/html_fragments.md b/docs/concepts/advanced/html_fragments.md index 33822c8d..569d4674 100644 --- a/docs/concepts/advanced/html_fragments.md +++ b/docs/concepts/advanced/html_fragments.md @@ -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: diff --git a/docs/scripts/extensions.py b/docs/scripts/extensions.py index 17555a41..661deaf1 100644 --- a/docs/scripts/extensions.py +++ b/docs/scripts/extensions.py @@ -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}" diff --git a/docs/scripts/gen_release_notes.py b/docs/scripts/gen_release_notes.py index 70c4804c..91f1b45b 100644 --- a/docs/scripts/gen_release_notes.py +++ b/docs/scripts/gen_release_notes.py @@ -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})" diff --git a/docs/scripts/mkdocs_util.py b/docs/scripts/mkdocs_util.py index e9080b26..70c8adf1 100644 --- a/docs/scripts/mkdocs_util.py +++ b/docs/scripts/mkdocs_util.py @@ -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 diff --git a/docs/scripts/reference.py b/docs/scripts/reference.py index a092a16e..90bb52cf 100644 --- a/docs/scripts/reference.py +++ b/docs/scripts/reference.py @@ -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 = "\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 = "\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.+)\)") - 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 = "\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 = "\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 = "\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 = "\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( diff --git a/pyproject.toml b/pyproject.toml index 17f10a24..b3207297 100644 --- a/pyproject.toml +++ b/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(): ...` or `# TODO @: ...` + "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", diff --git a/requirements-dev.in b/requirements-dev.in index 0b36d31c..57f1350f 100644 --- a/requirements-dev.in +++ b/requirements-dev.in @@ -6,11 +6,8 @@ pytest pytest-asyncio pytest-django syrupy -flake8 -flake8-pyproject -isort +ruff pre-commit -black mypy playwright requests diff --git a/requirements-dev.txt b/requirements-dev.txt index b78cd92e..9746d367 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -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 diff --git a/requirements-docs.txt b/requirements-docs.txt index a7aa23b2..43f34cbe 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -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 diff --git a/sampleproject/calendarapp/urls.py b/sampleproject/calendarapp/urls.py index df4a3da2..ffa4f4d3 100644 --- a/sampleproject/calendarapp/urls.py +++ b/sampleproject/calendarapp/urls.py @@ -1,6 +1,7 @@ -from calendarapp.views import calendar from django.urls import path +from calendarapp.views import calendar + urlpatterns = [ path("", calendar, name="calendar"), ] diff --git a/sampleproject/components/greeting.py b/sampleproject/components/greeting.py index 6543f25a..ce499266 100644 --- a/sampleproject/components/greeting.py +++ b/sampleproject/components/greeting.py @@ -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, diff --git a/sampleproject/components/nested/calendar/calendar.py b/sampleproject/components/nested/calendar/calendar.py index 4bc8b412..ccee0da7 100644 --- a/sampleproject/components/nested/calendar/calendar.py +++ b/sampleproject/components/nested/calendar/calendar.py @@ -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={ diff --git a/sampleproject/components/recursive.py b/sampleproject/components/recursive.py index 80bd4a10..e38d4425 100644 --- a/sampleproject/components/recursive.py +++ b/sampleproject/components/recursive.py @@ -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, diff --git a/sampleproject/components/urls.py b/sampleproject/components/urls.py index f94401f7..2e8108b7 100644 --- a/sampleproject/components/urls.py +++ b/sampleproject/components/urls.py @@ -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"), diff --git a/scripts/supported_versions.py b/scripts/supported_versions.py index f1971ef3..b276e0f2 100644 --- a/scripts/supported_versions.py +++ b/scripts/supported_versions.py @@ -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"

(.*?)

", 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, '
', @@ -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, '', @@ -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/") diff --git a/scripts/validate_links.py b/scripts/validate_links.py index e317ae84..1aac70c2 100644 --- a/scripts/validate_links.py +++ b/scripts/validate_links.py @@ -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__": diff --git a/src/django_components/__init__.py b/src/django_components/__init__.py index 32e5c4eb..9dd87afd 100644 --- a/src/django_components/__init__.py +++ b/src/django_components/__init__.py @@ -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", ] diff --git a/src/django_components/app_settings.py b/src/django_components/app_settings.py index a6eddefd..9ff90c94 100644 --- a/src/django_components/app_settings.py +++ b/src/django_components/app_settings.py @@ -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 diff --git a/src/django_components/apps.py b/src/django_components/apps.py index 21408c31..ea61e2d0 100644 --- a/src/django_components/apps.py +++ b/src/django_components/apps.py @@ -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: diff --git a/src/django_components/attributes.py b/src/django_components/attributes.py index b149bf15..a6b04440 100644 --- a/src/django_components/attributes.py +++ b/src/django_components/attributes.py @@ -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 diff --git a/src/django_components/autodiscovery.py b/src/django_components/autodiscovery.py index c4c52130..99a8a96b 100644 --- a/src/django_components/autodiscovery.py +++ b/src/django_components/autodiscovery.py @@ -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. diff --git a/src/django_components/cache.py b/src/django_components/cache.py index 0b4c443d..ad0bb683 100644 --- a/src/django_components/cache.py +++ b/src/django_components/cache.py @@ -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", diff --git a/src/django_components/commands/components.py b/src/django_components/commands/components.py index af489db2..6e07dc63 100644 --- a/src/django_components/commands/components.py +++ b/src/django_components/commands/components.py @@ -24,9 +24,9 @@ class ComponentsRootCommand(ComponentCommand): name = "components" help = "The entrypoint for the 'components' commands." - subcommands = [ + subcommands = ( CreateCommand, UpgradeCommand, ExtCommand, ComponentListCommand, - ] + ) diff --git a/src/django_components/commands/create.py b/src/django_components/commands/create.py index cc83a8e2..e6b53e83 100644 --- a/src/django_components/commands/create.py +++ b/src/django_components/commands/create.py @@ -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"""
@@ -188,11 +191,12 @@ class CreateCommand(ComponentCommand):
This is {{ param }} context value.
- """ + """, ) 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()) diff --git a/src/django_components/commands/ext.py b/src/django_components/commands/ext.py index 35c7d300..a7899ba6 100644 --- a/src/django_components/commands/ext.py +++ b/src/django_components/commands/ext.py @@ -16,7 +16,7 @@ class ExtCommand(ComponentCommand): name = "ext" help = "Run extension commands." - subcommands = [ + subcommands = ( ExtListCommand, ExtRunCommand, - ] + ) diff --git a/src/django_components/commands/ext_list.py b/src/django_components/commands/ext_list.py index dae4d6b4..fa50fc99 100644 --- a/src/django_components/commands/ext_list.py +++ b/src/django_components/commands/ext_list.py @@ -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 diff --git a/src/django_components/commands/ext_run.py b/src/django_components/commands/ext_run.py index d1dd0a8a..4665673f 100644 --- a/src/django_components/commands/ext_run.py +++ b/src/django_components/commands/ext_run.py @@ -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] diff --git a/src/django_components/commands/list.py b/src/django_components/commands/list.py index a8459811..dab6b6d4 100644 --- a/src/django_components/commands/list.py +++ b/src/django_components/commands/list.py @@ -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 diff --git a/src/django_components/commands/startcomponent.py b/src/django_components/commands/startcomponent.py index cf16383a..f22ee856 100644 --- a/src/django_components/commands/startcomponent.py +++ b/src/django_components/commands/startcomponent.py @@ -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." diff --git a/src/django_components/commands/upgrade.py b/src/django_components/commands/upgrade.py index a160c6f0..65767aa1 100644 --- a/src/django_components/commands/upgrade.py +++ b/src/django_components/commands/upgrade.py @@ -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*%})', diff --git a/src/django_components/commands/upgradecomponent.py b/src/django_components/commands/upgradecomponent.py index 07b568ad..1436895b 100644 --- a/src/django_components/commands/upgradecomponent.py +++ b/src/django_components/commands/upgradecomponent.py @@ -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." diff --git a/src/django_components/compat/django.py b/src/django_components/compat/django.py index 5dcce09e..033a611a 100644 --- a/src/django_components/compat/django.py +++ b/src/django_components/compat/django.py @@ -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 diff --git a/src/django_components/component.py b/src/django_components/component.py index 1e1e43f0..42bf6cb2 100644 --- a/src/django_components/component.py +++ b/src/django_components/component.py @@ -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. diff --git a/src/django_components/component_media.py b/src/django_components/component_media.py index d3957c78..f47f74f6 100644 --- a/src/django_components/component_media.py +++ b/src/django_components/component_media.py @@ -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 diff --git a/src/django_components/component_registry.py b/src/django_components/component_registry.py index 2142582f..e0d22d17 100644 --- a/src/django_components/component_registry.py +++ b/src/django_components/component_registry.py @@ -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 diff --git a/src/django_components/components/dynamic.py b/src/django_components/components/dynamic.py index db959227..d90afa83 100644 --- a/src/django_components/components/dynamic.py +++ b/src/django_components/components/dynamic.py @@ -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: diff --git a/src/django_components/dependencies.py b/src/django_components/dependencies.py index e575e028..a0bc417d 100644 --- a/src/django_components/dependencies.py +++ b/src/django_components/dependencies.py @@ -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 "' end tag. " - "This is not allowed, as it would break the HTML." + "This is not allowed, as it would break the HTML.", ) return f"" @@ -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 "' end tag. " - "This is not allowed, as it would break the HTML." + "This is not allowed, as it would break the HTML.", ) return f"" @@ -347,10 +345,10 @@ COMPONENT_COMMENT_REGEX = re.compile(rb"` """ # Extract all matched instances of `` 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 # # 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") diff --git a/src/django_components/expression.py b/src/django_components/expression.py index 9afcbc80..a7c6af0f 100644 --- a/src/django_components/expression.py +++ b/src/django_components/expression.py @@ -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['\"])", # 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: diff --git a/src/django_components/extension.py b/src/django_components/extension.py index 1f8ec233..eeba0a2a 100644 --- a/src/django_components/extension.py +++ b/src/django_components/extension.py @@ -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: diff --git a/src/django_components/extensions/cache.py b/src/django_components/extensions/cache.py index 5f07dede..aec33bb3 100644 --- a/src/django_components/extensions/cache.py +++ b/src/django_components/extensions/cache.py @@ -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 diff --git a/src/django_components/extensions/debug_highlight.py b/src/django_components/extensions/debug_highlight.py index acaf1f5c..52ea0e16 100644 --- a/src/django_components/extensions/debug_highlight.py +++ b/src/django_components/extensions/debug_highlight.py @@ -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""" -
+
{output}
""" diff --git a/src/django_components/extensions/defaults.py b/src/django_components/extensions/defaults.py index a2781d92..6834eb22 100644 --- a/src/django_components/extensions/defaults.py +++ b/src/django_components/extensions/defaults.py @@ -145,8 +145,6 @@ class ComponentDefaults(ExtensionComponentConfig): ``` """ - pass - class DefaultsExtension(ComponentExtension): """ diff --git a/src/django_components/extensions/view.py b/src/django_components/extensions/view.py index 68b7d47f..ba70ff76 100644 --- a/src/django_components/extensions/view.py +++ b/src/django_components/extensions/view.py @@ -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): diff --git a/src/django_components/finders.py b/src/django_components/finders.py index c91b7382..f4794fbd 100644 --- a/src/django_components/finders.py +++ b/src/django_components/finders.py @@ -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): diff --git a/src/django_components/library.py b/src/django_components/library.py index f04fde18..0c2bc297 100644 --- a/src/django_components/library.py +++ b/src/django_components/library.py @@ -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: diff --git a/src/django_components/node.py b/src/django_components/node.py index f458c939..614a2589 100644 --- a/src/django_components/node.py +++ b/src/django_components/node.py @@ -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, }, ) diff --git a/src/django_components/perfutil/component.py b/src/django_components/perfutil/component.py index 0af91856..cf137089 100644 --- a/src/django_components/perfutil/component.py +++ b/src/django_components/perfutil/component.py @@ -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''.format(COMP_ID_LENGTH=COMP_ID_LENGTH) + r''.format(COMP_ID_LENGTH=COMP_ID_LENGTH), # noqa: UP032 ) render_id_pattern = re.compile( - r'djc-render-id="(?P\w{{{COMP_ID_LENGTH}}})"'.format(COMP_ID_LENGTH=COMP_ID_LENGTH) + r'djc-render-id="(?P\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()` diff --git a/src/django_components/perfutil/provide.py b/src/django_components/perfutil/provide.py index be99167c..4ffb2497 100644 --- a/src/django_components/perfutil/provide.py +++ b/src/django_components/perfutil/provide.py @@ -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 diff --git a/src/django_components/provide.py b/src/django_components/provide.py index 41e2367f..9df43aec 100644 --- a/src/django_components/provide.py +++ b/src/django_components/provide.py @@ -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 diff --git a/src/django_components/slots.py b/src/django_components/slots.py index 060ce866..4c1c6d38 100644 --- a/src/django_components/slots.py +++ b/src/django_components/slots.py @@ -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, diff --git a/src/django_components/tag_formatter.py b/src/django_components/tag_formatter.py index 5daacc39..f278165a 100644 --- a/src/django_components/tag_formatter.py +++ b/src/django_components/tag_formatter.py @@ -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'") diff --git a/src/django_components/template.py b/src/django_components/template.py index 6b6ebb24..d789cbf3 100644 --- a/src/django_components/template.py +++ b/src/django_components/template.py @@ -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 diff --git a/src/django_components/template_loader.py b/src/django_components/template_loader.py index 4545ffda..32b09b51 100644 --- a/src/django_components/template_loader.py +++ b/src/django_components/template_loader.py @@ -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 diff --git a/src/django_components/urls.py b/src/django_components/urls.py index e8d3a137..e6e9d71d 100644 --- a/src/django_components/urls.py +++ b/src/django_components/urls.py @@ -10,7 +10,7 @@ urlpatterns = [ [ *dependencies_urlpatterns, *extension_urlpatterns, - ] + ], ), ), ] diff --git a/src/django_components/util/cache.py b/src/django_components/util/cache.py index e4743422..a4f5a2d9 100644 --- a/src/django_components/util/cache.py +++ b/src/django_components/util/cache.py @@ -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: """ diff --git a/src/django_components/util/command.py b/src/django_components/util/command.py index e4de943c..a951d21d 100644 --- a/src/django_components/util/command.py +++ b/src/django_components/util/command.py @@ -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: diff --git a/src/django_components/util/context.py b/src/django_components/util/context.py index 71e33488..47263d88 100644 --- a/src/django_components/util/context.py +++ b/src/django_components/util/context.py @@ -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 diff --git a/src/django_components/util/django_monkeypatch.py b/src/django_components/util/django_monkeypatch.py index c9db1e38..74e7d669 100644 --- a/src/django_components/util/django_monkeypatch.py +++ b/src/django_components/util/django_monkeypatch.py @@ -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 diff --git a/src/django_components/util/loader.py b/src/django_components/util/loader.py index 00e646a5..88160b37 100644 --- a/src/django_components/util/loader.py +++ b/src/django_components/util/loader.py @@ -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() diff --git a/src/django_components/util/logger.py b/src/django_components/util/logger.py index 5bd16aed..140bf1cb 100644 --- a/src/django_components/util/logger.py +++ b/src/django_components/util/logger.py @@ -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 diff --git a/src/django_components/util/misc.py b/src/django_components/util/misc.py index def9d6ee..8aa456e6 100644 --- a/src/django_components/util/misc.py +++ b/src/django_components/util/misc.py @@ -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 diff --git a/src/django_components/util/nanoid.py b/src/django_components/util/nanoid.py index 9e86024a..f389a850 100644 --- a/src/django_components/util/nanoid.py +++ b/src/django_components/util/nanoid.py @@ -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 diff --git a/src/django_components/util/routing.py b/src/django_components/util/routing.py index c8da0d4b..bba18f47 100644 --- a/src/django_components/util/routing.py +++ b/src/django_components/util/routing.py @@ -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 diff --git a/src/django_components/util/tag_parser.py b/src/django_components/util/tag_parser.py index 83153b9d..60e7025a 100644 --- a/src/django_components/util/tag_parser.py +++ b/src/django_components/util/tag_parser.py @@ -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 diff --git a/src/django_components/util/template_parser.py b/src/django_components/util/template_parser.py index c66cbcd9..baef73d2 100644 --- a/src/django_components/util/template_parser.py +++ b/src/django_components/util/template_parser.py @@ -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) diff --git a/src/django_components/util/template_tag.py b/src/django_components/util/template_tag.py index 6f382384..cd8f2fdb 100644 --- a/src/django_components/util/template_tag.py +++ b/src/django_components/util/template_tag.py @@ -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 diff --git a/src/django_components/util/testing.py b/src/django_components/util/testing.py index 9117692d..f864917b 100644 --- a/src/django_components/util/testing.py +++ b/src/django_components/util/testing.py @@ -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 diff --git a/src/django_components/util/types.py b/src/django_components/util/types.py index 4d490d9c..d1d89db0 100644 --- a/src/django_components/util/types.py +++ b/src/django_components/util/types.py @@ -26,5 +26,3 @@ class Empty(NamedTuple): Read more about [Typing and validation](../../concepts/fundamentals/typing_and_validation). """ - - pass diff --git a/src/django_components/util/weakref.py b/src/django_components/util/weakref.py index f0a51455..176a8f9d 100644 --- a/src/django_components/util/weakref.py +++ b/src/django_components/util/weakref.py @@ -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: diff --git a/src/django_components_js/build.py b/src/django_components_js/build.py index da40fe83..1176ce6b 100644 --- a/src/django_components_js/build.py +++ b/src/django_components_js/build.py @@ -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) diff --git a/tests/components/glob/glob.py b/tests/components/glob/glob.py index 803a32e8..b12fba86 100644 --- a/tests/components/glob/glob.py +++ b/tests/components/glob/glob.py @@ -1,4 +1,3 @@ - from django_components import Component diff --git a/tests/components/relative_file_pathobj/relative_file_pathobj.py b/tests/components/relative_file_pathobj/relative_file_pathobj.py index 2d49eba4..125ae902 100644 --- a/tests/components/relative_file_pathobj/relative_file_pathobj.py +++ b/tests/components/relative_file_pathobj/relative_file_pathobj.py @@ -20,8 +20,7 @@ class PathObj: if self.static_path.endswith(".js"): return format_html('', static(self.static_path)) - else: - return format_html('', static(self.static_path)) + return format_html('', static(self.static_path)) @register("relative_file_pathobj_component") diff --git a/tests/e2e/testserver/testserver/urls.py b/tests/e2e/testserver/testserver/urls.py index f405f77f..4035d6a3 100644 --- a/tests/e2e/testserver/testserver/urls.py +++ b/tests/e2e/testserver/testserver/urls.py @@ -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"), diff --git a/tests/e2e/testserver/testserver/views.py b/tests/e2e/testserver/testserver/views.py index ebcb9973..0deaed03 100644 --- a/tests/e2e/testserver/testserver/views.py +++ b/tests/e2e/testserver/testserver/views.py @@ -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 %} @@ -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 %} @@ -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 %} @@ -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 %} @@ -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 %} @@ -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 %} @@ -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 %} @@ -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 %} @@ -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 %} diff --git a/tests/e2e/utils.py b/tests/e2e/utils.py index 34248e7b..51badf54 100644 --- a/tests/e2e/utils.py +++ b/tests/e2e/utils.py @@ -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 diff --git a/tests/test_attributes.py b/tests/test_attributes.py index 61c5e6cb..bb40bd78 100644 --- a/tests/test_attributes.py +++ b/tests/test_attributes.py @@ -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:
content
- """ # noqa: E501 + """ def get_template_data(self, args, kwargs, slots, context): return { @@ -170,7 +172,7 @@ class TestHtmlAttrs:
content
- """ # 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:
content
- """ # noqa: E501 + """ def get_template_data(self, args, kwargs, slots, context): return { @@ -298,7 +302,7 @@ class TestHtmlAttrs:
content
- """, # noqa: E501 + """, ) assert "override-me" not in rendered @@ -344,7 +348,7 @@ class TestHtmlAttrs: %}> content
- """ # noqa: E501 + """ def get_template_data(self, args, kwargs, slots, context): return {"attrs": kwargs["attrs"]} @@ -389,7 +393,7 @@ class TestHtmlAttrs:
content
- """ # noqa: E501 + """ def get_template_data(self, args, kwargs, slots, context): return {"attrs": kwargs["attrs"]} @@ -419,7 +423,7 @@ class TestHtmlAttrs:
content
- """ # noqa: E501 + """ def get_template_data(self, args, kwargs, slots, context): return {"attrs": kwargs["attrs"]} diff --git a/tests/test_autodiscover.py b/tests/test_autodiscover.py index 03aee405..3d7765ce 100644 --- a/tests/test_autodiscover.py +++ b/tests/test_autodiscover.py @@ -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() diff --git a/tests/test_benchmark_django.py b/tests/test_benchmark_django.py index 8d1ea2e0..0b9c50c3 100644 --- a/tests/test_benchmark_django.py +++ b/tests/test_benchmark_django.py @@ -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"""
- {output['name']} + {output["name"]}
""", 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() diff --git a/tests/test_benchmark_django_small.py b/tests/test_benchmark_django_small.py index 7ac92eda..49682b4c 100644 --- a/tests/test_benchmark_django_small.py +++ b/tests/test_benchmark_django_small.py @@ -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() diff --git a/tests/test_benchmark_djc.py b/tests/test_benchmark_djc.py index 0757ce1b..553df0cf 100644 --- a/tests/test_benchmark_djc.py +++ b/tests/test_benchmark_djc.py @@ -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 itertools import chain @@ -31,15 +31,14 @@ 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.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 Component, registry, register, types +from django_components import Component, register, registry, types # DO NOT REMOVE - See https://github.com/django-components/django-components/pull/999 # ----------- IMPORTS END ------------ # @@ -61,9 +60,9 @@ if not settings.configured: "OPTIONS": { "builtins": [ "django_components.templatetags.component_tags", - ] + ], }, - } + }, ], COMPONENTS={ "autodiscover": False, @@ -74,9 +73,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() @@ -87,6 +86,7 @@ else: # RENDER ENTRYPOINT ##################################### + def gen_render_data(): data = load_project_data_from_json(data_json) @@ -101,7 +101,7 @@ def gen_render_data(): "text": "Test bookmark", "url": "http://localhost:8000/bookmarks/9/create", "attachment": None, - } + }, ] request = HttpRequest() @@ -644,162 +644,154 @@ data_json = """ # DATA LOADER ##################################### + 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) # First create lookup tables for objects that will be referenced - users_by_id = { - user['pk']: {'id': user['pk'], **user['fields']} - for user in data.get('users', []) - } + users_by_id = {user["pk"]: {"id": user["pk"], **user["fields"]} for user in data.get("users", [])} def _get_user(user_id: int): - return users_by_id[user_id] if user_id in users_by_id else data.get('users', [])[0] + return users_by_id[user_id] if user_id in users_by_id else data.get("users", [])[0] - organizations_by_id = { - org['pk']: {'id': org['pk'], **org['fields']} - for org in data.get('organizations', []) - } + organizations_by_id = {org["pk"]: {"id": org["pk"], **org["fields"]} for org in data.get("organizations", [])} - phase_templates_by_id = { - pt['pk']: {'id': pt['pk'], **pt['fields']} - for pt in data.get('phase_templates', []) - } + phase_templates_by_id = {pt["pk"]: {"id": pt["pk"], **pt["fields"]} for pt in data.get("phase_templates", [])} # 1. Resolve project's organization reference - project = {'id': data['project']['pk'], **data['project']['fields']} - if 'organization' in project: - org_id = project.pop('organization') # Remove the ID field - project['organization'] = organizations_by_id[org_id] # Add the reference + project = {"id": data["project"]["pk"], **data["project"]["fields"]} + if "organization" in project: + org_id = project.pop("organization") # Remove the ID field + project["organization"] = organizations_by_id[org_id] # Add the reference # 2. Project tags - no changes needed - project_tags = data['project_tags'] + project_tags = data["project_tags"] # 3. Resolve phases' references phases = [] phases_by_id = {} # We'll need this for resolving output references later - for phase_data in data['phases']: - phase = {'id': phase_data['pk'], **phase_data['fields']} - if 'project' in phase: - phase['project'] = project - if 'phase_template' in phase: - template_id = phase.pop('phase_template') - phase['phase_template'] = phase_templates_by_id[template_id] + for phase_data in data["phases"]: + phase = {"id": phase_data["pk"], **phase_data["fields"]} + if "project" in phase: + phase["project"] = project + if "phase_template" in phase: + template_id = phase.pop("phase_template") + phase["phase_template"] = phase_templates_by_id[template_id] phases.append(phase) - phases_by_id[phase['id']] = phase + phases_by_id[phase["id"]] = phase # 4. Resolve notes_1 references notes_1 = [] notes_1_by_id = {} # We'll need this for resolving notes references - for note_data in data['notes_1']: - note = {'id': note_data['pk'], **note_data['fields']} - if 'project' in note: - note['project'] = project + for note_data in data["notes_1"]: + note = {"id": note_data["pk"], **note_data["fields"]} + if "project" in note: + note["project"] = project notes_1.append(note) - notes_1_by_id[note['id']] = note + notes_1_by_id[note["id"]] = note # 5. Resolve comments_by_notes_1 references comments_by_notes_1 = {} - for note_id, comments_list in data['comments_by_notes_1'].items(): + for note_id, comments_list in data["comments_by_notes_1"].items(): resolved_comments = [] for comment_data in comments_list: - comment = {'id': comment_data['pk'], **comment_data['fields']} - if 'modified_by' in comment: - comment['modified_by'] = _get_user(comment['modified_by']) - if 'parent' in comment: - comment['parent'] = notes_1_by_id[comment['parent']] + comment = {"id": comment_data["pk"], **comment_data["fields"]} + if "modified_by" in comment: + comment["modified_by"] = _get_user(comment["modified_by"]) + if "parent" in comment: + comment["parent"] = notes_1_by_id[comment["parent"]] resolved_comments.append(comment) comments_by_notes_1[note_id] = resolved_comments # 6. Resolve notes_2' references notes_2 = [] notes_2_by_id = {} # We'll need this for resolving notes references - for note_data in data['notes_2']: - note = {'id': note_data['pk'], **note_data['fields']} - if 'project' in note: - note['project'] = project + for note_data in data["notes_2"]: + note = {"id": note_data["pk"], **note_data["fields"]} + if "project" in note: + note["project"] = project notes_2.append(note) - notes_2_by_id[note['id']] = note + notes_2_by_id[note["id"]] = note # 7. Resolve comments_by_notes_2 references comments_by_notes_2 = {} - for note_id, comments_list in data['comments_by_notes_2'].items(): + for note_id, comments_list in data["comments_by_notes_2"].items(): resolved_comments = [] for comment_data in comments_list: - comment = {'id': comment_data['pk'], **comment_data['fields']} - if 'modified_by' in comment: - comment['modified_by'] = _get_user(comment['modified_by']) - if 'parent' in comment: - comment['parent'] = notes_2_by_id[comment['parent']] + comment = {"id": comment_data["pk"], **comment_data["fields"]} + if "modified_by" in comment: + comment["modified_by"] = _get_user(comment["modified_by"]) + if "parent" in comment: + comment["parent"] = notes_2_by_id[comment["parent"]] resolved_comments.append(comment) comments_by_notes_2[note_id] = resolved_comments # 8. Resolve notes_3 references notes_3 = [] notes_3_by_id = {} # We'll need this for resolving notes references - for note_data in data['notes_3']: - note = {'id': note_data['pk'], **note_data['fields']} - if 'project' in note: - note['project'] = project + for note_data in data["notes_3"]: + note = {"id": note_data["pk"], **note_data["fields"]} + if "project" in note: + note["project"] = project notes_3.append(note) - notes_3_by_id[note['id']] = note + notes_3_by_id[note["id"]] = note # 9. Resolve comments_by_notes_3 references comments_by_notes_3 = {} - for note_id, comments_list in data['comments_by_notes_3'].items(): + for note_id, comments_list in data["comments_by_notes_3"].items(): resolved_comments = [] for comment_data in comments_list: - comment = {'id': comment_data['pk'], **comment_data['fields']} - if 'modified_by' in comment: - comment['modified_by'] = _get_user(comment['modified_by']) - if 'parent' in comment: - comment['parent'] = notes_3_by_id[comment['parent']] + comment = {"id": comment_data["pk"], **comment_data["fields"]} + if "modified_by" in comment: + comment["modified_by"] = _get_user(comment["modified_by"]) + if "parent" in comment: + comment["parent"] = notes_3_by_id[comment["parent"]] resolved_comments.append(comment) comments_by_notes_3[note_id] = resolved_comments # 10. Resolve roles_with_users references roles = [] - for role_data in data['roles_with_users']: - role = {'id': role_data['pk'], **role_data['fields']} - if 'project' in role: - role['project'] = project - if 'user' in role: - role['user'] = _get_user(role['user']) + for role_data in data["roles_with_users"]: + role = {"id": role_data["pk"], **role_data["fields"]} + if "project" in role: + role["project"] = project + if "user" in role: + role["user"] = _get_user(role["user"]) roles.append(role) # 11. Contacts - EMPTY, so no changes needed - contacts = data['contacts'] + contacts = data["contacts"] # 12. Resolve outputs references resolved_outputs = [] outputs_by_id = {} # For resolving dependencies # First pass: Create all output objects and build lookup - for output_tuple in data['outputs']: + for output_tuple in data["outputs"]: output_data = output_tuple[0] - output = {'id': output_data['pk'], **output_data['fields']} - if 'phase' in output: - output['phase'] = phases_by_id[output['phase']] - outputs_by_id[output['id']] = output + output = {"id": output_data["pk"], **output_data["fields"]} + if "phase" in output: + output["phase"] = phases_by_id[output["phase"]] + outputs_by_id[output["id"]] = output # Second pass: Process each output with its attachments and dependencies - for output_tuple in data['outputs']: + for output_tuple in data["outputs"]: output_data, attachments_data, dependencies_data = output_tuple - output = outputs_by_id[output_data['pk']] + output = outputs_by_id[output_data["pk"]] # Process attachments resolved_attachments = [] for attachment_tuple in attachments_data: attachment_data = attachment_tuple[0] - attachment = {'id': attachment_data['pk'], **attachment_data['fields']} - if 'created_by' in attachment: - attachment['created_by'] = _get_user(attachment['created_by']) - if 'output' in attachment: - attachment['output'] = outputs_by_id[attachment['output']] + attachment = {"id": attachment_data["pk"], **attachment_data["fields"]} + if "created_by" in attachment: + attachment["created_by"] = _get_user(attachment["created_by"]) + if "output" in attachment: + attachment["output"] = outputs_by_id[attachment["output"]] # Keep tags as is resolved_attachments.append((attachment, attachment_tuple[1])) @@ -807,36 +799,38 @@ def load_project_data_from_json(contents: str) -> dict: resolved_dependencies = [] for dep_tuple in dependencies_data: dep_data = dep_tuple[0] - dep_output = outputs_by_id[dep_data['pk']] + dep_output = outputs_by_id[dep_data["pk"]] # Keep the tuple structure but with resolved references resolved_dependencies.append((dep_output, dep_tuple[1])) resolved_outputs.append((output, resolved_attachments, resolved_dependencies)) return { - 'project': project, - 'project_tags': project_tags, - 'phases': phases, - 'notes_1': notes_1, - 'comments_by_notes_1': comments_by_notes_1, - 'notes_2': notes_2, - 'comments_by_notes_2': comments_by_notes_2, - 'notes_3': notes_3, - 'comments_by_notes_3': comments_by_notes_3, - 'roles_with_users': roles, - 'contacts': contacts, - 'outputs': resolved_outputs, - 'status_updates': data['status_updates'], - 'user_is_project_member': data['user_is_project_member'], - 'user_is_project_owner': data['user_is_project_owner'], - 'phase_titles': data['phase_titles'], - 'users': data['users'], + "project": project, + "project_tags": project_tags, + "phases": phases, + "notes_1": notes_1, + "comments_by_notes_1": comments_by_notes_1, + "notes_2": notes_2, + "comments_by_notes_2": comments_by_notes_2, + "notes_3": notes_3, + "comments_by_notes_3": comments_by_notes_3, + "roles_with_users": roles, + "contacts": contacts, + "outputs": resolved_outputs, + "status_updates": data["status_updates"], + "user_is_project_member": data["user_is_project_member"], + "user_is_project_owner": data["user_is_project_owner"], + "phase_titles": data["phase_titles"], + "users": data["users"], } + ##################################### # TYPES ##################################### + class User(TypedDict): id: int name: str @@ -938,6 +932,7 @@ class ProjectNoteComment(TypedDict): FORM_SHORT_TEXT_MAX_LEN = 255 + # This allows us to compare Enum values against strings class StrEnum(str, Enum): pass @@ -950,6 +945,7 @@ class TagResourceType(StrEnum): PROJECT_OUTPUT_ATTACHMENT = "PROJECT_OUTPUT_ATTACHMENT" PROJECT_TEMPLATE = "PROJECT_TEMPLATE" + class ProjectPhaseType(StrEnum): PHASE_1 = "PHASE_1" PHASE_2 = "PHASE_2" @@ -957,6 +953,7 @@ class ProjectPhaseType(StrEnum): PHASE_4 = "PHASE_4" PHASE_5 = "PHASE_5" + class TagTypeMeta(NamedTuple): allowed_values: Tuple[str, ...] @@ -984,7 +981,7 @@ TAG_TYPE_META = MappingProxyType( ), ), TagResourceType.PROJECT_OUTPUT: TagTypeMeta( - allowed_values=tuple(), + allowed_values=(), ), TagResourceType.PROJECT_OUTPUT_ATTACHMENT: TagTypeMeta( allowed_values=( @@ -1005,9 +1002,10 @@ TAG_TYPE_META = MappingProxyType( TagResourceType.PROJECT_TEMPLATE: TagTypeMeta( allowed_values=("Tag 21",), ), - } + }, ) + class ProjectOutputDef(NamedTuple): title: str description: Optional[str] = None @@ -1071,7 +1069,7 @@ PROJECT_PHASES_META = MappingProxyType( ProjectOutputDef(title="Lorem ipsum 14"), ], ), - } + }, ) ##################################### @@ -1083,6 +1081,7 @@ ThemeVariant = Literal["primary", "secondary"] VARIANTS = ["primary", "secondary"] + class ThemeStylingUnit(NamedTuple): """ Smallest unit of info, this class defines a specific styling of a specific @@ -1135,10 +1134,10 @@ _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" + color="bg-blue-300 text-blue-50 focus-visible:outline-blue-600 transition", ), secondary=ThemeStylingUnit( color="bg-white text-gray-800 ring-gray-300 hover:bg-gray-100 focus-visible:outline-gray-600 transition", @@ -1151,10 +1150,10 @@ theme = Theme( ), error=ThemeStylingVariant( primary=ThemeStylingUnit( - color="bg-red-600 text-white hover:bg-red-500 focus-visible:outline-red-600" + color="bg-red-600 text-white hover:bg-red-500 focus-visible:outline-red-600", ), primary_disabled=ThemeStylingUnit( - color="bg-red-300 text-white focus-visible:outline-red-600" + color="bg-red-300 text-white focus-visible:outline-red-600", ), secondary=ThemeStylingUnit( color="bg-white text-red-600 ring-red-300 hover:bg-red-100 focus-visible:outline-red-600", @@ -1167,10 +1166,10 @@ theme = Theme( ), alert=ThemeStylingVariant( primary=ThemeStylingUnit( - color="bg-amber-500 text-white hover:bg-amber-400 focus-visible:outline-amber-500" + color="bg-amber-500 text-white hover:bg-amber-400 focus-visible:outline-amber-500", ), primary_disabled=ThemeStylingUnit( - color="bg-amber-100 text-orange-300 focus-visible:outline-amber-500" + color="bg-amber-100 text-orange-300 focus-visible:outline-amber-500", ), secondary=ThemeStylingUnit( color="bg-white text-amber-500 ring-amber-300 hover:bg-amber-100 focus-visible:outline-amber-500", @@ -1183,10 +1182,10 @@ theme = Theme( ), success=ThemeStylingVariant( primary=ThemeStylingUnit( - color="bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600" + color="bg-green-600 text-white hover:bg-green-500 focus-visible:outline-green-600", ), primary_disabled=ThemeStylingUnit( - color="bg-green-300 text-white focus-visible:outline-green-600" + color="bg-green-300 text-white focus-visible:outline-green-600", ), secondary=ThemeStylingUnit( color="bg-white text-green-600 ring-green-300 hover:bg-green-100 focus-visible:outline-green-600", @@ -1199,10 +1198,10 @@ theme = Theme( ), info=ThemeStylingVariant( primary=ThemeStylingUnit( - color="bg-sky-600 text-white hover:bg-sky-500 focus-visible:outline-sky-600" + color="bg-sky-600 text-white hover:bg-sky-500 focus-visible:outline-sky-600", ), primary_disabled=ThemeStylingUnit( - color="bg-sky-300 text-white focus-visible:outline-sky-600" + color="bg-sky-300 text-white focus-visible:outline-sky-600", ), secondary=ThemeStylingUnit( color="bg-white text-sky-600 ring-sky-300 hover:bg-sky-100 focus-visible:outline-sky-600", @@ -1251,7 +1250,7 @@ def get_styling_css( if variant not in VARIANTS: raise ValueError( - f'Unknown theme variant "{variant}", must be one of {VARIANTS}' + f'Unknown theme variant "{variant}", must be one of {VARIANTS}', ) variant_name = variant if not disabled else f"{variant}_disabled" @@ -1268,6 +1267,7 @@ def get_styling_css( T = TypeVar("T") U = TypeVar("U") + def format_timestamp(timestamp: datetime): """ If the timestamp is more than 7 days ago, format it as "Jan 1, 2025". @@ -1275,8 +1275,8 @@ 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( lst: Iterable[T], @@ -1303,6 +1303,7 @@ def group_by( grouped[key].append(mapped_item) return grouped + def dynamic_apply(fn: Callable, *args): """ Given a function and positional arguments that should be applied to given function, @@ -1315,10 +1316,12 @@ def dynamic_apply(fn: Callable, *args): return fn(*first_n_args) + ##################################### # SHARED FORMS ##################################### + class ConditionalEditForm(forms.Form): """ Subclass of Django's Form that sets all fields as NON-editable based @@ -1338,10 +1341,12 @@ class ConditionalEditForm(forms.Form): for form_field in fields.values(): form_field.widget.attrs["readonly"] = True + ##################################### # TEMPLATE TAG FILTERS ##################################### + @default_library.filter("alpine") def to_alpine_json(value: dict): """ @@ -1354,20 +1359,24 @@ def to_alpine_json(value: dict): data = json.dumps(value).replace('"', "'") return data + @default_library.filter("json") def to_json(value: dict): """Serialize Python object to JSON.""" data = json.dumps(value) return data + @default_library.simple_tag def define(val=None): return val + @default_library.filter def get_item(dictionary: dict, key: str): return dictionary.get(key) + @default_library.filter("js") def serialize_to_js(obj): """ @@ -1388,22 +1397,23 @@ 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) + ##################################### # BUTTON ##################################### + @register("Button") class Button(Component): def get_context_data( @@ -1415,7 +1425,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 = ( @@ -1426,14 +1436,12 @@ class Button(Component): all_css_class = common_css else: button_classes = get_styling_css(variant, color, disabled) # type: ignore[arg-type] - all_css_class = ( - f"{button_classes} {common_css} px-3 py-2 justify-center rounded-md shadow-sm" - ) + all_css_class = f"{button_classes} {common_css} px-3 py-2 justify-center rounded-md shadow-sm" is_link = not disabled and (href or link) all_attrs = { - **(attrs or {}) + **(attrs or {}), } if disabled: all_attrs["aria-disabled"] = "true" @@ -1547,10 +1555,12 @@ class Menu(Component): all_list_attrs.update(list_attrs) if anchor: all_list_attrs[f"x-anchor.{anchor_dir}"] = anchor - all_list_attrs.update({ - "x-show": model, - "x-cloak": "", - }) + all_list_attrs.update( + { + "x-show": model, + "x-cloak": "", + }, + ) return { "model": model, @@ -1613,10 +1623,12 @@ class Menu(Component): """ + ##################################### # MENU LIST ##################################### + def _normalize_item(item: Union[MenuItem, str]): # Wrap plain value in MenuItem if not isinstance(item, MenuItem): @@ -1714,10 +1726,12 @@ class MenuList(Component): """ # noqa: E501 + ##################################### # TABLE ##################################### + class TableHeader(NamedTuple): """Table header data structure used with the `table` components.""" @@ -1771,8 +1785,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}" + f"TableCell.colspan must be a non-negative integer. Instead got {self.colspan}", ) @@ -1882,9 +1895,7 @@ class Table(Component): rows: List[TableRow], attrs: Optional[dict] = None, ): - rows_to_render = [ - tuple([row, prepare_row_headers(row, headers)]) for row in rows - ] + rows_to_render = [(row, prepare_row_headers(row, headers)) for row in rows] return { "headers": headers, @@ -1961,10 +1972,12 @@ class Table(Component): """ # noqa: E501 + ##################################### # ICON ##################################### + @register("Icon") class Icon(Component): def get_context_data( @@ -2059,9 +2072,13 @@ class Icon(Component): ICONS = { "outline": { "academic-cap": [ - {'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 - ] - } + { + "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 + }, + ], + }, } @@ -2075,9 +2092,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): @@ -2161,17 +2177,19 @@ class HeroIcon(Component): "attrs": kwargs.attrs, } + ##################################### # EXPANSION PANEL ##################################### + @register("ExpansionPanel") class ExpansionPanel(Component): def get_context_data( self, /, *, - open: Optional[bool] = False, + open: Optional[bool] = False, # noqa: A002 panel_id: Optional[str] = None, attrs: Optional[dict] = None, header_attrs: Optional[dict] = None, @@ -2253,10 +2271,12 @@ class ExpansionPanel(Component): }); """ + ##################################### # PROJECT_PAGE ##################################### + # Tabs on this page and the query params to open specific tabs on page load. class ProjectPageTabsToQueryParams(Enum): PROJECT_INFO = {"tabs-proj-right": "1"} @@ -2290,7 +2310,7 @@ class ProjectPage(Component): breadcrumbs: Optional[List["Breadcrumb"]] = None, ): rendered_phases: List[ListItem] = [] - phases_by_type = {p['phase_template']['type']: p for p in phases} + phases_by_type = {p["phase_template"]["type"]: p for p in phases} for phase_meta in PROJECT_PHASES_META.values(): phase = phases_by_type[phase_meta.type] title = phase_titles[phase_meta.type] @@ -2298,7 +2318,7 @@ class ProjectPage(Component): ListItem( value=title, link=f"/projects/{project['id']}/phases/{phase['phase_template']['type']}", - ) + ), ) redirect_url = f"/projects/{project['id']}" @@ -2402,10 +2422,12 @@ class ProjectPage(Component): {% endcomponent %} """ + ##################################### # PROJECT_LAYOUT_TABBED ##################################### + class ProjectLayoutData(NamedTuple): request: HttpRequest active_projects: List[Project] @@ -2540,10 +2562,12 @@ class ProjectLayoutTabbed(Component): {% endcomponent %} """ + ##################################### # LAYOUT ##################################### + class LayoutData(NamedTuple): request: HttpRequest active_projects: List[Project] @@ -2653,12 +2677,14 @@ class Layout(Component): # RENDER_CONTEXT_PROVIDER ##################################### + class RenderContext(NamedTuple): """ Data that's commonly available in all template rendering. In templates, we can assume that the data defined here is ALWAYS defined. """ + request: HttpRequest user: User csrf_token: str @@ -2694,10 +2720,12 @@ class RenderContextProvider(Component): {% endprovide %} """ + ##################################### # BASE ##################################### + @register("Base") class Base(Component): def get_context_data(self) -> dict: @@ -2989,10 +3017,12 @@ class Base(Component): }; """ + ##################################### # SIDEBAR ##################################### + class SidebarItem(NamedTuple): name: str icon: Optional[str] = None @@ -3017,7 +3047,7 @@ def gen_sidebar_menu_items(active_projects: List[Project]) -> List[SidebarItem]: href="/projects", children=[ SidebarItem( - name=project['name'], + name=project["name"], icon=None, href=f"/projects/{project['id']}", ) @@ -3160,10 +3190,12 @@ class Sidebar(Component): """ + ##################################### # NAVBAR ##################################### + @register("Navbar") class Navbar(Component): def get_context_data( @@ -3212,10 +3244,12 @@ class Navbar(Component): """ # noqa: E501 + ##################################### # DIALOG ##################################### + def construct_btn_onclick(model: str, btn_on_click: Optional[str]): """ We want to allow the component users to define Alpine.js `@click` actions. @@ -3422,10 +3456,12 @@ class Dialog(Component): """ # noqa: E501 + ##################################### # TAGS ##################################### + class TagEntry(NamedTuple): tag: str selected: bool = False @@ -3445,7 +3481,7 @@ class Tags(Component): tag_type: str, js_props: dict, editable: bool = True, - max_width: Union[int, str] = '300px', + max_width: Union[int, str] = "300px", attrs: Optional[dict] = None, ): all_tags = TAG_TYPE_META[tag_type.upper()].allowed_values # type: ignore[index] @@ -3654,17 +3690,19 @@ class Tags(Component): }); """ + ##################################### # FORM ##################################### + @register("Form") class Form(Component): def get_context_data( self, /, *, - type: Literal["table", "paragraph", "ul", None] = None, + type: Literal["table", "paragraph", "ul", None] = None, # noqa: A002 editable: bool = True, method: str = "post", # Submit btn @@ -3830,15 +3868,15 @@ class Form(Component): }); """ + ##################################### # BREADCRUMBS ##################################### + @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.""" @@ -3958,7 +3996,7 @@ class Bookmarks(Component): attachment_data: List[BookmarkItem] = [] for bookmark in bookmarks: - is_attachment = bookmark['attachment'] is not None + is_attachment = bookmark["attachment"] is not None if is_attachment: # Send user to the Output tab in Project page and open and scroll @@ -3972,9 +4010,9 @@ class Bookmarks(Component): edit_url = f"/edit/{project_id}/bookmark/{bookmark['id']}" entry = BookmarkItem( - text=bookmark['text'], - url=bookmark['url'], - id=bookmark['id'], + text=bookmark["text"], + url=bookmark["url"], + id=bookmark["id"], edit_url=edit_url, ) @@ -4129,10 +4167,12 @@ class Bookmarks(Component): }); """ + ##################################### # BOOKMARK ##################################### + class BookmarkItem(NamedTuple): id: int text: str @@ -4217,10 +4257,12 @@ class Bookmark(Component): }); """ + ##################################### # LIST ##################################### + @dataclass(frozen=True) class ListItem: """ @@ -4287,10 +4329,12 @@ class ListComponent(Component): """ # noqa: E501 + ##################################### # TABS ##################################### + class TabEntry(NamedTuple): header: str content: str @@ -4519,7 +4563,7 @@ class TabItem(Component): raise RuntimeError( f"Component '{self.name}' was called with no parent Tabs component. " f"Either wrap '{self.name}' in Tabs component, or check if the component " - f"is not a descendant of another instance of '{self.name}'" + f"is not a descendant of another instance of '{self.name}'", ) parent_tabs = tab_ctx.tabs @@ -4532,11 +4576,13 @@ class TabItem(Component): def on_render_after(self, context, template, content, error=None) -> None: parent_tabs: List[dict] = context["parent_tabs"] - parent_tabs.append({ - "header": context["header"], - "disabled": context["disabled"], - "content": mark_safe(content.strip()), - }) + parent_tabs.append( + { + "header": context["header"], + "disabled": context["disabled"], + "content": mark_safe(content.strip()), + }, + ) template: types.django_html = """ {% provide "_tab" tabs=empty_tabs enabled=False %} @@ -4623,6 +4669,7 @@ class TabsStatic(Component): # PROJECT_INFO ##################################### + class ProjectInfoEntry(NamedTuple): title: str value: str @@ -4655,9 +4702,9 @@ class ProjectInfo(Component): ] project_info = [ - ProjectInfoEntry("Org", project['organization']['name']), + ProjectInfoEntry("Org", project["organization"]["name"]), ProjectInfoEntry("Duration", f"{project['start_date']} - {project['end_date']}"), - ProjectInfoEntry("Status", project['status']), + ProjectInfoEntry("Status", project["status"]), ProjectInfoEntry("Tags", ", ".join(project_tags) or "-"), ] @@ -4781,21 +4828,19 @@ class ProjectInfo(Component): """ + ##################################### # PROJECT_NOTES ##################################### + 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'] - ) + modified_time_str = format_timestamp(datetime.fromisoformat(comment["modified"])) + formatted_modified_by = modified_time_str + " " + comment["modified_by"]["name"] return { "timestamp": formatted_modified_by, - "notes": comment['text'], + "notes": comment["text"], "edit_href": f"/update/{note['project']['id']}/note/{note['id']}/comment/{comment['id']}/", } @@ -4806,17 +4851,17 @@ def _make_notes_data( ): notes_data: List[dict] = [] for note in notes: - comments = comments_by_notes.get(note['id'], []) + comments = comments_by_notes.get(note["id"], []) comments_data = [_make_comments_data(note, comment) for comment in comments] notes_data.append( { - "text": note['text'], - "timestamp": note['created'], + "text": note["text"], + "timestamp": note["created"], "edit_href": f"/edit/{note['project']['id']}/note/{note['id']}/", "comments": comments_data, "create_comment_url": f"/create/{note['project']['id']}/note/{note['id']}/", - } + }, ) return notes_data @@ -4920,6 +4965,7 @@ class ProjectNotes(Component): # PROJECT_OUTPUTS_SUMMARY ##################################### + class AttachmentWithTags(NamedTuple): attachment: ProjectOutputAttachment tags: List[str] @@ -4947,7 +4993,7 @@ class ProjectOutputsSummary(Component): editable: bool, phase_titles: Dict[ProjectPhaseType, str], ): - outputs_by_phase = group_by(outputs, lambda output, _: output[0]['phase']['phase_template']['type']) + outputs_by_phase = group_by(outputs, lambda output, _: output[0]["phase"]["phase_template"]["type"]) groups: List[dict] = [] for phase_meta in PROJECT_PHASES_META.values(): @@ -4959,7 +5005,7 @@ class ProjectOutputsSummary(Component): "phase_type": phase_meta.type, "outputs": phase_outputs, "has_outputs": bool(phase_outputs), - } + }, ) return { @@ -5003,24 +5049,20 @@ class ProjectOutputsSummary(Component): # PROJECT_STATUS_UPDATES ##################################### + 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'] - ) + modified_time_str = format_timestamp(datetime.fromisoformat(status_update["modified"])) + formatted_modified_by = modified_time_str + " " + status_update["modified_by"]["name"] return { "timestamp": formatted_modified_by, - "text": status_update['text'], + "text": status_update["text"], "edit_href": f"/edit/{status_update['project']['id']}/status_update/{status_update['id']}", } @register("ProjectStatusUpdates") class ProjectStatusUpdates(Component): - def get_context_data( self, /, @@ -5125,14 +5167,14 @@ class ProjectUsers(Component): ): roles_table_rows = [] for role in roles_with_users: - user = role['user'] + user = role["user"] if editable: delete_action = ProjectUserAction.render( kwargs={ - "user_name": user['name'], + "user_name": user["name"], "project_id": project_id, - "role_id": role['id'], + "role_id": role["id"], }, deps_strategy="ignore", ) @@ -5142,27 +5184,20 @@ class ProjectUsers(Component): roles_table_rows.append( create_table_row( cols={ - "name": TableCell(user['name']), - "role": TableCell(role['name']), + "name": TableCell(user["name"]), + "role": TableCell(role["name"]), "delete": delete_action, - } - ) + }, + ), ) submit_url = f"/submit/{project_id}/role/create" project_url = f"/project/{project_id}" - if available_roles: - available_role_choices = [ - (role, role) for role in available_roles - ] - else: - available_role_choices = [] + available_role_choices = [(role, role) for role in available_roles] if available_roles else [] if available_users: - available_user_choices = [ - (str(user['id']), user['name']) for user in available_users - ] + available_user_choices = [(str(user["id"]), user["name"]) for user in available_users] else: available_user_choices = [] @@ -5264,13 +5299,14 @@ class ProjectUsers(Component): }); """ + ##################################### # PROJECT_USER_ACTION ##################################### + @register("ProjectUserAction") class ProjectUserAction(Component): - def get_context_data( self, /, @@ -5308,6 +5344,7 @@ class ProjectUserAction(Component): """ + ##################################### # PROJECT_OUTPUTS ##################################### @@ -5315,7 +5352,6 @@ class ProjectUserAction(Component): @register("ProjectOutputs") class ProjectOutputs(Component): - def get_context_data( self, /, @@ -5331,11 +5367,13 @@ class ProjectOutputs(Component): attach_data: List[RenderedAttachment] = [] for attachment in attachments: - attach_data.append(RenderedAttachment( - url=attachment[0]['url'], - text=attachment[0]['text'], - tags=attachment[1], - )) + attach_data.append( + RenderedAttachment( + url=attachment[0]["url"], + text=attachment[0]["text"], + tags=attachment[1], + ), + ) update_output_url = "/update" @@ -5349,18 +5387,16 @@ class ProjectOutputs(Component): phase_url=phase_url, attachments=[ { - "url": d.attachment['url'], - "text": d.attachment['text'], + "url": d.attachment["url"], + "text": d.attachment["text"], "tags": d.tags, } 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) outputs_data.append( RenderedProjectOutput( @@ -5372,7 +5408,7 @@ class ProjectOutputs(Component): }, attachments=attach_data, update_output_url=update_output_url, - ) + ), ) return { @@ -5421,10 +5457,12 @@ class ProjectOutputs(Component): """ + ##################################### # PROJECT_OUTPUT_BADGE ##################################### + @register("ProjectOutputBadge") class ProjectOutputBadge(Component): def get_context_data( @@ -5476,10 +5514,12 @@ class ProjectOutputBadge(Component):
""" # noqa: E501 + ##################################### # PROJECT_OUTPUT_DEPENDENCY ##################################### + @register("ProjectOutputDependency") class ProjectOutputDependency(Component): def get_context_data(self, /, *, dependency: "RenderedOutputDep"): @@ -5578,10 +5618,12 @@ class ProjectOutputDependency(Component): }); """ + ##################################### # PROJECT_OUTPUT_ATTACHMENTS ##################################### + class ProjectOutputAttachmentsJsProps(TypedDict): attachments: str @@ -6008,6 +6050,7 @@ class ProjectOutputForm(Component): from django_components.testing import djc_test # noqa: E402 + @djc_test def test_render(snapshot): registry.register("Button", Button) diff --git a/tests/test_benchmark_djc_small.py b/tests/test_benchmark_djc_small.py index 5c96b3bc..31a6832f 100644 --- a/tests/test_benchmark_djc_small.py +++ b/tests/test_benchmark_djc_small.py @@ -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() diff --git a/tests/test_cache.py b/tests/test_cache.py index 58630a93..327f506f 100644 --- a/tests/test_cache.py +++ b/tests/test_cache.py @@ -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() == "" diff --git a/tests/test_command_components.py b/tests/test_command_components.py index b47bf7e8..887888db 100644 --- a/tests/test_command_components.py +++ b/tests/test_command_components.py @@ -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}) diff --git a/tests/test_command_create.py b/tests/test_command_create.py index 761861f1..2e6dedc4 100644 --- a/tests/test_command_create.py +++ b/tests/test_command_create.py @@ -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) diff --git a/tests/test_command_ext.py b/tests/test_command_ext.py index c43dc54a..d7432b2f 100644 --- a/tests/test_command_ext.py +++ b/tests/test_command_ext.py @@ -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() ) diff --git a/tests/test_command_list.py b/tests/test_command_list.py index 2d3531bd..fd83e5f8 100644 --- a/tests/test_command_list.py +++ b/tests/test_command_list.py @@ -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..TestComponent tests/test_command_list.py - r"tests\.test_command_list\.TestComponentListCommand\.test_list_default\.\.TestComponent\s+tests{SLASH}test_command_list\.py".format( - SLASH=SLASH - ) + r"tests\.test_command_list\.TestComponentListCommand\.test_list_default\.\.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..TestComponent tests/test_command_list.py - r"TestComponent\s+tests\.test_command_list\.TestComponentListCommand\.test_list_all\.\.TestComponent\s+tests{SLASH}test_command_list\.py".format( - SLASH=SLASH - ) + r"TestComponent\s+tests\.test_command_list\.TestComponentListCommand\.test_list_all\.\.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..TestComponent - r"TestComponent\s+tests\.test_command_list\.TestComponentListCommand\.test_list_specific_columns\.\.TestComponent" + r"TestComponent\s+tests\.test_command_list\.TestComponentListCommand\.test_list_specific_columns\.\.TestComponent", ).search(output) def test_list_simple(self): @@ -166,25 +168,28 @@ class TestComponentListCommand: # tests.test_command_list.TestComponentListCommand.test_list_simple..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..TestComponent tests/test_command_list.py - r"tests\.test_command_list\.TestComponentListCommand\.test_list_simple\.\.TestComponent\s+tests{SLASH}test_command_list\.py".format( - SLASH=SLASH - ) + r"tests\.test_command_list\.TestComponentListCommand\.test_list_simple\.\.TestComponent\s+tests{SLASH}test_command_list\.py".format( # noqa: UP032 + SLASH=SLASH, + ), ).search(output) diff --git a/tests/test_component.py b/tests/test_component.py index 28d2d71e..80fdff8e 100644 --- a/tests/test_component.py +++ b/tests/test_component.py @@ -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: diff --git a/tests/test_component_cache.py b/tests/test_component_cache.py index b2ec2dc3..69ccb221 100644 --- a/tests/test_component_cache.py +++ b/tests/test_component_cache.py @@ -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) == "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"}, ) diff --git a/tests/test_component_defaults.py b/tests/test_component_defaults.py index e8ec6d6f..9ed0a3dd 100644 --- a/tests/test_component_defaults.py +++ b/tests/test_component_defaults.py @@ -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}) diff --git a/tests/test_component_highlight.py b/tests/test_component_highlight.py index 7aa1c65a..6ab7dcfe 100644 --- a/tests/test_component_highlight.py +++ b/tests/test_component_highlight.py @@ -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 = """ diff --git a/tests/test_component_media.py b/tests/test_component_media.py index 3bce7716..96c73f1c 100644 --- a/tests/test_component_media.py +++ b/tests/test_component_media.py @@ -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 %}
Content
- """ + """, ) 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 %}
Content
- """ + """, ) 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: test' 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: {{ variable }}\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'') @@ -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('', rendered) assertInHTML( - '', rendered + '', + rendered, ) assertInHTML( - '', rendered + '', + rendered, ) # `://` is escaped because Django's `Media.absolute_path()` doesn't consider `://` a valid URL assertInHTML( - '', rendered + '', + rendered, ) assertInHTML('', 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'') @@ -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'') @@ -889,7 +895,7 @@ class TestMediaRelativePath: {% endcomponent %} {% endslot %} - """ # 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"}) diff --git a/tests/test_component_typing.py b/tests/test_component_typing.py index 33a9297d..8d59bc9d 100644 --- a/tests/test_component_typing.py +++ b/tests/test_component_typing.py @@ -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 ), ) diff --git a/tests/test_component_view.py b/tests/test_component_view.py index 1b1a7662..d35e866a 100644 --- a/tests/test_component_view.py +++ b/tests/test_component_view.py @@ -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( '', 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( '', 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( '', 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( '', 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( '', 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( '', 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"', rendered, count=1 - ) # Inlined JS + assertInHTML('', rendered, count=1) # Inlined JS assertInHTML('', rendered, count=1) # Media.css assert rendered.count("console.log("xyz");', rendered, count=1 - ) # Inlined JS + assertInHTML('', rendered, count=1) # Inlined JS assertInHTML('', rendered, count=1) # Media.css assert rendered.count("foo\n' - ' \n' + " \n" ' ' ) @@ -541,7 +537,7 @@ class TestRenderDependencies: "toLoadJsTags": ["PHNjcmlwdCBzcmM9InNjcmlwdC5qcyI+PC9zY3JpcHQ+", "PHNjcmlwdCBzcmM9Ii9jb21wb25lbnRzL2NhY2hlL1NpbXBsZUNvbXBvbmVudF8zMTEwOTcuanMiPjwvc2NyaXB0Pg=="]} - """ # noqa: E501 + """ assertHTMLEqual(expected, rendered) @@ -555,7 +551,9 @@ class TestRenderDependencies: with pytest.raises( RuntimeError, - match=re.escape("Content of `Component.js` for component 'ComponentWithScript' contains '' end tag."), # noqa: E501 + match=re.escape( + "Content of `Component.js` for component 'ComponentWithScript' contains '' end tag.", + ), ): ComponentWithScript.render(kwargs={"variable": "foo"}) @@ -572,7 +570,9 @@ class TestRenderDependencies: with pytest.raises( RuntimeError, - match=re.escape("Content of `Component.css` for component 'ComponentWithScript' contains '' end tag."), # noqa: E501 + match=re.escape( + "Content of `Component.css` for component 'ComponentWithScript' contains '' end tag.", + ), ): ComponentWithScript.render(kwargs={"variable": "foo"}) @@ -1161,6 +1161,6 @@ class TestDependenciesStrategyRaw: assert rendered_raw.strip() == ( '\n' ' \n' - ' \n' + " \n" ' Variable: foo' ) diff --git a/tests/test_dependency_manager.py b/tests/test_dependency_manager.py index 7b293f3f..274a0f4a 100644 --- a/tests/test_dependency_manager.py +++ b/tests/test_dependency_manager.py @@ -1,13 +1,15 @@ import re -from typing import List +from typing import TYPE_CHECKING, List import pytest from playwright.async_api import Browser, Error, Page -from django_components import types from django_components.testing import djc_test -from tests.testutils import setup_test_config from tests.e2e.utils import TEST_SERVER_URL, with_playwright +from tests.testutils import setup_test_config + +if TYPE_CHECKING: + from django_components import types setup_test_config( components={"autodiscover": False}, @@ -33,7 +35,7 @@ async def _create_page_with_dep_manager(browser: Browser) -> Page: () => { eval(document.body.textContent); } - """ + """, ) # Ensure the body is clear @@ -43,7 +45,7 @@ async def _create_page_with_dep_manager(browser: Browser) -> Page: document.body.innerHTML = ''; document.head.innerHTML = ''; } - """ + """, ) return page @@ -52,7 +54,7 @@ async def _create_page_with_dep_manager(browser: Browser) -> Page: @djc_test( django_settings={ "STATIC_URL": "static/", - } + }, ) class TestDependencyManager: @with_playwright @@ -80,7 +82,7 @@ class TestDependencyManager: @djc_test( django_settings={ "STATIC_URL": "static/", - } + }, ) class TestLoadScript: @with_playwright @@ -207,7 +209,7 @@ class TestLoadScript: @djc_test( django_settings={ "STATIC_URL": "static/", - } + }, ) class TestCallComponent: @with_playwright diff --git a/tests/test_dependency_rendering.py b/tests/test_dependency_rendering.py index 336d6fd9..0e52f8da 100644 --- a/tests/test_dependency_rendering.py +++ b/tests/test_dependency_rendering.py @@ -3,6 +3,8 @@ Here we check that the logic around dependency rendering outputs correct HTML. During actual rendering, the HTML is then picked up by the JS-side dependency manager. """ +# ruff: noqa: E501 + import re from django.template import Context, Template @@ -126,8 +128,7 @@ class TestDependencyRendering: assert "toLoadCssTags" not in rendered assert rendered.strip() == ( - '\n' - ' ' + '\n ' ) def test_no_js_dependencies_when_no_components_used(self): @@ -387,8 +388,7 @@ class TestDependencyRendering: assert "toLoadCssTags" not in rendered assert rendered.strip() == ( - '\n' - ' ' + '\n ' ) def test_multiple_components_dependencies(self): diff --git a/tests/test_dependency_rendering_e2e.py b/tests/test_dependency_rendering_e2e.py index faebcddb..2d646bfa 100644 --- a/tests/test_dependency_rendering_e2e.py +++ b/tests/test_dependency_rendering_e2e.py @@ -4,14 +4,18 @@ in an actual browser. """ import re +from typing import TYPE_CHECKING -from playwright.async_api import Page from pytest_django.asserts import assertHTMLEqual, assertInHTML -from django_components import types from django_components.testing import djc_test -from tests.testutils import setup_test_config from tests.e2e.utils import TEST_SERVER_URL, with_playwright +from tests.testutils import setup_test_config + +if TYPE_CHECKING: + from playwright.async_api import Page + + from django_components import types setup_test_config({"autodiscover": False}) @@ -48,9 +52,10 @@ class TestE2eDependencyRendering: data = await page.evaluate(test_js) # Check that the actual HTML content was loaded - assert re.compile( - r'Variable: foo' - ).search(data["bodyHTML"]) is not None + assert ( + re.compile(r'Variable: foo').search(data["bodyHTML"]) + is not None + ) assertInHTML('
123
', data["bodyHTML"], count=1) assertInHTML('
xyz
', data["bodyHTML"], count=1) @@ -112,32 +117,35 @@ class TestE2eDependencyRendering: data = await page.evaluate(test_js) # Check that the actual HTML content was loaded - assert re.compile( - #
- # Variable: - # - # variable - # - # XYZ: - # - # variable_inner - # - #
- #
123
- #
xyz
- r'
\s*' - r"Variable:\s*" - r'\s*' - r"variable\s*" - r"<\/strong>\s*" - r"XYZ:\s*" - r'\s*' - r"variable_inner\s*" - r"<\/strong>\s*" - r"<\/div>\s*" - r'
123<\/div>\s*' - r'
xyz<\/div>\s*' - ).search(data["bodyHTML"]) is not None + assert ( + re.compile( + #
+ # Variable: + # + # variable + # + # XYZ: + # + # variable_inner + # + #
+ #
123
+ #
xyz
+ r'
\s*' + r"Variable:\s*" + r'\s*' + r"variable\s*" + r"<\/strong>\s*" + r"XYZ:\s*" + r'\s*' + r"variable_inner\s*" + r"<\/strong>\s*" + r"<\/div>\s*" + r'
123<\/div>\s*' + r'
xyz<\/div>\s*', + ).search(data["bodyHTML"]) + is not None + ) # Check components' inlined JS got loaded assert data["component1JsMsg"] == "kapowww!" @@ -216,20 +224,23 @@ class TestE2eDependencyRendering: #
#
123
#
xyz
- assert re.compile( - r'
\s*' - r"Variable:\s*" - r'\s*' - r"variable\s*" - r"<\/strong>\s*" - r"XYZ:\s*" - r'\s*' - r"variable_inner\s*" - r"<\/strong>\s*" - r"<\/div>\s*" - r'
123<\/div>\s*' - r'
xyz<\/div>\s*' - ).search(data["bodyHTML"]) is not None + assert ( + re.compile( + r'
\s*' + r"Variable:\s*" + r'\s*' + r"variable\s*" + r"<\/strong>\s*" + r"XYZ:\s*" + r'\s*' + r"variable_inner\s*" + r"<\/strong>\s*" + r"<\/div>\s*" + r'
123<\/div>\s*' + r'
xyz<\/div>\s*', + ).search(data["bodyHTML"]) + is not None + ) # Check components' inlined JS did NOT get loaded assert data["component1JsMsg"] is None @@ -358,7 +369,7 @@ class TestE2eDependencyRendering: # Wait until both JS and CSS are loaded await page.locator(".frag").wait_for(state="visible") await page.wait_for_function( - "() => document.head.innerHTML.includes(' document.head.innerHTML.includes('\s*' r"123\s*" r'xxx\s*' r"
" - ).search(data["fragHtml"]) is not None + assert ( + re.compile( + r'
\s*' + r"123\s*" + r'xxx\s*' + r"
", + ).search(data["fragHtml"]) + is not None + ) assert "rgb(0, 0, 255)" in data["fragBg"] # AKA 'background: blue' await page.close() @@ -427,9 +444,15 @@ class TestE2eDependencyRendering: data = await page.evaluate(test_js) assert data["targetHtml"] is None - assert re.compile( - r'
\s*' r"123\s*" r'xxx\s*' r"
" - ).search(data["fragHtml"]) is not None + assert ( + re.compile( + r'
\s*' + r"123\s*" + r'xxx\s*' + r"
", + ).search(data["fragHtml"]) + is not None + ) assert "rgb(0, 0, 255)" in data["fragBg"] # AKA 'background: blue' await page.close() @@ -460,7 +483,7 @@ class TestE2eDependencyRendering: # Wait until both JS and CSS are loaded await page.locator(".frag").wait_for(state="visible") await page.wait_for_function( - "() => document.head.innerHTML.includes(' document.head.innerHTML.includes('\s*' r"123\s*" r'xxx\s*' r"
" - ).search(data["targetHtml"]) is not None + assert ( + re.compile( + r'
\s*' + r"123\s*" + r'xxx\s*' + r"
", + ).search(data["targetHtml"]) + is not None + ) assert "rgb(0, 0, 255)" in data["fragBg"] # AKA 'background: blue' await page.close() @@ -513,7 +542,7 @@ class TestE2eDependencyRendering: # Wait until both JS and CSS are loaded await page.locator(".frag").wait_for(state="visible") await page.wait_for_function( - "() => document.head.innerHTML.includes(' document.head.innerHTML.includes(' document.head.innerHTML.includes(' document.head.innerHTML.includes('\s*' r"123\s*" r'xxx\s*' r"
" - ).search(data["fragHtml"]) is not None + assert ( + re.compile( + r'
\s*' + r"123\s*" + r'xxx\s*' + r"
", + ).search(data["fragHtml"]) + is not None + ) assert "rgb(0, 0, 255)" in data["fragBg"] # AKA 'background: blue' await page.close() @@ -599,7 +634,7 @@ class TestE2eDependencyRendering: await page.goto(single_comp_url) component_text = await page.locator('[x-data="alpine_test"]').text_content() - assertHTMLEqual(component_text.strip(), "ALPINE_TEST: 123") + assertHTMLEqual(component_text.strip(), "ALPINE_TEST: 123") # type: ignore[union-attr] await page.close() @@ -611,7 +646,7 @@ class TestE2eDependencyRendering: await page.goto(single_comp_url) component_text = await page.locator('[x-data="alpine_test"]').text_content() - assertHTMLEqual(component_text.strip(), "ALPINE_TEST: 123") + assertHTMLEqual(component_text.strip(), "ALPINE_TEST: 123") # type: ignore[union-attr] await page.close() @@ -623,7 +658,7 @@ class TestE2eDependencyRendering: await page.goto(single_comp_url) component_text = await page.locator('[x-data="alpine_test"]').text_content() - assertHTMLEqual(component_text.strip(), "ALPINE_TEST: 123") + assertHTMLEqual(component_text.strip(), "ALPINE_TEST: 123") # type: ignore[union-attr] await page.close() @@ -635,6 +670,6 @@ class TestE2eDependencyRendering: await page.goto(single_comp_url) component_text = await page.locator('[x-data="alpine_test"]').text_content() - assertHTMLEqual(component_text.strip(), "ALPINE_TEST:") + assertHTMLEqual(component_text.strip(), "ALPINE_TEST:") # type: ignore[union-attr] await page.close() diff --git a/tests/test_expression.py b/tests/test_expression.py index 8694043e..896c2925 100644 --- a/tests/test_expression.py +++ b/tests/test_expression.py @@ -10,8 +10,8 @@ from pytest_django.asserts import assertHTMLEqual from django_components import Component, register, registry, types from django_components.expression import DynamicFilterExpression, is_aggregate_key - from django_components.testing import djc_test + from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config setup_test_config({"autodiscover": False}) @@ -85,8 +85,7 @@ class TestDynamicExpr:
{{ list_var|safe }}
""" - template_str: types.django_html = ( - """ + template_str: types.django_html = """ {% load component_tags %} {% component 'test' "{{ var_a|lower }}" @@ -94,7 +93,6 @@ class TestDynamicExpr: list_var="{{ list|slice:':-1' }}" / %} """ - ) template = Template(template_str) rendered = template.render( @@ -103,7 +101,7 @@ class TestDynamicExpr: "var_a": "LoREM", "is_active": True, "list": [{"a": 1}, {"a": 2}, {"a": 3}], - } + }, ), ) @@ -149,8 +147,7 @@ class TestDynamicExpr:
{{ dict_var|safe }}
""" - template_str: types.django_html = ( - """ + template_str: types.django_html = """ {% load component_tags %} {% component 'test' "{% lorem var_a w %}" @@ -159,7 +156,6 @@ class TestDynamicExpr: dict_var="{% noop dict %}" / %} """ - ) template = Template(template_str) rendered = template.render( @@ -169,7 +165,7 @@ class TestDynamicExpr: "is_active": True, "list": [{"a": 1}, {"a": 2}], "dict": {"a": 3}, - } + }, ), ) @@ -216,8 +212,7 @@ class TestDynamicExpr:
{{ list_var|safe }}
""" - template_str: types.django_html = ( - """ + template_str: types.django_html = """ {% load component_tags %} {% component 'test' "{# lorem var_a w #}" @@ -226,7 +221,6 @@ class TestDynamicExpr: list_var=" {# noop list #} " / %} """ - ) template = Template(template_str) rendered: str = template.render( @@ -236,7 +230,7 @@ class TestDynamicExpr: "var_a": 3, "is_active": True, "list": [{"a": 1}, {"a": 2}], - } + }, ), ) @@ -284,8 +278,7 @@ class TestDynamicExpr:
{{ dict_var|safe }}
""" - template_str: types.django_html = ( - """ + template_str: types.django_html = """ {% load component_tags %} {% component 'test' " {% lorem var_a w %} " @@ -295,7 +288,6 @@ class TestDynamicExpr: dict_var=" {% noop dict %} " / %} """ - ) template = Template(template_str) rendered: str = template.render( @@ -306,7 +298,7 @@ class TestDynamicExpr: "is_active": True, "list": [{"a": 1}, {"a": 2}], "dict": {"a": 3}, - } + }, ), ) @@ -316,6 +308,7 @@ class TestDynamicExpr: assert captured["list_var"] == " [{'a': 1}, {'a': 2}] " # NOTE: This is whitespace-sensitive test, so we check exact output + # fmt: off assert rendered.strip() == ( "\n" '
lorem ipsum dolor
\n' @@ -324,6 +317,7 @@ class TestDynamicExpr: '
[{\'a\': 1}, {\'a\': 2}]
\n' '
{\'a\': 3}
' ) + # fmt: on @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) def test_ignores_invalid_tag(self, components_settings): @@ -344,12 +338,10 @@ class TestDynamicExpr:
{{ bool_var }}
""" - template_str: types.django_html = ( - """ + template_str: types.django_html = """ {% load component_tags %} {% component 'test' '"' "{%}" bool_var="{% noop is_active %}" / %} """ - ) template = Template(template_str) rendered = template.render( @@ -383,15 +375,13 @@ class TestDynamicExpr:
{{ bool_var }}
""" - template_str: types.django_html = ( - """ + template_str: types.django_html = """ {% load component_tags %} {% component 'test' "{% component 'test' '{{ var_a }}' bool_var=is_active / %}" bool_var="{% noop is_active %}" / %} """ - ) template = Template(template_str) rendered = template.render( @@ -399,7 +389,7 @@ class TestDynamicExpr: { "var_a": 3, "is_active": True, - } + }, ), ) @@ -441,8 +431,7 @@ class TestSpreadOperator:
{{ x }}
""" - template_str: types.django_html = ( - """ + template_str: types.django_html = """ {% load component_tags %} {% component 'test' var_a @@ -451,7 +440,6 @@ class TestSpreadOperator: x=123 / %} """ - ) template = Template(template_str) rendered = template.render( @@ -464,7 +452,7 @@ class TestSpreadOperator: "items": [1, 2, 3], }, "list": [{"a": 1}, {"a": 2}, {"a": 3}], - } + }, ), ) @@ -560,7 +548,7 @@ class TestSpreadOperator: "data": "slot_data", "default": "slot_default", }, - } + }, ), ) @@ -607,7 +595,7 @@ class TestSpreadOperator: "items": [1, 2, 3], }, "list": [{"a": 1}, {"a": 2}, {"a": 3}], - } + }, ), ) @@ -636,7 +624,7 @@ class TestSpreadOperator: "defaults:class": "my-class", "defaults:style": "NONO", }, - } + }, ), ) assertHTMLEqual( @@ -670,13 +658,12 @@ class TestSpreadOperator: "items": [1, 2, 3], }, "list": [{"a": 1, "x": "OVERWRITTEN_X"}, {"a": 2}, {"a": 3}], - } + }, ) # Mergingg like this will raise TypeError, because it's like # a function receiving multiple kwargs with the same name. - template_str1: types.django_html = ( - """ + template_str1: types.django_html = """ {% load component_tags %} {% component 'test' ...my_dict @@ -685,7 +672,6 @@ class TestSpreadOperator: ..."{{ list|first }}" / %} """ - ) template1 = Template(template_str1) @@ -694,8 +680,7 @@ class TestSpreadOperator: # But, similarly to python, we can merge multiple **kwargs by instead # merging them into a single dict, and spreading that. - template_str2: types.django_html = ( - """ + template_str2: types.django_html = """ {% load component_tags %} {% component 'test' ...{ @@ -706,7 +691,6 @@ class TestSpreadOperator: attrs:style="OVERWRITTEN" / %} """ - ) template2 = Template(template_str2) rendered2 = template2.render(context) @@ -727,15 +711,13 @@ class TestSpreadOperator: class SimpleComponent(Component): pass - template_str: types.django_html = ( - """ + template_str: types.django_html = """ {% load component_tags %} {% component 'test' var_a ... / %} """ - ) with pytest.raises(TemplateSyntaxError, match=re.escape("Spread syntax '...' is missing a value")): Template(template_str) @@ -752,22 +734,20 @@ class TestSpreadOperator: nonlocal captured captured = args, kwargs - template_str: types.django_html = ( - """ + template_str: types.django_html = """ {% load component_tags %} {% component 'test' ...var_a ...var_b / %} """ - ) template = Template(template_str) context = Context( { "var_a": "abc", "var_b": [1, 2, 3], - } + }, ) template.render(context) @@ -783,21 +763,19 @@ class TestSpreadOperator: class SimpleComponent(Component): pass - template_str: types.django_html = ( - """ + template_str: types.django_html = """ {% load component_tags %} {% component 'test' ...var_b / %} """ - ) template = Template(template_str) # List with pytest.raises( ValueError, - match=re.escape("Cannot spread non-iterable value: '...var_b' resolved to 123") + match=re.escape("Cannot spread non-iterable value: '...var_b' resolved to 123"), ): template.render(Context({"var_b": 123})) diff --git a/tests/test_extension.py b/tests/test_extension.py index cfd1c1cc..62ff305e 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -10,31 +10,31 @@ from django_components import Component, Slot, SlotNode, register, registry from django_components.app_settings import app_settings from django_components.component_registry import ComponentRegistry from django_components.extension import ( - URLRoute, ComponentExtension, ExtensionComponentConfig, OnComponentClassCreatedContext, OnComponentClassDeletedContext, + OnComponentDataContext, + OnComponentInputContext, + OnComponentRegisteredContext, + OnComponentRenderedContext, + OnComponentUnregisteredContext, + OnCssLoadedContext, + OnJsLoadedContext, OnRegistryCreatedContext, OnRegistryDeletedContext, - OnComponentRegisteredContext, - OnComponentUnregisteredContext, - OnComponentInputContext, - OnComponentDataContext, - OnComponentRenderedContext, OnSlotRenderedContext, - OnTemplateLoadedContext, OnTemplateCompiledContext, - OnJsLoadedContext, - OnCssLoadedContext, + OnTemplateLoadedContext, + URLRoute, ) from django_components.extensions.cache import CacheExtension from django_components.extensions.debug_highlight import DebugHighlightExtension from django_components.extensions.defaults import DefaultsExtension from django_components.extensions.dependencies import DependenciesExtension from django_components.extensions.view import ViewExtension - from django_components.testing import djc_test + from .testutils import setup_test_config setup_test_config({"autodiscover": False}) @@ -46,7 +46,7 @@ def dummy_view(request: HttpRequest): return HttpResponse("Hello, world!") -def dummy_view_2(request: HttpRequest, id: int, name: str): +def dummy_view_2(request: HttpRequest, id: int, name: str): # noqa: ARG001, A002 return HttpResponse(f"Hello, world! {id} {name}") @@ -64,9 +64,7 @@ class LegacyExtension(ComponentExtension): class DummyExtension(ComponentExtension): - """ - Test extension that tracks all hook calls and their arguments. - """ + """Test extension that tracks all hook calls and their arguments.""" name = "test_extension" @@ -265,7 +263,7 @@ class TestExtension: class TestExtensionHooks: @djc_test(components_settings={"extensions": [DummyExtension]}) def test_component_class_lifecycle_hooks(self): - extension = cast(DummyExtension, app_settings.EXTENSIONS[5]) + extension = cast("DummyExtension", app_settings.EXTENSIONS[5]) assert len(extension.calls["on_component_class_created"]) == 0 assert len(extension.calls["on_component_class_deleted"]) == 0 @@ -297,7 +295,7 @@ class TestExtensionHooks: @djc_test(components_settings={"extensions": [DummyExtension]}) def test_registry_lifecycle_hooks(self): - extension = cast(DummyExtension, app_settings.EXTENSIONS[5]) + extension = cast("DummyExtension", app_settings.EXTENSIONS[5]) assert len(extension.calls["on_registry_created"]) == 0 assert len(extension.calls["on_registry_deleted"]) == 0 @@ -334,7 +332,7 @@ class TestExtensionHooks: return {"name": kwargs.get("name", "World")} registry.register("test_comp", TestComponent) - extension = cast(DummyExtension, app_settings.EXTENSIONS[5]) + extension = cast("DummyExtension", app_settings.EXTENSIONS[5]) # Verify on_component_registered was called assert len(extension.calls["on_component_registered"]) == 1 @@ -372,7 +370,7 @@ class TestExtensionHooks: test_slots = {"content": "Some content"} TestComponent.render(context=test_context, args=("arg1", "arg2"), kwargs={"name": "Test"}, slots=test_slots) - extension = cast(DummyExtension, app_settings.EXTENSIONS[5]) + extension = cast("DummyExtension", app_settings.EXTENSIONS[5]) # Verify on_component_input was called with correct args assert len(extension.calls["on_component_input"]) == 1 @@ -421,7 +419,7 @@ class TestExtensionHooks: slots={"content": "Some content"}, ) - extension = cast(DummyExtension, app_settings.EXTENSIONS[5]) + extension = cast("DummyExtension", app_settings.EXTENSIONS[5]) # Verify on_component_rendered was called with correct args assert len(extension.calls["on_component_rendered"]) == 1 @@ -450,7 +448,7 @@ class TestExtensionHooks: assert rendered == "Hello Some content!" - extension = cast(DummyExtension, app_settings.EXTENSIONS[5]) + extension = cast("DummyExtension", app_settings.EXTENSIONS[5]) # Verify on_slot_rendered was called with correct args assert len(extension.calls["on_slot_rendered"]) == 1 @@ -462,7 +460,7 @@ class TestExtensionHooks: assert isinstance(slot_call.slot, Slot) assert slot_call.slot_name == "content" assert isinstance(slot_call.slot_node, SlotNode) - assert slot_call.slot_node.template_name.endswith("test_extension.py::TestComponent") # type: ignore + assert slot_call.slot_node.template_name.endswith("test_extension.py::TestComponent") # type: ignore[union-attr] assert slot_call.slot_node.template_component == TestComponent assert slot_call.slot_is_required is True assert slot_call.slot_is_default is True @@ -518,7 +516,7 @@ class TestExtensionHooks: # Render the component to trigger all hooks TestComponent.render(args=(), kwargs={"name": "Test"}) - extension = cast(DummyExtension, app_settings.EXTENSIONS[5]) + extension = cast("DummyExtension", app_settings.EXTENSIONS[5]) # on_template_loaded assert len(extension.calls["on_template_loaded"]) == 1 @@ -561,7 +559,7 @@ class TestExtensionHooks: # Render the component to trigger all hooks TestComponent.render(args=(), kwargs={"name": "Test"}) - extension = cast(DummyExtension, app_settings.EXTENSIONS[5]) + extension = cast("DummyExtension", app_settings.EXTENSIONS[5]) # on_template_loaded # NOTE: The template file gets picked up by 'django.template.loaders.filesystem.Loader', @@ -571,10 +569,10 @@ class TestExtensionHooks: assert ctx1.component_cls == TestComponent assert ctx1.content == ( '
\n' - ' {% csrf_token %}\n' + " {% csrf_token %}\n" ' \n' ' \n' - '
\n' + "\n" ) assert isinstance(ctx1.origin, Origin) assert ctx1.origin.name.endswith("relative_file.html") @@ -596,11 +594,7 @@ class TestExtensionHooks: assert len(extension.calls["on_css_loaded"]) == 1 ctx4: OnCssLoadedContext = extension.calls["on_css_loaded"][0] assert ctx4.component_cls == TestComponent - assert ctx4.content == ( - '.html-css-only {\n' - ' color: blue;\n' - '}\n' - ) + assert ctx4.content == ".html-css-only {\n color: blue;\n}\n" @djc_test(components_settings={"extensions": [OverrideAssetExtension]}) def test_asset_hooks_override(self): @@ -658,7 +652,7 @@ class TestExtensionDefaults: "extensions_defaults": { "test_extension": {}, }, - } + }, ) def test_no_defaults(self): class TestComponent(Component): @@ -675,13 +669,13 @@ class TestExtensionDefaults: "extensions_defaults": { "test_extension": { "foo": "NEW_FOO", - "baz": classmethod(lambda self: "OVERRIDEN"), + "baz": classmethod(lambda _self: "OVERRIDEN"), }, "nonexistent": { "1": "2", }, }, - } + }, ) def test_defaults(self): class TestComponent(Component): @@ -699,7 +693,7 @@ class TestLegacyApi: @djc_test( components_settings={ "extensions": [LegacyExtension], - } + }, ) def test_extension_class(self): class TestComponent(Component): diff --git a/tests/test_finders.py b/tests/test_finders.py index 32d3a3ee..552fee34 100644 --- a/tests/test_finders.py +++ b/tests/test_finders.py @@ -3,9 +3,9 @@ from pathlib import Path from django.contrib.staticfiles import finders from django.contrib.staticfiles.management.commands.collectstatic import Command -from django.test import SimpleTestCase from django_components.testing import djc_test + from .testutils import setup_test_config setup_test_config({"autodiscover": False}) @@ -20,16 +20,17 @@ setup_test_config({"autodiscover": False}) class MockCollectstaticCommand(Command): # NOTE: We do not expect this to be called def clear_dir(self, path): - raise NotImplementedError() + raise NotImplementedError # NOTE: We do not expect this to be called def link_file(self, path, prefixed_path, source_storage): - raise NotImplementedError() + raise NotImplementedError def copy_file(self, path, prefixed_path, source_storage): # Skip this file if it was already copied earlier if prefixed_path in self.copied_files: - return self.log("Skipping '%s' (already copied earlier)" % path) + self.log(f"Skipping '{path}' (already copied earlier)") + return # Delete the target file if needed or break if not self.delete_file(path, prefixed_path, source_storage): return @@ -37,9 +38,9 @@ class MockCollectstaticCommand(Command): source_path = source_storage.path(path) # Finally start copying if self.dry_run: - self.log("Pretending to copy '%s'" % source_path, level=1) + self.log(f"Pretending to copy '{source_path}'", level=1) else: - self.log("Copying '%s'" % source_path, level=2) + self.log(f"Copying '{source_path}'", level=2) # ############# OUR CHANGE ############## # with source_storage.open(path) as source_file: # self.storage.save(prefixed_path, source_file) @@ -79,7 +80,8 @@ COMPONENTS = { urlpatterns: list = [] -class StaticFilesFinderTests(SimpleTestCase): +@djc_test +class TestStaticFilesFinder: @djc_test( django_settings={ **common_settings, @@ -95,13 +97,13 @@ class StaticFilesFinderTests(SimpleTestCase): collected = do_collect() # Check that the component files are NOT loaded when our finder is NOT added - self.assertNotIn(Path("staticfiles/staticfiles.css"), collected["modified"]) - self.assertNotIn(Path("staticfiles/staticfiles.js"), collected["modified"]) - self.assertNotIn(Path("staticfiles/staticfiles.html"), collected["modified"]) - self.assertNotIn(Path("staticfiles/staticfiles.py"), collected["modified"]) + assert Path("staticfiles/staticfiles.css") not in collected["modified"] + assert Path("staticfiles/staticfiles.js") not in collected["modified"] + assert Path("staticfiles/staticfiles.html") not in collected["modified"] + assert Path("staticfiles/staticfiles.py") not in collected["modified"] - self.assertListEqual(collected["unmodified"], []) - self.assertListEqual(collected["post_processed"], []) + assert collected["unmodified"] == [] + assert collected["post_processed"] == [] @djc_test( django_settings={ @@ -120,13 +122,13 @@ class StaticFilesFinderTests(SimpleTestCase): collected = do_collect() # Check that our staticfiles_finder finds the files and OMITS .py and .html files - self.assertIn(Path("staticfiles/staticfiles.css"), collected["modified"]) - self.assertIn(Path("staticfiles/staticfiles.js"), collected["modified"]) - self.assertNotIn(Path("staticfiles/staticfiles.html"), collected["modified"]) - self.assertNotIn(Path("staticfiles/staticfiles.py"), collected["modified"]) + assert Path("staticfiles/staticfiles.css") in collected["modified"] + assert Path("staticfiles/staticfiles.js") in collected["modified"] + assert Path("staticfiles/staticfiles.html") not in collected["modified"] + assert Path("staticfiles/staticfiles.py") not in collected["modified"] - self.assertListEqual(collected["unmodified"], []) - self.assertListEqual(collected["post_processed"], []) + assert collected["unmodified"] == [] + assert collected["post_processed"] == [] @djc_test( django_settings={ @@ -151,13 +153,13 @@ class StaticFilesFinderTests(SimpleTestCase): collected = do_collect() # Check that our staticfiles_finder finds the files and OMITS .py and .html files - self.assertNotIn(Path("staticfiles/staticfiles.css"), collected["modified"]) - self.assertIn(Path("staticfiles/staticfiles.js"), collected["modified"]) - self.assertNotIn(Path("staticfiles/staticfiles.html"), collected["modified"]) - self.assertNotIn(Path("staticfiles/staticfiles.py"), collected["modified"]) + assert Path("staticfiles/staticfiles.css") not in collected["modified"] + assert Path("staticfiles/staticfiles.js") in collected["modified"] + assert Path("staticfiles/staticfiles.html") not in collected["modified"] + assert Path("staticfiles/staticfiles.py") not in collected["modified"] - self.assertListEqual(collected["unmodified"], []) - self.assertListEqual(collected["post_processed"], []) + assert collected["unmodified"] == [] + assert collected["post_processed"] == [] @djc_test( django_settings={ @@ -184,13 +186,13 @@ class StaticFilesFinderTests(SimpleTestCase): collected = do_collect() # Check that our staticfiles_finder finds the files and OMITS .py and .html files - self.assertIn(Path("staticfiles/staticfiles.css"), collected["modified"]) - self.assertNotIn(Path("staticfiles/staticfiles.js"), collected["modified"]) - self.assertIn(Path("staticfiles/staticfiles.html"), collected["modified"]) - self.assertIn(Path("staticfiles/staticfiles.py"), collected["modified"]) + assert Path("staticfiles/staticfiles.css") in collected["modified"] + assert Path("staticfiles/staticfiles.js") not in collected["modified"] + assert Path("staticfiles/staticfiles.html") in collected["modified"] + assert Path("staticfiles/staticfiles.py") in collected["modified"] - self.assertListEqual(collected["unmodified"], []) - self.assertListEqual(collected["post_processed"], []) + assert collected["unmodified"] == [] + assert collected["post_processed"] == [] @djc_test( django_settings={ @@ -218,13 +220,13 @@ class StaticFilesFinderTests(SimpleTestCase): collected = do_collect() # Check that our staticfiles_finder finds the files and OMITS .py and .html files - self.assertIn(Path("staticfiles/staticfiles.css"), collected["modified"]) - self.assertNotIn(Path("staticfiles/staticfiles.js"), collected["modified"]) - self.assertNotIn(Path("staticfiles/staticfiles.html"), collected["modified"]) - self.assertNotIn(Path("staticfiles/staticfiles.py"), collected["modified"]) + assert Path("staticfiles/staticfiles.css") in collected["modified"] + assert Path("staticfiles/staticfiles.js") not in collected["modified"] + assert Path("staticfiles/staticfiles.html") not in collected["modified"] + assert Path("staticfiles/staticfiles.py") not in collected["modified"] - self.assertListEqual(collected["unmodified"], []) - self.assertListEqual(collected["post_processed"], []) + assert collected["unmodified"] == [] + assert collected["post_processed"] == [] # Handle deprecated `all` parameter: # - In Django 5.2, the `all` parameter was deprecated in favour of `find_all`. diff --git a/tests/test_html_parser.py b/tests/test_html_parser.py index ca989365..8f56cdc5 100644 --- a/tests/test_html_parser.py +++ b/tests/test_html_parser.py @@ -1,8 +1,8 @@ +from djc_core_html_parser import set_html_attributes from pytest_django.asserts import assertHTMLEqual -from djc_core_html_parser import set_html_attributes - from django_components.testing import djc_test + from .testutils import setup_test_config setup_test_config({"autodiscover": False}) diff --git a/tests/test_integration_template_partials.py b/tests/test_integration_template_partials.py index 2e37309b..9cee73ec 100644 --- a/tests/test_integration_template_partials.py +++ b/tests/test_integration_template_partials.py @@ -11,11 +11,12 @@ See https://github.com/django-components/django-components/issues/1323#issuecomm from typing import NamedTuple import pytest -from django.shortcuts import render from django.http import HttpRequest +from django.shortcuts import render from django_components import Component, register from django_components.testing import djc_test + from .testutils import setup_test_config try: @@ -28,9 +29,7 @@ setup_test_config(components={"autodiscover": False}) # See https://github.com/django-components/django-components/issues/1323#issuecomment-3156654329 -@djc_test( - django_settings={"INSTALLED_APPS": ("template_partials", "django_components", "tests.test_app")} -) +@djc_test(django_settings={"INSTALLED_APPS": ("template_partials", "django_components", "tests.test_app")}) class TestTemplatePartialsIntegration: @pytest.mark.skipif(TemplateProxy is None, reason="template_partials not available") def test_render_partial(self): diff --git a/tests/test_loader.py b/tests/test_loader.py index 227ec04d..f2410dbc 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,14 +1,14 @@ -import re import os +import re from pathlib import Path from unittest.mock import MagicMock, patch import pytest from django.conf import settings +from django_components.testing import djc_test from django_components.util.loader import _filepath_to_python_module, get_component_dirs, get_component_files -from django_components.testing import djc_test from .testutils import setup_test_config setup_test_config({"autodiscover": False}) @@ -29,8 +29,7 @@ class TestComponentDirs: assert own_dirs == [ # Top-level /components dir - Path(__file__).parent.resolve() - / "components", + Path(__file__).parent.resolve() / "components", ] # Apps with a `components` dir @@ -42,7 +41,7 @@ class TestComponentDirs: @djc_test( django_settings={ - "BASE_DIR": Path(__file__).parent.resolve() / "test_structures" / "test_structure_1", # noqa + "BASE_DIR": Path(__file__).parent.resolve() / "test_structures" / "test_structure_1", }, ) def test_get_dirs__base_dir__complex(self): @@ -71,7 +70,7 @@ class TestComponentDirs: ("with_alias", Path(__file__).parent.resolve() / "components"), ("too_many", Path(__file__).parent.resolve() / "components", Path(__file__).parent.resolve()), ("with_not_str_alias", 3), - ], # noqa + ], }, ) @patch("django_components.util.loader.logger.warning") @@ -91,8 +90,7 @@ class TestComponentDirs: assert own_dirs == [ # Top-level /components dir - Path(__file__).parent.resolve() - / "components", + Path(__file__).parent.resolve() / "components", ] warn_inputs = [warn.args[0] for warn in mock_warning.call_args_list] @@ -164,8 +162,7 @@ class TestComponentDirs: assert own_dirs == [ # Top-level /components dir - Path(__file__).parent.resolve() - / "components", + Path(__file__).parent.resolve() / "components", ] @djc_test( @@ -183,8 +180,7 @@ class TestComponentDirs: assert own_dirs == [ # Top-level /components dir - Path(__file__).parent.resolve() - / "components", + Path(__file__).parent.resolve() / "components", ] @djc_test( @@ -202,8 +198,7 @@ class TestComponentDirs: assert own_dirs == [ # Top-level /components dir - Path(__file__).parent.resolve() - / "components", + Path(__file__).parent.resolve() / "components", ] @djc_test( @@ -227,8 +222,7 @@ class TestComponentDirs: assert own_dirs == [ # Top-level /components dir - Path(__file__).parent.resolve() - / "components", + Path(__file__).parent.resolve() / "components", ] @@ -303,13 +297,22 @@ class TestComponentFiles: @djc_test class TestFilepathToPythonModule: - def test_prepares_path(self): + def test_prepares_path__str(self): base_path = str(settings.BASE_DIR) - the_path = os.path.join(base_path, "tests.py") + the_path = os.path.join(base_path, "tests.py") # noqa: PTH118 assert _filepath_to_python_module(the_path, base_path, None) == "tests" - the_path = os.path.join(base_path, "tests/components/relative_file/relative_file.py") + the_path = os.path.join(base_path, "tests/components/relative_file/relative_file.py") # noqa: PTH118 + assert _filepath_to_python_module(the_path, base_path, None) == "tests.components.relative_file.relative_file" + + def test_prepares_path__path(self): + base_path = str(settings.BASE_DIR) + + the_path = Path(base_path) / "tests.py" + assert _filepath_to_python_module(the_path, base_path, None) == "tests" + + the_path = Path(base_path) / "tests/components/relative_file/relative_file.py" assert _filepath_to_python_module(the_path, base_path, None) == "tests.components.relative_file.relative_file" def test_handles_separators_based_on_os_name(self): @@ -320,7 +323,9 @@ class TestFilepathToPythonModule: assert _filepath_to_python_module(the_path, base_path, None) == "tests" the_path = base_path + "/" + "tests/components/relative_file/relative_file.py" - assert _filepath_to_python_module(the_path, base_path, None) == "tests.components.relative_file.relative_file" # noqa: E501 + assert ( + _filepath_to_python_module(the_path, base_path, None) == "tests.components.relative_file.relative_file" + ) base_path = str(settings.BASE_DIR).replace("/", "\\") with patch("os.name", new="nt"): @@ -328,11 +333,15 @@ class TestFilepathToPythonModule: assert _filepath_to_python_module(the_path, base_path, None) == "tests" the_path = base_path + "\\" + "tests\\components\\relative_file\\relative_file.py" - assert _filepath_to_python_module(the_path, base_path, None) == "tests.components.relative_file.relative_file" # noqa: E501 + assert ( + _filepath_to_python_module(the_path, base_path, None) == "tests.components.relative_file.relative_file" + ) # NOTE: Windows should handle also POSIX separator the_path = base_path + "/" + "tests.py" assert _filepath_to_python_module(the_path, base_path, None) == "tests" the_path = base_path + "/" + "tests/components/relative_file/relative_file.py" - assert _filepath_to_python_module(the_path, base_path, None) == "tests.components.relative_file.relative_file" # noqa: E501 + assert ( + _filepath_to_python_module(the_path, base_path, None) == "tests.components.relative_file.relative_file" + ) diff --git a/tests/test_node.py b/tests/test_node.py index e62e4fb7..52122a74 100644 --- a/tests/test_node.py +++ b/tests/test_node.py @@ -12,9 +12,9 @@ from django.template.exceptions import TemplateSyntaxError from django_components import Component, types from django_components.node import BaseNode, template_tag from django_components.templatetags import component_tags +from django_components.testing import djc_test from django_components.util.tag_parser import TagAttr -from django_components.testing import djc_test from .testutils import setup_test_config setup_test_config({"autodiscover": False}) @@ -23,7 +23,7 @@ setup_test_config({"autodiscover": False}) @djc_test class TestNode: def test_node_class_requires_tag(self): - with pytest.raises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 class CaptureNode(BaseNode): pass @@ -114,7 +114,7 @@ class TestNode: template = Template(template_str) template.render(Context({})) - allowed_flags, flags, active_flags = captured # type: ignore + allowed_flags, flags, active_flags = captured # type: ignore[misc] assert allowed_flags == ["required", "default"] assert flags == {"required": True, "default": False} assert active_flags == ["required"] @@ -157,7 +157,7 @@ class TestNode: class TestNode(BaseNode): tag = "mytag" - def render(self) -> str: # type: ignore + def render(self) -> str: # type: ignore[override] return "" def test_node_render_accepted_params_set_by_render_signature(self): @@ -179,7 +179,7 @@ class TestNode: """ {% load component_tags %} {% mytag 'John' msg='Hello' required %} - """ + """, ) template1.render(Context({})) assert captured == ("John", 1, "Hello", "default") @@ -189,7 +189,7 @@ class TestNode: """ {% load component_tags %} {% mytag 'John2' count=2 msg='Hello' mode='custom' required %} - """ + """, ) template2.render(Context({})) assert captured == ("John2", 2, "Hello", "custom") @@ -199,7 +199,7 @@ class TestNode: """ {% load component_tags %} {% mytag %} - """ + """, ) with pytest.raises( TypeError, @@ -212,7 +212,7 @@ class TestNode: """ {% load component_tags %} {% mytag msg='Hello' %} - """ + """, ) with pytest.raises( TypeError, @@ -225,7 +225,7 @@ class TestNode: """ {% load component_tags %} {% mytag name='John' %} - """ + """, ) with pytest.raises( TypeError, @@ -238,7 +238,7 @@ class TestNode: """ {% load component_tags %} {% mytag 123 count=1 name='John' %} - """ + """, ) with pytest.raises( TypeError, @@ -251,7 +251,7 @@ class TestNode: """ {% load component_tags %} {% mytag count=1 name='John' 123 %} - """ + """, ) with pytest.raises( TypeError, @@ -264,7 +264,7 @@ class TestNode: """ {% load component_tags %} {% mytag 'John' msg='Hello' mode='custom' var=123 %} - """ + """, ) with pytest.raises( TypeError, @@ -277,7 +277,7 @@ class TestNode: """ {% load component_tags %} {% mytag 'John' msg='Hello' mode='custom' data-id=123 class="pa-4" @click.once="myVar" %} - """ + """, ) with pytest.raises( TypeError, @@ -290,7 +290,7 @@ class TestNode: """ {% load component_tags %} {% mytag data-id=123 'John' msg='Hello' %} - """ + """, ) with pytest.raises( SyntaxError, @@ -321,7 +321,7 @@ class TestNode: 123 456 789 msg='Hello' a=1 b=2 c=3 required data-id=123 class="pa-4" @click.once="myVar" %} - """ + """, ) template1.render(Context({})) assert captured == ( @@ -358,7 +358,7 @@ class TestNode: @click="handleClick" v-if="isVisible" %} - """ + """, ) template.render(Context({})) @@ -377,7 +377,7 @@ class TestNode: """ {% load component_tags %} {% mytag "John" name="Mary" %} - """ + """, ) with pytest.raises( TypeError, @@ -396,14 +396,14 @@ class TestDecorator: match=re.escape("template_tag() missing 1 required positional argument: 'tag'"), ): - @template_tag(component_tags.register) # type: ignore - def mytag(node: BaseNode, context: Context) -> str: + @template_tag(component_tags.register) # type: ignore[call-arg] + def mytag(node: BaseNode, context: Context) -> str: # noqa: ARG001 return "" # Test that the template tag can be used within the template under the registered tag def test_decorator_tags(self): @template_tag(component_tags.register, tag="mytag", end_tag="endmytag") - def render(node: BaseNode, context: Context, name: str, **kwargs) -> str: + def render(node: BaseNode, context: Context, name: str, **kwargs) -> str: # noqa: ARG001 return f"Hello, {name}!" # Works with end tag and self-closing @@ -432,8 +432,8 @@ class TestDecorator: render._node.unregister(component_tags.register) # type: ignore[attr-defined] def test_decorator_no_end_tag(self): - @template_tag(component_tags.register, tag="mytag") # type: ignore - def render(node: BaseNode, context: Context, name: str, **kwargs) -> str: + @template_tag(component_tags.register, tag="mytag") + def render(node: BaseNode, context: Context, name: str, **kwargs) -> str: # noqa: ARG001 return f"Hello, {name}!" # Raises with end tag or self-closing @@ -462,7 +462,7 @@ class TestDecorator: def test_decorator_flags(self): @template_tag(component_tags.register, tag="mytag", end_tag="endmytag", allowed_flags=["required", "default"]) - def render(node: BaseNode, context: Context, name: str, **kwargs) -> str: + def render(node: BaseNode, context: Context, name: str, **kwargs) -> str: # noqa: ARG001 return "" render._node.unregister(component_tags.register) # type: ignore[attr-defined] @@ -471,8 +471,8 @@ class TestDecorator: # Check that the render function is called with the context captured = None - @template_tag(component_tags.register, tag="mytag") # type: ignore - def render(node: BaseNode, context: Context) -> str: + @template_tag(component_tags.register, tag="mytag") + def render(node: BaseNode, context: Context) -> str: # noqa: ARG001 nonlocal captured captured = context.flatten() return f"Hello, {context['name']}!" @@ -495,16 +495,22 @@ class TestDecorator: match=re.escape("Failed to create node class in 'template_tag()' for 'render'"), ): - @template_tag(component_tags.register, tag="mytag") # type: ignore - def render(node: BaseNode) -> str: # type: ignore + @template_tag(component_tags.register, tag="mytag") + def render(node: BaseNode) -> str: # noqa: ARG001 return "" def test_decorator_render_accepted_params_set_by_render_signature(self): captured = None - @template_tag(component_tags.register, tag="mytag", allowed_flags=["required", "default"]) # type: ignore + @template_tag(component_tags.register, tag="mytag", allowed_flags=["required", "default"]) def render( - node: BaseNode, context: Context, name: str, count: int = 1, *, msg: str, mode: str = "default" + node: BaseNode, # noqa: ARG001 + context: Context, # noqa: ARG001 + name: str, + count: int = 1, + *, + msg: str, + mode: str = "default", ) -> str: nonlocal captured captured = name, count, msg, mode @@ -515,7 +521,7 @@ class TestDecorator: """ {% load component_tags %} {% mytag 'John' msg='Hello' required %} - """ + """, ) template1.render(Context({})) assert captured == ("John", 1, "Hello", "default") @@ -525,7 +531,7 @@ class TestDecorator: """ {% load component_tags %} {% mytag 'John2' count=2 msg='Hello' mode='custom' required %} - """ + """, ) template2.render(Context({})) assert captured == ("John2", 2, "Hello", "custom") @@ -535,7 +541,7 @@ class TestDecorator: """ {% load component_tags %} {% mytag %} - """ + """, ) with pytest.raises( TypeError, @@ -548,7 +554,7 @@ class TestDecorator: """ {% load component_tags %} {% mytag msg='Hello' %} - """ + """, ) with pytest.raises( TypeError, @@ -561,7 +567,7 @@ class TestDecorator: """ {% load component_tags %} {% mytag name='John' %} - """ + """, ) with pytest.raises( TypeError, @@ -574,7 +580,7 @@ class TestDecorator: """ {% load component_tags %} {% mytag 123 count=1 name='John' %} - """ + """, ) with pytest.raises( TypeError, @@ -587,7 +593,7 @@ class TestDecorator: """ {% load component_tags %} {% mytag count=1 name='John' 123 %} - """ + """, ) with pytest.raises( TypeError, @@ -600,7 +606,7 @@ class TestDecorator: """ {% load component_tags %} {% mytag 'John' msg='Hello' mode='custom' var=123 %} - """ + """, ) with pytest.raises( TypeError, @@ -613,7 +619,7 @@ class TestDecorator: """ {% load component_tags %} {% mytag 'John' msg='Hello' mode='custom' data-id=123 class="pa-4" @click.once="myVar" %} - """ + """, ) with pytest.raises( TypeError, @@ -626,7 +632,7 @@ class TestDecorator: """ {% load component_tags %} {% mytag data-id=123 'John' msg='Hello' %} - """ + """, ) with pytest.raises( SyntaxError, @@ -639,8 +645,8 @@ class TestDecorator: def test_decorator_render_extra_args_and_kwargs(self): captured = None - @template_tag(component_tags.register, tag="mytag", allowed_flags=["required", "default"]) # type: ignore - def render(node: BaseNode, context: Context, name: str, *args, msg: str, **kwargs) -> str: + @template_tag(component_tags.register, tag="mytag", allowed_flags=["required", "default"]) + def render(node: BaseNode, context: Context, name: str, *args, msg: str, **kwargs) -> str: # noqa: ARG001 nonlocal captured captured = name, args, msg, kwargs return "" @@ -652,7 +658,7 @@ class TestDecorator: 123 456 789 msg='Hello' a=1 b=2 c=3 required data-id=123 class="pa-4" @click.once="myVar" %} - """ + """, ) template1.render(Context({})) assert captured == ( @@ -667,8 +673,8 @@ class TestDecorator: def test_decorator_render_kwargs_only(self): captured = None - @template_tag(component_tags.register, tag="mytag") # type: ignore - def render(node: BaseNode, context: Context, **kwargs) -> str: + @template_tag(component_tags.register, tag="mytag") + def render(node: BaseNode, context: Context, **kwargs) -> str: # noqa: ARG001 nonlocal captured captured = kwargs return "" @@ -685,7 +691,7 @@ class TestDecorator: @click="handleClick" v-if="isVisible" %} - """ + """, ) template.render(Context({})) @@ -704,7 +710,7 @@ class TestDecorator: """ {% load component_tags %} {% mytag "John" name="Mary" %} - """ + """, ) with pytest.raises( TypeError, @@ -717,7 +723,7 @@ class TestDecorator: def force_signature_validation(fn): """ - Creates a proxy around a function that makes __code__ inaccessible, + Create a proxy around a function that makes `__code__` inaccessible, forcing the use of signature-based validation. """ @@ -835,7 +841,7 @@ class TestSignatureBasedValidation: template = Template(template_str) template.render(Context({})) - allowed_flags, flags, active_flags = captured # type: ignore + allowed_flags, flags, active_flags = captured # type: ignore[misc] assert allowed_flags == ["required", "default"] assert flags == {"required": True, "default": False} assert active_flags == ["required"] @@ -874,7 +880,7 @@ class TestSignatureBasedValidation: template1 = Template(template_str1) template1.render(Context({})) - params1, nodelist1, node_id1, contents1, template_name1, template_component1 = captured # type: ignore + params1, nodelist1, node_id1, contents1, template_name1, template_component1 = captured # type: ignore[misc] assert len(params1) == 1 assert isinstance(params1[0], TagAttr) # NOTE: The comment node is not included in the nodelist @@ -887,9 +893,12 @@ class TestSignatureBasedValidation: assert isinstance(nodelist1[5], TextNode) assert isinstance(nodelist1[6], IfNode) assert isinstance(nodelist1[7], TextNode) - assert contents1 == "\n INSIDE TAG {{ my_var }} {# comment #} {% lorem 1 w %} {% if True %} henlo {% endif %}\n " # noqa: E501 + assert ( + contents1 + == "\n INSIDE TAG {{ my_var }} {# comment #} {% lorem 1 w %} {% if True %} henlo {% endif %}\n " # noqa: E501 + ) assert node_id1 == "a1bc3e" - assert template_name1 == '' + assert template_name1 == "" assert template_component1 is None captured = None # Reset captured @@ -902,14 +911,14 @@ class TestSignatureBasedValidation: template2 = Template(template_str2) template2.render(Context({})) - params2, nodelist2, node_id2, contents2, template_name2, template_component2 = captured # type: ignore - assert len(params2) == 1 # type: ignore - assert isinstance(params2[0], TagAttr) # type: ignore - assert len(nodelist2) == 0 # type: ignore - assert contents2 is None # type: ignore - assert node_id2 == "a1bc3f" # type: ignore - assert template_name2 == '' # type: ignore - assert template_component2 is None # type: ignore + params2, nodelist2, node_id2, contents2, template_name2, template_component2 = captured # type: ignore[misc] + assert len(params2) == 1 # type: ignore[has-type] + assert isinstance(params2[0], TagAttr) # type: ignore[has-type] + assert len(nodelist2) == 0 # type: ignore[has-type] + assert contents2 is None # type: ignore[has-type] + assert node_id2 == "a1bc3f" # type: ignore[has-type] + assert template_name2 == "" # type: ignore[has-type] + assert template_component2 is None # type: ignore[has-type] captured = None # Reset captured @@ -920,7 +929,14 @@ class TestSignatureBasedValidation: @force_signature_validation def render(self, context: Context, name: str, **kwargs) -> str: nonlocal captured - captured = self.params, self.nodelist, self.node_id, self.contents, self.template_name, self.template_component # noqa: E501 + captured = ( + self.params, + self.nodelist, + self.node_id, + self.contents, + self.template_name, + self.template_component, + ) return f"Hello, {name}!" TestNodeWithoutEndTag.register(component_tags.register) @@ -932,14 +948,14 @@ class TestSignatureBasedValidation: template3 = Template(template_str3) template3.render(Context({})) - params3, nodelist3, node_id3, contents3, template_name3, template_component3 = captured # type: ignore - assert len(params3) == 1 # type: ignore - assert isinstance(params3[0], TagAttr) # type: ignore - assert len(nodelist3) == 0 # type: ignore - assert contents3 is None # type: ignore - assert node_id3 == "a1bc40" # type: ignore - assert template_name3 == '' # type: ignore - assert template_component3 is None # type: ignore + params3, nodelist3, node_id3, contents3, template_name3, template_component3 = captured # type: ignore[misc] + assert len(params3) == 1 # type: ignore[has-type] + assert isinstance(params3[0], TagAttr) # type: ignore[has-type] + assert len(nodelist3) == 0 # type: ignore[has-type] + assert contents3 is None # type: ignore[has-type] + assert node_id3 == "a1bc40" # type: ignore[has-type] + assert template_name3 == "" # type: ignore[has-type] + assert template_component3 is None # type: ignore[has-type] # Case 4 - Node nested in Component end tag class TestComponent(Component): @@ -950,20 +966,20 @@ class TestSignatureBasedValidation: TestComponent.render(Context({})) - params4, nodelist4, node_id4, contents4, template_name4, template_component4 = captured # type: ignore - assert len(params4) == 1 # type: ignore - assert isinstance(params4[0], TagAttr) # type: ignore - assert len(nodelist4) == 0 # type: ignore - assert contents4 is None # type: ignore - assert node_id4 == "a1bc42" # type: ignore + params4, nodelist4, node_id4, contents4, template_name4, template_component4 = captured # type: ignore[misc] + assert len(params4) == 1 # type: ignore[has-type] + assert isinstance(params4[0], TagAttr) # type: ignore[has-type] + assert len(nodelist4) == 0 # type: ignore[has-type] + assert contents4 is None # type: ignore[has-type] + assert node_id4 == "a1bc42" # type: ignore[has-type] if os.name == "nt": - assert cast(str, template_name4).endswith("\\tests\\test_node.py::TestComponent") # type: ignore + assert cast("str", template_name4).endswith("\\tests\\test_node.py::TestComponent") # type: ignore[has-type] else: - assert cast(str, template_name4).endswith("/tests/test_node.py::TestComponent") # type: ignore + assert cast("str", template_name4).endswith("/tests/test_node.py::TestComponent") # type: ignore[has-type] - assert template_name4 == f"{__file__}::TestComponent" # type: ignore - assert template_component4 is TestComponent # type: ignore + assert template_name4 == f"{__file__}::TestComponent" # type: ignore[has-type] + assert template_component4 is TestComponent # type: ignore[has-type] # Cleanup TestNodeWithEndTag.unregister(component_tags.register) @@ -1005,7 +1021,7 @@ class TestSignatureBasedValidation: class TestNode(BaseNode): tag = "mytag" - def render(self) -> str: # type: ignore + def render(self) -> str: # type: ignore[override] return "" def test_node_render_accepted_params_set_by_render_signature(self): @@ -1028,7 +1044,7 @@ class TestSignatureBasedValidation: """ {% load component_tags %} {% mytag 'John' msg='Hello' required %} - """ + """, ) template1.render(Context({})) assert captured == ("John", 1, "Hello", "default") @@ -1038,7 +1054,7 @@ class TestSignatureBasedValidation: """ {% load component_tags %} {% mytag 'John2' count=2 msg='Hello' mode='custom' required %} - """ + """, ) template2.render(Context({})) assert captured == ("John2", 2, "Hello", "custom") @@ -1048,7 +1064,7 @@ class TestSignatureBasedValidation: """ {% load component_tags %} {% mytag %} - """ + """, ) with pytest.raises( TypeError, @@ -1061,7 +1077,7 @@ class TestSignatureBasedValidation: """ {% load component_tags %} {% mytag msg='Hello' %} - """ + """, ) with pytest.raises( TypeError, @@ -1074,7 +1090,7 @@ class TestSignatureBasedValidation: """ {% load component_tags %} {% mytag name='John' %} - """ + """, ) with pytest.raises( TypeError, @@ -1087,7 +1103,7 @@ class TestSignatureBasedValidation: """ {% load component_tags %} {% mytag 123 count=1 name='John' %} - """ + """, ) with pytest.raises( TypeError, @@ -1100,7 +1116,7 @@ class TestSignatureBasedValidation: """ {% load component_tags %} {% mytag count=1 name='John' 123 %} - """ + """, ) with pytest.raises( TypeError, @@ -1113,7 +1129,7 @@ class TestSignatureBasedValidation: """ {% load component_tags %} {% mytag 'John' msg='Hello' mode='custom' var=123 %} - """ + """, ) with pytest.raises( TypeError, @@ -1126,7 +1142,7 @@ class TestSignatureBasedValidation: """ {% load component_tags %} {% mytag 'John' msg='Hello' mode='custom' data-id=123 class="pa-4" @click.once="myVar" %} - """ + """, ) with pytest.raises( TypeError, @@ -1139,7 +1155,7 @@ class TestSignatureBasedValidation: """ {% load component_tags %} {% mytag data-id=123 'John' msg='Hello' %} - """ + """, ) with pytest.raises( SyntaxError, @@ -1171,7 +1187,7 @@ class TestSignatureBasedValidation: 123 456 789 msg='Hello' a=1 b=2 c=3 required data-id=123 class="pa-4" @click.once="myVar" %} - """ + """, ) template1.render(Context({})) assert captured == ( @@ -1209,7 +1225,7 @@ class TestSignatureBasedValidation: @click="handleClick" v-if="isVisible" %} - """ + """, ) template.render(Context({})) @@ -1228,7 +1244,7 @@ class TestSignatureBasedValidation: """ {% load component_tags %} {% mytag "John" name="Mary" %} - """ + """, ) with pytest.raises( TypeError, diff --git a/tests/test_registry.py b/tests/test_registry.py index d9b240c5..74fdfb2c 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -1,5 +1,6 @@ import pytest from django.template import Context, Engine, Library, Template +from pytest_django.asserts import assertHTMLEqual from django_components import ( AlreadyRegistered, @@ -16,9 +17,8 @@ from django_components import ( registry, types, ) -from pytest_django.asserts import assertHTMLEqual - from django_components.testing import djc_test + from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config setup_test_config({"autodiscover": False}) diff --git a/tests/test_settings.py b/tests/test_settings.py index 2968b98c..f95ac57c 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -5,8 +5,8 @@ import pytest from django.test import override_settings from django_components.app_settings import ComponentsSettings, app_settings - from django_components.testing import djc_test + from .testutils import setup_test_config setup_test_config(components={"autodiscover": False}) @@ -39,7 +39,7 @@ class TestSettings: }, ) def test_works_when_base_dir_is_string(self): - assert app_settings.DIRS == [Path("base_dir/components")] + assert [Path("base_dir/components")] == app_settings.DIRS @djc_test( django_settings={ @@ -47,7 +47,7 @@ class TestSettings: }, ) def test_works_when_base_dir_is_path(self): - assert app_settings.DIRS == [Path("base_dir/components")] + assert [Path("base_dir/components")] == app_settings.DIRS @djc_test( components_settings={ diff --git a/tests/test_signals.py b/tests/test_signals.py index 6dcabfe9..9cd59e04 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -3,8 +3,8 @@ from functools import wraps from django.template import Context, Template from django_components import Component, registry, types - from django_components.testing import djc_test + from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config setup_test_config({"autodiscover": False}) @@ -16,7 +16,7 @@ def _get_templates_used_to_render(subject_template, render_context=None): templates_used = [] - def receive_template_signal(sender, template, context, **_kwargs): + def receive_template_signal(sender, template, context, **_kwargs): # noqa: ARG001 templates_used.append(template.name) template_rendered.connect(receive_template_signal, dispatch_uid="test_method") @@ -29,8 +29,8 @@ def with_template_signal(func): @wraps(func) def wrapper(*args, **kwargs): # Emulate Django test instrumentation for TestCase (see setup_test_environment) - from django.test.utils import instrumented_test_render from django.template import Template + from django.test.utils import instrumented_test_render original_template_render = Template._render Template._render = instrumented_test_render diff --git a/tests/test_slots.py b/tests/test_slots.py index eb6f0fce..9b9a3333 100644 --- a/tests/test_slots.py +++ b/tests/test_slots.py @@ -7,15 +7,15 @@ import re import pytest from django.template import Context, Template, TemplateSyntaxError -from django.utils.safestring import mark_safe from django.template.base import NodeList, TextNode +from django.utils.safestring import mark_safe from pytest_django.asserts import assertHTMLEqual from django_components import Component, register, types from django_components.component import ComponentNode from django_components.slots import FillNode, Slot, SlotContext, SlotFallback - from django_components.testing import djc_test + from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config setup_test_config({"autodiscover": False}) @@ -32,7 +32,7 @@ class TestSlot: [{"context_behavior": "isolated"}, True], ], ["django", "isolated"], - ) + ), ) def test_render_slot_as_func(self, components_settings, is_isolated): class SimpleComponent(Component): @@ -75,7 +75,7 @@ class TestSlot: assert slot_data_expected == ctx.data assert isinstance(ctx.fallback, SlotFallback) - assert "SLOT_DEFAULT" == str(ctx.fallback).strip() + assert str(ctx.fallback).strip() == "SLOT_DEFAULT" return f"FROM_INSIDE_SLOT_FN | {ctx.fallback}" @@ -118,10 +118,10 @@ class TestSlot: ) def test_render_raises_on_slot_instance_in_slot_constructor(self): - slot: Slot = Slot(lambda ctx: "SLOT_FN") + slot: Slot = Slot(lambda _ctx: "SLOT_FN") with pytest.raises( - ValueError, + TypeError, match=re.escape("Slot received another Slot instance as `contents`"), ): Slot(slot) @@ -156,7 +156,7 @@ class TestSlot: assert slot_data_expected == ctx.data assert isinstance(ctx.fallback, str) - assert "SLOT_DEFAULT" == ctx.fallback + assert ctx.fallback == "SLOT_DEFAULT" return f"FROM_INSIDE_SLOT_FN | {ctx.fallback}" @@ -186,10 +186,10 @@ class TestSlot: ) def test_render_slot_unsafe_content__func(self): - def slot_fn1(ctx: SlotContext): + def slot_fn1(_ctx: SlotContext): return mark_safe("") - def slot_fn2(ctx: SlotContext): + def slot_fn2(_ctx: SlotContext): return "" slot1: Slot = Slot(slot_fn1) @@ -262,7 +262,7 @@ class TestSlot: nonlocal captured_slots captured_slots = slots - slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731 + slot_func = lambda _ctx: "FROM_INSIDE_SLOT" # noqa: E731 SimpleComponent.render( slots={"first": slot_func}, @@ -301,7 +301,7 @@ class TestSlot: nonlocal captured_slots captured_slots = slots - slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731 + slot_func = lambda _ctx: "FROM_INSIDE_SLOT" # noqa: E731 SimpleComponent.render( slots={"first": Slot(slot_func)}, @@ -456,7 +456,7 @@ class TestSlot: nonlocal captured_slots captured_slots = slots - slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731 + slot_func = lambda _ctx: "FROM_INSIDE_SLOT" # noqa: E731 SimpleComponent.render( slots={"first": slot_func}, @@ -486,7 +486,7 @@ class TestSlot: nonlocal captured_slots captured_slots = slots - slot_func = lambda ctx: "FROM_INSIDE_SLOT" # noqa: E731 + slot_func = lambda _ctx: "FROM_INSIDE_SLOT" # noqa: E731 SimpleComponent.render( slots={"first": Slot(slot_func, extra={"foo": "bar"}, slot_name="whoop")}, @@ -608,9 +608,9 @@ class TestSlot: template.render( Context( { - "my_slot": Slot(lambda ctx: "FROM_INSIDE_NAMED_SLOT", extra={"foo": "bar"}, slot_name="whoop"), - } - ) + "my_slot": Slot(lambda _ctx: "FROM_INSIDE_NAMED_SLOT", extra={"foo": "bar"}, slot_name="whoop"), + }, + ), ) first_slot_func = captured_slots["first"] @@ -638,7 +638,7 @@ class TestSlot: """ template = Template(template_str) - my_slot: Slot = Slot(lambda ctx: "FROM_INSIDE_NAMED_SLOT") + my_slot: Slot = Slot(lambda _ctx: "FROM_INSIDE_NAMED_SLOT") rendered: str = template.render(Context({"my_slot": my_slot})) assert rendered.strip() == "FROM_INSIDE_NAMED_SLOT" @@ -683,7 +683,7 @@ class TestSlot: """ template = Template(template_str) - my_slot: Slot = Slot(lambda ctx: "FROM_INSIDE_NAMED_SLOT") + my_slot: Slot = Slot(lambda _ctx: "FROM_INSIDE_NAMED_SLOT") with pytest.raises( TemplateSyntaxError, @@ -693,7 +693,7 @@ class TestSlot: @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) def test_slot_call_outside_render_context(self, components_settings): - from django_components import register, Component + from django_components import Component, register seen_slots = [] diff --git a/tests/test_tag_formatter.py b/tests/test_tag_formatter.py index 82a3ec1a..9f31f556 100644 --- a/tests/test_tag_formatter.py +++ b/tests/test_tag_formatter.py @@ -6,8 +6,8 @@ from pytest_django.asserts import assertHTMLEqual from django_components import Component, register, types from django_components.tag_formatter import ShorthandComponentFormatter - from django_components.testing import djc_test + from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config setup_test_config({"autodiscover": False}) @@ -66,7 +66,7 @@ class TestComponentTag: """ {% load component_tags %} {% component "simple" / %} - """ + """, ) rendered = template.render(Context()) assertHTMLEqual( @@ -99,7 +99,7 @@ class TestComponentTag: {% component "simple" %} OVERRIDEN! {% endcomponent %} - """ + """, ) rendered = template.render(Context()) assertHTMLEqual( @@ -135,7 +135,7 @@ class TestComponentTag: """ {% load component_tags %} {% component "simple" / %} - """ + """, ) rendered = template.render(Context()) assertHTMLEqual( @@ -173,7 +173,7 @@ class TestComponentTag: {% component "simple" %} OVERRIDEN! {% endcomponent %} - """ + """, ) rendered = template.render(Context()) assertHTMLEqual( @@ -209,7 +209,7 @@ class TestComponentTag: """ {% load component_tags %} {% simple / %} - """ + """, ) rendered = template.render(Context()) assertHTMLEqual( @@ -247,7 +247,7 @@ class TestComponentTag: {% simple %} OVERRIDEN! {% endsimple %} - """ + """, ) rendered = template.render(Context()) assertHTMLEqual( @@ -285,7 +285,7 @@ class TestComponentTag: {% simple %} OVERRIDEN! {% /simple %} - """ + """, ) rendered = template.render(Context()) assertHTMLEqual( @@ -321,7 +321,7 @@ class TestComponentTag: {% simple %} OVERRIDEN! {% endsimple %} - """ + """, ) rendered = template.render(Context()) assertHTMLEqual( @@ -356,27 +356,26 @@ class TestComponentTag: }, ) def test_raises_on_invalid_block_end_tag(self, components_settings): + @register("simple") + class SimpleComponent(Component): + template: types.django_html = """ + {% load component_tags %} +
+ {% slot "content" default %} SLOT_DEFAULT {% endslot %} +
+ """ + with pytest.raises( ValueError, match=re.escape("MultiwordBlockEndTagFormatter returned an invalid tag for end_tag: 'end simple'"), ): - - @register("simple") - class SimpleComponent(Component): - template: types.django_html = """ - {% load component_tags %} -
- {% slot "content" default %} SLOT_DEFAULT {% endslot %} -
- """ - Template( """ {% load component_tags %} {% simple %} OVERRIDEN! {% bar %} - """ + """, ) @djc_test( @@ -401,7 +400,7 @@ class TestComponentTag: """ {% load component_tags %} {% simple / %} - """ + """, ) rendered = template.render(Context()) assertHTMLEqual( @@ -421,7 +420,7 @@ class TestComponentTag: {% simple %} OVERRIDEN! {% endsimple %} - """ + """, ) rendered = template.render(Context()) assertHTMLEqual( diff --git a/tests/test_tag_parser.py b/tests/test_tag_parser.py index 859715d3..a1a74e91 100644 --- a/tests/test_tag_parser.py +++ b/tests/test_tag_parser.py @@ -7,9 +7,9 @@ from django.template.base import Parser from django.template.engine import Engine from django_components import Component, register, types +from django_components.testing import djc_test from django_components.util.tag_parser import TagAttr, TagValue, TagValuePart, TagValueStruct, parse_tag -from django_components.testing import djc_test from .testutils import setup_test_config setup_test_config({"autodiscover": False}) @@ -44,10 +44,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), ), @@ -62,9 +66,9 @@ class TestTagParser: entries=[ TagValue( parts=[ - TagValuePart(value="my_comp", quoted="'", spread=None, translation=False, filter=None) - ] - ) + TagValuePart(value="my_comp", quoted="'", spread=None, translation=False, filter=None), + ], + ), ], ), ), @@ -78,8 +82,10 @@ class TestTagParser: parser=None, entries=[ TagValue( - parts=[TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None)] - ) + parts=[ + TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None), + ], + ), ], ), ), @@ -94,9 +100,15 @@ class TestTagParser: entries=[ TagValue( parts=[ - TagValuePart(value="val2 two", quoted="'", spread=None, translation=False, filter=None) - ] - ) + TagValuePart( + value="val2 two", + quoted="'", + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), ), @@ -125,10 +137,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=0, @@ -143,9 +159,9 @@ class TestTagParser: entries=[ TagValue( parts=[ - TagValuePart(value="my_comp", quoted="'", spread=None, translation=False, filter=None) - ] - ) + TagValuePart(value="my_comp", quoted="'", spread=None, translation=False, filter=None), + ], + ), ], ), start_index=10, @@ -159,8 +175,10 @@ class TestTagParser: parser=None, entries=[ TagValue( - parts=[TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None)] - ) + parts=[ + TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None), + ], + ), ], ), start_index=20, @@ -176,10 +194,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value='val2 "two"', quoted="'", spread=None, translation=False, filter=None - ) - ] - ) + value='val2 "two"', + quoted="'", + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=28, @@ -195,10 +217,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="organisation's", quoted='"', spread=None, translation=False, filter=None - ) - ] - ) + value="organisation's", + quoted='"', + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=46, @@ -229,10 +255,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=0, @@ -247,9 +277,9 @@ class TestTagParser: entries=[ TagValue( parts=[ - TagValuePart(value="my_comp", quoted="'", spread=None, translation=False, filter=None) - ] - ) + TagValuePart(value="my_comp", quoted="'", spread=None, translation=False, filter=None), + ], + ), ], ), start_index=10, @@ -263,8 +293,10 @@ class TestTagParser: parser=None, entries=[ TagValue( - parts=[TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None)] - ) + parts=[ + TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None), + ], + ), ], ), start_index=20, @@ -280,10 +312,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value='val2 "two"', quoted="'", spread=None, translation=False, filter=None - ) - ] - ) + value='val2 "two"', + quoted="'", + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=28, @@ -299,10 +335,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="organisation's", quoted='"', spread=None, translation=False, filter=None - ) - ] - ) + value="organisation's", + quoted='"', + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=46, @@ -317,9 +357,9 @@ class TestTagParser: entries=[ TagValue( parts=[ - TagValuePart(value="'abc", quoted=None, spread=None, translation=False, filter=None) - ] - ) + TagValuePart(value="'abc", quoted=None, spread=None, translation=False, filter=None), + ], + ), ], ), start_index=68, @@ -351,10 +391,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=0, @@ -369,9 +413,9 @@ class TestTagParser: entries=[ TagValue( parts=[ - TagValuePart(value="my_comp", quoted='"', spread=None, translation=False, filter=None) - ] - ) + TagValuePart(value="my_comp", quoted='"', spread=None, translation=False, filter=None), + ], + ), ], ), start_index=10, @@ -385,8 +429,10 @@ class TestTagParser: parser=None, entries=[ TagValue( - parts=[TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None)] - ) + parts=[ + TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None), + ], + ), ], ), start_index=20, @@ -402,10 +448,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="val2 'two'", quoted='"', spread=None, translation=False, filter=None - ) - ] - ) + value="val2 'two'", + quoted='"', + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=28, @@ -421,14 +471,18 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value='organisation"s', quoted="'", spread=None, translation=False, filter=None - ) - ] - ) + value='organisation"s', + quoted="'", + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=46, - ), # noqa: E501 + ), TagAttr( key=None, value=TagValueStruct( @@ -439,9 +493,9 @@ class TestTagParser: entries=[ TagValue( parts=[ - TagValuePart(value='"abc', quoted=None, spread=None, translation=False, filter=None) - ] - ) + TagValuePart(value='"abc', quoted=None, spread=None, translation=False, filter=None), + ], + ), ], ), start_index=68, @@ -476,10 +530,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=0, @@ -494,9 +552,9 @@ class TestTagParser: entries=[ TagValue( parts=[ - TagValuePart(value="my_comp", quoted="'", spread=None, translation=False, filter=None) - ] - ) + TagValuePart(value="my_comp", quoted="'", spread=None, translation=False, filter=None), + ], + ), ], ), start_index=10, @@ -510,8 +568,10 @@ class TestTagParser: parser=None, entries=[ TagValue( - parts=[TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None)] - ) + parts=[ + TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None), + ], + ), ], ), start_index=20, @@ -527,10 +587,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value='val2 "two"', quoted="'", spread=None, translation=False, filter=None - ) - ] - ) + value='val2 "two"', + quoted="'", + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=28, @@ -546,10 +610,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="organisation's", quoted='"', spread=None, translation=False, filter=None - ) - ] - ) + value="organisation's", + quoted='"', + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=46, @@ -564,9 +632,9 @@ class TestTagParser: entries=[ TagValue( parts=[ - TagValuePart(value="'abc", quoted=None, spread=None, translation=False, filter=None) - ] - ) + TagValuePart(value="'abc", quoted=None, spread=None, translation=False, filter=None), + ], + ), ], ), start_index=68, @@ -601,10 +669,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=0, @@ -619,9 +691,9 @@ class TestTagParser: entries=[ TagValue( parts=[ - TagValuePart(value="my_comp", quoted='"', spread=None, translation=False, filter=None) - ] - ) + TagValuePart(value="my_comp", quoted='"', spread=None, translation=False, filter=None), + ], + ), ], ), start_index=10, @@ -635,8 +707,10 @@ class TestTagParser: parser=None, entries=[ TagValue( - parts=[TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None)] - ) + parts=[ + TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None), + ], + ), ], ), start_index=20, @@ -652,10 +726,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="val2 'two'", quoted='"', spread=None, translation=False, filter=None - ) - ] - ) + value="val2 'two'", + quoted='"', + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=28, @@ -671,10 +749,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value='organisation"s', quoted="'", spread=None, translation=False, filter=None - ) - ] - ) + value='organisation"s', + quoted="'", + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=46, @@ -689,9 +771,9 @@ class TestTagParser: entries=[ TagValue( parts=[ - TagValuePart(value='"abc', quoted=None, spread=None, translation=False, filter=None) - ] - ) + TagValuePart(value='"abc', quoted=None, spread=None, translation=False, filter=None), + ], + ), ], ), start_index=68, @@ -723,10 +805,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=0, @@ -741,9 +827,9 @@ class TestTagParser: entries=[ TagValue( parts=[ - TagValuePart(value="my_comp", quoted='"', spread=None, translation=False, filter=None) - ] - ) + TagValuePart(value="my_comp", quoted='"', spread=None, translation=False, filter=None), + ], + ), ], ), start_index=10, @@ -757,8 +843,8 @@ class TestTagParser: parser=None, entries=[ TagValue( - parts=[TagValuePart(value="one", quoted='"', spread=None, translation=True, filter=None)] - ) + parts=[TagValuePart(value="one", quoted='"', spread=None, translation=True, filter=None)], + ), ], ), start_index=20, @@ -772,8 +858,8 @@ class TestTagParser: parser=None, entries=[ TagValue( - parts=[TagValuePart(value="two", quoted='"', spread=None, translation=True, filter=None)] - ) + parts=[TagValuePart(value="two", quoted='"', spread=None, translation=True, filter=None)], + ), ], ), start_index=29, @@ -806,10 +892,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=0, @@ -824,9 +914,9 @@ class TestTagParser: entries=[ TagValue( parts=[ - TagValuePart(value="my_comp", quoted='"', spread=None, translation=False, filter=None) - ] - ) + TagValuePart(value="my_comp", quoted='"', spread=None, translation=False, filter=None), + ], + ), ], ), start_index=10, @@ -843,8 +933,8 @@ class TestTagParser: parts=[ TagValuePart(value="value", quoted=None, spread=None, translation=False, filter=None), TagValuePart(value="lower", quoted=None, spread=None, translation=False, filter="|"), - ] - ) + ], + ), ], ), start_index=20, @@ -862,8 +952,8 @@ class TestTagParser: TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None), TagValuePart(value="yesno", quoted=None, spread=None, translation=False, filter="|"), TagValuePart(value="yes,no", quoted='"', spread=None, translation=False, filter=":"), - ] - ) + ], + ), ], ), start_index=32, @@ -882,8 +972,8 @@ class TestTagParser: TagValuePart(value="default", quoted=None, spread=None, translation=False, filter="|"), TagValuePart(value="N/A", quoted='"', spread=None, translation=False, filter=":"), TagValuePart(value="upper", quoted=None, spread=None, translation=False, filter="|"), - ] - ) + ], + ), ], ), start_index=55, @@ -914,10 +1004,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=0, @@ -933,8 +1027,8 @@ class TestTagParser: TagValue( parts=[ TagValuePart(value="test", quoted='"', spread=None, translation=True, filter=None), - ] - ) + ], + ), ], ), start_index=10, @@ -957,10 +1051,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=0, @@ -977,8 +1075,8 @@ class TestTagParser: parts=[ TagValuePart(value="value", quoted=None, spread=None, translation=False, filter=None), TagValuePart(value="lower", quoted=None, spread=None, translation=False, filter="|"), - ] - ) + ], + ), ], ), start_index=10, @@ -995,8 +1093,8 @@ class TestTagParser: parts=[ TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None), TagValuePart(value="upper", quoted=None, spread=None, translation=False, filter="|"), - ] - ) + ], + ), ], ), start_index=29, @@ -1012,8 +1110,8 @@ class TestTagParser: TagValue( parts=[ TagValuePart(value="val2", quoted=None, spread=None, translation=False, filter=None), - ] - ) + ], + ), ], ), start_index=50, @@ -1044,10 +1142,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=0, @@ -1063,12 +1165,12 @@ class TestTagParser: TagValue( parts=[ TagValuePart(value="key", quoted='"', spread=None, translation=False, filter=None), - ] + ], ), TagValue( parts=[ TagValuePart(value="val", quoted='"', spread=None, translation=False, filter=None), - ] + ], ), ], ), @@ -1093,10 +1195,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=0, @@ -1112,12 +1218,12 @@ class TestTagParser: TagValue( parts=[ TagValuePart(value="key", quoted='"', spread=None, translation=False, filter=None), - ] + ], ), TagValue( parts=[ TagValuePart(value="val", quoted='"', spread=None, translation=False, filter=None), - ] + ], ), ], ), @@ -1163,10 +1269,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=0, @@ -1182,8 +1292,8 @@ class TestTagParser: TagValue( parts=[ TagValuePart(value="spread", quoted=None, spread="**", translation=False, filter=None), - ] - ) + ], + ), ], ), start_index=10, @@ -1207,10 +1317,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=0, @@ -1226,27 +1340,27 @@ class TestTagParser: TagValue( parts=[ TagValuePart(value="key", quoted='"', spread=None, translation=False, filter=None), - ] + ], ), TagValue( parts=[ TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None), - ] + ], ), TagValue( parts=[ TagValuePart(value="spread", quoted=None, spread="**", translation=False, filter=None), - ] + ], ), TagValue( parts=[ TagValuePart(value="key2", quoted='"', spread=None, translation=False, filter=None), - ] + ], ), TagValue( parts=[ TagValuePart(value="val2", quoted=None, spread=None, translation=False, filter=None), - ] + ], ), ], ), @@ -1281,10 +1395,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, ), - ] - ) + ], + ), ], ), start_index=0, @@ -1300,17 +1418,17 @@ class TestTagParser: TagValue( parts=[ TagValuePart(value="1", quoted=None, spread=None, translation=False, filter=None), - ] + ], ), TagValue( parts=[ TagValuePart(value="2", quoted=None, spread=None, translation=False, filter=None), - ] + ], ), TagValue( parts=[ TagValuePart(value="3", quoted=None, spread=None, translation=False, filter=None), - ] + ], ), ], ), @@ -1335,10 +1453,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, ), - ] - ) + ], + ), ], ), start_index=0, @@ -1354,17 +1476,17 @@ class TestTagParser: TagValue( parts=[ TagValuePart(value="1", quoted=None, spread=None, translation=False, filter=None), - ] + ], ), TagValue( parts=[ TagValuePart(value="2", quoted=None, spread=None, translation=False, filter=None), - ] + ], ), TagValue( parts=[ TagValuePart(value="3", quoted=None, spread=None, translation=False, filter=None), - ] + ], ), ], ), @@ -1409,10 +1531,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, ), - ] - ) + ], + ), ], ), start_index=17, @@ -1426,19 +1552,19 @@ class TestTagParser: parser=None, entries=[ TagValue( - parts=[TagValuePart(value="1", quoted=None, spread=None, translation=False, filter=None)] + parts=[TagValuePart(value="1", quoted=None, spread=None, translation=False, filter=None)], ), TagValue( parts=[ TagValuePart(value="2", quoted=None, spread=None, translation=False, filter=None), TagValuePart(value="add", quoted=None, spread=None, translation=False, filter="|"), TagValuePart(value="3", quoted=None, spread=None, translation=False, filter=":"), - ] + ], ), TagValue( parts=[ - TagValuePart(value="spread", quoted=None, spread="*", translation=False, filter=None) - ] + TagValuePart(value="spread", quoted=None, spread="*", translation=False, filter=None), + ], ), ], ), @@ -1456,20 +1582,20 @@ class TestTagParser: parts=[ TagValuePart(value="a", quoted='"', spread=None, translation=False, filter=None), TagValuePart(value="upper", quoted=None, spread=None, translation=False, filter="|"), - ] + ], ), TagValue( parts=[ TagValuePart(value="b", quoted="'", spread=None, translation=False, filter=None), TagValuePart(value="lower", quoted=None, spread=None, translation=False, filter="|"), - ] + ], ), TagValue( parts=[ TagValuePart(value="c", quoted=None, spread=None, translation=False, filter=None), TagValuePart(value="default", quoted=None, spread=None, translation=False, filter="|"), TagValuePart(value="d", quoted='"', spread=None, translation=False, filter=":"), - ] + ], ), ], ), @@ -1484,7 +1610,7 @@ class TestTagParser: parser=None, entries=[ TagValue( - parts=[TagValuePart(value="1", quoted=None, spread=None, translation=False, filter=None)] + parts=[TagValuePart(value="1", quoted=None, spread=None, translation=False, filter=None)], ), TagValueStruct( type="list", @@ -1500,8 +1626,8 @@ class TestTagParser: spread="*", translation=False, filter=None, - ) - ] + ), + ], ), ], ), @@ -1519,8 +1645,8 @@ class TestTagParser: spread=None, translation=False, filter=None, - ) - ] + ), + ], ), TagValue( parts=[ @@ -1530,8 +1656,8 @@ class TestTagParser: spread=None, translation=False, filter=None, - ) - ] + ), + ], ), ], ), @@ -1581,9 +1707,13 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, ), - ] + ], ), ], ), @@ -1598,14 +1728,14 @@ class TestTagParser: parser=None, entries=[ TagValue( - parts=[TagValuePart(value="a", quoted='"', spread=None, translation=False, filter=None)] + parts=[TagValuePart(value="a", quoted='"', spread=None, translation=False, filter=None)], ), TagValue( parts=[ TagValuePart(value="1", quoted=None, spread=None, translation=False, filter=None), TagValuePart(value="add", quoted=None, spread=None, translation=False, filter="|"), TagValuePart(value="2", quoted=None, spread=None, translation=False, filter=":"), - ] + ], ), ], ), @@ -1623,21 +1753,21 @@ class TestTagParser: parts=[ TagValuePart(value="key", quoted='"', spread=None, translation=False, filter=None), TagValuePart(value="upper", quoted=None, spread=None, translation=False, filter="|"), - ] + ], ), TagValue( parts=[ TagValuePart(value="val", quoted=None, spread=None, translation=False, filter=None), TagValuePart(value="lower", quoted=None, spread=None, translation=False, filter="|"), - ] + ], ), TagValue( parts=[ - TagValuePart(value="spread", quoted=None, spread="**", translation=False, filter=None) - ] + TagValuePart(value="spread", quoted=None, spread="**", translation=False, filter=None), + ], ), TagValue( - parts=[TagValuePart(value="obj", quoted='"', spread=None, translation=False, filter=None)] + parts=[TagValuePart(value="obj", quoted='"', spread=None, translation=False, filter=None)], ), TagValueStruct( type="dict", @@ -1648,14 +1778,22 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="x", quoted='"', spread=None, translation=False, filter=None - ) - ] + value="x", + quoted='"', + spread=None, + translation=False, + filter=None, + ), + ], ), TagValue( parts=[ TagValuePart( - value="1", quoted=None, spread=None, translation=False, filter=None + value="1", + quoted=None, + spread=None, + translation=False, + filter=None, ), TagValuePart( value="add", @@ -1665,9 +1803,13 @@ class TestTagParser: filter="|", ), TagValuePart( - value="2", quoted=None, spread=None, translation=False, filter=":" + value="2", + quoted=None, + spread=None, + translation=False, + filter=":", ), - ] + ], ), ], ), @@ -1687,26 +1829,26 @@ class TestTagParser: parts=[ TagValuePart(value="a", quoted='"', spread=None, translation=False, filter=None), TagValuePart(value="lower", quoted=None, spread=None, translation=False, filter="|"), - ] + ], ), TagValue( parts=[ TagValuePart(value="b", quoted='"', spread=None, translation=False, filter=None), TagValuePart(value="upper", quoted=None, spread=None, translation=False, filter="|"), - ] + ], ), TagValue( parts=[ TagValuePart(value="c", quoted=None, spread=None, translation=False, filter=None), TagValuePart(value="default", quoted=None, spread=None, translation=False, filter="|"), - ] + ], ), TagValue( parts=[ TagValuePart(value="e", quoted='"', spread=None, translation=False, filter=None), TagValuePart(value="yesno", quoted=None, spread=None, translation=False, filter="|"), TagValuePart(value="yes,no", quoted='"', spread=None, translation=False, filter=":"), - ] + ], ), ], ), @@ -1762,9 +1904,13 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, ), - ] + ], ), ], ), @@ -1780,8 +1926,8 @@ class TestTagParser: entries=[ TagValue( parts=[ - TagValuePart(value="items", quoted='"', spread=None, translation=False, filter=None) - ] + TagValuePart(value="items", quoted='"', spread=None, translation=False, filter=None), + ], ), TagValueStruct( type="list", @@ -1792,7 +1938,11 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="1", quoted=None, spread=None, translation=False, filter=None + value="1", + quoted=None, + spread=None, + translation=False, + filter=None, ), TagValuePart( value="add", @@ -1802,9 +1952,13 @@ class TestTagParser: filter="|", ), TagValuePart( - value="2", quoted=None, spread=None, translation=False, filter=":" + value="2", + quoted=None, + spread=None, + translation=False, + filter=":", ), - ] + ], ), TagValueStruct( type="dict", @@ -1828,7 +1982,7 @@ class TestTagParser: translation=False, filter="|", ), - ] + ], ), TagValue( parts=[ @@ -1853,7 +2007,7 @@ class TestTagParser: translation=False, filter=":", ), - ] + ], ), ], ), @@ -1874,14 +2028,14 @@ class TestTagParser: filter="|", ), TagValuePart(value="", quoted='"', spread=None, translation=False, filter=":"), - ] + ], ), ], ), TagValue( parts=[ - TagValuePart(value="nested", quoted='"', spread=None, translation=False, filter=None) - ] + TagValuePart(value="nested", quoted='"', spread=None, translation=False, filter=None), + ], ), TagValueStruct( type="dict", @@ -1892,9 +2046,13 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="a", quoted='"', spread=None, translation=False, filter=None - ) - ] + value="a", + quoted='"', + spread=None, + translation=False, + filter=None, + ), + ], ), TagValueStruct( type="list", @@ -1950,16 +2108,20 @@ class TestTagParser: translation=False, filter=":", ), - ] + ], ), ], ), TagValue( parts=[ TagValuePart( - value="b", quoted='"', spread=None, translation=False, filter=None - ) - ] + value="b", + quoted='"', + spread=None, + translation=False, + filter=None, + ), + ], ), TagValueStruct( type="dict", @@ -1975,8 +2137,8 @@ class TestTagParser: spread=None, translation=False, filter=None, - ) - ] + ), + ], ), TagValueStruct( type="list", @@ -2007,7 +2169,7 @@ class TestTagParser: translation=False, filter=":", ), - ] + ], ), ], ), @@ -2019,16 +2181,16 @@ class TestTagParser: parts=[ TagValuePart(value="rest", quoted=None, spread="**", translation=False, filter=None), TagValuePart(value="default", quoted=None, spread=None, translation=False, filter="|"), - ] + ], ), TagValue( - parts=[TagValuePart(value="key", quoted='"', spread=None, translation=False, filter=None)] + parts=[TagValuePart(value="key", quoted='"', spread=None, translation=False, filter=None)], ), TagValue( parts=[ TagValuePart(value="value", quoted="'", spread=None, translation=True, filter=None), TagValuePart(value="upper", quoted=None, spread=None, translation=False, filter="|"), - ] + ], ), ], ), @@ -2086,9 +2248,13 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, ), - ] + ], ), ], ), @@ -2105,19 +2271,23 @@ class TestTagParser: TagValue( parts=[ TagValuePart(value="a", quoted='"', spread=None, translation=False, filter=None), - ] + ], ), TagValue( parts=[ TagValuePart(value="b", quoted='"', spread=None, translation=False, filter=None), - ] + ], ), TagValue( parts=[ TagValuePart( - value="my_attr", quoted=None, spread="**", translation=False, filter=None + value="my_attr", + quoted=None, + spread="**", + translation=False, + filter=None, ), - ] + ], ), ], ), @@ -2134,12 +2304,12 @@ class TestTagParser: TagValue( parts=[ TagValuePart(value="a", quoted='"', spread=None, translation=False, filter=None), - ] + ], ), TagValue( parts=[ TagValuePart(value="my_list", quoted=None, spread="*", translation=False, filter=None), - ] + ], ), ], ), @@ -2231,10 +2401,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], spread=None, meta={}, @@ -2263,8 +2437,8 @@ class TestTagParser: spread=None, translation=False, filter=None, - ) - ] + ), + ], ), TagValue( parts=[ @@ -2274,18 +2448,18 @@ class TestTagParser: spread=None, translation=False, filter=None, - ) - ] + ), + ], ), ], ), TagValue( - parts=[TagValuePart(value="key", quoted='"', spread=None, translation=False, filter=None)] + parts=[TagValuePart(value="key", quoted='"', spread=None, translation=False, filter=None)], ), TagValue( parts=[ - TagValuePart(value="val1", quoted=None, spread=None, translation=False, filter=None) - ] + TagValuePart(value="val1", quoted=None, spread=None, translation=False, filter=None), + ], ), ], ), @@ -2314,10 +2488,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], ), start_index=0, @@ -2331,12 +2509,12 @@ class TestTagParser: parser=None, entries=[ TagValue( - parts=[TagValuePart(value="key", quoted='"', spread=None, translation=False, filter=None)] + parts=[TagValuePart(value="key", quoted='"', spread=None, translation=False, filter=None)], ), TagValue( parts=[ - TagValuePart(value="val2", quoted=None, spread=None, translation=False, filter=None) - ] + TagValuePart(value="val2", quoted=None, spread=None, translation=False, filter=None), + ], ), ], ), @@ -2363,10 +2541,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], spread=None, meta={}, @@ -2395,15 +2577,15 @@ class TestTagParser: spread=None, translation=False, filter=None, - ) - ] + ), + ], ), ], ), TagValue( parts=[ - TagValuePart(value="val2", quoted=None, spread=None, translation=False, filter=None) - ] + TagValuePart(value="val2", quoted=None, spread=None, translation=False, filter=None), + ], ), ], ), @@ -2430,10 +2612,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], spread=None, meta={}, @@ -2450,8 +2636,8 @@ class TestTagParser: entries=[ TagValue( parts=[ - TagValuePart(value="val1", quoted=None, spread=None, translation=False, filter=None) - ] + TagValuePart(value="val1", quoted=None, spread=None, translation=False, filter=None), + ], ), ], ), @@ -2477,10 +2663,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) # noqa: E501 + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), ], - ) + ), ], spread=None, meta={}, @@ -2496,10 +2686,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="{% lorem w 4 %}", quoted="'", spread=None, translation=False, filter=None - ) + value="{% lorem w 4 %}", + quoted="'", + spread=None, + translation=False, + filter=None, + ), ], - ) + ), ], spread=None, meta={}, @@ -2522,10 +2716,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) # noqa: E501 - ] - ) + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), + ], + ), ], spread=None, meta={}, @@ -2539,14 +2737,18 @@ class TestTagParser: type="dict", entries=[ TagValue( - parts=[TagValuePart(value="key", quoted='"', spread=None, translation=False, filter=None)] + parts=[TagValuePart(value="key", quoted='"', spread=None, translation=False, filter=None)], ), TagValue( parts=[ TagValuePart( - value="{% lorem w 4 %}", quoted='"', spread=None, translation=False, filter=None - ) # noqa: E501 - ] + value="{% lorem w 4 %}", + quoted='"', + spread=None, + translation=False, + filter=None, + ), + ], ), ], spread=None, @@ -2570,10 +2772,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="component", quoted=None, spread=None, translation=False, filter=None - ) # noqa: E501 + value="component", + quoted=None, + spread=None, + translation=False, + filter=None, + ), ], - ) + ), ], spread=None, meta={}, @@ -2589,10 +2795,14 @@ class TestTagParser: TagValue( parts=[ TagValuePart( - value="{% lorem w 4 %}", quoted="'", spread=None, translation=False, filter=None - ) - ] - ) + value="{% lorem w 4 %}", + quoted="'", + spread=None, + translation=False, + filter=None, + ), + ], + ), ], spread=None, meta={}, @@ -2683,7 +2893,7 @@ class TestResolver: "nums": [1, 2, 3], "more": ["baz", "qux"], "rest": {"a": "b"}, - } + }, ) resolved = attrs[0].value.resolve(context) @@ -2746,8 +2956,8 @@ class TestResolver: "nums": [1, 2, 3], "more": ["baz", "qux"], "rest": {"a": "b"}, - } - ) + }, + ), ) assert captured == { diff --git a/tests/test_template.py b/tests/test_template.py index a7a250a7..48aa68d3 100644 --- a/tests/test_template.py +++ b/tests/test_template.py @@ -1,9 +1,9 @@ from django.template import Template from django_components import Component, cached_template, types - from django_components.template import _get_component_template from django_components.testing import djc_test + from .testutils import setup_test_config setup_test_config({"autodiscover": False}) diff --git a/tests/test_template_parser.py b/tests/test_template_parser.py index 1c630643..29bc31b9 100644 --- a/tests/test_template_parser.py +++ b/tests/test_template_parser.py @@ -3,9 +3,9 @@ from django.template.base import Template, Token, TokenType from pytest_django.asserts import assertHTMLEqual from django_components import Component, register, types +from django_components.testing import djc_test from django_components.util.template_parser import parse_template -from django_components.testing import djc_test from .testutils import setup_test_config setup_test_config({"autodiscover": False}) @@ -103,7 +103,7 @@ class TestTemplateParser: """{% verbatim %} {{ this_is_not_a_var }} {% this_is_not_a_tag %} - {% endverbatim %}""" + {% endverbatim %}""", ) token_tuples = [token2tuple(token) for token in tokens] @@ -126,7 +126,7 @@ class TestTemplateParser: {% verbatim %} {% endverbatim %} {% endverbatim blockname %} - {% endverbatim myblock %}""" + {% endverbatim myblock %}""", ) token_tuples = [token2tuple(token) for token in tokens] @@ -178,7 +178,7 @@ class TestTemplateParser: {% endcomponent %} {% endverbatim %} {% endcomponent %} - {% endif %}""" + {% endif %}""", ) token_tuples = [token2tuple(token) for token in tokens] diff --git a/tests/test_templatetags.py b/tests/test_templatetags.py index 3c494ea3..17c2208d 100644 --- a/tests/test_templatetags.py +++ b/tests/test_templatetags.py @@ -2,9 +2,10 @@ from django.template import Context, Template from pytest_django.asserts import assertHTMLEqual -from django_components import Component, register, registry, types +from django_components import Component, register, registry, types from django_components.testing import djc_test + from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config setup_test_config({"autodiscover": False}) diff --git a/tests/test_templatetags_component.py b/tests/test_templatetags_component.py index bf5addcd..026d9056 100644 --- a/tests/test_templatetags_component.py +++ b/tests/test_templatetags_component.py @@ -4,9 +4,10 @@ from typing import NamedTuple import pytest from django.template import Context, Template, TemplateSyntaxError from pytest_django.asserts import assertHTMLEqual -from django_components import AlreadyRegistered, Component, NotRegistered, register, registry, types +from django_components import AlreadyRegistered, Component, NotRegistered, register, registry, types from django_components.testing import djc_test + from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config setup_test_config({"autodiscover": False}) @@ -775,7 +776,6 @@ class TestRecursiveComponent: depth = 0 def get_template_data(self, args, kwargs, slots, context): - print("depth:", kwargs["depth"]) return {"depth": kwargs["depth"] + 1, "DEPTH": DEPTH} template: types.django_html = """ @@ -837,20 +837,23 @@ class TestComponentTemplateSyntaxError: with pytest.raises( TemplateSyntaxError, - match=re.escape("Illegal content passed to component 'test'. Explicit 'fill' tags cannot occur alongside other text"), # noqa: E501 + match=re.escape( + "Illegal content passed to component 'test'. Explicit 'fill' tags cannot occur alongside other text", + ), ): template.render(Context()) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) def test_unclosed_component_is_error(self, components_settings): registry.register("test", gen_slotted_component()) + + template_str: types.django_html = """ + {% load component_tags %} + {% component "test" %} + {% fill "header" %}{% endfill %} + """ with pytest.raises( TemplateSyntaxError, match=re.escape("Unclosed tag on line 3: 'component'"), ): - template_str: types.django_html = """ - {% load component_tags %} - {% component "test" %} - {% fill "header" %}{% endfill %} - """ Template(template_str) diff --git a/tests/test_templatetags_extends.py b/tests/test_templatetags_extends.py index a935f221..76150f3c 100644 --- a/tests/test_templatetags_extends.py +++ b/tests/test_templatetags_extends.py @@ -4,8 +4,8 @@ from django.template import Context, Template from pytest_django.asserts import assertHTMLEqual from django_components import Component, register, registry, types - from django_components.testing import djc_test + from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config setup_test_config({"autodiscover": False}) @@ -146,7 +146,8 @@ class TestExtendsCompat: @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) def test_double_extends_on_main_template_and_component_two_different_components_same_parent( - self, components_settings + self, + components_settings, ): registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component()) @@ -214,7 +215,8 @@ class TestExtendsCompat: @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) def test_double_extends_on_main_template_and_component_two_different_components_different_parent( - self, components_settings + self, + components_settings, ): registry.register("blocked_and_slotted_component", gen_blocked_and_slotted_component()) diff --git a/tests/test_templatetags_provide.py b/tests/test_templatetags_provide.py index 347c697e..b61ca21f 100644 --- a/tests/test_templatetags_provide.py +++ b/tests/test_templatetags_provide.py @@ -5,9 +5,9 @@ from django.template import Context, Template, TemplateSyntaxError from pytest_django.asserts import assertHTMLEqual from django_components import Component, register, types -from django_components.perfutil.provide import provide_cache, provide_references, all_reference_ids - +from django_components.perfutil.provide import all_reference_ids, provide_cache, provide_references from django_components.testing import djc_test + from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config setup_test_config({"autodiscover": False}) @@ -293,8 +293,8 @@ class TestProvideTemplateTag: Context( { "var_a": "my_provide", - } - ) + }, + ), ) assertHTMLEqual( @@ -336,8 +336,8 @@ class TestProvideTemplateTag: "key": "hi", "another": 9, }, - } - ) + }, + ), ) assertHTMLEqual( @@ -979,7 +979,6 @@ class TestProvideCache: # Cache should be cleared even if there is an error. def test_provide_outside_component_with_error(self): - @register("injectee") class Injectee(Component): template = "" diff --git a/tests/test_templatetags_slot_fill.py b/tests/test_templatetags_slot_fill.py index ffa57d90..9965f67e 100644 --- a/tests/test_templatetags_slot_fill.py +++ b/tests/test_templatetags_slot_fill.py @@ -6,8 +6,8 @@ from django.template import Context, Template, TemplateSyntaxError from pytest_django.asserts import assertHTMLEqual from django_components import Component, Slot, register, registry, types - from django_components.testing import djc_test + from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config setup_test_config({"autodiscover": False}) @@ -23,8 +23,10 @@ def _gen_slotted_component():
{% slot "footer" %}Default footer{% endslot %}
""" + return SlottedComponent + ####################### # TESTS ####################### @@ -138,7 +140,7 @@ class TestComponentSlot: [{"context_behavior": "isolated"}, ""], ], ["django", "isolated"], - ) + ), ) def test_slotted_template_with_context_var(self, components_settings, expected): @register("test1") @@ -384,7 +386,7 @@ class TestComponentSlot: TemplateSyntaxError, match=re.escape( "Multiple fill tags cannot target the same slot name in component 'test': " - "Detected duplicate fill tag name 'title'" + "Detected duplicate fill tag name 'title'", ), ): template.render(Context()) @@ -413,7 +415,7 @@ class TestComponentSlot: with pytest.raises( TemplateSyntaxError, match=re.escape( - "Slot 'title' of component 'test' was filled twice: once explicitly and once implicitly as 'default'" + "Slot 'title' of component 'test' was filled twice: once explicitly and once implicitly as 'default'", ), ): template.render(Context()) @@ -443,7 +445,7 @@ class TestComponentSlot: TemplateSyntaxError, match=re.escape( "Multiple fill tags cannot target the same slot name in component 'test': " - "Detected duplicate fill tag name 'default'" + "Detected duplicate fill tag name 'default'", ), ): template.render(Context()) @@ -673,7 +675,7 @@ class TestComponentSlotDefault: with pytest.raises( TemplateSyntaxError, match=re.escape( - "Only one component slot may be marked as 'default', found 'main' and 'other'" + "Only one component slot may be marked as 'default', found 'main' and 'other'", ), ): template.render(Context({})) @@ -752,19 +754,19 @@ class TestComponentSlotDefault:
""" + template_str: types.django_html = """ + {% load component_tags %} + {% component 'test_comp' %} + {% fill "main" %}Main content{% endfill %} +

And add this too!

+ {% endcomponent %} + """ with pytest.raises( TemplateSyntaxError, match=re.escape( - "Illegal content passed to component 'test_comp'. Explicit 'fill' tags cannot occur alongside other text" # noqa: E501 + "Illegal content passed to component 'test_comp'. Explicit 'fill' tags cannot occur alongside other text", # noqa: E501 ), ): - template_str: types.django_html = """ - {% load component_tags %} - {% component 'test_comp' %} - {% fill "main" %}Main content{% endfill %} -

And add this too!

- {% endcomponent %} - """ Template(template_str).render(Context({})) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) @@ -1187,6 +1189,7 @@ class TestNestedSlots:
{% endslot %} """ + return NestedSlots @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) @@ -1366,7 +1369,6 @@ class TestSlottedTemplateRegression: @djc_test class TestSlotFallback: - # TODO_v1 - REMOVE @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) def test_basic_legacy(self, components_settings): @@ -1771,7 +1773,9 @@ class TestScopedSlot: """ with pytest.raises( RuntimeError, - match=re.escape("Fill 'my_slot' received the same string for slot fallback (fallback=...) and slot data (data=...)"), # noqa: E501 + match=re.escape( + "Fill 'my_slot' received the same string for slot fallback (fallback=...) and slot data (data=...)", + ), ): Template(template).render(Context()) @@ -1884,8 +1888,8 @@ class TestScopedSlot: { "fill_name": "my_slot", "data_var": "slot_data_in_fill", - } - ) + }, + ), ) expected = """ @@ -1929,8 +1933,8 @@ class TestScopedSlot: "name": "my_slot", "data": "slot_data_in_fill", }, - } - ) + }, + ), ) expected = """ @@ -2005,6 +2009,7 @@ class TestDuplicateSlot: return { "name": kwargs.get("name", None), } + return DuplicateSlotComponent def _gen_duplicate_slot_nested_component(self): @@ -2032,11 +2037,12 @@ class TestDuplicateSlot: return { "items": kwargs["items"], } + return DuplicateSlotNestedComponent def _gen_calendar_component(self): class CalendarComponent(Component): - """Nested in ComponentWithNestedComponent""" + """Nested in ComponentWithNestedComponent.""" template: types.django_html = """ {% load component_tags %} @@ -2051,6 +2057,7 @@ class TestDuplicateSlot:
""" + return CalendarComponent # NOTE: Second arg is the input for the "name" component kwarg @@ -2064,9 +2071,9 @@ class TestDuplicateSlot: [{"context_behavior": "isolated"}, None], ], ["django", "isolated"], - ) + ), ) - def test_duplicate_slots(self, components_settings, input): + def test_duplicate_slots(self, components_settings, input): # noqa: A002 registry.register(name="duplicate_slot", component=self._gen_duplicate_slot_component()) registry.register(name="calendar", component=self._gen_calendar_component()) @@ -2204,54 +2211,58 @@ class TestDuplicateSlot: class TestSlotFillTemplateSyntaxError: @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) def test_fill_with_no_parent_is_error(self, components_settings): + template_str: types.django_html = """ + {% load component_tags %} + {% fill "header" %}contents{% endfill %} + """ with pytest.raises( TemplateSyntaxError, - match=re.escape("FillNode.render() (AKA {% fill ... %} block) cannot be rendered outside of a Component context"), # noqa: E501 + match=re.escape( + "FillNode.render() (AKA {% fill ... %} block) cannot be rendered outside of a Component context", + ), ): - template_str: types.django_html = """ - {% load component_tags %} - {% fill "header" %}contents{% endfill %} - """ Template(template_str).render(Context({})) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) def test_non_unique_fill_names_is_error(self, components_settings): registry.register("test", _gen_slotted_component()) + + template_str: types.django_html = """ + {% load component_tags %} + {% component "test" %} + {% fill "header" %}Custom header {% endfill %} + {% fill "header" %}Other header{% endfill %} + {% endcomponent %} + """ with pytest.raises( TemplateSyntaxError, match=re.escape( "Multiple fill tags cannot target the same slot name in component 'test': " - "Detected duplicate fill tag name 'header'" + "Detected duplicate fill tag name 'header'", ), ): - template_str: types.django_html = """ - {% load component_tags %} - {% component "test" %} - {% fill "header" %}Custom header {% endfill %} - {% fill "header" %}Other header{% endfill %} - {% endcomponent %} - """ Template(template_str).render(Context({})) @djc_test(parametrize=PARAMETRIZE_CONTEXT_BEHAVIOR) def test_non_unique_fill_names_is_error_via_vars(self, components_settings): registry.register("test", _gen_slotted_component()) + + template_str: types.django_html = """ + {% load component_tags %} + {% with var1="header" var2="header" %} + {% component "test" %} + {% fill var1 %}Custom header {% endfill %} + {% fill var2 %}Other header{% endfill %} + {% endcomponent %} + {% endwith %} + """ with pytest.raises( TemplateSyntaxError, match=re.escape( "Multiple fill tags cannot target the same slot name in component 'test': " - "Detected duplicate fill tag name 'header'" + "Detected duplicate fill tag name 'header'", ), ): - template_str: types.django_html = """ - {% load component_tags %} - {% with var1="header" var2="header" %} - {% component "test" %} - {% fill var1 %}Custom header {% endfill %} - {% fill var2 %}Other header{% endfill %} - {% endcomponent %} - {% endwith %} - """ Template(template_str).render(Context({})) @@ -2440,16 +2451,16 @@ class TestSlotInput: assert seen_slots == {} - header_slot: Slot = Slot(lambda ctx: "HEADER_SLOT") + header_slot: Slot = Slot(lambda _ctx: "HEADER_SLOT") main_slot_str = "MAIN_SLOT" - footer_slot_fn = lambda ctx: "FOOTER_SLOT" # noqa: E731 + footer_slot_fn = lambda _ctx: "FOOTER_SLOT" # noqa: E731 SlottedComponent.render( slots={ "header": header_slot, "main": main_slot_str, "footer": footer_slot_fn, - } + }, ) assert isinstance(seen_slots["header"], Slot) diff --git a/tests/test_templatetags_templating.py b/tests/test_templatetags_templating.py index 53799b7c..f1656925 100644 --- a/tests/test_templatetags_templating.py +++ b/tests/test_templatetags_templating.py @@ -1,11 +1,13 @@ -"""This file tests various ways how the individual tags can be combined inside the templates""" +"""Tests various ways how the individual tags can be combined inside the templates.""" + +from typing import Dict from django.template import Context, Template from pytest_django.asserts import assertHTMLEqual from django_components import Component, register, registry, types - from django_components.testing import djc_test + from .testutils import PARAMETRIZE_CONTEXT_BEHAVIOR, setup_test_config setup_test_config({"autodiscover": False}) @@ -88,7 +90,7 @@ class TestNestedSlot: [{"context_behavior": "isolated"}, "Jannete"], ], ["django", "isolated"], - ) + ), ) def test_fill_inside_fill_with_same_name(self, components_settings, expected): class SlottedComponent(Component): @@ -281,7 +283,7 @@ class TestSlotIteration: [{"context_behavior": "isolated"}, ""], ], ["django", "isolated"], - ) + ), ) def test_inner_slot_iteration_basic(self, components_settings, expected): registry.register("slot_in_a_loop", self._get_component_simple_slot_in_a_loop()) @@ -310,7 +312,7 @@ class TestSlotIteration: [{"context_behavior": "isolated"}, "OUTER_SCOPE_VARIABLE OUTER_SCOPE_VARIABLE"], ], ["django", "isolated"], - ) + ), ) def test_inner_slot_iteration_with_variable_from_outer_scope(self, components_settings, expected): registry.register("slot_in_a_loop", self._get_component_simple_slot_in_a_loop()) @@ -331,8 +333,8 @@ class TestSlotIteration: { "objects": objects, "outer_scope_variable": "OUTER_SCOPE_VARIABLE", - } - ) + }, + ), ) assertHTMLEqual(rendered, expected) @@ -346,7 +348,7 @@ class TestSlotIteration: [{"context_behavior": "isolated"}, ""], ], ["django", "isolated"], - ) + ), ) def test_inner_slot_iteration_nested(self, components_settings, expected): registry.register("slot_in_a_loop", self._get_component_simple_slot_in_a_loop()) @@ -397,7 +399,7 @@ class TestSlotIteration: [{"context_behavior": "isolated"}, "OUTER_SCOPE_VARIABLE1 OUTER_SCOPE_VARIABLE1"], ], ["django", "isolated"], - ) + ), ) def test_inner_slot_iteration_nested_with_outer_scope_variable(self, components_settings, expected): registry.register("slot_in_a_loop", self._get_component_simple_slot_in_a_loop()) @@ -428,8 +430,8 @@ class TestSlotIteration: "objects": objects, "outer_scope_variable_1": "OUTER_SCOPE_VARIABLE1", "outer_scope_variable_2": "OUTER_SCOPE_VARIABLE2", - } - ) + }, + ), ) assertHTMLEqual(rendered, expected) @@ -439,11 +441,14 @@ class TestSlotIteration: parametrize=( ["components_settings", "expected"], [ - [{"context_behavior": "django"}, "ITER1_OBJ1 default ITER1_OBJ2 default ITER2_OBJ1 default ITER2_OBJ2 default"], # noqa: E501 + [ + {"context_behavior": "django"}, + "ITER1_OBJ1 default ITER1_OBJ2 default ITER2_OBJ1 default ITER2_OBJ2 default", + ], [{"context_behavior": "isolated"}, ""], ], ["django", "isolated"], - ) + ), ) def test_inner_slot_iteration_nested_with_slot_fallback(self, components_settings, expected): registry.register("slot_in_a_loop", self._get_component_simple_slot_in_a_loop()) @@ -496,7 +501,7 @@ class TestSlotIteration: [{"context_behavior": "isolated"}, "OUTER_SCOPE_VARIABLE1 OUTER_SCOPE_VARIABLE1"], ], ["django", "isolated"], - ) + ), ) def test_inner_slot_iteration_nested_with_slot_fallback_and_outer_scope_variable( self, @@ -531,8 +536,8 @@ class TestSlotIteration: "objects": objects, "outer_scope_variable_1": "OUTER_SCOPE_VARIABLE1", "outer_scope_variable_2": "OUTER_SCOPE_VARIABLE2", - } - ) + }, + ), ) assertHTMLEqual(rendered, expected) @@ -571,8 +576,8 @@ class TestSlotIteration: "objects": objects, "outer_scope_variable_1": "OUTER_SCOPE_VARIABLE1", "outer_scope_variable_2": "OUTER_SCOPE_VARIABLE2", - } - ) + }, + ), ) assertHTMLEqual( @@ -596,7 +601,7 @@ class TestSlotIteration: class TestComponentNesting: def _get_calendar_component(self): class CalendarComponent(Component): - """Nested in ComponentWithNestedComponent""" + """Nested in ComponentWithNestedComponent.""" template: types.django_html = """ {% load component_tags %} @@ -611,6 +616,7 @@ class TestComponentNesting:
""" + return CalendarComponent def _get_dashboard_component(self): @@ -631,6 +637,7 @@ class TestComponentNesting:
""" + return DashboardComponent # NOTE: Second arg in tuple are expected names in nested fills. In "django" mode, @@ -644,7 +651,7 @@ class TestComponentNesting: [{"context_behavior": "isolated"}, "Jannete", "Jannete"], ], ["django", "isolated"], - ) + ), ) def test_component_inside_slot(self, components_settings, first_name, second_name): registry.register("dashboard", self._get_dashboard_component()) @@ -717,7 +724,7 @@ class TestComponentNesting: [{"context_behavior": "isolated"}, ""], ], ["django", "isolated"], - ) + ), ) def test_component_nesting_component_without_fill(self, components_settings, expected): registry.register("dashboard", self._get_dashboard_component()) @@ -755,7 +762,7 @@ class TestComponentNesting: [{"context_behavior": "isolated"}, ""], ], ["django", "isolated"], - ) + ), ) def test_component_nesting_slot_inside_component_fill(self, components_settings, expected): registry.register("dashboard", self._get_dashboard_component()) @@ -910,7 +917,7 @@ class TestComponentNesting: slots={ "left_panel": "LEFT PANEL SLOT FROM FILL", "content": "CONTENT SLOT FROM FILL", - } + }, ) expected = """ @@ -957,9 +964,9 @@ class TestComponentNesting: [{"context_behavior": "isolated"}, ""], ], ["django", "isolated"], - ) + ), ) - def test_component_nesting_component_with_slot_fallback(self, components_settings, expected): + def test_component_nesting_component_with_slot_fallback(self, components_settings: Dict, expected: str): registry.register("dashboard", self._get_dashboard_component()) registry.register("calendar", self._get_calendar_component()) diff --git a/tests/test_utils.py b/tests/test_utils.py index 3d746e62..bc876dae 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,5 +1,6 @@ from django_components.util.misc import is_str_wrapped_in_quotes + class TestUtils: def test_is_str_wrapped_in_quotes(self): assert is_str_wrapped_in_quotes("word") is False diff --git a/tests/testutils.py b/tests/testutils.py index 9f3a56d4..c7fa653c 100644 --- a/tests/testutils.py +++ b/tests/testutils.py @@ -20,7 +20,7 @@ PARAMETRIZE_CONTEXT_BEHAVIOR = ( def setup_test_config( components: Optional[Dict] = None, extra_settings: Optional[Dict] = None, -): +) -> None: if settings.configured: return @@ -38,16 +38,16 @@ def setup_test_config( "builtins": [ "django_components.templatetags.component_tags", ], - 'loaders': [ + "loaders": [ # Default Django loader - 'django.template.loaders.filesystem.Loader', + "django.template.loaders.filesystem.Loader", # Including this is the same as APP_DIRS=True - 'django.template.loaders.app_directories.Loader', + "django.template.loaders.app_directories.Loader", # Components loader - 'django_components.template_loader.Loader', + "django_components.template_loader.Loader", ], }, - } + }, ], "COMPONENTS": { **(components or {}), @@ -57,7 +57,7 @@ def setup_test_config( "default": { "ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:", - } + }, }, "SECRET_KEY": "secret", "ROOT_URLCONF": "django_components.urls", @@ -67,7 +67,7 @@ def setup_test_config( **{ **default_settings, **(extra_settings or {}), - } + }, ) django.setup() diff --git a/tox.ini b/tox.ini index 1fc774e7..237fbf4c 100644 --- a/tox.ini +++ b/tox.ini @@ -7,11 +7,9 @@ envlist = py{38,39}-django42 py{310,311,312}-django{42,51,52} py{313}-django{51,52} - flake8 - isort + ruff coverage mypy - black [gh-actions] python = @@ -20,7 +18,7 @@ python = 3.10: py310-django{42,51,52} 3.11: py311-django{42,51,52} 3.12: py312-django{42,51,52} - 3.13: py313-django{51,52}, flake8, isort, coverage, mypy, black + 3.13: py313-django{51,52}, ruff, coverage, mypy isolated_build = true @@ -48,14 +46,11 @@ deps = django-template-partials commands = pytest {posargs} -[testenv:flake8] -deps = flake8 - flake8-pyproject -commands = flake8 . - -[testenv:isort] -deps = isort -commands = isort --check-only --diff src/django_components +[testenv:ruff] +deps = ruff +commands = + ruff check . + ruff format --check . [testenv:coverage] deps = @@ -76,7 +71,3 @@ deps = mypy types-requests commands = mypy . - -[testenv:black] -deps = black -commands = black --check src/django_components