django-stubs icon indicating copy to clipboard operation
django-stubs copied to clipboard

Drop dict and Optional from argument type

Open Majsvaffla opened this issue 3 years ago • 13 comments

I have made things!

I have corrected the type definition for the context argument to django.template.base.Template.render. The argument is no longer typed as optional and does no longer accept a dict value.

Related issues

  • Closes #1147

Majsvaffla avatar Sep 23 '22 13:09 Majsvaffla

(I meant to click "Comment", not "Approve")

sobolevn avatar Sep 23 '22 18:09 sobolevn

I'd like to make some hints conditional by Django version, if that could be possible... perhaps the mypy plugin could provide the current version?

adamchainz avatar Sep 26 '22 09:09 adamchainz

As far as I know - custom builtins are not supported. https://github.com/python/mypy/issues/12172

We can do something like:

_DjangoVersion4_1 = _DjangoVersion[4, 1]

Inside our plugin we can return Literal[True] for this using get_type_analyze_hook() from https://mypy.readthedocs.io/en/stable/extending_mypy.html

But, this is quite complex!

sobolevn avatar Sep 26 '22 09:09 sobolevn

I was thinking about typing django.VERSION conditionally, so we could use the same syntax as at runtime:

if django.VERSION >= (4 ,1):
    ...  # new stuff
else:
    ...  # old stuff

It seems though that Mypy cannot type narrow on comparisons of literal ints in tuples, like below:

from typing import Literal
from typing_extensions import assert_never

version: tuple[Literal[4], Literal[1]] = (4, 1)

if version >= (4, 1):
    reveal_type(version)
else:
    assert_never(version)

It doesn't work:

$ mypy example.py
example.py:7: note: Revealed type is "Tuple[Literal[4], Literal[1]]"
example.py:9: note: Revealed type is "Tuple[Literal[4], Literal[1]]"
example.py:10: error: Argument 1 to "assert_never" has incompatible type "Tuple[Literal[4], Literal[1]]"; expected "NoReturn"
Found 1 error in 1 file (checked 1 source file)

It seems this limitation also applies to literal ints in general:

from typing import Literal
from typing_extensions import assert_never

version: Literal[4] = 4

if version >= 4:
    reveal_type(version)
else:
    reveal_type(version)
    assert_never(version)

Perhaps the better approach is as you suggested, some alternative true/false constants. Perhaps we fix to the latest version but override that in the plugin based on the installed version of django that we find, or some config.

adamchainz avatar Sep 26 '22 10:09 adamchainz

sys.version_info check is hard-coded. This is why I thought about Literal[True] / Literal[False] hacks with analyzed dynamic types.

sobolevn avatar Sep 26 '22 10:09 sobolevn

Also found this related issue: https://github.com/python/typing/issues/693

Following on from Adam's example, I did note that == and != with a literal number do work as expected:

from typing import Literal
from typing_extensions import assert_never

version: Literal[4] = 4

if version == 4:
    reveal_type(version)
else:
    reveal_type(version)
    assert_never(version)

if version != 4:
    reveal_type(version)
    assert_never(version)
else:
    reveal_type(version)

if version == 4 or version == 5:
    reveal_type(version)
else:
    reveal_type(version)
    assert_never(version)

This yields the following:

❯ mypy test.py 
test.py:7: note: Revealed type is "Literal[4]"
test.py:16: note: Revealed type is "Literal[4]"
test.py:19: note: Revealed type is "Literal[4]"
Success: no issues found in 1 source file

So you could possibly construct something very verbose to achieve this by exposing major and minor versions as independent variables and thrashing out all combinations.

I'm currently looking into whether we can extend mypy to allow for checking additional things as well as sys.version_info when inferring condition values. It looks like it should be relatively straightforward to piggy back off the existing logic and provide a callback... I'll have to have a go.

ngnpope avatar Oct 31 '22 12:10 ngnpope

So you could possibly construct something very verbose to achieve this by exposing major and minor versions as independent variables and thrashing out all combinations.

We could also use a single numbers: 41 for Django 4.1, 42 for Django 4.2, etc.

I'm currently looking into whether we can extend mypy to allow for checking additional things as well as sys.version_info when inferring condition values. It looks like it should be relatively straightforward to piggy back off the existing logic and provide a callback... I'll have to have a go.

Sick

adamchainz avatar Oct 31 '22 14:10 adamchainz

Removing this completely means that older versions of django become unsupported.

Note that this is not as bad as it actually looks.

If you get your template via the django.template.loader.get_template() or get_template() method of the template engine, you still get a Template whose render() supports dict and None.

As noted in my other comment:

Oh, wait. There are two Template classes in Django

django.template.base.Template.render() doesn't support dict, but django.template.backends.django.Template.render() is a wrapper class that does support dict and None.

django.template.backends.django.DjangoTemplates.get_template() wraps the first Template in the latter wrapper class. Confusing!

intgr avatar Nov 04 '22 14:11 intgr

We could also use a single numbers: 41 for Django 4.1, 42 for Django 4.2, etc.

Yep! Or a NamedTuple if we really needed this specificity:

from typing import Literal, NamedTuple
from typing_extensions import assert_never


class Version(NamedTuple):
    major: Literal[4]
    minor: Literal[1]
    
version = Version(4, 1)

if version.major == 4:
    reveal_type(version.major)
else:
    reveal_type(version.major)
    assert_never(version)

if version.major != 4:
    reveal_type(version.major)
    assert_never(version)
else:
    reveal_type(version.major)

if version.major == 4 or version.major == 5:
    reveal_type(version.major)
else:
    reveal_type(version.major)
    assert_never(version)

christianbundy avatar Nov 08 '22 19:11 christianbundy

I opened an issue about conditional types based on Django version: #1244

adamchainz avatar Nov 09 '22 10:11 adamchainz

Let's merge it right after 1.13.1 release

sobolevn avatar Nov 09 '22 10:11 sobolevn

I haven't followed along completely but if I understand correctly, I could resolve the conflicts and this is still to be merged?

Majsvaffla avatar Jun 29 '23 13:06 Majsvaffla

Yes :)

sobolevn avatar Jun 29 '23 13:06 sobolevn

Thanks for the PR and sorry for being so super slow getting it merged

flaeppe avatar Apr 19 '24 11:04 flaeppe