Test Failed
Push — master ( d00c81...2f33ee )
by Ramon
07:41 queued 06:02
created

typish._classes.Something.like()   A

Complexity

Conditions 1

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nop 2
dl 0
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
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 self._hash:
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 _type_repr(self, obj):
115
        """Return the repr() of an object, special-casing types (internal helper).
116
117
        If obj is a type, we return a shorter version than the default
118
        type.__repr__, based on the module and qualified name, which is
119
        typically enough to uniquely identify a type.  For everything
120
        else, we fall back on repr(obj).
121
        """
122
        if isinstance(obj, type) and not issubclass(obj, Callable):
123
            if obj.__module__ == 'builtins':
124
                return obj.__qualname__
125
            return '{}.{}'.format(obj.__module__, obj.__qualname__)
126
        if obj is ...:
127
            return '...'
128
        if isinstance(obj, types.FunctionType):
129
            return obj.__name__
130
        return repr(obj)
131
132
133
class Something(type, metaclass=_SomethingMeta):
134
    """
135
    This class allows one to define an interface for something that has some
136
    attributes, such as objects or classes or maybe even modules.
137
    """
138
    @classmethod
139
    def signature(mcs) -> Dict[str, type]:
140
        """
141
        Return the signature of this ``Something`` as a dict.
142
        :return: a dict with attribute names as keys and types as values.
143
        """
144
        result = OrderedDict()
145
        args = mcs.__args__
146
        if isinstance(mcs.__args__, slice):
147
            args = (mcs.__args__,)
148
149
        arg_keys = sorted(args)
150
        if isinstance(mcs.__args__, dict):
151
            for key in arg_keys:
152
                result[key] = mcs.__args__[key]
153
        else:
154
            for slice_ in arg_keys:
155
                result[slice_.start] = slice_.stop
156
        return result
157
158
    def __getattr__(cls, item):
159
        # This method exists solely to fool the IDE into believing that
160
        # Something can have any attribute.
161
        return type.__getattr__(cls, item)
162
163
    @staticmethod
164
    def of(obj: Any, exclude_privates: bool = True) -> 'Something':
165
        warnings.warn('Something.of is deprecated and will be removed in the '
166
                      'next minor release. Use Something.like instead.',
167
                      category=DeprecationWarning, stacklevel=2)
168
        return Something.like(obj, exclude_privates)
169
170
    @staticmethod
171
    def like(obj: Any, exclude_privates: bool = True) -> 'Something':
172
        """
173
        Return a ``Something`` for the given ``obj``.
174
        :param obj: the object of which a ``Something`` is to be made.
175
        :param exclude_privates: if ``True``, private variables are excluded.
176
        :return: a ``Something`` that corresponds to ``obj``.
177
        """
178
        signature = {attr: get_type(getattr(obj, attr)) for attr in dir(obj)
179
                     if not exclude_privates or not attr.startswith('_')}
180
        return Something[signature]
181
182
183
TypingType = Something['__origin__': type, '__args__': Tuple[type, ...]]
184