Skip to content

Commit

Permalink
mypy plugin: More precisely detect when fields are required. (#4086)
Browse files Browse the repository at this point in the history
The mypy plugin would previously incorrectly determine that a field was
not required in a few scenarios where the field really is required. This
specifically affected cases when the `Field()` function is used, where
the plugin assumed that the first argument would always be `default`.

This changes the code to examine each argument more closely, and it now
properly handles several more scenarios where the default is explicitly
named or when the default_factory named argument is used.
  • Loading branch information
richardxia committed Aug 8, 2022
1 parent da8f0c0 commit 02a076d
Show file tree
Hide file tree
Showing 5 changed files with 33 additions and 2 deletions.
1 change: 1 addition & 0 deletions changes/4086-richardxia.md
@@ -0,0 +1 @@
Improve mypy plugin's ability to detect required fields.
11 changes: 9 additions & 2 deletions pydantic/mypy.py
Expand Up @@ -494,8 +494,15 @@ def get_is_required(cls: ClassDef, stmt: AssignmentStmt, lhs: NameExpr) -> bool:
return True
if isinstance(expr, CallExpr) and isinstance(expr.callee, RefExpr) and expr.callee.fullname == FIELD_FULLNAME:
# The "default value" is a call to `Field`; at this point, the field is
# only required if default is Ellipsis (i.e., `field_name: Annotation = Field(...)`)
return len(expr.args) > 0 and expr.args[0].__class__ is EllipsisExpr
# only required if default is Ellipsis (i.e., `field_name: Annotation = Field(...)`) or if default_factory
# is specified.
for arg, name in zip(expr.args, expr.arg_names):
# If name is None, then this arg is the default because it is the only positonal argument.
if name is None or name == 'default':
return arg.__class__ is EllipsisExpr
if name == 'default_factory':
return False
return True
# Only required if the "default value" is Ellipsis (i.e., `field_name: Annotation = ...`)
return isinstance(expr, EllipsisExpr)

Expand Down
17 changes: 17 additions & 0 deletions tests/mypy/modules/fail_defaults.py
@@ -0,0 +1,17 @@
from pydantic import BaseModel, Field


class Model(BaseModel):
# Required
undefined_default_no_args: int = Field()
undefined_default: int = Field(description='my desc')
positional_ellipsis_default: int = Field(...)
named_ellipsis_default: int = Field(default=...)

# Not required
positional_default: int = Field(1)
named_default: int = Field(default=2)
named_default_factory: int = Field(default_factory=lambda: 3)


Model()
4 changes: 4 additions & 0 deletions tests/mypy/outputs/fail_defaults.txt
@@ -0,0 +1,4 @@
17: error: Missing named argument "undefined_default_no_args" for "Model" [call-arg]
17: error: Missing named argument "undefined_default" for "Model" [call-arg]
17: error: Missing named argument "positional_ellipsis_default" for "Model" [call-arg]
17: error: Missing named argument "named_ellipsis_default" for "Model" [call-arg]
2 changes: 2 additions & 0 deletions tests/mypy/test_mypy.py
Expand Up @@ -25,6 +25,7 @@
('mypy-plugin.ini', 'plugin_fail.py', 'plugin-fail.txt'),
('mypy-plugin-strict.ini', 'plugin_success.py', 'plugin-success-strict.txt'),
('mypy-plugin-strict.ini', 'plugin_fail.py', 'plugin-fail-strict.txt'),
('mypy-plugin-strict.ini', 'fail_defaults.py', 'fail_defaults.txt'),
('mypy-default.ini', 'success.py', None),
('mypy-default.ini', 'fail1.py', 'fail1.txt'),
('mypy-default.ini', 'fail2.py', 'fail2.txt'),
Expand All @@ -40,6 +41,7 @@
('pyproject-plugin.toml', 'plugin_fail.py', 'plugin-fail.txt'),
('pyproject-plugin-strict.toml', 'plugin_success.py', 'plugin-success-strict.txt'),
('pyproject-plugin-strict.toml', 'plugin_fail.py', 'plugin-fail-strict.txt'),
('pyproject-plugin-strict.toml', 'fail_defaults.py', 'fail_defaults.txt'),
]
executable_modules = list({fname[:-3] for _, fname, out_fname in cases if out_fname is None})

Expand Down

0 comments on commit 02a076d

Please sign in to comment.