sopel.trigger.PreTrigger.__init__()   F
last analyzed

Complexity

Conditions 17

Size

Total Lines 81
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 45
dl 0
loc 81
rs 1.8
c 0
b 0
f 0
cc 17
nop 3

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like sopel.trigger.PreTrigger.__init__() 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
# coding=utf-8
2
"""Sopel IRC Trigger Lines"""
3
from __future__ import unicode_literals, absolute_import, print_function, division
4
5
import re
6
import sys
7
import datetime
8
9
from sopel import tools
10
11
12
__all__ = [
13
    'PreTrigger',
14
    'Trigger',
15
]
16
17
if sys.version_info.major >= 3:
18
    unicode = str
19
    basestring = str
20
21
22
class PreTrigger(object):
23
    """A parsed message from the server, which has not been matched against
24
    any rules."""
25
    component_regex = re.compile(r'([^!]*)!?([^@]*)@?(.*)')
26
    intent_regex = re.compile('\x01(\\S+) ?(.*)\x01')
27
28
    def __init__(self, own_nick, line):
29
        """own_nick is the bot's nick, needed to correctly parse sender.
30
        line is the full line from the server."""
31
        line = line.strip('\r')
32
        self.line = line
33
34
        # Break off IRCv3 message tags, if present
35
        self.tags = {}
36
        if line.startswith('@'):
37
            tagstring, line = line.split(' ', 1)
38
            for tag in tagstring[1:].split(';'):
39
                tag = tag.split('=', 1)
40
                if len(tag) > 1:
41
                    self.tags[tag[0]] = tag[1]
42
                else:
43
                    self.tags[tag[0]] = None
44
45
        self.time = datetime.datetime.utcnow()
46
        if 'time' in self.tags:
47
            try:
48
                self.time = datetime.datetime.strptime(self.tags['time'], '%Y-%m-%dT%H:%M:%S.%fZ')
49
            except ValueError:
50
                pass  # Server isn't conforming to spec, ignore the server-time
51
52
        # Grabs hostmask from line.
53
        # Example: line = ':Sopel!foo@bar PRIVMSG #sopel :foobar!'
54
        #          print(hostmask)  # Sopel!foo@bar
55
        # All lines start with ":" except PING.
56
        if line.startswith(':'):
57
            self.hostmask, line = line[1:].split(' ', 1)
58
        else:
59
            self.hostmask = None
60
61
        # Parses the line into a list of arguments.
62
        # Some events like MODE don't have a secondary string argument, i.e. no ' :' inside the line.
63
        # Example 1:  line = ':nick!ident@domain PRIVMSG #sopel :foo bar!'
64
        #             print(text)    # 'foo bar!'
65
        #             print(argstr)  # ':nick!ident@domain PRIVMSG #sopel'
66
        #             print(args)    # [':nick!ident@domain', 'PRIVMSG', '#sopel', 'foo bar!']
67
        # Example 2:  line = 'irc.freenode.net MODE Sopel +i'
68
        #             print(text)    # '+i'
69
        #             print(args)    # ['irc.freenode.net', 'MODE', 'Sopel', '+i']
70
        if ' :' in line:
71
            argstr, text = line.split(' :', 1)
72
            self.args = argstr.split(' ')
73
            self.args.append(text)
74
        else:
75
            self.args = line.split(' ')
76
            self.text = self.args[-1]
77
78
        self.event = self.args[0]
79
        self.args = self.args[1:]
80
        components = PreTrigger.component_regex.match(self.hostmask or '').groups()
81
        self.nick, self.user, self.host = components
82
        self.nick = tools.Identifier(self.nick)
83
84
        # If we have arguments, the first one is the sender
85
        # Unless it's a QUIT event
86
        if self.args and self.event != 'QUIT':
87
            target = tools.Identifier(self.args[0])
88
        else:
89
            target = None
90
91
        # Unless we're messaging the bot directly, in which case that second
92
        # arg will be our bot's name.
93
        if target and target.lower() == own_nick.lower():
94
            target = self.nick
95
        self.sender = target
96
97
        # Parse CTCP into a form consistent with IRCv3 intents
98
        if self.event == 'PRIVMSG' or self.event == 'NOTICE':
99
            intent_match = PreTrigger.intent_regex.match(self.args[-1])
100
            if intent_match:
101
                intent, message = intent_match.groups()
102
                self.tags['intent'] = intent
103
                self.args[-1] = message or ''
104
105
        # Populate account from extended-join messages
106
        if self.event == 'JOIN' and len(self.args) == 3:
107
            # Account is the second arg `...JOIN #Sopel account :realname`
108
            self.tags['account'] = self.args[1]
109
110
111
class Trigger(unicode):
0 ignored issues
show
introduced by
The variable unicode does not seem to be defined in case sys.version_info.major >= 3 on line 17 is False. Are you sure this can never be the case?
Loading history...
112
    """A line from the server, which has matched a callable's rules.
113
114
    Note that CTCP messages (`PRIVMSG`es and `NOTICE`es which start and end
115
    with `'\\x01'`) will have the `'\\x01'` bytes stripped, and the command
116
    (e.g. `ACTION`) placed mapped to the `'intent'` key in `Trigger.tags`.
117
    """
118
    sender = property(lambda self: self._pretrigger.sender)
119
    """The channel from which the message was sent.
120
121
    In a private message, this is the nick that sent the message."""
122
    time = property(lambda self: self._pretrigger.time)
123
    """A datetime object at which the message was received by the IRC server.
124
125
    If the server does not support server-time, then `time` will be the time
126
    that the message was received by Sopel"""
127
    raw = property(lambda self: self._pretrigger.line)
128
    """The entire message, as sent from the server. This includes the CTCP
129
    \\x01 bytes and command, if they were included."""
130
    is_privmsg = property(lambda self: self._is_privmsg)
131
    """True if the trigger is from a user, False if it's from a channel."""
132
    hostmask = property(lambda self: self._pretrigger.hostmask)
133
    """Hostmask of the person who sent the message as <nick>!<user>@<host>"""
134
    user = property(lambda self: self._pretrigger.user)
135
    """Local username of the person who sent the message"""
136
    nick = property(lambda self: self._pretrigger.nick)
137
    """The :class:`sopel.tools.Identifier` of the person who sent the message.
138
    """
139
    host = property(lambda self: self._pretrigger.host)
140
    """The hostname of the person who sent the message"""
141
    event = property(lambda self: self._pretrigger.event)
142
    """The IRC event (e.g. ``PRIVMSG`` or ``MODE``) which triggered the
143
    message."""
144
    match = property(lambda self: self._match)
145
    """The regular expression :class:`re.MatchObject` for the triggering line.
146
    """
147
    group = property(lambda self: self._match.group)
148
    """The ``group`` function of the ``match`` attribute.
149
150
    See Python :mod:`re` documentation for details."""
151
    groups = property(lambda self: self._match.groups)
152
    """The ``groups`` function of the ``match`` attribute.
153
154
    See Python :mod:`re` documentation for details."""
155
    groupdict = property(lambda self: self._match.groupdict)
156
    """The ``groupdict`` function of the ``match`` attribute.
157
158
    See Python :mod:`re` documentation for details."""
159
    args = property(lambda self: self._pretrigger.args)
160
    """
161
    A tuple containing each of the arguments to an event. These are the
162
    strings passed between the event name and the colon. For example,
163
    setting ``mode -m`` on the channel ``#example``, args would be
164
    ``('#example', '-m')``
165
    """
166
    tags = property(lambda self: self._pretrigger.tags)
167
    """A map of the IRCv3 message tags on the message."""
168
    admin = property(lambda self: self._admin)
169
    """True if the nick which triggered the command is one of the bot's admins.
170
    """
171
    owner = property(lambda self: self._owner)
172
    """True if the nick which triggered the command is the bot's owner."""
173
    account = property(lambda self: self.tags.get('account') or self._account)
174
    """The account name of the user sending the message.
175
176
    This is only available if either the account-tag or the account-notify and
177
    extended-join capabilites are available. If this isn't the case, or the user
178
    sending the message isn't logged in, this will be None.
179
    """
180
181
    def __new__(cls, config, message, match, account=None):
182
        self = unicode.__new__(cls, message.args[-1] if message.args else '')
0 ignored issues
show
introduced by
The variable unicode does not seem to be defined in case sys.version_info.major >= 3 on line 17 is False. Are you sure this can never be the case?
Loading history...
183
        self._account = account
184
        self._pretrigger = message
185
        self._match = match
186
        self._is_privmsg = message.sender and message.sender.is_nick()
187
188
        def match_host_or_nick(pattern):
189
            pattern = tools.get_hostmask_regex(pattern)
190
            return bool(
191
                pattern.match(self.nick) or
192
                pattern.match('@'.join((self.nick, self.host)))
193
            )
194
195
        if config.core.owner_account:
196
            self._owner = config.core.owner_account == self.account
197
        else:
198
            self._owner = match_host_or_nick(config.core.owner)
199
        self._admin = (
200
            self._owner or
201
            self.account in config.core.admin_accounts or
202
            any(match_host_or_nick(item) for item in config.core.admins)
203
        )
204
205
        return self
206