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

Extra inputs are not permitted with AliasChoices #148

Closed
levsh opened this issue Aug 22, 2023 · 6 comments
Closed

Extra inputs are not permitted with AliasChoices #148

levsh opened this issue Aug 22, 2023 · 6 comments
Assignees

Comments

@levsh
Copy link

levsh commented Aug 22, 2023

Hi!
Example code to reproduce:

from unittest import mock
from pydantic import AliasChoices, Field
from pydantic_settings import BaseSettings

def test_settings():
    class Settings(BaseSettings):
        field: str = Field(validation_alias=AliasChoices("field", "my_field"))

    env = {}
    with mock.patch.dict(os.environ, env, clear=True):
        assert Settings(field="bar").field == "bar"  # Ok

    env = {"my_field": "foo"}
    with mock.patch.dict(os.environ, env, clear=True):
        assert Settings().field == "foo"  # Ok

    env = {"my_field": "foo"}
    with mock.patch.dict(os.environ, env, clear=True):
        assert Settings(field="bar").field == "bar"  # Not ok, raise ValidationError

Third case raises

        env = {"my_field": "foo"}
        with mock.patch.dict(os.environ, env, clear=True):
>           assert Settings(field="bar").field == "bar"

tests/.../test_settings.py: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

__pydantic_self__ = <[AttributeError('__pydantic_extra__') raised in repr()] Settings object at 0x7efdbe6eca40>, _case_sensitive = None, _env_prefix = None, _env_file = PosixPath('.')
_env_file_encoding = None, _env_nested_delimiter = None, _secrets_dir = None, values = {'field': 'bar'}

    def __init__(
        __pydantic_self__,
        _case_sensitive: bool | None = None,
        _env_prefix: str | None = None,
        _env_file: DotenvType | None = ENV_FILE_SENTINEL,
        _env_file_encoding: str | None = None,
        _env_nested_delimiter: str | None = None,
        _secrets_dir: str | Path | None = None,
        **values: Any,
    ) -> None:
        # Uses something other than `self` the first arg to allow "self" as a settable attribute
>       super().__init__(
            **__pydantic_self__._settings_build_values(
                values,
                _case_sensitive=_case_sensitive,
                _env_prefix=_env_prefix,
                _env_file=_env_file,
                _env_file_encoding=_env_file_encoding,
                _env_nested_delimiter=_env_nested_delimiter,
                _secrets_dir=_secrets_dir,
            )
        )
E       pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings
E       my_field
E         Extra inputs are not permitted [type=extra_forbidden, input_value='foo', input_type=str]
E           For further information visit https://errors.pydantic.dev/2.2/v/extra_forbidden

Expect environment variable 'my_field' to be ignored in favor of explicitly passed field='bar'.

pydantic                  2.2.1
pydantic_core             2.6.1
pydantic-settings         2.0.3

Selected Assignee: @samuelcolvin

@hramezani
Copy link
Member

Thanks @levsh for reporting this issue 🙏

By default extra config in pydantic-settings is forbid. That's why you see the error.

@levsh
Copy link
Author

levsh commented Aug 22, 2023

@hramezani thank you for reply.
My goal is to get a Settings model with property name 'field' and possibility of a default value from environment variable 'my_field'. With extra = 'forbid' on whole model to prevent other fields except 'field' and 'my_field'.
Settings.field should be initialized directly as init 'field' argument: Settings(field="bar") or from env variable 'my_field': Settings()
But if env varialbe 'my_field' exists it is not possible to set 'field' directly

@hramezani
Copy link
Member

Its intended behavior inPydantic. I think in this case its better to raise an error instead of losing the data.

from pydantic import AliasChoices, BaseModel, ConfigDict, Field

class Settings(BaseModel):
    model_config = ConfigDict(extra='forbid', populate_by_name=True)

    field: str = Field(alias=AliasChoices("field", "my_field"))

Settings(my_field='foo', field='bar')
"""
pydantic_core._pydantic_core.ValidationError: 1 validation error for Settings
my_field
  Extra inputs are not permitted [type=extra_forbidden, input_value='foo', input_type=str]
    For further information visit https://errors.pydantic.dev/2.2/v/extra_forbidden
"""

@levsh
Copy link
Author

levsh commented Aug 22, 2023

I migrate my application from Pydantic v1 to v2. My current code is

class Settings(BaseSettings):
    field: str = Field(env="my_field")
    class Config:
        extra="forbid"

so i can instantiate my service Settings from env variable my_field

settings = Settings()

or do it explicit

settings = Settings(field="abc")

Unfortunately, according to docs env option is removed in v2, so i can't move my current logic to v2 without change it.
Only use extra='ignore'

Thanks for the clarifications

@hramezani
Copy link
Member

Why you don't use alias instead of env:

field: str = Field(alias="my_field")

@levsh
Copy link
Author

levsh commented Aug 23, 2023

@hramezani with field: str = Field(alias="my_field")

import os
from typing import Optional
from unittest import mock
from pydantic import AliasChoices, Field
from pydantic_settings import BaseSettings

def test_settings():
    class Settings(BaseSettings):
        field: str = Field(alias="my_field")

    # 1
    env = {}
    with mock.patch.dict(os.environ, env, clear=True):
        assert Settings(field="bar").field == "bar"  # 1

    # 2
    env = {"my_field": "foo"}
    with mock.patch.dict(os.environ, env, clear=True):
        assert Settings().field == "foo"  # 2

    # 3
    env = {"my_field": "foo"}
    with mock.patch.dict(os.environ, env, clear=True):
        assert Settings(field="bar").field == "bar"  # 3

test_settings()

Exception

ValidationError                           Traceback (most recent call last)
Cell In[2], line 1
----> 1 test_settings()

Cell In[1], line 14, in test_settings()
     12 env = {}
     13 with mock.patch.dict(os.environ, env, clear=True):
---> 14     assert Settings(field="bar").field == "bar"  # 1
     16 # 2
     17 env = {"my_field": "foo"}

File ~/.pyenv/versions/3.11.4/lib/python3.11/site-packages/pydantic_settings/main.py:71, in BaseSettings.__init__(__pydantic_self__, _case_sensitive, _env_prefix, _env_file, _env_file_encoding, _env_nested_delimiter, _secrets_dir, **values)
     60 def __init__(
     61     __pydantic_self__,
     62     _case_sensitive: bool | None = None,
   (...)
     69 ) -> None:
     70     # Uses something other than `self` the first arg to allow "self" as a settable attribute
---> 71     super().__init__(
     72         **__pydantic_self__._settings_build_values(
     73             values,
     74             _case_sensitive=_case_sensitive,
     75             _env_prefix=_env_prefix,
     76             _env_file=_env_file,
     77             _env_file_encoding=_env_file_encoding,
     78             _env_nested_delimiter=_env_nested_delimiter,
     79             _secrets_dir=_secrets_dir,
     80         )
     81     )

File ~/.pyenv/versions/3.11.4/lib/python3.11/site-packages/pydantic/main.py:159, in BaseModel.__init__(__pydantic_self__, **data)
    157 # `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks
    158 __tracebackhide__ = True
--> 159 __pydantic_self__.__pydantic_validator__.validate_python(data, self_instance=__pydantic_self__)

ValidationError: 2 validation errors for Settings
my_field
  Field required [type=missing, input_value={'field': 'bar'}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.1/v/missing
field
  Extra inputs are not permitted [type=extra_forbidden, input_value='bar', input_type=str]
    For further information visit https://errors.pydantic.dev/2.1/v/extra_forbidden

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants