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 for settings #6760

Merged
merged 5 commits into from
Jul 19, 2023
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
42 changes: 35 additions & 7 deletions pydantic/mypy.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ def transform(self) -> bool:

is_settings = any(base.fullname == BASESETTINGS_FULLNAME for base in info.mro[:-1])
self.add_initializer(fields, config, is_settings)
self.add_model_construct_method(fields, config)
self.add_model_construct_method(fields, config, is_settings)
self.set_frozen(fields, frozen=config.frozen is True)

self.adjust_decorator_signatures()
Expand Down Expand Up @@ -781,18 +781,37 @@ def add_initializer(self, fields: list[PydanticModelField], config: ModelConfigD

typed = self.plugin_config.init_typed
use_alias = config.populate_by_name is not True
force_all_optional = is_settings or bool(config.has_alias_generator and not config.populate_by_name)
requires_dynamic_aliases = bool(config.has_alias_generator and not config.populate_by_name)
with state.strict_optional_set(self._api.options.strict_optional):
args = self.get_field_arguments(
fields, typed=typed, force_all_optional=force_all_optional, use_alias=use_alias
fields,
typed=typed,
requires_dynamic_aliases=requires_dynamic_aliases,
use_alias=use_alias,
is_settings=is_settings,
)
if is_settings:
base_settings_info = self._api.lookup_fully_qualified(BASESETTINGS_FULLNAME).node.defn.info
base_settings_init_arguments = base_settings_info.names['__init__'].node.arguments
settings_init_arguments = []
a: Argument
for a in base_settings_init_arguments:
if a.variable.name.startswith('__') or not a.variable.name.startswith('_'):
continue
analyzed_variable_type = self._api.anal_type(a.variable.type)
variable = Var(a.variable.name, analyzed_variable_type)
settings_init_arguments.append(Argument(variable, analyzed_variable_type, None, ARG_OPT))
args.extend(settings_init_arguments)

if not self.should_init_forbid_extra(fields, config):
var = Var('kwargs')
args.append(Argument(var, AnyType(TypeOfAny.explicit), None, ARG_STAR2))

add_method(self._api, self._cls, '__init__', args=args, return_type=NoneType())

def add_model_construct_method(self, fields: list[PydanticModelField], config: ModelConfigData) -> None:
def add_model_construct_method(
self, fields: list[PydanticModelField], config: ModelConfigData, is_settings: bool
) -> None:
"""Adds a fully typed `model_construct` classmethod to the class.

Similar to the fields-aware __init__ method, but always uses the field names (not aliases),
Expand All @@ -802,7 +821,9 @@ def add_model_construct_method(self, fields: list[PydanticModelField], config: M
optional_set_str = UnionType([set_str, NoneType()])
fields_set_argument = Argument(Var('_fields_set', optional_set_str), optional_set_str, None, ARG_OPT)
with state.strict_optional_set(self._api.options.strict_optional):
args = self.get_field_arguments(fields, typed=True, force_all_optional=False, use_alias=False)
args = self.get_field_arguments(
fields, typed=True, requires_dynamic_aliases=False, use_alias=False, is_settings=is_settings
)
if not self.should_init_forbid_extra(fields, config):
var = Var('kwargs')
args.append(Argument(var, AnyType(TypeOfAny.explicit), None, ARG_STAR2))
Expand Down Expand Up @@ -926,15 +947,22 @@ def get_alias_info(stmt: AssignmentStmt) -> tuple[str | None, bool]:
return None, False

def get_field_arguments(
self, fields: list[PydanticModelField], typed: bool, force_all_optional: bool, use_alias: bool
self,
fields: list[PydanticModelField],
typed: bool,
use_alias: bool,
requires_dynamic_aliases: bool,
is_settings: bool,
) -> list[Argument]:
"""Helper function used during the construction of the `__init__` and `model_construct` method signatures.

Returns a list of mypy Argument instances for use in the generated signatures.
"""
info = self._cls.info
arguments = [
field.to_argument(info, typed=typed, force_optional=force_all_optional, use_alias=use_alias)
field.to_argument(
info, typed=typed, force_optional=requires_dynamic_aliases or is_settings, use_alias=use_alias
)
for field in fields
if not (use_alias and field.has_dynamic_alias)
]
Expand Down
4 changes: 4 additions & 0 deletions tests/mypy/modules/pydantic_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,7 @@ class Settings(BaseSettings):


s = Settings()

s = Settings(foo='test', _case_sensitive=True, _env_prefix='test__', _env_file='test')

s = Settings(foo='test', _case_sensitive=1, _env_prefix=2, _env_file=3)
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ class Settings(BaseSettings):


s = Settings()

s = Settings(foo='test', _case_sensitive=True, _env_prefix='test__', _env_file='test')

s = Settings(foo='test', _case_sensitive=1, _env_prefix=2, _env_file=3)
# MYPY: error: Argument "_case_sensitive" to "Settings" has incompatible type "int"; expected "Optional[bool]" [arg-type]
# MYPY: error: Argument "_env_prefix" to "Settings" has incompatible type "int"; expected "Optional[str]" [arg-type]
# MYPY: error: Argument "_env_file" to "Settings" has incompatible type "int"; expected "Optional[Union[Path, str, List[Union[Path, str]], Tuple[Union[Path, str], ...]]]" [arg-type]
7 changes: 7 additions & 0 deletions tests/mypy/outputs/1.0.1/mypy-plugin_ini/pydantic_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ class Settings(BaseSettings):


s = Settings()

s = Settings(foo='test', _case_sensitive=True, _env_prefix='test__', _env_file='test')

s = Settings(foo='test', _case_sensitive=1, _env_prefix=2, _env_file=3)
# MYPY: error: Argument "_case_sensitive" to "Settings" has incompatible type "int"; expected "Optional[bool]" [arg-type]
# MYPY: error: Argument "_env_prefix" to "Settings" has incompatible type "int"; expected "Optional[str]" [arg-type]
# MYPY: error: Argument "_env_file" to "Settings" has incompatible type "int"; expected "Optional[Union[Path, str, List[Union[Path, str]], Tuple[Union[Path, str], ...]]]" [arg-type]
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ class Settings(BaseSettings):


s = Settings()

s = Settings(foo='test', _case_sensitive=True, _env_prefix='test__', _env_file='test')

s = Settings(foo='test', _case_sensitive=1, _env_prefix=2, _env_file=3)
# MYPY: error: Argument "_case_sensitive" to "Settings" has incompatible type "int"; expected "Optional[bool]" [arg-type]
# MYPY: error: Argument "_env_prefix" to "Settings" has incompatible type "int"; expected "Optional[str]" [arg-type]
# MYPY: error: Argument "_env_file" to "Settings" has incompatible type "int"; expected "Optional[Union[Path, str, List[Union[Path, str]], Tuple[Union[Path, str], ...]]]" [arg-type]
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,10 @@ class Settings(BaseSettings):


s = Settings()

s = Settings(foo='test', _case_sensitive=True, _env_prefix='test__', _env_file='test')

s = Settings(foo='test', _case_sensitive=1, _env_prefix=2, _env_file=3)
# MYPY: error: Argument "_case_sensitive" to "Settings" has incompatible type "int"; expected "Optional[bool]" [arg-type]
# MYPY: error: Argument "_env_prefix" to "Settings" has incompatible type "int"; expected "Optional[str]" [arg-type]
# MYPY: error: Argument "_env_file" to "Settings" has incompatible type "int"; expected "Optional[Union[Path, str, List[Union[Path, str]], Tuple[Union[Path, str], ...]]]" [arg-type]
10 changes: 10 additions & 0 deletions tests/mypy/outputs/1.1.1/mypy-default_ini/pydantic_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,13 @@ class Settings(BaseSettings):

s = Settings()
# MYPY: error: Missing named argument "foo" for "Settings" [call-arg]

s = Settings(foo='test', _case_sensitive=True, _env_prefix='test__', _env_file='test')
# MYPY: error: Unexpected keyword argument "_case_sensitive" for "Settings" [call-arg]
# MYPY: error: Unexpected keyword argument "_env_prefix" for "Settings" [call-arg]
# MYPY: error: Unexpected keyword argument "_env_file" for "Settings" [call-arg]

s = Settings(foo='test', _case_sensitive=1, _env_prefix=2, _env_file=3)
# MYPY: error: Unexpected keyword argument "_case_sensitive" for "Settings" [call-arg]
# MYPY: error: Unexpected keyword argument "_env_prefix" for "Settings" [call-arg]
# MYPY: error: Unexpected keyword argument "_env_file" for "Settings" [call-arg]
15 changes: 15 additions & 0 deletions tests/mypy/outputs/1.1.1/mypy-plugin_ini/pydantic_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
foo: str


s = Settings()

s = Settings(foo='test', _case_sensitive=True, _env_prefix='test__', _env_file='test')

s = Settings(foo='test', _case_sensitive=1, _env_prefix=2, _env_file=3)
# MYPY: error: Argument "_case_sensitive" to "Settings" has incompatible type "int"; expected "Optional[bool]" [arg-type]
# MYPY: error: Argument "_env_prefix" to "Settings" has incompatible type "int"; expected "Optional[str]" [arg-type]
# MYPY: error: Argument "_env_file" to "Settings" has incompatible type "int"; expected "Optional[Union[Path, str, List[Union[Path, str]], Tuple[Union[Path, str], ...]]]" [arg-type]
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,13 @@ class Settings(BaseSettings):

s = Settings()
# MYPY: error: Missing named argument "foo" for "Settings" [call-arg]

s = Settings(foo='test', _case_sensitive=True, _env_prefix='test__', _env_file='test')
# MYPY: error: Unexpected keyword argument "_case_sensitive" for "Settings" [call-arg]
# MYPY: error: Unexpected keyword argument "_env_prefix" for "Settings" [call-arg]
# MYPY: error: Unexpected keyword argument "_env_file" for "Settings" [call-arg]

s = Settings(foo='test', _case_sensitive=1, _env_prefix=2, _env_file=3)
# MYPY: error: Unexpected keyword argument "_case_sensitive" for "Settings" [call-arg]
# MYPY: error: Unexpected keyword argument "_env_prefix" for "Settings" [call-arg]
# MYPY: error: Unexpected keyword argument "_env_file" for "Settings" [call-arg]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
foo: str


s = Settings()

s = Settings(foo='test', _case_sensitive=True, _env_prefix='test__', _env_file='test')

s = Settings(foo='test', _case_sensitive=1, _env_prefix=2, _env_file=3)
# MYPY: error: Argument "_case_sensitive" to "Settings" has incompatible type "int"; expected "Optional[bool]" [arg-type]
# MYPY: error: Argument "_env_prefix" to "Settings" has incompatible type "int"; expected "Optional[str]" [arg-type]
# MYPY: error: Argument "_env_file" to "Settings" has incompatible type "int"; expected "Optional[Union[Path, str, List[Union[Path, str]], Tuple[Union[Path, str], ...]]]" [arg-type]
15 changes: 15 additions & 0 deletions tests/mypy/outputs/1.4.1/mypy-plugin_ini/pydantic_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
foo: str


s = Settings()

s = Settings(foo='test', _case_sensitive=True, _env_prefix='test__', _env_file='test')

s = Settings(foo='test', _case_sensitive=1, _env_prefix=2, _env_file=3)
# MYPY: error: Argument "_case_sensitive" to "Settings" has incompatible type "int"; expected "bool | None" [arg-type]
# MYPY: error: Argument "_env_prefix" to "Settings" has incompatible type "int"; expected "str | None" [arg-type]
# MYPY: error: Argument "_env_file" to "Settings" has incompatible type "int"; expected "Path | str | list[Path | str] | tuple[Path | str, ...] | None" [arg-type]
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from pydantic_settings import BaseSettings


class Settings(BaseSettings):
foo: str


s = Settings()

s = Settings(foo='test', _case_sensitive=True, _env_prefix='test__', _env_file='test')

s = Settings(foo='test', _case_sensitive=1, _env_prefix=2, _env_file=3)
# MYPY: error: Argument "_case_sensitive" to "Settings" has incompatible type "int"; expected "bool | None" [arg-type]
# MYPY: error: Argument "_env_prefix" to "Settings" has incompatible type "int"; expected "str | None" [arg-type]
# MYPY: error: Argument "_env_file" to "Settings" has incompatible type "int"; expected "Path | str | list[Path | str] | tuple[Path | str, ...] | None" [arg-type]