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 | 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.6.0 | 0.780 | 2.2.x \|\| 3.x | ^3.6
| 1.5.0 | 0.770 | 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 | 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 ### I cannot use QuerySet or Manager with type annotations
You can get a `TypeError: 'type' object is not subscriptable` 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. 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 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 django.core.management.utils import find_command as find_command, popen_wrapper as popen_wrapper
from typing import List, Tuple, Union 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 import caches as caches
from django.core.cache.backends.db import BaseDatabaseCache as BaseDatabaseCache from django.core.cache.backends.db import BaseDatabaseCache as BaseDatabaseCache
from django.core.management.base import BaseCommand as BaseCommand, CommandError as CommandError 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 from typing import Any, List
class Command(BaseCommand): 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 from django.db import DEFAULT_DB_ALIAS as DEFAULT_DB_ALIAS, connections as connections
class Command(BaseCommand): ... 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]: ... def module_to_dict(module: Any, omittable: Callable[[str], bool] = ...) -> Dict[str, str]: ...
class Command(BaseCommand): class Command(BaseCommand):
def output_hash(self, user_settings: Dict[str, str], default_settings: Dict[str, str], **options: Any) -> List[str]: ... def output_hash(
def output_unified(self, user_settings: Dict[str, str], default_settings: Dict[str, str], **options: Any) -> List[str]: ... 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] = ... stealth_options: Tuple[str] = ...
db_module: str = ... db_module: str = ...
def handle_inspection(self, options: Dict[str, Any]) -> Iterable[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_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.apps import apps as apps
from django.conf import settings as settings 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.core.management.base import (
from django.db import DEFAULT_DB_ALIAS as DEFAULT_DB_ALIAS, OperationalError as OperationalError, connections as connections, router as router 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 import Migration as Migration
from django.db.migrations.autodetector import MigrationAutodetector as MigrationAutodetector from django.db.migrations.autodetector import MigrationAutodetector as MigrationAutodetector
from django.db.migrations.loader import MigrationLoader as MigrationLoader 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.state import ProjectState as ProjectState
from django.db.migrations.utils import get_migration_name_timestamp as get_migration_name_timestamp from django.db.migrations.utils import get_migration_name_timestamp as get_migration_name_timestamp
from django.db.migrations.writer import MigrationWriter as MigrationWriter from django.db.migrations.writer import MigrationWriter as MigrationWriter

View File

@@ -1,6 +1,13 @@
from django.apps import apps as apps 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.base import (
from django.core.management.sql import emit_post_migrate_signal as emit_post_migrate_signal, emit_pre_migrate_signal as emit_pre_migrate_signal 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 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.autodetector import MigrationAutodetector as MigrationAutodetector
from django.db.migrations.executor import MigrationExecutor as MigrationExecutor 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.migration import Migration
from django.db.migrations.state import ProjectState 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 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 import django
from django.contrib.admin import ModelAdmin from django.contrib.admin import ModelAdmin
from django.contrib.admin.options import BaseModelAdmin 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 from django.views.generic.edit import FormMixin
_T = TypeVar("_T") _T = TypeVar("_T")
_VersionSpec = Tuple[int, int]
class MPGeneric(Generic[_T]): class MPGeneric(Generic[_T]):
@@ -21,11 +24,15 @@ class MPGeneric(Generic[_T]):
version: Optional[int] version: Optional[int]
cls: Type[_T] 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.""" """Set the data fields, basic constructor."""
self.version = version self.version = version
self.cls = cls 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 # 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 # annotate them: https://github.com/typeddjango/django-stubs/issues/507
@@ -34,13 +41,20 @@ _need_generic: List[MPGeneric[Any]] = [
MPGeneric(ModelAdmin), MPGeneric(ModelAdmin),
MPGeneric(FormMixin), MPGeneric(FormMixin),
MPGeneric(BaseModelAdmin), 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 # currently just adds the __class_getitem__ dunder. if more monkeypatching is needed, add it here
def monkeypatch() -> None: def monkeypatch() -> None:
"""Monkey patch django as necessary to work properly with mypy.""" """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) el.cls.__class_getitem__ = classmethod(lambda cls, *args, **kwargs: cls)

View File

@@ -1,11 +1,49 @@
import django_stubs_ext import pytest
from django_stubs_ext.monkeypatch import _need_generic
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() 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.""" """Test that the generics actually get patched."""
make_generic_classes()
for el in _need_generic: for el in _need_generic:
# This only throws an exception if the monkeypatch failed if el.version is None:
assert el.cls[type] == el.cls # `type` is arbitrary 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