diff --git a/changes/4457-samuelcolvin.md b/changes/4457-samuelcolvin.md new file mode 100644 index 0000000000..e2a0c7f849 --- /dev/null +++ b/changes/4457-samuelcolvin.md @@ -0,0 +1 @@ +Fix mypy plugin when using bare types like `list` and `dict` as `default_factory`. diff --git a/pydantic/mypy.py b/pydantic/mypy.py index 2619259094..1955799d97 100644 --- a/pydantic/mypy.py +++ b/pydantic/mypy.py @@ -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]': @@ -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 diff --git a/tests/mypy/modules/plugin_default_factory.py b/tests/mypy/modules/plugin_default_factory.py new file mode 100644 index 0000000000..4f81566f1a --- /dev/null +++ b/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) diff --git a/tests/mypy/outputs/plugin-fail-strict.txt b/tests/mypy/outputs/plugin-fail-strict.txt index db93869290..05d8873164 100644 --- a/tests/mypy/outputs/plugin-fail-strict.txt +++ b/tests/mypy/outputs/plugin-fail-strict.txt @@ -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] 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] diff --git a/tests/mypy/outputs/plugin-fail.txt b/tests/mypy/outputs/plugin-fail.txt index ff93213837..3630284f64 100644 --- a/tests/mypy/outputs/plugin-fail.txt +++ b/tests/mypy/outputs/plugin-fail.txt @@ -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] diff --git a/tests/mypy/outputs/plugin_default_factory.txt b/tests/mypy/outputs/plugin_default_factory.txt new file mode 100644 index 0000000000..3a97d228fe --- /dev/null +++ b/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] diff --git a/tests/mypy/test_mypy.py b/tests/mypy/test_mypy.py index 9f006a4eb4..fb1a2bd735 100644 --- a/tests/mypy/test_mypy.py +++ b/tests/mypy/test_mypy.py @@ -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})