fix couple edge cases with __init__

This commit is contained in:
Maxim Kurnikov
2019-02-10 04:32:27 +03:00
parent 5f6f597266
commit 6b7507206a
8 changed files with 172 additions and 71 deletions

View File

@@ -7,19 +7,14 @@ from django.db import models
SITE_CACHE: Any SITE_CACHE: Any
class SiteManager(models.Manager): class SiteManager(models.Manager):
creation_counter: int
model: None
name: None
use_in_migrations: bool = ...
def get_current(self, request: Optional[HttpRequest] = ...) -> Site: ... def get_current(self, request: Optional[HttpRequest] = ...) -> Site: ...
def clear_cache(self) -> None: ... def clear_cache(self) -> None: ...
def get_by_natural_key(self, domain: str) -> Site: ... def get_by_natural_key(self, domain: str) -> Site: ...
class Site(models.Model): class Site(models.Model):
id: int domain: models.CharField = ...
domain: str = ... name: models.CharField = ...
name: str = ... objects: SiteManager = ...
objects: Any = ...
def natural_key(self) -> Tuple[str]: ... def natural_key(self) -> Tuple[str]: ...
def clear_site_cache(sender: Type[Site], **kwargs: Any) -> None: ... def clear_site_cache(sender: Type[Site], **kwargs: Any) -> None: ...

View File

@@ -1,4 +1,4 @@
from typing import Any, Dict, List, Optional, Set, Tuple, TypeVar, Union, ClassVar from typing import Any, Dict, List, Optional, Set, Tuple, TypeVar, Union, ClassVar, Sequence
from django.db.models.manager import Manager from django.db.models.manager import Manager
@@ -22,8 +22,16 @@ class Model(metaclass=ModelBase):
force_insert: bool = ..., force_insert: bool = ...,
force_update: bool = ..., force_update: bool = ...,
using: Optional[str] = ..., using: Optional[str] = ...,
update_fields: Optional[Union[List[str], str]] = ..., update_fields: Optional[Union[Sequence[str], str]] = ...,
) -> None: ... ) -> None: ...
def save_base(
self,
raw: bool = ...,
force_insert: bool = ...,
force_update: bool = ...,
using: Optional[str] = ...,
update_fields: Optional[Union[Sequence[str], str]] = ...,
): ...
def refresh_from_db(self: _Self, using: Optional[str] = ..., fields: Optional[List[str]] = ...) -> _Self: ... def refresh_from_db(self: _Self, using: Optional[str] = ..., fields: Optional[List[str]] = ...) -> _Self: ...
def get_deferred_fields(self) -> Set[str]: ... def get_deferred_fields(self) -> Set[str]: ...

View File

@@ -62,7 +62,7 @@ class Field(RegisterLookupMixin):
def to_python(self, value: Any) -> Any: ... def to_python(self, value: Any) -> Any: ...
class IntegerField(Field): class IntegerField(Field):
def __set__(self, instance, value: Union[int, F]) -> None: ... def __set__(self, instance, value: Union[int, Combinable]) -> None: ...
def __get__(self, instance, owner) -> int: ... def __get__(self, instance, owner) -> int: ...
class PositiveIntegerRelDbTypeMixin: class PositiveIntegerRelDbTypeMixin:
@@ -231,7 +231,7 @@ class DateField(DateTimeCheckMixin, Field):
validators: Iterable[_ValidatorCallable] = ..., validators: Iterable[_ValidatorCallable] = ...,
error_messages: Optional[_ErrorMessagesToOverride] = ..., error_messages: Optional[_ErrorMessagesToOverride] = ...,
): ... ): ...
def __set__(self, instance, value: Any) -> None: ... def __set__(self, instance, value: Union[str, date, Combinable]) -> None: ...
def __get__(self, instance, owner) -> date: ... def __get__(self, instance, owner) -> date: ...
class TimeField(DateTimeCheckMixin, Field): class TimeField(DateTimeCheckMixin, Field):
@@ -257,11 +257,11 @@ class TimeField(DateTimeCheckMixin, Field):
validators: Iterable[_ValidatorCallable] = ..., validators: Iterable[_ValidatorCallable] = ...,
error_messages: Optional[_ErrorMessagesToOverride] = ..., error_messages: Optional[_ErrorMessagesToOverride] = ...,
): ... ): ...
def __set__(self, instance, value: Any) -> None: ... def __set__(self, instance, value: Union[str, time, datetime, Combinable]) -> None: ...
def __get__(self, instance, owner) -> time: ... def __get__(self, instance, owner) -> time: ...
class DateTimeField(DateField): class DateTimeField(DateField):
def __set__(self, instance, value: Any) -> None: ... def __set__(self, instance, value: Union[str, date, datetime, Combinable]) -> None: ...
def __get__(self, instance, owner) -> datetime: ... def __get__(self, instance, owner) -> datetime: ...
class UUIDField(Field): class UUIDField(Field):

View File

@@ -1,9 +1,10 @@
import typing import typing
from typing import Dict, Optional from typing import Dict, Optional
from mypy.nodes import Expression, FuncDef, ImportedName, MypyFile, NameExpr, SymbolNode, TypeInfo, Var from mypy.nodes import Expression, FuncDef, ImportedName, MypyFile, NameExpr, SymbolNode, TypeInfo, Var, AssignmentStmt, \
CallExpr
from mypy.plugin import FunctionContext from mypy.plugin import FunctionContext
from mypy.types import AnyType, CallableType, Instance, Type, TypeOfAny, TypeVarType from mypy.types import AnyType, CallableType, Instance, Type, TypeOfAny, TypeVarType, UnionType
MODEL_CLASS_FULLNAME = 'django.db.models.base.Model' MODEL_CLASS_FULLNAME = 'django.db.models.base.Model'
FIELD_FULLNAME = 'django.db.models.fields.Field' FIELD_FULLNAME = 'django.db.models.fields.Field'
@@ -101,10 +102,23 @@ def fill_typevars_with_any(instance: Instance) -> Type:
return reparametrize_with(instance, [AnyType(TypeOfAny.unannotated)]) return reparametrize_with(instance, [AnyType(TypeOfAny.unannotated)])
def extract_typevar_value(tp: Instance, typevar_name: str): def extract_typevar_value(tp: Instance, typevar_name: str) -> Type:
if typevar_name in {'_T', '_T_co'}:
if '_T' in tp.type.type_vars:
return tp.args[tp.type.type_vars.index('_T')]
if '_T_co' in tp.type.type_vars:
return tp.args[tp.type.type_vars.index('_T_co')]
return tp.args[tp.type.type_vars.index(typevar_name)] return tp.args[tp.type.type_vars.index(typevar_name)]
def fill_typevars(tp: Instance, type_to_fill: Instance) -> Instance:
typevar_values: typing.List[Type] = []
for typevar_arg in type_to_fill.args:
if isinstance(typevar_arg, TypeVarType):
typevar_values.append(extract_typevar_value(tp, typevar_arg.name))
return reparametrize_with(type_to_fill, typevar_values)
def extract_field_setter_type(tp: Instance) -> Optional[Type]: def extract_field_setter_type(tp: Instance) -> Optional[Type]:
if tp.type.has_base(FIELD_FULLNAME): if tp.type.has_base(FIELD_FULLNAME):
set_method = tp.type.get_method('__set__') set_method = tp.type.get_method('__set__')
@@ -112,13 +126,15 @@ def extract_field_setter_type(tp: Instance) -> Optional[Type]:
if 'value' in set_method.type.arg_names: if 'value' in set_method.type.arg_names:
set_value_type = set_method.type.arg_types[set_method.type.arg_names.index('value')] set_value_type = set_method.type.arg_types[set_method.type.arg_names.index('value')]
if isinstance(set_value_type, Instance): if isinstance(set_value_type, Instance):
typevar_values: typing.List[Type] = [] set_value_type = fill_typevars(tp, set_value_type)
for typevar_arg in set_value_type.args:
if isinstance(typevar_arg, TypeVarType):
typevar_values.append(extract_typevar_value(tp, typevar_arg.name))
# if there are typevars, extract from
set_value_type = reparametrize_with(set_value_type, typevar_values)
return set_value_type return set_value_type
elif isinstance(set_value_type, UnionType):
items_no_typevars = []
for item in set_value_type.items:
if isinstance(item, Instance):
item = fill_typevars(tp, item)
items_no_typevars.append(item)
return UnionType(items_no_typevars)
get_method = tp.type.get_method('__get__') get_method = tp.type.get_method('__get__')
if isinstance(get_method, FuncDef) and isinstance(get_method.type, CallableType): if isinstance(get_method, FuncDef) and isinstance(get_method.type, CallableType):
@@ -131,31 +147,20 @@ def extract_field_setter_type(tp: Instance) -> Optional[Type]:
def extract_primary_key_type(model: TypeInfo) -> Optional[Type]: def extract_primary_key_type(model: TypeInfo) -> Optional[Type]:
# only primary keys defined in current class for now # only primary keys defined in current class for now
for sym in model.names.values(): for stmt in model.defn.defs.body:
if isinstance(sym.node, Var) and isinstance(sym.node.type, Instance): if isinstance(stmt, AssignmentStmt) and isinstance(stmt.rvalue, CallExpr):
tp = sym.node.type name_expr = stmt.lvalues[0]
if tp.type.metadata.get('django', {}).get('defined_as_primary_key'): if isinstance(name_expr, NameExpr):
field_type = extract_field_setter_type(tp) name = name_expr.name
return field_type if 'primary_key' in stmt.rvalue.arg_names:
is_primary_key = stmt.rvalue.args[stmt.rvalue.arg_names.index('primary_key')]
if is_primary_key:
return extract_field_setter_type(model.names[name].type)
return None return None
def extract_expected_types(ctx: FunctionContext, model: TypeInfo) -> Dict[str, Type]: def extract_expected_types(ctx: FunctionContext, model: TypeInfo) -> Dict[str, Type]:
expected_types: Dict[str, Type] = {} expected_types: Dict[str, Type] = {}
for base in model.mro:
for name, sym in base.names.items():
if isinstance(sym.node, Var) and isinstance(sym.node.type, Instance):
tp = sym.node.type
field_type = extract_field_setter_type(tp)
if tp.type.fullname() == FOREIGN_KEY_FULLNAME:
ref_to_model = tp.args[0]
if isinstance(ref_to_model, Instance) and ref_to_model.type.has_base(MODEL_CLASS_FULLNAME):
primary_key_type = extract_primary_key_type(ref_to_model.type)
if not primary_key_type:
primary_key_type = AnyType(TypeOfAny.special_form)
expected_types[name + '_id'] = primary_key_type
if field_type:
expected_types[name] = field_type
primary_key_type = extract_primary_key_type(model) primary_key_type = extract_primary_key_type(model)
if not primary_key_type: if not primary_key_type:
@@ -164,6 +169,21 @@ def extract_expected_types(ctx: FunctionContext, model: TypeInfo) -> Dict[str, T
expected_types['id'] = ctx.api.named_generic_type('builtins.int', []) expected_types['id'] = ctx.api.named_generic_type('builtins.int', [])
expected_types['pk'] = primary_key_type expected_types['pk'] = primary_key_type
for base in model.mro:
for name, sym in base.names.items():
if isinstance(sym.node, Var) and isinstance(sym.node.type, Instance):
tp = sym.node.type
field_type = extract_field_setter_type(tp)
if tp.type.fullname() in {FOREIGN_KEY_FULLNAME, ONETOONE_FIELD_FULLNAME}:
ref_to_model = tp.args[0]
if isinstance(ref_to_model, Instance) and ref_to_model.type.has_base(MODEL_CLASS_FULLNAME):
primary_key_type = extract_primary_key_type(ref_to_model.type)
if not primary_key_type:
primary_key_type = AnyType(TypeOfAny.special_form)
expected_types[name + '_id'] = primary_key_type
if field_type:
expected_types[name] = field_type
return expected_types return expected_types

View File

@@ -1,6 +1,6 @@
import os import os
from configparser import ConfigParser from configparser import ConfigParser
from typing import Callable, Dict, Optional, cast from typing import Callable, Dict, Optional, Set, cast
from dataclasses import dataclass from dataclasses import dataclass
from mypy.checker import TypeChecker from mypy.checker import TypeChecker
@@ -10,7 +10,6 @@ from mypy.plugin import ClassDefContext, FunctionContext, MethodContext, Plugin
from mypy.types import Instance, Type from mypy.types import Instance, Type
from mypy_django_plugin import helpers, monkeypatch from mypy_django_plugin import helpers, monkeypatch
from mypy_django_plugin.helpers import parse_bool
from mypy_django_plugin.plugins.fields import determine_type_of_array_field from mypy_django_plugin.plugins.fields import determine_type_of_array_field
from mypy_django_plugin.plugins.migrations import determine_model_cls_from_string_for_migrations from mypy_django_plugin.plugins.migrations import determine_model_cls_from_string_for_migrations
from mypy_django_plugin.plugins.models import process_model_class from mypy_django_plugin.plugins.models import process_model_class
@@ -57,6 +56,16 @@ def determine_proper_manager_type(ctx: FunctionContext) -> Type:
return ret return ret
def extract_base_pointer_args(model: TypeInfo) -> Set[str]:
pointer_args: Set[str] = set()
for base in model.bases:
if base.type.has_base(helpers.MODEL_CLASS_FULLNAME):
parent_name = base.type.name().lower()
pointer_args.add(f'{parent_name}_ptr')
pointer_args.add(f'{parent_name}_ptr_id')
return pointer_args
def redefine_model_init(ctx: FunctionContext) -> Type: def redefine_model_init(ctx: FunctionContext) -> Type:
assert isinstance(ctx.default_return_type, Instance) assert isinstance(ctx.default_return_type, Instance)
@@ -64,9 +73,33 @@ def redefine_model_init(ctx: FunctionContext) -> Type:
model: TypeInfo = ctx.default_return_type.type model: TypeInfo = ctx.default_return_type.type
expected_types = helpers.extract_expected_types(ctx, model) expected_types = helpers.extract_expected_types(ctx, model)
for actual_name, actual_type in zip(ctx.arg_names[0], ctx.arg_types[0]): # order is preserved, can use for positionals
positional_names = list(expected_types.keys())
positional_names.remove('pk')
visited_positionals = set()
# check positionals
for i, (_, actual_pos_type) in enumerate(zip(ctx.arg_names[0], ctx.arg_types[0])):
actual_pos_name = positional_names[i]
api.check_subtype(actual_pos_type, expected_types[actual_pos_name],
ctx.context,
'Incompatible type for "{}" of "{}"'.format(actual_pos_name,
model.name()),
'got', 'expected')
visited_positionals.add(actual_pos_name)
# extract name of base models for _ptr
base_pointer_args = extract_base_pointer_args(model)
# check kwargs
for i, (actual_name, actual_type) in enumerate(zip(ctx.arg_names[1], ctx.arg_types[1])):
if actual_name in base_pointer_args:
# parent_ptr args are not supported
continue
if actual_name in visited_positionals:
continue
if actual_name is None: if actual_name is None:
# We can't check kwargs reliably. # unpacked dict as kwargs is not supported
continue continue
if actual_name not in expected_types: if actual_name not in expected_types:
ctx.api.fail('Unexpected attribute "{}" for model "{}"'.format(actual_name, ctx.api.fail('Unexpected attribute "{}" for model "{}"'.format(actual_name,
@@ -81,16 +114,6 @@ def redefine_model_init(ctx: FunctionContext) -> Type:
return ctx.default_return_type return ctx.default_return_type
def set_primary_key_marking(ctx: FunctionContext) -> Type:
primary_key_arg = helpers.get_argument_by_name(ctx, 'primary_key')
if primary_key_arg:
is_primary_key = parse_bool(primary_key_arg)
if is_primary_key:
info = ctx.default_return_type.type
info.metadata.setdefault('django', {})['defined_as_primary_key'] = True
return ctx.default_return_type
@dataclass @dataclass
class Config: class Config:
django_settings_module: Optional[str] = None django_settings_module: Optional[str] = None
@@ -171,8 +194,6 @@ class DjangoPlugin(Plugin):
sym = self.lookup_fully_qualified(fullname) sym = self.lookup_fully_qualified(fullname)
if sym and isinstance(sym.node, TypeInfo): if sym and isinstance(sym.node, TypeInfo):
if sym.node.has_base(helpers.FIELD_FULLNAME):
return set_primary_key_marking
if sym.node.metadata.get('django', {}).get('generated_init'): if sym.node.metadata.get('django', {}).get('generated_init'):
return redefine_model_init return redefine_model_init

View File

@@ -3,7 +3,7 @@ from typing import Dict, Iterator, Optional, Tuple, cast
import dataclasses import dataclasses
from mypy.nodes import AssignmentStmt, CallExpr, ClassDef, Context, Expression, Lvalue, MDEF, MemberExpr, \ from mypy.nodes import AssignmentStmt, CallExpr, ClassDef, Context, Expression, Lvalue, MDEF, MemberExpr, \
MypyFile, NameExpr, StrExpr, SymbolTableNode, TypeInfo, Var, Argument, ARG_STAR2 MypyFile, NameExpr, StrExpr, SymbolTableNode, TypeInfo, Var, Argument, ARG_STAR2, ARG_STAR
from mypy.plugin import ClassDefContext from mypy.plugin import ClassDefContext
from mypy.plugins.common import add_method from mypy.plugins.common import add_method
from mypy.semanal import SemanticAnalyzerPass2 from mypy.semanal import SemanticAnalyzerPass2
@@ -202,9 +202,13 @@ def extract_ref_to_fullname(rvalue_expr: CallExpr,
def add_dummy_init_method(ctx: ClassDefContext) -> None: def add_dummy_init_method(ctx: ClassDefContext) -> None:
any = AnyType(TypeOfAny.special_form) any = AnyType(TypeOfAny.special_form)
var = Var('kwargs', any)
kw_arg = Argument(variable=var, type_annotation=any, initializer=None, kind=ARG_STAR2) pos_arg = Argument(variable=Var('args', any),
add_method(ctx, '__init__', [kw_arg], NoneTyp()) type_annotation=any, initializer=None, kind=ARG_STAR)
kw_arg = Argument(variable=Var('kwargs', any),
type_annotation=any, initializer=None, kind=ARG_STAR2)
add_method(ctx, '__init__', [pos_arg, kw_arg], NoneTyp())
# mark as model class # mark as model class
ctx.cls.info.metadata.setdefault('django', {})['generated_init'] = True ctx.cls.info.metadata.setdefault('django', {})['generated_init'] = True

View File

@@ -101,7 +101,8 @@ IGNORED_ERRORS = {
], ],
'basic': [ 'basic': [
'Unexpected keyword argument "unknown_kwarg" for "refresh_from_db" of "Model"', 'Unexpected keyword argument "unknown_kwarg" for "refresh_from_db" of "Model"',
'"refresh_from_db" of "Model" defined here' '"refresh_from_db" of "Model" defined here',
'Unexpected attribute "foo" for model "Article"'
], ],
'builtin_server': [ 'builtin_server': [
'has no attribute "getvalue"' 'has no attribute "getvalue"'
@@ -174,6 +175,10 @@ IGNORED_ERRORS = {
'Unexpected keyword argument "name" for "Person"', 'Unexpected keyword argument "name" for "Person"',
'Cannot assign multiple types to name "PersonTwoImages" without an explicit "Type[...]" annotation', 'Cannot assign multiple types to name "PersonTwoImages" without an explicit "Type[...]" annotation',
], ],
'model_regress': [
'Too many arguments for "Worker"',
re.compile(r'Incompatible type for "[a-z]+" of "Worker" \(got "int", expected')
],
'modeladmin': [ 'modeladmin': [
'BandAdmin', 'BandAdmin',
], ],
@@ -200,6 +205,9 @@ IGNORED_ERRORS = {
'DummyJSONField', 'DummyJSONField',
'Argument "encoder" to "JSONField" has incompatible type "DjangoJSONEncoder"; expected "Optional[Type[JSONEncoder]]"' 'Argument "encoder" to "JSONField" has incompatible type "DjangoJSONEncoder"; expected "Optional[Type[JSONEncoder]]"'
], ],
'properties': [
re.compile('Unexpected attribute "(full_name|full_name_2)" for model "Person"')
],
'requests': [ 'requests': [
'Incompatible types in assignment (expression has type "Dict[str, str]", variable has type "QueryDict")' 'Incompatible types in assignment (expression has type "Dict[str, str]", variable has type "QueryDict")'
], ],
@@ -420,7 +428,7 @@ TESTS_DIRS = [
'model_package', 'model_package',
'model_regress', 'model_regress',
# not practical # not practical
# 'modeladmin', 'modeladmin',
# TODO: 'multiple_database', # TODO: 'multiple_database',
'mutually_referential', 'mutually_referential',
'nested_foreign_keys', 'nested_foreign_keys',

View File

@@ -16,7 +16,7 @@ class MyUser(models.Model):
age = models.IntegerField() age = models.IntegerField()
user = MyUser(name=1, age=12) user = MyUser(name=1, age=12)
[out] [out]
main:6: error: Incompatible type for "name" of "MyUser" (got "int", expected "str") main:6: error: Incompatible type for "name" of "MyUser" (got "int", expected "Union[str, Combinable]")
[CASE arguments_to_init_combined_from_base_classes] [CASE arguments_to_init_combined_from_base_classes]
from django.db import models from django.db import models
@@ -53,7 +53,7 @@ from django.db import models
class MyUser1(models.Model): class MyUser1(models.Model):
mypk = models.CharField(primary_key=True) mypk = models.CharField(primary_key=True)
class MyUser2(models.Model): class MyUser2(models.Model):
pass name = models.CharField(max_length=100)
user2 = MyUser1(pk='hello') user2 = MyUser1(pk='hello')
user3= MyUser2(pk=1) user3= MyUser2(pk=1)
[out] [out]
@@ -63,7 +63,7 @@ from django.db import models
class MyUser1(models.Model): class MyUser1(models.Model):
mypk = models.CharField(primary_key=True) mypk = models.CharField(primary_key=True)
user = MyUser1(pk=1) # E: Incompatible type for "pk" of "MyUser1" (got "int", expected "str") user = MyUser1(pk=1) # E: Incompatible type for "pk" of "MyUser1" (got "int", expected "Union[str, Combinable]")
[out] [out]
[CASE can_set_foreign_key_by_its_primary_key] [CASE can_set_foreign_key_by_its_primary_key]
@@ -78,7 +78,7 @@ class Book(models.Model):
publisher_with_char_pk = models.ForeignKey(PublisherWithCharPK, on_delete=models.CASCADE) publisher_with_char_pk = models.ForeignKey(PublisherWithCharPK, on_delete=models.CASCADE)
Book(publisher_id=1, publisher_with_char_pk_id='hello') Book(publisher_id=1, publisher_with_char_pk_id='hello')
Book(publisher_id=1, publisher_with_char_pk_id=1) # E: Incompatible type for "publisher_with_char_pk_id" of "Book" (got "int", expected "str") Book(publisher_id=1, publisher_with_char_pk_id=1) # E: Incompatible type for "publisher_with_char_pk_id" of "Book" (got "int", expected "Union[str, Combinable]")
[out] [out]
[CASE setting_value_to_an_array_of_ints] [CASE setting_value_to_an_array_of_ints]
@@ -100,7 +100,52 @@ MyModel(array=array_val3) # E: Incompatible type for "array" of "MyModel" (got
[CASE if_no_explicit_primary_key_id_can_be_passed] [CASE if_no_explicit_primary_key_id_can_be_passed]
from django.db import models from django.db import models
class MyModel(models.Model):
name = models.CharField(max_length=100)
MyModel(id=1, name='maxim')
[out]
[CASE arguments_can_be_passed_as_positionals]
from django.db import models
class MyModel(models.Model): class MyModel(models.Model):
pass pass
MyModel(id=1) MyModel(1)
[out]
class MyModel2(models.Model):
name = models.CharField(max_length=100)
MyModel2(1, 'Maxim')
MyModel2(1, 12) # E: Incompatible type for "name" of "MyModel2" (got "int", expected "Union[str, Combinable]")
[out]
[CASE arguments_passed_as_dictionary_unpacking_are_not_supported]
from django.db import models
class MyModel(models.Model):
name = models.CharField(max_length=100)
MyModel(**{'name': 'hello'})
[out]
[CASE pointer_to_parent_model_is_not_supported]
from django.db import models
class Place(models.Model):
pass
class Restaurant(Place):
pass
place = Place()
Restaurant(place_ptr=place)
Restaurant(place_ptr_id=place.id)
[out]
[CASE extract_type_of_init_param_from_set_method]
from typing import Union
from datetime import time
from django.db import models
class MyField(models.Field):
def __set__(self, instance, value: Union[str, time]) -> None: pass
def __get__(self, instance, owner) -> time: pass
class MyModel(models.Model):
field = MyField()
MyModel(field=time())
MyModel(field='12:00')
MyModel(field=100) # E: Incompatible type for "field" of "MyModel" (got "int", expected "Union[str, time]")