From b554208b35edef64b2aa65a4e1013f3c00437c85 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Sun, 14 Jan 2024 12:23:58 +0100 Subject: [PATCH 1/7] WIP: Improve typing in template registry --- django-stubs/template/defaultfilters.pyi | 9 +++++--- django-stubs/template/library.pyi | 23 +++++++++++++++---- tests/typecheck/template/test_library.yml | 28 +++++++++++++++++++++++ 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/django-stubs/template/defaultfilters.pyi b/django-stubs/template/defaultfilters.pyi index 7eacd3fa7..b560ea252 100644 --- a/django-stubs/template/defaultfilters.pyi +++ b/django-stubs/template/defaultfilters.pyi @@ -2,13 +2,16 @@ from collections.abc import Callable from datetime import date as _date from datetime import datetime from datetime import time as _time -from typing import Any +from typing import Any, TypeVar +from django.template.library import Library from django.utils.safestring import SafeString -register: Any +_C = TypeVar("_C", bound=Callable[..., Any]) -def stringfilter(func: Callable) -> Callable: ... +register: Library + +def stringfilter(func: _C) -> _C: ... def addslashes(value: str) -> str: ... def capfirst(value: str) -> str: ... def escapejs_filter(value: str) -> SafeString: ... diff --git a/django-stubs/template/library.pyi b/django-stubs/template/library.pyi index 927918e2c..d98ec8376 100644 --- a/django-stubs/template/library.pyi +++ b/django-stubs/template/library.pyi @@ -10,9 +10,10 @@ from .base import Node, Template class InvalidTemplateLibrary(Exception): ... _C = TypeVar("_C", bound=Callable[..., Any]) +_FilterC = TypeVar("_FilterC", bound=Callable[[Any], Any] | Callable[[Any, Any], Any]) class Library: - filters: dict[str, Callable] + filters: dict[str, Callable[[Any], Any] | Callable[[Any, Any], Any]] tags: dict[str, Callable] def __init__(self) -> None: ... @overload @@ -23,11 +24,25 @@ class Library: def tag(self, name: str | None = ..., compile_function: None = ...) -> Callable[[_C], _C]: ... def tag_function(self, func: _C) -> _C: ... @overload - def filter(self, name: _C, filter_func: None = ..., **flags: Any) -> _C: ... + def filter(self, name: _FilterC, /) -> _FilterC: ... @overload - def filter(self, name: str | None, filter_func: _C, **flags: Any) -> _C: ... + def filter( + self, + name: str | None = ..., + filter_func: None = ..., + is_safe: bool = ..., + needs_autoescape: bool = ..., + expects_localtime: bool = ..., + ) -> Callable[[_FilterC], _FilterC]: ... @overload - def filter(self, name: str | None = ..., filter_func: None = ..., **flags: Any) -> Callable[[_C], _C]: ... + def filter( + self, + name: str, + filter_func: _FilterC, + is_safe: bool = ..., + needs_autoescape: bool = ..., + expects_localtime: bool = ..., + ) -> _FilterC: ... @overload def simple_tag(self, func: _C) -> _C: ... @overload diff --git a/tests/typecheck/template/test_library.yml b/tests/typecheck/template/test_library.yml index b3fd4dde0..a8529a5d7 100644 --- a/tests/typecheck/template/test_library.yml +++ b/tests/typecheck/template/test_library.yml @@ -20,6 +20,34 @@ reveal_type(lower) # N: Revealed type is "def (value: builtins.str) -> builtins.str" +- case: register_filter_no_decorator + main: | + from django import template + register = template.Library() + + def lower(value: str) -> str: + return value.lower() + + registered = register.filter("tolower", lower) + + reveal_type(registered) # N: Revealed type is "def (value: builtins.str) -> builtins.str" + +- case: register_bad_filters + main: | + from django import template + register = template.Library() + + @register.filter + def lower() -> str: + return "" + + @register.filter(name="toomanyargs") + def toomanyargs(arg1: str, arg2: str, arg3: str) -> str: + return "" + out: | + main:4: error: Value of type variable "_FilterC" of "filter" of "Library" cannot be "Callable[[], str]" [type-var] + main:8: error: Value of type variable "_FilterC" of function cannot be "Callable[[str, str, str], str]" [type-var] + - case: register_simple_tag_no_args main: | import datetime From 15cbe0ccf43b7bc111f36dd1abc772fd9948b0ea Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Sun, 14 Jan 2024 21:53:24 +0100 Subject: [PATCH 2/7] WIP: more work and fixes --- django-stubs/template/defaultfilters.pyi | 7 +++++-- django-stubs/template/library.pyi | 23 +++++++++++++++++------ tests/typecheck/template/test_library.yml | 10 ++++++++++ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/django-stubs/template/defaultfilters.pyi b/django-stubs/template/defaultfilters.pyi index b560ea252..870b86cd9 100644 --- a/django-stubs/template/defaultfilters.pyi +++ b/django-stubs/template/defaultfilters.pyi @@ -4,14 +4,17 @@ from datetime import datetime from datetime import time as _time from typing import Any, TypeVar +from typing_extensions import Concatenate, ParamSpec + from django.template.library import Library from django.utils.safestring import SafeString -_C = TypeVar("_C", bound=Callable[..., Any]) +_P = ParamSpec("_P") +_R = TypeVar("_R") register: Library -def stringfilter(func: _C) -> _C: ... +def stringfilter(func: Callable[Concatenate[str, _P], _R]) -> Callable[Concatenate[object, _P], _R]: ... def addslashes(value: str) -> str: ... def capfirst(value: str) -> str: ... def escapejs_filter(value: str) -> SafeString: ... diff --git a/django-stubs/template/library.pyi b/django-stubs/template/library.pyi index d98ec8376..3438550e1 100644 --- a/django-stubs/template/library.pyi +++ b/django-stubs/template/library.pyi @@ -1,5 +1,7 @@ from collections.abc import Callable, Collection, Iterable, Mapping, Sequence, Sized -from typing import Any, TypeVar, overload +from typing import Any, Literal, TypeVar, overload + +from typing_extensions import Concatenate from django.template.base import FilterExpression, Origin, Parser, Token from django.template.context import Context @@ -11,23 +13,25 @@ class InvalidTemplateLibrary(Exception): ... _C = TypeVar("_C", bound=Callable[..., Any]) _FilterC = TypeVar("_FilterC", bound=Callable[[Any], Any] | Callable[[Any, Any], Any]) +_TakesContextC = TypeVar("_TakesContextC", bound=Callable[Concatenate[Context, ...], Any]) class Library: filters: dict[str, Callable[[Any], Any] | Callable[[Any, Any], Any]] tags: dict[str, Callable] def __init__(self) -> None: ... @overload - def tag(self, name: _C) -> _C: ... + def tag(self, name: _C, /) -> _C: ... @overload - def tag(self, name: str, compile_function: _C) -> _C: ... + def tag(self, *, name: str, compile_function: _C) -> _C: ... @overload - def tag(self, name: str | None = ..., compile_function: None = ...) -> Callable[[_C], _C]: ... + def tag(self, *, name: str | None = ..., compile_function: None = ...) -> Callable[[_C], _C]: ... def tag_function(self, func: _C) -> _C: ... @overload def filter(self, name: _FilterC, /) -> _FilterC: ... @overload def filter( self, + *, name: str | None = ..., filter_func: None = ..., is_safe: bool = ..., @@ -37,6 +41,7 @@ class Library: @overload def filter( self, + *, name: str, filter_func: _FilterC, is_safe: bool = ..., @@ -44,9 +49,15 @@ class Library: expects_localtime: bool = ..., ) -> _FilterC: ... @overload - def simple_tag(self, func: _C) -> _C: ... + def simple_tag(self, func: _C, /) -> _C: ... + @overload + def simple_tag( + self, *, takes_context: Literal[True], name: str | None = ... + ) -> Callable[[_TakesContextC], _TakesContextC]: ... @overload - def simple_tag(self, takes_context: bool | None = ..., name: str | None = ...) -> Callable[[_C], _C]: ... + def simple_tag( + self, *, takes_context: Literal[False] | None = ..., name: str | None = ... + ) -> Callable[[_C], _C]: ... def inclusion_tag( self, filename: Template | str, diff --git a/tests/typecheck/template/test_library.yml b/tests/typecheck/template/test_library.yml index a8529a5d7..23f42da2c 100644 --- a/tests/typecheck/template/test_library.yml +++ b/tests/typecheck/template/test_library.yml @@ -122,3 +122,13 @@ return ', '.join(results) reveal_type(format_results) # N: Revealed type is "def (results: builtins.list[builtins.str]) -> builtins.str" + +- case: stringfilter + main: | + from django.template.defaultfilters import stringfilter + + @stringfilter + def lower(value: str) -> str: + return value.lower() + + reveal_type(lower) # N: Revealed type is "def (value: object) -> builtins.str" From 28925709becd18d8485d1dc8815b35acc76608f2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 14 Jan 2024 20:53:40 +0000 Subject: [PATCH 3/7] [pre-commit.ci] auto fixes from pre-commit.com hooks --- django-stubs/template/defaultfilters.pyi | 3 +-- django-stubs/template/library.pyi | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/django-stubs/template/defaultfilters.pyi b/django-stubs/template/defaultfilters.pyi index 870b86cd9..f6a40d4ff 100644 --- a/django-stubs/template/defaultfilters.pyi +++ b/django-stubs/template/defaultfilters.pyi @@ -4,10 +4,9 @@ from datetime import datetime from datetime import time as _time from typing import Any, TypeVar -from typing_extensions import Concatenate, ParamSpec - from django.template.library import Library from django.utils.safestring import SafeString +from typing_extensions import Concatenate, ParamSpec _P = ParamSpec("_P") _R = TypeVar("_R") diff --git a/django-stubs/template/library.pyi b/django-stubs/template/library.pyi index 3438550e1..e1dce92cd 100644 --- a/django-stubs/template/library.pyi +++ b/django-stubs/template/library.pyi @@ -1,11 +1,10 @@ from collections.abc import Callable, Collection, Iterable, Mapping, Sequence, Sized from typing import Any, Literal, TypeVar, overload -from typing_extensions import Concatenate - from django.template.base import FilterExpression, Origin, Parser, Token from django.template.context import Context from django.utils.safestring import SafeString +from typing_extensions import Concatenate from .base import Node, Template From b21f0f5a11e99631ef53e410e8cd78ea6ad22583 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Mon, 15 Jan 2024 09:07:32 +0100 Subject: [PATCH 4/7] Fix stararg position --- django-stubs/template/library.pyi | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/django-stubs/template/library.pyi b/django-stubs/template/library.pyi index e1dce92cd..8838ac844 100644 --- a/django-stubs/template/library.pyi +++ b/django-stubs/template/library.pyi @@ -21,9 +21,9 @@ class Library: @overload def tag(self, name: _C, /) -> _C: ... @overload - def tag(self, *, name: str, compile_function: _C) -> _C: ... + def tag(self, name: str, compile_function: _C) -> _C: ... @overload - def tag(self, *, name: str | None = ..., compile_function: None = ...) -> Callable[[_C], _C]: ... + def tag(self, name: str | None = ..., compile_function: None = ...) -> Callable[[_C], _C]: ... def tag_function(self, func: _C) -> _C: ... @overload def filter(self, name: _FilterC, /) -> _FilterC: ... @@ -40,9 +40,9 @@ class Library: @overload def filter( self, - *, name: str, filter_func: _FilterC, + *, is_safe: bool = ..., needs_autoescape: bool = ..., expects_localtime: bool = ..., From 329090ea83c4fcbdc2cb5f28c13016423785ab85 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Mon, 15 Jan 2024 14:04:15 +0100 Subject: [PATCH 5/7] More improvements --- django-stubs/template/library.pyi | 25 ++++++++++++++++------- tests/typecheck/template/test_library.yml | 21 ++++++++++++++++--- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/django-stubs/template/library.pyi b/django-stubs/template/library.pyi index 8838ac844..93be4f83c 100644 --- a/django-stubs/template/library.pyi +++ b/django-stubs/template/library.pyi @@ -11,20 +11,21 @@ from .base import Node, Template class InvalidTemplateLibrary(Exception): ... _C = TypeVar("_C", bound=Callable[..., Any]) +_CompileC = TypeVar("_CompileC", bound=Callable[[Parser, Token], Node]) _FilterC = TypeVar("_FilterC", bound=Callable[[Any], Any] | Callable[[Any, Any], Any]) _TakesContextC = TypeVar("_TakesContextC", bound=Callable[Concatenate[Context, ...], Any]) class Library: filters: dict[str, Callable[[Any], Any] | Callable[[Any, Any], Any]] - tags: dict[str, Callable] + tags: dict[str, Callable[[Parser, Token], Node]] def __init__(self) -> None: ... @overload - def tag(self, name: _C, /) -> _C: ... + def tag(self, name: _CompileC, /) -> _CompileC: ... @overload - def tag(self, name: str, compile_function: _C) -> _C: ... + def tag(self, name: str, compile_function: _CompileC) -> _CompileC: ... @overload - def tag(self, name: str | None = ..., compile_function: None = ...) -> Callable[[_C], _C]: ... - def tag_function(self, func: _C) -> _C: ... + def tag(self, name: str | None = ..., compile_function: None = ...) -> Callable[[_CompileC], _CompileC]: ... + def tag_function(self, func: _CompileC) -> _CompileC: ... @overload def filter(self, name: _FilterC, /) -> _FilterC: ... @overload @@ -57,11 +58,21 @@ class Library: def simple_tag( self, *, takes_context: Literal[False] | None = ..., name: str | None = ... ) -> Callable[[_C], _C]: ... + @overload + def inclusion_tag( + self, + filename: Template | str, + func: Callable[..., Any] | None = ..., + *, + takes_context: Literal[True], + name: str | None = ..., + ) -> Callable[[_TakesContextC], _TakesContextC]: ... + @overload def inclusion_tag( self, filename: Template | str, - func: Callable | None = ..., - takes_context: bool | None = ..., + func: Callable[..., Any] | None = ..., + takes_context: Literal[False] | None = ..., name: str | None = ..., ) -> Callable[[_C], _C]: ... diff --git a/tests/typecheck/template/test_library.yml b/tests/typecheck/template/test_library.yml index 23f42da2c..3ed0b8336 100644 --- a/tests/typecheck/template/test_library.yml +++ b/tests/typecheck/template/test_library.yml @@ -63,15 +63,16 @@ - case: register_simple_tag_context main: | from django import template + from django.template.context import Context from typing import Dict, Any register = template.Library() @register.simple_tag(takes_context=True) - def current_time(context: Dict[str, Any], format_string: str) -> str: + def current_time(context: Context, format_string: str) -> str: timezone = context['timezone'] return "test" - reveal_type(current_time) # N: Revealed type is "def (context: builtins.dict[builtins.str, Any], format_string: builtins.str) -> builtins.str" + reveal_type(current_time) # N: Revealed type is "def (context: Context, format_string: builtins.str) -> builtins.str" - case: register_simple_tag_named main: | @@ -123,6 +124,20 @@ reveal_type(format_results) # N: Revealed type is "def (results: builtins.list[builtins.str]) -> builtins.str" +- case: register_inclusion_tag_takes_context + main: | + from django import template + from django.template.context import Context + + from typing import List + register = template.Library() + + @register.inclusion_tag('results.html', takes_context=True) + def format_results(context: Context, results: List[str]) -> str: + return ', '.join(results) + + reveal_type(format_results) # N: Revealed type is "def (context: Context, results: builtins.list[builtins.str]) -> builtins.str" + - case: stringfilter main: | from django.template.defaultfilters import stringfilter @@ -131,4 +146,4 @@ def lower(value: str) -> str: return value.lower() - reveal_type(lower) # N: Revealed type is "def (value: object) -> builtins.str" + reveal_type(lower) # N: Revealed type is "def (builtins.object) -> builtins.str" From e65f0b5a5a94d9e29bfd8ac40d4a14ab688b5076 Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Mon, 15 Jan 2024 17:50:01 +0100 Subject: [PATCH 6/7] Fix tests --- tests/typecheck/template/test_library.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/typecheck/template/test_library.yml b/tests/typecheck/template/test_library.yml index 3ed0b8336..0759099c1 100644 --- a/tests/typecheck/template/test_library.yml +++ b/tests/typecheck/template/test_library.yml @@ -72,7 +72,7 @@ timezone = context['timezone'] return "test" - reveal_type(current_time) # N: Revealed type is "def (context: Context, format_string: builtins.str) -> builtins.str" + reveal_type(current_time) # N: Revealed type is "def (context: django.template.context.Context, format_string: builtins.str) -> builtins.str" - case: register_simple_tag_named main: | @@ -136,7 +136,7 @@ def format_results(context: Context, results: List[str]) -> str: return ', '.join(results) - reveal_type(format_results) # N: Revealed type is "def (context: Context, results: builtins.list[builtins.str]) -> builtins.str" + reveal_type(format_results) # N: Revealed type is "def (context: django.template.context.Context, results: builtins.list[builtins.str]) -> builtins.str" - case: stringfilter main: | From 8b80b84d8bed361fb125f1cb824bdf8bae8f1d2e Mon Sep 17 00:00:00 2001 From: Viicos <65306057+Viicos@users.noreply.github.com> Date: Tue, 2 Apr 2024 19:52:51 +0200 Subject: [PATCH 7/7] Add comments Comments taken from the Django source code --- django-stubs/template/library.pyi | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/django-stubs/template/library.pyi b/django-stubs/template/library.pyi index 93be4f83c..6327ef084 100644 --- a/django-stubs/template/library.pyi +++ b/django-stubs/template/library.pyi @@ -19,15 +19,22 @@ class Library: filters: dict[str, Callable[[Any], Any] | Callable[[Any, Any], Any]] tags: dict[str, Callable[[Parser, Token], Node]] def __init__(self) -> None: ... + # @register.tag @overload def tag(self, name: _CompileC, /) -> _CompileC: ... + # register.tag("somename", somefunc) @overload def tag(self, name: str, compile_function: _CompileC) -> _CompileC: ... + # @register.tag() + # @register.tag("somename") or @register.tag(name="somename") @overload def tag(self, name: str | None = ..., compile_function: None = ...) -> Callable[[_CompileC], _CompileC]: ... def tag_function(self, func: _CompileC) -> _CompileC: ... + # @register.filter @overload def filter(self, name: _FilterC, /) -> _FilterC: ... + # @register.filter() + # @register.filter("somename") or @register.filter(name='somename') @overload def filter( self, @@ -38,6 +45,7 @@ class Library: needs_autoescape: bool = ..., expects_localtime: bool = ..., ) -> Callable[[_FilterC], _FilterC]: ... + # register.filter("somename", somefunc) @overload def filter( self, @@ -48,12 +56,16 @@ class Library: needs_autoescape: bool = ..., expects_localtime: bool = ..., ) -> _FilterC: ... + # @register.simple_tag @overload def simple_tag(self, func: _C, /) -> _C: ... + # @register.simple_tag(takes_context=True) @overload def simple_tag( self, *, takes_context: Literal[True], name: str | None = ... ) -> Callable[[_TakesContextC], _TakesContextC]: ... + # @register.simple_tag(takes_context=False) + # @register.simple_tag(...) @overload def simple_tag( self, *, takes_context: Literal[False] | None = ..., name: str | None = ...