sopel.loader.is_triggerable()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 33
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 14
dl 0
loc 33
rs 9.7
c 0
b 0
f 0
cc 1
nop 1
1
# coding=utf-8
2
from __future__ import unicode_literals, absolute_import, print_function, division
3
4
import re
5
import sys
6
7
from sopel.tools import (compile_rule, itervalues, get_command_regexp,
8
                         get_nickname_command_regexp, get_action_command_regexp)
9
from sopel.config import core_section
10
11
default_prefix = core_section.CoreSection.help_prefix.default
12
del core_section
13
14
if sys.version_info.major >= 3:
15
    basestring = (str, bytes)
16
17
18
def trim_docstring(doc):
19
    """Get the docstring as a series of lines that can be sent"""
20
    if not doc:
21
        return []
22
    lines = doc.expandtabs().splitlines()
23
    indent = sys.maxsize
24
    for line in lines[1:]:
25
        stripped = line.lstrip()
26
        if stripped:
27
            indent = min(indent, len(line) - len(stripped))
28
    trimmed = [lines[0].strip()]
29
    if indent < sys.maxsize:
30
        for line in lines[1:]:
31
            trimmed.append(line[:].rstrip())
32
    while trimmed and not trimmed[-1]:
33
        trimmed.pop()
34
    while trimmed and not trimmed[0]:
35
        trimmed.pop(0)
36
    return trimmed
37
38
39
def clean_callable(func, config):
40
    """Compiles the regexes, moves commands into func.rule, fixes up docs and
41
    puts them in func._docs, and sets defaults"""
42
    nick = config.core.nick
43
    alias_nicks = config.core.alias_nicks
44
    prefix = config.core.prefix
45
    help_prefix = config.core.help_prefix
46
    func._docs = {}
47
    doc = trim_docstring(func.__doc__)
48
    examples = []
49
50
    func.unblockable = getattr(func, 'unblockable', False)
51
    func.echo = getattr(func, 'echo', False)
52
    func.priority = getattr(func, 'priority', 'medium')
53
    func.thread = getattr(func, 'thread', True)
54
    func.rate = getattr(func, 'rate', 0)
55
    func.channel_rate = getattr(func, 'channel_rate', 0)
56
    func.global_rate = getattr(func, 'global_rate', 0)
57
58
    if not hasattr(func, 'event'):
59
        func.event = ['PRIVMSG']
60
    else:
61
        if isinstance(func.event, basestring):
0 ignored issues
show
introduced by
The variable basestring does not seem to be defined in case sys.version_info.major >= 3 on line 14 is False. Are you sure this can never be the case?
Loading history...
62
            func.event = [func.event.upper()]
63
        else:
64
            func.event = [event.upper() for event in func.event]
65
66
    if hasattr(func, 'rule'):
67
        if isinstance(func.rule, basestring):
68
            func.rule = [func.rule]
69
        func.rule = [compile_rule(nick, rule, alias_nicks) for rule in func.rule]
70
71
    if any(hasattr(func, attr) for attr in ['commands', 'nickname_commands', 'action_commands']):
72
        func.rule = getattr(func, 'rule', [])
73
        for command in getattr(func, 'commands', []):
74
            regexp = get_command_regexp(prefix, command)
75
            if regexp not in func.rule:
76
                func.rule.append(regexp)
77
        for command in getattr(func, 'nickname_commands', []):
78
            regexp = get_nickname_command_regexp(nick, command, alias_nicks)
79
            if regexp not in func.rule:
80
                func.rule.append(regexp)
81
        for command in getattr(func, 'action_commands', []):
82
            regexp = get_action_command_regexp(command)
83
            if regexp not in func.rule:
84
                func.rule.append(regexp)
85
        if hasattr(func, 'example'):
86
            # If no examples are flagged as user-facing, just show the first one like Sopel<7 did
87
            examples = [rec["example"] for rec in func.example if rec["help"]] or [func.example[0]["example"]]
88
            for i, example in enumerate(examples):
89
                example = example.replace('$nickname', nick)
90
                if example[0] != help_prefix and not example.startswith(nick):
91
                    example = example.replace(default_prefix, help_prefix, 1)
92
                examples[i] = example
93
        if doc or examples:
94
            cmds = []
95
            cmds.extend(getattr(func, 'commands', []))
96
            cmds.extend(getattr(func, 'nickname_commands', []))
97
            for command in cmds:
98
                func._docs[command] = (doc, examples)
99
100
    if hasattr(func, 'intents'):
101
        # Can be implementation-dependent
102
        _regex_type = type(re.compile(''))
103
        func.intents = [
104
            (intent
105
                if isinstance(intent, _regex_type)
106
                else re.compile(intent, re.IGNORECASE))
107
            for intent in func.intents
108
        ]
109
110
111
def is_triggerable(obj):
112
    """Check if ``obj`` can handle the bot's triggers.
113
114
    :param obj: any :term:`function` to check
115
    :return: ``True`` if ``obj`` can handle the bot's triggers
116
117
    A triggerable is a callable that will be used by the bot to handle a
118
    particular trigger (i.e. an IRC message): it can be a regex rule, an
119
    event, an intent, a command, a nickname command, or an action command.
120
    However, it must not be a job or a URL callback.
121
122
    .. seealso::
123
124
        Many of the decorators defined in :mod:`sopel.module` make the
125
        decorated function a triggerable object.
126
    """
127
    forbidden_attrs = (
128
        'interval',
129
        'url_regex',
130
    )
131
    forbidden = any(hasattr(obj, attr) for attr in forbidden_attrs)
132
133
    allowed_attrs = (
134
        'rule',
135
        'event',
136
        'intents',
137
        'commands',
138
        'nickname_commands',
139
        'action_commands',
140
    )
141
    allowed = any(hasattr(obj, attr) for attr in allowed_attrs)
142
143
    return allowed and not forbidden
144
145
146
def clean_module(module, config):
147
    callables = []
148
    shutdowns = []
149
    jobs = []
150
    urls = []
151
    for obj in itervalues(vars(module)):
152
        if callable(obj):
153
            if getattr(obj, '__name__', None) == 'shutdown':
154
                shutdowns.append(obj)
155
            elif is_triggerable(obj):
156
                clean_callable(obj, config)
157
                callables.append(obj)
158
            elif hasattr(obj, 'interval'):
159
                clean_callable(obj, config)
160
                jobs.append(obj)
161
            elif hasattr(obj, 'url_regex'):
162
                urls.append(obj)
163
    return callables, jobs, shutdowns, urls
164