From d0686d0b16d50160dd4630d9b66994bfaf9e0801 Mon Sep 17 00:00:00 2001 From: jinhyuk9714 Date: Wed, 20 May 2026 23:57:19 +0900 Subject: [PATCH 1/2] Report incompatible ast-serialize versions clearly --- mypy/nativeparse.py | 57 +++++++++++++++++++++++++++++++++++ mypy/test/test_nativeparse.py | 51 +++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/mypy/nativeparse.py b/mypy/nativeparse.py index d048e9bce65e2..31b921d91ef4a 100644 --- a/mypy/nativeparse.py +++ b/mypy/nativeparse.py @@ -18,6 +18,7 @@ from __future__ import annotations +import importlib.metadata import os import time from typing import Final, cast @@ -47,6 +48,7 @@ read_str_opt, read_tag, ) +from mypy.errors import CompileError from mypy.nodes import ( ARG_KINDS, ARG_POS, @@ -164,6 +166,59 @@ # There is no way to create reasonable fallbacks at this stage, # they must be patched later. _dummy_fallback: Final = Instance(MISSING_FALLBACK, [], -1) +_AST_SERIALIZE_REQUIREMENT: Final = "ast-serialize>=0.5.0,<1.0.0" +_AST_SERIALIZE_MIN_VERSION: Final = (0, 5, 0) +_AST_SERIALIZE_MAX_VERSION: Final = (1, 0, 0) +_ast_serialize_version_checked = False + + +def _parse_ast_serialize_version(version: str) -> tuple[int, int, int] | None: + release = version.split("+", 1)[0].split("-", 1)[0] + components = release.split(".") + if len(components) > 3: + components = components[:3] + + parts: list[int] = [] + for component in components: + if not component.isdigit(): + return None + parts.append(int(component)) + + while len(parts) < 3: + parts.append(0) + return parts[0], parts[1], parts[2] + + +def _validate_ast_serialize_version() -> None: + global _ast_serialize_version_checked + if _ast_serialize_version_checked: + return + + try: + version = importlib.metadata.version("ast-serialize") + except importlib.metadata.PackageNotFoundError: + _ast_serialize_version_checked = True + return + + parsed_version = _parse_ast_serialize_version(version) + if parsed_version is None: + _ast_serialize_version_checked = True + return + if parsed_version < _AST_SERIALIZE_MIN_VERSION: + raise CompileError( + [ + f"mypy: error: ast-serialize {version} is too old; " + f"mypy requires {_AST_SERIALIZE_REQUIREMENT}" + ] + ) + if parsed_version >= _AST_SERIALIZE_MAX_VERSION: + raise CompileError( + [ + f"mypy: error: ast-serialize {version} is too new; " + f"mypy requires {_AST_SERIALIZE_REQUIREMENT}" + ] + ) + _ast_serialize_version_checked = True class State: @@ -250,6 +305,8 @@ def read_statements(state: State, data: ReadBuffer, n: int) -> list[Statement]: def parse_to_binary_ast( filename: str, options: Options, skip_function_bodies: bool = False ) -> tuple[bytes, list[ParseError], TypeIgnores, bytes, bool, bool, str, list[tuple[int, str]]]: + _validate_ast_serialize_version() + # This is a horrible hack to work around a mypyc bug where imported # module may be not ready in a thread sometimes. t0 = time.time() diff --git a/mypy/test/test_nativeparse.py b/mypy/test/test_nativeparse.py index b50da5f5d02c7..e8fd8be4378f0 100644 --- a/mypy/test/test_nativeparse.py +++ b/mypy/test/test_nativeparse.py @@ -7,10 +7,12 @@ from __future__ import annotations import contextlib +import importlib.metadata import os import tempfile import unittest from collections.abc import Iterator +from unittest.mock import patch from librt.internal import ReadBuffer @@ -36,6 +38,7 @@ # If the experimental ast_serialize module isn't installed, the following import will fail # and we won't run any native parser tests. try: + from mypy import nativeparse from mypy.nativeparse import ( State, deserialize_imports, @@ -232,6 +235,54 @@ def format_reachable_imports(node: MypyFile) -> list[str]: return output +@unittest.skipUnless(has_nativeparse, "nativeparse not available") +class TestNativeParserVersion(unittest.TestCase): + def setUp(self) -> None: + nativeparse._ast_serialize_version_checked = False + + def tearDown(self) -> None: + nativeparse._ast_serialize_version_checked = False + + def test_ast_serialize_version_accepts_minimum(self) -> None: + with patch("importlib.metadata.version", return_value="0.5.0"): + nativeparse._validate_ast_serialize_version() + + def test_ast_serialize_version_reports_old_version(self) -> None: + for version in ["0.2.3", "0.4.0"]: + with self.subTest(version=version): + with patch("importlib.metadata.version", return_value=version): + with self.assertRaises(CompileError) as context: + nativeparse._validate_ast_serialize_version() + + self.assertEqual( + context.exception.messages, + [ + f"mypy: error: ast-serialize {version} is too old; " + "mypy requires ast-serialize>=0.5.0,<1.0.0" + ], + ) + + def test_ast_serialize_version_reports_too_new_version(self) -> None: + with patch("importlib.metadata.version", return_value="1.0.0"): + with self.assertRaises(CompileError) as context: + nativeparse._validate_ast_serialize_version() + + self.assertEqual( + context.exception.messages, + [ + "mypy: error: ast-serialize 1.0.0 is too new; " + "mypy requires ast-serialize>=0.5.0,<1.0.0" + ], + ) + + def test_ast_serialize_version_ignores_missing_metadata(self) -> None: + with patch( + "importlib.metadata.version", + side_effect=importlib.metadata.PackageNotFoundError("ast-serialize"), + ): + nativeparse._validate_ast_serialize_version() + + @unittest.skipUnless(has_nativeparse, "nativeparse not available") class TestNativeParserBinaryFormat(unittest.TestCase): def test_trivial_binary_data(self) -> None: From c76f10dc1ce3cefd0b80010823b21a90f6fa1a4e Mon Sep 17 00:00:00 2001 From: jinhyuk9714 Date: Thu, 21 May 2026 00:38:09 +0900 Subject: [PATCH 2/2] Avoid native parser version cache global --- mypy/nativeparse.py | 13 +++---------- mypy/test/test_nativeparse.py | 10 ++-------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/mypy/nativeparse.py b/mypy/nativeparse.py index 31b921d91ef4a..83e2163c65d92 100644 --- a/mypy/nativeparse.py +++ b/mypy/nativeparse.py @@ -18,7 +18,6 @@ from __future__ import annotations -import importlib.metadata import os import time from typing import Final, cast @@ -169,7 +168,6 @@ _AST_SERIALIZE_REQUIREMENT: Final = "ast-serialize>=0.5.0,<1.0.0" _AST_SERIALIZE_MIN_VERSION: Final = (0, 5, 0) _AST_SERIALIZE_MAX_VERSION: Final = (1, 0, 0) -_ast_serialize_version_checked = False def _parse_ast_serialize_version(version: str) -> tuple[int, int, int] | None: @@ -190,19 +188,15 @@ def _parse_ast_serialize_version(version: str) -> tuple[int, int, int] | None: def _validate_ast_serialize_version() -> None: - global _ast_serialize_version_checked - if _ast_serialize_version_checked: - return + from importlib import metadata try: - version = importlib.metadata.version("ast-serialize") - except importlib.metadata.PackageNotFoundError: - _ast_serialize_version_checked = True + version = metadata.version("ast-serialize") + except metadata.PackageNotFoundError: return parsed_version = _parse_ast_serialize_version(version) if parsed_version is None: - _ast_serialize_version_checked = True return if parsed_version < _AST_SERIALIZE_MIN_VERSION: raise CompileError( @@ -218,7 +212,6 @@ def _validate_ast_serialize_version() -> None: f"mypy requires {_AST_SERIALIZE_REQUIREMENT}" ] ) - _ast_serialize_version_checked = True class State: diff --git a/mypy/test/test_nativeparse.py b/mypy/test/test_nativeparse.py index e8fd8be4378f0..35273ffe057a9 100644 --- a/mypy/test/test_nativeparse.py +++ b/mypy/test/test_nativeparse.py @@ -7,11 +7,11 @@ from __future__ import annotations import contextlib -import importlib.metadata import os import tempfile import unittest from collections.abc import Iterator +from importlib import metadata from unittest.mock import patch from librt.internal import ReadBuffer @@ -237,12 +237,6 @@ def format_reachable_imports(node: MypyFile) -> list[str]: @unittest.skipUnless(has_nativeparse, "nativeparse not available") class TestNativeParserVersion(unittest.TestCase): - def setUp(self) -> None: - nativeparse._ast_serialize_version_checked = False - - def tearDown(self) -> None: - nativeparse._ast_serialize_version_checked = False - def test_ast_serialize_version_accepts_minimum(self) -> None: with patch("importlib.metadata.version", return_value="0.5.0"): nativeparse._validate_ast_serialize_version() @@ -278,7 +272,7 @@ def test_ast_serialize_version_reports_too_new_version(self) -> None: def test_ast_serialize_version_ignores_missing_metadata(self) -> None: with patch( "importlib.metadata.version", - side_effect=importlib.metadata.PackageNotFoundError("ast-serialize"), + side_effect=metadata.PackageNotFoundError("ast-serialize"), ): nativeparse._validate_ast_serialize_version()