|
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 | +_ParserSourceDataSeenKey: TypeAlias = tuple[Path, str] |
| 102 | +_parser_source_data_cache: OrderedDict[_ParserSourceDataCacheKey, YamlValue] = OrderedDict() |
| 103 | +_parser_source_data_seen_keys: OrderedDict[_ParserSourceDataSeenKey, None] = OrderedDict() |
| 104 | +_parser_source_data_cache_lock = RLock() |
97 | 105 |
|
98 | 106 |
|
99 | 107 | def load_yaml(stream: str | TextIO) -> YamlValue: |
@@ -202,16 +210,99 @@ def load_data_from_path(path: Path, encoding: str) -> dict[str, YamlValue]: |
202 | 210 |
|
203 | 211 | For file input: tries json.load() for .json files (more efficient than |
204 | 212 | 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. |
| 213 | + (e.g., trailing commas), and requires the parsed content to be a dict. |
| 214 | + Uses YAML for all other extensions. |
206 | 215 | """ |
| 216 | + result = _load_parser_source_data_from_path(path, encoding, cache_on_first_load=False) |
| 217 | + if not isinstance(result, dict): |
| 218 | + msg = f"Expected dict, got {type(result).__name__}" |
| 219 | + raise TypeError(msg) |
| 220 | + return result |
| 221 | + |
| 222 | + |
| 223 | +def _load_parser_source_data_from_path( |
| 224 | + path: Path, |
| 225 | + encoding: str, |
| 226 | + *, |
| 227 | + cache_on_first_load: bool = False, |
| 228 | +) -> YamlValue: |
| 229 | + resolved_path = path.resolve() |
| 230 | + seen_key = (resolved_path, encoding) |
| 231 | + with _parser_source_data_cache_lock: |
| 232 | + use_cache = cache_on_first_load or seen_key in _parser_source_data_seen_keys |
| 233 | + if not cache_on_first_load: |
| 234 | + _parser_source_data_seen_keys[seen_key] = None |
| 235 | + _parser_source_data_seen_keys.move_to_end(seen_key) |
| 236 | + while len(_parser_source_data_seen_keys) > _PARSER_SOURCE_DATA_CACHE_MAX_SIZE: |
| 237 | + _parser_source_data_seen_keys.popitem(last=False) |
| 238 | + |
| 239 | + data = resolved_path.read_bytes() |
| 240 | + if not use_cache: |
| 241 | + return _load_parser_source_data_from_bytes(resolved_path, data, encoding) |
| 242 | + |
| 243 | + digest = sha256(data).hexdigest() |
| 244 | + return _load_parser_source_data_from_bytes_with_cache(resolved_path, data, digest, encoding) |
| 245 | + |
| 246 | + |
| 247 | +def _load_cached_parser_source_data(cache_key: _ParserSourceDataCacheKey) -> YamlValue | None: |
| 248 | + with _parser_source_data_cache_lock: |
| 249 | + if cache_key in _parser_source_data_cache: |
| 250 | + _parser_source_data_cache.move_to_end(cache_key) |
| 251 | + return _parser_source_data_cache[cache_key] |
| 252 | + return None |
| 253 | + |
| 254 | + |
| 255 | +def _store_parser_source_data(cache_key: _ParserSourceDataCacheKey, parsed_data: YamlValue) -> YamlValue: |
| 256 | + with _parser_source_data_cache_lock: |
| 257 | + _parser_source_data_cache[cache_key] = parsed_data |
| 258 | + _parser_source_data_cache.move_to_end(cache_key) |
| 259 | + while len(_parser_source_data_cache) > _PARSER_SOURCE_DATA_CACHE_MAX_SIZE: |
| 260 | + _parser_source_data_cache.popitem(last=False) |
| 261 | + return parsed_data |
| 262 | + |
| 263 | + |
| 264 | +def _load_parser_source_data_from_bytes_with_cache( |
| 265 | + path: Path, |
| 266 | + data: bytes, |
| 267 | + digest: str, |
| 268 | + encoding: str, |
| 269 | +) -> YamlValue: |
207 | 270 | import json # noqa: PLC0415 |
208 | 271 |
|
| 272 | + text = data.decode(encoding) |
209 | 273 | 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) |
| 274 | + cache_key = (path, digest, encoding, "json") |
| 275 | + if (cached_data := _load_cached_parser_source_data(cache_key)) is not None: |
| 276 | + return cached_data |
| 277 | + |
| 278 | + with contextlib.suppress(json.JSONDecodeError): |
| 279 | + return _store_parser_source_data(cache_key, json.loads(text)) |
| 280 | + |
| 281 | + from datamodel_code_generator.util import get_yaml_backend # noqa: PLC0415 |
| 282 | + |
| 283 | + parser_backend = f"yaml:{get_yaml_backend()}" |
| 284 | + cache_key = (path, digest, encoding, parser_backend) |
| 285 | + if (cached_data := _load_cached_parser_source_data(cache_key)) is not None: |
| 286 | + return cached_data |
| 287 | + |
| 288 | + return _store_parser_source_data(cache_key, load_yaml(text)) |
| 289 | + |
| 290 | + |
| 291 | +def _load_parser_source_data_from_bytes(path: Path, data: bytes, encoding: str) -> YamlValue: |
| 292 | + import json # noqa: PLC0415 |
| 293 | + |
| 294 | + text = data.decode(encoding) |
| 295 | + if path.suffix.lower() == ".json": |
| 296 | + with contextlib.suppress(json.JSONDecodeError): |
| 297 | + return json.loads(text) |
| 298 | + |
| 299 | + return load_yaml(text) |
| 300 | + |
| 301 | + |
| 302 | +def _clear_parser_source_data_cache() -> None: |
| 303 | + with _parser_source_data_cache_lock: |
| 304 | + _parser_source_data_cache.clear() |
| 305 | + _parser_source_data_seen_keys.clear() |
215 | 306 |
|
216 | 307 |
|
217 | 308 | @_lru_cache(maxsize=256) |
|
0 commit comments