MatchTemplate.match_instance()   A
last analyzed

Complexity

Conditions 2

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 2
nop 3
1
"""Common classes and functions for implementing dynamic matchers."""
2
3
from __future__ import annotations
4
5
import functools
6
import typing
7
8
from ... import _class_placeholder
9
from ..._adt import prewritten_methods
10
from .. import destructure
11
12
T = typing.TypeVar("T")  # pylint: disable=invalid-name
13
14
Matcher = typing.Union[T, _class_placeholder.Placeholder[T]]
15
16
17
def _apply(structure: Matcher[T], base: typing.Optional[type]) -> T:
18
    if isinstance(structure, _class_placeholder.Placeholder):
19
        # The cast is safe because we do the abstract check higher up
20
        new = structure.func(typing.cast(type, base))
21
        _check_structure(new)
22
        return new
23
    non_placeholder = typing.cast(T, structure)
24
    _check_structure(non_placeholder)
25
    return non_placeholder
26
27
28
class MatchTemplate(typing.Generic[T]):
29
    """The core data type for managing dynamic matching functions."""
30
31
    def __init__(self) -> None:
32
        self._templates: typing.List[typing.Tuple[Matcher[T], typing.Callable]] = []
33
        self._abstract = False
34
        self._cache: typing.Dict[
35
            typing.Optional[type], typing.List[typing.Tuple[T, typing.Callable]]
36
        ] = {}
37
38
    def copy_into(self, other: MatchTemplate[T]) -> None:
39
        """Given another template, copy this one's contents into it."""
40
        for structure, func in self._templates:
41
            other.add_structure(structure, func)
42
43
    # @property
44
    # def abstract(self):
45
    #     return self._abstract
46
47
    def add_structure(self, structure: Matcher[T], func: typing.Callable) -> None:
48
        """Add the given structure and function to the match template."""
49
        self._templates.append((structure, func))
50
        if isinstance(structure, _class_placeholder.Placeholder):
51
            self._abstract = True
52
            self._cache.pop(None, None)
53
        for base, structures in self._cache.items():
54
            structures.append((_apply(structure, base), func))
55
56
    def _get_matchers(
57
        self, base: type
58
    ) -> typing.List[typing.Tuple[T, typing.Callable]]:
59
        if base in self._cache:
60
            return self._cache[base]
61
        return self._cache.setdefault(
62
            base,
63
            [(_apply(structure, base), func) for (structure, func) in self._templates],
64
        )
65
66
    def match_instance(self, matchable, instance) -> typing.Iterator[typing.Callable]:
67
        """Get the base associated with instance, if any, and match with it."""
68
        base = prewritten_methods.sum_base(instance) if self._abstract else None
69
        yield from self.match(matchable, base)
70
71
    def match(self, matchable, base) -> typing.Iterator[typing.Callable]:
72
        """If there is a match in the context of base, yield implementation."""
73
        if base is None and self._abstract:
74
            raise ValueError
75
        for structure, func in self._get_matchers(base):
76
            if matchable(structure):
77
                break
78
        else:
79
            return
80
        # https://github.com/PyCQA/pylint/issues/1175
81
        yield func  # pylint: disable=undefined-loop-variable
82
83
84
def _check_structure(structure) -> None:
85
    destructure.names(structure)  # Raise ValueError if there are duplicates
86
87
88
def decorate(matchers: MatchTemplate[T], structure: Matcher[T]):
89
    """Create a function decorator using the given structure, MatchTemplate."""
90
91
    def decorator(func: typing.Callable) -> typing.Callable:
92
        matchers.add_structure(structure, func)
93
        return func
94
95
    return decorator
96
97
98
class Descriptor:
99
    """Base class for decorator classes."""
100
101
    __wrapped__: typing.Optional[typing.Callable] = None
102
    __name__: typing.Optional[str] = None
103
104
    def __new__(cls, func: typing.Optional[typing.Callable]) -> Descriptor:
105
        new = super().__new__(cls)
106
        new.__doc__ = None
107
        if func is None:
108
            return new
109
        return typing.cast(Descriptor, functools.wraps(func)(new))
110
111
    def __set_name__(self, owner: type, name: str) -> None:
112
        vars(self).setdefault("__name__", name)
113
114
115
SENTINEL = object()
116
117
118
def owns(descriptor: Descriptor, owner: type) -> bool:
119
    """Return whether the given class owns the given descriptor."""
120
    name = descriptor.__name__
121
    if name is None:
122
        return False
123
    return vars(owner).get(name, SENTINEL) is descriptor
124