| Total Complexity | 79 |
| Total Lines | 708 |
| Duplicated Lines | 15.96 % |
| 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 sopel.module 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 | """This contains decorators and tools for creating callable plugin functions. |
||
| 3 | """ |
||
| 4 | # Copyright 2013, Ari Koivula, <[email protected]> |
||
| 5 | # Copyright © 2013, Elad Alfassa <[email protected]> |
||
| 6 | # Copyright 2013, Lior Ramati <[email protected]> |
||
| 7 | # Licensed under the Eiffel Forum License 2. |
||
| 8 | |||
| 9 | from __future__ import unicode_literals, absolute_import, print_function, division |
||
| 10 | |||
| 11 | import functools |
||
| 12 | import re |
||
| 13 | |||
| 14 | __all__ = [ |
||
| 15 | # constants |
||
| 16 | 'NOLIMIT', 'VOICE', 'HALFOP', 'OP', 'ADMIN', 'OWNER', |
||
| 17 | # decorators |
||
| 18 | 'action_commands', |
||
| 19 | 'commands', |
||
| 20 | 'echo', |
||
| 21 | 'example', |
||
| 22 | 'intent', |
||
| 23 | 'interval', |
||
| 24 | 'nickname_commands', |
||
| 25 | 'priority', |
||
| 26 | 'rate', |
||
| 27 | 'require_admin', |
||
| 28 | 'require_chanmsg', |
||
| 29 | 'require_owner', |
||
| 30 | 'require_privilege', |
||
| 31 | 'require_privmsg', |
||
| 32 | 'rule', |
||
| 33 | 'thread', |
||
| 34 | 'unblockable', |
||
| 35 | 'url', |
||
| 36 | ] |
||
| 37 | |||
| 38 | |||
| 39 | NOLIMIT = 1 |
||
| 40 | """Return value for ``callable``\\s, which suppresses rate limiting for the call. |
||
| 41 | |||
| 42 | Returning this value means the triggering user will not be |
||
| 43 | prevented from triggering the command again within the rate limit. This can |
||
| 44 | be used, for example, to allow a user to retry a failed command immediately. |
||
| 45 | |||
| 46 | .. versionadded:: 4.0 |
||
| 47 | """ |
||
| 48 | |||
| 49 | VOICE = 1 |
||
| 50 | """Privilege level for the +v channel permission |
||
| 51 | |||
| 52 | .. versionadded:: 4.1 |
||
| 53 | """ |
||
| 54 | |||
| 55 | HALFOP = 2 |
||
| 56 | """Privilege level for the +h channel permission |
||
| 57 | |||
| 58 | .. versionadded:: 4.1 |
||
| 59 | """ |
||
| 60 | |||
| 61 | OP = 4 |
||
| 62 | """Privilege level for the +o channel permission |
||
| 63 | |||
| 64 | .. versionadded:: 4.1 |
||
| 65 | """ |
||
| 66 | |||
| 67 | ADMIN = 8 |
||
| 68 | """Privilege level for the +a channel permission |
||
| 69 | |||
| 70 | .. versionadded:: 4.1 |
||
| 71 | """ |
||
| 72 | |||
| 73 | OWNER = 16 |
||
| 74 | """Privilege level for the +q channel permission |
||
| 75 | |||
| 76 | .. versionadded:: 4.1 |
||
| 77 | """ |
||
| 78 | |||
| 79 | |||
| 80 | def unblockable(function): |
||
| 81 | """Decorator which exempts the function from nickname and hostname blocking. |
||
| 82 | |||
| 83 | This can be used to ensure events such as JOIN are always recorded. |
||
| 84 | """ |
||
| 85 | function.unblockable = True |
||
| 86 | return function |
||
| 87 | |||
| 88 | |||
| 89 | def interval(*intervals): |
||
| 90 | """Decorates a function to be called by the bot every X seconds. |
||
| 91 | |||
| 92 | This decorator can be used multiple times for multiple intervals, or all |
||
| 93 | intervals can be given at once as arguments. The first time the function |
||
| 94 | will be called is X seconds after the bot was started. |
||
| 95 | |||
| 96 | Unlike other plugin functions, ones decorated by interval must only take a |
||
| 97 | :class:`sopel.bot.Sopel` as their argument; they do not get a trigger. The |
||
| 98 | bot argument will not have a context, so functions like ``bot.say()`` will |
||
| 99 | not have a default destination. |
||
| 100 | |||
| 101 | There is no guarantee that the bot is connected to a server or joined a |
||
| 102 | channel when the function is called, so care must be taken. |
||
| 103 | |||
| 104 | Example:: |
||
| 105 | |||
| 106 | from sopel import module |
||
| 107 | |||
| 108 | @module.interval(5) |
||
| 109 | def spam_every_5s(bot): |
||
| 110 | if "#here" in bot.channels: |
||
| 111 | bot.say("It has been five seconds!", "#here") |
||
| 112 | |||
| 113 | """ |
||
| 114 | def add_attribute(function): |
||
| 115 | if not hasattr(function, "interval"): |
||
| 116 | function.interval = [] |
||
| 117 | for arg in intervals: |
||
| 118 | if arg not in function.interval: |
||
| 119 | function.interval.append(arg) |
||
| 120 | return function |
||
| 121 | |||
| 122 | return add_attribute |
||
| 123 | |||
| 124 | |||
| 125 | def rule(*patterns): |
||
| 126 | """Decorate a function to be called when a line matches the given pattern |
||
| 127 | |||
| 128 | Each argument is a regular expression which will trigger the function. |
||
| 129 | |||
| 130 | This decorator can be used multiple times to add more rules. |
||
| 131 | |||
| 132 | If the Sopel instance is in a channel, or sent a PRIVMSG, where a string |
||
| 133 | matching this expression is said, the function will execute. Note that |
||
| 134 | captured groups here will be retrievable through the Trigger object later. |
||
| 135 | |||
| 136 | Inside the regular expression, some special directives can be used. $nick |
||
| 137 | will be replaced with the nick of the bot and , or :, and $nickname will be |
||
| 138 | replaced with the nick of the bot. |
||
| 139 | |||
| 140 | .. versionchanged:: 7.0 |
||
| 141 | |||
| 142 | The :func:`rule` decorator can be called with multiple positional |
||
| 143 | arguments, each used to add a rule. This is equivalent to decorating |
||
| 144 | the same function multiple times with this decorator. |
||
| 145 | |||
| 146 | """ |
||
| 147 | def add_attribute(function): |
||
| 148 | if not hasattr(function, "rule"): |
||
| 149 | function.rule = [] |
||
| 150 | for value in patterns: |
||
| 151 | if value not in function.rule: |
||
| 152 | function.rule.append(value) |
||
| 153 | return function |
||
| 154 | |||
| 155 | return add_attribute |
||
| 156 | |||
| 157 | |||
| 158 | def thread(value): |
||
| 159 | """Decorate a function to specify if it should be run in a separate thread. |
||
| 160 | |||
| 161 | :param bool value: if true, the function is called in a separate thread; |
||
| 162 | otherwise from the bot's main thread |
||
| 163 | |||
| 164 | Functions run in a separate thread (as is the default) will not prevent the |
||
| 165 | bot from executing other functions at the same time. Functions not run in a |
||
| 166 | separate thread may be started while other functions are still running, but |
||
| 167 | additional functions will not start until it is completed. |
||
| 168 | """ |
||
| 169 | threaded = bool(value) |
||
| 170 | |||
| 171 | def add_attribute(function): |
||
| 172 | function.thread = threaded |
||
| 173 | return function |
||
| 174 | |||
| 175 | return add_attribute |
||
| 176 | |||
| 177 | |||
| 178 | def echo(function=None): |
||
| 179 | """Decorate a function to specify if it should receive echo messages. |
||
| 180 | |||
| 181 | This decorator can be used to listen in on the messages that Sopel is |
||
| 182 | sending and react accordingly. |
||
| 183 | """ |
||
| 184 | def add_attribute(function): |
||
| 185 | function.echo = True |
||
| 186 | return function |
||
| 187 | |||
| 188 | # hack to allow both @echo and @echo() to work |
||
| 189 | if callable(function): |
||
| 190 | return add_attribute(function) |
||
| 191 | return add_attribute |
||
| 192 | |||
| 193 | |||
| 194 | def commands(*command_list): |
||
| 195 | """Decorate a function to set one or more commands to trigger it. |
||
| 196 | |||
| 197 | This decorator can be used to add multiple commands to one callable in a |
||
| 198 | single line. The resulting match object will have the command as the first |
||
| 199 | group, rest of the line, excluding leading whitespace, as the second group. |
||
| 200 | Parameters 1 through 4, separated by whitespace, will be groups 3-6. |
||
| 201 | |||
| 202 | Args: |
||
| 203 | command: A string, which can be a regular expression. |
||
| 204 | |||
| 205 | Returns: |
||
| 206 | A function with a new command appended to the commands |
||
| 207 | attribute. If there is no commands attribute, it is added. |
||
| 208 | |||
| 209 | Example: |
||
| 210 | @commands("hello"): |
||
| 211 | If the command prefix is "\\.", this would trigger on lines starting |
||
| 212 | with ".hello". |
||
| 213 | |||
| 214 | @commands('j', 'join') |
||
| 215 | If the command prefix is "\\.", this would trigger on lines starting |
||
| 216 | with either ".j" or ".join". |
||
| 217 | |||
| 218 | """ |
||
| 219 | def add_attribute(function): |
||
| 220 | if not hasattr(function, "commands"): |
||
| 221 | function.commands = [] |
||
| 222 | for command in command_list: |
||
| 223 | if command not in function.commands: |
||
| 224 | function.commands.append(command) |
||
| 225 | return function |
||
| 226 | return add_attribute |
||
| 227 | |||
| 228 | |||
| 229 | def nickname_commands(*command_list): |
||
| 230 | """Decorate a function to trigger on lines starting with "$nickname: command". |
||
| 231 | |||
| 232 | This decorator can be used multiple times to add multiple rules. The |
||
| 233 | resulting match object will have the command as the first group, rest of |
||
| 234 | the line, excluding leading whitespace, as the second group. Parameters 1 |
||
| 235 | through 4, separated by whitespace, will be groups 3-6. |
||
| 236 | |||
| 237 | Args: |
||
| 238 | command: A string, which can be a regular expression. |
||
| 239 | |||
| 240 | Returns: |
||
| 241 | A function with a new regular expression appended to the rule |
||
| 242 | attribute. If there is no rule attribute, it is added. |
||
| 243 | |||
| 244 | Example: |
||
| 245 | @nickname_commands("hello!"): |
||
| 246 | Would trigger on "$nickname: hello!", "$nickname, hello!", |
||
| 247 | "$nickname hello!", "$nickname hello! parameter1" and |
||
| 248 | "$nickname hello! p1 p2 p3 p4 p5 p6 p7 p8 p9". |
||
| 249 | @nickname_commands(".*"): |
||
| 250 | Would trigger on anything starting with "$nickname[:,]? ", and |
||
| 251 | would never have any additional parameters, as the command would |
||
| 252 | match the rest of the line. |
||
| 253 | |||
| 254 | """ |
||
| 255 | def add_attribute(function): |
||
| 256 | if not hasattr(function, 'nickname_commands'): |
||
| 257 | function.nickname_commands = [] |
||
| 258 | for cmd in command_list: |
||
| 259 | if cmd not in function.nickname_commands: |
||
| 260 | function.nickname_commands.append(cmd) |
||
| 261 | return function |
||
| 262 | return add_attribute |
||
| 263 | |||
| 264 | |||
| 265 | def action_commands(*command_list): |
||
| 266 | """Decorate a function to trigger on CTCP ACTION lines. |
||
| 267 | |||
| 268 | This decorator can be used multiple times to add multiple rules. The |
||
| 269 | resulting match object will have the command as the first group, rest of |
||
| 270 | the line, excluding leading whitespace, as the second group. Parameters 1 |
||
| 271 | through 4, separated by whitespace, will be groups 3-6. |
||
| 272 | |||
| 273 | Args: |
||
| 274 | command: A string, which can be a regular expression. |
||
| 275 | |||
| 276 | Returns: |
||
| 277 | A function with a new regular expression appended to the rule |
||
| 278 | attribute. If there is no rule attribute, it is added. |
||
| 279 | |||
| 280 | Example: |
||
| 281 | @action_commands("hello!"): |
||
| 282 | Would trigger on "/me hello!" |
||
| 283 | """ |
||
| 284 | def add_attribute(function): |
||
| 285 | function.intents = ['ACTION'] |
||
| 286 | if not hasattr(function, 'action_commands'): |
||
| 287 | function.action_commands = [] |
||
| 288 | for cmd in command_list: |
||
| 289 | if cmd not in function.action_commands: |
||
| 290 | function.action_commands.append(cmd) |
||
| 291 | return function |
||
| 292 | return add_attribute |
||
| 293 | |||
| 294 | |||
| 295 | def priority(value): |
||
| 296 | """Decorate a function to be executed with higher or lower priority. |
||
| 297 | |||
| 298 | Args: |
||
| 299 | value: Priority can be one of "high", "medium", "low". Defaults to |
||
| 300 | medium. |
||
| 301 | |||
| 302 | Priority allows you to control the order of callable execution, if your |
||
| 303 | module needs it. |
||
| 304 | |||
| 305 | """ |
||
| 306 | def add_attribute(function): |
||
| 307 | function.priority = value |
||
| 308 | return function |
||
| 309 | return add_attribute |
||
| 310 | |||
| 311 | |||
| 312 | def event(*event_list): |
||
| 313 | """Decorate a function to be triggered on specific IRC events. |
||
| 314 | |||
| 315 | This is one of a number of events, such as 'JOIN', 'PART', 'QUIT', etc. |
||
| 316 | (More details can be found in RFC 1459.) When the Sopel bot is sent one of |
||
| 317 | these events, the function will execute. Note that functions with an event |
||
| 318 | must also be given a rule to match (though it may be '.*', which will |
||
| 319 | always match) or they will not be triggered. |
||
| 320 | |||
| 321 | :class:`sopel.tools.events` provides human-readable names for many of the |
||
| 322 | numeric events, which may help your code be clearer. |
||
| 323 | """ |
||
| 324 | def add_attribute(function): |
||
| 325 | if not hasattr(function, "event"): |
||
| 326 | function.event = [] |
||
| 327 | for name in event_list: |
||
| 328 | if name not in function.event: |
||
| 329 | function.event.append(name) |
||
| 330 | return function |
||
| 331 | return add_attribute |
||
| 332 | |||
| 333 | |||
| 334 | def intent(*intent_list): |
||
| 335 | """Decorate a callable trigger on a message with any of the given intents. |
||
| 336 | |||
| 337 | .. versionadded:: 5.2.0 |
||
| 338 | """ |
||
| 339 | def add_attribute(function): |
||
| 340 | if not hasattr(function, "intents"): |
||
| 341 | function.intents = [] |
||
| 342 | for name in intent_list: |
||
| 343 | if name not in function.intents: |
||
| 344 | function.intents.append(name) |
||
| 345 | return function |
||
| 346 | return add_attribute |
||
| 347 | |||
| 348 | |||
| 349 | def rate(user=0, channel=0, server=0): |
||
| 350 | """Decorate a function to limit how often it can be triggered on a per-user |
||
| 351 | basis, in a channel, or across the server (bot). A value of zero means no |
||
| 352 | limit. If a function is given a rate of 20, that function may only be used |
||
| 353 | once every 20 seconds in the scope corresponding to the parameter. |
||
| 354 | Users on the admin list in Sopel’s configuration are exempted from rate |
||
| 355 | limits. |
||
| 356 | |||
| 357 | Rate-limited functions that use scheduled future commands should import |
||
| 358 | threading.Timer() instead of sched, or rate limiting will not work properly. |
||
| 359 | """ |
||
| 360 | def add_attribute(function): |
||
| 361 | function.rate = user |
||
| 362 | function.channel_rate = channel |
||
| 363 | function.global_rate = server |
||
| 364 | return function |
||
| 365 | return add_attribute |
||
| 366 | |||
| 367 | |||
| 368 | View Code Duplication | def require_privmsg(message=None, reply=False): |
|
|
|
|||
| 369 | """Decorate a function to only be triggerable from a private message. |
||
| 370 | |||
| 371 | :param str message: optional message said if triggered in a channel |
||
| 372 | :param bool reply: use :meth:`~sopel.bot.Sopel.reply` instead of |
||
| 373 | :meth:`~sopel.bot.Sopel.say` when ``True``; defaults to |
||
| 374 | ``False`` |
||
| 375 | |||
| 376 | If it is triggered in a channel message, ``message`` will be said if |
||
| 377 | given. By default, it uses :meth:`bot.say() <.bot.Sopel.say>`, but when |
||
| 378 | ``reply`` is true, then it uses :meth:`bot.reply() <.bot.Sopel.reply>` |
||
| 379 | instead. |
||
| 380 | |||
| 381 | .. versionchanged:: 7.0.0 |
||
| 382 | Added the ``reply`` parameter. |
||
| 383 | """ |
||
| 384 | def actual_decorator(function): |
||
| 385 | @functools.wraps(function) |
||
| 386 | def _nop(*args, **kwargs): |
||
| 387 | # Assign trigger and bot for easy access later |
||
| 388 | bot, trigger = args[0:2] |
||
| 389 | if trigger.is_privmsg: |
||
| 390 | return function(*args, **kwargs) |
||
| 391 | else: |
||
| 392 | if message and not callable(message): |
||
| 393 | if reply: |
||
| 394 | bot.reply(message) |
||
| 395 | else: |
||
| 396 | bot.say(message) |
||
| 397 | return _nop |
||
| 398 | |||
| 399 | # Hack to allow decorator without parens |
||
| 400 | if callable(message): |
||
| 401 | return actual_decorator(message) |
||
| 402 | return actual_decorator |
||
| 403 | |||
| 404 | |||
| 405 | View Code Duplication | def require_chanmsg(message=None, reply=False): |
|
| 406 | """Decorate a function to only be triggerable from a channel message. |
||
| 407 | |||
| 408 | :param str message: optional message said if triggered in private message |
||
| 409 | :param bool reply: use :meth:`~.bot.Sopel.reply` instead of |
||
| 410 | :meth:`~.bot.Sopel.say` when ``True``; defaults to |
||
| 411 | ``False`` |
||
| 412 | |||
| 413 | If it is triggered in a private message, ``message`` will be said if |
||
| 414 | given. By default, it uses :meth:`bot.say() <.bot.Sopel.say>`, but when |
||
| 415 | ``reply`` is true, then it uses :meth:`bot.reply() <.bot.Sopel.reply>` |
||
| 416 | instead. |
||
| 417 | |||
| 418 | .. versionchanged:: 7.0.0 |
||
| 419 | Added the ``reply`` parameter. |
||
| 420 | """ |
||
| 421 | def actual_decorator(function): |
||
| 422 | @functools.wraps(function) |
||
| 423 | def _nop(*args, **kwargs): |
||
| 424 | # Assign trigger and bot for easy access later |
||
| 425 | bot, trigger = args[0:2] |
||
| 426 | if not trigger.is_privmsg: |
||
| 427 | return function(*args, **kwargs) |
||
| 428 | else: |
||
| 429 | if message and not callable(message): |
||
| 430 | if reply: |
||
| 431 | bot.reply(message) |
||
| 432 | else: |
||
| 433 | bot.say(message) |
||
| 434 | return _nop |
||
| 435 | |||
| 436 | # Hack to allow decorator without parens |
||
| 437 | if callable(message): |
||
| 438 | return actual_decorator(message) |
||
| 439 | return actual_decorator |
||
| 440 | |||
| 441 | |||
| 442 | def require_privilege(level, message=None, reply=False): |
||
| 443 | """Decorate a function to require at least the given channel permission. |
||
| 444 | |||
| 445 | :param int level: required privilege level to use this command |
||
| 446 | :param str message: optional message said to insufficiently privileged user |
||
| 447 | :param bool reply: use :meth:`~.bot.Sopel.reply` instead of |
||
| 448 | :meth:`~.bot.Sopel.say` when ``True``; defaults to |
||
| 449 | ``False`` |
||
| 450 | |||
| 451 | ``level`` can be one of the privilege level constants defined in this |
||
| 452 | module. If the user does not have the privilege, the bot will say |
||
| 453 | ``message`` if given. By default, it uses :meth:`bot.say() |
||
| 454 | <.bot.Sopel.say>`, but when ``reply`` is true, then it uses |
||
| 455 | :meth:`bot.reply() <.bot.Sopel.reply>` instead. |
||
| 456 | |||
| 457 | Privilege requirements are ignored in private messages. |
||
| 458 | |||
| 459 | .. versionchanged:: 7.0.0 |
||
| 460 | Added the ``reply`` parameter. |
||
| 461 | """ |
||
| 462 | def actual_decorator(function): |
||
| 463 | @functools.wraps(function) |
||
| 464 | def guarded(bot, trigger, *args, **kwargs): |
||
| 465 | # If this is a privmsg, ignore privilege requirements |
||
| 466 | if trigger.is_privmsg: |
||
| 467 | return function(bot, trigger, *args, **kwargs) |
||
| 468 | channel_privs = bot.channels[trigger.sender].privileges |
||
| 469 | allowed = channel_privs.get(trigger.nick, 0) >= level |
||
| 470 | if not trigger.is_privmsg and not allowed: |
||
| 471 | if message and not callable(message): |
||
| 472 | if reply: |
||
| 473 | bot.reply(message) |
||
| 474 | else: |
||
| 475 | bot.say(message) |
||
| 476 | else: |
||
| 477 | return function(bot, trigger, *args, **kwargs) |
||
| 478 | return guarded |
||
| 479 | return actual_decorator |
||
| 480 | |||
| 481 | |||
| 482 | View Code Duplication | def require_admin(message=None, reply=False): |
|
| 483 | """Decorate a function to require the triggering user to be a bot admin. |
||
| 484 | |||
| 485 | :param str message: optional message said to non-admin user |
||
| 486 | :param bool reply: use :meth:`~.bot.Sopel.reply` instead of |
||
| 487 | :meth:`~.bot.Sopel.say` when ``True``; defaults to |
||
| 488 | ``False`` |
||
| 489 | |||
| 490 | When the triggering user is not an admin, the command is not run, and the |
||
| 491 | bot will say the ``message`` if given. By default, it uses |
||
| 492 | :meth:`bot.say() <.bot.Sopel.say>`, but when ``reply`` is true, then it |
||
| 493 | uses :meth:`bot.reply() <.bot.Sopel.reply>` instead. |
||
| 494 | |||
| 495 | .. versionchanged:: 7.0.0 |
||
| 496 | Added the ``reply`` parameter. |
||
| 497 | """ |
||
| 498 | def actual_decorator(function): |
||
| 499 | @functools.wraps(function) |
||
| 500 | def guarded(bot, trigger, *args, **kwargs): |
||
| 501 | if not trigger.admin: |
||
| 502 | if message and not callable(message): |
||
| 503 | if reply: |
||
| 504 | bot.reply(message) |
||
| 505 | else: |
||
| 506 | bot.say(message) |
||
| 507 | else: |
||
| 508 | return function(bot, trigger, *args, **kwargs) |
||
| 509 | return guarded |
||
| 510 | |||
| 511 | # Hack to allow decorator without parens |
||
| 512 | if callable(message): |
||
| 513 | return actual_decorator(message) |
||
| 514 | |||
| 515 | return actual_decorator |
||
| 516 | |||
| 517 | |||
| 518 | View Code Duplication | def require_owner(message=None, reply=False): |
|
| 519 | """Decorate a function to require the triggering user to be the bot owner. |
||
| 520 | |||
| 521 | :param str message: optional message said to non-owner user |
||
| 522 | :param bool reply: use :meth:`~.bot.Sopel.reply` instead of |
||
| 523 | :meth:`~.bot.Sopel.say` when ``True``; defaults to |
||
| 524 | ``False`` |
||
| 525 | |||
| 526 | When the triggering user is not the bot's owner, the command is not run, |
||
| 527 | and the bot will say ``message`` if given. By default, it uses |
||
| 528 | :meth:`bot.say() <.bot.Sopel.say>`, but when ``reply`` is true, then it |
||
| 529 | uses :meth:`bot.reply() <.bot.Sopel.reply>` instead. |
||
| 530 | |||
| 531 | .. versionchanged:: 7.0.0 |
||
| 532 | Added the ``reply`` parameter. |
||
| 533 | """ |
||
| 534 | def actual_decorator(function): |
||
| 535 | @functools.wraps(function) |
||
| 536 | def guarded(bot, trigger, *args, **kwargs): |
||
| 537 | if not trigger.owner: |
||
| 538 | if message and not callable(message): |
||
| 539 | if reply: |
||
| 540 | bot.reply(message) |
||
| 541 | else: |
||
| 542 | bot.say(message) |
||
| 543 | else: |
||
| 544 | return function(bot, trigger, *args, **kwargs) |
||
| 545 | return guarded |
||
| 546 | |||
| 547 | # Hack to allow decorator without parens |
||
| 548 | if callable(message): |
||
| 549 | return actual_decorator(message) |
||
| 550 | return actual_decorator |
||
| 551 | |||
| 552 | |||
| 553 | def url(*url_rules): |
||
| 554 | """Decorate a function to handle URLs. |
||
| 555 | |||
| 556 | :param str url_rule: regex pattern to match URLs |
||
| 557 | |||
| 558 | This decorator takes a regex string that will be matched against URLs in a |
||
| 559 | message. The function it decorates, in addition to the bot and trigger, |
||
| 560 | must take a third argument ``match``, which is the regular expression match |
||
| 561 | of the URL:: |
||
| 562 | |||
| 563 | from sopel import module |
||
| 564 | |||
| 565 | @module.url(r'https://example.com/bugs/([a-z0-9]+)') |
||
| 566 | @module.url(r'https://short.com/([a-z0-9]+)') |
||
| 567 | def handle_example_bugs(bot, trigger, match): |
||
| 568 | bot.reply('Found bug ID #%s' % match.group(1)) |
||
| 569 | |||
| 570 | This should be used rather than the matching in trigger, in order to |
||
| 571 | support e.g. the ``.title`` command. |
||
| 572 | |||
| 573 | Under the hood, when Sopel collects the decorated handler it uses |
||
| 574 | :meth:`sopel.bot.Sopel.register_url_callback` to register the handler. |
||
| 575 | |||
| 576 | .. versionchanged:: 7.0 |
||
| 577 | |||
| 578 | The same function can be decorated multiple times with :func:`url` |
||
| 579 | to register different URL patterns. |
||
| 580 | |||
| 581 | .. versionchanged:: 7.0 |
||
| 582 | |||
| 583 | More than one pattern can be provided as positional argument at once. |
||
| 584 | |||
| 585 | .. seealso:: |
||
| 586 | |||
| 587 | To detect URLs, Sopel uses a matching pattern built from a list of URL |
||
| 588 | schemes, configured by |
||
| 589 | :attr:`~sopel.config.core_section.CoreSection.auto_url_schemes`. |
||
| 590 | |||
| 591 | """ |
||
| 592 | def actual_decorator(function): |
||
| 593 | if not hasattr(function, 'url_regex'): |
||
| 594 | function.url_regex = [] |
||
| 595 | for url_rule in url_rules: |
||
| 596 | url_regex = re.compile(url_rule) |
||
| 597 | if url_regex not in function.url_regex: |
||
| 598 | function.url_regex.append(url_regex) |
||
| 599 | return function |
||
| 600 | return actual_decorator |
||
| 601 | |||
| 602 | |||
| 603 | class example(object): |
||
| 604 | """Decorate a function with an example. |
||
| 605 | |||
| 606 | Args: |
||
| 607 | msg: |
||
| 608 | (required) The example command as sent by a user on IRC. If it is |
||
| 609 | a prefixed command, the command prefix used in the example must |
||
| 610 | match the default `config.core.help_prefix` for compatibility with |
||
| 611 | the built-in help module. |
||
| 612 | result: |
||
| 613 | What the example command is expected to output. If given, a test is |
||
| 614 | generated using `msg` as input. The test behavior can be modified |
||
| 615 | by the remaining optional arguments. |
||
| 616 | privmsg: |
||
| 617 | If true, the test will behave as if the input was sent to the bot |
||
| 618 | in a private message. If false (default), the test will treat the |
||
| 619 | input as having come from a channel. |
||
| 620 | admin: |
||
| 621 | Whether to treat the test message as having been sent by a bot |
||
| 622 | admin (`trigger.admin == True`). |
||
| 623 | owner: |
||
| 624 | Whether to treat the test message as having been sent by the bot's |
||
| 625 | owner (`trigger.owner == True`). |
||
| 626 | repeat: |
||
| 627 | Integer number of times to repeat the test. Useful for commands |
||
| 628 | that return random results. |
||
| 629 | re: |
||
| 630 | If true, `result` is parsed as a regular expression. Also useful |
||
| 631 | for commands that return random results, or that call an external |
||
| 632 | API that doesn't always return the same value. |
||
| 633 | ignore: |
||
| 634 | List of outputs to ignore. Strings in this list are always |
||
| 635 | interpreted as regular expressions. |
||
| 636 | user_help: |
||
| 637 | Whether this example should be displayed in user-facing help output |
||
| 638 | such as `.help command`. |
||
| 639 | online: |
||
| 640 | If true, pytest will mark it as "online". |
||
| 641 | """ |
||
| 642 | def __init__(self, msg, result=None, privmsg=False, admin=False, |
||
| 643 | owner=False, repeat=1, re=False, ignore=None, |
||
| 644 | user_help=False, online=False): |
||
| 645 | # Wrap result into a list for get_example_test |
||
| 646 | if isinstance(result, list): |
||
| 647 | self.result = result |
||
| 648 | elif result is not None: |
||
| 649 | self.result = [result] |
||
| 650 | else: |
||
| 651 | self.result = None |
||
| 652 | self.use_re = re |
||
| 653 | self.msg = msg |
||
| 654 | self.privmsg = privmsg |
||
| 655 | self.admin = admin |
||
| 656 | self.owner = owner |
||
| 657 | self.repeat = repeat |
||
| 658 | self.online = online |
||
| 659 | |||
| 660 | if isinstance(ignore, list): |
||
| 661 | self.ignore = ignore |
||
| 662 | elif ignore is not None: |
||
| 663 | self.ignore = [ignore] |
||
| 664 | else: |
||
| 665 | self.ignore = [] |
||
| 666 | |||
| 667 | self.user_help = user_help |
||
| 668 | |||
| 669 | def __call__(self, func): |
||
| 670 | if not hasattr(func, "example"): |
||
| 671 | func.example = [] |
||
| 672 | |||
| 673 | import sys |
||
| 674 | |||
| 675 | import sopel.test_tools # TODO: fix circular import with sopel.bot and sopel.test_tools |
||
| 676 | |||
| 677 | # only inject test-related stuff if we're running tests |
||
| 678 | # see https://stackoverflow.com/a/44595269/5991 |
||
| 679 | if 'pytest' in sys.modules and self.result: |
||
| 680 | # avoids doing `import pytest` and causing errors when |
||
| 681 | # dev-dependencies aren't installed |
||
| 682 | pytest = sys.modules['pytest'] |
||
| 683 | |||
| 684 | test = sopel.test_tools.get_example_test( |
||
| 685 | func, self.msg, self.result, self.privmsg, self.admin, |
||
| 686 | self.owner, self.repeat, self.use_re, self.ignore |
||
| 687 | ) |
||
| 688 | |||
| 689 | if self.online: |
||
| 690 | test = pytest.mark.online(test) |
||
| 691 | |||
| 692 | sopel.test_tools.insert_into_module( |
||
| 693 | test, func.__module__, func.__name__, 'test_example' |
||
| 694 | ) |
||
| 695 | sopel.test_tools.insert_into_module( |
||
| 696 | sopel.test_tools.get_disable_setup(), func.__module__, func.__name__, 'disable_setup' |
||
| 697 | ) |
||
| 698 | |||
| 699 | record = { |
||
| 700 | "example": self.msg, |
||
| 701 | "result": self.result, |
||
| 702 | "privmsg": self.privmsg, |
||
| 703 | "admin": self.admin, |
||
| 704 | "help": self.user_help, |
||
| 705 | } |
||
| 706 | func.example.append(record) |
||
| 707 | return func |
||
| 708 |