mirror of
https://github.com/django-components/django-components.git
synced 2025-08-09 16:57:59 +00:00
feat: add type hints everywhere
This commit is contained in:
parent
c11f30ec7c
commit
b9f4e596a4
12 changed files with 138 additions and 98 deletions
|
@ -46,6 +46,11 @@ exclude = [
|
||||||
'build',
|
'build',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[tool.mypy.overrides]]
|
||||||
|
module = "django_components.*"
|
||||||
|
disallow_untyped_defs = true
|
||||||
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = [
|
testpaths = [
|
||||||
"tests"
|
"tests"
|
||||||
|
|
|
@ -2,6 +2,7 @@ import importlib
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
import django
|
import django
|
||||||
from django.utils.module_loading import autodiscover_modules
|
from django.utils.module_loading import autodiscover_modules
|
||||||
|
@ -12,7 +13,7 @@ if django.VERSION < (3, 2):
|
||||||
default_app_config = "django_components.apps.ComponentsConfig"
|
default_app_config = "django_components.apps.ComponentsConfig"
|
||||||
|
|
||||||
|
|
||||||
def autodiscover():
|
def autodiscover() -> None:
|
||||||
from django_components.app_settings import app_settings
|
from django_components.app_settings import app_settings
|
||||||
|
|
||||||
if app_settings.AUTODISCOVER:
|
if app_settings.AUTODISCOVER:
|
||||||
|
@ -24,11 +25,11 @@ def autodiscover():
|
||||||
for path in component_filepaths:
|
for path in component_filepaths:
|
||||||
import_file(path)
|
import_file(path)
|
||||||
|
|
||||||
for path in app_settings.LIBRARIES:
|
for path_lib in app_settings.LIBRARIES:
|
||||||
importlib.import_module(path)
|
importlib.import_module(path_lib)
|
||||||
|
|
||||||
|
|
||||||
def import_file(path):
|
def import_file(path: Union[str, Path]) -> None:
|
||||||
MODULE_PATH = path
|
MODULE_PATH = path
|
||||||
MODULE_NAME = Path(path).stem
|
MODULE_NAME = Path(path).stem
|
||||||
spec = importlib.util.spec_from_file_location(MODULE_NAME, MODULE_PATH)
|
spec = importlib.util.spec_from_file_location(MODULE_NAME, MODULE_PATH)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
@ -9,27 +10,27 @@ class ContextBehavior(Enum):
|
||||||
|
|
||||||
|
|
||||||
class AppSettings:
|
class AppSettings:
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
self.settings = getattr(settings, "COMPONENTS", {})
|
self.settings = getattr(settings, "COMPONENTS", {})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def AUTODISCOVER(self):
|
def AUTODISCOVER(self) -> bool:
|
||||||
return self.settings.setdefault("autodiscover", True)
|
return self.settings.setdefault("autodiscover", True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def LIBRARIES(self):
|
def LIBRARIES(self) -> List:
|
||||||
return self.settings.setdefault("libraries", [])
|
return self.settings.setdefault("libraries", [])
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def TEMPLATE_CACHE_SIZE(self):
|
def TEMPLATE_CACHE_SIZE(self) -> int:
|
||||||
return self.settings.setdefault("template_cache_size", 128)
|
return self.settings.setdefault("template_cache_size", 128)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def CONTEXT_BEHAVIOR(self):
|
def CONTEXT_BEHAVIOR(self) -> ContextBehavior:
|
||||||
raw_value = self.settings.setdefault("context_behavior", ContextBehavior.GLOBAL.value)
|
raw_value = self.settings.setdefault("context_behavior", ContextBehavior.GLOBAL.value)
|
||||||
return self._validate_context_behavior(raw_value)
|
return self._validate_context_behavior(raw_value)
|
||||||
|
|
||||||
def _validate_context_behavior(self, raw_value):
|
def _validate_context_behavior(self, raw_value: ContextBehavior) -> ContextBehavior:
|
||||||
try:
|
try:
|
||||||
return ContextBehavior(raw_value)
|
return ContextBehavior(raw_value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
|
|
@ -4,5 +4,5 @@ from django.apps import AppConfig
|
||||||
class ComponentsConfig(AppConfig):
|
class ComponentsConfig(AppConfig):
|
||||||
name = "django_components"
|
name = "django_components"
|
||||||
|
|
||||||
def ready(self):
|
def ready(self) -> None:
|
||||||
self.module.autodiscover()
|
self.module.autodiscover()
|
||||||
|
|
|
@ -2,7 +2,22 @@ import difflib
|
||||||
import inspect
|
import inspect
|
||||||
import os
|
import os
|
||||||
from collections import ChainMap
|
from collections import ChainMap
|
||||||
from typing import Any, ClassVar, Dict, Iterable, List, Optional, Set, Tuple, Union
|
from pathlib import Path
|
||||||
|
from typing import (
|
||||||
|
Any,
|
||||||
|
ClassVar,
|
||||||
|
Dict,
|
||||||
|
Iterable,
|
||||||
|
List,
|
||||||
|
Mapping,
|
||||||
|
MutableMapping,
|
||||||
|
Optional,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
|
Type,
|
||||||
|
TypeVar,
|
||||||
|
Union,
|
||||||
|
)
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.forms.widgets import Media, MediaDefiningClass
|
from django.forms.widgets import Media, MediaDefiningClass
|
||||||
|
@ -12,7 +27,7 @@ from django.template.context import Context
|
||||||
from django.template.exceptions import TemplateSyntaxError
|
from django.template.exceptions import TemplateSyntaxError
|
||||||
from django.template.loader import get_template
|
from django.template.loader import get_template
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import SafeString, mark_safe
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
# Global registry var and register() function moved to separate module.
|
# Global registry var and register() function moved to separate module.
|
||||||
|
@ -39,9 +54,11 @@ from django_components.templatetags.component_tags import (
|
||||||
)
|
)
|
||||||
from django_components.utils import search
|
from django_components.utils import search
|
||||||
|
|
||||||
|
_T = TypeVar("_T")
|
||||||
|
|
||||||
|
|
||||||
class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
||||||
def __new__(mcs, name, bases, attrs):
|
def __new__(mcs, name: str, bases: Tuple[Type, ...], attrs: Dict[str, Any]) -> Type:
|
||||||
if "Media" in attrs:
|
if "Media" in attrs:
|
||||||
media: Component.Media = attrs["Media"]
|
media: Component.Media = attrs["Media"]
|
||||||
|
|
||||||
|
@ -68,7 +85,7 @@ class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
||||||
return super().__new__(mcs, name, bases, attrs)
|
return super().__new__(mcs, name, bases, attrs)
|
||||||
|
|
||||||
|
|
||||||
def _resolve_component_relative_files(attrs: dict):
|
def _resolve_component_relative_files(attrs: MutableMapping) -> None:
|
||||||
"""
|
"""
|
||||||
Check if component's HTML, JS and CSS files refer to files in the same directory
|
Check if component's HTML, JS and CSS files refer to files in the same directory
|
||||||
as the component class. If so, modify the attributes so the class Django's rendering
|
as the component class. If so, modify the attributes so the class Django's rendering
|
||||||
|
@ -96,7 +113,7 @@ def _resolve_component_relative_files(attrs: dict):
|
||||||
# Check if filepath refers to a file that's in the same directory as the component class.
|
# Check if filepath refers to a file that's in the same directory as the component class.
|
||||||
# If yes, modify the path to refer to the relative file.
|
# If yes, modify the path to refer to the relative file.
|
||||||
# If not, don't modify anything.
|
# If not, don't modify anything.
|
||||||
def resolve_file(filepath: str):
|
def resolve_file(filepath: str) -> str:
|
||||||
maybe_resolved_filepath = os.path.join(comp_dir_abs, filepath)
|
maybe_resolved_filepath = os.path.join(comp_dir_abs, filepath)
|
||||||
component_import_filepath = os.path.join(comp_dir_rel, filepath)
|
component_import_filepath = os.path.join(comp_dir_rel, filepath)
|
||||||
|
|
||||||
|
@ -128,7 +145,9 @@ def _resolve_component_relative_files(attrs: dict):
|
||||||
media.js = [resolve_file(filepath) for filepath in media.js]
|
media.js = [resolve_file(filepath) for filepath in media.js]
|
||||||
|
|
||||||
|
|
||||||
def _get_dir_path_from_component_module_path(component_module_path: str, candidate_dirs: List[str]):
|
def _get_dir_path_from_component_module_path(
|
||||||
|
component_module_path: str, candidate_dirs: Union[List[str], List[Path]]
|
||||||
|
) -> Tuple[str, str]:
|
||||||
# Transform python module notation "pkg.module.name" to file path "pkg/module/name"
|
# Transform python module notation "pkg.module.name" to file path "pkg/module/name"
|
||||||
# Thus, we should get file path relative to Django project root
|
# Thus, we should get file path relative to Django project root
|
||||||
comp_path = os.sep.join(component_module_path.split("."))
|
comp_path = os.sep.join(component_module_path.split("."))
|
||||||
|
@ -185,19 +204,19 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
self.outer_context: Context = outer_context or Context()
|
self.outer_context: Context = outer_context or Context()
|
||||||
self.fill_content = fill_content
|
self.fill_content = fill_content
|
||||||
|
|
||||||
def __init_subclass__(cls, **kwargs):
|
def __init_subclass__(cls, **kwargs: Any) -> None:
|
||||||
cls.class_hash = hash(inspect.getfile(cls) + cls.__name__)
|
cls.class_hash = hash(inspect.getfile(cls) + cls.__name__)
|
||||||
|
|
||||||
def get_context_data(self, *args, **kwargs) -> Dict[str, Any]:
|
def get_context_data(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_template_name(self, context) -> Optional[str]:
|
def get_template_name(self, context: Mapping) -> Optional[str]:
|
||||||
return self.template_name
|
return self.template_name
|
||||||
|
|
||||||
def get_template_string(self, context) -> Optional[str]:
|
def get_template_string(self, context: Mapping) -> Optional[str]:
|
||||||
return self.template
|
return self.template
|
||||||
|
|
||||||
def render_dependencies(self):
|
def render_dependencies(self) -> SafeString:
|
||||||
"""Helper function to render all dependencies for a component."""
|
"""Helper function to render all dependencies for a component."""
|
||||||
dependencies = []
|
dependencies = []
|
||||||
|
|
||||||
|
@ -211,19 +230,19 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
|
|
||||||
return mark_safe("\n".join(dependencies))
|
return mark_safe("\n".join(dependencies))
|
||||||
|
|
||||||
def render_css_dependencies(self):
|
def render_css_dependencies(self) -> SafeString:
|
||||||
"""Render only CSS dependencies available in the media class or provided as a string."""
|
"""Render only CSS dependencies available in the media class or provided as a string."""
|
||||||
if self.css is not None:
|
if self.css is not None:
|
||||||
return mark_safe(f"<style>{self.css}</style>")
|
return mark_safe(f"<style>{self.css}</style>")
|
||||||
return mark_safe("\n".join(self.media.render_css()))
|
return mark_safe("\n".join(self.media.render_css()))
|
||||||
|
|
||||||
def render_js_dependencies(self):
|
def render_js_dependencies(self) -> SafeString:
|
||||||
"""Render only JS dependencies available in the media class or provided as a string."""
|
"""Render only JS dependencies available in the media class or provided as a string."""
|
||||||
if self.js is not None:
|
if self.js is not None:
|
||||||
return mark_safe(f"<script>{self.js}</script>")
|
return mark_safe(f"<script>{self.js}</script>")
|
||||||
return mark_safe("\n".join(self.media.render_js()))
|
return mark_safe("\n".join(self.media.render_js()))
|
||||||
|
|
||||||
def get_template(self, context) -> Template:
|
def get_template(self, context: Mapping) -> Template:
|
||||||
template_string = self.get_template_string(context)
|
template_string = self.get_template_string(context)
|
||||||
if template_string is not None:
|
if template_string is not None:
|
||||||
return Template(template_string)
|
return Template(template_string)
|
||||||
|
@ -260,9 +279,9 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
context_data: Dict[str, Any],
|
context_data: Dict[str, Any],
|
||||||
slots_data: Optional[Dict[SlotName, str]] = None,
|
slots_data: Optional[Dict[SlotName, str]] = None,
|
||||||
escape_slots_content: bool = True,
|
escape_slots_content: bool = True,
|
||||||
*args,
|
*args: Any,
|
||||||
**kwargs,
|
**kwargs: Any,
|
||||||
):
|
) -> HttpResponse:
|
||||||
return HttpResponse(
|
return HttpResponse(
|
||||||
self.render(context_data, slots_data, escape_slots_content),
|
self.render(context_data, slots_data, escape_slots_content),
|
||||||
*args,
|
*args,
|
||||||
|
@ -273,7 +292,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
self,
|
self,
|
||||||
slots_data: Dict[SlotName, str],
|
slots_data: Dict[SlotName, str],
|
||||||
escape_content: bool = True,
|
escape_content: bool = True,
|
||||||
):
|
) -> None:
|
||||||
"""Fill component slots outside of template rendering."""
|
"""Fill component slots outside of template rendering."""
|
||||||
self.fill_content = [
|
self.fill_content = [
|
||||||
(
|
(
|
||||||
|
@ -342,7 +361,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
||||||
f"even though none of its slots is marked as 'default'."
|
f"even though none of its slots is marked as 'default'."
|
||||||
)
|
)
|
||||||
|
|
||||||
unfilled_slots: Set[str] = set(k for k, v in slot_name2fill_content.items() if v is None)
|
unfilled_slots: Set[str] = {k for k, v in slot_name2fill_content.items() if v is None}
|
||||||
unmatched_fills: Set[str] = named_fills_content.keys() - slot_name2fill_content.keys()
|
unmatched_fills: Set[str] = named_fills_content.keys() - slot_name2fill_content.keys()
|
||||||
|
|
||||||
# Check that 'required' slots are filled.
|
# Check that 'required' slots are filled.
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import os
|
import os
|
||||||
from textwrap import dedent
|
from textwrap import dedent
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError, CommandParser
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Creates a new component"
|
help = "Creates a new component"
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser: CommandParser) -> None:
|
||||||
parser.add_argument("name", type=str, help="The name of the component to create")
|
parser.add_argument("name", type=str, help="The name of the component to create")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--path",
|
"--path",
|
||||||
|
@ -51,7 +52,7 @@ class Command(BaseCommand):
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **kwargs):
|
def handle(self, *args: Any, **kwargs: Any) -> None:
|
||||||
name = kwargs["name"]
|
name = kwargs["name"]
|
||||||
|
|
||||||
if name:
|
if name:
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand, CommandParser
|
||||||
from django.template.engine import Engine
|
from django.template.engine import Engine
|
||||||
|
|
||||||
from django_components.template_loader import Loader
|
from django_components.template_loader import Loader
|
||||||
|
@ -12,10 +13,10 @@ from django_components.template_loader import Loader
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = "Updates component and component_block tags to the new syntax"
|
help = "Updates component and component_block tags to the new syntax"
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser: CommandParser) -> None:
|
||||||
parser.add_argument("--path", type=str, help="Path to search for components")
|
parser.add_argument("--path", type=str, help="Path to search for components")
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args: Any, **options: Any) -> None:
|
||||||
current_engine = Engine.get_default()
|
current_engine = Engine.get_default()
|
||||||
loader = Loader(current_engine)
|
loader = Loader(current_engine)
|
||||||
dirs = loader.get_dirs()
|
dirs = loader.get_dirs()
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
import re
|
import re
|
||||||
|
from collections.abc import Callable
|
||||||
|
from typing import TYPE_CHECKING, Iterable
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.forms import Media
|
from django.forms import Media
|
||||||
from django.http import StreamingHttpResponse
|
from django.http import HttpRequest, HttpResponse, StreamingHttpResponse
|
||||||
|
from django.http.response import HttpResponseBase
|
||||||
|
|
||||||
from django_components.component_registry import registry
|
from django_components.component_registry import registry
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from django_components.component import Component
|
||||||
|
|
||||||
RENDERED_COMPONENTS_CONTEXT_KEY = "_COMPONENT_DEPENDENCIES"
|
RENDERED_COMPONENTS_CONTEXT_KEY = "_COMPONENT_DEPENDENCIES"
|
||||||
CSS_DEPENDENCY_PLACEHOLDER = '<link name="CSS_PLACEHOLDER">'
|
CSS_DEPENDENCY_PLACEHOLDER = '<link name="CSS_PLACEHOLDER">'
|
||||||
JS_DEPENDENCY_PLACEHOLDER = '<script name="JS_PLACEHOLDER"></script>'
|
JS_DEPENDENCY_PLACEHOLDER = '<script name="JS_PLACEHOLDER"></script>'
|
||||||
|
@ -24,10 +30,10 @@ class ComponentDependencyMiddleware:
|
||||||
|
|
||||||
dependency_regex = COMPONENT_COMMENT_REGEX
|
dependency_regex = COMPONENT_COMMENT_REGEX
|
||||||
|
|
||||||
def __init__(self, get_response):
|
def __init__(self, get_response: "Callable[[HttpRequest], HttpResponse]") -> None:
|
||||||
self.get_response = get_response
|
self.get_response = get_response
|
||||||
|
|
||||||
def __call__(self, request):
|
def __call__(self, request: HttpRequest) -> HttpResponseBase:
|
||||||
response = self.get_response(request)
|
response = self.get_response(request)
|
||||||
if (
|
if (
|
||||||
getattr(settings, "COMPONENTS", {}).get("RENDER_DEPENDENCIES", False)
|
getattr(settings, "COMPONENTS", {}).get("RENDER_DEPENDENCIES", False)
|
||||||
|
@ -38,7 +44,7 @@ class ComponentDependencyMiddleware:
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
def process_response_content(content):
|
def process_response_content(content: bytes) -> bytes:
|
||||||
component_names_seen = {match.group("name") for match in COMPONENT_COMMENT_REGEX.finditer(content)}
|
component_names_seen = {match.group("name") for match in COMPONENT_COMMENT_REGEX.finditer(content)}
|
||||||
all_components = [registry.get(name.decode("utf-8"))("") for name in component_names_seen]
|
all_components = [registry.get(name.decode("utf-8"))("") for name in component_names_seen]
|
||||||
all_media = join_media(all_components)
|
all_media = join_media(all_components)
|
||||||
|
@ -47,7 +53,7 @@ def process_response_content(content):
|
||||||
return PLACEHOLDER_REGEX.sub(DependencyReplacer(css_dependencies, js_dependencies), content)
|
return PLACEHOLDER_REGEX.sub(DependencyReplacer(css_dependencies, js_dependencies), content)
|
||||||
|
|
||||||
|
|
||||||
def add_module_attribute_to_scripts(scripts):
|
def add_module_attribute_to_scripts(scripts: str) -> str:
|
||||||
return re.sub(SCRIPT_TAG_REGEX, '<script type="module"', scripts)
|
return re.sub(SCRIPT_TAG_REGEX, '<script type="module"', scripts)
|
||||||
|
|
||||||
|
|
||||||
|
@ -58,11 +64,11 @@ class DependencyReplacer:
|
||||||
CSS_PLACEHOLDER = bytes(CSS_DEPENDENCY_PLACEHOLDER, encoding="utf-8")
|
CSS_PLACEHOLDER = bytes(CSS_DEPENDENCY_PLACEHOLDER, encoding="utf-8")
|
||||||
JS_PLACEHOLDER = bytes(JS_DEPENDENCY_PLACEHOLDER, encoding="utf-8")
|
JS_PLACEHOLDER = bytes(JS_DEPENDENCY_PLACEHOLDER, encoding="utf-8")
|
||||||
|
|
||||||
def __init__(self, css_string, js_string):
|
def __init__(self, css_string: bytes, js_string: bytes) -> None:
|
||||||
self.js_string = js_string
|
self.js_string = js_string
|
||||||
self.css_string = css_string
|
self.css_string = css_string
|
||||||
|
|
||||||
def __call__(self, match):
|
def __call__(self, match: "re.Match[bytes]") -> bytes:
|
||||||
if match[0] == self.CSS_PLACEHOLDER:
|
if match[0] == self.CSS_PLACEHOLDER:
|
||||||
replacement, self.css_string = self.css_string, b""
|
replacement, self.css_string = self.css_string, b""
|
||||||
elif match[0] == self.JS_PLACEHOLDER:
|
elif match[0] == self.JS_PLACEHOLDER:
|
||||||
|
@ -72,7 +78,7 @@ class DependencyReplacer:
|
||||||
return replacement
|
return replacement
|
||||||
|
|
||||||
|
|
||||||
def join_media(components):
|
def join_media(components: Iterable["Component"]) -> Media:
|
||||||
"""Return combined media object for iterable of components."""
|
"""Return combined media object for iterable of components."""
|
||||||
|
|
||||||
return sum([component.media for component in components], Media())
|
return sum([component.media for component in components], Media())
|
||||||
|
|
|
@ -4,7 +4,7 @@ Template loader that loads templates from each Django app's "components" directo
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Set
|
from typing import List, Set
|
||||||
|
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
@ -15,7 +15,7 @@ from django_components.logger import logger
|
||||||
|
|
||||||
# Same as `Path.is_relative_to`, defined as standalone function because `Path.is_relative_to`
|
# Same as `Path.is_relative_to`, defined as standalone function because `Path.is_relative_to`
|
||||||
# is marked for deprecation.
|
# is marked for deprecation.
|
||||||
def path_is_relative_to(child_path: str, parent_path: str):
|
def path_is_relative_to(child_path: str, parent_path: str) -> bool:
|
||||||
# If the relative path doesn't start with `..`, then child is descendant of parent
|
# If the relative path doesn't start with `..`, then child is descendant of parent
|
||||||
# See https://stackoverflow.com/a/7288073/9788634
|
# See https://stackoverflow.com/a/7288073/9788634
|
||||||
rel_path = os.path.relpath(child_path, parent_path)
|
rel_path = os.path.relpath(child_path, parent_path)
|
||||||
|
@ -23,7 +23,7 @@ def path_is_relative_to(child_path: str, parent_path: str):
|
||||||
|
|
||||||
|
|
||||||
class Loader(FilesystemLoader):
|
class Loader(FilesystemLoader):
|
||||||
def get_dirs(self):
|
def get_dirs(self) -> List[Path]:
|
||||||
# Allow to configure from settings which dirs should be checked for components
|
# Allow to configure from settings which dirs should be checked for components
|
||||||
if hasattr(settings, "STATICFILES_DIRS") and len(settings.STATICFILES_DIRS):
|
if hasattr(settings, "STATICFILES_DIRS") and len(settings.STATICFILES_DIRS):
|
||||||
component_dirs = settings.STATICFILES_DIRS
|
component_dirs = settings.STATICFILES_DIRS
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import sys
|
import sys
|
||||||
from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type, Union
|
from typing import TYPE_CHECKING, Any, Iterable, List, Mapping, Optional, Set, Tuple, Type, Union
|
||||||
|
|
||||||
if sys.version_info[:2] < (3, 9):
|
if sys.version_info[:2] < (3, 9):
|
||||||
from typing import ChainMap
|
from typing import ChainMap
|
||||||
|
@ -9,11 +9,11 @@ else:
|
||||||
import django.template
|
import django.template
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.template import Context, Template
|
from django.template import Context, Template
|
||||||
from django.template.base import FilterExpression, Node, NodeList, TextNode, TokenType
|
from django.template.base import FilterExpression, Node, NodeList, Parser, TextNode, Token, TokenType
|
||||||
from django.template.defaulttags import CommentNode
|
from django.template.defaulttags import CommentNode
|
||||||
from django.template.exceptions import TemplateSyntaxError
|
from django.template.exceptions import TemplateSyntaxError
|
||||||
from django.template.library import parse_bits
|
from django.template.library import parse_bits
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import SafeString, mark_safe
|
||||||
|
|
||||||
from django_components.app_settings import app_settings
|
from django_components.app_settings import app_settings
|
||||||
from django_components.component_registry import ComponentRegistry
|
from django_components.component_registry import ComponentRegistry
|
||||||
|
@ -46,7 +46,7 @@ FillContent = Tuple[NodeList, Optional[AliasName]]
|
||||||
FilledSlotsContext = ChainMap[Tuple[SlotName, Template], FillContent]
|
FilledSlotsContext = ChainMap[Tuple[SlotName, Template], FillContent]
|
||||||
|
|
||||||
|
|
||||||
def get_components_from_registry(registry: ComponentRegistry):
|
def get_components_from_registry(registry: ComponentRegistry) -> List["Component"]:
|
||||||
"""Returns a list unique components from the registry."""
|
"""Returns a list unique components from the registry."""
|
||||||
|
|
||||||
unique_component_classes = set(registry.all().values())
|
unique_component_classes = set(registry.all().values())
|
||||||
|
@ -58,7 +58,7 @@ def get_components_from_registry(registry: ComponentRegistry):
|
||||||
return components
|
return components
|
||||||
|
|
||||||
|
|
||||||
def get_components_from_preload_str(preload_str):
|
def get_components_from_preload_str(preload_str: str) -> List["Component"]:
|
||||||
"""Returns a list of unique components from a comma-separated str"""
|
"""Returns a list of unique components from a comma-separated str"""
|
||||||
|
|
||||||
components = []
|
components = []
|
||||||
|
@ -73,7 +73,7 @@ def get_components_from_preload_str(preload_str):
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(name="component_dependencies")
|
@register.simple_tag(name="component_dependencies")
|
||||||
def component_dependencies_tag(preload=""):
|
def component_dependencies_tag(preload: str = "") -> SafeString:
|
||||||
"""Marks location where CSS link and JS script tags should be rendered."""
|
"""Marks location where CSS link and JS script tags should be rendered."""
|
||||||
|
|
||||||
if is_dependency_middleware_active():
|
if is_dependency_middleware_active():
|
||||||
|
@ -90,7 +90,7 @@ def component_dependencies_tag(preload=""):
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(name="component_css_dependencies")
|
@register.simple_tag(name="component_css_dependencies")
|
||||||
def component_css_dependencies_tag(preload=""):
|
def component_css_dependencies_tag(preload: str = "") -> SafeString:
|
||||||
"""Marks location where CSS link tags should be rendered."""
|
"""Marks location where CSS link tags should be rendered."""
|
||||||
|
|
||||||
if is_dependency_middleware_active():
|
if is_dependency_middleware_active():
|
||||||
|
@ -107,7 +107,7 @@ def component_css_dependencies_tag(preload=""):
|
||||||
|
|
||||||
|
|
||||||
@register.simple_tag(name="component_js_dependencies")
|
@register.simple_tag(name="component_js_dependencies")
|
||||||
def component_js_dependencies_tag(preload=""):
|
def component_js_dependencies_tag(preload: str = "") -> SafeString:
|
||||||
"""Marks location where JS script tags should be rendered."""
|
"""Marks location where JS script tags should be rendered."""
|
||||||
|
|
||||||
if is_dependency_middleware_active():
|
if is_dependency_middleware_active():
|
||||||
|
@ -157,7 +157,7 @@ class TemplateAwareNodeMixin:
|
||||||
)
|
)
|
||||||
|
|
||||||
@template.setter
|
@template.setter
|
||||||
def template(self, value) -> None:
|
def template(self, value: Template) -> None:
|
||||||
self._template = value
|
self._template = value
|
||||||
|
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@ class SlotNode(Node, TemplateAwareNodeMixin):
|
||||||
self.is_default = is_default
|
self.is_default = is_default
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_flags(self):
|
def active_flags(self) -> List[str]:
|
||||||
m = []
|
m = []
|
||||||
if self.is_required:
|
if self.is_required:
|
||||||
m.append("required")
|
m.append("required")
|
||||||
|
@ -183,10 +183,10 @@ class SlotNode(Node, TemplateAwareNodeMixin):
|
||||||
m.append("default")
|
m.append("default")
|
||||||
return m
|
return m
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f"<Slot Node: {self.name}. Contents: {repr(self.nodelist)}. Options: {self.active_flags}>"
|
return f"<Slot Node: {self.name}. Contents: {repr(self.nodelist)}. Options: {self.active_flags}>"
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context: Context) -> SafeString:
|
||||||
try:
|
try:
|
||||||
filled_slots_map: FilledSlotsContext = context[FILLED_SLOTS_CONTENT_CONTEXT_KEY]
|
filled_slots_map: FilledSlotsContext = context[FILLED_SLOTS_CONTENT_CONTEXT_KEY]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -213,7 +213,7 @@ class SlotNode(Node, TemplateAwareNodeMixin):
|
||||||
|
|
||||||
|
|
||||||
@register.tag("slot")
|
@register.tag("slot")
|
||||||
def do_slot(parser, token):
|
def do_slot(parser: Parser, token: Token) -> SlotNode:
|
||||||
bits = token.split_contents()
|
bits = token.split_contents()
|
||||||
args = bits[1:]
|
args = bits[1:]
|
||||||
# e.g. {% slot <name> %}
|
# e.g. {% slot <name> %}
|
||||||
|
@ -258,10 +258,10 @@ class BaseFillNode(Node):
|
||||||
def __init__(self, nodelist: NodeList):
|
def __init__(self, nodelist: NodeList):
|
||||||
self.nodelist: NodeList = nodelist
|
self.nodelist: NodeList = nodelist
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context: Context) -> str:
|
||||||
raise TemplateSyntaxError(
|
raise TemplateSyntaxError(
|
||||||
"{% fill ... %} block cannot be rendered directly. "
|
"{% fill ... %} block cannot be rendered directly. "
|
||||||
"You are probably seeing this because you have used one outside "
|
"You are probably seeing this because you have used one outside "
|
||||||
|
@ -280,7 +280,7 @@ class NamedFillNode(BaseFillNode):
|
||||||
self.name_fexp = name_fexp
|
self.name_fexp = name_fexp
|
||||||
self.alias_fexp = alias_fexp
|
self.alias_fexp = alias_fexp
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f"<{type(self)} Name: {self.name_fexp}. Contents: {repr(self.nodelist)}.>"
|
return f"<{type(self)} Name: {self.name_fexp}. Contents: {repr(self.nodelist)}.>"
|
||||||
|
|
||||||
|
|
||||||
|
@ -291,12 +291,12 @@ class ImplicitFillNode(BaseFillNode):
|
||||||
as 'default'.
|
as 'default'.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f"<{type(self)} Contents: {repr(self.nodelist)}.>"
|
return f"<{type(self)} Contents: {repr(self.nodelist)}.>"
|
||||||
|
|
||||||
|
|
||||||
@register.tag("fill")
|
@register.tag("fill")
|
||||||
def do_fill(parser, token):
|
def do_fill(parser: Parser, token: Token) -> NamedFillNode:
|
||||||
"""Block tag whose contents 'fill' (are inserted into) an identically named
|
"""Block tag whose contents 'fill' (are inserted into) an identically named
|
||||||
'slot'-block in the component template referred to by a parent component.
|
'slot'-block in the component template referred to by a parent component.
|
||||||
It exists to make component nesting easier.
|
It exists to make component nesting easier.
|
||||||
|
@ -333,11 +333,11 @@ class ComponentNode(Node):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name_fexp: FilterExpression,
|
name_fexp: FilterExpression,
|
||||||
context_args,
|
context_args: List[FilterExpression],
|
||||||
context_kwargs,
|
context_kwargs: Mapping[str, FilterExpression],
|
||||||
isolated_context=False,
|
isolated_context: bool = False,
|
||||||
fill_nodes: Union[ImplicitFillNode, Iterable[NamedFillNode]] = (),
|
fill_nodes: Union[ImplicitFillNode, Iterable[NamedFillNode]] = (),
|
||||||
):
|
) -> None:
|
||||||
self.name_fexp = name_fexp
|
self.name_fexp = name_fexp
|
||||||
self.context_args = context_args or []
|
self.context_args = context_args or []
|
||||||
self.context_kwargs = context_kwargs or {}
|
self.context_kwargs = context_kwargs or {}
|
||||||
|
@ -345,19 +345,19 @@ class ComponentNode(Node):
|
||||||
self.fill_nodes = fill_nodes
|
self.fill_nodes = fill_nodes
|
||||||
self.nodelist = self._create_nodelist(fill_nodes)
|
self.nodelist = self._create_nodelist(fill_nodes)
|
||||||
|
|
||||||
def _create_nodelist(self, fill_nodes) -> NodeList:
|
def _create_nodelist(self, fill_nodes: Union[Iterable[Node], ImplicitFillNode]) -> NodeList:
|
||||||
if isinstance(fill_nodes, ImplicitFillNode):
|
if isinstance(fill_nodes, ImplicitFillNode):
|
||||||
return NodeList([fill_nodes])
|
return NodeList([fill_nodes])
|
||||||
else:
|
else:
|
||||||
return NodeList(fill_nodes)
|
return NodeList(fill_nodes)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return "<ComponentNode: %s. Contents: %r>" % (
|
return "<ComponentNode: {}. Contents: {!r}>".format(
|
||||||
self.name_fexp,
|
self.name_fexp,
|
||||||
getattr(self, "nodelist", None), # 'nodelist' attribute only assigned later.
|
getattr(self, "nodelist", None), # 'nodelist' attribute only assigned later.
|
||||||
)
|
)
|
||||||
|
|
||||||
def render(self, context: Context):
|
def render(self, context: Context) -> str:
|
||||||
resolved_component_name = self.name_fexp.resolve(context)
|
resolved_component_name = self.name_fexp.resolve(context)
|
||||||
component_cls: Type[Component] = component_registry.get(resolved_component_name)
|
component_cls: Type[Component] = component_registry.get(resolved_component_name)
|
||||||
|
|
||||||
|
@ -408,7 +408,7 @@ class ComponentNode(Node):
|
||||||
|
|
||||||
|
|
||||||
@register.tag(name="component")
|
@register.tag(name="component")
|
||||||
def do_component(parser, token):
|
def do_component(parser: Parser, token: Token) -> ComponentNode:
|
||||||
"""
|
"""
|
||||||
To give the component access to the template context:
|
To give the component access to the template context:
|
||||||
{% component "name" positional_arg keyword_arg=value ... %}
|
{% component "name" positional_arg keyword_arg=value ... %}
|
||||||
|
@ -497,7 +497,7 @@ def try_parse_as_default_fill(
|
||||||
return ImplicitFillNode(nodelist=nodelist)
|
return ImplicitFillNode(nodelist=nodelist)
|
||||||
|
|
||||||
|
|
||||||
def block_has_content(nodelist) -> bool:
|
def block_has_content(nodelist: NodeList) -> bool:
|
||||||
for node in nodelist:
|
for node in nodelist:
|
||||||
if isinstance(node, TextNode) and node.s.isspace():
|
if isinstance(node, TextNode) and node.s.isspace():
|
||||||
pass
|
pass
|
||||||
|
@ -512,16 +512,16 @@ def is_whitespace_node(node: Node) -> bool:
|
||||||
return isinstance(node, TextNode) and node.s.isspace()
|
return isinstance(node, TextNode) and node.s.isspace()
|
||||||
|
|
||||||
|
|
||||||
def is_whitespace_token(token):
|
def is_whitespace_token(token: Token) -> bool:
|
||||||
return token.token_type == TokenType.TEXT and not token.contents.strip()
|
return token.token_type == TokenType.TEXT and not token.contents.strip()
|
||||||
|
|
||||||
|
|
||||||
def is_block_tag_token(token, name):
|
def is_block_tag_token(token: Token, name: str) -> bool:
|
||||||
return token.token_type == TokenType.BLOCK and token.split_contents()[0] == name
|
return token.token_type == TokenType.BLOCK and token.split_contents()[0] == name
|
||||||
|
|
||||||
|
|
||||||
@register.tag(name="if_filled")
|
@register.tag(name="if_filled")
|
||||||
def do_if_filled_block(parser, token):
|
def do_if_filled_block(parser: Parser, token: Token) -> "IfSlotFilledNode":
|
||||||
"""
|
"""
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
|
@ -623,7 +623,7 @@ class _IfSlotFilledBranchNode(Node):
|
||||||
def render(self, context: Context) -> str:
|
def render(self, context: Context) -> str:
|
||||||
return self.nodelist.render(context)
|
return self.nodelist.render(context)
|
||||||
|
|
||||||
def evaluate(self, context) -> bool:
|
def evaluate(self, context: Context) -> bool:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
@ -632,13 +632,13 @@ class IfSlotFilledConditionBranchNode(_IfSlotFilledBranchNode, TemplateAwareNode
|
||||||
self,
|
self,
|
||||||
slot_name: str,
|
slot_name: str,
|
||||||
nodelist: NodeList,
|
nodelist: NodeList,
|
||||||
is_positive=True,
|
is_positive: Union[bool, None] = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.slot_name = slot_name
|
self.slot_name = slot_name
|
||||||
self.is_positive: bool = is_positive
|
self.is_positive: Optional[bool] = is_positive
|
||||||
super().__init__(nodelist)
|
super().__init__(nodelist)
|
||||||
|
|
||||||
def evaluate(self, context) -> bool:
|
def evaluate(self, context: Context) -> bool:
|
||||||
try:
|
try:
|
||||||
filled_slots: FilledSlotsContext = context[FILLED_SLOTS_CONTENT_CONTEXT_KEY]
|
filled_slots: FilledSlotsContext = context[FILLED_SLOTS_CONTENT_CONTEXT_KEY]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -654,7 +654,7 @@ class IfSlotFilledConditionBranchNode(_IfSlotFilledBranchNode, TemplateAwareNode
|
||||||
|
|
||||||
|
|
||||||
class IfSlotFilledElseBranchNode(_IfSlotFilledBranchNode):
|
class IfSlotFilledElseBranchNode(_IfSlotFilledBranchNode):
|
||||||
def evaluate(self, context) -> bool:
|
def evaluate(self, context: Context) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -666,13 +666,13 @@ class IfSlotFilledNode(Node):
|
||||||
self.branches = branches
|
self.branches = branches
|
||||||
self.nodelist = self._create_nodelist(branches)
|
self.nodelist = self._create_nodelist(branches)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f"<{self.__class__.__name__}>"
|
return f"<{self.__class__.__name__}>"
|
||||||
|
|
||||||
def _create_nodelist(self, branches) -> NodeList:
|
def _create_nodelist(self, branches: List[_IfSlotFilledBranchNode]) -> NodeList:
|
||||||
return NodeList(branches)
|
return NodeList(branches)
|
||||||
|
|
||||||
def render(self, context):
|
def render(self, context: Context) -> str:
|
||||||
for node in self.branches:
|
for node in self.branches:
|
||||||
if isinstance(node, IfSlotFilledElseBranchNode):
|
if isinstance(node, IfSlotFilledElseBranchNode):
|
||||||
return node.render(context)
|
return node.render(context)
|
||||||
|
@ -682,7 +682,7 @@ class IfSlotFilledNode(Node):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def check_for_isolated_context_keyword(bits):
|
def check_for_isolated_context_keyword(bits: List[str]) -> Tuple[List[str], bool]:
|
||||||
"""Return True and strip the last word if token ends with 'only' keyword or if CONTEXT_BEHAVIOR is 'isolated'."""
|
"""Return True and strip the last word if token ends with 'only' keyword or if CONTEXT_BEHAVIOR is 'isolated'."""
|
||||||
|
|
||||||
if bits[-1] == "only":
|
if bits[-1] == "only":
|
||||||
|
@ -694,7 +694,9 @@ def check_for_isolated_context_keyword(bits):
|
||||||
return bits, False
|
return bits, False
|
||||||
|
|
||||||
|
|
||||||
def parse_component_with_args(parser, bits, tag_name):
|
def parse_component_with_args(
|
||||||
|
parser: Parser, bits: List[str], tag_name: str
|
||||||
|
) -> Tuple[str, List[FilterExpression], Mapping[str, FilterExpression]]:
|
||||||
tag_args, tag_kwargs = parse_bits(
|
tag_args, tag_kwargs = parse_bits(
|
||||||
parser=parser,
|
parser=parser,
|
||||||
bits=bits,
|
bits=bits,
|
||||||
|
@ -726,21 +728,21 @@ def parse_component_with_args(parser, bits, tag_name):
|
||||||
return component_name, context_args, context_kwargs
|
return component_name, context_args, context_kwargs
|
||||||
|
|
||||||
|
|
||||||
def safe_resolve(context_item, context):
|
def safe_resolve(context_item: FilterExpression, context: Context) -> Any:
|
||||||
"""Resolve FilterExpressions and Variables in context if possible. Return other items unchanged."""
|
"""Resolve FilterExpressions and Variables in context if possible. Return other items unchanged."""
|
||||||
|
|
||||||
return context_item.resolve(context) if hasattr(context_item, "resolve") else context_item
|
return context_item.resolve(context) if hasattr(context_item, "resolve") else context_item
|
||||||
|
|
||||||
|
|
||||||
def is_wrapped_in_quotes(s):
|
def is_wrapped_in_quotes(s: str) -> bool:
|
||||||
return s.startswith(('"', "'")) and s[0] == s[-1]
|
return s.startswith(('"', "'")) and s[0] == s[-1]
|
||||||
|
|
||||||
|
|
||||||
def is_dependency_middleware_active():
|
def is_dependency_middleware_active() -> bool:
|
||||||
return getattr(settings, "COMPONENTS", {}).get("RENDER_DEPENDENCIES", False)
|
return getattr(settings, "COMPONENTS", {}).get("RENDER_DEPENDENCIES", False)
|
||||||
|
|
||||||
|
|
||||||
def norm_and_validate_name(name: str, tag: str, context: Optional[str] = None):
|
def norm_and_validate_name(name: str, tag: str, context: Optional[str] = None) -> str:
|
||||||
"""
|
"""
|
||||||
Notes:
|
Notes:
|
||||||
- Value of `tag` in {"slot", "fill", "alias"}
|
- Value of `tag` in {"slot", "fill", "alias"}
|
||||||
|
@ -756,7 +758,7 @@ def strip_quotes(s: str) -> str:
|
||||||
return s.strip("\"'")
|
return s.strip("\"'")
|
||||||
|
|
||||||
|
|
||||||
def bool_from_string(s: str):
|
def bool_from_string(s: str) -> bool:
|
||||||
s = strip_quotes(s.lower())
|
s = strip_quotes(s.lower())
|
||||||
if s == "true":
|
if s == "true":
|
||||||
return True
|
return True
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
|
import typing
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from typing import Annotated # type: ignore
|
from typing import Annotated # type: ignore
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
||||||
|
@typing.no_type_check
|
||||||
class Annotated: # type: ignore
|
class Annotated: # type: ignore
|
||||||
def __init__(self, type_, *args, **kwargs):
|
def __init__(self, type_: str, *args: Any, **kwargs: Any):
|
||||||
self.type_ = type_
|
self.type_ = type_
|
||||||
self.metadata = args, kwargs
|
self.metadata = args, kwargs
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self) -> str:
|
||||||
return f"Annotated[{self.type_}, {self.metadata[0]!r}, {self.metadata[1]!r}]"
|
return f"Annotated[{self.type_}, {self.metadata[0]!r}, {self.metadata[1]!r}]"
|
||||||
|
|
||||||
def __getitem__(self, params):
|
def __getitem__(self, params: Any) -> "Annotated": # type: ignore
|
||||||
if not isinstance(params, tuple):
|
if not isinstance(params, tuple):
|
||||||
params = (params,)
|
params = (params,)
|
||||||
return Annotated(self.type_, *params, **self.metadata[1]) # type: ignore
|
return Annotated(self.type_, *params, **self.metadata[1]) # type: ignore
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import glob
|
import glob
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Union
|
||||||
|
|
||||||
from django.template.engine import Engine
|
from django.template.engine import Engine
|
||||||
|
|
||||||
from django_components.template_loader import Loader
|
from django_components.template_loader import Loader
|
||||||
|
|
||||||
|
|
||||||
def search(search_glob: Optional[str] = None, engine: Optional[Engine] = None):
|
def search(search_glob: Optional[str] = None, engine: Optional[Engine] = None) -> Union[List[str], List[Path]]:
|
||||||
"""
|
"""
|
||||||
Search for directories that may contain components.
|
Search for directories that may contain components.
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue