-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Feature Request: Auto-Type Dispatch + Pattern Matching
Executive Summary
Enhance SmartSwitch with two complementary features that eliminate boilerplate and enable powerful namespace-based dispatch:
- Auto-Type Inference: Automatically derive type-based dispatch from function type hints
- Pattern Matching: Enable regex-based function name matching with automatic type disambiguation
Motivation
Problem 1: Type Hint Duplication
Currently, SmartSwitch requires explicit typerule even when type hints are present:
@sw(typerule={'data': str}) # Redundant - already in signature
def process_string(data: str):
return data.upper()
@sw(typerule={'data': int}) # Redundant - already in signature
def process_number(data: int):
return data * 2This violates DRY principle - type information exists in two places.
Problem 2: Namespace-Based Operations
When implementing operations that accept different formats (JSON/dict, different protocols, etc.), we need separate function names but unified dispatch:
# Current: Must call by exact name
@sw
def add_json(data: str):
record = json.loads(data)
return db.insert(record)
@sw
def add_dict(data: dict):
return db.insert(data)
# User must know which to call
sw('add_json')(data='{"name": "Alice"}')
sw('add_dict')(data={'name': 'Bob'})
# Desired: Unified namespace dispatch
sw('add.*')(data='{"name": "Alice"}') # Auto-routes to add_json (str)
sw('add.*')(data={'name': 'Bob'}) # Auto-routes to add_dict (dict)Proposed Solution
Feature 1: Auto-Type Inference
Before (v0.6.0):
@sw(typerule={'data': str})
def process_string(data: str):
return data.upper()
@sw(typerule={'data': int})
def process_number(data: int):
return data * 2After (v0.7.0):
@sw # Type hints auto-inferred
def process_string(data: str):
return data.upper()
@sw # Type hints auto-inferred
def process_number(data: int):
return data * 2
# Auto-dispatch based on type
sw()(data="hello") # → process_string
sw()(data=42) # → process_numberDesign Rules:
- Auto-infer type hints ONLY when:
- No explicit
typeruleprovided - No explicit
valruleprovided - Function has at least one type hint
- No explicit
- Auto-inferred typerule created from ALL parameters with hints
- Explicit
typerulealways overrides hints - Return type hints are ignored (not used for dispatch)
Feature 2: Pattern Matching
Syntax: Full regex support via Python's re module
Examples:
sw('add.*') # Match: add_json, add_dict, add_csv
sw('add_(json|dict)') # Match: add_json, add_dict only
sw(r'process_\w+') # Match: process_string, process_number, etc.Dispatch Algorithm:
- Filter functions by regex pattern
- For each candidate (in registration order):
- Check signature compatibility (
sig.bind()) - Check type compatibility (using auto-inferred or explicit typerule)
- If compatible → execute and return
- Check signature compatibility (
- If no match → raise TypeError with candidates list
Priority Rules:
1. Named exact match: sw('add_json')
2. Valrule match: @sw(valrule=lambda x: x > 0)
3. Type match: Auto-inferred from hints
4. Fallback: No rules
Real-World Use Case
CRUD API with Multiple Formats:
from smartswitch import Switcher
import json
api = Switcher()
# JSON format
@api
def add_json(data: str, validate: bool = False):
"""Add record from JSON string."""
record = json.loads(data)
if validate:
schema_check(record)
return db.insert(record)
# Dict format with merge support
@api
def add_dict(data: dict, merge: bool = False, validate: bool = False):
"""Add record from dict."""
if validate:
schema_check(data)
if merge:
existing = db.get(data['id'])
data = {**existing, **data}
return db.insert(data)
# Unified dispatch - right function selected automatically
api('add.*')(data='{"name": "Alice"}') # → add_json (str)
api('add.*')(data={'name': 'Bob'}) # → add_dict (dict)
api('add.*')(data={'name': 'Charlie'}, merge=True) # → add_dict (accepts merge)
api('add.*')(data='{}', validate=True) # → add_json (str + validate)Why this matters:
- Unified API: Client calls
add.*, notadd_jsonvsadd_dict - Type safety: Automatic routing based on actual data type
- Extensibility: Add
add_csv,add_xmlwithout changing callers - Optional parameters: Functions with more specific params win when those params are provided
Technical Specification
Auto-Type Inference Implementation
# In Switcher.__call__() decorator
def __call__(self, name_or_func=None, /, **rules):
def wrapper(func):
# Auto-infer types if no explicit rules
if "typerule" not in rules and "valrule" not in rules:
from typing import get_type_hints
hints = get_type_hints(func)
if hints:
# Build typerule from hints (exclude 'return')
rules["typerule"] = {
param: hint
for param, hint in hints.items()
if param != "return"
}
# Existing registration logic...Pattern Matching Implementation
def _wildcard_dispatch(self, pattern: str):
"""Dispatch with regex pattern."""
import re
# Compile regex
try:
regex = re.compile(pattern)
except re.error as e:
raise ValueError(f"Invalid regex pattern '{pattern}': {e}")
# Filter candidates
candidates = [
(name, func)
for name, func in self._spells.items()
if regex.match(name)
]
if not candidates:
raise ValueError(f"No handlers match pattern: {pattern}")
# Return dispatcher
def dispatcher(*args, **kwargs):
import inspect
from typing import get_type_hints
# Try each candidate in registration order
for name, func in candidates:
sig = inspect.signature(func)
# Check signature compatibility
try:
bound = sig.bind(*args, **kwargs)
except TypeError:
continue # Signature doesn't accept these args
# Check type compatibility
hints = get_type_hints(func)
compatible = True
for param_name, value in bound.arguments.items():
if param_name in hints:
expected_type = hints[param_name]
if not isinstance(value, expected_type):
compatible = False
break
if compatible:
return func(*args, **kwargs)
# No match
names = ', '.join(n for n, _ in candidates)
raise TypeError(
f"No handler in [{names}] compatible with arguments: "
f"args={args}, kwargs={kwargs}"
)
return dispatcherPrecedence & Ambiguity Resolution
Dispatch Precedence:
- Exact name match:
sw('add_json')→ direct lookup - Pattern match:
sw('add.*')→ filter + type dispatch - Rule-based:
sw()→ valrule → typerule → fallback
Within pattern match:
- Signature compatibility (must accept provided args)
- Type compatibility (types must match)
- First compatible wins (registration order)
Optional Parameter Scoring:
- Function accepting more provided optional params is preferred
- If
merge=Trueprovided,add_dict(data, merge)wins overadd_dict(data)
API Changes
New in v0.7.0
Auto-type inference (always active):
@sw
def func(x: int): pass # Auto-creates typerule={'x': int}Pattern dispatch:
sw('pattern.*')(args) # Regex matching + type dispatch
sw(r'add_\w+')(args) # Full regex syntax supportedBackward compatibility: 100% - existing code works unchanged
Testing Strategy
Auto-Type Inference Tests
def test_auto_infer_single_param():
sw = Switcher()
@sw
def process_str(data: str):
return "string"
@sw
def process_int(data: int):
return "integer"
assert sw()(data="test") == "string"
assert sw()(data=42) == "integer"
def test_explicit_typerule_overrides_hints():
sw = Switcher()
@sw(typerule={'data': int}) # Explicit override
def process(data: str): # Hint ignored
return "int"
assert sw()(data=42) == "int"
with pytest.raises(TypeError):
sw()(data="test") # str not accepted
def test_valrule_disables_auto_infer():
sw = Switcher()
@sw(valrule=lambda x: x > 0)
def positive(x: int): # Hint not used for dispatch
return "positive"
assert sw()(x=5) == "positive"
assert sw()(x=-5) raises NoMatchError # valrule failedPattern Matching Tests
def test_pattern_with_different_types():
sw = Switcher()
@sw
def add_json(data: str):
return "json"
@sw
def add_dict(data: dict):
return "dict"
# Pattern + type dispatch
assert sw('add.*')(data='{}') == "json"
assert sw('add.*')(data={}) == "dict"
def test_pattern_with_optional_params():
sw = Switcher()
@sw
def add_simple(data: dict):
return "simple"
@sw
def add_with_merge(data: dict, merge: bool = False):
return "merge"
# Without merge: first match (simple)
assert sw('add.*')(data={}) == "simple"
# With merge: add_with_merge wins (accepts merge)
assert sw('add.*')(data={}, merge=True) == "merge"
def test_pattern_no_match_error():
sw = Switcher()
@sw
def add_json(data: str):
pass
# Pattern matches but type doesn't
with pytest.raises(TypeError, match="No handler.*compatible"):
sw('add.*')(data=123) # int not accepted by add_jsonImplementation Phases
Phase 1: Auto-Type Inference (v0.7.0)
- Implement auto-infer logic in
Switcher.__call__() - Add comprehensive tests (15+ test cases)
- Update documentation with examples
- Verify backward compatibility (all existing tests pass)
Phase 2: Pattern Matching (v0.7.0)
- Implement regex filtering in
Switcher.__call__() - Implement
_wildcard_dispatch()method - Add signature compatibility checking
- Add comprehensive tests (20+ test cases)
- Update documentation with use cases
Phase 3: Integration & Polish (v0.7.0)
- Performance benchmarks (overhead of auto-infer + pattern match)
- Edge case testing (complex types, Union, Optional, etc.)
- Documentation: Migration guide for existing code
- Documentation: Best practices for pattern-based APIs
Breaking Changes
None - this is a pure enhancement with 100% backward compatibility.
- Existing code without type hints: works as before
- Existing code with explicit typerule: works as before
- Pattern matching: new feature, no existing behavior changed
Open Questions
-
Union types: How to handle
Union[int, str]?- Proposal: Match either type (try int, then str)
-
Optional types: How to handle
Optional[str]?- Proposal: Match
strorNone
- Proposal: Match
-
Generic types: How to handle
List[str],Dict[str, int]?- Proposal: Match base type only (
list,dict), ignore generic params
- Proposal: Match base type only (
-
Performance: What's the overhead of auto-infer + pattern match?
- Need benchmarks before finalizing
Expected Benefits
- Less boilerplate: Eliminate redundant typerule declarations
- More Pythonic: Leverage existing type hints
- Powerful APIs: Namespace-based routing enables flexible interfaces
- Better DX: Users call logical operations (
add.*), not implementation details (add_json) - Extensible: Add new formats without changing client code
Target Version
v0.7.0 - Expected Q1 2025
References
- Python Type Hints (PEP 484)
- Python Regex (re module)
- Python singledispatch - Similar pattern for inspiration
Status: Proposal - awaiting feedback and approval
Labels: enhancement, v0.7.0