CreateException._find_inner_exception()   F
last analyzed

Complexity

Conditions 13

Size

Total Lines 42

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 161.7274

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 42
ccs 1
cts 24
cp 0.0417
rs 2.7716
cc 13
crap 161.7274

How to fix   Complexity   

Complexity

Complex classes like CreateException._find_inner_exception() 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 1
from plugin.core.constants import PLUGIN_VERSION_BASE, PLUGIN_VERSION_BRANCH, PMS_PATH
2 1
from plugin.core.exceptions import ConnectionError
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in ConnectionError.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
3 1
from plugin.core.helpers.error import ErrorHasher
4 1
from plugin.managers.core.base import Manager, Create
5 1
from plugin.managers.message import MessageManager
6 1
from plugin.models.exception import Exception
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in Exception.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
7 1
from plugin.models.message import Message
8
9 1
from datetime import datetime
10 1
from requests import exceptions as requests_exceptions
11 1
from requests.packages.urllib3 import exceptions as urllib3_exceptions
12 1
from six.moves.urllib.parse import urlparse
13 1
import logging
14 1
import os
15 1
import re
16 1
import socket
17 1
import ssl
0 ignored issues
show
Unused Code introduced by
The import ssl seems to be unused.
Loading history...
18 1
import sys
19 1
import trakt
20
21 1
VERSION_BASE = '.'.join([str(x) for x in PLUGIN_VERSION_BASE])
22 1
VERSION_BRANCH = PLUGIN_VERSION_BRANCH
23
24 1
RE_TRACEBACK = re.compile(r"\w+ \(most recent call last\)\:\n(?P<traceback>(?:.*?\n)*)(?P<type>\w+)\: (?P<message>.*?)(?:\n|$)", re.IGNORECASE)
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (143/120).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
25
26 1
FINAL_EXCEPTION_TYPES = [
27
    urllib3_exceptions.ConnectTimeoutError,
28
    urllib3_exceptions.ProxyError
29
]
30
31 1
log = logging.getLogger(__name__)
32
33
34 1
class CreateException(Create):
35
    #
36
    # exc_info
37
    #
38
39 1
    def from_exc_info(self, exc_info=None):
40
        if exc_info is None:
41
            # Retrieve `exc_info` of last exception
42
            exc_info = sys.exc_info()
43
44
        # Parse exception
45
        message_type = Message.Type.Exception
46
47
        try:
48
            message_type, exc_info = self._parse_exception(exc_info)
49
        except Exception as ex:
50
            log.warn('Unable to parse exception: %s', ex, exc_info=True)
51
52
        # Create exception
53
        exception = self.model(
54
            type=ErrorHasher.exc_type(exc_info[0]),
55
            message=ErrorHasher.exc_message(exc_info[1]),
56
            traceback=ErrorHasher.exc_traceback(exc_info[2]),
57
58
            timestamp=datetime.utcnow(),
59
            version_base=VERSION_BASE,
60
            version_branch=VERSION_BRANCH
61
        )
62
63
        # Calculate exception hash
64
        exception.hash = ErrorHasher.hash(
65
            exception,
66
            include_traceback=message_type == Message.Type.Exception
67
        )
68
69
        # Create/Lookup message for exception
70
        exception.error = MessageManager.get.from_exception(
0 ignored issues
show
Bug introduced by
It seems like a value for argument exception is missing in the unbound method call.
Loading history...
71
            exception,
72
            message_type=message_type
73
        )
74
75
        # Save exception details
76
        exception.save()
77
78
        return exception, exception.error
79
80
    #
81
    # message
82
    #
83
84 1
    def from_message(self, message):
85
        match = RE_TRACEBACK.match(message)
86
87
        if match is None:
88
            return
89
90
        # Create exception
91
        exception = self.model(
92
            type=match.group('type'),
93
            message=match.group('message'),
94
            traceback=self.strip_traceback(match.group('traceback')),
95
96
            timestamp=datetime.utcnow(),
97
            version_base=VERSION_BASE,
98
            version_branch=VERSION_BRANCH
99
        )
100
101
        # Calculate exception hash
102
        exception.hash = ErrorHasher.hash(exception)
103
104
        # Create/Lookup message for exception
105
        exception.error = MessageManager.get.from_exception(exception)
0 ignored issues
show
Bug introduced by
It seems like a value for argument exception is missing in the unbound method call.
Loading history...
106
107
        # Save exception details
108
        exception.save()
109
110
        return exception, exception.error
111
112 1
    @staticmethod
113
    def strip_traceback(tb):
114
        lines = tb.split('\n')
115
116
        for x in xrange(len(lines)):
0 ignored issues
show
Comprehensibility Best Practice introduced by
Undefined variable 'xrange'
Loading history...
117
            line = lines[x]
118
119
            if not line.startswith('  File'):
120
                continue
121
122
            try:
123
                # Try find path start/end quotes
124
                path_start = line.index('"') + 1
125
                path_end = line.index('"', path_start)
126
            except ValueError:
127
                # Unable to find path quotes
128
                continue
129
130
            # Convert path to relative
131
            path = os.path.relpath(line[path_start:path_end], PMS_PATH)
132
133
            # Update line
134
            lines[x] = line[:path_start] + path + line[path_end:]
135
136
        return '\n'.join(lines)
137
138 1
    def _parse_exception(self, exc_info):
139
        if type(exc_info) is not tuple or len(exc_info) != 3:
140
            return Message.Type.Exception, exc_info
141
142
        # Parse exception
143
        _, ex, tb = exc_info
0 ignored issues
show
Unused Code introduced by
The variable tb seems to be unused.
Loading history...
144
145
        if isinstance(ex, trakt.RequestError):
146
            return self._parse_trakt_error(exc_info)
147
148
        if isinstance(ex, requests_exceptions.RequestException):
149
            return self._parse_request_exception(exc_info)
150
151
        return Message.Type.Exception, exc_info
152
153 1
    def _parse_trakt_error(self, exc_info):
154
        if type(exc_info) is not tuple or len(exc_info) != 3:
155
            return Message.Type.Exception, exc_info
156
157
        # Parse exception information
158
        _, ex, tb = exc_info
159
160
        if not isinstance(ex, trakt.RequestError):
161
            return Message.Type.Exception, exc_info
162
163
        # Construct connection error
164
        return Message.Type.Trakt, (
165
            ConnectionError,
166
            ConnectionError(self._format_exception(ex)),
167
            tb
168
        )
169
170 1
    def _parse_request_exception(self, exc_info):
171
        if type(exc_info) is not tuple or len(exc_info) != 3:
172
            return Message.Type.Exception, exc_info
173
174
        # Parse exception information
175
        _, ex, tb = exc_info
176
177
        if not isinstance(ex, requests_exceptions.RequestException) or not ex.request:
178
            return Message.Type.Exception, exc_info
179
180
        # Parse request url
181
        url = urlparse(ex.request.url)
182
183
        if not url:
184
            return Message.Type.Exception, exc_info
185
186
        # Retrieve service title
187
        if url.netloc == 'sentry.skipthe.net':
188
            message_type = Message.Type.Sentry
189
        elif url.netloc == 'plex.tv' or url.netloc.endswith('.plex.tv'):
190
            message_type = Message.Type.Plex
191
        elif url.netloc == 'trakt.tv' or url.netloc.endswith('.trakt.tv'):
192
            message_type = Message.Type.Trakt
193
        else:
194
            return Message.Type.Exception, exc_info
195
196
        # Construct connection error
197
        return message_type, (
198
            ConnectionError,
199
            ConnectionError(self._format_exception(ex)),
200
            tb
201
        )
202
203 1
    @classmethod
204 1
    def _format_exception(cls, ex, include_type=True):
205
        ex = cls._find_inner_exception(ex)
206
207
        if isinstance(ex, urllib3_exceptions.ProxyError):
208
            return '%s: %s' % (
209
                type(ex).__name__,
210
                cls._format_exception(ex.args, include_type=False) or ex.message
0 ignored issues
show
Bug introduced by
The Instance of Exception does not seem to have a member named args.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
211
            )
212
213
        # Trakt
214
        if isinstance(ex, trakt.RequestError):
215
            return '%s (code: %r)' % (
216
                ex.error[1],
217
                ex.status_code
0 ignored issues
show
Bug introduced by
The Instance of Exception does not seem to have a member named status_code.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
218
            )
219
220
        # Socket
221
        if isinstance(ex, socket.error):
222
            if ex.errno is None:
0 ignored issues
show
Bug introduced by
The Instance of Exception does not seem to have a member named errno.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
223
                return '%s' % (
224
                    ex.message or ex.strerror
0 ignored issues
show
Bug introduced by
The Instance of Exception does not seem to have a member named strerror.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
225
                )
226
227
            return '%s (code: %r)' % (
228
                ex.message or ex.strerror,
0 ignored issues
show
Bug introduced by
The Instance of Exception does not seem to have a member named strerror.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
229
                ex.errno
0 ignored issues
show
Bug introduced by
The Instance of Exception does not seem to have a member named errno.

This check looks for calls to members that are non-existent. These calls will fail.

The member could have been renamed or removed.

Loading history...
230
            )
231
232
        # Generic exception
233
        if not include_type:
234
            return ex.message
235
236
        return '%s: %s' % (
237
            type(ex).__name__,
238
            ex.message
239
        )
240
241 1
    @classmethod
242
    def _find_inner_exception(cls, ex):
243
        # Find exceptions inside list or tuple
244
        if type(ex) is list or type(ex) is tuple:
245
            for value in ex:
246
                if not issubclass(value.__class__, BaseException):
247
                    continue
248
249
                return cls._find_inner_exception(value)
250
251
            return Exception('Unknown Error')
252
253
        # Ensure `ex` is an exception
254
        if not issubclass(ex.__class__, BaseException):
255
            return ex
256
257
        # Return final exceptions
258
        if type(ex) in FINAL_EXCEPTION_TYPES:
259
            return ex
260
261
        # Requests
262
        if isinstance(ex, requests_exceptions.RequestException):
263
            if len(ex.args) < 1:
264
                return ex
265
266
            return cls._find_inner_exception(ex.args)
267
268
        # urllib3
269
        if isinstance(ex, urllib3_exceptions.MaxRetryError):
270
            return cls._find_inner_exception(ex.reason)
271
272
        if isinstance(ex, urllib3_exceptions.HTTPError):
273
            if issubclass(ex.message.__class__, BaseException):
274
                return cls._find_inner_exception(ex.message)
275
276
            if len(ex.args) < 1:
277
                return ex
278
279
            return cls._find_inner_exception(ex.args)
280
281
        # Generic exception
282
        return ex
283
284
285 1
class ExceptionManager(Manager):
286 1
    create = CreateException
287
288
    model = Exception
289