File: multimethod.py

package info (click to toggle)
python-generic 1.1.6-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 360 kB
  • sloc: python: 879; makefile: 126; sh: 2
file content (111 lines) | stat: -rw-r--r-- 3,545 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
"""Multi-method builds on the functionality provided by `multidispatch` to
provide generic methods."""

from __future__ import annotations

import functools
import inspect
import logging
import threading
import types
from typing import Any, Callable, TypeVar, Union, cast

from generic.multidispatch import FunctionDispatcher, KeyType

__all__ = ("multimethod", "has_multimethods")

C = TypeVar("C")
T = TypeVar("T", bound=Union[Callable[..., Any], type])

logger = logging.getLogger(__name__)


def multimethod(*argtypes: KeyType) -> Callable[[T], MethodDispatcher[T]]:
    """Declare method as multimethod.

    This decorator works exactly the same as :func:`.multidispatch` decorator
    but replaces decorated method with :class:`.MethodDispatcher` object
    instead.

    Should be used only for decorating methods and enclosing class should have
    :func:`.has_multimethods` decorator.
    """

    def _replace_with_dispatcher(func):
        nonlocal argtypes
        argspec = inspect.getfullargspec(func)

        dispatcher = cast(
            MethodDispatcher,
            functools.update_wrapper(
                MethodDispatcher(argspec, len(argtypes) + 1), func
            ),
        )
        dispatcher.register_unbound_rule(func, *argtypes)
        return dispatcher

    return _replace_with_dispatcher


def has_multimethods(cls: type[C]) -> type[C]:
    """Declare class as one that have multimethods.

    Should only be used for decorating classes which have methods decorated with
    :func:`.multimethod` decorator.
    """
    for _name, obj in cls.__dict__.items():
        if isinstance(obj, MethodDispatcher):
            obj.proceed_unbound_rules(cls)
    return cls


class MethodDispatcher(FunctionDispatcher[T]):
    """Multiple dispatch for methods.

    This object dispatch call to method by its class and arguments types.
    Usually it is produced by :func:`.multimethod` decorator.

    You should not manually create objects of this type.
    """

    def __init__(self, argspec: inspect.FullArgSpec, params_arity: int) -> None:
        super().__init__(argspec, params_arity)

        # some data, that should be local to thread of execution
        self.local = threading.local()
        self.local.unbound_rules = []

    def register_unbound_rule(self, func, *argtypes) -> None:
        """Register unbound rule that should be processed by
        ``proceed_unbound_rules`` later."""
        self.local.unbound_rules.append((argtypes, func))

    def proceed_unbound_rules(self, cls) -> None:
        """Process all unbound rule by binding them to ``cls`` type."""
        for argtypes, func in self.local.unbound_rules:
            argtypes = (cls,) + argtypes
            logger.debug("register rule %s", argtypes)
            self.register_rule(func, *argtypes)
        self.local.unbound_rules = []

    def __get__(self, obj, cls):
        return self if obj is None else types.MethodType(self, obj)

    def register(self, *argtypes: KeyType) -> Callable[[T], T]:
        """Register new case for multimethod for ``argtypes``"""

        def make_declaration(meth):
            self.register_unbound_rule(meth, *argtypes)
            return self

        return make_declaration

    @property
    def otherwise(self) -> Callable[[T], T]:
        """Decorator which registers "catch-all" case for multimethod."""

        def make_declaration(meth):
            self.register_unbound_rule(meth, *([object] * (self.params_arity - 1)))
            return self

        return make_declaration