Skip to content

Commit ff7f643

Browse files
author
Jason Munro
authored
Pydantic 2 support (#847)
* Update api utils to work with pydantic 2 * Fix api sanitize * Fix MPDataDoc creation * Lazy load all nested resters * Bump to python>=3.9 * Cache api sanitize * Cache emmet version retrieval * Fix client tests * Migrate __fields__ * Change materials test input * Move flat models util func to emmet * api_sanitize allow_dict behavior * Bump emmet * Update maggma util import * Linting and deprecated changes * Spelling * Final linting * Bump emmet * Linting * Remove repeat code * Linting * Add maggma to deps
1 parent ffd9713 commit ff7f643

10 files changed

Lines changed: 44 additions & 41 deletions

File tree

mp_api/client/core/client.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -842,7 +842,7 @@ def _submit_request_and_process(
842842
data_model(
843843
**{
844844
field: value
845-
for field, value in raw_doc.dict().items()
845+
for field, value in raw_doc.model_dump().items()
846846
if field in set_fields
847847
}
848848
)
@@ -877,29 +877,29 @@ def _submit_request_and_process(
877877

878878
def _generate_returned_model(self, doc):
879879
set_fields = [
880-
field for field, _ in doc if field in doc.dict(exclude_unset=True)
880+
field for field, _ in doc if field in doc.model_dump(exclude_unset=True)
881881
]
882-
unset_fields = [field for field in doc.__fields__ if field not in set_fields]
882+
unset_fields = [field for field in doc.model_fields if field not in set_fields]
883883

884884
data_model = create_model(
885885
"MPDataDoc",
886-
fields_not_requested=unset_fields,
886+
fields_not_requested=(list[str], unset_fields),
887887
__base__=self.document_model,
888888
)
889889

890-
data_model.__fields__ = {
890+
data_model.model_fields = {
891891
**{
892892
name: description
893-
for name, description in data_model.__fields__.items()
893+
for name, description in data_model.model_fields.items()
894894
if name in set_fields
895895
},
896-
"fields_not_requested": data_model.__fields__["fields_not_requested"],
896+
"fields_not_requested": data_model.model_fields["fields_not_requested"],
897897
}
898898

899899
def new_repr(self) -> str:
900900
extra = ",\n".join(
901901
f"\033[1m{n}\033[0;0m={getattr(self, n)!r}"
902-
for n in data_model.__fields__
902+
for n in data_model.model_fields
903903
)
904904

905905
s = f"\033[4m\033[1m{self.__class__.__name__}<{self.__class__.__base__.__name__}>\033[0;0m\033[0;0m(\n{extra}\n)" # noqa: E501
@@ -908,7 +908,7 @@ def new_repr(self) -> str:
908908
def new_str(self) -> str:
909909
extra = ",\n".join(
910910
f"\033[1m{n}\033[0;0m={getattr(self, n)!r}"
911-
for n in data_model.__fields__
911+
for n in data_model.model_fields
912912
if n != "fields_not_requested"
913913
)
914914

@@ -927,7 +927,7 @@ def new_getattr(self, attr) -> str:
927927
)
928928

929929
def new_dict(self, *args, **kwargs):
930-
d = super(data_model, self).dict(*args, **kwargs)
930+
d = super(data_model, self).model_dump(*args, **kwargs)
931931
return jsanitize(d)
932932

933933
data_model.__repr__ = new_repr
@@ -1155,7 +1155,7 @@ def count(self, criteria: dict | None = None) -> int | str:
11551155
def available_fields(self) -> list[str]:
11561156
if self.document_model is None:
11571157
return ["Unknown fields."]
1158-
return list(self.document_model.schema()["properties"].keys()) # type: ignore
1158+
return list(self.document_model.model_json_schema()["properties"].keys()) # type: ignore
11591159

11601160
def __repr__(self): # pragma: no cover
11611161
return f"<{self.__class__.__name__} {self.endpoint}>"

mp_api/client/core/settings.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
from multiprocessing import cpu_count
33
from typing import List
44

5-
from pydantic import BaseSettings, Field
5+
from pydantic import Field
6+
from pydantic_settings import BaseSettings
67
from pymatgen.core import _load_pmg_settings
78

89
from mp_api.client import __file__ as root_dir

mp_api/client/core/utils.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
import re
44
from functools import cache
5-
from typing import get_args
5+
from typing import Optional, get_args
66

7+
from maggma.utils import get_flat_models_from_model
78
from monty.json import MSONable
89
from pydantic import BaseModel
9-
from pydantic.schema import get_flat_models_from_model
10-
from pydantic.utils import lenient_issubclass
10+
from pydantic._internal._utils import lenient_issubclass
11+
from pydantic.fields import FieldInfo
1112

1213

1314
def validate_ids(id_list: list[str]):
@@ -62,33 +63,33 @@ def api_sanitize(
6263

6364
for model in models:
6465
model_fields_to_leave = {f[1] for f in fields_tuples if model.__name__ == f[0]}
65-
for name, field in model.__fields__.items():
66-
field_type = field.type_
67-
68-
if name not in model_fields_to_leave:
69-
field.required = False
70-
field.default = None
71-
field.default_factory = None
72-
field.allow_none = True
73-
field.field_info.default = None
74-
field.field_info.default_factory = None
66+
for name in model.model_fields:
67+
field = model.model_fields[name]
68+
field_type = field.annotation
7569

7670
if field_type is not None and allow_dict_msonable:
7771
if lenient_issubclass(field_type, MSONable):
78-
field.type_ = allow_msonable_dict(field_type)
72+
field_type = allow_msonable_dict(field_type)
7973
else:
8074
for sub_type in get_args(field_type):
8175
if lenient_issubclass(sub_type, MSONable):
8276
allow_msonable_dict(sub_type)
83-
field.populate_validators()
77+
78+
if name not in model_fields_to_leave:
79+
new_field = FieldInfo.from_annotated_attribute(
80+
Optional[field_type], None
81+
)
82+
model.model_fields[name] = new_field
83+
84+
model.model_rebuild(force=True)
8485

8586
return pydantic_model
8687

8788

8889
def allow_msonable_dict(monty_cls: type[MSONable]):
8990
"""Patch Monty to allow for dict values for MSONable."""
9091

91-
def validate_monty(cls, v):
92+
def validate_monty(cls, v, _):
9293
"""Stub validator for MSONable as a dictionary only."""
9394
if isinstance(v, cls):
9495
return v
@@ -110,6 +111,6 @@ def validate_monty(cls, v):
110111
else:
111112
raise ValueError(f"Must provide {cls.__name__} or MSONable dictionary")
112113

113-
monty_cls.validate_monty = classmethod(validate_monty)
114+
monty_cls.validate_monty_v2 = classmethod(validate_monty)
114115

115116
return monty_cls

mp_api/client/mprester.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,7 @@ def get_entries(
748748
if property_data:
749749
for property in property_data:
750750
entry_dict["data"][property] = (
751-
doc.dict()[property]
751+
doc.model_dump()[property]
752752
if self.use_document_model
753753
else doc[property]
754754
)

mp_api/client/routes/materials/electronic_structure.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ def get_bandstructure_from_material_id(
285285
f"No {path_type.value} band structure data found for {material_id}"
286286
)
287287
else:
288-
bs_data = bs_data.dict()
288+
bs_data = bs_data.model_dump()
289289

290290
if bs_data.get(path_type.value, None):
291291
bs_task_id = bs_data[path_type.value]["task_id"]
@@ -303,7 +303,7 @@ def get_bandstructure_from_material_id(
303303
f"No uniform band structure data found for {material_id}"
304304
)
305305
else:
306-
bs_data = bs_data.dict()
306+
bs_data = bs_data.model_dump()
307307

308308
if bs_data.get("total", None):
309309
bs_task_id = bs_data["total"]["1"]["task_id"]
@@ -444,7 +444,7 @@ def get_dos_from_material_id(self, material_id: str):
444444

445445
dos_data = es_rester.get_data_by_id(
446446
document_id=material_id, fields=["dos"]
447-
).dict()
447+
).model_dump()
448448

449449
if dos_data["dos"]:
450450
dos_task_id = dos_data["dos"]["total"]["1"]["task_id"]

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,17 @@ classifiers = [
2222
dependencies = [
2323
"setuptools",
2424
"msgpack",
25+
"maggma",
2526
"pymatgen>=2022.3.7",
2627
"typing-extensions>=3.7.4.1",
2728
"requests>=2.23.0",
28-
"monty>=2021.3.12",
29-
"emmet-core>=0.54.0",
29+
"monty>=2023.9.25",
30+
"emmet-core>=0.69.2",
3031
]
3132
dynamic = ["version"]
3233

3334
[project.optional-dependencies]
34-
all = ["emmet-core[all]>=0.54.0", "custodian", "mpcontribs-client", "boto3"]
35+
all = ["emmet-core[all]>=0.69.1", "custodian", "mpcontribs-client", "boto3"]
3536
test = [
3637
"pre-commit",
3738
"pytest",

tests/materials/core_function.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def client_search_testing(
6060
"num_chunks": 1,
6161
}
6262

63-
doc = search_method(**q)[0].dict()
63+
doc = search_method(**q)[0].model_dump()
6464

6565
for sub_field in sub_doc_fields:
6666
if sub_field in doc:

tests/materials/test_electronic_structure.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def test_bs_client(bs_rester):
9292
"chunk_size": 1,
9393
"num_chunks": 1,
9494
}
95-
doc = search_method(**q)[0].dict()
95+
doc = search_method(**q)[0].model_dump()
9696

9797
for sub_field in bs_sub_doc_fields:
9898
if sub_field in doc:
@@ -137,7 +137,7 @@ def test_dos_client(dos_rester):
137137
"chunk_size": 1,
138138
"num_chunks": 1,
139139
}
140-
doc = search_method(**q)[0].dict()
140+
doc = search_method(**q)[0].model_dump()
141141
for sub_field in dos_sub_doc_fields:
142142
if sub_field in doc:
143143
doc = doc[sub_field]

tests/molecules/core_function.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def client_search_testing(
5858
docs = search_method(**q)
5959

6060
if len(docs) > 0:
61-
doc = docs[0].dict()
61+
doc = docs[0].model_dump()
6262
else:
6363
raise ValueError("No documents returned")
6464

tests/test_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def test_generic_get_methods(rester):
7878

7979
if name not in search_only_resters:
8080
doc = rester.get_data_by_id(
81-
doc.dict()[rester.primary_key], fields=[rester.primary_key]
81+
doc.model_dump()[rester.primary_key], fields=[rester.primary_key]
8282
)
8383
assert isinstance(doc, rester.document_model)
8484

0 commit comments

Comments
 (0)