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

Render env_nested_delimiter #31

Merged
merged 4 commits into from
Jan 31, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .idea/runConfigurations/integration_tests.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions .idea/runConfigurations/unit_tests.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ Types of changes are:
- `foo`: explanation of foo
- `bar`: explanation of bar
```

### Features

- Added rendering of `env_prefix` from `pydantic_settings.SettingsConfigDict` by @dekkers in #29.
- Added rendering of `env_nested_delimiter` from `pydantic_settings.SettingsConfigDict`.

### Fixes

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
<a href="https://github.com/radeklat/settings-doc/tags">
<img alt="GitHub tag (latest SemVer)" src="https://img.shields.io/github/tag/radeklat/settings-doc">
</a>
<img alt="Maintenance" src="https://img.shields.io/maintenance/yes/2023">
<img alt="Maintenance" src="https://img.shields.io/maintenance/yes/2024">
<a href="https://github.com/radeklat/settings-doc/commits/main">
<img alt="GitHub last commit" src="https://img.shields.io/github/last-commit/radeklat/settings-doc">
</a>
Expand Down
37 changes: 27 additions & 10 deletions src/settings_doc/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import itertools
import logging
import re
import shutil
Expand Down Expand Up @@ -59,16 +60,32 @@ def is_typing_literal(field: FieldInfo) -> bool:
return field.annotation.__class__.__name__ == "_LiteralGenericAlias"


def _model_fields(settings: Set[Type[BaseSettings]]) -> Iterator[Tuple[str, FieldInfo]]:
for cls in settings:
for field_name, model_field in cls.model_fields.items():
if model_field.validation_alias is not None:
if isinstance(model_field.validation_alias, str):
yield model_field.validation_alias, model_field
else:
LOGGER.error(f"Unsupported validation alias type '{type(model_field.validation_alias)}'.")
def _model_fields_recursive(
cls: Type[BaseSettings], prefix: str, env_nested_delimiter: Optional[str]
) -> Iterator[Tuple[str, FieldInfo]]:
for field_name, model_field in cls.model_fields.items():
if model_field.validation_alias is not None:
if isinstance(model_field.validation_alias, str):
yield model_field.validation_alias, model_field
else:
yield cls.model_config["env_prefix"] + field_name, model_field
LOGGER.error(f"Unsupported validation alias type '{type(model_field.validation_alias)}'.")
elif (
model_field.annotation is not None
and hasattr(model_field.annotation, "model_fields")
and env_nested_delimiter is not None
):
# There are nested fields and they can be joined by a delimiter. Generate variable names recursively.
yield from _model_fields_recursive(
model_field.annotation,
prefix + field_name + env_nested_delimiter,
env_nested_delimiter,
)
else:
yield prefix + field_name, model_field


def _model_fields(cls: Type[BaseSettings]) -> Iterator[Tuple[str, FieldInfo]]:
yield from _model_fields_recursive(cls, cls.model_config["env_prefix"], cls.model_config["env_nested_delimiter"])


@app.command()
Expand Down Expand Up @@ -136,7 +153,7 @@ def generate(
secho("No sources of data were specified. Use the '--module' or '--class' options.", fg=colors.RED, err=True)
raise Abort()

fields = list(_model_fields(settings))
fields = itertools.chain.from_iterable(_model_fields(cls) for cls in settings)
classes: Dict[Type[BaseSettings], List[FieldInfo]] = {cls: list(cls.model_fields.values()) for cls in settings}

render_kwargs = {"heading_offset": heading_offset, "fields": fields, "classes": classes}
Expand Down
39 changes: 38 additions & 1 deletion tests/fixtures/valid_settings.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import Literal

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

SETTINGS_ATTR = "logging_level"
Expand Down Expand Up @@ -70,3 +70,40 @@ class EnvPrefixSettings(BaseSettings):
logging_level: str

model_config = SettingsConfigDict(env_prefix="PREFIX_")


class DeepSubModel(BaseModel):
leaf: str


class SubModel(BaseModel):
nested: str
deep: DeepSubModel


class EnvNestedDelimiterSettings(BaseSettings):
"""Expected environment variables.

- `DIRECT`
- `SUB_MODEL__NESTED`
- `SUB_MODEL__DEEP__LEAF`
"""

model_config = SettingsConfigDict(env_nested_delimiter="__")

direct: str
sub_model: SubModel


class EnvPrefixAndNestedDelimiterSettings(BaseSettings):
"""Expected environment variables.

- `PREFIX_DIRECT`
- `PREFIX_SUB_MODEL__NESTED`
- `PREFIX_SUB_MODEL__DEEP__LEAF`
"""

model_config = SettingsConfigDict(env_prefix="PREFIX_", env_nested_delimiter="__")

direct: str
sub_model: SubModel
6 changes: 6 additions & 0 deletions tests/integration/generate/test_import_module_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from tests.fixtures.module_with_single_settings_class import SingleSettingsInModule
from tests.fixtures.valid_settings import (
EmptySettings,
EnvNestedDelimiterSettings,
EnvPrefixAndNestedDelimiterSettings,
EnvPrefixSettings,
ExamplesSettings,
FullSettings,
Expand Down Expand Up @@ -46,6 +48,8 @@ class TestImportModulePath:
ValidationAliasChoicesSettings,
ExamplesSettings,
EnvPrefixSettings,
EnvNestedDelimiterSettings,
EnvPrefixAndNestedDelimiterSettings,
},
id="for a module with multiple matching classes",
),
Expand All @@ -63,6 +67,8 @@ class TestImportModulePath:
ValidationAliasChoicesSettings,
ExamplesSettings,
EnvPrefixSettings,
EnvNestedDelimiterSettings,
EnvPrefixAndNestedDelimiterSettings,
},
id="for multiple modules with multiple matching classes",
),
Expand Down
23 changes: 23 additions & 0 deletions tests/unit/test_dotenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

from tests.fixtures.valid_settings import (
SETTINGS_ATTR,
EnvNestedDelimiterSettings,
EnvPrefixAndNestedDelimiterSettings,
EnvPrefixSettings,
ExamplesSettings,
FullSettings,
PossibleValuesSettings,
Expand Down Expand Up @@ -113,3 +116,23 @@ class Settings(BaseSettings):
assert f"# `{a_value}`\n" in stdout
assert f"# - `{b_value}`\n" in stdout
assert "# `[1, 2, 3]`, `[4, 5, 6, 7]`\n" in stdout

@staticmethod
def should_show_env_prefix(runner: CliRunner, mocker: MockerFixture):
assert "prefix_logging_level=" in run_app_with_settings(mocker, runner, EnvPrefixSettings, fmt="dotenv")

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

assert "direct=" in actual_string
assert "sub_model__nested=" in actual_string
assert "sub_model__deep__leaf=" in actual_string

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

assert "prefix_direct=" in actual_string
assert "prefix_sub_model__nested=" in actual_string
assert "prefix_sub_model__deep__leaf=" in actual_string
20 changes: 19 additions & 1 deletion tests/unit/test_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from tests.fixtures.valid_settings import (
SETTINGS_MARKDOWN_FIRST_LINE,
EmptySettings,
EnvNestedDelimiterSettings,
EnvPrefixAndNestedDelimiterSettings,
EnvPrefixSettings,
ExamplesSettings,
FullSettings,
Expand Down Expand Up @@ -202,6 +204,22 @@ def should_end_with_a_single_empty_line(runner: CliRunner, mocker: MockerFixture
assert not stdout.endswith("`\n\n"), f"'{stdout}' ends with empty line"

@staticmethod
def should_include_env_prefix(runner: CliRunner, mocker: MockerFixture):
def should_show_env_prefix(runner: CliRunner, mocker: MockerFixture):
expected_string = "# `prefix_logging_level`\n\n**required**\n\n"
assert run_app_with_settings(mocker, runner, EnvPrefixSettings) == expected_string

@staticmethod
def should_generate_env_nested_delimiter(runner: CliRunner, mocker: MockerFixture):
actual_string = run_app_with_settings(mocker, runner, EnvNestedDelimiterSettings)

assert "`direct`" in actual_string
assert "`sub_model__nested`" in actual_string
assert "`sub_model__deep__leaf`" in actual_string

@staticmethod
def should_generate_env_prefix_and_nested_delimiter(runner: CliRunner, mocker: MockerFixture):
actual_string = run_app_with_settings(mocker, runner, EnvPrefixAndNestedDelimiterSettings)

assert "`prefix_direct`" in actual_string
assert "`prefix_sub_model__nested`" in actual_string
assert "`prefix_sub_model__deep__leaf`" in actual_string
Loading