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

mypy plugin: More precisely detect when fields are required. #4086

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/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 @@ -435,8 +435,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