tessl install tessl/pypi-typeshed-client@2.8.4A library for accessing stubs in typeshed.
This document covers advanced patterns, edge cases, and tricky scenarios when using typeshed_client.
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__}")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())}")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")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")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}")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']}")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']}")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']}")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'])}")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")# ❌ 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)