| 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 |