Skip to content

Commit 3039f8b

Browse files
authored
Serialize flag for lazy imports (#91)
Set a new import flag bit for lazy imports (PEP 810), i.e. 'lazy import x', 'lazy from x import y' and 'lazy from x import *'. We need the flag so that mypy can reject lazy imports when targeting a pre-3.15 Python version. This doesn't need a cache version bump, since deserializers ignore unknown bits in the import flags bitfield. Work on python/mypy#20978. Created using Claude Code.
1 parent e667a2b commit 3039f8b

1 file changed

Lines changed: 46 additions & 6 deletions

File tree

src/serialize_ast.rs

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -359,10 +359,12 @@ pub(crate) fn serialize_python_file(
359359
))
360360
}
361361

362-
// Bit flags for import statement metadata
362+
// Bit flags for import statement metadata. Deserializers ignore unknown bits,
363+
// so adding a flag here doesn't require a cache version bump.
363364
const IMPORT_FLAG_TOP_LEVEL: u8 = 1 << 0; // true if import is not within a function
364365
const IMPORT_FLAG_UNREACHABLE: u8 = 1 << 1; // true if import is in unreachable code
365366
const IMPORT_FLAG_MYPY_ONLY: u8 = 1 << 2; // true if import is mypy-only (e.g., in TYPE_CHECKING block)
367+
const IMPORT_FLAG_LAZY: u8 = 1 << 3; // true if this is a lazy import (PEP 810), e.g. 'lazy import x'
366368

367369
// Used to report which imports are used in a file
368370
pub(crate) enum ImportStatement {
@@ -1616,7 +1618,7 @@ impl Ser for ast::Stmt {
16161618
ser.write_location(a.range());
16171619
}
16181620
ast::Stmt::Import(i) => {
1619-
let flags = make_import_flags(ser);
1621+
let flags = make_import_flags(ser, i.is_lazy);
16201622
ser.write_tag(TAG_IMPORT);
16211623
// Write number of imports
16221624
ser.write_tagged_int(i.names.len() as i64);
@@ -1644,7 +1646,7 @@ impl Ser for ast::Stmt {
16441646
}
16451647
}
16461648
ast::Stmt::ImportFrom(ifrom) => {
1647-
let flags = make_import_flags(ser);
1649+
let flags = make_import_flags(ser, ifrom.is_lazy);
16481650
// Check if this is a wildcard import (from m import *)
16491651
if ifrom.names.len() == 1 && ifrom.names[0].name.as_str() == "*" {
16501652
// Serialize as ImportAll
@@ -3445,8 +3447,8 @@ fn extract_int_literal_value(expr: &ast::Expr) -> Option<i64> {
34453447
}
34463448
}
34473449

3448-
/// Build import flags from current serializer state
3449-
fn make_import_flags(ser: &Serializer) -> u8 {
3450+
/// Build import flags from current serializer state and the `lazy` modifier of the statement
3451+
fn make_import_flags(ser: &Serializer, is_lazy: bool) -> u8 {
34503452
(if !ser.in_function {
34513453
IMPORT_FLAG_TOP_LEVEL
34523454
} else {
@@ -3459,7 +3461,7 @@ fn make_import_flags(ser: &Serializer) -> u8 {
34593461
IMPORT_FLAG_MYPY_ONLY
34603462
} else {
34613463
0
3462-
})
3464+
}) | (if is_lazy { IMPORT_FLAG_LAZY } else { 0 })
34633465
}
34643466

34653467
/// Serialize a list of import statements to bytes.
@@ -4051,6 +4053,44 @@ mod tests {
40514053
assert_eq!(bytes, expected);
40524054
}
40534055

4056+
/// Serialize `text` and return the import flags of each encountered import statement.
4057+
fn import_flags(text: &str) -> Vec<u8> {
4058+
let ast = parse_unchecked(text, ParseOptions::from(PySourceType::Python)).into_syntax();
4059+
let mut ser = make_ser(text);
4060+
ast.serialize(&mut ser);
4061+
ser.imports
4062+
.iter()
4063+
.map(|import| match import {
4064+
ImportStatement::Import { flags, .. }
4065+
| ImportStatement::ImportFrom { flags, .. }
4066+
| ImportStatement::ImportAll { flags, .. } => *flags,
4067+
})
4068+
.collect()
4069+
}
4070+
4071+
#[test]
4072+
fn test_lazy_import_flag() {
4073+
// Lazy imports (PEP 810) set IMPORT_FLAG_LAZY.
4074+
let lazy = IMPORT_FLAG_TOP_LEVEL | IMPORT_FLAG_LAZY;
4075+
assert_eq!(import_flags("lazy import foo\n"), [lazy]);
4076+
assert_eq!(import_flags("lazy import foo as bar\n"), [lazy]);
4077+
assert_eq!(import_flags("lazy import foo, bar\n"), [lazy, lazy]);
4078+
assert_eq!(import_flags("lazy from foo import bar\n"), [lazy]);
4079+
assert_eq!(import_flags("lazy from foo import bar as baz\n"), [lazy]);
4080+
assert_eq!(import_flags("lazy from . import foo\n"), [lazy]);
4081+
// 'lazy from foo import *' is invalid, but we still serialize the flag.
4082+
assert_eq!(import_flags("lazy from foo import *\n"), [lazy]);
4083+
4084+
// Regular imports don't set the flag.
4085+
let plain = IMPORT_FLAG_TOP_LEVEL;
4086+
assert_eq!(import_flags("import foo\n"), [plain]);
4087+
assert_eq!(import_flags("from foo import bar\n"), [plain]);
4088+
assert_eq!(import_flags("from foo import *\n"), [plain]);
4089+
// 'lazy' is a soft keyword, so it can still be used as a name.
4090+
assert_eq!(import_flags("import foo as lazy\n"), [plain]);
4091+
assert_eq!(import_flags("from lazy import foo\n"), [plain]);
4092+
}
4093+
40544094
fn extract_mypy_comments(source: &str) -> Vec<(usize, String)> {
40554095
let parsed = parse_unchecked(source, ParseOptions::from(PySourceType::Python));
40564096
let line_index = LineIndex::from_source_text(source);

0 commit comments

Comments
 (0)