Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix mypy with default_factory=list etc. #4471

Merged
merged 3 commits into from Sep 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/4457-samuelcolvin.md
@@ -0,0 +1 @@
Fix mypy plugin when using bare types like `list` and `dict` as `default_factory`.
15 changes: 12 additions & 3 deletions pydantic/mypy.py
Expand Up @@ -82,7 +82,8 @@ def parse_mypy_version(version: str) -> Tuple[int, ...]:
return tuple(int(part) for part in version.split('+', 1)[0].split('.'))


BUILTINS_NAME = 'builtins' if parse_mypy_version(mypy_version) >= (0, 930) else '__builtins__'
MYPY_VERSION_TUPLE = parse_mypy_version(mypy_version)
BUILTINS_NAME = 'builtins' if MYPY_VERSION_TUPLE >= (0, 930) else '__builtins__'


def plugin(version: str) -> 'TypingType[Plugin]':
Expand Down Expand Up @@ -162,14 +163,22 @@ def _pydantic_field_callback(self, ctx: FunctionContext) -> 'Type':
# Functions which use `ParamSpec` can be overloaded, exposing the callable's types as a parameter
# Pydantic calls the default factory without any argument, so we retrieve the first item
if isinstance(default_factory_type, Overloaded):
if float(mypy_version) > 0.910:
if MYPY_VERSION_TUPLE > (0, 910):
default_factory_type = default_factory_type.items[0]
else:
# Mypy0.910 exposes the items of overloaded types in a function
default_factory_type = default_factory_type.items()[0] # type: ignore[operator]

if isinstance(default_factory_type, CallableType):
return default_factory_type.ret_type
ret_type = default_factory_type.ret_type
# mypy doesn't think `ret_type` has `args`, you'd think mypy should know,
# add this check in case it varies by version
args = getattr(ret_type, 'args', None)
if args:
if all(isinstance(arg, TypeVarType) for arg in args):
# Looks like the default factory is a type like `list` or `dict`, replace all args with `Any`
ret_type.args = tuple(default_any_type for _ in args) # type: ignore[attr-defined]
return ret_type

return default_any_type

Expand Down
21 changes: 21 additions & 0 deletions tests/mypy/modules/plugin_default_factory.py
@@ -0,0 +1,21 @@
"""
See https://github.com/pydantic/pydantic/issues/4457
"""

from typing import Dict, List

from pydantic import BaseModel, Field


def new_list() -> List[int]:
return []


class Model(BaseModel):
l1: List[str] = Field(default_factory=list)
l2: List[int] = Field(default_factory=new_list)
l3: List[str] = Field(default_factory=lambda: list())
l4: Dict[str, str] = Field(default_factory=dict)
l5: int = Field(default_factory=lambda: 123)
l6_error: List[str] = Field(default_factory=new_list)
l7_error: int = Field(default_factory=list)
3 changes: 1 addition & 2 deletions tests/mypy/outputs/plugin-fail-strict.txt
Expand Up @@ -36,9 +36,8 @@
219: error: Property "y" defined in "FrozenModel" is read-only [misc]
240: error: Incompatible types in assignment (expression has type "None", variable has type "int") [assignment]
241: error: Incompatible types in assignment (expression has type "None", variable has type "int") [assignment]
244: error: Incompatible types in assignment (expression has type "Set[_T]", variable has type "str") [assignment]
244: error: Incompatible types in assignment (expression has type "Set[Any]", variable has type "str") [assignment]
samuelcolvin marked this conversation as resolved.
Show resolved Hide resolved
245: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment]
246: error: Incompatible types in assignment (expression has type "List[_T]", variable has type "List[int]") [assignment]
247: error: Argument "default_factory" to "Field" has incompatible type "int"; expected "Optional[Callable[[], Any]]" [arg-type]
250: error: Field default and default_factory cannot be specified together [pydantic-field]
260: error: Missing positional argument "self" in call to "instance_method" of "ModelWithAnnotatedValidator" [call-arg]
3 changes: 1 addition & 2 deletions tests/mypy/outputs/plugin-fail.txt
Expand Up @@ -25,9 +25,8 @@
219: error: Property "y" defined in "FrozenModel" is read-only [misc]
240: error: Incompatible types in assignment (expression has type "None", variable has type "int") [assignment]
241: error: Incompatible types in assignment (expression has type "None", variable has type "int") [assignment]
244: error: Incompatible types in assignment (expression has type "Set[_T]", variable has type "str") [assignment]
244: error: Incompatible types in assignment (expression has type "Set[Any]", variable has type "str") [assignment]
245: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment]
246: error: Incompatible types in assignment (expression has type "List[_T]", variable has type "List[int]") [assignment]
247: error: Argument "default_factory" to "Field" has incompatible type "int"; expected "Optional[Callable[[], Any]]" [arg-type]
250: error: Field default and default_factory cannot be specified together [pydantic-field]
260: error: Missing positional argument "self" in call to "instance_method" of "ModelWithAnnotatedValidator" [call-arg]
2 changes: 2 additions & 0 deletions tests/mypy/outputs/plugin_default_factory.txt
@@ -0,0 +1,2 @@
20: error: Incompatible types in assignment (expression has type "List[int]", variable has type "List[str]") [assignment]
21: error: Incompatible types in assignment (expression has type "List[Any]", variable has type "int") [assignment]
1 change: 1 addition & 0 deletions tests/mypy/test_mypy.py
Expand Up @@ -49,6 +49,7 @@
('pyproject-plugin-strict.toml', 'plugin_fail.py', 'plugin-fail-strict.txt'),
('pyproject-plugin-strict.toml', 'fail_defaults.py', 'fail_defaults.txt'),
('mypy-plugin-strict.ini', 'settings_config.py', None),
('mypy-plugin-strict.ini', 'plugin_default_factory.py', 'plugin_default_factory.txt'),
]
executable_modules = list({fname[:-3] for _, fname, out_fname in cases if out_fname is None})

Expand Down