Skip to main content

Runtime dispatch with decorator-based overload registration.

Project description

Tree Interval Logo

WizeDispatcher

A lightweight, version-robust Python runtime dispatch library with powerful overload registration and type-based selection

Try it on Replit

1. ✨ Features

  • 🎯 Overload Registration via Decorators
    Register multiple implementations for the same function, method, or property setter, with keyword or positional type constraints.

  • 📝 Type Hint Overrides
    Overloads can specify types in the decorator to override or partially use type hints from the function signature.

  • Partial Type Specification
    Missing type constraints in an overload are automatically filled from the fallback/default implementation.

  • 📊 Weighted Specificity Scoring
    Runtime match scoring system is heuristic-based and typing-aware (see Weight-Based Evaluation).

  • 🛠 Full Typing Support
    Union, Optional, Literal, generic containers (list[int], tuple[int, ...]), and callable detection.

  • 📦 Method & Property Support
    Works with instance methods, @classmethod, @staticmethod, and property setters.

  • 🚀 Fast Cached Dispatch
    Caches previous matches to speed up repeated calls.

  • 🧩 Varargs & Kwargs Handling
    Fully supports *args and **kwargs in overloads, resolving them according to parameter order.

  • 🐍 Version Robust
    Works consistently across Python 3.8+ with no dependencies.

2. 🚀 Quick Start

2.1 Basic Usage Example

from wizedispatcher import dispatch

# Fallback
def greet(name: object) -> str:
    return f"Hello, {name}!"

# Keyword constraint
@dispatch.greet(name=str)
def _(name: str) -> str:
    return f"Hello, {name}, nice to meet you."

# Positional constraint
@dispatch.greet(str, int)
def _(name, age) -> str:
    return f"{name} is {age} years old"

print(greet("Alice"))   # Hello, Alice, nice to meet you.
print(greet("Bob", 30)) # Bob is 30 years old

3. 📊 Weight-Based Evaluation

3.1 Matching and Scoring Overview

WizeDispatcher first filters overloads by type compatibility and then scores the remaining candidates to pick the most specific one.

3.1.1 Compatibility filter

For each parameter in dispatch order, the runtime value must match the overload’s effective hint. Matching supports:

  • Union | PEP 604, Optional, Literal, Annotated, ClassVar
  • Type[T] / type, protocols (runtime), TypedDict-like classes
  • Callables with parameter shapes, containers (list/tuple/dict/set/...)
  • TypeVar / ParamSpec (constraints/bounds respected)

Overload defaults participate in matching:
If an overload defines a default for a parameter and the caller omitted it, the default value is used as the value to match/score for that parameter.

3.1.2 Scoring the compatible candidates

For each parameter, we compute:

score += specificity(value, hint)
score += (40 if hint is not Any/object/WILDCARD else 20)

Then we apply a small penalty if the overload uses *args:

score -= 2  # has VAR_POSITIONAL

Finally, the overload with the highest total score wins. If multiple overloads tie, the one registered first remains selected (deterministic). If no overload is compatible, the original (fallback) is called.

Specificity highlights (per-parameter)

Below is a compact view of the core heuristic used by _type_specificity_score(value, hint):

Hint shape Specificity (approx)
Literal[...] 100
Annotated[T, ...] 1 + specificity(value, T)
ClassVar[T] specificity(value, T)
Union[T1, T2, ...] max(specificity(...)) - len(Union)
Type[T] / type[T] 15 + specificity(value, T)
Bare Type / type 8
Callable[[args...], ...] 12 + Σ specificity(arg_i)
Mapping[K, V] / dict[K, V] 20 + specificity(K) + specificity(V)
Sequence[T] / iterables 16 (unparam) or 18 + spec(T)
Concrete container w/ params 20 + Σ specificity(param_i)
Unparameterized Tuple/List/Dict 10
Concrete class C 5 + max(0, 50 - mro_distance(value, C))
Any, object, or WILDCARD 0

Note: The extra +40 / +20 bonus per param encourages overloads that declare types (even loosely) over ones that leave things unconstrained.

3.2 Example (why one wins)

# Fallback
def greet(name: object) -> str: ...

@dispatch.greet(name=str)          # declares a concrete type for 'name'
def _(name: str) -> str: ...

@dispatch.greet(Any)               # explicitly Any
def _(name) -> str: ...

A call greet("Alice"):

  • name=str overload:
    • Specificity for str with value "Alice": high (concrete class match)
    • +40 bonus for a concrete (non-Any) hint
  • name=Any overload:
    • Specificity: 0
    • +20 bonus (declared but Any)

→ The name=str overload’s total is higher, so it wins.

3.3 Caching

Selections are cached by the tuple of runtime parameter types (in dispatch order) for fast repeat calls.

4. 📐 Type Resolution Precedence

4.1 Precedence Rules Overview

WizeDispatcher determines the effective type for each parameter using a clear, three-tier precedence. This governs what is matched and scored.

  1. Decorator overrides function annotations

    • @dispatch.func(a=int) means: for parameter a, use int even if the overload function annotates something else (e.g., a: str).
    • Positional decorator args map by parameter order: @dispatch.func(int, str) → first param int, second str.
  2. If the decorator omits a param, use the overload function annotation

    • Example: overload is def _(a: str, b: bytes) -> ... and decorator is @dispatch.func(a=int). Effective types → a=int (override), b=bytes (from function).
  3. If both decorator and overload omit a param, fall back to the default

    • The default (original) function annotations fill any remaining gaps.
    • If the default is also missing an annotation, that param becomes a wildcard (matches anything) and scores accordingly.

3.1.4 TL;DR Summary

Decorator > Overload function annotations > Default function annotations > Wildcard


4.2 Case 1 — Bare decorator uses overload annotations — Bare decorator: use overload function annotations

from wizedispatcher import dispatch

# Default (fallback) function
def process(a: int, b: str, c: float) -> str:
    return f"default: a={a!r}, b={b!r}, c={c!r}"

# Bare decorator → takes annotations from the overload itself
@dispatch.process
def _(a: int, b: bytes, c: float) -> str:
    return f"overload1: b_is={type(b).__name__}"

print(process(1, b"hi", 2.0))  # ✅ matches overload (b: bytes)
print(process(1, "hi", 2.0))   # ➜ falls back (b is str, not bytes)

Why: No decorator args were provided, so the overload’s own annotations (b: bytes) are the effective constraint for matching.


4.3 Case 2 — Decorator overrides overload annotations — Decorator overrides overload annotations

from wizedispatcher import dispatch

def process(a: int, b: str, c: float) -> str:
    return "default"

# Decorator forces a=str, overriding the overload's (a: int)
@dispatch.process(a=str)
def _(a: int, b: bytes, c: float) -> str:
    return "overload2"

print(process("x", b"y", 1.0))  # ✅ matches overload (a must be str)
print(process(1, b"y", 1.0))    # ➜ fallback (a is int, but decorator requires str)

Positional decorator example (maps by parameter order):

from wizedispatcher import dispatch

def process(a: int, b: str, c: float) -> str:
    return "default"

# Positional mapping → a=str, b=bytes, c=float
@dispatch.process(str, bytes, float)
def _(a, b, c) -> str:
    return "overload3"

print(process("x", b"y", 1.0))  # ✅ matches overload3
print(process("x", "y", 1.0))   # ➜ fallback (b is str, expected bytes)

Why: When decorator arguments exist, they override the overload’s annotations for the covered parameters.


4.4 Case 3 — Missing on both decorator and overload → use default — Missing on both decorator and overload → use default

from wizedispatcher import dispatch

# Default provides types for all params
def process(a: int, b: str, c: float) -> str:
    return "default"

# Decorator sets only 'a', overload omits annotation for 'b'
@dispatch.process(a=str)       # no info for 'b' here
def _(a: int, b, c: float) -> str:  # no type for 'b' here either
    return "overload4"

print(process("x", "hello", 1.0))  # ✅ matches overload4
#   effective types: a=str (decorator), b=str (from default), c=float (overload)

print(process("x", 123, 1.0))      # ➜ fallback
#   'b' is int — default says 'b: str', so overload4 is incompatible

Wildcard note: If the default also lacks an annotation for a parameter, that parameter becomes a wildcard (matches anything but is scored as such).

5. 🧩 Partial Type Specification

# Default function defines all parameters
def process(a: int, b: str, c: float) -> str:
    return "default"

# Overload defines only 'a', inherits 'b' and 'c' types from default
@dispatch.process(a=str)
def _(a: str, b, c) -> str:
    return f"a is str, b is {type(b)}, c is {type(c)}"

6. 🛠 Methods & Properties

class Converter:
    @property
    def value(self) -> int:
        return self._value

    @value.setter
    def value(self, val: object) -> None:
        self._value = val  # fallback setter

    @dispatch.value(value=int)
    def _(self, value: int) -> None:
        self._value = value * 10

    @dispatch.value(value=str)
    def _(self, value: str) -> None:
        self._value = int(value)

c = Converter()
c.value = 3
print(c.value)  # 30
c.value = "7"
print(c.value)  # 7

7. 📦 Installation

pip install wizedispatcher

8. 📚 Documentation

  • Wiki: Complete documentation in /wizedispatcher_wiki
  • Examples: Ready-to-run demos in /demo

9. 📝 License

This project is licensed under the MIT License - see the LICENSE file.

🎯 How Type Constraints Are Determined

When deciding which types to use for overload matching, WizeDispatcher follows a strict precedence order. This allows you to be as explicit or as implicit as you like when defining overloads.

3.1.1 No decorator arguments

@dispatch.func
def _(a: int, b: str) -> None:
    ...

If the decorator has no arguments, the type hints are taken directly from the overload function’s own signature.

3.1.2 Decorator with arguments

@dispatch.func(a=str)
def _(a: int, b: str) -> None:
    ...

If the decorator has arguments, those override the type hints for the specified parameters, ignoring the overload function's own hints for those parameters.

3.1.3 Missing arguments in both decorator and overload

# Default (fallback) function defines all parameters
def func(a: int, b: str) -> None:
    ...

# Overload defines only 'a' in the decorator, leaves 'b' undefined
@dispatch.func(a=str)
def _(a, b) -> None:
    ...

If a parameter is missing from both the decorator arguments and the overload function’s type hints, WizeDispatcher uses the type hint from the default (fallback) function.

Summary Table

Source Priority
Decorator arguments Highest
Overload function's type hints Medium
Default function's type hints Lowest

This precedence ensures that you can:

  • Override only what you need without redefining all types.
  • Inherit defaults from the fallback function.
  • Use explicit decorator arguments when you want to fully control matching.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wizedispatcher-0.1.4.tar.gz (30.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wizedispatcher-0.1.4-py3-none-any.whl (19.3 kB view details)

Uploaded Python 3

File details

Details for the file wizedispatcher-0.1.4.tar.gz.

File metadata

  • Download URL: wizedispatcher-0.1.4.tar.gz
  • Upload date:
  • Size: 30.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.11.13

File hashes

Hashes for wizedispatcher-0.1.4.tar.gz
Algorithm Hash digest
SHA256 3e52dd10902b3579689e24930babade257dada9c70cad2247da37139a11b094b
MD5 6569ed872828f091c429bbf6cd9431c2
BLAKE2b-256 7f0e2e610db5f72d5a3d05849a347dca6375cc2a7603285b24fc95ea0b10c65e

See more details on using hashes here.

File details

Details for the file wizedispatcher-0.1.4-py3-none-any.whl.

File metadata

  • Download URL: wizedispatcher-0.1.4-py3-none-any.whl
  • Upload date:
  • Size: 19.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.11.13

File hashes

Hashes for wizedispatcher-0.1.4-py3-none-any.whl
Algorithm Hash digest
SHA256 66017347cc10afa5c5566076b3a1a5021d71a1c87435d9f7e6b30e5fbe237afb
MD5 1b3b10d6d19285993f0fd1add6397f7d
BLAKE2b-256 52ef59deec5b6e1a4578eb0c63dd61ed49855e1821215a7663af6e5b7848e43d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page