Skip to content

Commit 3618909

Browse files
authored
🐛 fixes schema-json in settings-CLI (ITISFoundation#2825)
* fixes schema-json in settings-CLI * added exclude-unset option
1 parent d8159e2 commit 3618909

File tree

3 files changed

+74
-32
lines changed

3 files changed

+74
-32
lines changed

packages/settings-library/src/settings_library/base.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import warnings
2-
31
from functools import cached_property
42
from typing import Sequence, get_args
53

@@ -89,9 +87,10 @@ def prepare_field(cls, field: ModelField) -> None:
8987
assert field.field_info.default is Undefined
9088
assert field.field_info.default_factory is None
9189

90+
# Transform it into something like `Field(default_factory=create_settings_from_env(field))`
9291
field.default_factory = create_settings_from_env(field)
93-
# Having a default value, makes this field automatically optional
94-
field.required = False
92+
field.default = None
93+
field.required = False # has a default now
9594

9695
elif issubclass(field_type, BaseSettings):
9796
raise ValueError(
@@ -106,10 +105,7 @@ def prepare_field(cls, field: ModelField) -> None:
106105

107106
@classmethod
108107
def create_from_envs(cls, **overrides):
109-
# Kept for legacy
108+
# Kept for legacy. Identical to the constructor.
110109
# Optional to use to make the code more readable
111110
# More explicit and pylance seems to get less confused
112-
warnings.warn(
113-
"please use constructor instead of `create_from_envs`", DeprecationWarning
114-
)
115111
return cls(**overrides)

packages/settings-library/src/settings_library/utils_cli.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@
1111
from .base import BaseCustomSettings
1212

1313

14-
def print_as_envfile(settings_obj, *, compact: bool, verbose: bool, show_secrets: bool):
14+
def print_as_envfile(
15+
settings_obj,
16+
*,
17+
compact: bool,
18+
verbose: bool,
19+
show_secrets: bool,
20+
**pydantic_export_options,
21+
):
1522
for field in settings_obj.__fields__.values():
1623

1724
value = getattr(settings_obj, field.name)
@@ -20,7 +27,7 @@ def print_as_envfile(settings_obj, *, compact: bool, verbose: bool, show_secrets
2027

2128
if isinstance(value, BaseSettings):
2229
if compact:
23-
value = f"'{value.json()}'" # flat
30+
value = f"'{value.json(**pydantic_export_options)}'" # flat
2431
else:
2532
if verbose:
2633
typer.echo(f"\n# --- {field.name} --- ")
@@ -37,8 +44,10 @@ def print_as_envfile(settings_obj, *, compact: bool, verbose: bool, show_secrets
3744
typer.echo(f"{field.name}={value}")
3845

3946

40-
def print_as_json(settings_obj, *, compact=False):
41-
typer.echo(settings_obj.json(indent=None if compact else 2))
47+
def print_as_json(settings_obj, *, compact=False, **pydantic_export_options):
48+
typer.echo(
49+
settings_obj.json(indent=None if compact else 2, **pydantic_export_options)
50+
)
4251

4352

4453
def create_settings_command(
@@ -58,8 +67,14 @@ def settings(
5867
compact: bool = typer.Option(False, help="Print compact form"),
5968
verbose: bool = False,
6069
show_secrets: bool = False,
70+
exclude_unset: bool = typer.Option(
71+
False,
72+
help="displays settings that were explicitly set"
73+
"This represents current config (i.e. required+ defaults overriden).",
74+
),
6175
):
6276
"""Resolves settings and prints envfile"""
77+
pydantic_export_options = {"exclude_unset": exclude_unset}
6378

6479
if as_json_schema:
6580
typer.echo(settings_cls.schema_json(indent=0 if compact else 2))
@@ -73,7 +88,8 @@ def settings(
7388

7489
assert logger is not None # nosec
7590
logger.error(
76-
"Invalid application settings. Typically an environment variable is missing or mistyped :\n%s",
91+
"Invalid settings. "
92+
"Typically this is due to an environment variable missing or misspelled :\n%s",
7793
"\n".join(
7894
[
7995
HEADER_STR.format("detail"),
@@ -95,13 +111,14 @@ def settings(
95111
raise
96112

97113
if as_json:
98-
print_as_json(settings_obj, compact=compact)
114+
print_as_json(settings_obj, compact=compact, **pydantic_export_options)
99115
else:
100116
print_as_envfile(
101117
settings_obj,
102118
compact=compact,
103119
verbose=verbose,
104120
show_secrets=show_secrets,
121+
**pydantic_export_options,
105122
)
106123

107124
return settings

packages/settings-library/tests/test_utils_cli.py

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,20 @@ def fake_granular_env_file_content() -> str:
7272

7373

7474
def test_compose_commands(cli: typer.Typer, cli_runner: CliRunner):
75-
result = cli_runner.invoke(cli, ["--help"])
75+
# NOTE: this tests is mostly here to raise awareness about what options
76+
# are exposed in the CLI so we can add tests if there is any update
77+
#
78+
result = cli_runner.invoke(cli, ["--help"], catch_exceptions=False)
7679
print(result.stdout)
7780
assert result.exit_code == 0, result
7881

7982
# first command
80-
result = cli_runner.invoke(cli, ["run", "--help"])
83+
result = cli_runner.invoke(cli, ["run", "--help"], catch_exceptions=False)
8184
print(result.stdout)
8285
assert result.exit_code == 0, result
8386

8487
# settings command
85-
result = cli_runner.invoke(cli, ["settings", "--help"])
88+
result = cli_runner.invoke(cli, ["settings", "--help"], catch_exceptions=False)
8689
print(result.stdout)
8790

8891
assert "--compact" in result.stdout
@@ -96,34 +99,58 @@ def extract_lines(text):
9699

97100

98101
HELP = """
99-
Usage: app settings [OPTIONS]
100-
101-
Resolves settings and prints envfile
102-
103-
Options:
104-
--as-json / --no-as-json [default: no-as-json]
105-
--as-json-schema / --no-as-json-schema
106-
[default: no-as-json-schema]
107-
--compact / --no-compact Print compact form [default: no-compact]
108-
--verbose / --no-verbose [default: no-verbose]
109-
--show-secrets / --no-show-secrets
110-
[default: no-show-secrets]
111-
--help Show this message and exit.
102+
Usage: app settings [OPTIONS]
103+
104+
Resolves settings and prints envfile
105+
106+
Options:
107+
--as-json / --no-as-json [default: no-as-json]
108+
--as-json-schema / --no-as-json-schema
109+
[default: no-as-json-schema]
110+
--compact / --no-compact Print compact form [default: no-compact]
111+
--verbose / --no-verbose [default: no-verbose]
112+
--show-secrets / --no-show-secrets
113+
[default: no-show-secrets]
114+
--exclude-unset / --no-exclude-unset
115+
displays settings that were explicitly setThis
116+
represents current config (i.e. required+
117+
defaults overriden). [default: no-exclude-
118+
unset]
119+
--help Show this message and exit.
112120
"""
113121

114122

115123
def test_settings_as_json(
116-
cli: typer.Typer, fake_settings_class, mock_environment, cli_runner: CliRunner
124+
cli: typer.Typer,
125+
fake_settings_class: Type[BaseCustomSettings],
126+
mock_environment,
127+
cli_runner: CliRunner,
117128
):
118129

119-
result = cli_runner.invoke(cli, ["settings", "--as-json"])
130+
result = cli_runner.invoke(cli, ["settings", "--as-json"], catch_exceptions=False)
120131
print(result.stdout)
121132

122133
# reuse resulting json to build settings
123134
settings: Dict = json.loads(result.stdout)
124135
assert fake_settings_class.parse_obj(settings)
125136

126137

138+
def test_settings_as_json_schema(
139+
cli: typer.Typer,
140+
fake_settings_class: Type[BaseCustomSettings],
141+
mock_environment,
142+
cli_runner: CliRunner,
143+
):
144+
145+
result = cli_runner.invoke(
146+
cli, ["settings", "--as-json-schema"], catch_exceptions=False
147+
)
148+
print(result.stdout)
149+
150+
# reuse resulting json to build settings
151+
settings_schema: Dict = json.loads(result.stdout)
152+
153+
127154
def test_cli_default_settings_envs(
128155
cli: typer.Typer,
129156
fake_settings_class: Type[BaseCustomSettings],
@@ -139,6 +166,7 @@ def test_cli_default_settings_envs(
139166
cli_settings_output = cli_runner.invoke(
140167
cli,
141168
["settings", "--show-secrets"],
169+
catch_exceptions=False,
142170
).stdout
143171

144172
# now let's use these as env vars
@@ -207,6 +235,7 @@ def test_cli_compact_settings_envs(
207235
setting_env_content_compact = cli_runner.invoke(
208236
cli,
209237
["settings", "--compact"],
238+
catch_exceptions=False,
210239
).stdout
211240

212241
# now we use these as env vars

0 commit comments

Comments
 (0)