Skip to content

Commit fa173ab

Browse files
authored
Add more pydantic utilities (#148)
1 parent d50bea2 commit fa173ab

3 files changed

Lines changed: 123 additions & 13 deletions

File tree

src/pystow/utils/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,15 @@
8282
ModelValidateFailureAction,
8383
iter_pydantic_jsonl,
8484
iter_pydantic_tsv,
85+
model_dump_yaml,
8586
read_pydantic_json,
8687
read_pydantic_jsonl,
8788
read_pydantic_tsv,
8889
read_pydantic_yaml,
8990
stream_write_pydantic_jsonl,
91+
write_pydantic_json,
9092
write_pydantic_jsonl,
93+
write_pydantic_yaml,
9194
)
9295
from .safe_open import (
9396
is_url,
@@ -97,6 +100,8 @@
97100
safe_open_dict_reader,
98101
safe_open_json,
99102
safe_open_yaml,
103+
write_json,
104+
write_yaml,
100105
)
101106
from ..constants import README_TEXT, TimeoutHint
102107

@@ -153,6 +158,7 @@
153158
"mkdir",
154159
"mock_envvar",
155160
"mock_home",
161+
"model_dump_yaml",
156162
"n",
157163
"name_from_s3_key",
158164
"name_from_url",
@@ -188,11 +194,15 @@
188194
"stream_write_pydantic_jsonl",
189195
"tarfile_writestr",
190196
"use_appdirs",
197+
"write_json",
191198
"write_lzma_csv",
192199
"write_pickle_gz",
200+
"write_pydantic_json",
193201
"write_pydantic_jsonl",
202+
"write_pydantic_yaml",
194203
"write_tarfile_csv",
195204
"write_tarfile_xml",
205+
"write_yaml",
196206
"write_zipfile_csv",
197207
"write_zipfile_np",
198208
"write_zipfile_rdf",

src/pystow/utils/pydantic_utils.py

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@
1010

1111
from tqdm import tqdm
1212

13-
from .safe_open import safe_open, safe_open_dict_reader, safe_open_json, safe_open_yaml
13+
from .safe_open import (
14+
_open_read_text,
15+
_open_write_text,
16+
safe_open_dict_reader,
17+
safe_open_json,
18+
safe_open_yaml,
19+
write_json,
20+
write_yaml,
21+
)
1422

1523
if TYPE_CHECKING:
1624
import pydantic
@@ -19,12 +27,15 @@
1927
"ModelValidateFailureAction",
2028
"iter_pydantic_jsonl",
2129
"iter_pydantic_tsv",
30+
"model_dump_yaml",
2231
"read_pydantic_json",
2332
"read_pydantic_jsonl",
2433
"read_pydantic_tsv",
2534
"read_pydantic_yaml",
2635
"stream_write_pydantic_jsonl",
36+
"write_pydantic_json",
2737
"write_pydantic_jsonl",
38+
"write_pydantic_yaml",
2839
]
2940

3041
logger = logging.getLogger(__name__)
@@ -55,9 +66,7 @@ def iter_pydantic_jsonl(
5566
}
5667
if tqdm_kwargs is not None:
5768
_tqdm_kwargs.update(tqdm_kwargs)
58-
with safe_open(
59-
file, operation="read", representation="text", encoding=encoding, newline=newline
60-
) as file:
69+
with _open_read_text(file, encoding=encoding, newline=newline) as file:
6170
for i, line in enumerate(tqdm(file, disable=not progress, **_tqdm_kwargs)):
6271
try:
6372
yv = model_cls.model_validate_json(line.strip())
@@ -85,7 +94,7 @@ def write_pydantic_jsonl(
8594
kwargs.setdefault("exclude_none", True)
8695
kwargs.setdefault("exclude_unset", True)
8796
kwargs.setdefault("exclude_defaults", True)
88-
with safe_open(file, operation="write", representation="text") as file:
97+
with _open_write_text(file) as file:
8998
for model in models:
9099
file.write(model.model_dump_json(**kwargs) + "\n")
91100

@@ -97,7 +106,7 @@ def stream_write_pydantic_jsonl(
97106
kwargs.setdefault("exclude_none", True)
98107
kwargs.setdefault("exclude_unset", True)
99108
kwargs.setdefault("exclude_defaults", True)
100-
with safe_open(file, operation="write", representation="text") as file:
109+
with _open_write_text(file) as file:
101110
for model in models:
102111
file.write(model.model_dump_json(**kwargs) + "\n")
103112
yield model
@@ -161,3 +170,44 @@ def read_pydantic_yaml(
161170
) -> BaseModelVar:
162171
"""Read a YAML file into a model."""
163172
return model_cls.model_validate(safe_open_yaml(path_or_url, encoding=encoding, newline=newline))
173+
174+
175+
def model_dump_yaml(
176+
model: pydantic.BaseModel,
177+
*,
178+
exclude_none: bool = False,
179+
exclude_unset: bool = False,
180+
) -> str:
181+
"""Dump the model as YAML string."""
182+
import yaml
183+
184+
data = model.model_dump(mode="json", exclude_none=exclude_none, exclude_unset=exclude_unset)
185+
return yaml.safe_dump(data)
186+
187+
188+
def write_pydantic_yaml(
189+
model: pydantic.BaseModel,
190+
path: str | Path | TextIO,
191+
*,
192+
exclude_none: bool = False,
193+
exclude_unset: bool = False,
194+
encoding: str | None = None,
195+
newline: str | None = None,
196+
) -> None:
197+
"""Write a model to a YAML file."""
198+
data = model_dump_yaml(model, exclude_none=exclude_none, exclude_unset=exclude_unset)
199+
write_yaml(data, path, encoding=encoding, newline=newline)
200+
201+
202+
def write_pydantic_json(
203+
model: pydantic.BaseModel,
204+
path: str | Path | TextIO,
205+
*,
206+
exclude_none: bool = False,
207+
exclude_unset: bool = False,
208+
encoding: str | None = None,
209+
newline: str | None = None,
210+
) -> None:
211+
"""Write a model to a JSON file."""
212+
data = model.model_dump(mode="json", exclude_none=exclude_none, exclude_unset=exclude_unset)
213+
write_json(data, path, encoding=encoding, newline=newline)

src/pystow/utils/safe_open.py

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
"safe_open_dict_reader",
3636
"safe_open_json",
3737
"safe_open_yaml",
38+
"write_json",
39+
"write_yaml",
3840
]
3941

4042

@@ -142,16 +144,38 @@ def safe_open( # noqa:C901
142144
raise TypeError(f"unsupported type for opening: {type(path)} - {path}")
143145

144146

147+
@contextlib.contextmanager
148+
def _open_read_text(
149+
path: str | Path | TextIO,
150+
encoding: str | None = None,
151+
newline: str | None = None,
152+
) -> Generator[typing.TextIO, None, None]:
153+
with safe_open(
154+
path, representation="text", operation="read", encoding=encoding, newline=newline
155+
) as file:
156+
yield file
157+
158+
159+
@contextlib.contextmanager
160+
def _open_write_text(
161+
path: str | Path | TextIO,
162+
encoding: str | None = None,
163+
newline: str | None = None,
164+
) -> Generator[typing.TextIO, None, None]:
165+
with safe_open(
166+
path, representation="text", operation="write", encoding=encoding, newline=newline
167+
) as file:
168+
yield file
169+
170+
145171
def safe_open_json(
146172
path_or_url: str | Path | TextIO,
147173
*,
148174
encoding: str | None = None,
149175
newline: str | None = None,
150176
) -> Any:
151177
"""Safely open a file and parse as JSON."""
152-
with safe_open(
153-
path_or_url, representation="text", operation="read", encoding=encoding, newline=newline
154-
) as file:
178+
with _open_read_text(path_or_url, encoding=encoding, newline=newline) as file:
155179
return json.load(file)
156180

157181

@@ -164,12 +188,38 @@ def safe_open_yaml(
164188
"""Safely open a file and parse as YAML."""
165189
import yaml
166190

167-
with safe_open(
168-
path_or_url, representation="text", operation="read", encoding=encoding, newline=newline
169-
) as file:
191+
with _open_read_text(path_or_url, encoding=encoding, newline=newline) as file:
170192
return yaml.safe_load(file)
171193

172194

195+
def write_yaml(
196+
data: Any,
197+
path: str | Path | TextIO,
198+
*,
199+
encoding: str | None = None,
200+
newline: str | None = None,
201+
**kwargs: Any,
202+
) -> Any:
203+
"""Write YAML to a file."""
204+
import yaml
205+
206+
with _open_write_text(path, encoding=encoding, newline=newline) as file:
207+
yaml.safe_dump(data, file, **kwargs)
208+
209+
210+
def write_json(
211+
data: Any,
212+
path: str | Path | TextIO,
213+
*,
214+
encoding: str | None = None,
215+
newline: str | None = None,
216+
**kwargs: Any,
217+
) -> Any:
218+
"""Write JSON to a file."""
219+
with _open_write_text(path, encoding=encoding, newline=newline) as file:
220+
json.dump(data, file, **kwargs)
221+
222+
173223
# docstr-coverage:excused `overload`
174224
@typing.overload
175225
@contextlib.contextmanager
@@ -237,7 +287,7 @@ def safe_open_dict_reader(
237287
238288
:yields: A CSV reader object, constructed from :func:`csv.DictReader`
239289
"""
240-
with safe_open(f, operation="read", representation="text") as file:
290+
with _open_read_text(f) as file:
241291
yield csv.DictReader(file, delimiter=delimiter, **kwargs)
242292

243293

0 commit comments

Comments
 (0)