Skip to content

Commit 92e771a

Browse files
authored
Updates for pydantic v2 (#619)
1 parent 2b873ef commit 92e771a

File tree

10 files changed

+83
-90
lines changed

10 files changed

+83
-90
lines changed

ci/environment-docs.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dependencies:
1414
- myst-nb
1515
- netcdf4!=1.6.1
1616
- pip
17-
- pydantic>=1.9
17+
- pydantic>=2.0
1818
- python-graphviz
1919
- python=3.11
2020
- s3fs >=2023.05
@@ -27,6 +27,6 @@ dependencies:
2727
- zarr>=2.12
2828
- furo>=2022.09.15
2929
- pip:
30+
- git+https://github.com/ncar-xdev/ecgtools
3031
- sphinxext-opengraph
31-
- autodoc_pydantic
3232
- -e ..

ci/environment-upstream-dev.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ dependencies:
1818
- pooch
1919
- pre-commit
2020
- psutil
21-
- pydantic>=1.9
21+
- pydantic>=2.0
2222
- pydap
2323
- pyproj
2424
- pytest

ci/environment.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ dependencies:
1616
- pip
1717
- pooch
1818
- pre-commit
19-
- pydantic>=1.9
19+
- pydantic>=2.0
2020
- pydap
2121
- pytest
2222
- pytest-cov

docs/source/conf.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
'myst_nb',
1717
'sphinxext.opengraph',
1818
'sphinx_copybutton',
19-
'sphinxcontrib.autodoc_pydantic',
2019
'sphinx_design',
2120
]
2221

@@ -29,9 +28,6 @@
2928
copybutton_prompt_text = r'>>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: '
3029
copybutton_prompt_is_regexp = True
3130

32-
autodoc_pydantic_model_show_json = True
33-
autodoc_pydantic_model_show_config = False
34-
3531
nb_execution_mode = 'cache'
3632
nb_execution_timeout = 600
3733
nb_execution_raise_on_error = True

docs/source/reference/api.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,19 @@ For more details and examples, refer to the relevant chapters in the main part o
2424
## ESM Catalog
2525

2626
```{eval-rst}
27-
.. autopydantic_model:: intake_esm.cat.ESMCatalogModel
27+
.. autoclass:: intake_esm.cat.ESMCatalogModel
28+
:members:
29+
:noindex:
30+
:special-members: __init__
2831
```
2932

3033
## Query Model
3134

3235
```{eval-rst}
33-
.. autopydantic_model:: intake_esm.cat.QueryModel
36+
.. autoclass:: intake_esm.cat.QueryModel
37+
:members:
38+
:noindex:
39+
:special-members: __init__
3440
```
3541

3642
## Derived Variable Registry

intake_esm/cat.py

Lines changed: 41 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import pandas as pd
1010
import pydantic
1111
import tlz
12+
from pydantic import ConfigDict
1213

1314
from ._search import search, search_apply_require_all_on
1415

@@ -40,9 +41,7 @@ class AggregationType(str, enum.Enum):
4041
join_existing = 'join_existing'
4142
union = 'union'
4243

43-
class Config:
44-
validate_all = True
45-
validate_assignment = True
44+
model_config = ConfigDict(validate_default=True, validate_assignment=True)
4645

4746

4847
class DataFormat(str, enum.Enum):
@@ -51,57 +50,47 @@ class DataFormat(str, enum.Enum):
5150
reference = 'reference'
5251
opendap = 'opendap'
5352

54-
class Config:
55-
validate_all = True
56-
validate_assignment = True
53+
model_config = ConfigDict(validate_default=True, validate_assignment=True)
5754

5855

5956
class Attribute(pydantic.BaseModel):
6057
column_name: pydantic.StrictStr
6158
vocabulary: pydantic.StrictStr = ''
6259

63-
class Config:
64-
validate_all = True
65-
validate_assignment = True
60+
model_config = ConfigDict(validate_default=True, validate_assignment=True)
6661

6762

6863
class Assets(pydantic.BaseModel):
6964
column_name: pydantic.StrictStr
70-
format: typing.Optional[DataFormat]
71-
format_column_name: typing.Optional[pydantic.StrictStr]
65+
format: typing.Optional[DataFormat] = None
66+
format_column_name: typing.Optional[pydantic.StrictStr] = None
7267

73-
class Config:
74-
validate_all = True
75-
validate_assignment = True
68+
model_config = ConfigDict(validate_default=True, validate_assignment=True)
7669

77-
@pydantic.root_validator
78-
def _validate_data_format(cls, values):
79-
data_format, format_column_name = values.get('format'), values.get('format_column_name')
70+
@pydantic.model_validator(mode='after')
71+
def _validate_data_format(cls, model):
72+
data_format, format_column_name = model.format, model.format_column_name
8073
if data_format is not None and format_column_name is not None:
8174
raise ValueError('Cannot set both format and format_column_name')
8275
elif data_format is None and format_column_name is None:
8376
raise ValueError('Must set one of format or format_column_name')
84-
return values
77+
return model
8578

8679

8780
class Aggregation(pydantic.BaseModel):
8881
type: AggregationType
8982
attribute_name: pydantic.StrictStr
90-
options: typing.Optional[dict] = {}
83+
options: dict = {}
9184

92-
class Config:
93-
validate_all = True
94-
validate_assignment = True
85+
model_config = ConfigDict(validate_default=True, validate_assignment=True)
9586

9687

9788
class AggregationControl(pydantic.BaseModel):
9889
variable_column_name: pydantic.StrictStr
9990
groupby_attrs: list[pydantic.StrictStr]
10091
aggregations: list[Aggregation] = []
10192

102-
class Config:
103-
validate_all = True
104-
validate_assignment = True
93+
model_config = ConfigDict(validate_default=True, validate_assignment=True)
10594

10695

10796
class ESMCatalogModel(pydantic.BaseModel):
@@ -113,35 +102,33 @@ class ESMCatalogModel(pydantic.BaseModel):
113102
attributes: list[Attribute]
114103
assets: Assets
115104
aggregation_control: typing.Optional[AggregationControl] = None
116-
id: typing.Optional[str] = ''
105+
id: str = ''
117106
catalog_dict: typing.Optional[list[dict]] = None
118-
catalog_file: pydantic.StrictStr = None
119-
description: pydantic.StrictStr = None
120-
title: pydantic.StrictStr = None
107+
catalog_file: typing.Optional[pydantic.StrictStr] = None
108+
description: typing.Optional[pydantic.StrictStr] = None
109+
title: typing.Optional[pydantic.StrictStr] = None
121110
last_updated: typing.Optional[typing.Union[datetime.datetime, datetime.date]] = None
122-
_df: typing.Optional[pd.DataFrame] = pydantic.PrivateAttr()
111+
_df: pd.DataFrame = pydantic.PrivateAttr()
123112

124-
class Config:
125-
arbitrary_types_allowed = True
126-
underscore_attrs_are_private = True
127-
validate_all = True
128-
validate_assignment = True
113+
model_config = ConfigDict(
114+
arbitrary_types_allowed=True, validate_default=True, validate_assignment=True
115+
)
129116

130-
@pydantic.root_validator
131-
def validate_catalog(cls, values):
132-
catalog_dict, catalog_file = values.get('catalog_dict'), values.get('catalog_file')
117+
@pydantic.model_validator(mode='after')
118+
def validate_catalog(cls, model):
119+
catalog_dict, catalog_file = model.catalog_dict, model.catalog_file
133120
if catalog_dict is not None and catalog_file is not None:
134121
raise ValueError('catalog_dict and catalog_file cannot be set at the same time')
135122

136-
return values
123+
return model
137124

138125
@classmethod
139126
def from_dict(cls, data: dict) -> 'ESMCatalogModel':
140127
esmcat = data['esmcat']
141128
df = data['df']
142129
if 'last_updated' not in esmcat:
143130
esmcat['last_updated'] = None
144-
cat = cls.parse_obj(esmcat)
131+
cat = cls.model_validate(esmcat)
145132
cat._df = df
146133
return cat
147134

@@ -254,7 +241,7 @@ def load(
254241
data = json.loads(fobj.read())
255242
if 'last_updated' not in data:
256243
data['last_updated'] = None
257-
cat = cls.parse_obj(data)
244+
cat = cls.model_validate(data)
258245
if cat.catalog_file:
259246
if _mapper.fs.exists(cat.catalog_file):
260247
csv_path = cat.catalog_file
@@ -417,32 +404,32 @@ class QueryModel(pydantic.BaseModel):
417404

418405
query: dict[pydantic.StrictStr, typing.Union[typing.Any, list[typing.Any]]]
419406
columns: list[str]
420-
require_all_on: typing.Union[str, list[typing.Any]] = None
407+
require_all_on: typing.Optional[typing.Union[str, list[typing.Any]]] = None
421408

422-
class Config:
423-
validate_all = True
424-
validate_assignment = True
409+
# TODO: Seem to be unable to modify fields in model_validator with
410+
# validate_assignment=True since it leads to recursion
411+
model_config = ConfigDict(validate_default=True, validate_assignment=False)
425412

426-
@pydantic.root_validator(pre=False)
427-
def validate_query(cls, values):
428-
query = values.get('query', {})
429-
columns = values.get('columns')
430-
require_all_on = values.get('require_all_on', [])
413+
@pydantic.model_validator(mode='after')
414+
def validate_query(cls, model):
415+
query = model.query
416+
columns = model.columns
417+
require_all_on = model.require_all_on
431418

432419
if query:
433420
for key in query:
434421
if key not in columns:
435422
raise ValueError(f'Column {key} not in columns {columns}')
436423
if isinstance(require_all_on, str):
437-
values['require_all_on'] = [require_all_on]
424+
model.require_all_on = [require_all_on]
438425
if require_all_on is not None:
439-
for key in values['require_all_on']:
426+
for key in model.require_all_on:
440427
if key not in columns:
441428
raise ValueError(f'Column {key} not in columns {columns}')
442429
_query = query.copy()
443430
for key, value in _query.items():
444431
if isinstance(value, (str, int, float, bool)) or value is None or value is pd.NA:
445432
_query[key] = [value]
446433

447-
values['query'] = _query
448-
return values
434+
model.query = _query
435+
return model

intake_esm/core.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,11 @@ def _ipython_key_completions_(self):
329329
return self.__dir__()
330330

331331
@pydantic.validate_arguments
332-
def search(self, require_all_on: typing.Union[str, list[str]] = None, **query: typing.Any):
332+
def search(
333+
self,
334+
require_all_on: typing.Optional[typing.Union[str, list[str]]] = None,
335+
**query: typing.Any,
336+
):
333337
"""Search for entries in the catalog.
334338
335339
Parameters
@@ -443,11 +447,11 @@ def search(self, require_all_on: typing.Union[str, list[str]] = None, **query: t
443447
def serialize(
444448
self,
445449
name: pydantic.StrictStr,
446-
directory: typing.Union[pydantic.DirectoryPath, pydantic.StrictStr] = None,
450+
directory: typing.Optional[typing.Union[pydantic.DirectoryPath, pydantic.StrictStr]] = None,
447451
catalog_type: str = 'dict',
448-
to_csv_kwargs: dict[typing.Any, typing.Any] = None,
449-
json_dump_kwargs: dict[typing.Any, typing.Any] = None,
450-
storage_options: dict[str, typing.Any] = None,
452+
to_csv_kwargs: typing.Optional[dict[typing.Any, typing.Any]] = None,
453+
json_dump_kwargs: typing.Optional[dict[typing.Any, typing.Any]] = None,
454+
storage_options: typing.Optional[dict[str, typing.Any]] = None,
451455
) -> None:
452456
"""Serialize catalog to corresponding json and csv files.
453457
@@ -536,12 +540,12 @@ def unique(self) -> pd.Series:
536540
@pydantic.validate_arguments
537541
def to_dataset_dict(
538542
self,
539-
xarray_open_kwargs: dict[str, typing.Any] = None,
540-
xarray_combine_by_coords_kwargs: dict[str, typing.Any] = None,
541-
preprocess: typing.Callable = None,
542-
storage_options: dict[pydantic.StrictStr, typing.Any] = None,
543-
progressbar: pydantic.StrictBool = None,
544-
aggregate: pydantic.StrictBool = None,
543+
xarray_open_kwargs: typing.Optional[dict[str, typing.Any]] = None,
544+
xarray_combine_by_coords_kwargs: typing.Optional[dict[str, typing.Any]] = None,
545+
preprocess: typing.Optional[typing.Callable] = None,
546+
storage_options: typing.Optional[dict[pydantic.StrictStr, typing.Any]] = None,
547+
progressbar: typing.Optional[pydantic.StrictBool] = None,
548+
aggregate: typing.Optional[pydantic.StrictBool] = None,
545549
skip_on_error: pydantic.StrictBool = False,
546550
**kwargs,
547551
) -> dict[str, xr.Dataset]:
@@ -686,12 +690,12 @@ def to_dataset_dict(
686690
@pydantic.validate_arguments
687691
def to_datatree(
688692
self,
689-
xarray_open_kwargs: dict[str, typing.Any] = None,
690-
xarray_combine_by_coords_kwargs: dict[str, typing.Any] = None,
691-
preprocess: typing.Callable = None,
692-
storage_options: dict[pydantic.StrictStr, typing.Any] = None,
693-
progressbar: pydantic.StrictBool = None,
694-
aggregate: pydantic.StrictBool = None,
693+
xarray_open_kwargs: typing.Optional[dict[str, typing.Any]] = None,
694+
xarray_combine_by_coords_kwargs: typing.Optional[dict[str, typing.Any]] = None,
695+
preprocess: typing.Optional[typing.Callable] = None,
696+
storage_options: typing.Optional[dict[pydantic.StrictStr, typing.Any]] = None,
697+
progressbar: typing.Optional[pydantic.StrictBool] = None,
698+
aggregate: typing.Optional[pydantic.StrictBool] = None,
695699
skip_on_error: pydantic.StrictBool = False,
696700
**kwargs,
697701
):

intake_esm/derived.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class DerivedVariable(pydantic.BaseModel):
1717
query: dict[pydantic.StrictStr, typing.Union[typing.Any, list[typing.Any]]]
1818
prefer_derived: bool
1919

20-
@pydantic.validator('query')
20+
@pydantic.field_validator('query')
2121
def validate_query(cls, values):
2222
_query = values.copy()
2323
for key, value in _query.items():
@@ -46,7 +46,7 @@ def __call__(self, *args, variable_key_name: str = None, **kwargs) -> xr.Dataset
4646
class DerivedVariableRegistry:
4747
"""Registry of derived variables"""
4848

49-
def __post_init_post_parse__(self):
49+
def __post_init__(self):
5050
self._registry = {}
5151

5252
@classmethod

intake_esm/source.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,12 @@ def __init__(
136136
*,
137137
variable_column_name: typing.Optional[pydantic.StrictStr] = None,
138138
aggregations: typing.Optional[list[Aggregation]] = None,
139-
requested_variables: list[str] = None,
140-
preprocess: typing.Callable = None,
141-
storage_options: dict[str, typing.Any] = None,
142-
xarray_open_kwargs: dict[str, typing.Any] = None,
143-
xarray_combine_by_coords_kwargs: dict[str, typing.Any] = None,
144-
intake_kwargs: dict[str, typing.Any] = None,
139+
requested_variables: typing.Optional[list[str]] = None,
140+
preprocess: typing.Optional[typing.Callable] = None,
141+
storage_options: typing.Optional[dict[str, typing.Any]] = None,
142+
xarray_open_kwargs: typing.Optional[dict[str, typing.Any]] = None,
143+
xarray_combine_by_coords_kwargs: typing.Optional[dict[str, typing.Any]] = None,
144+
intake_kwargs: typing.Optional[dict[str, typing.Any]] = None,
145145
):
146146
"""An intake compatible Data Source for ESM data.
147147

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ netCDF4>=1.5.5
66
requests>=2.24.0
77
xarray>=2022.06
88
zarr>=2.12
9-
pydantic>=1.9
9+
pydantic>=2.0

0 commit comments

Comments
 (0)