Skip to content

Commit 446ffd1

Browse files
author
Pietro Albini
committed
Merge branch 'feature/better-commands'
2 parents 8ac4aba + 70d88e9 commit 446ffd1

File tree

14 files changed

+342
-143
lines changed

14 files changed

+342
-143
lines changed

botogram/bot.py

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ def __init__(self, api_connection):
3333

3434
self.about = ""
3535
self.owner = ""
36-
self.hide_commands = ["start"]
3736

3837
self.before_help = []
3938
self.after_help = []
@@ -43,6 +42,9 @@ def __init__(self, api_connection):
4342
self._lang = ""
4443
self._lang_inst = None
4544

45+
# Support for the old, deprecated bot.hide_commands
46+
self._hide_commands = []
47+
4648
# Set the default language to english
4749
self.lang = "en"
4850

@@ -128,10 +130,11 @@ def __(func):
128130
return func
129131
return __
130132

131-
def command(self, name):
133+
def command(self, name, hidden=False):
132134
"""Register a new command"""
133135
def __(func):
134-
self._main_component.add_command(name, func, _from_main=True)
136+
self._main_component.add_command(name, func, hidden,
137+
_from_main=True)
135138
return func
136139
return __
137140

@@ -193,17 +196,11 @@ def freeze(self):
193196
chains = components.merge_chains(self._main_component,
194197
*self._components)
195198

196-
# Get the list of commands for the bot
197-
commands = self._components[-1]._get_commands()
198-
for component in reversed(self._components[:-1]):
199-
commands.update(component._get_commands())
200-
commands.update(self._main_component._get_commands())
201-
202199
return frozenbot.FrozenBot(self.api, self.about, self.owner,
203-
self.hide_commands, self.before_help,
200+
self._hide_commands, self.before_help,
204201
self.after_help, self.process_backlog,
205202
self.lang, self.itself, self._commands_re,
206-
commands, chains, self._scheduler,
203+
self._commands, chains, self._scheduler,
207204
self._main_component._component_id,
208205
self._bot_id, self._shared_memory)
209206

@@ -220,15 +217,35 @@ def lang(self, lang):
220217
self._lang_inst = utils.get_language(lang)
221218
self._lang = lang
222219

223-
def _get_commands(self):
220+
@property
221+
def _commands(self):
224222
"""Get all the commands this bot implements"""
225-
result = {}
226-
for component in self._components:
227-
result.update(component._get_commands())
228-
result.update(self._main_component._get_commands())
223+
# This is marked as a property so the available_commands method becomes
224+
# dynamic (it's static on FrozenBot instead)
225+
commands = self._components[-1]._get_commands()
226+
for component in reversed(self._components[:-1]):
227+
commands.update(component._get_commands())
228+
commands.update(self._main_component._get_commands())
229229

230+
result = {}
231+
for name, command in commands.items():
232+
result[name] = command.for_bot(self)
230233
return result
231234

235+
# These functions allows to use the old, deprecated bot.hide_commands
236+
237+
@property
238+
@utils.deprecated("bot.hide_commands", "1.0", "Use @bot.command(\"name\", "
239+
"hidden=True) instead")
240+
def hide_commands(self):
241+
return self._hide_commands
242+
243+
@hide_commands.setter
244+
@utils.deprecated("bot.hide_commands", "1.0", "Use @bot.command(\"name\", "
245+
"hidden=True) instead", back=1)
246+
def hide_commands(self, value):
247+
self._hide_commands = value
248+
232249

233250
def create(api_key, *args, **kwargs):
234251
"""Create a new bot"""

botogram/commands.py

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
"""
2+
botogram.commands
3+
Core logic for commands
4+
5+
Copyright (c) 2016 Pietro Albini <[email protected]>
6+
Released under the MIT license
7+
"""
8+
9+
10+
class Command:
11+
"""Representation of a single command"""
12+
13+
def __init__(self, hook, _bot=None):
14+
# Get some parameters from the hook
15+
self.name = hook._name
16+
self.hidden = hook._hidden
17+
18+
self._hook = hook
19+
self._component_id = hook.component_id
20+
21+
self._bot = _bot
22+
23+
def __reduce__(self):
24+
return rebuild_command, (self._hook,)
25+
26+
def for_bot(self, bot):
27+
"""Get the command instance for a specific bot"""
28+
return self.__class__(self._hook, _bot=bot)
29+
30+
@property
31+
def raw_docstring(self):
32+
"""Get the raw docstring of this command"""
33+
func = self._hook.func
34+
35+
if hasattr(func, "_botogram_help_message"):
36+
if self._bot is not None:
37+
return self._bot._call(func._botogram_help_message,
38+
self._component_id)
39+
else:
40+
return func._botogram_help_message()
41+
elif func.__doc__:
42+
return func.__doc__
43+
44+
return
45+
46+
@property
47+
def docstring(self):
48+
"""Get the docstring of this command"""
49+
docstring = self.raw_docstring
50+
if docstring is None:
51+
return
52+
53+
result = []
54+
for line in self.raw_docstring.split("\n"):
55+
# Remove leading whitespaces
56+
line = line.strip()
57+
58+
# Allow only a single blackline
59+
if line == "" and len(result) and result[-1] == "":
60+
continue
61+
62+
result.append(line)
63+
64+
# Remove empty lines at the end or at the start of the docstring
65+
for pos in 0, -1:
66+
if result[pos] == "":
67+
result.pop(pos)
68+
69+
return "\n".join(result)
70+
71+
@property
72+
def summary(self):
73+
"""Get a summary of the command"""
74+
docstring = self.docstring
75+
if docstring is None:
76+
return
77+
78+
return docstring.split("\n", 1)[0]
79+
80+
81+
def rebuild_command(hook):
82+
"""Rebuild a Command after being pickled"""
83+
return Command(hook)

botogram/components.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from . import utils
1212
from . import tasks
1313
from . import hooks
14+
from . import commands
1415

1516

1617
class Component:
@@ -95,7 +96,7 @@ def add_message_matches_hook(self, regex, func, flags=0, multiple=False):
9596
})
9697
self.__processors.append(hook)
9798

98-
def add_command(self, name, func, _from_main=False):
99+
def add_command(self, name, func, hidden=False, _from_main=False):
99100
"""Register a new command"""
100101
if name in self.__commands:
101102
raise NameError("The command /%s already exists" % name)
@@ -110,8 +111,10 @@ def add_command(self, name, func, _from_main=False):
110111

111112
hook = hooks.CommandHook(func, self, {
112113
"name": name,
114+
"hidden": hidden,
113115
})
114-
self.__commands[name] = hook
116+
command = commands.Command(hook)
117+
self.__commands[name] = command
115118

116119
def add_timer(self, interval, func):
117120
"""Register a new timer"""
@@ -157,7 +160,8 @@ def _get_chains(self):
157160
"""Get the full hooks chain for this component"""
158161
messages = [
159162
self.__before_processors[:],
160-
[self.__commands[name] for name in sorted(self.__commands.keys())],
163+
[self.__commands[name]._hook
164+
for name in sorted(self.__commands.keys())],
161165
self.__no_commands[:],
162166
self.__processors[:],
163167
]

botogram/defaults.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
Released under the MIT license
77
"""
88

9-
from . import utils
109
from . import components
1110
from . import decorators
1211

@@ -17,7 +16,7 @@ class DefaultComponent(components.Component):
1716
component_name = "botogram"
1817

1918
def __init__(self):
20-
self.add_command("start", self.start_command)
19+
self.add_command("start", self.start_command, hidden=True)
2120
self.add_command("help", self.help_command)
2221

2322
self._add_no_commands_hook(self.no_commands_hook)
@@ -43,7 +42,7 @@ def _start_command_help(bot):
4342
# /help command
4443

4544
def help_command(self, bot, chat, args):
46-
commands = bot._get_commands()
45+
commands = {cmd.name: cmd for cmd in bot.available_commands()}
4746
if len(args) > 1:
4847
message = [bot._("<b>Error!</b> The <code>/help</code> command "
4948
"allows up to one argument.")]
@@ -77,14 +76,10 @@ def _help_generic_message(self, bot, commands):
7776
if len(commands) > 0:
7877
message.append(bot._("<b>This bot supports those commands:</b>"))
7978
for name in sorted(commands.keys()):
80-
# Allow to hide commands in the help message
81-
if name in bot.hide_commands:
82-
continue
83-
84-
func = commands[name]
85-
docstring = utils.docstring_of(func, bot, format=True) \
86-
.split("\n", 1)[0]
87-
message.append("/%s <code>-</code> %s" % (name, docstring))
79+
summary = commands[name].summary
80+
if summary is None:
81+
summary = "<i>%s</i>" % bot._("No description available.")
82+
message.append("/%s <code>-</code> %s" % (name, summary))
8883
message.append("")
8984
message.append(bot._("You can also use <code>/help &lt;command&gt;"
9085
"</code> to get help about a specific "
@@ -109,8 +104,9 @@ def _help_command_message(self, bot, commands, command):
109104
"""Generate a command's help message"""
110105
message = []
111106

112-
func = commands[command]
113-
docstring = utils.docstring_of(func, bot, format=True)
107+
docstring = commands[command].docstring
108+
if docstring is None:
109+
docstring = "<i>%s</i>" % bot._("No description available.")
114110
message.append("/%s <code>-</code> %s" % (command, docstring))
115111

116112
# Show the owner informations

botogram/frozenbot.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def __init__(self, api, about, owner, hide_commands, before_help,
3232
self.api = api
3333
self.about = about
3434
self.owner = owner
35-
self.hide_commands = hide_commands
35+
self._hide_commands = hide_commands
3636
self.before_help = before_help
3737
self.after_help = after_help
3838
self.process_backlog = process_backlog
@@ -43,7 +43,8 @@ def __init__(self, api, about, owner, hide_commands, before_help,
4343
self._shared_memory = shared_memory
4444
self._scheduler = scheduler
4545
self._chains = chains
46-
self._commands = commands
46+
self._commands = {name: command.for_bot(self)
47+
for name, command in commands.items()}
4748

4849
# Setup the logger
4950
self.logger = logbook.Logger('botogram bot')
@@ -60,7 +61,7 @@ def __init__(self, api, about, owner, hide_commands, before_help,
6061

6162
def __reduce__(self):
6263
args = (
63-
self.api, self.about, self.owner, self.hide_commands,
64+
self.api, self.about, self.owner, self._hide_commands,
6465
self.before_help, self.after_help, self.process_backlog,
6566
self.lang, self.itself, self._commands_re, self._commands,
6667
self._chains, self._scheduler, self._main_component_id,
@@ -104,7 +105,7 @@ def message_matches(self, regex, flags=0, multiple=False):
104105
"""Add a message matches hook"""
105106
raise FrozenBotError("Can't add hooks to a bot at runtime")
106107

107-
def command(self, name):
108+
def command(self, name, hidden=False):
108109
"""Register a new command"""
109110
raise FrozenBotError("Can't add commands to a bot at runtime")
110111

@@ -222,9 +223,13 @@ def _(self, message, **args):
222223

223224
# And some internal methods used by botogram
224225

225-
def _get_commands(self):
226-
"""Get all the commands this bot implements"""
227-
return self._commands
226+
def available_commands(self, all=False):
227+
"""Get a list of the commands this bot implements"""
228+
for command in self._commands.values():
229+
# Remove `or command.name in self.hide_commands` in botogram 1.0
230+
is_hidden = command.hidden or command.name in self._hide_commands
231+
if all or not is_hidden:
232+
yield command
228233

229234
def _call(self, func, component=None, **available):
230235
"""Wrapper for calling user-provided functions"""
@@ -241,6 +246,14 @@ def lazy_shared():
241246

242247
return utils.call(func, **available)
243248

249+
# This function allows to use the old, deprecated bot.hide_commands
250+
251+
@utils.deprecated("bot.hide_commands", "1.0", "Use @bot.command(\"name\", "
252+
"hidden=True) instead")
253+
@property
254+
def hide_commands(self):
255+
return self._hide_commands
256+
244257

245258
# Those are shortcuts to send messages directly to someone
246259
# Create dynamic methods for each of the send methods. They're *really*

botogram/hooks.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,11 @@ def _after_init(self, args):
170170
self._regex = re.compile(r'^\/' + args["name"] + r'(@[a-zA-Z0-9_]+)?'
171171
r'( .*)?$')
172172

173+
self._name = args["name"]
174+
self._hidden = False
175+
if "hidden" in args:
176+
self._hidden = args["hidden"]
177+
173178
def _call(self, bot, update):
174179
message = update.message
175180
text = message.text.replace("\n", " ").replace("\t", " ")

0 commit comments

Comments
 (0)