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

examples

edge-cases.mdreal-world-scenarios.md
index.md
tile.json

tessl/pypi-typeshed-client

tessl install tessl/pypi-typeshed-client@2.8.4

A library for accessing stubs in typeshed.

edge-cases.mddocs/examples/

Edge Cases & Advanced Patterns

This document covers advanced patterns, edge cases, and tricky scenarios when using typeshed_client.

Edge Case 1: Following Deep Import Chains

Some names are re-exported multiple times before reaching the original definition.

from typeshed_client import Resolver, ImportedInfo, ImportedName, NameInfo

def follow_import_chain(resolver: Resolver, qualified_name: str, max_depth=10):
    """Follow import chain to original definition, handling deep chains."""
    chain = [qualified_name]
    result = resolver.get_fully_qualified_name(qualified_name)
    depth = 0
    
    while isinstance(result, ImportedInfo) and depth < max_depth:
        # Track the chain
        source = '.'.join(result.source_module)
        name = result.info.name
        next_name = f"{source}.{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 name
            )
        else:
            # Found the original definition
            break
        
        depth += 1
    
    if depth >= max_depth:
        return chain, None, "Max depth reached"
    
    return chain, result, None

# Usage
resolver = Resolver()
chain, original, error = follow_import_chain(resolver, 'collections.OrderedDict')

if error:
    print(f"Error: {error}")
else:
    print(f"Import chain ({len(chain)} steps):")
    for i, step in enumerate(chain):
        print(f"  {i}. {step}")
    if original:
        print(f"Original: {type(original).__name__}")

Edge Case 2: Handling Module vs Name Ambiguity

A name might refer to either a module or a name within a module.

from typeshed_client import Resolver, ModulePath, NameInfo, ImportedInfo

def resolve_ambiguous_name(resolver: Resolver, name: str) -> dict:
    """Handle cases where name could be module or attribute."""
    parts = name.split('.')
    results = {}
    
    # Try as full module
    module_result = resolver.get_fully_qualified_name(name)
    if isinstance(module_result, ModulePath):
        results['as_module'] = {'type': 'module', 'path': module_result}
    
    # Try as parent module + attribute
    if len(parts) >= 2:
        parent = '.'.join(parts[:-1])
        attr = parts[-1]
        module = resolver.get_module(ModulePath(tuple(parent.split('.'))))
        
        if module.exists:
            attr_result = module.get_name(attr, resolver)
            if attr_result:
                results['as_attribute'] = {
                    'parent_module': parent,
                    'attribute': attr,
                    'type': type(attr_result).__name__
                }
    
    return results

# Usage
resolver = Resolver()

# 'collections.abc' could be a submodule or an attribute
results = resolve_ambiguous_name(resolver, 'collections.abc')
print(f"Interpretations: {list(results.keys())}")

Edge Case 3: Circular Imports

Handle modules that import from each other.

from typeshed_client import Resolver, ModulePath, ImportedName

def detect_circular_imports(resolver: Resolver, module_name: str, visited=None) -> list:
    """Detect circular import dependencies."""
    if visited is None:
        visited = []
    
    if module_name in visited:
        # Found a cycle
        cycle_start = visited.index(module_name)
        return visited[cycle_start:] + [module_name]
    
    module = resolver.get_module(ModulePath(tuple(module_name.split('.'))))
    if not module.exists:
        return []
    
    visited = visited + [module_name]
    
    # Check each import
    for name, info in module.names.items():
        if isinstance(info.ast, ImportedName):
            dep = '.'.join(info.ast.module_name)
            cycle = detect_circular_imports(resolver, dep, visited)
            if cycle:
                return cycle
    
    return []

# Usage
resolver = Resolver()
cycle = detect_circular_imports(resolver, 'typing')
if cycle:
    print(f"Circular dependency: {' -> '.join(cycle)}")
else:
    print("No circular dependencies detected")

Edge Case 4: Version-Specific Overloads

Overloads may differ between Python versions.

from typeshed_client import get_search_context, get_stub_names, OverloadedName
import ast

def compare_overloads_across_versions(
    module_name: str, 
    func_name: str, 
    versions: list[tuple[int, int]]
) -> dict:
    """Compare function overloads across Python versions."""
    results = {}
    
    for version in versions:
        ctx = get_search_context(version=version)
        names = get_stub_names(module_name, search_context=ctx)
        
        if not names or func_name not in names:
            results[version] = {'exists': False}
            continue
        
        info = names[func_name]
        
        if isinstance(info.ast, OverloadedName):
            overload_count = len(info.ast.definitions)
            signatures = []
            
            for defn in info.ast.definitions:
                if isinstance(defn, ast.FunctionDef):
                    args = [arg.arg for arg in defn.args.args]
                    ret = ast.unparse(defn.returns) if defn.returns else "Any"
                    signatures.append(f"({', '.join(args)}) -> {ret}")
            
            results[version] = {
                'exists': True,
                'overload_count': overload_count,
                'signatures': signatures
            }
        elif isinstance(info.ast, ast.FunctionDef):
            args = [arg.arg for arg in info.ast.args.args]
            ret = ast.unparse(info.ast.returns) if info.ast.returns else "Any"
            results[version] = {
                'exists': True,
                'overload_count': 1,
                'signatures': [f"({', '.join(args)}) -> {ret}"]
            }
        else:
            results[version] = {'exists': True, 'not_function': True}
    
    return results

# Usage
versions = [(3, 9), (3, 10), (3, 11)]
results = compare_overloads_across_versions('typing', 'cast', versions)

for version, data in results.items():
    if data.get('exists'):
        count = data.get('overload_count', 0)
        print(f"Python {version}: {count} overload(s)")
    else:
        print(f"Python {version}: Not found")

Edge Case 5: Platform-Specific Class Members

Class methods/attributes may vary by platform.

from typeshed_client import get_search_context, get_stub_names
import ast

def compare_class_members_across_platforms(
    module_name: str,
    class_name: str,
    platforms: list[str]
) -> dict:
    """Compare class members across platforms."""
    results = {}
    
    for platform in platforms:
        ctx = get_search_context(platform=platform)
        names = get_stub_names(module_name, search_context=ctx)
        
        if not names or class_name not in names:
            results[platform] = {'exists': False}
            continue
        
        class_info = names[class_name]
        
        if not isinstance(class_info.ast, ast.ClassDef):
            results[platform] = {'not_a_class': True}
            continue
        
        if class_info.child_nodes:
            members = {
                name: {
                    'exported': info.is_exported,
                    'type': type(info.ast).__name__
                }
                for name, info in class_info.child_nodes.items()
            }
            results[platform] = {'exists': True, 'members': members}
        else:
            results[platform] = {'exists': True, 'members': {}}
    
    # Find platform-specific members
    all_members = set()
    for platform_data in results.values():
        if platform_data.get('members'):
            all_members.update(platform_data['members'].keys())
    
    platform_specific = {}
    for platform, data in results.items():
        if data.get('members'):
            platform_members = set(data['members'].keys())
            other_platforms_members = set()
            for other_platform, other_data in results.items():
                if other_platform != platform and other_data.get('members'):
                    other_platforms_members.update(other_data['members'].keys())
            
            unique = platform_members - other_platforms_members
            if unique:
                platform_specific[platform] = unique
    
    return {
        'all_members': all_members,
        'platform_specific': platform_specific,
        'details': results
    }

# Usage
platforms = ['linux', 'win32', 'darwin']
results = compare_class_members_across_platforms('os', 'stat_result', platforms)

if results['platform_specific']:
    print("Platform-specific members:")
    for platform, members in results['platform_specific'].items():
        print(f"  {platform}: {members}")

Edge Case 6: Empty Modules

Handle modules with no exported names.

from typeshed_client import get_stub_names

def analyze_empty_module(module_name: str) -> dict:
    """Analyze modules that appear empty."""
    names = get_stub_names(module_name)
    
    if names is None:
        return {'status': 'not_found'}
    
    if len(names) == 0:
        return {'status': 'truly_empty', 'total': 0}
    
    exported = sum(1 for info in names.values() if info.is_exported)
    
    if exported == 0:
        return {
            'status': 'all_private',
            'total': len(names),
            'exported': 0,
            'private': len(names)
        }
    
    return {
        'status': 'normal',
        'total': len(names),
        'exported': exported,
        'private': len(names) - exported
    }

# Usage
test_modules = ['typing', 'sys', '_typeshed']
for module in test_modules:
    result = analyze_empty_module(module)
    print(f"{module}: {result['status']}")

Edge Case 7: Nested Class Definitions

Access deeply nested class members.

from typeshed_client import get_stub_names, NameInfo
import ast

def access_nested_class(module_name: str, class_path: list[str]) -> dict:
    """Access nested class definitions."""
    names = get_stub_names(module_name)
    
    if not names:
        return {'error': 'Module not found'}
    
    current = None
    path_taken = []
    
    for class_name in class_path:
        path_taken.append(class_name)
        
        if current is None:
            # First level - look in module
            if class_name not in names:
                return {
                    'error': f'{class_name} not found in {module_name}',
                    'path_taken': path_taken
                }
            current = names[class_name]
        else:
            # Nested level - look in child_nodes
            if not current.child_nodes or class_name not in current.child_nodes:
                return {
                    'error': f'{class_name} not found in {".".join(path_taken[:-1])}',
                    'path_taken': path_taken
                }
            current = current.child_nodes[class_name]
        
        # Verify it's a class
        if not isinstance(current.ast, ast.ClassDef):
            return {
                'error': f'{class_name} is not a class',
                'path_taken': path_taken,
                'actual_type': type(current.ast).__name__
            }
    
    # Successfully found nested class
    result = {
        'found': True,
        'path': '.'.join([module_name] + class_path),
        'exported': current.is_exported
    }
    
    if current.child_nodes:
        result['members'] = list(current.child_nodes.keys())
    
    return result

# Usage
# Try to access typing.TypedDict.__init__
result = access_nested_class('typing', ['TypedDict', '__init__'])
print(f"Found: {result.get('found', False)}")
if result.get('error'):
    print(f"Error: {result['error']}")

Edge Case 8: Type Alias vs Actual Type

Distinguish between type aliases and actual type definitions.

from typeshed_client import get_stub_names
import ast

def identify_type_category(module_name: str, name: str) -> dict:
    """Identify if a name is a type alias, class, or other."""
    names = get_stub_names(module_name)
    
    if not names or name not in names:
        return {'category': 'not_found'}
    
    info = names[name]
    
    # Check AST node type
    if isinstance(info.ast, ast.ClassDef):
        return {
            'category': 'class',
            'name': name,
            'bases': [ast.unparse(b) for b in info.ast.bases] if info.ast.bases else []
        }
    
    if isinstance(info.ast, ast.AnnAssign):
        # Type alias with annotation
        annotation = ast.unparse(info.ast.annotation) if info.ast.annotation else "?"
        return {
            'category': 'type_alias_annotated',
            'name': name,
            'annotation': annotation
        }
    
    if isinstance(info.ast, ast.Assign):
        # Type alias without annotation
        if info.ast.value:
            value = ast.unparse(info.ast.value)
            return {
                'category': 'type_alias_simple',
                'name': name,
                'value': value
            }
    
    if isinstance(info.ast, ast.FunctionDef):
        return {
            'category': 'function',
            'name': name
        }
    
    return {
        'category': 'other',
        'name': name,
        'ast_type': type(info.ast).__name__
    }

# Usage
type_names = ['List', 'Dict', 'TypedDict', 'Union']
for name in type_names:
    result = identify_type_category('typing', name)
    print(f"{name}: {result['category']}")

Edge Case 9: Missing all with Conditional Exports

Handle modules where exports depend on version/platform without all.

from typeshed_client import get_search_context, get_stub_names

def compare_conditional_exports(
    module_name: str,
    conditions: list[dict]  # [{'version': (3,10)}, {'platform': 'win32'}, ...]
) -> dict:
    """Compare exports under different conditions."""
    results = {}
    
    for i, condition in enumerate(conditions):
        ctx = get_search_context(**condition)
        names = get_stub_names(module_name, search_context=ctx)
        
        if names:
            exported = {name for name, info in names.items() if info.is_exported}
            label = f"condition_{i}"
            for key, value in condition.items():
                label = f"{key}={value}"
                break
            results[label] = exported
        else:
            results[label] = set()
    
    # Analyze differences
    all_exports = set().union(*results.values())
    always_exported = set.intersection(*results.values()) if results else set()
    conditional = all_exports - always_exported
    
    return {
        'always_exported': always_exported,
        'conditionally_exported': conditional,
        'by_condition': results
    }

# Usage
conditions = [
    {'version': (3, 9)},
    {'version': (3, 12)},
    {'platform': 'win32'},
    {'platform': 'linux'}
]

results = compare_conditional_exports('typing', conditions)
print(f"Always exported: {len(results['always_exported'])}")
print(f"Conditional: {len(results['conditionally_exported'])}")

Edge Case 10: Handling Invalid SearchContext Combinations

Gracefully handle invalid configuration combinations.

from typeshed_client import get_search_context
from pathlib import Path

def safe_create_context(**kwargs) -> tuple:
    """Safely create SearchContext, catching errors."""
    try:
        ctx = get_search_context(**kwargs)
        return ctx, None
    except ValueError as e:
        return None, f"ValueError: {e}"
    except FileNotFoundError as e:
        return None, f"FileNotFoundError: {e}"
    except Exception as e:
        return None, f"Unexpected error: {e}"

# Test various invalid combinations
test_cases = [
    {
        'name': 'Both python_executable and search_path',
        'kwargs': {
            'python_executable': '/usr/bin/python3',
            'search_path': [Path('/tmp')]
        }
    },
    {
        'name': 'Nonexistent python_executable',
        'kwargs': {
            'python_executable': '/nonexistent/python'
        }
    },
    {
        'name': 'Invalid version tuple',
        'kwargs': {
            'version': (3,)  # Should be (major, minor)
        }
    }
]

for test in test_cases:
    ctx, error = safe_create_context(**test['kwargs'])
    if error:
        print(f"✗ {test['name']}: {error}")
    else:
        print(f"✓ {test['name']}: Success")

Best Practices for Edge Cases

  1. Always validate inputs: Check for None, empty collections, invalid types
  2. Set recursion limits: Prevent infinite loops in import chains
  3. Handle missing data: Not all NameInfo objects have child_nodes
  4. Check AST node types: Use isinstance() before accessing node attributes
  5. Catch specific exceptions: Handle InvalidStub, ValueError, FileNotFoundError
  6. Provide fallbacks: Have default behavior when edge cases occur
  7. Log unexpected cases: Track unusual patterns for debugging
  8. Test boundary conditions: Empty modules, deeply nested structures, circular dependencies

Common Anti-Patterns to Avoid

# ❌ WRONG: Not checking for None
names = get_stub_names('module')
for name in names:  # Crash if names is None
    print(name)

# ✓ CORRECT: Check for None
names = get_stub_names('module')
if names:
    for name in names:
        print(name)

# ❌ WRONG: Assuming child_nodes exists
class_info = names['MyClass']
for method in class_info.child_nodes:  # Crash if None
    print(method)

# ✓ CORRECT: Check child_nodes
if class_info.child_nodes:
    for method in class_info.child_nodes:
        print(method)

# ❌ WRONG: Not limiting recursion
def follow_imports(name):
    result = resolver.get_fully_qualified_name(name)
    if isinstance(result, ImportedInfo):
        follow_imports(...)  # Infinite loop possible

# ✓ CORRECT: Limit recursion depth
def follow_imports(name, depth=0, max_depth=10):
    if depth >= max_depth:
        return None
    result = resolver.get_fully_qualified_name(name)
    if isinstance(result, ImportedInfo):
        return follow_imports(..., depth + 1, max_depth)