Passed
Pull Request — master (#12)
by Ramon
01:35
created

typish._classes.ClsFunction.__call__()   A

Complexity

Conditions 3

Size

Total Lines 10
Code Lines 9

Duplication

Lines 10
Ratio 100 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
nop 3
dl 10
loc 10
rs 9.95
c 0
b 0
f 0
1
"""
2
PRIVATE MODULE: do not import (from) it directly.
3
4
This module contains class implementations.
5
"""
6
import inspect
7
import types
8
from collections import OrderedDict
9
from typing import Any, Callable, Dict, Tuple, Optional, Union, List, Iterable
10
11
from typish._types import Empty
12
from typish._functions import (
13
    get_type,
14
    subclass_of,
15
    instance_of,
16
    get_args_and_return_type,
17
    is_type_annotation,
18
)
19
20
21
class _SubscribedType(type):
22
    """
23
    This class is a placeholder to let the IDE know the attributes of the
24
    returned type after a __getitem__.
25
    """
26
    __origin__ = None
27
    __args__ = None
28
29
30 View Code Duplication
class SubscriptableType(type):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
31
    """
32
    This metaclass will allow a type to become subscriptable.
33
34
    >>> class SomeType(metaclass=SubscriptableType):
35
    ...     pass
36
    >>> SomeTypeSub = SomeType['some args']
37
    >>> SomeTypeSub.__args__
38
    'some args'
39
    >>> SomeTypeSub.__origin__.__name__
40
    'SomeType'
41
    """
42
    def __init_subclass__(mcs, **kwargs):
43
        mcs._hash = None
44
        mcs.__args__ = None
45
        mcs.__origin__ = None
46
47
    def __getitem__(self, item) -> _SubscribedType:
48
        body = {
49
            **self.__dict__,
50
            '__args__': item,
51
            '__origin__': self,
52
        }
53
        bases = self, *self.__bases__
54
        result = type(self.__name__, bases, body)
55
        if hasattr(result, '_after_subscription'):
56
            # TODO check if _after_subscription is static
57
            result._after_subscription(item)
58
        return result
59
60
    def __eq__(self, other):
61
        self_args = getattr(self, '__args__', None)
62
        self_origin = getattr(self, '__origin__', None)
63
        other_args = getattr(other, '__args__', None)
64
        other_origin = getattr(other, '__origin__', None)
65
        return self_args == other_args and self_origin == other_origin
66
67
    def __hash__(self):
68
        if not getattr(self, '_hash', None):
69
            self._hash = hash('{}{}'.format(self.__origin__, self.__args__))
70
        return self._hash
71
72
73 View Code Duplication
class _SomethingMeta(SubscriptableType):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
74
    """
75
    This metaclass is coupled to ``Interface``.
76
    """
77
    def __instancecheck__(self, instance: object) -> bool:
78
        # Check if all attributes from self.signature are also present in
79
        # instance and also check that their types correspond.
80
        sig = self.signature()
81
        for key in sig:
82
            attr = getattr(instance, key, None)
83
            if not attr or not instance_of(attr, sig[key]):
84
                return False
85
        return True
86
87
    def __subclasscheck__(self, subclass: type) -> bool:
88
        # If an instance of type subclass is an instance of self, then subclass
89
        # is a sub class of self.
90
        self_sig = self.signature()
91
        other_sig = Something.like(subclass).signature()
92
        for attr in self_sig:
93
            if attr in other_sig:
94
                attr_sig = other_sig[attr]
95
                if (not isinstance(subclass.__dict__[attr], staticmethod)
96
                        and not isinstance(subclass.__dict__[attr], classmethod)
97
                        and subclass_of(attr_sig, Callable)):
98
                    # The attr must be a regular method or class method, so the
99
                    # first parameter should be ignored.
100
                    args, rt = get_args_and_return_type(attr_sig)
101
                    attr_sig = Callable[list(args[1:]), rt]
102
                if not subclass_of(attr_sig, self_sig[attr]):
103
                    return False
104
        return True
105
106
    def __eq__(self, other: 'Something') -> bool:
107
        return (isinstance(other, _SomethingMeta)
108
                and self.signature() == other.signature())
109
110
    def __repr__(self):
111
        sig = self.signature()
112
        sig_ = ', '.join(["'{}': {}".format(k, self._type_repr(sig[k]))
113
                          for k in sig])
114
        return 'typish.Something[{}]'.format(sig_)
115
116
    def __hash__(self):
117
        # This explicit super call is required for Python 3.5 and 3.6.
118
        return super.__hash__(self)
119
120
    def _type_repr(self, obj):
121
        """Return the repr() of an object, special-casing types (internal helper).
122
123
        If obj is a type, we return a shorter version than the default
124
        type.__repr__, based on the module and qualified name, which is
125
        typically enough to uniquely identify a type.  For everything
126
        else, we fall back on repr(obj).
127
        """
128
        if isinstance(obj, type) and not issubclass(obj, Callable):
129
            if obj.__module__ == 'builtins':
130
                return obj.__qualname__
131
            return '{}.{}'.format(obj.__module__, obj.__qualname__)
132
        if obj is ...:
133
            return '...'
134
        if isinstance(obj, types.FunctionType):
135
            return obj.__name__
136
        return repr(obj)
137
138
139 View Code Duplication
class Something(type, metaclass=_SomethingMeta):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
140
    """
141
    This class allows one to define an interface for something that has some
142
    attributes, such as objects or classes or maybe even modules.
143
    """
144
    @classmethod
145
    def signature(mcs) -> Dict[str, type]:
146
        """
147
        Return the signature of this ``Something`` as a dict.
148
        :return: a dict with attribute names as keys and types as values.
149
        """
150
        result = OrderedDict()
151
        args = mcs.__args__
152
        if isinstance(mcs.__args__, slice):
153
            args = (mcs.__args__,)
154
155
        arg_keys = sorted(args)
156
        if isinstance(mcs.__args__, dict):
157
            for key in arg_keys:
158
                result[key] = mcs.__args__[key]
159
        else:
160
            for slice_ in arg_keys:
161
                result[slice_.start] = slice_.stop
162
        return result
163
164
    def __getattr__(cls, item):
165
        # This method exists solely to fool the IDE into believing that
166
        # Something can have any attribute.
167
        return type.__getattr__(cls, item)
168
169
    @staticmethod
170
    def like(obj: Any, exclude_privates: bool = True) -> 'Something':
171
        """
172
        Return a ``Something`` for the given ``obj``.
173
        :param obj: the object of which a ``Something`` is to be made.
174
        :param exclude_privates: if ``True``, private variables are excluded.
175
        :return: a ``Something`` that corresponds to ``obj``.
176
        """
177
        signature = {attr: get_type(getattr(obj, attr)) for attr in dir(obj)
178
                     if not exclude_privates or not attr.startswith('_')}
179
        return Something[signature]
180
181
182 View Code Duplication
class ClsDict(OrderedDict):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
183
    """
184
    ClsDict is a dict that accepts (only) types as keys and will return its
185
    values depending on instance checks rather than equality checks.
186
    """
187
    def __new__(cls, *args, **kwargs):
188
        """
189
        Construct a new instance of ``ClsDict``.
190
        :param args: a dict.
191
        :param kwargs: any kwargs that ``dict`` accepts.
192
        :return: a ``ClsDict``.
193
        """
194
        if len(args) > 1:
195
            raise TypeError('TypeDict accepts only one positional argument, '
196
                            'which must be a dict.')
197
        if args and not isinstance(args[0], dict):
198
            raise TypeError('TypeDict accepts only a dict as positional '
199
                            'argument.')
200
        if not all([is_type_annotation(key) for key in args[0]]):
201
            raise TypeError('The given dict must only hold types as keys.')
202
        return super().__new__(cls, args[0], **kwargs)
203
204
    def __getitem__(self, item: Any) -> Any:
205
        """
206
        Return the value of the first encounter of a key for which
207
        ``is_instance(item, key)`` holds ``True``.
208
        :param item: any item.
209
        :return: the value of which the type corresponds with item.
210
        """
211
        item_type = get_type(item, use_union=True)
212
        for key, value in self.items():
213
            if subclass_of(item_type, key):
214
                return value
215
        raise KeyError('No match for {}'.format(item))
216
217
    def get(self, item: Any, default: Any = None) -> Optional[Any]:
218
        try:
219
            return self.__getitem__(item)
220
        except KeyError:
221
            return default
222
223
224 View Code Duplication
class ClsFunction:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
225
    """
226
    ClsDict is a callable that takes a ClsDict or a dict. When called, it uses
227
    the first argument to check for the right function in its body, executes it
228
    and returns the result.
229
    """
230
    def __init__(self, body: Union[ClsDict, dict, Iterable[Callable]]):
231
        if isinstance(body, ClsDict):
232
            self.body = body
233
        elif isinstance(body, dict):
234
            self.body = ClsDict(body)
235
        elif instance_of(body, Iterable[Callable]):
236
            list_of_tuples = []
237
            for func in body:
238
                signature = inspect.signature(func)
239
                params = list(signature.parameters.keys())
240
                if not params:
241
                    raise TypeError('ClsFunction expects callables that take '
242
                                    'at least one parameter, {} does not.'
243
                                    .format(func.__name__))
244
                first_param = signature.parameters[params[0]]
245
                hint = first_param.annotation
246
                key = Any if hint == Empty else hint
247
                list_of_tuples.append((key, func))
248
            self.body = ClsDict(OrderedDict(list_of_tuples))
249
        else:
250
            raise TypeError('ClsFunction expects a ClsDict or a dict that can '
251
                            'be turned to a ClsDict or an iterable of '
252
                            'callables.')
253
254
        if not all(isinstance(value, Callable) for value in self.body.values()):
255
            raise TypeError('ClsFunction expects a dict or ClsDict with only '
256
                            'callables as values.')
257
258
    def understands(self, item: Any) -> bool:
259
        """
260
        Check to see if this ClsFunction can take item.
261
        :param item: the item that is checked.
262
        :return: True if this ClsFunction can take item.
263
        """
264
        try:
265
            self.body[item]
266
            return True
267
        except KeyError:
268
            return False
269
270
    def __call__(self, *args, **kwargs):
271
        if not args:
272
            raise TypeError('ClsFunction must be called with at least 1 '
273
                            'positional argument.')
274
        callable_ = self.body[args[0]]
275
        try:
276
            return callable_(*args, **kwargs)
277
        except TypeError as err:
278
            raise TypeError('Unable to call function for \'{}\': {}'
279
                            .format(args[0], err.args[0]))
280
281
282 View Code Duplication
class _LiteralMeta(SubscriptableType):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
283
    """
284
    A Metaclass that exists to serve Literal and alter the __args__ attribute.
285
    """
286
    def __getattribute__(cls, item):
287
        """
288
        This method makes sure that __args__ is a tuple, like with
289
        typing.Literal.
290
        :param item: the name of the attribute that is obtained.
291
        :return: the attribute.
292
        """
293
        if item == '__args__':
294
            try:
295
                result = SubscriptableType.__getattribute__(cls, item),
296
            except AttributeError:
297
                # In case of Python 3.5
298
                result = tuple()
299
        elif item == '__origin__':
300
            result = 'Literal'
301
        else:
302
            result = SubscriptableType.__getattribute__(cls, item)
303
        return result
304
305
    def __instancecheck__(self, instance):
306
        return self.__args__ and self.__args__[0] == instance
307
308
309
class Literal(metaclass=_LiteralMeta):
310
    """
311
    This is a backwards compatible variant of typing.Literal (Python 3.8+).
312
    """
313
    _name = 'Literal'
314
315
316
TypingType = Something['__origin__': type, '__args__': Tuple[type, ...]]
317