Test Failed
Push — master ( 2f33ee...d68237 )
by Ramon
01:23
created

typish._classes._SomethingMeta.__hash__()   A

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nop 1
dl 0
loc 3
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
import warnings
8
from collections import OrderedDict
9
from typing import Any, Callable, Dict, Tuple
10
11
from typish._functions import (
12
    get_type,
13
    subclass_of,
14
    instance_of,
15
    get_args_and_return_type,
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
class SubscriptableType(type):
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
class _SomethingMeta(SubscriptableType):
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
class Something(type, metaclass=_SomethingMeta):
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 of(obj: Any, exclude_privates: bool = True) -> 'Something':
169
        warnings.warn('Something.of is deprecated and will be removed in the '
170
                      'next minor release. Use Something.like instead.',
171
                      category=DeprecationWarning, stacklevel=2)
172
        return Something.like(obj, exclude_privates)
173
174
    @staticmethod
175
    def like(obj: Any, exclude_privates: bool = True) -> 'Something':
176
        """
177
        Return a ``Something`` for the given ``obj``.
178
        :param obj: the object of which a ``Something`` is to be made.
179
        :param exclude_privates: if ``True``, private variables are excluded.
180
        :return: a ``Something`` that corresponds to ``obj``.
181
        """
182
        signature = {attr: get_type(getattr(obj, attr)) for attr in dir(obj)
183
                     if not exclude_privates or not attr.startswith('_')}
184
        return Something[signature]
185
186
187
TypingType = Something['__origin__': type, '__args__': Tuple[type, ...]]
188