From c3daf0fda2358feda5aa8f1d8dfc0a7754894192 Mon Sep 17 00:00:00 2001 From: "Dr. Jan Paskarbeit" Date: Sat, 13 Jul 2024 21:46:21 +0200 Subject: [PATCH 1/6] Added check for complexity of RootModel's root type. --- pydantic_settings/sources.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index ec9f604c..9f73e4f0 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -31,7 +31,7 @@ import typing_extensions from dotenv import dotenv_values -from pydantic import AliasChoices, AliasPath, BaseModel, Json +from pydantic import AliasChoices, AliasPath, BaseModel, Json, RootModel from pydantic._internal._repr import Representation from pydantic._internal._typing_extra import WithArgsTypes, origin_is_union, typing_base from pydantic._internal._utils import deep_update, is_model_class, lenient_issubclass @@ -183,6 +183,8 @@ def field_is_complex(self, field: FieldInfo) -> bool: Returns: Whether the field is complex. """ + if issubclass(field.annotation, RootModel): + return _annotation_is_complex(field.annotation.__annotations__['root'], field.metadata) return _annotation_is_complex(field.annotation, field.metadata) def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any: From 5b1d2ae8fad158cafa0b05c287439be7e20f3fb3 Mon Sep 17 00:00:00 2001 From: "Dr. Jan Paskarbeit" Date: Sat, 13 Jul 2024 21:47:10 +0200 Subject: [PATCH 2/6] Added tests for non-complex RootModel types. --- tests/test_settings.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/test_settings.py b/tests/test_settings.py index f0888e3b..8bbd552f 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -2,6 +2,7 @@ import dataclasses import json import os +import pathlib import re import sys import typing @@ -2167,6 +2168,43 @@ class Settings(BaseSettings): assert s.model_dump() == {'z': [{'x': 1, 'y': {'foo': 1}}, {'x': 2, 'y': {'foo': 2}}]} +def test_str_based_root_model(env): + """Testing to pass string directly to root model.""" + + class Foo(RootModel[str]): + root: str + + class Settings(BaseSettings): + foo: Foo + plain: str + + TEST_STR = 'hello world' + env.set('foo', TEST_STR) + env.set('plain', TEST_STR) + s = Settings() + assert s.model_dump() == {'foo': TEST_STR, 'plain': TEST_STR} + + +def test_path_based_root_model(env): + """Testing to pass path directly to root model.""" + + class Foo(RootModel[pathlib.PurePosixPath]): + root: pathlib.PurePosixPath + + class Settings(BaseSettings): + foo: Foo + plain: pathlib.PurePosixPath + + TEST_PATH: str = '/hello/world' + env.set('foo', TEST_PATH) + env.set('plain', TEST_PATH) + s = Settings() + assert s.model_dump() == { + 'foo': pathlib.PurePosixPath(TEST_PATH), + 'plain': pathlib.PurePosixPath(TEST_PATH), + } + + def test_optional_field_from_env(env): class Settings(BaseSettings): x: Optional[str] = None From c529c3ddc16ae4c428b21243d3011c520b932db2 Mon Sep 17 00:00:00 2001 From: "Dr. Jan Paskarbeit" Date: Sat, 13 Jul 2024 22:10:32 +0200 Subject: [PATCH 3/6] Added check for None as field annotation. --- pydantic_settings/sources.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index 9f73e4f0..1f08008b 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -183,7 +183,7 @@ def field_is_complex(self, field: FieldInfo) -> bool: Returns: Whether the field is complex. """ - if issubclass(field.annotation, RootModel): + if field.annotation is not None and issubclass(field.annotation, RootModel): return _annotation_is_complex(field.annotation.__annotations__['root'], field.metadata) return _annotation_is_complex(field.annotation, field.metadata) From 4a01bff783b593736a755a466a236545a9343121 Mon Sep 17 00:00:00 2001 From: "Dr. Jan Paskarbeit" Date: Sun, 14 Jul 2024 14:57:18 +0200 Subject: [PATCH 4/6] Added some more checks. --- pydantic_settings/sources.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index 1f08008b..db0fa2df 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -183,8 +183,14 @@ def field_is_complex(self, field: FieldInfo) -> bool: Returns: Whether the field is complex. """ - if field.annotation is not None and issubclass(field.annotation, RootModel): - return _annotation_is_complex(field.annotation.__annotations__['root'], field.metadata) + # Making sure the field annotation is really a class before checking whether + # it is a subclass of RootModel. + if isinstance(field.annotation, type) and issubclass(field.annotation, RootModel): + # In some rare cases (see test_root_model_as_field), + # the root attribute is not available. + root_annotation = field.annotation.__annotations__.get('root', None) + if root_annotation is not None: + return _annotation_is_complex(root_annotation, field.metadata) return _annotation_is_complex(field.annotation, field.metadata) def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any: From 5a50a73769957056f012ea70c66a8f92c79f7ae2 Mon Sep 17 00:00:00 2001 From: "Dr. Jan Paskarbeit" Date: Tue, 13 Aug 2024 08:09:11 +0000 Subject: [PATCH 5/6] Fixed checks for python 3.8 and 3.9. --- pydantic_settings/sources.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index db0fa2df..860ccc87 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -183,14 +183,6 @@ def field_is_complex(self, field: FieldInfo) -> bool: Returns: Whether the field is complex. """ - # Making sure the field annotation is really a class before checking whether - # it is a subclass of RootModel. - if isinstance(field.annotation, type) and issubclass(field.annotation, RootModel): - # In some rare cases (see test_root_model_as_field), - # the root attribute is not available. - root_annotation = field.annotation.__annotations__.get('root', None) - if root_annotation is not None: - return _annotation_is_complex(root_annotation, field.metadata) return _annotation_is_complex(field.annotation, field.metadata) def prepare_field_value(self, field_name: str, field: FieldInfo, value: Any, value_is_complex: bool) -> Any: @@ -1776,6 +1768,16 @@ def read_env_file( def _annotation_is_complex(annotation: type[Any] | None, metadata: list[Any]) -> bool: + # If the model is a root model, the root annotation should be used to + # evaluate the complexity. + if isinstance(annotation, type) and issubclass(annotation, RootModel): + # In some rare cases (see test_root_model_as_field), + # the root attribute is not available. For these cases, python 3.8 and 3.9 + # return 'RootModelRootType'. + root_annotation = annotation.__annotations__.get('root', None) + if root_annotation is not None and root_annotation != 'RootModelRootType': + annotation = root_annotation + if any(isinstance(md, Json) for md in metadata): # type: ignore[misc] return False # Check if annotation is of the form Annotated[type, metadata]. From 8ba15f09b0ccd8c3226aed627961c3fb0c777aa4 Mon Sep 17 00:00:00 2001 From: "Dr. Jan Paskarbeit" Date: Tue, 13 Aug 2024 08:14:53 +0000 Subject: [PATCH 6/6] Fixed formatting. --- pydantic_settings/sources.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index 860ccc87..02c34e5d 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -1768,12 +1768,12 @@ def read_env_file( def _annotation_is_complex(annotation: type[Any] | None, metadata: list[Any]) -> bool: - # If the model is a root model, the root annotation should be used to + # If the model is a root model, the root annotation should be used to # evaluate the complexity. if isinstance(annotation, type) and issubclass(annotation, RootModel): # In some rare cases (see test_root_model_as_field), # the root attribute is not available. For these cases, python 3.8 and 3.9 - # return 'RootModelRootType'. + # return 'RootModelRootType'. root_annotation = annotation.__annotations__.get('root', None) if root_annotation is not None and root_annotation != 'RootModelRootType': annotation = root_annotation