Skip to content

Commit

Permalink
Implement _StrPromise attribute hook.
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
PIG208 committed Aug 8, 2022
1 parent 5875c41 commit 71a6b57
Show file tree
Hide file tree
Showing 5 changed files with 66 additions and 0 deletions.
2 changes: 2 additions & 0 deletions django-stubs/utils/functional.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class _StrPromise(Promise, Sequence[str]):
def __mod__(self, __x: Any) -> str: ...
def __mul__(self, __n: SupportsIndex) -> str: ...
def __rmul__(self, __n: SupportsIndex) -> str: ...
# Mypy requires this for the attribute hook to take effect
def __getattribute__(self, __name: str) -> Any: ...

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

Expand Down
2 changes: 2 additions & 0 deletions mypy_django_plugin/lib/fullnames.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,5 @@
F_EXPRESSION_FULLNAME = "django.db.models.expressions.F"

ANY_ATTR_ALLOWED_CLASS_FULLNAME = "django_stubs_ext.AnyAttrAllowed"

STR_PROMISE_FULLNAME = "django.utils.functional._StrPromise"
4 changes: 4 additions & 0 deletions mypy_django_plugin/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from mypy_django_plugin.django.context import DjangoContext
from mypy_django_plugin.lib import fullnames, helpers
from mypy_django_plugin.transformers import fields, forms, init_create, meta, querysets, request, settings
from mypy_django_plugin.transformers.functional import resolve_str_promise_attribute
from mypy_django_plugin.transformers.managers import (
create_new_manager_class_from_from_queryset_method,
resolve_manager_method,
Expand Down Expand Up @@ -285,6 +286,9 @@ def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeConte
):
return resolve_manager_method

if info and info.has_base(fullnames.STR_PROMISE_FULLNAME):
return resolve_str_promise_attribute

return None

def get_type_analyze_hook(self, fullname: str) -> Optional[Callable[[AnalyzeTypeContext], MypyType]]:
Expand Down
31 changes: 31 additions & 0 deletions mypy_django_plugin/transformers/functional.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from mypy.errorcodes import ATTR_DEFINED
from mypy.nodes import MemberExpr
from mypy.plugin import AttributeContext
from mypy.types import AnyType, CallableType, Instance
from mypy.types import Type as MypyType
from mypy.types import TypeOfAny

from mypy_django_plugin.lib import helpers


def resolve_str_promise_attribute(ctx: AttributeContext) -> MypyType:
assert isinstance(ctx.type, Instance)
assert isinstance(ctx.context, MemberExpr)

str_info = helpers.lookup_fully_qualified_typeinfo(helpers.get_typechecker_api(ctx), f"builtins.str")
assert str_info is not None
method = str_info.get(ctx.context.name)

if method is None or method.type is None:
ctx.api.fail(
f'"{ctx.type.type.fullname}" has no attribute "{ctx.context.name}"', ctx.context, code=ATTR_DEFINED
)
return AnyType(TypeOfAny.from_error)

assert isinstance(method.type, CallableType)
# The proxied str methods are only meant to be used as instance methods.
# We need to drop the first `self` argument in them.
assert method.type.arg_names[0] == "self"
return method.type.copy_modified(
arg_kinds=method.type.arg_kinds[1:], arg_names=method.type.arg_names[1:], arg_types=method.type.arg_types[1:]
)
27 changes: 27 additions & 0 deletions tests/typecheck/utils/test_functional.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,30 @@
f = Foo()
reveal_type(f.attr) # N: Revealed type is "builtins.list[builtins.str]"
f.attr.name # E: "List[str]" has no attribute "name"
- case: str_promise_proxy
main: |
from django.utils.functional import Promise, lazystr
s = lazystr("asd")
reveal_type(s) # N: Revealed type is "django.utils.functional._StrPromise"
reveal_type(s.format("asd")) # N: Revealed type is "builtins.str"
reveal_type(s.capitalize()) # N: Revealed type is "builtins.str"
reveal_type(s.swapcase) # N: Revealed type is "def () -> builtins.str"
reveal_type(s.__getnewargs__) # N: Revealed type is "def () -> Tuple[builtins.str]"
s.nonsense # E: "django.utils.functional._StrPromise" has no attribute "nonsense"
reveal_type(s + "bar") # N: Revealed type is "builtins.str"
reveal_type("foo" + s) # N: Revealed type is "Any"
reveal_type(s % "asd") # N: Revealed type is "builtins.str"
def foo(content: str) -> None:
...
def bar(content: Promise) -> None:
...
foo(s) # E: Argument 1 to "foo" has incompatible type "_StrPromise"; expected "str"
bar(s)

0 comments on commit 71a6b57

Please sign in to comment.