|
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, 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,76 @@ 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 _parser_source_data_yaml_backend(resolved_path: Path) -> str: |
| 224 | + if resolved_path.suffix.lower() != ".json": |
| 225 | + from datamodel_code_generator.util import get_yaml_backend # noqa: PLC0415 |
| 226 | + |
| 227 | + return get_yaml_backend() |
| 228 | + return "" |
| 229 | + |
| 230 | + |
| 231 | +def _load_parser_source_data_from_path( |
| 232 | + path: Path, |
| 233 | + encoding: str, |
| 234 | + *, |
| 235 | + cache_on_first_load: bool = True, |
| 236 | +) -> YamlValue: |
| 237 | + resolved_path = path.resolve() |
| 238 | + yaml_backend = _parser_source_data_yaml_backend(resolved_path) |
| 239 | + seen_key = (resolved_path, encoding, yaml_backend) |
| 240 | + with _parser_source_data_cache_lock: |
| 241 | + use_cache = cache_on_first_load or seen_key in _parser_source_data_seen_keys |
| 242 | + if not cache_on_first_load: |
| 243 | + _parser_source_data_seen_keys[seen_key] = None |
| 244 | + _parser_source_data_seen_keys.move_to_end(seen_key) |
| 245 | + while len(_parser_source_data_seen_keys) > _PARSER_SOURCE_DATA_CACHE_MAX_SIZE: |
| 246 | + _parser_source_data_seen_keys.popitem(last=False) |
| 247 | + |
| 248 | + data = resolved_path.read_bytes() |
| 249 | + if not use_cache: |
| 250 | + return _load_parser_source_data_from_bytes(resolved_path, data, encoding) |
| 251 | + |
| 252 | + digest = sha256(data).hexdigest() |
| 253 | + cache_key = (resolved_path, digest, encoding, yaml_backend) |
| 254 | + with _parser_source_data_cache_lock: |
| 255 | + if cache_key in _parser_source_data_cache: |
| 256 | + _parser_source_data_cache.move_to_end(cache_key) |
| 257 | + return _parser_source_data_cache[cache_key] |
| 258 | + |
| 259 | + parsed_data = _load_parser_source_data_from_bytes(resolved_path, data, encoding) |
| 260 | + with _parser_source_data_cache_lock: |
| 261 | + _parser_source_data_cache[cache_key] = parsed_data |
| 262 | + _parser_source_data_cache.move_to_end(cache_key) |
| 263 | + while len(_parser_source_data_cache) > _PARSER_SOURCE_DATA_CACHE_MAX_SIZE: |
| 264 | + _parser_source_data_cache.popitem(last=False) |
| 265 | + return parsed_data |
| 266 | + |
| 267 | + |
| 268 | +def _load_parser_source_data_from_bytes(path: Path, data: bytes, encoding: str) -> YamlValue: |
207 | 269 | import json # noqa: PLC0415 |
208 | 270 |
|
| 271 | + text = data.decode(encoding) |
209 | 272 | 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) |
| 273 | + with contextlib.suppress(json.JSONDecodeError): |
| 274 | + return json.loads(text) |
| 275 | + |
| 276 | + return load_yaml(text) |
| 277 | + |
| 278 | + |
| 279 | +def _clear_parser_source_data_cache() -> None: |
| 280 | + with _parser_source_data_cache_lock: |
| 281 | + _parser_source_data_cache.clear() |
| 282 | + _parser_source_data_seen_keys.clear() |
215 | 283 |
|
216 | 284 |
|
217 | 285 | @_lru_cache(maxsize=256) |
|
0 commit comments