Passed
Push — master ( 781642...0cf0fb )
by Ramon
38s queued 11s
created

typish._classes.ClsFunction.understands()   A

Complexity

Conditions 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 11
Ratio 100 %

Importance

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