From 63ef17ec4d4765d40513ccb9c1fa8afb2ed19006 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 10 Sep 2026 18:35:12 +0100 Subject: [PATCH 1/2] Use native parse_type_string() --- mypy/exprtotype.py | 6 +++--- mypy/nativeparse.py | 21 +++++++++++++++++++++ mypy/parse.py | 20 +++++++++++++++++++- test-data/unit/check-literal.test | 17 +++++++++-------- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/mypy/exprtotype.py b/mypy/exprtotype.py index 1c9323be056dd..9ec54ce44b675 100644 --- a/mypy/exprtotype.py +++ b/mypy/exprtotype.py @@ -4,7 +4,6 @@ from collections.abc import Callable -from mypy.fastparse import parse_type_string from mypy.nodes import ( MISSING_FALLBACK, BytesExpr, @@ -30,6 +29,7 @@ get_member_expr_fullname, ) from mypy.options import Options +from mypy.parse import parse_type_string from mypy.types import ( ANNOTATED_TYPE_NAMES, AnyType, @@ -221,9 +221,9 @@ def expr_to_unanalyzed_type( column=expr.column, ) elif isinstance(expr, StrExpr): - return parse_type_string(expr.value, "builtins.str", expr.line, expr.column) + return parse_type_string(expr, options) elif isinstance(expr, BytesExpr): - return parse_type_string(expr.value, "builtins.bytes", expr.line, expr.column) + return RawExpressionType(expr.value, "builtins.bytes", expr.line, expr.column) elif isinstance(expr, UnaryExpr): typ = expr_to_unanalyzed_type( expr.expr, options, allow_new_syntax, lookup_qualified=lookup_qualified diff --git a/mypy/nativeparse.py b/mypy/nativeparse.py index d45d8bbb0af70..cc4df167e14a2 100644 --- a/mypy/nativeparse.py +++ b/mypy/nativeparse.py @@ -254,6 +254,27 @@ def native_parse( return node, errors, ignores +def native_parse_type_string( + expr_string: str, line: int, column: int, end_line: int, end_column: int, options: Options +) -> ProperType: + # 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() + while ast_serialize is None: + time.sleep(0.0001) # type: ignore[unreachable] + if time.time() - t0 > 10.0: + raise ImportError("Cannot import ast_serialize") + + ast_bytes = ast_serialize.parse_type_string( + expr_string, (line, column, end_line, end_column), cache_version=5 + ) + state = State(options) + data = ReadBuffer(ast_bytes) + ret = read_type(state, data) + assert isinstance(ret, ProperType) + return ret + + def expect_end_tag(data: ReadBuffer) -> None: assert read_tag(data) == END_TAG diff --git a/mypy/parse.py b/mypy/parse.py index 47e2f95f0f307..f440be5d3d852 100644 --- a/mypy/parse.py +++ b/mypy/parse.py @@ -7,8 +7,9 @@ from mypy import errorcodes as codes from mypy.cache import read_int from mypy.errors import Errors -from mypy.nodes import FileRawData, MypyFile, ParseError +from mypy.nodes import FileRawData, MypyFile, ParseError, StrExpr from mypy.options import Options +from mypy.types import ProperType def parse( @@ -115,3 +116,20 @@ def report_parse_error(error: ParseError, errors: Errors) -> None: # Fallback to [syntax] for backwards compatibility. error_code = codes.error_codes.get(error_code) or codes.SYNTAX errors.report(error["line"], error["column"], message, blocker=is_blocker, code=error_code) + + +def parse_type_string(expr: StrExpr, options: Options) -> ProperType: + if options.native_parser: + import mypy.nativeparse + + return mypy.nativeparse.native_parse_type_string( + expr.value, + expr.line, + expr.column, + expr.end_line or expr.line, + expr.end_column or expr.column, + options, + ) + import mypy.fastparse + + return mypy.fastparse.parse_type_string(expr.value, "builtins.str", expr.line, expr.column) diff --git a/test-data/unit/check-literal.test b/test-data/unit/check-literal.test index f795f1f5b354b..595e4c7688d9c 100644 --- a/test-data/unit/check-literal.test +++ b/test-data/unit/check-literal.test @@ -224,7 +224,7 @@ accepts_bytes(c_alias) [builtins fixtures/tuple.pyi] [out] -[case testLiteralMixingUnicodeAndBytesPython3ForwardStrings_no_native_parse] +[case testLiteralMixingUnicodeAndBytesPython3ForwardStrings] from typing import Literal, TypeVar, Generic a_unicode_wrapper: u"Literal[u'foo']" @@ -235,7 +235,7 @@ a_str_wrapper: "Literal[u'foo']" b_str_wrapper: "Literal['foo']" c_str_wrapper: "Literal[b'foo']" -# In Python 3, forward references MUST be str, not bytes +# In Python 3, forward references MUST be str, not bytes. a_bytes_wrapper: b"Literal[u'foo']" # E: Invalid type comment or annotation b_bytes_wrapper: b"Literal['foo']" # E: Invalid type comment or annotation c_bytes_wrapper: b"Literal[b'foo']" # E: Invalid type comment or annotation @@ -265,9 +265,10 @@ a_str_wrapper_alias: AStrWrapperAlias b_str_wrapper_alias: BStrWrapperAlias c_str_wrapper_alias: CStrWrapperAlias -ABytesWrapperAlias = Wrap[b"Literal[u'foo']"] -BBytesWrapperAlias = Wrap[b"Literal['foo']"] -CBytesWrapperAlias = Wrap[b"Literal[b'foo']"] +# Bytes literals are not valid as forward references. +ABytesWrapperAlias = Wrap[b"Literal[u'foo']"] # E: Invalid type comment or annotation +BBytesWrapperAlias = Wrap[b"Literal['foo']"] # E: Invalid type comment or annotation +CBytesWrapperAlias = Wrap[b"Literal[b'foo']"] # E: Invalid type comment or annotation a_bytes_wrapper_alias: ABytesWrapperAlias b_bytes_wrapper_alias: BBytesWrapperAlias c_bytes_wrapper_alias: CBytesWrapperAlias @@ -282,9 +283,9 @@ reveal_type(a_str_wrapper_alias) # N: Revealed type is "__main__.Wrap[Liter reveal_type(b_str_wrapper_alias) # N: Revealed type is "__main__.Wrap[Literal['foo']]" reveal_type(c_str_wrapper_alias) # N: Revealed type is "__main__.Wrap[Literal[b'foo']]" -reveal_type(a_bytes_wrapper_alias) # N: Revealed type is "__main__.Wrap[Literal['foo']]" -reveal_type(b_bytes_wrapper_alias) # N: Revealed type is "__main__.Wrap[Literal['foo']]" -reveal_type(c_bytes_wrapper_alias) # N: Revealed type is "__main__.Wrap[Literal[b'foo']]" +reveal_type(a_bytes_wrapper_alias) # N: Revealed type is "__main__.Wrap[Any]" +reveal_type(b_bytes_wrapper_alias) # N: Revealed type is "__main__.Wrap[Any]" +reveal_type(c_bytes_wrapper_alias) # N: Revealed type is "__main__.Wrap[Any]" [builtins fixtures/tuple.pyi] [out] From 604f38806ced24655ab3afd80f025473e9cb0c1a Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Thu, 10 Sep 2026 18:47:40 +0100 Subject: [PATCH 2/2] Delete import hack; add docstring --- mypy/nativeparse.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/mypy/nativeparse.py b/mypy/nativeparse.py index cc4df167e14a2..00c3b0ff7e1b1 100644 --- a/mypy/nativeparse.py +++ b/mypy/nativeparse.py @@ -257,14 +257,10 @@ def native_parse( def native_parse_type_string( expr_string: str, line: int, column: int, end_line: int, end_column: int, options: Options ) -> ProperType: - # 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() - while ast_serialize is None: - time.sleep(0.0001) # type: ignore[unreachable] - if time.time() - t0 > 10.0: - raise ImportError("Cannot import ast_serialize") + """Try to parse a string literal as a type expression (i.e. resolve a forward reference). + If parsing fails, a RawExpressionType will be returned. + """ ast_bytes = ast_serialize.parse_type_string( expr_string, (line, column, end_line, end_column), cache_version=5 )