Skip to content

Commit

Permalink
Handle nested BaseSettings without nested delimiter
Browse files Browse the repository at this point in the history
Fixes #36
  • Loading branch information
mssalvatore committed Sep 18, 2024
1 parent b3f90f2 commit cc7f771
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 0 deletions.
13 changes: 13 additions & 0 deletions src/settings_doc/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import re
import shutil
from enum import Enum, auto
from inspect import isclass
from os import listdir
from pathlib import Path
from typing import Final, Iterator
Expand Down Expand Up @@ -61,6 +62,18 @@ def _model_fields_recursive(
prefix + field_name + env_nested_delimiter,
env_nested_delimiter,
)
elif isclass(model_field.annotation) and issubclass(model_field.annotation, BaseSettings):
# There are nested fields that do not require a delimiter to be joined. Generate variable names recursively.
submodel_prefix = model_field.annotation.model_config.get("env_prefix", "")

if not submodel_prefix:
submodel_prefix = prefix + field_name + "_"

yield from _model_fields_recursive(
model_field.annotation,
submodel_prefix,
model_field.annotation.model_config.get("env_nested_delimiter", None),
)
else:
yield prefix + field_name, model_field

Expand Down
40 changes: 40 additions & 0 deletions tests/fixtures/valid_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from pydantic import AliasChoices, AliasPath, BaseModel, Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from typing_extensions import Annotated

SETTINGS_ATTR = "logging_level"
SETTINGS_MARKDOWN_FIRST_LINE = f"# `{SETTINGS_ATTR}`\n"
Expand Down Expand Up @@ -107,3 +108,42 @@ class EnvPrefixAndNestedDelimiterSettings(BaseSettings):

direct: str
sub_model: SubModel


class SimpleSubModel(BaseModel):
leaf: str


class SettingsSubModelWithSubModel(BaseSettings):
model_config = SettingsConfigDict(env_prefix="PREFIX_SUB_MODEL_", env_nested_delimiter="__")

nested: str
sub_model_2: SimpleSubModel


class SettingsWithSettingsSubModel(BaseSettings):
"""Expected environment variables.
- `PREFIX_DIRECT`
- `PREFIX_SUB_MODEL_NESTED`
- `PREFIX_SUB_MODEL_SUB_MODEL_2__LEAF`
"""

model_config = SettingsConfigDict(env_prefix="PREFIX_")

direct: str
sub_model_1: Annotated[SettingsSubModelWithSubModel, Field(default_factory=SettingsSubModelWithSubModel)]


class SettingsSubModel(BaseSettings):
model_config = SettingsConfigDict(env_prefix="")
leaf: str


class SettingsWithSettingsSubModelNoPrefixOrDelimiter(BaseSettings):
"""Expected environment variables.
- `EMPTY_LOGGING_LEVEL`
"""

my_leaf: SettingsSubModel
12 changes: 12 additions & 0 deletions tests/integration/generate/test_import_module_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
MultipleSettings,
PossibleValuesSettings,
RequiredSettings,
SettingsSubModel,
SettingsSubModelWithSubModel,
SettingsWithSettingsSubModel,
SettingsWithSettingsSubModelNoPrefixOrDelimiter,
ValidationAliasChoicesSettings,
ValidationAliasPathSettings,
ValidationAliasSettings,
Expand Down Expand Up @@ -50,6 +54,10 @@ class TestImportModulePath:
EnvPrefixSettings: None,
EnvNestedDelimiterSettings: None,
EnvPrefixAndNestedDelimiterSettings: None,
SettingsSubModelWithSubModel: None,
SettingsWithSettingsSubModel: None,
SettingsSubModel: None,
SettingsWithSettingsSubModelNoPrefixOrDelimiter: None,
},
id="for a module with multiple matching classes",
),
Expand All @@ -69,6 +77,10 @@ class TestImportModulePath:
EnvPrefixSettings: None,
EnvNestedDelimiterSettings: None,
EnvPrefixAndNestedDelimiterSettings: None,
SettingsSubModelWithSubModel: None,
SettingsWithSettingsSubModel: None,
SettingsSubModel: None,
SettingsWithSettingsSubModelNoPrefixOrDelimiter: None,
},
id="for multiple modules with multiple matching classes",
),
Expand Down
20 changes: 20 additions & 0 deletions tests/unit/test_dotenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
ExamplesSettings,
FullSettings,
PossibleValuesSettings,
SettingsWithSettingsSubModel,
SettingsWithSettingsSubModelNoPrefixOrDelimiter,
ValidationAliasChoicesSettings,
ValidationAliasPathSettings,
ValidationAliasSettings,
Expand Down Expand Up @@ -138,6 +140,24 @@ def should_generate_env_prefix_and_nested_delimiter(runner: CliRunner, mocker: M
assert "prefix_sub_model__nested=" in actual_string
assert "prefix_sub_model__deep__leaf=" in actual_string

@staticmethod
def should_generate_settings_with_settings_sub_model(runner: CliRunner, mocker: MockerFixture):
actual_string = run_app_with_settings(mocker, runner, SettingsWithSettingsSubModel, fmt="dotenv")

assert "prefix_direct=" in actual_string
assert "prefix_sub_model_nested=" in actual_string
assert "prefix_sub_model_sub_model_2__leaf=" in actual_string

@staticmethod
def should_generate_settings_with_settings_sub_model_no_prefix_or_delimiter(
runner: CliRunner, mocker: MockerFixture
):
actual_string = run_app_with_settings(
mocker, runner, SettingsWithSettingsSubModelNoPrefixOrDelimiter, fmt="dotenv"
)

assert "my_leaf_leaf=" in actual_string


@pytest.mark.skipif(sys.version_info < (3, 11), reason="StrEnum is not available in Python 3.10 and below")
class TestDotEnvFormatFromStrEnum:
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/test_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
MultipleSettings,
PossibleValuesSettings,
RequiredSettings,
SettingsWithSettingsSubModel,
SettingsWithSettingsSubModelNoPrefixOrDelimiter,
ValidationAliasChoicesSettings,
ValidationAliasPathSettings,
ValidationAliasSettings,
Expand Down Expand Up @@ -225,6 +227,22 @@ def should_generate_env_prefix_and_nested_delimiter(runner: CliRunner, mocker: M
assert "`prefix_sub_model__nested`" in actual_string
assert "`prefix_sub_model__deep__leaf`" in actual_string

@staticmethod
def should_generate_settings_with_settings_sub_model(runner: CliRunner, mocker: MockerFixture):
actual_string = run_app_with_settings(mocker, runner, SettingsWithSettingsSubModel)

assert "`prefix_direct`" in actual_string
assert "`prefix_sub_model_nested`" in actual_string
assert "`prefix_sub_model_sub_model_2__leaf`" in actual_string

@staticmethod
def should_generate_settings_with_settings_sub_model_no_prefix_or_delimiter(
runner: CliRunner, mocker: MockerFixture
):
actual_string = run_app_with_settings(mocker, runner, SettingsWithSettingsSubModelNoPrefixOrDelimiter)

assert "`my_leaf_leaf`" in actual_string


@pytest.mark.skipif(sys.version_info < (3, 11), reason="StrEnum is not available in Python 3.10 and below")
class TestMarkdownFormatFromStrEnum:
Expand Down

0 comments on commit cc7f771

Please sign in to comment.