feat: add type hints everywhere

This commit is contained in:
Gabriel Dugny 2024-03-24 23:29:52 +01:00
parent c11f30ec7c
commit b9f4e596a4
12 changed files with 138 additions and 98 deletions

View file

@ -46,6 +46,11 @@ exclude = [
'build',
]
[[tool.mypy.overrides]]
module = "django_components.*"
disallow_untyped_defs = true
[tool.pytest.ini_options]
testpaths = [
"tests"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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