|
9 | 9 | import contextlib |
10 | 10 | import os |
11 | 11 | import sys |
12 | | -from collections import defaultdict |
| 12 | +from collections import OrderedDict, defaultdict |
13 | 13 | from collections.abc import Callable, Iterator, Mapping |
14 | 14 | from datetime import datetime, timezone |
15 | 15 | from functools import lru_cache as _lru_cache |
| 16 | +from hashlib import sha256 |
16 | 17 | from pathlib import Path |
| 18 | +from threading import RLock |
17 | 19 | from typing import ( |
18 | 20 | IO, |
19 | 21 | TYPE_CHECKING, |
|
94 | 96 |
|
95 | 97 | DEFAULT_BASE_CLASS: str = "pydantic.BaseModel" |
96 | 98 | _IGNORED_TEXT_PREFIX_CHARS: frozenset[str] = frozenset({"\ufeff", " ", "\t", "\r", "\n"}) |
| 99 | +_PARSER_SOURCE_DATA_CACHE_MAX_SIZE = 128 |
| 100 | +_ParserSourceDataCacheKey: TypeAlias = tuple[Path, str, str, str] |
| 101 | +_parser_source_data_cache: OrderedDict[_ParserSourceDataCacheKey, YamlValue] = OrderedDict() |
| 102 | +_parser_source_data_cache_lock = RLock() |
97 | 103 |
|
98 | 104 |
|
99 | 105 | def load_yaml(stream: str | TextIO) -> YamlValue: |
@@ -202,16 +208,55 @@ def load_data_from_path(path: Path, encoding: str) -> dict[str, YamlValue]: |
202 | 208 |
|
203 | 209 | For file input: tries json.load() for .json files (more efficient than |
204 | 210 | read_text + json.loads), falls back to YAML if JSON parsing fails |
205 | | - (e.g., trailing commas) or if content is not a dict. Uses YAML for all other extensions. |
| 211 | + (e.g., trailing commas), and requires the parsed content to be a dict. |
| 212 | + Uses YAML for all other extensions. |
206 | 213 | """ |
| 214 | + result = _load_parser_source_data_from_path(path, encoding) |
| 215 | + if not isinstance(result, dict): |
| 216 | + msg = f"Expected dict, got {type(result).__name__}" |
| 217 | + raise TypeError(msg) |
| 218 | + return result |
| 219 | + |
| 220 | + |
| 221 | +def _load_parser_source_data_from_path(path: Path, encoding: str) -> YamlValue: |
| 222 | + resolved_path = path.resolve() |
| 223 | + data = resolved_path.read_bytes() |
| 224 | + digest = sha256(data).hexdigest() |
| 225 | + yaml_backend = "" |
| 226 | + if resolved_path.suffix.lower() != ".json": |
| 227 | + from datamodel_code_generator.util import get_yaml_backend # noqa: PLC0415 |
| 228 | + |
| 229 | + yaml_backend = get_yaml_backend() |
| 230 | + |
| 231 | + cache_key = (resolved_path, digest, encoding, yaml_backend) |
| 232 | + with _parser_source_data_cache_lock: |
| 233 | + if cache_key in _parser_source_data_cache: |
| 234 | + _parser_source_data_cache.move_to_end(cache_key) |
| 235 | + return _parser_source_data_cache[cache_key] |
| 236 | + |
| 237 | + parsed_data = _load_parser_source_data_from_bytes(resolved_path, data, encoding) |
| 238 | + with _parser_source_data_cache_lock: |
| 239 | + _parser_source_data_cache[cache_key] = parsed_data |
| 240 | + _parser_source_data_cache.move_to_end(cache_key) |
| 241 | + while len(_parser_source_data_cache) > _PARSER_SOURCE_DATA_CACHE_MAX_SIZE: |
| 242 | + _parser_source_data_cache.popitem(last=False) |
| 243 | + return parsed_data |
| 244 | + |
| 245 | + |
| 246 | +def _load_parser_source_data_from_bytes(path: Path, data: bytes, encoding: str) -> YamlValue: |
207 | 247 | import json # noqa: PLC0415 |
208 | 248 |
|
| 249 | + text = data.decode(encoding) |
209 | 250 | if path.suffix.lower() == ".json": |
210 | | - with contextlib.suppress(json.JSONDecodeError), path.open(encoding=encoding) as f: |
211 | | - result = json.load(f) |
212 | | - if isinstance(result, dict): |
213 | | - return result |
214 | | - return load_yaml_dict_from_path(path, encoding) |
| 251 | + with contextlib.suppress(json.JSONDecodeError): |
| 252 | + return json.loads(text) |
| 253 | + |
| 254 | + return load_yaml(text) |
| 255 | + |
| 256 | + |
| 257 | +def _clear_parser_source_data_cache() -> None: |
| 258 | + with _parser_source_data_cache_lock: |
| 259 | + _parser_source_data_cache.clear() |
215 | 260 |
|
216 | 261 |
|
217 | 262 | @_lru_cache(maxsize=256) |
|
0 commit comments