Adds more types to patch

This commit is contained in:
sobolevn
2020-11-14 20:46:32 +03:00
parent 3e0f144148
commit 517ae648e5
11 changed files with 128 additions and 25 deletions

View File

@@ -42,6 +42,7 @@ We rely on different `django` and `mypy` versions:
| django-stubs | mypy version | django version | python version
| ------------ | ---- | ---- | ---- |
| 1.7.0 | 0.790 | 2.2.x \|\| 3.x | ^3.6
| 1.6.0 | 0.780 | 2.2.x \|\| 3.x | ^3.6
| 1.5.0 | 0.770 | 2.2.x \|\| 3.x | ^3.6
| 1.4.0 | 0.760 | 2.2.x \|\| 3.x | ^3.6
@@ -77,11 +78,13 @@ option to get extra information about the error.
### I cannot use QuerySet or Manager with type annotations
You can get a `TypeError: 'type' object is not subscriptable`
when you will try to use `QuerySet[MyModel]` or `Manager[MyModel]`.
when you will try to use `QuerySet[MyModel]`, `Manager[MyModel]` or some other Django-based Generic types.
This happens because Django classes do not support [`__class_getitem__`](https://www.python.org/dev/peps/pep-0560/#class-getitem) magic method.
This happens because these Django classes do not support [`__class_getitem__`](https://www.python.org/dev/peps/pep-0560/#class-getitem) magic method in runtime.
You can use strings instead: `'QuerySet[MyModel]'` and `'Manager[MyModel]'`, this way it will work as a type for `mypy` and as a regular `str` in runtime.
1. You can go with our
2. You can use strings instead: `'QuerySet[MyModel]'` and `'Manager[MyModel]'`, this way it will work as a type for `mypy` and as a regular `str` in runtime.
Currently we [are working](https://github.com/django/django/pull/12405) on providing `__class_getitem__` to the classes where we need them.

View File

@@ -1,5 +1,9 @@
import os
from django.core.management.base import BaseCommand as BaseCommand, CommandError as CommandError, CommandParser as CommandParser
from django.core.management.base import (
BaseCommand as BaseCommand,
CommandError as CommandError,
CommandParser as CommandParser,
)
from django.core.management.utils import find_command as find_command, popen_wrapper as popen_wrapper
from typing import List, Tuple, Union

View File

@@ -2,7 +2,14 @@ from django.conf import settings as settings
from django.core.cache import caches as caches
from django.core.cache.backends.db import BaseDatabaseCache as BaseDatabaseCache
from django.core.management.base import BaseCommand as BaseCommand, CommandError as CommandError
from django.db import DEFAULT_DB_ALIAS as DEFAULT_DB_ALIAS, DatabaseError as DatabaseError, connections as connections, models as models, router as router, transaction as transaction
from django.db import (
DEFAULT_DB_ALIAS as DEFAULT_DB_ALIAS,
DatabaseError as DatabaseError,
connections as connections,
models as models,
router as router,
transaction as transaction,
)
from typing import Any, List
class Command(BaseCommand):

View File

@@ -1,4 +1,8 @@
from django.core.management.base import BaseCommand as BaseCommand, CommandError as CommandError, CommandParser as CommandParser
from django.core.management.base import (
BaseCommand as BaseCommand,
CommandError as CommandError,
CommandParser as CommandParser,
)
from django.db import DEFAULT_DB_ALIAS as DEFAULT_DB_ALIAS, connections as connections
class Command(BaseCommand): ...

View File

@@ -4,5 +4,9 @@ from typing import Any, Callable, Dict, List
def module_to_dict(module: Any, omittable: Callable[[str], bool] = ...) -> Dict[str, str]: ...
class Command(BaseCommand):
def output_hash(self, user_settings: Dict[str, str], default_settings: Dict[str, str], **options: Any) -> List[str]: ...
def output_unified(self, user_settings: Dict[str, str], default_settings: Dict[str, str], **options: Any) -> List[str]: ...
def output_hash(
self, user_settings: Dict[str, str], default_settings: Dict[str, str], **options: Any
) -> List[str]: ...
def output_unified(
self, user_settings: Dict[str, str], default_settings: Dict[str, str], **options: Any
) -> List[str]: ...

View File

@@ -7,6 +7,10 @@ class Command(BaseCommand):
stealth_options: Tuple[str] = ...
db_module: str = ...
def handle_inspection(self, options: Dict[str, Any]) -> Iterable[str]: ...
def normalize_col_name(self, col_name: str, used_column_names: List[str], is_relation: bool) -> Tuple[str, Dict[str, str], List[str]]: ...
def normalize_col_name(
self, col_name: str, used_column_names: List[str], is_relation: bool
) -> Tuple[str, Dict[str, str], List[str]]: ...
def get_field_type(self, connection: Any, table_name: Any, row: Any) -> Tuple[str, Dict[str, str], List[str]]: ...
def get_meta(self, table_name: str, constraints: Any, column_to_field_name: Any, is_view: Any, is_partition: Any) -> List[str]: ...
def get_meta(
self, table_name: str, constraints: Any, column_to_field_name: Any, is_view: Any, is_partition: Any
) -> List[str]: ...

View File

@@ -1,11 +1,24 @@
from django.apps import apps as apps
from django.conf import settings as settings
from django.core.management.base import BaseCommand as BaseCommand, CommandError as CommandError, no_translations as no_translations
from django.db import DEFAULT_DB_ALIAS as DEFAULT_DB_ALIAS, OperationalError as OperationalError, connections as connections, router as router
from django.core.management.base import (
BaseCommand as BaseCommand,
CommandError as CommandError,
no_translations as no_translations,
)
from django.db import (
DEFAULT_DB_ALIAS as DEFAULT_DB_ALIAS,
OperationalError as OperationalError,
connections as connections,
router as router,
)
from django.db.migrations import Migration as Migration
from django.db.migrations.autodetector import MigrationAutodetector as MigrationAutodetector
from django.db.migrations.loader import MigrationLoader as MigrationLoader
from django.db.migrations.questioner import InteractiveMigrationQuestioner as InteractiveMigrationQuestioner, MigrationQuestioner as MigrationQuestioner, NonInteractiveMigrationQuestioner as NonInteractiveMigrationQuestioner
from django.db.migrations.questioner import (
InteractiveMigrationQuestioner as InteractiveMigrationQuestioner,
MigrationQuestioner as MigrationQuestioner,
NonInteractiveMigrationQuestioner as NonInteractiveMigrationQuestioner,
)
from django.db.migrations.state import ProjectState as ProjectState
from django.db.migrations.utils import get_migration_name_timestamp as get_migration_name_timestamp
from django.db.migrations.writer import MigrationWriter as MigrationWriter

View File

@@ -1,6 +1,13 @@
from django.apps import apps as apps
from django.core.management.base import BaseCommand as BaseCommand, CommandError as CommandError, no_translations as no_translations
from django.core.management.sql import emit_post_migrate_signal as emit_post_migrate_signal, emit_pre_migrate_signal as emit_pre_migrate_signal
from django.core.management.base import (
BaseCommand as BaseCommand,
CommandError as CommandError,
no_translations as no_translations,
)
from django.core.management.sql import (
emit_post_migrate_signal as emit_post_migrate_signal,
emit_pre_migrate_signal as emit_pre_migrate_signal,
)
from django.db import DEFAULT_DB_ALIAS as DEFAULT_DB_ALIAS, connections as connections, router as router
from django.db.migrations.autodetector import MigrationAutodetector as MigrationAutodetector
from django.db.migrations.executor import MigrationExecutor as MigrationExecutor

View File

@@ -3,7 +3,12 @@ from typing import Any, Dict, Optional, Sequence, Set, Tuple, Union
from django.db.migrations.migration import Migration
from django.db.migrations.state import ProjectState
from .exceptions import AmbiguityError as AmbiguityError, BadMigrationError as BadMigrationError, InconsistentMigrationHistory as InconsistentMigrationHistory, NodeNotFoundError as NodeNotFoundError
from .exceptions import (
AmbiguityError as AmbiguityError,
BadMigrationError as BadMigrationError,
InconsistentMigrationHistory as InconsistentMigrationHistory,
NodeNotFoundError as NodeNotFoundError,
)
MIGRATIONS_MODULE_NAME: str

View File

@@ -1,11 +1,14 @@
from typing import Any, Generic, List, Optional, Type, TypeVar
from typing import Any, Generic, List, Optional, Tuple, Type, TypeVar
import django
from django.contrib.admin import ModelAdmin
from django.contrib.admin.options import BaseModelAdmin
from django.db.models.manager import BaseManager
from django.db.models.query import QuerySet
from django.views.generic.edit import FormMixin
_T = TypeVar("_T")
_VersionSpec = Tuple[int, int]
class MPGeneric(Generic[_T]):
@@ -21,11 +24,15 @@ class MPGeneric(Generic[_T]):
version: Optional[int]
cls: Type[_T]
def __init__(self, cls: Type[_T], version: Optional[int] = None):
def __init__(self, cls: Type[_T], version: Optional[_VersionSpec] = None):
"""Set the data fields, basic constructor."""
self.version = version
self.cls = cls
def __repr__(self) -> str:
"""Better representation in tests and debug."""
return "<MPGeneric: {0}, versions={1}>".format(self.cls, self.version or "all")
# certain django classes need to be generic, but lack the __class_getitem__ dunder needed to
# annotate them: https://github.com/typeddjango/django-stubs/issues/507
@@ -34,13 +41,20 @@ _need_generic: List[MPGeneric[Any]] = [
MPGeneric(ModelAdmin),
MPGeneric(FormMixin),
MPGeneric(BaseModelAdmin),
# These types do have native `__class_getitem__` method since django 3.1:
MPGeneric(QuerySet, (3, 1)),
MPGeneric(BaseManager, (3, 1)),
]
# currently just adds the __class_getitem__ dunder. if more monkeypatching is needed, add it here
def monkeypatch() -> None:
"""Monkey patch django as necessary to work properly with mypy."""
for el in filter(lambda x: django.VERSION[0] == x.version or x.version is None, _need_generic):
suited_for_this_version = filter(
spec.version is None or django.VERSION[:2] <= spec.version,
_need_generic,
)
for el in suited_for_this_version:
el.cls.__class_getitem__ = classmethod(lambda cls, *args, **kwargs: cls)

View File

@@ -1,11 +1,49 @@
import django_stubs_ext
from django_stubs_ext.monkeypatch import _need_generic
import pytest
import django_stubs_ext
from django_stubs_ext.monkeypatch import _need_generic, _VersionSpec, django
@pytest.fixture(scope="function")
def make_generic_classes(request, monkeypatch):
def fin():
for el in _need_generic:
delattr(el.cls, "__class_getitem__")
def factory(django_version=None):
if django_version is not None:
monkeypatch.setattr(django, "VERSION", django_version)
django_stubs_ext.monkeypatch()
request.addfinalizer(fin)
return factory
def test_patched_generics() -> None:
def test_patched_generics(make_generic_classes) -> None:
"""Test that the generics actually get patched."""
make_generic_classes()
for el in _need_generic:
# This only throws an exception if the monkeypatch failed
assert el.cls[type] == el.cls # `type` is arbitrary
if el.version is None:
assert el.cls[type] is el.cls # `type` is arbitrary
@pytest.mark.parametrize(
"django_version",
[
(2, 2),
(3, 0),
(3, 1),
(3, 2),
],
)
def test_patched_version_specific(
django_version: _VersionSpec,
make_generic_classes,
) -> None:
"""Test version speicific types."""
make_generic_classes(django_version)
for el in _need_generic:
if el.version is not None and el.version[:2] <= django_version:
assert el.cls[int] is el.cls