diff --git a/mypy/nativeparse.py b/mypy/nativeparse.py index d048e9bce65e..83e2163c65d9 100644 --- a/mypy/nativeparse.py +++ b/mypy/nativeparse.py @@ -47,6 +47,7 @@ read_str_opt, read_tag, ) +from mypy.errors import CompileError from mypy.nodes import ( ARG_KINDS, ARG_POS, @@ -164,6 +165,53 @@ # 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) + + +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: + from importlib import metadata + + try: + version = metadata.version("ast-serialize") + except metadata.PackageNotFoundError: + return + + parsed_version = _parse_ast_serialize_version(version) + if parsed_version is None: + 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}" + ] + ) class State: @@ -250,6 +298,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 b50da5f5d02c..35273ffe057a 100644 --- a/mypy/test/test_nativeparse.py +++ b/mypy/test/test_nativeparse.py @@ -11,6 +11,8 @@ import tempfile import unittest from collections.abc import Iterator +from importlib import metadata +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,48 @@ def format_reachable_imports(node: MypyFile) -> list[str]: return output +@unittest.skipUnless(has_nativeparse, "nativeparse not available") +class TestNativeParserVersion(unittest.TestCase): + 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=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: