|
20 | 20 |
|
21 | 21 | import os |
22 | 22 | import time |
| 23 | +from functools import cache |
| 24 | +from importlib import metadata |
23 | 25 | from typing import Final, cast |
24 | 26 |
|
25 | 27 | import ast_serialize |
|
161 | 163 |
|
162 | 164 | TypeIgnores = list[tuple[int, list[str]]] |
163 | 165 |
|
| 166 | +AST_SERIALIZE_REQUIREMENT: Final = ">=0.3.0,<1.0.0" |
| 167 | +AST_SERIALIZE_MIN_VERSION: Final = (0, 3, 0) |
| 168 | +AST_SERIALIZE_MAX_VERSION: Final = (1, 0, 0) |
| 169 | + |
| 170 | + |
| 171 | +class NativeParserError(Exception): |
| 172 | + """Raised when the native parser cannot produce compatible serialized data.""" |
| 173 | + |
| 174 | + |
| 175 | +@cache |
| 176 | +def ast_serialize_version() -> str | None: |
| 177 | + """Return the installed ast-serialize package version, if available.""" |
| 178 | + try: |
| 179 | + return metadata.version("ast-serialize") |
| 180 | + except metadata.PackageNotFoundError: |
| 181 | + return None |
| 182 | + |
| 183 | + |
| 184 | +def _parse_release(version: str) -> tuple[int, ...] | None: |
| 185 | + release = version.split("+", 1)[0].split("-", 1)[0] |
| 186 | + parts = [] |
| 187 | + for part in release.split("."): |
| 188 | + digits = "" |
| 189 | + for char in part: |
| 190 | + if not char.isdigit(): |
| 191 | + break |
| 192 | + digits += char |
| 193 | + if not digits: |
| 194 | + break |
| 195 | + parts.append(int(digits)) |
| 196 | + return tuple(parts) if parts else None |
| 197 | + |
| 198 | + |
| 199 | +def _is_release_less(left: tuple[int, ...], right: tuple[int, ...]) -> bool: |
| 200 | + size = max(len(left), len(right)) |
| 201 | + left += (0,) * (size - len(left)) |
| 202 | + right += (0,) * (size - len(right)) |
| 203 | + return left < right |
| 204 | + |
| 205 | + |
| 206 | +@cache |
| 207 | +def _check_ast_serialize_version() -> None: |
| 208 | + version = ast_serialize_version() |
| 209 | + if version is None: |
| 210 | + return |
| 211 | + release = _parse_release(version) |
| 212 | + if release is None: |
| 213 | + return |
| 214 | + if _is_release_less(release, AST_SERIALIZE_MIN_VERSION) or not _is_release_less( |
| 215 | + release, AST_SERIALIZE_MAX_VERSION |
| 216 | + ): |
| 217 | + raise NativeParserError( |
| 218 | + f"Incompatible ast-serialize version {version} is installed; " |
| 219 | + f"mypy requires ast-serialize{AST_SERIALIZE_REQUIREMENT}. " |
| 220 | + "Upgrade ast-serialize or reinstall mypy's dependencies." |
| 221 | + ) |
| 222 | + |
| 223 | + |
| 224 | +def _format_native_parser_exception(err: BaseException) -> str: |
| 225 | + detail = str(err) |
| 226 | + return f"{type(err).__name__}: {detail}" if detail else type(err).__name__ |
| 227 | + |
| 228 | + |
| 229 | +def invalid_ast_serialize_data_message(err: BaseException) -> str: |
| 230 | + version = ast_serialize_version() |
| 231 | + installed = f" (installed ast-serialize: {version})" if version is not None else "" |
| 232 | + return ( |
| 233 | + "The native parser produced serialized AST data that mypy cannot read. " |
| 234 | + "This usually means an incompatible ast-serialize version is installed" |
| 235 | + f"{installed}; mypy requires ast-serialize{AST_SERIALIZE_REQUIREMENT}. " |
| 236 | + "Upgrade ast-serialize or reinstall mypy's dependencies. " |
| 237 | + f"Original error: {_format_native_parser_exception(err)}" |
| 238 | + ) |
| 239 | + |
| 240 | + |
164 | 241 | # There is no way to create reasonable fallbacks at this stage, |
165 | 242 | # they must be patched later. |
166 | 243 | _dummy_fallback: Final = Instance(MISSING_FALLBACK, [], -1) |
@@ -257,25 +334,29 @@ def parse_to_binary_ast( |
257 | 334 | time.sleep(0.0001) # type: ignore[unreachable] |
258 | 335 | if time.time() - t0 > 10.0: |
259 | 336 | raise ImportError("Cannot import ast_serialize") |
260 | | - ast_bytes, errors, ignores, import_bytes, ast_data = ast_serialize.parse( |
261 | | - filename, |
262 | | - skip_function_bodies=skip_function_bodies, |
263 | | - python_version=options.python_version, |
264 | | - platform=options.platform, |
265 | | - always_true=options.always_true, |
266 | | - always_false=options.always_false, |
267 | | - cache_version=3, |
268 | | - ) |
269 | | - return ( |
270 | | - ast_bytes, |
271 | | - errors, |
272 | | - ignores, |
273 | | - import_bytes, |
274 | | - ast_data["is_partial_package"], |
275 | | - ast_data["uses_template_strings"], |
276 | | - ast_data["source_hash"], |
277 | | - ast_data["mypy_comments"], |
278 | | - ) |
| 337 | + _check_ast_serialize_version() |
| 338 | + try: |
| 339 | + ast_bytes, errors, ignores, import_bytes, ast_data = ast_serialize.parse( |
| 340 | + filename, |
| 341 | + skip_function_bodies=skip_function_bodies, |
| 342 | + python_version=options.python_version, |
| 343 | + platform=options.platform, |
| 344 | + always_true=options.always_true, |
| 345 | + always_false=options.always_false, |
| 346 | + cache_version=3, |
| 347 | + ) |
| 348 | + return ( |
| 349 | + ast_bytes, |
| 350 | + errors, |
| 351 | + ignores, |
| 352 | + import_bytes, |
| 353 | + ast_data["is_partial_package"], |
| 354 | + ast_data["uses_template_strings"], |
| 355 | + ast_data["source_hash"], |
| 356 | + ast_data["mypy_comments"], |
| 357 | + ) |
| 358 | + except (AssertionError, KeyError, TypeError, ValueError) as err: |
| 359 | + raise NativeParserError(invalid_ast_serialize_data_message(err)) from err |
279 | 360 |
|
280 | 361 |
|
281 | 362 | def read_statement(state: State, data: ReadBuffer) -> Statement: |
|
0 commit comments