Skip to content

Commit 71a6b57

Browse files
committed
Implement _StrPromise attribute hook.
This implements an attribute hook that provides type information for methods that are available on `builtins.str` for `_StrPromise` except the supported operators. This allows us to avoid copying stubs from the builtins for all supported methods on `str`. Signed-off-by: Zixuan James Li <p359101898@gmail.com>
1 parent 5875c41 commit 71a6b57

File tree

5 files changed

+66
-0
lines changed

5 files changed

+66
-0
lines changed

django-stubs/utils/functional.pyi

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ class _StrPromise(Promise, Sequence[str]):
4646
def __mod__(self, __x: Any) -> str: ...
4747
def __mul__(self, __n: SupportsIndex) -> str: ...
4848
def __rmul__(self, __n: SupportsIndex) -> str: ...
49+
# Mypy requires this for the attribute hook to take effect
50+
def __getattribute__(self, __name: str) -> Any: ...
4951

5052
_C = TypeVar("_C", bound=Callable)
5153

mypy_django_plugin/lib/fullnames.py

+2
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,5 @@
4141
F_EXPRESSION_FULLNAME = "django.db.models.expressions.F"
4242

4343
ANY_ATTR_ALLOWED_CLASS_FULLNAME = "django_stubs_ext.AnyAttrAllowed"
44+
45+
STR_PROMISE_FULLNAME = "django.utils.functional._StrPromise"

mypy_django_plugin/main.py

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from mypy_django_plugin.django.context import DjangoContext
2323
from mypy_django_plugin.lib import fullnames, helpers
2424
from mypy_django_plugin.transformers import fields, forms, init_create, meta, querysets, request, settings
25+
from mypy_django_plugin.transformers.functional import resolve_str_promise_attribute
2526
from mypy_django_plugin.transformers.managers import (
2627
create_new_manager_class_from_from_queryset_method,
2728
resolve_manager_method,
@@ -285,6 +286,9 @@ def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeConte
285286
):
286287
return resolve_manager_method
287288

289+
if info and info.has_base(fullnames.STR_PROMISE_FULLNAME):
290+
return resolve_str_promise_attribute
291+
288292
return None
289293

290294
def get_type_analyze_hook(self, fullname: str) -> Optional[Callable[[AnalyzeTypeContext], MypyType]]:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
from mypy.errorcodes import ATTR_DEFINED
2+
from mypy.nodes import MemberExpr
3+
from mypy.plugin import AttributeContext
4+
from mypy.types import AnyType, CallableType, Instance
5+
from mypy.types import Type as MypyType
6+
from mypy.types import TypeOfAny
7+
8+
from mypy_django_plugin.lib import helpers
9+
10+
11+
def resolve_str_promise_attribute(ctx: AttributeContext) -> MypyType:
12+
assert isinstance(ctx.type, Instance)
13+
assert isinstance(ctx.context, MemberExpr)
14+
15+
str_info = helpers.lookup_fully_qualified_typeinfo(helpers.get_typechecker_api(ctx), f"builtins.str")
16+
assert str_info is not None
17+
method = str_info.get(ctx.context.name)
18+
19+
if method is None or method.type is None:
20+
ctx.api.fail(
21+
f'"{ctx.type.type.fullname}" has no attribute "{ctx.context.name}"', ctx.context, code=ATTR_DEFINED
22+
)
23+
return AnyType(TypeOfAny.from_error)
24+
25+
assert isinstance(method.type, CallableType)
26+
# The proxied str methods are only meant to be used as instance methods.
27+
# We need to drop the first `self` argument in them.
28+
assert method.type.arg_names[0] == "self"
29+
return method.type.copy_modified(
30+
arg_kinds=method.type.arg_kinds[1:], arg_names=method.type.arg_names[1:], arg_types=method.type.arg_types[1:]
31+
)

tests/typecheck/utils/test_functional.yml

+27
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,30 @@
1616
f = Foo()
1717
reveal_type(f.attr) # N: Revealed type is "builtins.list[builtins.str]"
1818
f.attr.name # E: "List[str]" has no attribute "name"
19+
20+
- case: str_promise_proxy
21+
main: |
22+
from django.utils.functional import Promise, lazystr
23+
24+
s = lazystr("asd")
25+
26+
reveal_type(s) # N: Revealed type is "django.utils.functional._StrPromise"
27+
28+
reveal_type(s.format("asd")) # N: Revealed type is "builtins.str"
29+
reveal_type(s.capitalize()) # N: Revealed type is "builtins.str"
30+
reveal_type(s.swapcase) # N: Revealed type is "def () -> builtins.str"
31+
reveal_type(s.__getnewargs__) # N: Revealed type is "def () -> Tuple[builtins.str]"
32+
s.nonsense # E: "django.utils.functional._StrPromise" has no attribute "nonsense"
33+
34+
reveal_type(s + "bar") # N: Revealed type is "builtins.str"
35+
reveal_type("foo" + s) # N: Revealed type is "Any"
36+
reveal_type(s % "asd") # N: Revealed type is "builtins.str"
37+
38+
def foo(content: str) -> None:
39+
...
40+
41+
def bar(content: Promise) -> None:
42+
...
43+
44+
foo(s) # E: Argument 1 to "foo" has incompatible type "_StrPromise"; expected "str"
45+
bar(s)

0 commit comments

Comments
 (0)