From bbec1dfcb837ba28996e9761fe319593dd8e2cb8 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 18 Feb 2026 12:41:08 +0000 Subject: [PATCH 1/2] Detect partial stub packages --- src/lib.rs | 6 +++--- src/serialize_ast.rs | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index d25b570..7fbfbd3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,7 +47,7 @@ fn parse( platform: Option, always_true: Option>, always_false: Option>, -) -> PyResult<(Vec, Vec, Vec, Vec)> { +) -> PyResult<(Vec, Vec, Vec, Vec, bool)> { // Get defaults from Python if not provided let python_version = match python_version { Some(v) => v, @@ -63,7 +63,7 @@ fn parse( let always_false = always_false.unwrap_or_default(); let path = Path::new(&fnam); - let (ast_bytes, syntax_errors, type_ignore_lines, import_bytes) = py + let (ast_bytes, syntax_errors, type_ignore_lines, import_bytes, is_partial_package) = py .allow_threads(|| { serialize_ast::serialize_python_file( path, @@ -102,7 +102,7 @@ fn parse( .collect(); let py_type_ignores = py_type_ignores?; - Ok((ast_bytes, py_errors, py_type_ignores, import_bytes)) + Ok((ast_bytes, py_errors, py_type_ignores, import_bytes, is_partial_package)) } /// Get the default Python version from sys.version_info diff --git a/src/serialize_ast.rs b/src/serialize_ast.rs index 2651ed5..59757c1 100644 --- a/src/serialize_ast.rs +++ b/src/serialize_ast.rs @@ -171,10 +171,14 @@ pub(crate) fn serialize_python_file( file_path: &Path, skip_function_bodies: bool, options: Options, -) -> Result<(Vec, Vec, Vec<(usize, Vec)>, Vec)> { +) -> Result<(Vec, Vec, Vec<(usize, Vec)>, Vec, bool)> { let source_type = PySourceType::from(file_path); let source_text = std::fs::read_to_string(file_path)?; let line_index = LineIndex::from_source_text(&source_text); + let is_stub_package = match file_path.file_name() { + Some(file) => file.to_str().map(|name| name == "__init__.pyi").unwrap_or_else(|| false), + _ => false, + }; // Check if file is all ASCII and build per-line non-ASCII flags if needed let is_all_ascii = source_text.is_ascii(); @@ -221,6 +225,7 @@ pub(crate) fn serialize_python_file( options, current_unreachable: false, current_mypy_only: false, + top_level_getattr: false, }; parsed.syntax().serialize(&mut ser); @@ -233,7 +238,10 @@ pub(crate) fn serialize_python_file( Some(ser.lines_with_non_ascii), ); - Ok((ser.bytes, syntax_errors, type_ignore_lines, import_bytes)) + // Return this directly to caller, so that it can check this without deserialization + let is_partial_package = is_stub_package && ser.top_level_getattr; + + Ok((ser.bytes, syntax_errors, type_ignore_lines, import_bytes, is_partial_package)) } // Bit flags for import statement metadata @@ -279,6 +287,7 @@ struct Serializer<'a> { options: Options, // Reachability analysis options current_unreachable: bool, // Whether we're currently in an unreachable block current_mypy_only: bool, // Whether we're currently in a mypy-only block (e.g., if TYPE_CHECKING) + top_level_getattr: bool, // Does module have top-level __getattr__() function } impl<'a> Serializer<'a> { @@ -864,6 +873,10 @@ impl Ser for ast::Stmt { true }; + if !ser.in_class && !ser.in_function && f.name.as_str() == "__getattr__" { + ser.top_level_getattr = true; + }; + // Body - mark that we're inside a function let was_in_function = ser.in_function; ser.in_function = true; @@ -2580,6 +2593,7 @@ pub fn serialize_imports( options: Options::default(), current_unreachable: false, current_mypy_only: false, + top_level_getattr: false, }; // Write list of imports @@ -2708,6 +2722,7 @@ mod tests { options: Options::default(), current_unreachable: false, current_mypy_only: false, + top_level_getattr: false, } } From 2713361bf474ca8621623401ad24d0712d6156ef Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 18 Feb 2026 12:59:29 +0000 Subject: [PATCH 2/2] Simplify filename check --- src/serialize_ast.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/serialize_ast.rs b/src/serialize_ast.rs index 59757c1..dc5846e 100644 --- a/src/serialize_ast.rs +++ b/src/serialize_ast.rs @@ -176,7 +176,7 @@ pub(crate) fn serialize_python_file( let source_text = std::fs::read_to_string(file_path)?; let line_index = LineIndex::from_source_text(&source_text); let is_stub_package = match file_path.file_name() { - Some(file) => file.to_str().map(|name| name == "__init__.pyi").unwrap_or_else(|| false), + Some(file) => file.as_encoded_bytes() == b"__init__.pyi", _ => false, };