Total Complexity | 55 |
Total Lines | 292 |
Duplicated Lines | 83.56 % |
Changes | 0 |
Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like typish._classes often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
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): |
|
|
|||
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): |
|
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): |
|
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): |
|
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: |
|
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): |
|
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 |