Skip to content

Commit 4a4a84f

Browse files
committed
Use .model_dump(mode='json') to create a real dict
Using EnvConfig.from_dict() creates an EnvConfig object that is not always preferable. The above method call creates a real dict with all objects resolved to basic types like strings, bools, int etc.
1 parent ed9a105 commit 4a4a84f

File tree

2 files changed

+50
-44
lines changed

2 files changed

+50
-44
lines changed

src/docbuild/cli/cmd_cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ def cli(
183183
try:
184184
# Pydantic validation handles placeholder replacement via @model_validator
185185
# The result is the validated Pydantic object, stored in context.envconfig
186-
context.envconfig = EnvConfig.from_dict(raw_envconfig)
186+
context.envconfig = EnvConfig.from_dict(raw_envconfig).model_dump(mode='json')
187187
except (ValueError, ValidationError) as e:
188188
log.error(
189189
"Environment configuration failed validation: "

tests/cli/test_cmd_cli.py

Lines changed: 49 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import docbuild.cli.cmd_cli as cli_mod
99
from docbuild.cli.context import DocBuildContext
1010
from docbuild.models.config_model.app import AppConfig
11-
from docbuild.models.config_model.env import EnvConfig
11+
from docbuild.models.config_model.env import EnvConfig
1212

1313
cli = cli_mod.cli
1414

@@ -25,14 +25,12 @@ def capture(ctx):
2525

2626
@pytest.fixture
2727
def mock_config_models(monkeypatch):
28-
"""
29-
Fixture to mock AppConfig.from_dict and EnvConfig.from_dict.
30-
28+
"""Fixture to mock AppConfig.from_dict and EnvConfig.from_dict.
29+
3130
Ensures the mock AppConfig instance has the necessary
3231
logging attributes and methods (.logging.model_dump) that the CLI calls
3332
during setup_logging.
3433
"""
35-
3634
# Mock the nested logging attribute and its model_dump method
3735
mock_logging_dump = Mock(return_value={'version': 1, 'log_setup': True})
3836
mock_logging_attribute = Mock()
@@ -42,21 +40,26 @@ def mock_config_models(monkeypatch):
4240
mock_app_instance = Mock(spec=AppConfig)
4341
# Assign the mock logging attribute to the app instance
4442
mock_app_instance.logging = mock_logging_attribute
45-
43+
4644
# Env config mock doesn't need logging setup
45+
# The CLI calls .model_dump() on the instance, so we mock that call.
46+
mock_env_dump = Mock(return_value={'env_data': 'from_mock_dump'})
4747
mock_env_instance = Mock(spec=EnvConfig)
48-
48+
mock_env_instance.model_dump.return_value = mock_env_dump
49+
4950
# Mock the static methods that perform validation
5051
mock_app_from_dict = Mock(return_value=mock_app_instance)
5152
mock_env_from_dict = Mock(return_value=mock_env_instance)
52-
53+
54+
5355
# Patch the actual classes
5456
monkeypatch.setattr(AppConfig, 'from_dict', mock_app_from_dict)
5557
monkeypatch.setattr(EnvConfig, 'from_dict', mock_env_from_dict)
56-
58+
5759
return {
5860
'app_instance': mock_app_instance,
5961
'env_instance': mock_env_instance,
62+
'env_dump': mock_env_dump,
6063
'app_from_dict': mock_app_from_dict,
6164
'env_from_dict': mock_env_from_dict,
6265
}
@@ -79,26 +82,29 @@ def fake_handle_config(user_path, *a, **kw):
7982
return (Path('default_env.toml'),), {'env_data': 'from_default'}, True
8083

8184
monkeypatch.setattr(cli_mod, 'handle_config', fake_handle_config)
82-
85+
8386
context = DocBuildContext()
84-
87+
8588
result = runner.invoke(
86-
cli,
89+
cli,
8790
['--app-config', str(app_file), 'capture'],
8891
obj=context,
8992
catch_exceptions=False
9093
)
91-
94+
9295
assert result.exit_code == 0
9396
assert 'capture' in result.output.strip()
94-
97+
9598
# Assert that the raw data was passed to the Pydantic models
9699
mock_config_models['app_from_dict'].assert_called_once()
97100
mock_config_models['env_from_dict'].assert_called_once()
98-
99-
# Assert that the context now holds the MOCKED VALIDATED OBJECTS
101+
102+
# Assert that the context holds the app's Pydantic model instance
100103
assert context.appconfig is mock_config_models['app_instance']
101-
assert context.envconfig is mock_config_models['env_instance']
104+
105+
# Assert that the context holds the DICTIONARY from model_dump()
106+
# for the environment config.
107+
assert context.envconfig is mock_config_models['env_dump']
102108
assert context.envconfig_from_defaults is True
103109

104110

@@ -107,15 +113,15 @@ def test_cli_with_app_and_env_config(monkeypatch, runner, tmp_path, mock_config_
107113
# Create real temporary files for Click to validate
108114
app_file = tmp_path / 'app.toml'
109115
env_file = tmp_path / 'env.toml'
110-
app_file.write_text('[logging]\nversion=1')
116+
app_file.write_text('[logging]\nversion=1')
111117
env_file.write_text('dummy = true')
112118

113119
def fake_handle_config(user_path, *a, **kw):
114120
if str(user_path) == str(app_file):
115121
return (app_file,), {'logging': {'version': 1}}, False
116122
if str(user_path) == str(env_file):
117123
return (env_file,), {'server': {'host': '1.2.3.4'}}, False
118-
return (None,), {'default_data': 'default_content'}, True
124+
return (None,), {'default_data': 'default_content'}, True
119125

120126
monkeypatch.setattr(cli_mod, 'handle_config', fake_handle_config)
121127

@@ -130,9 +136,9 @@ def fake_handle_config(user_path, *a, **kw):
130136
'capture',
131137
],
132138
obj=context,
133-
catch_exceptions=False,
139+
catch_exceptions=False,
134140
)
135-
141+
136142
# Check for success and context variables
137143
assert result.exit_code == 0
138144

@@ -141,7 +147,7 @@ def fake_handle_config(user_path, *a, **kw):
141147

142148
# Assert that the context now holds the MOCKED VALIDATED OBJECTS
143149
assert context.appconfig is mock_config_models['app_instance']
144-
assert context.envconfig is mock_config_models['env_instance']
150+
assert context.envconfig is mock_config_models['env_dump']
145151
assert context.envconfigfiles == (env_file,)
146152
assert context.envconfig_from_defaults is False
147153

@@ -151,44 +157,44 @@ def test_cli_config_validation_failure(
151157
monkeypatch, runner, tmp_path, mock_config_models, is_app_config_failure
152158
):
153159
"""Test that the CLI handles Pydantic validation errors gracefully for both configs."""
154-
160+
155161
app_file = tmp_path / 'app.toml'
156-
app_file.write_text('bad data')
157-
162+
app_file.write_text('bad data')
163+
158164
# 1. Mock the log.error function to check output
159165
mock_log_error = Mock()
160166
monkeypatch.setattr(cli_mod.log, 'error', mock_log_error)
161167

162168
# 2. Configure the Pydantic mocks to simulate failure
163169
mock_validation_error = ValidationError.from_exception_data(
164-
'TestModel',
170+
'TestModel',
165171
[
166172
{
167-
'type': 'int_parsing',
173+
'type': 'int_parsing',
168174
'loc': ('server', 'port'),
169175
'input': 'not_an_int',
170176
}
171177
]
172178
)
173-
179+
174180
# Define the simple error structure that the CLI error formatting relies on:
175181
MOCK_ERROR_DETAIL = {
176-
'loc': ('server', 'port'),
177-
'msg': 'value is not a valid integer (mocked)',
182+
'loc': ('server', 'port'),
183+
'msg': 'value is not a valid integer (mocked)',
178184
'input': 'not_an_int'
179185
}
180186

181-
187+
182188
if is_app_config_failure:
183189
mock_config_models['app_from_dict'].side_effect = mock_validation_error
184190
else:
185191
mock_config_models['env_from_dict'].side_effect = mock_validation_error
186-
192+
187193
# 3. Mock handle_config to return raw data successfully (no file read error)
188194
def fake_handle_config(user_path, *a, **kw):
189195
if user_path == app_file:
190196
return (app_file,), {'raw_app_data': 'x'}, False
191-
return (Path('env.toml'),), {'raw_env_data': 'y'}, False
197+
return (Path('env.toml'),), {'raw_env_data': 'y'}, False
192198

193199
monkeypatch.setattr(cli_mod, 'handle_config', fake_handle_config)
194200

@@ -199,28 +205,28 @@ def fake_handle_config(user_path, *a, **kw):
199205
obj=context,
200206
catch_exceptions=True,
201207
)
202-
208+
203209
# 4. Assertions
204-
assert result.exit_code == 1
205-
210+
assert result.exit_code == 1
211+
206212
if is_app_config_failure:
207213
assert 'Application configuration failed validation' in mock_log_error.call_args_list[0][0][0]
208214
else:
209215
assert 'Environment configuration failed validation' in mock_log_error.call_args_list[0][0][0]
210-
216+
211217
# --- REMOVE FRAGILE ASSERTIONS ON LOG CALL COUNT ---
212-
# assert mock_log_error.call_count > 1
218+
# assert mock_log_error.call_count > 1
213219
# assert mock_log_error.call_count >= 2
214220
# assert any("Field: (" in call[0][0] for call in mock_log_error.call_args_list)
215221

216-
assert mock_log_error.call_count >= 1
222+
assert mock_log_error.call_count >= 1
217223

218224

219225
def test_cli_verbose_and_debug(monkeypatch, runner, tmp_path, mock_config_models):
220226
"""Test that verbosity and debug flags are passed correctly to context."""
221227
# Create a real temporary file for Click to validate
222228
app_file = tmp_path / 'app.toml'
223-
app_file.write_text('[logging]\nversion=1')
229+
app_file.write_text('[logging]\nversion=1')
224230

225231
def fake_handle_config(user_path, *a, **kw):
226232
if user_path == app_file:
@@ -237,13 +243,13 @@ def fake_handle_config(user_path, *a, **kw):
237243
obj=context,
238244
catch_exceptions=False,
239245
)
240-
246+
241247
# Check for success and context variables
242248
assert result.exit_code == 0
243249
assert 'capture\n' in result.output
244250
assert context.verbose == 3
245251
assert context.debug is True
246-
252+
247253
# Assertions on config structure must now reference the MOCKED Pydantic objects
248254
assert context.appconfig is mock_config_models['app_instance']
249-
assert context.envconfig is mock_config_models['env_instance']
255+
assert context.envconfig is mock_config_models['env_dump']

0 commit comments

Comments
 (0)