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',
|
||||
]
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = "django_components.*"
|
||||
disallow_untyped_defs = true
|
||||
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = [
|
||||
"tests"
|
||||
|
|
|
@ -2,6 +2,7 @@ import importlib
|
|||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
import django
|
||||
from django.utils.module_loading import autodiscover_modules
|
||||
|
@ -12,7 +13,7 @@ if django.VERSION < (3, 2):
|
|||
default_app_config = "django_components.apps.ComponentsConfig"
|
||||
|
||||
|
||||
def autodiscover():
|
||||
def autodiscover() -> None:
|
||||
from django_components.app_settings import app_settings
|
||||
|
||||
if app_settings.AUTODISCOVER:
|
||||
|
@ -24,11 +25,11 @@ def autodiscover():
|
|||
for path in component_filepaths:
|
||||
import_file(path)
|
||||
|
||||
for path in app_settings.LIBRARIES:
|
||||
importlib.import_module(path)
|
||||
for path_lib in app_settings.LIBRARIES:
|
||||
importlib.import_module(path_lib)
|
||||
|
||||
|
||||
def import_file(path):
|
||||
def import_file(path: Union[str, Path]) -> None:
|
||||
MODULE_PATH = path
|
||||
MODULE_NAME = Path(path).stem
|
||||
spec = importlib.util.spec_from_file_location(MODULE_NAME, MODULE_PATH)
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
@ -9,27 +10,27 @@ class ContextBehavior(Enum):
|
|||
|
||||
|
||||
class AppSettings:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
self.settings = getattr(settings, "COMPONENTS", {})
|
||||
|
||||
@property
|
||||
def AUTODISCOVER(self):
|
||||
def AUTODISCOVER(self) -> bool:
|
||||
return self.settings.setdefault("autodiscover", True)
|
||||
|
||||
@property
|
||||
def LIBRARIES(self):
|
||||
def LIBRARIES(self) -> List:
|
||||
return self.settings.setdefault("libraries", [])
|
||||
|
||||
@property
|
||||
def TEMPLATE_CACHE_SIZE(self):
|
||||
def TEMPLATE_CACHE_SIZE(self) -> int:
|
||||
return self.settings.setdefault("template_cache_size", 128)
|
||||
|
||||
@property
|
||||
def CONTEXT_BEHAVIOR(self):
|
||||
def CONTEXT_BEHAVIOR(self) -> ContextBehavior:
|
||||
raw_value = self.settings.setdefault("context_behavior", ContextBehavior.GLOBAL.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:
|
||||
return ContextBehavior(raw_value)
|
||||
except ValueError:
|
||||
|
|
|
@ -4,5 +4,5 @@ from django.apps import AppConfig
|
|||
class ComponentsConfig(AppConfig):
|
||||
name = "django_components"
|
||||
|
||||
def ready(self):
|
||||
def ready(self) -> None:
|
||||
self.module.autodiscover()
|
||||
|
|
|
@ -2,7 +2,22 @@ import difflib
|
|||
import inspect
|
||||
import os
|
||||
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.forms.widgets import Media, MediaDefiningClass
|
||||
|
@ -12,7 +27,7 @@ from django.template.context import Context
|
|||
from django.template.exceptions import TemplateSyntaxError
|
||||
from django.template.loader import get_template
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
_T = TypeVar("_T")
|
||||
|
||||
|
||||
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:
|
||||
media: Component.Media = attrs["Media"]
|
||||
|
||||
|
@ -68,7 +85,7 @@ class SimplifiedInterfaceMediaDefiningClass(MediaDefiningClass):
|
|||
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
|
||||
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.
|
||||
# If yes, modify the path to refer to the relative file.
|
||||
# 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)
|
||||
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]
|
||||
|
||||
|
||||
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"
|
||||
# Thus, we should get file path relative to Django project root
|
||||
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.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__)
|
||||
|
||||
def get_context_data(self, *args, **kwargs) -> Dict[str, Any]:
|
||||
def get_context_data(self, *args: Any, **kwargs: Any) -> Dict[str, Any]:
|
||||
return {}
|
||||
|
||||
def get_template_name(self, context) -> Optional[str]:
|
||||
def get_template_name(self, context: Mapping) -> Optional[str]:
|
||||
return self.template_name
|
||||
|
||||
def get_template_string(self, context) -> Optional[str]:
|
||||
def get_template_string(self, context: Mapping) -> Optional[str]:
|
||||
return self.template
|
||||
|
||||
def render_dependencies(self):
|
||||
def render_dependencies(self) -> SafeString:
|
||||
"""Helper function to render all dependencies for a component."""
|
||||
dependencies = []
|
||||
|
||||
|
@ -211,19 +230,19 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
|||
|
||||
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."""
|
||||
if self.css is not None:
|
||||
return mark_safe(f"<style>{self.css}</style>")
|
||||
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."""
|
||||
if self.js is not None:
|
||||
return mark_safe(f"<script>{self.js}</script>")
|
||||
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)
|
||||
if template_string is not None:
|
||||
return Template(template_string)
|
||||
|
@ -260,9 +279,9 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
|||
context_data: Dict[str, Any],
|
||||
slots_data: Optional[Dict[SlotName, str]] = None,
|
||||
escape_slots_content: bool = True,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> HttpResponse:
|
||||
return HttpResponse(
|
||||
self.render(context_data, slots_data, escape_slots_content),
|
||||
*args,
|
||||
|
@ -273,7 +292,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
|||
self,
|
||||
slots_data: Dict[SlotName, str],
|
||||
escape_content: bool = True,
|
||||
):
|
||||
) -> None:
|
||||
"""Fill component slots outside of template rendering."""
|
||||
self.fill_content = [
|
||||
(
|
||||
|
@ -342,7 +361,7 @@ class Component(View, metaclass=SimplifiedInterfaceMediaDefiningClass):
|
|||
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()
|
||||
|
||||
# Check that 'required' slots are filled.
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import os
|
||||
from textwrap import dedent
|
||||
from typing import Any
|
||||
|
||||
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):
|
||||
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(
|
||||
"--path",
|
||||
|
@ -51,7 +52,7 @@ class Command(BaseCommand):
|
|||
default=False,
|
||||
)
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
def handle(self, *args: Any, **kwargs: Any) -> None:
|
||||
name = kwargs["name"]
|
||||
|
||||
if name:
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
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_components.template_loader import Loader
|
||||
|
@ -12,10 +13,10 @@ from django_components.template_loader import Loader
|
|||
class Command(BaseCommand):
|
||||
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")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
def handle(self, *args: Any, **options: Any) -> None:
|
||||
current_engine = Engine.get_default()
|
||||
loader = Loader(current_engine)
|
||||
dirs = loader.get_dirs()
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import re
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, Iterable
|
||||
|
||||
from django.conf import settings
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from django_components.component import Component
|
||||
|
||||
RENDERED_COMPONENTS_CONTEXT_KEY = "_COMPONENT_DEPENDENCIES"
|
||||
CSS_DEPENDENCY_PLACEHOLDER = '<link name="CSS_PLACEHOLDER">'
|
||||
JS_DEPENDENCY_PLACEHOLDER = '<script name="JS_PLACEHOLDER"></script>'
|
||||
|
@ -24,10 +30,10 @@ class ComponentDependencyMiddleware:
|
|||
|
||||
dependency_regex = COMPONENT_COMMENT_REGEX
|
||||
|
||||
def __init__(self, get_response):
|
||||
def __init__(self, get_response: "Callable[[HttpRequest], HttpResponse]") -> None:
|
||||
self.get_response = get_response
|
||||
|
||||
def __call__(self, request):
|
||||
def __call__(self, request: HttpRequest) -> HttpResponseBase:
|
||||
response = self.get_response(request)
|
||||
if (
|
||||
getattr(settings, "COMPONENTS", {}).get("RENDER_DEPENDENCIES", False)
|
||||
|
@ -38,7 +44,7 @@ class ComponentDependencyMiddleware:
|
|||
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)}
|
||||
all_components = [registry.get(name.decode("utf-8"))("") for name in component_names_seen]
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
@ -58,11 +64,11 @@ class DependencyReplacer:
|
|||
CSS_PLACEHOLDER = bytes(CSS_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.css_string = css_string
|
||||
|
||||
def __call__(self, match):
|
||||
def __call__(self, match: "re.Match[bytes]") -> bytes:
|
||||
if match[0] == self.CSS_PLACEHOLDER:
|
||||
replacement, self.css_string = self.css_string, b""
|
||||
elif match[0] == self.JS_PLACEHOLDER:
|
||||
|
@ -72,7 +78,7 @@ class DependencyReplacer:
|
|||
return replacement
|
||||
|
||||
|
||||
def join_media(components):
|
||||
def join_media(components: Iterable["Component"]) -> Media:
|
||||
"""Return combined media object for iterable of components."""
|
||||
|
||||
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
|
||||
from pathlib import Path
|
||||
from typing import Set
|
||||
from typing import List, Set
|
||||
|
||||
from django.apps import apps
|
||||
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`
|
||||
# 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
|
||||
# See https://stackoverflow.com/a/7288073/9788634
|
||||
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):
|
||||
def get_dirs(self):
|
||||
def get_dirs(self) -> List[Path]:
|
||||
# Allow to configure from settings which dirs should be checked for components
|
||||
if hasattr(settings, "STATICFILES_DIRS") and len(settings.STATICFILES_DIRS):
|
||||
component_dirs = settings.STATICFILES_DIRS
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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):
|
||||
from typing import ChainMap
|
||||
|
@ -9,11 +9,11 @@ else:
|
|||
import django.template
|
||||
from django.conf import settings
|
||||
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.exceptions import TemplateSyntaxError
|
||||
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.component_registry import ComponentRegistry
|
||||
|
@ -46,7 +46,7 @@ FillContent = Tuple[NodeList, Optional[AliasName]]
|
|||
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."""
|
||||
|
||||
unique_component_classes = set(registry.all().values())
|
||||
|
@ -58,7 +58,7 @@ def get_components_from_registry(registry: ComponentRegistry):
|
|||
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"""
|
||||
|
||||
components = []
|
||||
|
@ -73,7 +73,7 @@ def get_components_from_preload_str(preload_str):
|
|||
|
||||
|
||||
@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."""
|
||||
|
||||
if is_dependency_middleware_active():
|
||||
|
@ -90,7 +90,7 @@ def component_dependencies_tag(preload=""):
|
|||
|
||||
|
||||
@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."""
|
||||
|
||||
if is_dependency_middleware_active():
|
||||
|
@ -107,7 +107,7 @@ def component_css_dependencies_tag(preload=""):
|
|||
|
||||
|
||||
@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."""
|
||||
|
||||
if is_dependency_middleware_active():
|
||||
|
@ -157,7 +157,7 @@ class TemplateAwareNodeMixin:
|
|||
)
|
||||
|
||||
@template.setter
|
||||
def template(self, value) -> None:
|
||||
def template(self, value: Template) -> None:
|
||||
self._template = value
|
||||
|
||||
|
||||
|
@ -175,7 +175,7 @@ class SlotNode(Node, TemplateAwareNodeMixin):
|
|||
self.is_default = is_default
|
||||
|
||||
@property
|
||||
def active_flags(self):
|
||||
def active_flags(self) -> List[str]:
|
||||
m = []
|
||||
if self.is_required:
|
||||
m.append("required")
|
||||
|
@ -183,10 +183,10 @@ class SlotNode(Node, TemplateAwareNodeMixin):
|
|||
m.append("default")
|
||||
return m
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
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:
|
||||
filled_slots_map: FilledSlotsContext = context[FILLED_SLOTS_CONTENT_CONTEXT_KEY]
|
||||
except KeyError:
|
||||
|
@ -213,7 +213,7 @@ class SlotNode(Node, TemplateAwareNodeMixin):
|
|||
|
||||
|
||||
@register.tag("slot")
|
||||
def do_slot(parser, token):
|
||||
def do_slot(parser: Parser, token: Token) -> SlotNode:
|
||||
bits = token.split_contents()
|
||||
args = bits[1:]
|
||||
# e.g. {% slot <name> %}
|
||||
|
@ -258,10 +258,10 @@ class BaseFillNode(Node):
|
|||
def __init__(self, nodelist: NodeList):
|
||||
self.nodelist: NodeList = nodelist
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def render(self, context):
|
||||
def render(self, context: Context) -> str:
|
||||
raise TemplateSyntaxError(
|
||||
"{% fill ... %} block cannot be rendered directly. "
|
||||
"You are probably seeing this because you have used one outside "
|
||||
|
@ -280,7 +280,7 @@ class NamedFillNode(BaseFillNode):
|
|||
self.name_fexp = name_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)}.>"
|
||||
|
||||
|
||||
|
@ -291,12 +291,12 @@ class ImplicitFillNode(BaseFillNode):
|
|||
as 'default'.
|
||||
"""
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"<{type(self)} Contents: {repr(self.nodelist)}.>"
|
||||
|
||||
|
||||
@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
|
||||
'slot'-block in the component template referred to by a parent component.
|
||||
It exists to make component nesting easier.
|
||||
|
@ -333,11 +333,11 @@ class ComponentNode(Node):
|
|||
def __init__(
|
||||
self,
|
||||
name_fexp: FilterExpression,
|
||||
context_args,
|
||||
context_kwargs,
|
||||
isolated_context=False,
|
||||
context_args: List[FilterExpression],
|
||||
context_kwargs: Mapping[str, FilterExpression],
|
||||
isolated_context: bool = False,
|
||||
fill_nodes: Union[ImplicitFillNode, Iterable[NamedFillNode]] = (),
|
||||
):
|
||||
) -> None:
|
||||
self.name_fexp = name_fexp
|
||||
self.context_args = context_args or []
|
||||
self.context_kwargs = context_kwargs or {}
|
||||
|
@ -345,19 +345,19 @@ class ComponentNode(Node):
|
|||
self.fill_nodes = 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):
|
||||
return NodeList([fill_nodes])
|
||||
else:
|
||||
return NodeList(fill_nodes)
|
||||
|
||||
def __repr__(self):
|
||||
return "<ComponentNode: %s. Contents: %r>" % (
|
||||
def __repr__(self) -> str:
|
||||
return "<ComponentNode: {}. Contents: {!r}>".format(
|
||||
self.name_fexp,
|
||||
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)
|
||||
component_cls: Type[Component] = component_registry.get(resolved_component_name)
|
||||
|
||||
|
@ -408,7 +408,7 @@ class ComponentNode(Node):
|
|||
|
||||
|
||||
@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:
|
||||
{% component "name" positional_arg keyword_arg=value ... %}
|
||||
|
@ -497,7 +497,7 @@ def try_parse_as_default_fill(
|
|||
return ImplicitFillNode(nodelist=nodelist)
|
||||
|
||||
|
||||
def block_has_content(nodelist) -> bool:
|
||||
def block_has_content(nodelist: NodeList) -> bool:
|
||||
for node in nodelist:
|
||||
if isinstance(node, TextNode) and node.s.isspace():
|
||||
pass
|
||||
|
@ -512,16 +512,16 @@ def is_whitespace_node(node: Node) -> bool:
|
|||
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()
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
@register.tag(name="if_filled")
|
||||
def do_if_filled_block(parser, token):
|
||||
def do_if_filled_block(parser: Parser, token: Token) -> "IfSlotFilledNode":
|
||||
"""
|
||||
### Usage
|
||||
|
||||
|
@ -623,7 +623,7 @@ class _IfSlotFilledBranchNode(Node):
|
|||
def render(self, context: Context) -> str:
|
||||
return self.nodelist.render(context)
|
||||
|
||||
def evaluate(self, context) -> bool:
|
||||
def evaluate(self, context: Context) -> bool:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
|
@ -632,13 +632,13 @@ class IfSlotFilledConditionBranchNode(_IfSlotFilledBranchNode, TemplateAwareNode
|
|||
self,
|
||||
slot_name: str,
|
||||
nodelist: NodeList,
|
||||
is_positive=True,
|
||||
is_positive: Union[bool, None] = True,
|
||||
) -> None:
|
||||
self.slot_name = slot_name
|
||||
self.is_positive: bool = is_positive
|
||||
self.is_positive: Optional[bool] = is_positive
|
||||
super().__init__(nodelist)
|
||||
|
||||
def evaluate(self, context) -> bool:
|
||||
def evaluate(self, context: Context) -> bool:
|
||||
try:
|
||||
filled_slots: FilledSlotsContext = context[FILLED_SLOTS_CONTENT_CONTEXT_KEY]
|
||||
except KeyError:
|
||||
|
@ -654,7 +654,7 @@ class IfSlotFilledConditionBranchNode(_IfSlotFilledBranchNode, TemplateAwareNode
|
|||
|
||||
|
||||
class IfSlotFilledElseBranchNode(_IfSlotFilledBranchNode):
|
||||
def evaluate(self, context) -> bool:
|
||||
def evaluate(self, context: Context) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
|
@ -666,13 +666,13 @@ class IfSlotFilledNode(Node):
|
|||
self.branches = branches
|
||||
self.nodelist = self._create_nodelist(branches)
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return f"<{self.__class__.__name__}>"
|
||||
|
||||
def _create_nodelist(self, branches) -> NodeList:
|
||||
def _create_nodelist(self, branches: List[_IfSlotFilledBranchNode]) -> NodeList:
|
||||
return NodeList(branches)
|
||||
|
||||
def render(self, context):
|
||||
def render(self, context: Context) -> str:
|
||||
for node in self.branches:
|
||||
if isinstance(node, IfSlotFilledElseBranchNode):
|
||||
return node.render(context)
|
||||
|
@ -682,7 +682,7 @@ class IfSlotFilledNode(Node):
|
|||
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'."""
|
||||
|
||||
if bits[-1] == "only":
|
||||
|
@ -694,7 +694,9 @@ def check_for_isolated_context_keyword(bits):
|
|||
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(
|
||||
parser=parser,
|
||||
bits=bits,
|
||||
|
@ -726,21 +728,21 @@ def parse_component_with_args(parser, bits, tag_name):
|
|||
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."""
|
||||
|
||||
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]
|
||||
|
||||
|
||||
def is_dependency_middleware_active():
|
||||
def is_dependency_middleware_active() -> bool:
|
||||
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:
|
||||
- Value of `tag` in {"slot", "fill", "alias"}
|
||||
|
@ -756,7 +758,7 @@ def strip_quotes(s: str) -> str:
|
|||
return s.strip("\"'")
|
||||
|
||||
|
||||
def bool_from_string(s: str):
|
||||
def bool_from_string(s: str) -> bool:
|
||||
s = strip_quotes(s.lower())
|
||||
if s == "true":
|
||||
return True
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
import typing
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
from typing import Annotated # type: ignore
|
||||
except ImportError:
|
||||
|
||||
@typing.no_type_check
|
||||
class Annotated: # type: ignore
|
||||
def __init__(self, type_, *args, **kwargs):
|
||||
def __init__(self, type_: str, *args: Any, **kwargs: Any):
|
||||
self.type_ = type_
|
||||
self.metadata = args, kwargs
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
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):
|
||||
params = (params,)
|
||||
return Annotated(self.type_, *params, **self.metadata[1]) # type: ignore
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import glob
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
from typing import List, Optional, Union
|
||||
|
||||
from django.template.engine import Engine
|
||||
|
||||
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.
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue