Skip to content

Commit cb7ee07

Browse files
cosmo-grantCosmo Grant
andauthored
fix: respect --extra-fields option in pydantic v2 models (#2423)
* test --extra-fields with pydantic_v2 * fix how extra fields are handled in pydantic_v2 * refactor to bypass pyright error * improve test coverage --------- Co-authored-by: Cosmo Grant <cosmo.grant@postcodelottery.co.uk>
1 parent 0886c0e commit cb7ee07

9 files changed

Lines changed: 252 additions & 9 deletions

File tree

src/datamodel_code_generator/model/pydantic_v2/base_model.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -232,9 +232,20 @@ def __init__( # noqa: PLR0913
232232
self.extra_template_data["config"] = ConfigDict.parse_obj(config_parameters) # pyright: ignore[reportArgumentType]
233233
self._additional_imports.append(IMPORT_CONFIG_DICT)
234234

235-
def _get_config_extra(self) -> Literal["'allow'", "'forbid'"] | None:
235+
def _get_config_extra(self) -> Literal["'allow'", "'forbid'", "'ignore'"] | None:
236236
additional_properties = self.extra_template_data.get("additionalProperties")
237237
allow_extra_fields = self.extra_template_data.get("allow_extra_fields")
238-
if additional_properties is not None or allow_extra_fields:
239-
return "'allow'" if additional_properties or allow_extra_fields else "'forbid'"
240-
return None
238+
extra_fields = self.extra_template_data.get("extra_fields")
239+
240+
config_extra = None
241+
if allow_extra_fields or extra_fields == "allow":
242+
config_extra = "'allow'"
243+
elif extra_fields == "forbid":
244+
config_extra = "'forbid'"
245+
elif extra_fields == "ignore":
246+
config_extra = "'ignore'"
247+
elif additional_properties is True:
248+
config_extra = "'allow'"
249+
elif additional_properties is False:
250+
config_extra = "'forbid'"
251+
return config_extra
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# generated by datamodel-codegen:
2+
# filename: extra_fields.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from pydantic import BaseModel, Extra
10+
11+
12+
class Foo(BaseModel):
13+
class Config:
14+
extra = Extra.allow
15+
16+
x: Optional[int] = None
17+
18+
19+
class Bar(BaseModel):
20+
class Config:
21+
extra = Extra.allow
22+
23+
y: Optional[int] = None
24+
25+
26+
class Baz(BaseModel):
27+
class Config:
28+
extra = Extra.allow
29+
30+
z: Optional[int] = None
31+
32+
33+
class Test(BaseModel):
34+
class Config:
35+
extra = Extra.allow
36+
37+
foo: Foo
38+
bar: Optional[Bar] = None
39+
baz: Optional[Baz] = None
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# generated by datamodel-codegen:
2+
# filename: extra_fields.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from pydantic import BaseModel, Extra
10+
11+
12+
class Foo(BaseModel):
13+
class Config:
14+
extra = Extra.forbid
15+
16+
x: Optional[int] = None
17+
18+
19+
class Bar(BaseModel):
20+
class Config:
21+
extra = Extra.forbid
22+
23+
y: Optional[int] = None
24+
25+
26+
class Baz(BaseModel):
27+
class Config:
28+
extra = Extra.forbid
29+
30+
z: Optional[int] = None
31+
32+
33+
class Test(BaseModel):
34+
class Config:
35+
extra = Extra.forbid
36+
37+
foo: Foo
38+
bar: Optional[Bar] = None
39+
baz: Optional[Baz] = None

tests/data/expected/main/jsonschema/extra_fields_ignore.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# generated by datamodel-codegen:
2-
# filename: extra_fields_ignore.json
2+
# filename: extra_fields.json
33
# timestamp: 2019-07-26T00:00:00+00:00
44

55
from __future__ import annotations
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# generated by datamodel-codegen:
2+
# filename: extra_fields.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from pydantic import BaseModel, ConfigDict
10+
11+
12+
class Foo(BaseModel):
13+
model_config = ConfigDict(
14+
extra='allow',
15+
)
16+
x: Optional[int] = None
17+
18+
19+
class Bar(BaseModel):
20+
model_config = ConfigDict(
21+
extra='allow',
22+
)
23+
y: Optional[int] = None
24+
25+
26+
class Baz(BaseModel):
27+
model_config = ConfigDict(
28+
extra='allow',
29+
)
30+
z: Optional[int] = None
31+
32+
33+
class Test(BaseModel):
34+
model_config = ConfigDict(
35+
extra='allow',
36+
)
37+
foo: Foo
38+
bar: Optional[Bar] = None
39+
baz: Optional[Baz] = None
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# generated by datamodel-codegen:
2+
# filename: extra_fields.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from pydantic import BaseModel, ConfigDict
10+
11+
12+
class Foo(BaseModel):
13+
model_config = ConfigDict(
14+
extra='forbid',
15+
)
16+
x: Optional[int] = None
17+
18+
19+
class Bar(BaseModel):
20+
model_config = ConfigDict(
21+
extra='forbid',
22+
)
23+
y: Optional[int] = None
24+
25+
26+
class Baz(BaseModel):
27+
model_config = ConfigDict(
28+
extra='forbid',
29+
)
30+
z: Optional[int] = None
31+
32+
33+
class Test(BaseModel):
34+
model_config = ConfigDict(
35+
extra='forbid',
36+
)
37+
foo: Foo
38+
bar: Optional[Bar] = None
39+
baz: Optional[Baz] = None
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# generated by datamodel-codegen:
2+
# filename: extra_fields.json
3+
# timestamp: 2019-07-26T00:00:00+00:00
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional
8+
9+
from pydantic import BaseModel, ConfigDict
10+
11+
12+
class Foo(BaseModel):
13+
model_config = ConfigDict(
14+
extra='ignore',
15+
)
16+
x: Optional[int] = None
17+
18+
19+
class Bar(BaseModel):
20+
model_config = ConfigDict(
21+
extra='ignore',
22+
)
23+
y: Optional[int] = None
24+
25+
26+
class Baz(BaseModel):
27+
model_config = ConfigDict(
28+
extra='ignore',
29+
)
30+
z: Optional[int] = None
31+
32+
33+
class Test(BaseModel):
34+
model_config = ConfigDict(
35+
extra='ignore',
36+
)
37+
foo: Foo
38+
bar: Optional[Bar] = None
39+
baz: Optional[Baz] = None
File renamed without changes.

tests/main/jsonschema/test_main_jsonschema.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3249,19 +3249,56 @@ def test_main_json_pointer_percent_encoded_segments() -> None:
32493249
assert "".join(result.split()) == "".join(expected.split())
32503250

32513251

3252+
@pytest.mark.parametrize(
3253+
("extra_fields", "output_model", "expected_output"),
3254+
[
3255+
(
3256+
"allow",
3257+
"pydantic.BaseModel",
3258+
"extra_fields_allow.py",
3259+
),
3260+
(
3261+
"forbid",
3262+
"pydantic.BaseModel",
3263+
"extra_fields_forbid.py",
3264+
),
3265+
(
3266+
"ignore",
3267+
"pydantic.BaseModel",
3268+
"extra_fields_ignore.py",
3269+
),
3270+
(
3271+
"allow",
3272+
"pydantic_v2.BaseModel",
3273+
"extra_fields_v2_allow.py",
3274+
),
3275+
(
3276+
"forbid",
3277+
"pydantic_v2.BaseModel",
3278+
"extra_fields_v2_forbid.py",
3279+
),
3280+
(
3281+
"ignore",
3282+
"pydantic_v2.BaseModel",
3283+
"extra_fields_v2_ignore.py",
3284+
),
3285+
],
3286+
)
32523287
@freeze_time("2019-07-26")
3253-
def test_main_extra_fields_ignore() -> None:
3288+
def test_main_extra_fields(extra_fields: str, output_model: str, expected_output: str) -> None:
32543289
with TemporaryDirectory() as output_dir:
32553290
output_file: Path = Path(output_dir) / "output.py"
32563291
return_code: Exit = main([
32573292
"--input",
3258-
str(JSON_SCHEMA_DATA_PATH / "extra_fields_ignore.json"),
3293+
str(JSON_SCHEMA_DATA_PATH / "extra_fields.json"),
32593294
"--output",
32603295
str(output_file),
32613296
"--input-file-type",
32623297
"jsonschema",
32633298
"--extra-fields",
3264-
"ignore",
3299+
extra_fields,
3300+
"--output-model-type",
3301+
output_model,
32653302
])
32663303
assert return_code == Exit.OK
3267-
assert output_file.read_text() == (EXPECTED_JSON_SCHEMA_PATH / "extra_fields_ignore.py").read_text()
3304+
assert output_file.read_text() == (EXPECTED_JSON_SCHEMA_PATH / expected_output).read_text()

0 commit comments

Comments
 (0)