Ctrl + k

or run

tessl search
Log in

Version

Workspace
tessl
Visibility
Public
Created
Last updated
Describes
pypipkg:pypi/typeshed-client@2.8.x

docs

index.md
tile.json

tessl/pypi-typeshed-client

tessl install tessl/pypi-typeshed-client@2.8.4

A library for accessing stubs in typeshed.

resolving-names.mddocs/reference/

Resolving Names

The resolver module provides functionality for resolving fully qualified names to their definitions, following imports and re-exports across module boundaries. This is useful for finding where a name is actually defined, even when it's imported and re-exported by multiple modules.

Quick Reference

Method/FunctionReturn TypeCan Return None?
Resolver.__init__(search_context)ResolverNo
resolver.get_fully_qualified_name(name)ResolvedNameYes (None means not found)
resolver.get_name(module_name, name)ResolvedNameYes (None means not found)
resolver.get_module(module_name)ModuleNo (check Module.exists)
module.get_name(name, resolver)ResolvedNameYes
module.get_dunder_all(resolver)Optional[list[str]]Yes

ResolvedName Type: Union[ModulePath, ImportedInfo, NameInfo, None]

Capabilities

Resolver Class

The main class for resolving names across modules.

class Resolver:
    """
    Resolves fully qualified names to their definitions.

    Maintains caches for modules and names to avoid redundant parsing.
    Thread Safety: Not thread-safe; do not share instances across threads.
    """

    def __init__(self, search_context: Optional[SearchContext] = None) -> None:
        """
        Create a new Resolver.

        Args:
            search_context (SearchContext, optional): Context for finding stubs. Uses default if None.

        Example:
            resolver = Resolver()
            # Or with custom context:
            ctx = get_search_context(version=(3, 11))
            resolver = Resolver(search_context=ctx)
        """

Key Points:

  • Creates internal caches for parsed modules and resolved names
  • Caches persist for the lifetime of the Resolver instance
  • Not thread-safe; create separate instances for different threads
  • SearchContext is stored and used for all operations

Get Fully Qualified Name

Resolves a dotted name string to its definition.

def get_fully_qualified_name(self, name: str) -> ResolvedName:
    """
    Resolve a fully qualified name to its definition.

    Args:
        name (str): Fully qualified name as dotted string (e.g., 'collections.OrderedDict')

    Returns:
        ResolvedName: One of:
            - ModulePath: If the name refers to a module
            - ImportedInfo: If the name is imported, with source module and NameInfo
            - NameInfo: If the name is defined directly in the module
            - None: If the name cannot be resolved

    Example:
        resolver = Resolver()

        # Resolve collections.OrderedDict
        info = resolver.get_fully_qualified_name('collections.OrderedDict')

        # Resolve typing.List
        info = resolver.get_fully_qualified_name('typing.List')
    """

Return Value Details:

  • None: Name not found (module doesn't exist or name not in module)
  • ModulePath: Name refers to a module (e.g., 'collections' resolves to module)
  • ImportedInfo: Name is imported; contains source_module and info fields
  • NameInfo: Name is defined directly in the module

Caching: Results are cached; subsequent calls with same name return cached result

Get Name from Module

Resolves a name within a specific module.

def get_name(self, module_name: ModulePath, name: str) -> ResolvedName:
    """
    Get a name from a specific module.

    Args:
        module_name (ModulePath): Module path as tuple of strings
        name (str): Name to look up

    Returns:
        ResolvedName: Resolved name information

    Example:
        from typeshed_client import Resolver, ModulePath

        resolver = Resolver()
        info = resolver.get_name(ModulePath(('collections',)), 'OrderedDict')
    """

Return Value Details:

  • Same return types as get_fully_qualified_name()
  • None if module doesn't exist or name not found
  • Results follow imports to source definitions

Usage Note: Use ModulePath(('module', 'submodule')) to create ModulePath

Get Module

Retrieves a Module object for a given module path.

def get_module(self, module_name: ModulePath) -> Module:
    """
    Get a Module object for the given module name.

    Results are cached to avoid redundant parsing.

    Args:
        module_name (ModulePath): Module path as tuple of strings

    Returns:
        Module: Module object with name lookup capabilities

    Example:
        from typeshed_client import Resolver, ModulePath

        resolver = Resolver()
        module = resolver.get_module(ModulePath(('typing',)))
        print(module.exists)  # True
        print(len(module.names))  # Number of names in typing module
    """

Return Value Details:

  • Always returns a Module object (never None)
  • Check Module.exists to see if module was actually found
  • If module doesn't exist: module.exists == False and module.names == {}
  • Results are cached for the lifetime of the Resolver

Performance: First call parses module; subsequent calls use cache

Module Class

Represents a parsed module with name lookup capabilities. Module objects are obtained via Resolver.get_module() and are not typically instantiated directly by users.

class Module:
    """
    A parsed module with name lookup.

    Module objects are created by Resolver.get_module() and should not be
    instantiated directly by users.

    Attributes:
        names (NameDict): Dictionary of names defined in the module
        ctx (SearchContext): Search context used for parsing
        exists (bool): Whether the module exists
    """

    names: NameDict
    ctx: SearchContext
    exists: bool

    def __init__(
        self,
        names: NameDict,
        ctx: SearchContext,
        *,
        exists: bool = True
    ) -> None:
        """
        Initialize a Module object.

        Note: Module objects are typically created by Resolver.get_module()
        and should not be instantiated directly by users.

        Args:
            names (NameDict): Dictionary of names defined in the module
            ctx (SearchContext): Search context used for parsing
            exists (bool, optional): Whether the module exists. Default: True
        """

    def get_name(self, name: str, resolver: Resolver) -> ResolvedName:
        """
        Look up a name in this module.

        Results are cached to avoid redundant lookups.

        Args:
            name (str): Name to look up
            resolver (Resolver): Resolver instance for following imports

        Returns:
            ResolvedName: Resolved name information

        Example:
            resolver = Resolver()
            module = resolver.get_module(ModulePath(('typing',)))
            info = module.get_name('List', resolver)
        """

    def get_dunder_all(self, resolver: Resolver) -> Optional[list[str]]:
        """
        Return the contents of __all__, or None if it does not exist.

        Args:
            resolver (Resolver): Resolver instance for following imports

        Returns:
            list[str]: List of names in __all__, or None if __all__ is not defined

        Example:
            from typeshed_client import Resolver, ModulePath

            resolver = Resolver()
            module = resolver.get_module(ModulePath(('typing',)))
            all_names = module.get_dunder_all(resolver)
            print(all_names)  # ['Any', 'Union', 'Optional', ...]
        """

Key Points:

  • exists attribute indicates if module was found
  • names is empty dict if module doesn't exist
  • get_name() method caches results internally
  • get_dunder_all() returns None if __all__ not defined or cannot be extracted

ResolvedName Type

The result of resolving a name can be one of four types:

ResolvedName = Union[ModulePath, ImportedInfo, NameInfo, None]

Type Descriptions:

  1. None: Name not found

    • Module doesn't exist
    • Name doesn't exist in module
    • Name is private and not accessible
  2. ModulePath: Name refers to a module

    • Example: 'collections' resolves to ModulePath(('collections',))
    • Can be used with resolver.get_module() to access module
  3. ImportedInfo: Name is imported from another module

    • Contains source_module: ModulePath (where it's defined)
    • Contains info: NameInfo (information about the name)
    • Indicates the name is re-exported
  4. NameInfo: Name is defined directly in the module

    • Contains AST node and metadata
    • Most direct result type

Usage Pattern:

result = resolver.get_fully_qualified_name('some.name')

if result is None:
    print("Not found")
elif isinstance(result, ModulePath):
    print(f"Module: {'.'.join(result)}")
elif isinstance(result, ImportedInfo):
    print(f"Imported from: {'.'.join(result.source_module)}")
    print(f"Name: {result.info.name}")
elif isinstance(result, NameInfo):
    print(f"Defined directly: {result.name}")

ImportedInfo Structure

When a name is imported from another module, it's wrapped in an ImportedInfo:

class ImportedInfo(NamedTuple):
    """
    Information about an imported name with its source module.

    Attributes:
        source_module (ModulePath): Module where the name originates
        info (NameInfo): Information about the name
    """
    source_module: ModulePath
    info: NameInfo

Usage:

from typeshed_client import Resolver, ImportedInfo

resolver = Resolver()
result = resolver.get_fully_qualified_name('collections.OrderedDict')

if isinstance(result, ImportedInfo):
    print(f"Originally defined in: {'.'.join(result.source_module)}")
    print(f"Name: {result.info.name}")
    print(f"Exported: {result.info.is_exported}")
    print(f"AST type: {type(result.info.ast).__name__}")

Usage Examples

Basic Name Resolution

from typeshed_client import Resolver

resolver = Resolver()

# Resolve typing.List
result = resolver.get_fully_qualified_name('typing.List')
if result:
    print(f"typing.List resolved to: {type(result).__name__}")
else:
    print("typing.List not found")

# Resolve collections.OrderedDict
result = resolver.get_fully_qualified_name('collections.OrderedDict')
if result:
    print(f"collections.OrderedDict resolved to: {type(result).__name__}")
else:
    print("collections.OrderedDict not found")

# Try non-existent name
result = resolver.get_fully_qualified_name('nonexistent.Name')
if result is None:
    print("nonexistent.Name not found")

Following Re-exports

from typeshed_client import Resolver, ImportedInfo, NameInfo, ModulePath

resolver = Resolver()

# Some names may be re-exported from other modules
result = resolver.get_fully_qualified_name('collections.OrderedDict')

if result is None:
    print("Not found")
elif isinstance(result, ModulePath):
    print(f"It's a module: {'.'.join(result)}")
elif isinstance(result, ImportedInfo):
    print(f"Imported from: {'.'.join(result.source_module)}")
    print(f"Original name: {result.info.name}")
    print(f"Exported: {result.info.is_exported}")
    
    # Access the actual NameInfo
    info = result.info
    print(f"AST type: {type(info.ast).__name__}")
elif isinstance(result, NameInfo):
    print(f"Defined directly in collections")
    print(f"Name: {result.name}")

Checking if Name is a Module

from typeshed_client import Resolver, ModulePath

resolver = Resolver()

names_to_check = ['collections', 'collections.OrderedDict', 'typing.List']

for name in names_to_check:
    result = resolver.get_fully_qualified_name(name)
    
    if isinstance(result, ModulePath):
        print(f"✓ '{name}' is a module")
    elif result is not None:
        print(f"✗ '{name}' is not a module, it's a {type(result).__name__}")
    else:
        print(f"✗ '{name}' not found")

Extracting Type Information

from typeshed_client import Resolver, NameInfo, ImportedInfo
import ast

resolver = Resolver()

# Get the definition of typing.Union
result = resolver.get_fully_qualified_name('typing.Union')

# Extract NameInfo from result
info = None
if isinstance(result, NameInfo):
    info = result
elif isinstance(result, ImportedInfo):
    info = result.info

if info:
    # Access the AST node
    if isinstance(info.ast, ast.Assign):
        print(f"typing.Union is defined as an assignment")
    elif isinstance(info.ast, ast.ClassDef):
        print(f"typing.Union is a class")
    elif isinstance(info.ast, ast.FunctionDef):
        print(f"typing.Union is a function")
    
    # Check if it's exported
    print(f"Exported: {info.is_exported}")
else:
    print("Could not extract NameInfo")

Working with Module Objects

from typeshed_client import Resolver, ModulePath

resolver = Resolver()

# Get the typing module
module = resolver.get_module(ModulePath(('typing',)))

# Check if module exists
if module.exists:
    print(f"✓ typing module found")
    print(f"  Names: {len(module.names)}")
    
    # Get __all__
    all_names = module.get_dunder_all(resolver)
    if all_names:
        print(f"  Exports {len(all_names)} names via __all__")
        print(f"  First 5: {all_names[:5]}")
    else:
        print(f"  No __all__ defined")
    
    # Look up a specific name
    result = module.get_name('List', resolver)
    if result:
        print(f"  typing.List: {type(result).__name__}")
    else:
        print(f"  typing.List not found")
else:
    print("✗ typing module not found")

# Try non-existent module
bad_module = resolver.get_module(ModulePath(('nonexistent',)))
print(f"\nnonexistent module exists: {bad_module.exists}")
print(f"nonexistent module names: {len(bad_module.names)}")

Resolving Names from Different Modules

from typeshed_client import Resolver, ModulePath

resolver = Resolver()

# Resolve 'deque' from collections module
result = resolver.get_name(ModulePath(('collections',)), 'deque')
if result:
    print(f"collections.deque: {type(result).__name__}")
else:
    print("collections.deque not found")

# Resolve 'OrderedDict' from collections module
result = resolver.get_name(ModulePath(('collections',)), 'OrderedDict')
if result:
    print(f"collections.OrderedDict: {type(result).__name__}")
else:
    print("collections.OrderedDict not found")

# Try non-existent name
result = resolver.get_name(ModulePath(('collections',)), 'NonExistent')
if result is None:
    print("collections.NonExistent not found")

Finding Original Definition

from typeshed_client import Resolver, ImportedInfo, NameInfo, ModulePath, ImportedName

def find_original_definition(resolver: Resolver, qualified_name: str):
    """Follow imports to find the original definition."""
    result = resolver.get_fully_qualified_name(qualified_name)
    
    # Track the chain of imports
    chain = [qualified_name]

    # Follow ImportedInfo chain
    while isinstance(result, ImportedInfo):
        source_module = result.source_module
        original_name = result.info.name
        
        next_name = f"{'.'.join(source_module)}.{original_name}"
        chain.append(next_name)
        
        # Check if the info itself is an import
        if isinstance(result.info.ast, ImportedName):
            # Follow to the imported module
            result = resolver.get_name(
                result.info.ast.module_name,
                result.info.ast.name if result.info.ast.name else original_name
            )
        else:
            # Found the original definition
            break

    return result, chain

resolver = Resolver()
original, import_chain = find_original_definition(resolver, 'collections.OrderedDict')

print(f"Import chain: {' -> '.join(import_chain)}")
if original:
    result_type = type(original).__name__
    print(f"Original definition type: {result_type}")
    
    # Extract NameInfo if available
    if isinstance(original, NameInfo):
        print(f"Name: {original.name}")
    elif isinstance(original, ImportedInfo):
        print(f"Name: {original.info.name}")
else:
    print("Could not resolve to original definition")

Inspecting Class Definitions

from typeshed_client import Resolver, NameInfo, ImportedInfo
import ast

resolver = Resolver()

# Get OrderedDict class
result = resolver.get_fully_qualified_name('collections.OrderedDict')

# Extract NameInfo
info = None
if isinstance(result, NameInfo):
    info = result
elif isinstance(result, ImportedInfo):
    info = result.info

if info and isinstance(info.ast, ast.ClassDef):
    class_def = info.ast
    
    print(f"Class: {info.name}")
    print(f"Exported: {info.is_exported}")
    
    # Get base classes
    if class_def.bases:
        bases = [ast.unparse(base) for base in class_def.bases]
        print(f"Base classes: {', '.join(bases)}")
    
    # Get methods
    if info.child_nodes:
        public_methods = [
            name for name, child_info in info.child_nodes.items()
            if child_info.is_exported
        ]
        private_methods = [
            name for name, child_info in info.child_nodes.items()
            if not child_info.is_exported
        ]
        print(f"Public methods: {len(public_methods)}")
        print(f"Private methods: {len(private_methods)}")
        print(f"Some public methods: {public_methods[:5]}")
    else:
        print("No child nodes available")
else:
    print("Not a class or not found")

Version-Specific Resolution

from typeshed_client import Resolver, get_search_context

versions = [(3, 9), (3, 11), (3, 12)]
name_to_check = 'typing.TypedDict'

print(f"Checking '{name_to_check}' across Python versions:")
for version in versions:
    # Create version-specific context and resolver
    ctx = get_search_context(version=version)
    resolver = Resolver(search_context=ctx)
    
    # Resolve the name
    result = resolver.get_fully_qualified_name(name_to_check)
    
    if result is None:
        status = "Not found"
    else:
        status = f"Found ({type(result).__name__})"
    
    print(f"  Python {version[0]}.{version[1]}: {status}")

Checking Module Existence

from typeshed_client import Resolver, ModulePath

resolver = Resolver()

# Check if modules exist
modules_to_check = [
    ('typing',),
    ('collections',),
    ('collections', 'abc'),
    ('nonexistent_module',),
]

print("Module existence check:")
for module_path in modules_to_check:
    module = resolver.get_module(ModulePath(module_path))
    status = "✓ exists" if module.exists else "✗ not found"
    module_name = '.'.join(module_path)
    print(f"  {module_name}: {status}")
    
    if module.exists:
        print(f"    ({len(module.names)} names)")

Building a Name Index

from typeshed_client import Resolver, ModulePath, NameInfo, ImportedName, OverloadedName
import ast

def index_module(resolver: Resolver, module_path: ModulePath):
    """Build an index of all names in a module."""
    module = resolver.get_module(module_path)
    
    if not module.exists:
        return None

    index = {
        'functions': [],
        'classes': [],
        'constants': [],
        'imports': [],
        'overloaded': [],
        'exported_count': 0,
        'private_count': 0,
    }

    for name, info in module.names.items():
        # Count export status
        if info.is_exported:
            index['exported_count'] += 1
        else:
            index['private_count'] += 1
        
        # Categorize by type
        if isinstance(info.ast, ast.FunctionDef):
            index['functions'].append(name)
        elif isinstance(info.ast, ast.ClassDef):
            index['classes'].append(name)
        elif isinstance(info.ast, ImportedName):
            index['imports'].append(name)
        elif isinstance(info.ast, OverloadedName):
            index['overloaded'].append(name)
        else:
            index['constants'].append(name)

    return index

resolver = Resolver()
index = index_module(resolver, ModulePath(('typing',)))

if index:
    print("typing module index:")
    print(f"  Functions: {len(index['functions'])}")
    print(f"  Classes: {len(index['classes'])}")
    print(f"  Constants: {len(index['constants'])}")
    print(f"  Imports: {len(index['imports'])}")
    print(f"  Overloaded: {len(index['overloaded'])}")
    print(f"  Exported: {index['exported_count']}")
    print(f"  Private: {index['private_count']}")
else:
    print("Could not index typing module")

Advanced Patterns

Pattern: Resolve All Module Names

from typeshed_client import Resolver, ModulePath

def resolve_all_names(resolver: Resolver, module_path: ModulePath):
    """Resolve all names in a module."""
    module = resolver.get_module(module_path)
    
    if not module.exists:
        return {}
    
    resolved = {}
    for name in module.names.keys():
        result = module.get_name(name, resolver)
        resolved[name] = {
            'result': result,
            'type': type(result).__name__ if result else 'None'
        }
    
    return resolved

resolver = Resolver()
all_resolved = resolve_all_names(resolver, ModulePath(('collections',)))

print(f"Resolved {len(all_resolved)} names from collections:")
# Show first 10
for name, details in list(all_resolved.items())[:10]:
    print(f"  {name}: {details['type']}")

Pattern: Find Cross-Module Dependencies

from typeshed_client import Resolver, ModulePath, ImportedName

def find_dependencies(resolver: Resolver, module_path: ModulePath):
    """Find all modules that a given module depends on."""
    module = resolver.get_module(module_path)
    
    if not module.exists:
        return set()
    
    dependencies = set()
    for name, info in module.names.items():
        if isinstance(info.ast, ImportedName):
            dep_module = '.'.join(info.ast.module_name)
            dependencies.add(dep_module)
    
    return dependencies

resolver = Resolver()
deps = find_dependencies(resolver, ModulePath(('collections',)))

print(f"collections depends on:")
for dep in sorted(deps):
    print(f"  - {dep}")

Pattern: Compare Name Across Versions

from typeshed_client import Resolver, get_search_context, NameInfo, ImportedInfo
import ast

def compare_name_across_versions(name: str, versions):
    """Compare how a name is defined across Python versions."""
    results = {}
    
    for version in versions:
        ctx = get_search_context(version=version)
        resolver = Resolver(search_context=ctx)
        result = resolver.get_fully_qualified_name(name)
        
        # Extract type of definition
        if result is None:
            results[version] = "Not found"
        elif isinstance(result, ModulePath):
            results[version] = "Module"
        elif isinstance(result, NameInfo):
            results[version] = type(result.ast).__name__
        elif isinstance(result, ImportedInfo):
            results[version] = f"Imported ({type(result.info.ast).__name__})"
        else:
            results[version] = type(result).__name__
    
    return results

# Compare typing.Literal across versions
versions = [(3, 7), (3, 8), (3, 9), (3, 10), (3, 11), (3, 12)]
comparison = compare_name_across_versions('typing.Literal', versions)

print("typing.Literal across Python versions:")
for version, status in comparison.items():
    print(f"  Python {version[0]}.{version[1]}: {status}")

Pattern: Extract Function Signatures

from typeshed_client import Resolver, NameInfo, ImportedInfo, OverloadedName
import ast

def extract_function_signature(resolver: Resolver, qualified_name: str):
    """Extract function signature(s) from a resolved name."""
    result = resolver.get_fully_qualified_name(qualified_name)
    
    # Extract NameInfo
    info = None
    if isinstance(result, NameInfo):
        info = result
    elif isinstance(result, ImportedInfo):
        info = result.info
    
    if not info:
        return None
    
    signatures = []
    
    # Handle overloaded functions
    if isinstance(info.ast, OverloadedName):
        for defn in info.ast.definitions:
            if isinstance(defn, ast.FunctionDef):
                # Build signature string
                args = [f"{arg.arg}" for arg in defn.args.args]
                ret = "Any"
                if defn.returns:
                    ret = ast.unparse(defn.returns)
                sig = f"def {defn.name}({', '.join(args)}) -> {ret}"
                signatures.append(sig)
    elif isinstance(info.ast, ast.FunctionDef):
        # Single function
        defn = info.ast
        args = [f"{arg.arg}" for arg in defn.args.args]
        ret = "Any"
        if defn.returns:
            ret = ast.unparse(defn.returns)
        sig = f"def {defn.name}({', '.join(args)}) -> {ret}"
        signatures.append(sig)
    
    return signatures

resolver = Resolver()

# Test with a function that may have overloads
test_names = ['typing.cast', 'typing.overload', 'collections.OrderedDict']
for name in test_names:
    sigs = extract_function_signature(resolver, name)
    if sigs:
        print(f"\n{name}:")
        for i, sig in enumerate(sigs, 1):
            print(f"  {i}. {sig}")
    else:
        print(f"\n{name}: No signatures (may not be a function)")

Pattern: Validate Name Accessibility

from typeshed_client import Resolver, NameInfo, ImportedInfo

def is_name_accessible(resolver: Resolver, qualified_name: str) -> bool:
    """Check if a name is accessible (exists and is exported)."""
    result = resolver.get_fully_qualified_name(qualified_name)
    
    if result is None:
        return False
    
    # Modules are always accessible
    if isinstance(result, ModulePath):
        return True
    
    # Extract NameInfo
    info = None
    if isinstance(result, NameInfo):
        info = result
    elif isinstance(result, ImportedInfo):
        info = result.info
    
    return info is not None and info.is_exported

resolver = Resolver()

# Check some names
names_to_check = [
    'typing.List',
    'typing._SpecialForm',  # Private
    'collections.OrderedDict',
    'nonexistent.Module',
]

print("Name accessibility check:")
for name in names_to_check:
    accessible = is_name_accessible(resolver, name)
    status = "✓ accessible" if accessible else "✗ not accessible"
    print(f"  {name}: {status}")

Pattern: Resolve Attribute Access Chain

from typeshed_client import Resolver, NameInfo, ImportedInfo
import ast

def resolve_attribute_chain(resolver: Resolver, base_name: str, attributes: list[str]):
    """Resolve a chain of attribute accesses like 'module.Class.method'."""
    # Start with the base name
    result = resolver.get_fully_qualified_name(base_name)
    
    if result is None:
        return None
    
    # Follow each attribute
    for attr in attributes:
        # Extract NameInfo
        info = None
        if isinstance(result, NameInfo):
            info = result
        elif isinstance(result, ImportedInfo):
            info = result.info
        else:
            return None  # Can't follow attributes on modules
        
        # Check child_nodes for the attribute
        if info.child_nodes and attr in info.child_nodes:
            result = info.child_nodes[attr]
        else:
            return None  # Attribute not found
    
    return result

resolver = Resolver()

# Resolve typing.TypedDict.__init__
result = resolve_attribute_chain(resolver, 'typing.TypedDict', ['__init__'])
if result:
    print(f"✓ Found typing.TypedDict.__init__: {type(result).__name__}")
    if isinstance(result, NameInfo):
        print(f"  Exported: {result.is_exported}")
else:
    print("✗ typing.TypedDict.__init__ not found")

# Try another chain
result = resolve_attribute_chain(resolver, 'collections.OrderedDict', ['__setitem__'])
if result:
    print(f"✓ Found collections.OrderedDict.__setitem__")
else:
    print("✗ collections.OrderedDict.__setitem__ not accessible")

Resolution Strategy

The resolver follows this strategy when resolving names:

  1. Check local cache: If the name has been resolved before, return cached result
  2. Look up in module: Find the name in the module's NameDict
  3. Handle direct definitions: If the name is defined directly (not imported), return NameInfo
  4. Follow imports: If the name is an ImportedName:
    • Check if it's a module import (name is None)
    • Check if the imported module exists
    • Recursively resolve from the source module
    • Wrap in ImportedInfo if found
  5. Return None: If name cannot be resolved

This strategy handles complex import chains and re-exports correctly, following the chain until reaching the original definition.

Caching Behavior

The Resolver maintains internal caches:

  • Module cache: Parsed modules are cached to avoid re-parsing

    • Key: ModulePath
    • Value: Module object
    • Lifetime: Duration of Resolver instance
  • Name cache: Resolved names within modules are cached to avoid redundant lookups

    • Key: (module_path, name)
    • Value: ResolvedName
    • Lifetime: Duration of Module instance

Best practices:

  • Reuse the same Resolver instance for multiple lookups
  • Don't create new resolvers unnecessarily
  • Cache is automatically managed; no manual clearing needed
  • Cache is memory-efficient (only caches actually accessed modules)
from typeshed_client import Resolver

# GOOD: Reuse resolver for caching benefit
resolver = Resolver()
for name in ['typing.List', 'typing.Dict', 'typing.Set']:
    result = resolver.get_fully_qualified_name(name)  # Benefits from caching

# BAD: Create new resolver each time (no caching benefit)
for name in ['typing.List', 'typing.Dict', 'typing.Set']:
    resolver = Resolver()  # Wasteful, no caching benefit
    result = resolver.get_fully_qualified_name(name)

Performance Characteristics:

  • First resolution: Parses stub + resolves name
  • Subsequent resolutions of same name: O(1) cache lookup
  • Different names in same module: Only parses module once

Best Practices for Agents

Instance Reuse

  1. Reuse Resolver instances: The resolver caches parsed modules and resolved names
# CORRECT - efficient caching
resolver = Resolver()
result1 = resolver.get_fully_qualified_name('typing.List')
result2 = resolver.get_fully_qualified_name('typing.Dict')
result3 = resolver.get_fully_qualified_name('typing.Set')

# INCORRECT - no caching benefit
resolver1 = Resolver()
result1 = resolver1.get_fully_qualified_name('typing.List')
resolver2 = Resolver()  # Loses all caches
result2 = resolver2.get_fully_qualified_name('typing.Dict')

Type Checking

  1. Check result type: Always use isinstance() to check if result is ModulePath, ImportedInfo, NameInfo, or None
# CORRECT - explicit type checking
result = resolver.get_fully_qualified_name('collections.OrderedDict')
if result is None:
    print("Not found")
elif isinstance(result, ModulePath):
    print("It's a module")
elif isinstance(result, ImportedInfo):
    print(f"Imported from {'.'.join(result.source_module)}")
elif isinstance(result, NameInfo):
    print("Defined directly")

# INCORRECT - assumes result is NameInfo
result = resolver.get_fully_qualified_name('collections.OrderedDict')
print(result.name)  # AttributeError if result is ModulePath or None

Extracting NameInfo

  1. Extract NameInfo correctly: ImportedInfo wraps NameInfo; extract it to access AST
# CORRECT - extract NameInfo from ImportedInfo
from typeshed_client import ImportedInfo, NameInfo

result = resolver.get_fully_qualified_name('some.name')
info = None

if isinstance(result, NameInfo):
    info = result
elif isinstance(result, ImportedInfo):
    info = result.info

if info:
    # Now safe to access NameInfo fields
    print(f"Name: {info.name}")
    print(f"Exported: {info.is_exported}")

Type Guards

  1. Use type guards: Protect attribute access with type checks to avoid AttributeError
# CORRECT - type guards
import ast

if isinstance(result, (NameInfo, ImportedInfo)):
    info = result.info if isinstance(result, ImportedInfo) else result
    if isinstance(info.ast, ast.ClassDef):
        # Safe to access ClassDef attributes
        print(f"Class with {len(info.ast.bases)} bases")

# INCORRECT - no type guards
info = result.info  # AttributeError if result is None or ModulePath
print(info.ast.bases)  # May fail if ast is not ClassDef

Module Existence

  1. Check module.exists: Not all modules exist; check exists attribute before accessing names
# CORRECT
module = resolver.get_module(ModulePath(('some_module',)))
if module.exists:
    print(f"Found {len(module.names)} names")
else:
    print("Module doesn't exist")

# MISLEADING - module.names is empty dict even if module doesn't exist
module = resolver.get_module(ModulePath(('nonexistent',)))
print(f"Names: {len(module.names)}")  # Prints 0, but module doesn't exist

Following Imports

  1. Follow import chains: Use recursive logic to follow imports to their original definition
# CORRECT - follow imports recursively
from typeshed_client import ImportedInfo, ImportedName

def follow_to_source(resolver, qualified_name):
    result = resolver.get_fully_qualified_name(qualified_name)
    
    while isinstance(result, ImportedInfo):
        if isinstance(result.info.ast, ImportedName):
            # Follow to source
            result = resolver.get_name(
                result.info.ast.module_name,
                result.info.ast.name
            )
        else:
            # Found the original
            break
    
    return result

Version Awareness

  1. Version awareness: Use appropriate SearchContext when resolving version-specific names
# CORRECT - version-specific resolvers
ctx_39 = get_search_context(version=(3, 9))
ctx_312 = get_search_context(version=(3, 12))

resolver_39 = Resolver(search_context=ctx_39)
resolver_312 = Resolver(search_context=ctx_312)

# Check availability in each version
result_39 = resolver_39.get_fully_qualified_name('typing.TypeGuard')
result_312 = resolver_312.get_fully_qualified_name('typing.TypeGuard')

Cache-Friendly Patterns

  1. Cache-friendly patterns: Structure code to maximize benefit from resolver's internal caches
# CORRECT - batch operations with same resolver
resolver = Resolver()
names = ['typing.List', 'typing.Dict', 'typing.Tuple', 'typing.Set']
results = {name: resolver.get_fully_qualified_name(name) for name in names}

# INCORRECT - creates resolver inside loop
for name in names:
    resolver = Resolver()  # New cache each iteration
    result = resolver.get_fully_qualified_name(name)

Null Handling

  1. Handle None gracefully: Resolution can fail; always check for None before using results
# CORRECT - check for None
result = resolver.get_fully_qualified_name('maybe.exists')
if result is not None:
    # Safe to use result
    print(f"Found: {type(result).__name__}")
else:
    print("Not found")

# INCORRECT - assumes result exists
result = resolver.get_fully_qualified_name('maybe.exists')
print(result.name)  # AttributeError if result is None

Thread Safety

  1. Thread safety: Do not share Resolver instances across threads; create separate instances
# CORRECT - separate resolver per thread
import threading

def process_in_thread(names):
    resolver = Resolver()  # Thread-local resolver
    for name in names:
        result = resolver.get_fully_qualified_name(name)
        # Process result

# INCORRECT - sharing resolver
resolver = Resolver()  # Shared

def bad_thread_func(names):
    for name in names:
        result = resolver.get_fully_qualified_name(name)  # Not thread-safe