diff --git a/pyproject.toml b/pyproject.toml
index 168db0db..e1be8f7d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -46,6 +46,11 @@ exclude = [
'build',
]
+[[tool.mypy.overrides]]
+module = "django_components.*"
+disallow_untyped_defs = true
+
+
[tool.pytest.ini_options]
testpaths = [
"tests"
diff --git a/src/django_components/__init__.py b/src/django_components/__init__.py
index b7f9ae82..12a573c8 100644
--- a/src/django_components/__init__.py
+++ b/src/django_components/__init__.py
@@ -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)
diff --git a/src/django_components/app_settings.py b/src/django_components/app_settings.py
index d4550e98..71e525fc 100644
--- a/src/django_components/app_settings.py
+++ b/src/django_components/app_settings.py
@@ -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:
diff --git a/src/django_components/apps.py b/src/django_components/apps.py
index 6cf3cb52..b96c1190 100644
--- a/src/django_components/apps.py
+++ b/src/django_components/apps.py
@@ -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()
diff --git a/src/django_components/component.py b/src/django_components/component.py
index d840db87..12e9310e 100644
--- a/src/django_components/component.py
+++ b/src/django_components/component.py
@@ -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"")
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"")
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.
diff --git a/src/django_components/management/commands/startcomponent.py b/src/django_components/management/commands/startcomponent.py
index 6054ebbd..3e3b0c01 100644
--- a/src/django_components/management/commands/startcomponent.py
+++ b/src/django_components/management/commands/startcomponent.py
@@ -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:
diff --git a/src/django_components/management/commands/upgradecomponent.py b/src/django_components/management/commands/upgradecomponent.py
index 5c912e06..ab31fa1a 100644
--- a/src/django_components/management/commands/upgradecomponent.py
+++ b/src/django_components/management/commands/upgradecomponent.py
@@ -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()
diff --git a/src/django_components/middleware.py b/src/django_components/middleware.py
index 4655398c..3005827a 100644
--- a/src/django_components/middleware.py
+++ b/src/django_components/middleware.py
@@ -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 = ''
JS_DEPENDENCY_PLACEHOLDER = ''
@@ -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, '