diff --git a/peps/pep-8108.rst b/peps/pep-8108.rst new file mode 100644 index 00000000000..6d7c24e85fc --- /dev/null +++ b/peps/pep-8108.rst @@ -0,0 +1,220 @@ +PEP: 8108 +Title: Adding coroutinedispatch to functools for Method Overloading +Author: Ricardo Robles +Status: Draft +Type: Standards Track +Created: 16-Nov-2025 + +Abstract +======== + +This PEP proposes the addition of a ``coroutinedispatch`` class to the ``functools`` module in Python's standard library. The ``coroutinedispatch`` class enables method overloading based on argument types, supporting both synchronous and asynchronous methods. This feature simplifies the implementation of polymorphic behavior in Python, making it easier to write clean and maintainable code. + +Motivation +========== + +Python currently lacks built-in support for method overloading based on argument types. While developers can achieve similar functionality using conditional statements or external libraries, these approaches often result in verbose and less readable code. Adding a ``coroutinedispatch`` class to the standard library would: + +- Provide a standardized and Pythonic way to implement method overloading. +- Enhance code readability and maintainability. +- Support both synchronous and asynchronous methods, addressing a common limitation in existing third-party solutions. + +Rationale +========= + +The ``coroutinedispatch`` class leverages Python's type hints and introspection capabilities to match method implementations based on argument types. This design aligns with Python's dynamic nature while encouraging the use of type annotations. By integrating this functionality into the ``functools`` module, we ensure consistency with other utility features for functional programming. + +Specification +============= + +The ``coroutinedispatch`` class will be implemented as follows: + +- **Registration of Methods**: Developers can register multiple implementations of a method using the ``@coroutinedispatch`` decorator. The decorator inspects the argument types of the decorated function and stores the implementation accordingly. +- **Type Matching**: When the method is called, the ``coroutinedispatch`` class matches the provided arguments against the registered implementations. If no exact match is found, a ``TypeError`` is raised. +- **Support for Async Methods**: The ``coroutinedispatch`` class distinguishes between synchronous and asynchronous contexts, allowing developers to define separate implementations for each. + +Example Usage +------------- + +.. code-block:: python + + import asyncio + from functools import coroutinedispatch + + class Example: + @coroutinedispatch + def process(self, x: int) -> str: + return f"Processing integer: {x}" + + @process.register + async def _(self, x: int) -> str: + await asyncio.sleep(0.1) + return f"Processing integer asynchronously: {x}" + + @process.register + def _(self, x: str) -> str: + return f"Processing string: {x}" + + @process.register + async def _(self, x: str) -> str: + await asyncio.sleep(0.1) + return f"Processing string asynchronously: {x}" + + example = Example() + print(example.process(42)) # Processing integer: 42 + print(example.process("hello")) # Processing string: hello + + async def main(): + print(await example.process(42)) # Processing integer asynchronously: 42 + print(await example.process("hello")) # Processing string asynchronously: hello + + asyncio.run(main()) + +Backwards Compatibility +======================= + +This proposal introduces a new class to the ``functools`` module and does not modify any existing functionality. As such, it is fully backwards compatible. + +Security Implications +===================== + +The ``coroutinedispatch`` class relies on Python's type hints and introspection capabilities. It does not introduce any new security risks beyond those inherent to dynamic type checking. + +How to Teach This +================= + +The ``coroutinedispatch`` class should be documented in the Python standard library reference, with examples demonstrating its usage. Tutorials and guides on functional programming in Python can include sections on ``coroutinedispatch`` to showcase its benefits. + +Reference Implementation +========================= + +A reference implementation of the ``coroutinedispatch`` class is provided below: + +.. code-block:: python + + import inspect + import asyncio + from typing import Any, Callable, get_type_hints, get_origin + from functools import lru_cache + + + class coroutinedispatch: + def __init__(self, func: Callable): + self._sync_methods = {} + self._async_methods = {} + self._name = func.__name__ + + arg_types = self.get_arg_types(func) + + if inspect.iscoroutinefunction(func): + self._async_methods[arg_types] = func + else: + self._sync_methods[arg_types] = func + + def get_arg_types(self, func: Callable) -> tuple: + try: + hints = get_type_hints(func) + sig = inspect.signature(func) + params = list(sig.parameters.values()) + + if params and params[0].name in ("self", "cls"): + params = params[1:] + + return tuple(hints.get(p.name, Any) for p in params) + except Exception: + return () + + @lru_cache(maxsize=128) + def match_types(self, provided_args: tuple, expected_types: tuple) -> bool: + if len(provided_args) != len(expected_types): + return False + + for arg, expected in zip(provided_args, expected_types): + if expected is Any: + continue + + origin = get_origin(expected) + if origin is not None: + expected = origin + + if not isinstance(arg, expected): + return False + + return True + + @lru_cache(maxsize=128) + def _find_matching_method(self, is_async: bool, *args: Any) -> Callable: + """Find the method that matches the argument types.""" + methods = self._async_methods if is_async else self._sync_methods + + # Search for exact match + for arg_types, method in methods.items(): + if self.match_types(args, arg_types): + return method + + # If no match, raise descriptive error + arg_type_names = tuple(type(arg).__name__ for arg in args) + context = "async" if is_async else "sync" + available = list(methods.keys()) + + raise TypeError( + f"No matching {context} method '{self._name}' found for " + f"arguments: {arg_type_names}. Available: {available}" + ) + + def register(self, func: Callable) -> Callable: + """Register a new method overload.""" + arg_types = self.get_arg_types(func) + + if inspect.iscoroutinefunction(func): + self._async_methods[arg_types] = func + else: + self._sync_methods[arg_types] = func + + return self + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + try: + asyncio.get_event_loop() + is_async_context = True + except RuntimeError: + is_async_context = False + pass + + # Exclude 'self' from arguments if present + check_args = args[1:] if args and hasattr(args[0], self._name) else args + + try: + method = self._find_matching_method(is_async_context, *check_args) + result = method(*args, **kwargs) + + return result + except TypeError: + # If no async/sync method, try the other + try: + is_async_context = not is_async_context + method = self._find_matching_method(is_async_context, *check_args) + result = method(*args, **kwargs) + + return result + except TypeError: + raise + + def __get__(self, obj, objtype=None): + """Support for bound methods""" + if obj is None: + return self + + import functools + + return functools.partial(self.__call__, obj) + +Acknowledgements +================ + +Thanks to the Python community for their feedback and contributions to this proposal. + +Copyright +========= + +This document has been placed in the public domain.