Skip to content

autoprofile cannot handle multiple imports on the same line #433

Description

@TTsangSC

Background

Ran into a bug when writing a new test for #431. So I was rewriting the test-module code passed to kernprof, and upon refactoring I'm now importing two targets instead of one from another module. Curiously, this caused profiling info to be lost for my target function – even when I'm not actively using multiprocessing , so my new code isn't to blame.

Example

Run the following Python code (click to expand; needs a POSIX-ish `diff` executable):
from __future__ import annotations

import ast
import subprocess
from pathlib import Path
from tempfile import TemporaryDirectory

from line_profiler.autoprofile.ast_tree_profiler import AstTreeProfiler


IMPORTS_ON_SAME_LINE = ('''
import foo, bar as baz
from foobar import spam, ham, eggs


def main() -> None:
    pass
''').strip('\n')
IMPORTS_ON_SEPARATE_LINES = ('''
import foo
import bar as baz
from foobar import spam
from foobar import ham
from foobar import eggs


def main() -> None:
    pass
''').strip('\n')


def main() -> None:
    with TemporaryDirectory() as tmpdir_:
        tmpdir = Path(tmpdir_)
        fname_in = tmpdir / 'input.py'
        fname_out = tmpdir / 'output.py'
        for header, module_text in {
            'imports on the same line': IMPORTS_ON_SAME_LINE,
            'imports on separate lines': IMPORTS_ON_SEPARATE_LINES,
        }.items():
            print('====>>', header, '<<====', end='\n\n')
            fname_in.write_text(module_text + '\n')
            module_ast = AstTreeProfiler(
                str(fname_in), ['foo', 'bar', 'foobar'], False
            ).profile()
            fname_out.write_text(ast.unparse(module_ast) + '\n')
            cmd = ['diff', '--unified', str(fname_in), str(fname_out)]
            subprocess.run(cmd)
            print('')


if __name__ == '__main__':
    main()
Output (click to expand):
 $ python ../433-demo.py                                                                                                                                                 
====>> imports on the same line <<====                                                                                                                                            
                                                                                                                                                                                  
--- /var/folders/5b/44kj9cdn7gngz2k1z1wxt0240000gp/T/tmpo20w9kuk/input.py       2026-06-29 19:19:14                                                                               
+++ /var/folders/5b/44kj9cdn7gngz2k1z1wxt0240000gp/T/tmpo20w9kuk/output.py      2026-06-29 19:19:14                                                                               
@@ -1,6 +1,7 @@                                                                                                                                                                   
 import foo, bar as baz                                                                                                                                                           
+profile.add_imported_function_or_module(baz)                                                                                                                                     
 from foobar import spam, ham, eggs                                                                                                                                               
+profile.add_imported_function_or_module(eggs)                                                                                                                                    
                                                                                                                                                                                  
-                                                                                                                                                                                 
 def main() -> None:                                                                                                                                                              
     pass                                                                                                                                                                         
                                                                                                                                                                                  
====>> imports on separate lines <<====                                                                                                                                           
                                                                                                                                                                                  
--- /var/folders/5b/44kj9cdn7gngz2k1z1wxt0240000gp/T/tmpo20w9kuk/input.py       2026-06-29 19:19:14                                                                               
+++ /var/folders/5b/44kj9cdn7gngz2k1z1wxt0240000gp/T/tmpo20w9kuk/output.py      2026-06-29 19:19:14                                                                               
@@ -1,9 +1,13 @@                                                                                                                                                                  
 import foo                                                                                                                                                                       
+profile.add_imported_function_or_module(foo)                                                                                                                                     
 import bar as baz                                                                                                                                                                
+profile.add_imported_function_or_module(baz)                                                                                                                                     
 from foobar import spam                                                                                                                                                          
+profile.add_imported_function_or_module(spam)                                                                                                                                    
 from foobar import ham                                                                                                                                                           
+profile.add_imported_function_or_module(ham)                                                                                                                                     
 from foobar import eggs                                                                                                                                                          
+profile.add_imported_function_or_module(eggs)                                                                                                                                    
                                                                                                                                                                                  
-                                                                                                                                                                                 
 def main() -> None:                                                                                                                                                              
     pass                                                                                                                                                                         

Cause

line_profiler.autoprofile.profmod_extractor.ProfmodExtractor._find_modnames_in_tree_imports() Implicitly asserts that each Import[From] AST node in the tree only has one target, overwriting preceding ones with latter ones.

Possible solutions

This would have been otherwise trivial to fix, if not for how it cannot be done without changing the API because the "public" method ProfmodExtractor.run() also assumes the same (1 import target per statement), returning a dict[int, str] instead of e.g. a dict[int, list[str]].

A zanier fix without API changes could be to split multiple imports into their own Import[From] nodes inside ProfmodExtractor.run(), but one can argue that AST changes are the responsibility of AstTreeProfiler and AstProfileTransformer, and ProfmodExtractor should analyze the tree without modifying it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions