Python Import Magic - automagically add, remove and manage imports
npx @tessl/cli install tessl/pypi-importmagic@0.1.0A Python library for automated import management and symbol resolution. ImportMagic builds comprehensive symbol indexes from installed packages, analyzes Python source code to identify unresolved symbol references, and automatically generates appropriate import statements to resolve them.
pip install importmagicimport importmagicSpecific functionality imports:
from importmagic import SymbolIndex, Scope, Imports, get_update, update_importsimport importmagic
import sys
# Build an index of available symbols
index = importmagic.SymbolIndex()
index.build_index(sys.path)
# Analyze source code for symbol usage
python_source = '''
import os
def example():
basename = path.basename("/tmp/file.txt") # unresolved: path
return basename
'''
scope = importmagic.Scope.from_source(python_source)
unresolved, unreferenced = scope.find_unresolved_and_unreferenced_symbols()
# Get updated source with resolved imports
new_source = importmagic.update_imports(python_source, index, unresolved, unreferenced)
print(new_source)
# Output includes: from os import pathImportMagic follows a three-phase approach to automated import management:
SymbolIndex)Scope)Imports)SymbolIndex ──┐
├─→ Imports ──→ Updated Source Code
Source Code ──┘ ↗
│ │
↓ │
Scope ──→ Analysis Results ─┘The workflow typically involves:
SymbolIndex from available Python pathsScope from source code to identify symbol usageImports to resolve unresolved symbols and clean up unreferenced importsThis architecture enables both high-level convenience functions (update_imports) and fine-grained control over each phase for advanced use cases.
Build searchable indexes of Python symbols from modules and packages for import resolution.
class SymbolIndex:
def __init__(self, name=None, parent=None, score=1.0, location='L', blacklist_re=None, locations=None):
"""
Create a new symbol index.
Parameters:
- name: Optional name for this index node
- parent: Parent SymbolIndex node
- score: Score multiplier for symbols in this index
- location: Location classification ('F', '3', 'S', 'L')
- blacklist_re: Regex pattern for modules to exclude
- locations: Library location mappings
"""
def build_index(self, paths):
"""
Build comprehensive symbol index from list of paths.
Parameters:
- paths: List of directory paths to index (typically sys.path)
"""
def symbol_scores(self, symbol):
"""
Find scored matches for a symbol name.
Parameters:
- symbol: Dotted symbol name to search for
Returns:
List of tuples (score, module, variable) ordered by score
"""
def index_path(self, root):
"""
Index a single path (file or directory).
Parameters:
- root: File path or package directory to index
"""
def serialize(self, fd=None):
"""
Serialize index to JSON format.
Parameters:
- fd: Optional file descriptor to write to
Returns:
JSON string if fd is None, otherwise writes to fd
"""
@classmethod
def deserialize(cls, file):
"""
Load index from JSON file.
Parameters:
- file: File object to read from
Returns:
SymbolIndex instance
"""
def index_source(self, filename, source):
"""
Index symbols from Python source code.
Parameters:
- filename: Name or path of the source file
- source: Python source code string
"""
def index_file(self, module, filename):
"""
Index symbols from a Python file.
Parameters:
- module: Module name for the file
- filename: Path to the Python file
"""
def index_builtin(self, name, location):
"""
Index symbols from a built-in module.
Parameters:
- name: Built-in module name
- location: Location classification for the module
"""
def find(self, path):
"""
Find index node for a dotted path.
Parameters:
- path: Dotted symbol path to find
Returns:
SymbolIndex node or None if not found
"""
def location_for(self, path):
"""
Get location classification for a symbol path.
Parameters:
- path: Dotted symbol path
Returns:
Location code string ('F', '3', 'S', 'L')
"""
def add(self, name, score):
"""
Add a symbol with score to current index node.
Parameters:
- name: Symbol name to add
- score: Score value for the symbol
"""
def add_explicit_export(self, name, score):
"""
Add an explicitly exported symbol with score.
Parameters:
- name: Symbol name to export
- score: Score value for the exported symbol
"""
def enter(self, name, location='L', score=1.0):
"""
Enter a sub-namespace context manager.
Parameters:
- name: Namespace name
- location: Location classification (default 'L')
- score: Score multiplier (default 1.0)
Returns:
Context manager yielding child SymbolIndex
"""
def depth(self):
"""
Get the depth of this index node in the tree.
Returns:
Integer depth from root (0 for root)
"""
def path(self):
"""
Get dotted path from root to this index node.
Returns:
Dotted string path
"""
def boost(self):
"""
Get boost multiplier for this location type.
Returns:
Float boost multiplier
"""Analyze Python source code to identify symbol definitions, references, and scope relationships.
class Scope:
def __init__(self, parent=None, define_builtins=True, is_class=False):
"""
Create a new scope for symbol tracking.
Parameters:
- parent: Parent scope (None for root scope)
- define_builtins: Whether to include Python builtins
- is_class: Whether this scope represents a class
"""
@classmethod
def from_source(cls, src, trace=False, define_builtins=True):
"""
Create scope by analyzing Python source code.
Parameters:
- src: Python source code string or AST node
- trace: Enable tracing for debugging
- define_builtins: Whether to include Python builtins
Returns:
Scope instance with analyzed symbol information
"""
def find_unresolved_and_unreferenced_symbols(self):
"""
Find symbols that need import resolution or removal.
Returns:
Tuple of (unresolved_set, unreferenced_set)
"""
def define(self, name):
"""
Mark a symbol as defined in current scope.
Parameters:
- name: Symbol name to define
"""
def reference(self, name):
"""
Mark a symbol as referenced.
Parameters:
- name: Symbol name that was referenced
"""
def enter(self, is_class=False):
"""
Enter a child scope context manager.
Parameters:
- is_class: Whether the child scope represents a class
Returns:
Context manager yielding child Scope
"""
def start_symbol(self):
"""
Start building a compound symbol context manager.
Returns:
Context manager for compound symbol construction
"""
def start_definition(self):
"""
Start a symbol definition context manager.
Returns:
Context manager for symbol definition
"""
def start_reference(self):
"""
Start a symbol reference context manager.
Returns:
Context manager for symbol reference
"""
def extend_symbol(self, segment, extend_only=False):
"""
Extend current compound symbol with a segment.
Parameters:
- segment: Symbol segment to add
- extend_only: If True, only extend without creating reference
"""
def end_symbol(self):
"""
End current compound symbol and mark as referenced.
"""
def flush_symbol(self):
"""
Flush any pending symbol operations.
"""Manage import statements with parsing, modification, and PEP8-compliant formatting.
class Imports:
def __init__(self, index, source):
"""
Create import manager for source code.
Parameters:
- index: SymbolIndex for symbol resolution
- source: Python source code string
"""
def add_import(self, name, alias=None):
"""
Add a direct module import.
Parameters:
- name: Module name to import
- alias: Optional import alias
"""
def add_import_from(self, module, name, alias=None):
"""
Add a from-module import.
Parameters:
- module: Module to import from
- name: Symbol name to import
- alias: Optional import alias
"""
def remove(self, references):
"""
Remove imports by referenced names.
Parameters:
- references: Set of symbol names to remove
"""
def get_update(self):
"""
Get import block update information.
Returns:
Tuple of (start_line, end_line, import_text)
"""
def update_source(self):
"""
Return complete updated source code.
Returns:
Updated Python source code string
"""
@classmethod
def set_style(cls, **kwargs):
"""
Configure import formatting style.
Parameters:
- multiline: Multiline style ('parentheses' or 'backslash')
- max_columns: Maximum line length for imports
"""
class Import:
def __init__(self, location, name, alias):
"""
Represent a single import statement.
Parameters:
- location: Import location classification
- name: Module or symbol name
- alias: Import alias or None
"""
def __hash__(self):
"""
Hash function for Import objects.
Returns:
Hash value based on location, name, and alias
"""
def __eq__(self, other):
"""
Equality comparison for Import objects.
Parameters:
- other: Other Import object to compare
Returns:
True if imports are equal
"""
def __lt__(self, other):
"""
Less-than comparison for sorting Import objects.
Parameters:
- other: Other Import object to compare
Returns:
True if this import should sort before other
"""Convenient functions for automated import management without manual class instantiation.
def get_update(src, index, unresolved, unreferenced):
"""
Generate import block update for source code.
Parameters:
- src: Python source code string
- index: SymbolIndex instance for symbol resolution
- unresolved: Set of unresolved symbol names
- unreferenced: Set of unreferenced import names
Returns:
Tuple of (start_line, end_line, import_block_text)
"""
def update_imports(src, index, unresolved, unreferenced):
"""
Update source code with resolved imports.
Parameters:
- src: Python source code string
- index: SymbolIndex instance for symbol resolution
- unresolved: Set of unresolved symbol names
- unreferenced: Set of unreferenced import names
Returns:
Updated Python source code string
"""# Location Classifications
LOCATION_ORDER = 'FS3L' # Future, System, Third-party, Local
# Symbol Index Locations
LOCATIONS = {
'F': 'Future', # __future__ imports
'3': 'Third party', # Third-party packages
'S': 'System', # Standard library
'L': 'Local' # Local modules
}
# Location Score Boosts
LOCATION_BOOSTS = {
'3': 1.2, # Third-party packages
'L': 1.5, # Local modules
}
# Package Aliases with Score Boosts
PACKAGE_ALIASES = {
'os.path': ('posixpath', 1.2), # Prefer os.path over posixpath/ntpath
'os': ('os', 1.2), # Boost os due to heavy aliasing
}
# Built-in Modules (treated specially during indexing)
BUILTIN_MODULES = sys.builtin_module_names + ('os',)
# Default module blacklist pattern
DEFAULT_BLACKLIST_RE = re.compile(r'\btest[s]?|test[s]?\b', re.I)
# Builtin Symbol Sets
GLOBALS = ['__name__', '__file__', '__loader__', '__package__', '__path__']
PYTHON3_BUILTINS = ['PermissionError']
ALL_BUILTINS = set(dir(__builtin__)) | set(GLOBALS) | set(PYTHON3_BUILTINS)
# Iterator class for token processing
class Iterator:
def __init__(self, tokens, start=None, end=None):
"""
Iterator for processing token sequences.
Parameters:
- tokens: List of tokens to iterate over
- start: Starting index (default 0)
- end: Ending index (default len(tokens))
"""
def next(self):
"""
Get next token and advance cursor.
Returns:
Tuple of (index, token) or (None, None) if exhausted
"""
def peek(self):
"""
Get current token without advancing cursor.
Returns:
Current token or None if exhausted
"""
def until(self, type):
"""
Collect tokens until specified type is found.
Parameters:
- type: Token type to search for
Returns:
List of (index, token) tuples
"""
def rewind(self):
"""
Move cursor back one position.
"""import importmagic
import sys
# Build index
index = importmagic.SymbolIndex()
index.build_index(sys.path)
# Analyze source
python_source = '''
def example():
result = basename("/tmp/file.txt")
data = json.loads('{"key": "value"}')
return result, data
'''
scope = importmagic.Scope.from_source(python_source)
unresolved, unreferenced = scope.find_unresolved_and_unreferenced_symbols()
# Manual import management
imports = importmagic.Imports(index, python_source)
imports.remove(unreferenced)
# Add specific imports with control over selection
for symbol in unresolved:
scores = index.symbol_scores(symbol)
if scores:
score, module, variable = scores[0] # Take highest scored match
if variable is None:
imports.add_import(module) # Direct module import
else:
imports.add_import_from(module, variable) # From-import
updated_source = imports.update_source()# Configure import style
importmagic.Imports.set_style(
multiline='parentheses', # Use parentheses for multiline imports
max_columns=80 # Maximum line length
)
# Process source with custom formatting
imports = importmagic.Imports(index, source)
# ... add/remove imports
formatted_source = imports.update_source()# Save index to file
index = importmagic.SymbolIndex()
index.build_index(sys.path)
with open('symbol_index.json', 'w') as f:
index.serialize(f)
# Load index from file
with open('symbol_index.json', 'r') as f:
index = importmagic.SymbolIndex.deserialize(f)ImportMagic handles various error conditions gracefully:
The library is designed to be robust for use in development tools and IDEs where source code may be incomplete or contain syntax errors.