From f11e2344df0c8d54839b17ed0713b9a8564d80fb Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Sun, 6 Oct 2019 22:21:34 +0300 Subject: [PATCH 1/5] very very first draft of DEP for static typechecking of Django --- draft/0484-type-hints.rst | 140 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 draft/0484-type-hints.rst diff --git a/draft/0484-type-hints.rst b/draft/0484-type-hints.rst new file mode 100644 index 00000000..fa77b357 --- /dev/null +++ b/draft/0484-type-hints.rst @@ -0,0 +1,140 @@ +## Abstract + +Add mypy (and other type checker) support for Django. + + +## Specification + +Currently, there are two ways to accomplish that: +1. Provide type annotations right in the Django codebase. + +Pros: +* much more accurate types +* easier to keep them in sync with the code changes +* additional value for typechecking of the codebase itself + +Cons: +* clutter of the codebase + +Django is very dynamic, so some functions have a lot of different signatures, which could not be expressed in the codebase and require `@overload` clauses +https://www.python.org/dev/peps/pep-0484/#function-method-overloading + +An example would be a `Field` - it should behave different whether it's invoked on model class, or model instance. Class returns `Field` object itself, and instance resolve field into an underlying python object +```python +# _ST - set type +# _GT - get type +# self: _T -> _T allows mypy to extract type of `self` and return it + +class Field(Generic[_ST, _GT]) + @overload + def __get__(self: _T, instance: None, owner) -> _T: ... + # Model instance access + @overload + def __get__(self, instance: Model, owner) -> _GT: ... + # non-Model instances + @overload + def __get__(self: _T, instance, owner) -> _T: ... +``` + +2. Store type stubs separately. +https://www.python.org/dev/peps/pep-0484/#stub-files + + Pros: + * non-invasive change, could be stored / installed as a separate package + + Cons: + * Out-of-sync with Django itself + + * Hard to test. Mypy (as of now) can't use stub definitions to typecheck codebase itself. There are some solutions to this problem, like + https://github.com/ambv/retype + https://github.com/google/pytype/tree/master/pytype/tools/merge_pyi + + but I've miserably failed in making them work for django-stubs and Django, there's a lot of things to consider. There's also a possibility of writing our own solution, shouldn't be too hard. + + django-stubs uses Django test suite to test stubs right now. + + +## How django-stubs currently implemented + +`django-stubs` uses a mix of static analysis provided by mypy, and runtime type inference from Django own introspection facilities. + For example, newly introduced typechecking of `QuerySet.filter` uses Django _meta API to extract possible lookups for every field, to resolve kwargs like `name__iexact`. + + +## Current issues and limitations of django-stubs + +1. Generic parameters of `QuerySet`. + + For example, we have a model + ```python + class User: + name = models.CharField() + ``` + + 1. A simple `QuerySet` which is a result of `User.objects.filter()` returns `QuerySet[User]`. + + 2. When we add `values_list('name')` method to the picture, we need to remember (and encode in the generic params) both the fact that it's a `QuerySet` of the `User` model, and that the return item will be a tuple object of `name`. + So, it becomes `QuerySet[User, Tuple[str]]`. + + 3. To implement `.annotate(upper=Upper('name'))` we need to remember all the fields that created from `annotate`, so it becomes + `QuerySet[User, Tuple[str], TypedDict('upper': str)]` + +2. Manager inheritance. + + ```python + class BaseUser(models.Model): + class Meta: + abstract = True + + objects = BaseUserManager() + + class User(BaseUser): + objects = UserManager() + ``` + Mypy will flag those `objects` managers as incompatible as they violate Liskov Substitution principle. + +3. Generic parameters for `Field` + + ```python + class User: + name = models.CharField() + surname = models.CharField(null=True) + ``` + + `name` and `surname` props are recognized by mypy as generic descriptors. Here's the stub for the `Field` + + ```python + class Field(Generic[_ST, _GT]): + def __set__(self, instance, value: _ST) -> None: ... + # class access + @overload + def __get__(self: _T, instance: None, owner) -> _T: ... + # Model instance access + @overload + def __get__(self, instance: Model, owner) -> _GT: ... + # non-Model instances + @overload + def __get__(self: _T, instance, owner) -> _T: ... + + class CharField(Field[_ST, _GT]): + _pyi_private_set_type: Union[str, int, Combinable] + _pyi_private_get_type: str + ``` + + In the plugin `django-stubs` dynamically marks `name` and `surname` as `CharField[Optional[Union[str, int, Combinable]], Optional[str]]`. We cannot use (as far as I know), + + ```python + class CharField(Field[Union[str, int, Combinable], str]): + pass + ``` + because then we won't be able to change generic params for `CharField` dynamically. + + And it also creates a UX issue, as `Field` has two generic params which makes zero sense semantically. + +4. `BaseManager.from_queryset()`, `QuerySet.as_manager()` + + Not implementable as of now, see + https://github.com/python/mypy/issues/2813 + https://github.com/python/mypy/issues/7266 + + + From 662e3ef3ca7c30ec5c7478753e1161c50b0b9897 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Sun, 6 Oct 2019 22:48:23 +0300 Subject: [PATCH 2/5] add approximate list of features of django-stubs --- draft/0484-type-hints.rst | 75 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/draft/0484-type-hints.rst b/draft/0484-type-hints.rst index fa77b357..221a8f72 100644 --- a/draft/0484-type-hints.rst +++ b/draft/0484-type-hints.rst @@ -59,6 +59,81 @@ https://www.python.org/dev/peps/pep-0484/#stub-files `django-stubs` uses a mix of static analysis provided by mypy, and runtime type inference from Django own introspection facilities. For example, newly introduced typechecking of `QuerySet.filter` uses Django _meta API to extract possible lookups for every field, to resolve kwargs like `name__iexact`. + ## What is currently implemented (and therefore possible) + +1. Fields inference. + + ```python + class User(models.Model): + name = models.CharField() + surname = models.CharField(null=True) + + user = User() + user.name # inferred type: str + user.surname # inferred type: Optional[str] + + # objects is added to every model + User.objects.get() # inferred type: User + User.objects.filter(unknown=True) # will fail with "no such field" + User.objects.filter(name=True) # will fail with "incompatible types 'bool' and 'str'" + User.objects.filter(name__iexact=True) # will fail with "incompatible types 'bool' and 'str'" + User.objects.filter(name='hello') # passes + User.objects.filter(name__iexact='hello') # passes + ``` + +2. Typechecking for `__init__` and `create()` + ```python + class User(models.Model): + name = models.CharField() + User(name=1) # fail + User(unknown=1) # fail + User(name='hello') # pass + ``` + same for `create()` with different `Optional`ity conditions. + + +3. RelatedField's support, support for different apps in the RelatedField's to= argument + + ```python + class User: + pass + class Profile: + user = models.OneToOneField(to=User, related_name='profile') + + Profile().user # inferred type 'User' + User().profile # inferred type 'Profile' + ``` + + ```python + class CustomProfile: + user = models.ForeignKey(to='some_custom_app.User') + CustomProfile().user # will be correctly inferred as 'some_custom_app.User' + ``` + +4. Support for unannotated third-party base models, + ```python + class User(ThirdPartyModel): + pass + ``` + will be recognized as correct model. + +5. `values`, `values_list` support + + ```python + class User: + name = models.CharField() + surname = models.CharField() + User.objects.values_list('name', 'surname')[0] # will return Tuple[str, str] + ``` + +6. settings support + ```python + from django.conf import settings + settings.INSTALLED_APPS # will be inferred as Sequence[str] + ``` + +7. `get_user_model()` infers current model class + ## Current issues and limitations of django-stubs From 99663dee5c90e8c9efffffd82a69b87d07e0017f Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Tue, 8 Oct 2019 20:46:22 +0300 Subject: [PATCH 3/5] align with DEP template, add proper migration path and some info of how to use django-stubs and inline annotations together --- draft/0484-type-hints.rst | 98 +++++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 30 deletions(-) diff --git a/draft/0484-type-hints.rst b/draft/0484-type-hints.rst index 221a8f72..9a20ebc2 100644 --- a/draft/0484-type-hints.rst +++ b/draft/0484-type-hints.rst @@ -1,3 +1,16 @@ +# DEP 0484: Static type checking for Django + +| | | +| --- | --- | +| **DEP:** | 0484 | +| **Author:** | Maksim Kurnikov | +| **Implementation team:** | Maksim Kurnikov | +| **Shepherd:** | Carlton Gibson | +| **Type:** | Feature | +| **Status:** | Draft | +| **Created:** | 2019-10-08 | +| **Last modified:** | 2019-10-08 | + ## Abstract Add mypy (and other type checker) support for Django. @@ -5,16 +18,60 @@ Add mypy (and other type checker) support for Django. ## Specification -Currently, there are two ways to accomplish that: -1. Provide type annotations right in the Django codebase. +I propose to add type hinting support for Django via mypy and PEP484. All at once it's too big of a change, so I want to propose an incremental migration, using both stub files and inline type annotations. + +https://www.python.org/dev/peps/pep-0484/#stub-files + +Back in a day, there was some friction about gradually improving the type checking coverage of different parts of Python ecosystem. So PEP561 was accepted based on the discussion. + +It defines how PEP484-based typecheckers would look for a type annotations information across the different places. + +https://www.python.org/dev/peps/pep-0561 + +Specifically, it defines a "Type Checker Method Resolution Order" +https://www.python.org/dev/peps/pep-0561/#type-checker-module-resolution-order + +> 1. Stubs or Python source manually put in the beginning of the path. Type checkers SHOULD provide this to allow the user complete control of which stubs to use, and to patch broken stubs/inline types from packages. In mypy the $MYPYPATH environment variable can be used for this. +> 2. User code - the files the type checker is running on. +> 3. Stub packages - these packages SHOULD supersede any installed inline package. They can be found at foopkg-stubs for package foopkg. +> 4. Inline packages - if there is nothing overriding the installed package, and it opts into type checking, inline types SHOULD be used. +> 5. Typeshed (if used) - Provides the stdlib types and several third party libraries. + +What is means for Django, it that we can split type annotations into stub files, and inline annotations. Where there will be a corresponding `.pyi` file, mypy would use that, otherwise fallback to inline type annotations. + +There's an existing `django-stubs` package where most of the Django codebase files have a `.pyi` counterpart with type annotations. + +https://github.com/typeddjango/django-stubs + +It also has some plugin code, which takes care of the dynamic nature of Django models. + +It's desirable that this package would be usable alongside the Django type annotations migration. + + +### Incremental migration path: +1. Add `py.typed` file inside the Django top-level module, to mark that it has inline annotations. +See https://www.python.org/dev/peps/pep-0561/#packaging-type-information + +2. Add `__class_getitem__` implementation for the `QuerySet` class to support generic instantiation. -Pros: -* much more accurate types -* easier to keep them in sync with the code changes -* additional value for typechecking of the codebase itself +3. Decide on file-by-file based, whether it's appropriate to have inline type annotation, or have it separate for the sake of readability. For those files, merge annotations from `django-stubs`, removing those files in the library. -Cons: -* clutter of the codebase +4. Adopt `django-stubs` as an official Django library to catch more bugs, push users a bit more towards type annotations and prepare them for a change. + +5. Do some work on a `merge-pyi` side to make it complete enough for `django-stubs` and Django. For that, we can react out for mypy folks and work with them. + +6. Add stubs checking CI step: + 1. Use `merge-pyi` to merge `django-stubs` into the Django codebase. + 2. Run `mypy` and report errors. + + This would allow us to keep `django-stubs` in sync with Django codebase, and prevent false-positives to happen. + +7. Based on gained experience, merge more stubs into the codebase. + + +## Notes + +### Overload clutter Django is very dynamic, so some functions have a lot of different signatures, which could not be expressed in the codebase and require `@overload` clauses https://www.python.org/dev/peps/pep-0484/#function-method-overloading @@ -36,30 +93,13 @@ class Field(Generic[_ST, _GT]) def __get__(self: _T, instance, owner) -> _T: ... ``` -2. Store type stubs separately. -https://www.python.org/dev/peps/pep-0484/#stub-files - - Pros: - * non-invasive change, could be stored / installed as a separate package - - Cons: - * Out-of-sync with Django itself - - * Hard to test. Mypy (as of now) can't use stub definitions to typecheck codebase itself. There are some solutions to this problem, like - https://github.com/ambv/retype - https://github.com/google/pytype/tree/master/pytype/tools/merge_pyi - - but I've miserably failed in making them work for django-stubs and Django, there's a lot of things to consider. There's also a possibility of writing our own solution, shouldn't be too hard. - django-stubs uses Django test suite to test stubs right now. - - -## How django-stubs currently implemented +### How django-stubs currently implemented `django-stubs` uses a mix of static analysis provided by mypy, and runtime type inference from Django own introspection facilities. For example, newly introduced typechecking of `QuerySet.filter` uses Django _meta API to extract possible lookups for every field, to resolve kwargs like `name__iexact`. - ## What is currently implemented (and therefore possible) + ### What is currently implemented (and therefore possible) 1. Fields inference. @@ -135,7 +175,7 @@ https://www.python.org/dev/peps/pep-0484/#stub-files 7. `get_user_model()` infers current model class -## Current issues and limitations of django-stubs +### Current issues and limitations of django-stubs 1. Generic parameters of `QuerySet`. @@ -211,5 +251,3 @@ https://www.python.org/dev/peps/pep-0484/#stub-files https://github.com/python/mypy/issues/2813 https://github.com/python/mypy/issues/7266 - - From 8cddafafd78e7a6ecd9921f4fc46f77307ad6afb Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Tue, 8 Oct 2019 20:47:33 +0300 Subject: [PATCH 4/5] change it to markdown to be inline renderable --- draft/{0484-type-hints.rst => 0484-type-hints.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename draft/{0484-type-hints.rst => 0484-type-hints.md} (100%) diff --git a/draft/0484-type-hints.rst b/draft/0484-type-hints.md similarity index 100% rename from draft/0484-type-hints.rst rename to draft/0484-type-hints.md From 2253703d279fd672b5bd1e1b4d9004ff92394e17 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Sun, 13 Oct 2019 00:11:30 +0300 Subject: [PATCH 5/5] some motivation points, fix some example, change migration path a bit --- draft/0484-type-hints.md | 259 +++++++++++++++++++++++++-------------- 1 file changed, 166 insertions(+), 93 deletions(-) diff --git a/draft/0484-type-hints.md b/draft/0484-type-hints.md index 9a20ebc2..98357e5a 100644 --- a/draft/0484-type-hints.md +++ b/draft/0484-type-hints.md @@ -5,7 +5,7 @@ | **DEP:** | 0484 | | **Author:** | Maksim Kurnikov | | **Implementation team:** | Maksim Kurnikov | -| **Shepherd:** | Carlton Gibson | +| **Shepherd:** | Carlton Gibson | | **Type:** | Feature | | **Status:** | Draft | | **Created:** | 2019-10-08 | @@ -13,95 +13,182 @@ ## Abstract -Add mypy (and other type checker) support for Django. +Add mypy (and other type checkers) support for Django. +## Motivation -## Specification +### Internal use -I propose to add type hinting support for Django via mypy and PEP484. All at once it's too big of a change, so I want to propose an incremental migration, using both stub files and inline type annotations. +1. Inline documentation + * Lower barrier to entry. + * Easier to think about what's going on and edgecases -https://www.python.org/dev/peps/pep-0484/#stub-files +2. Catching hard-to-find bugs + * not all codebase is covered with tests, so `None` related functionality is never tested, because nobody actually thought about it being used that way. + +3. Another testsuite to prevent regressions + * accidental changes break other parts of the codebase + * easier review, easier to understand what contributor meant to do + +### External use + +1. Typechecking of user codebases. + + * mypy is increasingly popular in proprietary projects. + + * shorten time for new developer to get up to speed with the codebase. + * prevent bugs / regressions, basically another test suite. + + * add scalability to the codebase -Back in a day, there was some friction about gradually improving the type checking coverage of different parts of Python ecosystem. So PEP561 was accepted based on the discussion. + * third-party apps that use / modify Django internal tools would benefit from typechecking -It defines how PEP484-based typecheckers would look for a type annotations information across the different places. +### IDE support -https://www.python.org/dev/peps/pep-0561 +* go to definition, find usages, refactorings. -Specifically, it defines a "Type Checker Method Resolution Order" -https://www.python.org/dev/peps/pep-0561/#type-checker-module-resolution-order +Example: -> 1. Stubs or Python source manually put in the beginning of the path. Type checkers SHOULD provide this to allow the user complete control of which stubs to use, and to patch broken stubs/inline types from packages. In mypy the $MYPYPATH environment variable can be used for this. -> 2. User code - the files the type checker is running on. -> 3. Stub packages - these packages SHOULD supersede any installed inline package. They can be found at foopkg-stubs for package foopkg. -> 4. Inline packages - if there is nothing overriding the installed package, and it opts into type checking, inline types SHOULD be used. -> 5. Typeshed (if used) - Provides the stdlib types and several third party libraries. +```python +class User(models.Model): + name = models.CharField() -What is means for Django, it that we can split type annotations into stub files, and inline annotations. Where there will be a corresponding `.pyi` file, mypy would use that, otherwise fallback to inline type annotations. +# other file +def get_username(user): + return user.name +``` -There's an existing `django-stubs` package where most of the Django codebase files have a `.pyi` counterpart with type annotations. +Rename `name` -> `username`. Any IDE will have a hard time understanding that `user` param is a `User` instance. Trivial with type annotation. -https://github.com/typeddjango/django-stubs +```python +def get_username(user: User): + return user.name # will be renamed into user.username +``` + +* interactive typechecking of passed params and return values -It also has some plugin code, which takes care of the dynamic nature of Django models. +* support in vscode/emacs/vim (microsoft's python-language-server), PyCharm (internal implementation of the typechecker), pyre, mypy -It's desirable that this package would be usable alongside the Django type annotations migration. +* inline annotations -> IDE is always up to date with code changes (no `typeshed` syncing) +## Implementation -### Incremental migration path: -1. Add `py.typed` file inside the Django top-level module, to mark that it has inline annotations. +### Migration path + +1. Add `py.typed` file inside the Django top-level module, to mark that it has inline annotations. See https://www.python.org/dev/peps/pep-0561/#packaging-type-information -2. Add `__class_getitem__` implementation for the `QuerySet` class to support generic instantiation. +2. Add `__class_getitem__` implementation to classes which need to support generic parameters. For example, for `QuerySet` class (`QuerySet[MyModel]` annotation) +https://docs.python.org/3/reference/datamodel.html#emulating-generic-types -3. Decide on file-by-file based, whether it's appropriate to have inline type annotation, or have it separate for the sake of readability. For those files, merge annotations from `django-stubs`, removing those files in the library. + It's just -4. Adopt `django-stubs` as an official Django library to catch more bugs, push users a bit more towards type annotations and prepare them for a change. + ```python + def __class_getitem__(cls, *args, **kwargs): + return cls + ``` + + (some additional parameters checking could be added later) -5. Do some work on a `merge-pyi` side to make it complete enough for `django-stubs` and Django. For that, we can react out for mypy folks and work with them. +3. Add `mypy` to CI, like it's done in this PR for DRF +https://github.com/encode/django-rest-framework/pull/6988 -6. Add stubs checking CI step: - 1. Use `merge-pyi` to merge `django-stubs` into the Django codebase. - 2. Run `mypy` and report errors. + Make it pass. It would require some type annotations around the codebase, some cases should be silenced via `# type: ignore` + https://mypy.readthedocs.io/en/latest/common_issues.html#spurious-errors-and-locally-silencing-the-checker - This would allow us to keep `django-stubs` in sync with Django codebase, and prevent false-positives to happen. +4. Merge `django-stubs` annotations into the codebase. This one could be done incrementally, on file-per-file basis. -7. Based on gained experience, merge more stubs into the codebase. +5. For complex cases use `.pyi` counterpart (described below). To be able to remain in sync with the codebase, `merge-pyi` tool invocation must be added to the CI. +https://github.com/google/pytype/tree/master/pytype/tools/merge_pyi -## Notes +### Complementing with `.pyi` stub files for complex cases -### Overload clutter +There are some cases, for which stores type information inline creates more problems than benefits. -Django is very dynamic, so some functions have a lot of different signatures, which could not be expressed in the codebase and require `@overload` clauses +Django is very dynamic, some functions have more than on relationship between argument types and return value type. For those cases, there's an `@overload` clause available https://www.python.org/dev/peps/pep-0484/#function-method-overloading -An example would be a `Field` - it should behave different whether it's invoked on model class, or model instance. Class returns `Field` object itself, and instance resolve field into an underlying python object -```python -# _ST - set type -# _GT - get type -# self: _T -> _T allows mypy to extract type of `self` and return it - -class Field(Generic[_ST, _GT]) - @overload - def __get__(self: _T, instance: None, owner) -> _T: ... - # Model instance access - @overload - def __get__(self, instance: Model, owner) -> _GT: ... - # non-Model instances - @overload - def __get__(self: _T, instance, owner) -> _T: ... -``` +For these cases, type information should be stored in the separate stub file. +https://www.python.org/dev/peps/pep-0484/#stub-files +Examples: -### How django-stubs currently implemented +* `@overload` clauses -`django-stubs` uses a mix of static analysis provided by mypy, and runtime type inference from Django own introspection facilities. - For example, newly introduced typechecking of `QuerySet.filter` uses Django _meta API to extract possible lookups for every field, to resolve kwargs like `name__iexact`. + Django is very dynamic, some functions have more than one relationship between argument types and return value type. For those cases, there's an `@overload` clause available + https://www.python.org/dev/peps/pep-0484/#function-method-overloading - ### What is currently implemented (and therefore possible) + Example is `__get__` method of the `Field`. + 1. Returns underlying python type when called on instance. + 2. Returns `Field` instance when called on class. -1. Fields inference. + ```python + # _ST - set type + # _GT - get type + # self: _T -> _T allows mypy to extract type of `self` and return it + + class Field(Generic[_ST, _GT]) + @overload + def __get__(self: _T, instance: None, owner) -> _T: ... + # Model instance access + @overload + def __get__(self, instance: Model, owner) -> _GT: ... + # non-Model instances + @overload + def __get__(self: _T, instance, owner) -> _T: ... + ``` + +* `**kwargs` for `Field` classes + + Mypy (and other typecheckers) doesn't understand `**kwargs`. There are ways to make it work via `TypedDict` and type aliases, but it's hard to read. + + ```python + class CharField(Field): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.validators.append(validators.MaxLengthValidator(self.max_length)) + ``` + + How it would look like, if it would be inline: + + ```python + class CharField(Field): + def __init__( + self, + verbose_name: Optional[Union[str, bytes]] = ..., + name: Optional[str] = ..., + primary_key: bool = ..., + max_length: Optional[int] = ..., + unique: bool = ..., + blank: bool = ..., + null: bool = ..., + db_index: bool = ..., + default: Any = ..., + editable: bool = ..., + auto_created: bool = ..., + serialize: bool = ..., + unique_for_date: Optional[str] = ..., + unique_for_month: Optional[str] = ..., + unique_for_year: Optional[str] = ..., + choices: Optional[_FieldChoices] = ..., + help_text: str = ..., + db_column: Optional[str] = ..., + db_tablespace: Optional[str] = ..., + validators: Iterable[_ValidatorCallable] = ..., + error_messages: Optional[_ErrorMessagesToOverride] = ..., + ): + super().__init__(*args, **kwargs) + self.validators.append(validators.MaxLengthValidator(self.max_length)) + ``` + +### Mypy plugin support + +Not all Django behavious expressable via type system - Django will have to provide those features via mypy plugin. Most of those are implemented in `django-stubs`, inside `mypy_django_plugin` sub-package. + +Those are features implemented so far in the `mypy_django_plugin`: + +1. Fields and managers inference. ```python class User(models.Model): @@ -129,10 +216,9 @@ class Field(Generic[_ST, _GT]) User(unknown=1) # fail User(name='hello') # pass ``` - same for `create()` with different `Optional`ity conditions. - + same for `create()` with different `Optional`ity conditions. -3. RelatedField's support, support for different apps in the RelatedField's to= argument +3. RelatedField's support, support for different apps in the RelatedField's `to=` argument ```python class User: @@ -147,53 +233,36 @@ class Field(Generic[_ST, _GT]) ```python class CustomProfile: user = models.ForeignKey(to='some_custom_app.User') - CustomProfile().user # will be correctly inferred as 'some_custom_app.User' + CustomProfile().user # will be correctly inferred as 'some_custom_app.models.User' ``` -4. Support for unannotated third-party base models, +4. Support for unannotated third-party base models, ```python class User(ThirdPartyModel): pass ``` - will be recognized as correct model. - -5. `values`, `values_list` support - - ```python - class User: - name = models.CharField() - surname = models.CharField() - User.objects.values_list('name', 'surname')[0] # will return Tuple[str, str] - ``` + will be recognized as correct model. 6. settings support ```python from django.conf import settings - settings.INSTALLED_APPS # will be inferred as Sequence[str] + settings.ALLOWED_HOSTS # inferred as Sequence[str] ``` -7. `get_user_model()` infers current model class +7. `get_user_model()` correctly infers current user model class. -### Current issues and limitations of django-stubs +### Limitations of the plugin -1. Generic parameters of `QuerySet`. +0. `mypy_django_plugin` uses a mix of static and dynamic analysis to support Django features. It basically does `django.setup()` inside to gain access to all the _meta API and `Apps` information. - For example, we have a model - ```python - class User: - name = models.CharField() - ``` + * users needs to be aware of that, and work on preventing side-effects of `django.setup()`, if there's any - 1. A simple `QuerySet` which is a result of `User.objects.filter()` returns `QuerySet[User]`. + * typechecking of Django app with invalid syntax or semantics will crash the plugin - 2. When we add `values_list('name')` method to the picture, we need to remember (and encode in the generic params) both the fact that it's a `QuerySet` of the `User` model, and that the return item will be a tuple object of `name`. - So, it becomes `QuerySet[User, Tuple[str]]`. + Possible solution: reimplement all Django logic of traversing models in apps for the plugin. - 3. To implement `.annotate(upper=Upper('name'))` we need to remember all the fields that created from `annotate`, so it becomes - `QuerySet[User, Tuple[str], TypedDict('upper': str)]` - -2. Manager inheritance. +1. Manager inheritance. ```python class BaseUser(models.Model): @@ -205,9 +274,11 @@ class Field(Generic[_ST, _GT]) class User(BaseUser): objects = UserManager() ``` - Mypy will flag those `objects` managers as incompatible as they violate Liskov Substitution principle. + Mypy will flag those `objects` managers as incompatible as they violate Liskov Substitution principle. + + Possible solution: https://github.com/python/mypy/issues/7468 -3. Generic parameters for `Field` +2. Generic parameters for `Field` ```python class User: @@ -235,19 +306,21 @@ class Field(Generic[_ST, _GT]) _pyi_private_get_type: str ``` - In the plugin `django-stubs` dynamically marks `name` and `surname` as `CharField[Optional[Union[str, int, Combinable]], Optional[str]]`. We cannot use (as far as I know), + In the plugin `django-stubs` dynamically marks `name` and `surname` as `CharField[Optional[Union[str, int, Combinable]], Optional[str]]`. We cannot use (as far as I know), ```python class CharField(Field[Union[str, int, Combinable], str]): pass ``` - because then we won't be able to change generic params for `CharField` dynamically. + because then we won't be able to change generic params for `CharField` dynamically. + + And it also creates a UX issue, as `Field` has two generic params which makes zero sense semantically. - And it also creates a UX issue, as `Field` has two generic params which makes zero sense semantically. + Possible solution: whole bunch of `@overload` statements over `__new__` method. -4. `BaseManager.from_queryset()`, `QuerySet.as_manager()` +3. `BaseManager.from_queryset()`, `QuerySet.as_manager()` - Not implementable as of now, see + Not implementable as of now, see https://github.com/python/mypy/issues/2813 https://github.com/python/mypy/issues/7266