Skip to content

Commit 66ed6d5

Browse files
committed
Merge branch 'smart-plugins-enhancements' into develop
2 parents 6d7d7f6 + c8e8fe0 commit 66ed6d5

2 files changed

Lines changed: 370 additions & 50 deletions

File tree

docs/source/resources/SmartPlugins.rst

Lines changed: 250 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
Smart Plugins
22
=============
33

4-
Pyrogram embeds a **smart** (automatic) and lightweight plugin system that is meant to further simplify the organization
5-
of large projects and to provide a way for creating pluggable components that can be **easily shared** across different
6-
Pyrogram applications with **minimal boilerplate code**.
4+
Pyrogram embeds a **smart**, lightweight yet powerful plugin system that is meant to further simplify the organization
5+
of large projects and to provide a way for creating pluggable (modular) components that can be **easily shared** across
6+
different Pyrogram applications with **minimal boilerplate code**.
77

88
.. tip::
99

@@ -13,7 +13,8 @@ Introduction
1313
------------
1414

1515
Prior to the Smart Plugin system, pluggable handlers were already possible. For example, if you wanted to modularize
16-
your applications, you had to do something like this...
16+
your applications, you had to put your function definitions in separate files and register them inside your main script,
17+
like this:
1718

1819
.. note::
1920

@@ -63,19 +64,19 @@ your applications, you had to do something like this...
6364
6465
app.run()
6566
66-
...which is already nice and doesn't add *too much* boilerplate code, but things can get boring still; you have to
67+
This is already nice and doesn't add *too much* boilerplate code, but things can get boring still; you have to
6768
manually ``import``, manually :meth:`add_handler <pyrogram.Client.add_handler>` and manually instantiate each
6869
:obj:`MessageHandler <pyrogram.MessageHandler>` object because **you can't use those cool decorators** for your
69-
functions. So... What if you could?
70+
functions. So, what if you could? Smart Plugins solve this issue by taking care of handlers registration automatically.
7071

7172
Using Smart Plugins
7273
-------------------
7374

74-
Setting up your Pyrogram project to accommodate Smart Plugins is pretty straightforward:
75+
Setting up your Pyrogram project to accommodate Smart Plugins is straightforward:
7576

76-
#. Create a new folder to store all the plugins (e.g.: "plugins").
77-
#. Put your files full of plugins inside.
78-
#. Enable plugins in your Client.
77+
#. Create a new folder to store all the plugins (e.g.: "plugins", "handlers", ...).
78+
#. Put your python files full of plugins inside. Organize them as you wish.
79+
#. Enable plugins in your Client or via the *config.ini* file.
7980

8081
.. note::
8182

@@ -107,20 +108,252 @@ Setting up your Pyrogram project to accommodate Smart Plugins is pretty straight
107108
def echo_reversed(client, message):
108109
message.reply(message.text[::-1])
109110
111+
- ``config.ini``
112+
113+
.. code-block:: ini
114+
115+
[plugins]
116+
root = plugins
117+
110118
- ``main.py``
111119

112120
.. code-block:: python
113121
114122
from pyrogram import Client
115123
116-
Client("my_account", plugins_dir="plugins").run()
124+
Client("my_account").run()
125+
126+
Alternatively, without using the *config.ini* file:
117127

118-
The first important thing to note is the new ``plugins`` folder, whose name is passed to the the ``plugins_dir``
119-
parameter when creating a :obj:`Client <pyrogram.Client>` in the ``main.py`` file — you can put *any python file* in
120-
there and each file can contain *any decorated function* (handlers) with only one limitation: within a single plugin
121-
file you must use different names for each decorated function. Your Pyrogram Client instance will **automatically**
122-
scan the folder upon creation to search for valid handlers and register them for you.
128+
.. code-block:: python
129+
130+
from pyrogram import Client
131+
132+
plugins = dict(
133+
root="plugins"
134+
)
135+
136+
Client("my_account", plugins=plugins).run()
137+
138+
The first important thing to note is the new ``plugins`` folder. You can put *any python file* in *any subfolder* and
139+
each file can contain *any decorated function* (handlers) with one limitation: within a single module (file) you must
140+
use different names for each decorated function.
141+
142+
The second thing is telling Pyrogram where to look for your plugins: you can either use the *config.ini* file or
143+
the Client parameter "plugins"; the *root* value must match the name of your plugins folder. Your Pyrogram Client
144+
instance will **automatically** scan the folder upon starting to search for valid handlers and register them for you.
123145

124146
Then you'll notice you can now use decorators. That's right, you can apply the usual decorators to your callback
125147
functions in a static way, i.e. **without having the Client instance around**: simply use ``@Client`` (Client class)
126-
instead of the usual ``@app`` (Client instance) namespace and things will work just the same.
148+
instead of the usual ``@app`` (Client instance) and things will work just the same.
149+
150+
Specifying the Plugins to include
151+
---------------------------------
152+
153+
By default, if you don't explicitly supply a list of plugins, every valid one found inside your plugins root folder will
154+
be included by following the alphabetical order of the directory structure (files and subfolders); the single handlers
155+
found inside each module will be, instead, loaded in the order they are defined, from top to bottom.
156+
157+
.. note::
158+
159+
Remember: there can be at most one handler, within a group, dealing with a specific update. Plugins with overlapping
160+
filters included a second time will not work. Learn more at `More on Updates <MoreOnUpdates.html>`_.
161+
162+
This default loading behaviour is usually enough, but sometimes you want to have more control on what to include (or
163+
exclude) and in which exact order to load plugins. The way to do this is to make use of ``include`` and ``exclude``
164+
keys, either in the *config.ini* file or in the dictionary passed as Client argument. Here's how they work:
165+
166+
- If both ``include`` and ``exclude`` are omitted, all plugins are loaded as described above.
167+
- If ``include`` is given, only the specified plugins will be loaded, in the order they are passed.
168+
- If ``exclude`` is given, the plugins specified here will be unloaded.
169+
170+
The ``include`` and ``exclude`` value is a **list of strings**. Each string containing the path of the module relative
171+
to the plugins root folder, in Python notation (dots instead of slashes).
172+
173+
E.g.: ``subfolder.module`` refers to ``plugins/subfolder/module.py``, with ``root="plugins"`.
174+
175+
You can also choose the order in which the single handlers inside a module are loaded, thus overriding the default
176+
top-to-bottom loading policy. You can do this by appending the name of the functions to the module path, each one
177+
separated by a blank space.
178+
179+
E.g.: ``subfolder.module fn2 fn1 fn3`` will load *fn2*, *fn1* and *fn3* from *subfolder.module*, in this order.
180+
181+
Examples
182+
^^^^^^^^
183+
184+
Given this plugins folder structure with three modules, each containing their own handlers (fn1, fn2, etc...), which are
185+
also organized in subfolders:
186+
187+
.. code-block:: text
188+
189+
myproject/
190+
plugins/
191+
subfolder1/
192+
plugins1.py
193+
- fn1
194+
- fn2
195+
- fn3
196+
subfolder2/
197+
plugins2.py
198+
...
199+
plugins0.py
200+
...
201+
...
202+
203+
- Load every handler from every module, namely *plugins0.py*, *plugins1.py* and *plugins2.py* in alphabetical order
204+
(files) and definition order (handlers inside files):
205+
206+
Using *config.ini* file:
207+
208+
.. code-block:: ini
209+
210+
[plugins]
211+
root = plugins
212+
213+
Using *Client*'s parameter:
214+
215+
.. code-block:: python
216+
217+
plugins = dict(
218+
root="plugins"
219+
)
220+
221+
Client("my_account", plugins=plugins).run()
222+
223+
- Load only handlers defined inside *plugins2.py* and *plugins0.py*, in this order:
224+
225+
Using *config.ini* file:
226+
227+
.. code-block:: ini
228+
229+
[plugins]
230+
root = plugins
231+
include =
232+
subfolder2.plugins2
233+
plugins0
234+
235+
Using *Client*'s parameter:
236+
237+
.. code-block:: python
238+
239+
plugins = dict(
240+
root="plugins",
241+
include=[
242+
"subfolder2.plugins2",
243+
"plugins0"
244+
]
245+
)
246+
247+
Client("my_account", plugins=plugins).run()
248+
249+
- Load everything except the handlers inside *plugins2.py*:
250+
251+
Using *config.ini* file:
252+
253+
.. code-block:: ini
254+
255+
[plugins]
256+
root = plugins
257+
exclude = subfolder2.plugins2
258+
259+
Using *Client*'s parameter:
260+
261+
.. code-block:: python
262+
263+
plugins = dict(
264+
root="plugins",
265+
exclude=["subfolder2.plugins2"]
266+
)
267+
268+
Client("my_account", plugins=plugins).run()
269+
270+
- Load only *fn3*, *fn1* and *fn2* (in this order) from *plugins1.py*:
271+
272+
Using *config.ini* file:
273+
274+
.. code-block:: ini
275+
276+
[plugins]
277+
root = plugins
278+
include = subfolder1.plugins1 fn3 fn1 fn2
279+
280+
Using *Client*'s parameter:
281+
282+
.. code-block:: python
283+
284+
plugins = dict(
285+
root="plugins",
286+
include=["subfolder1.plugins1 fn3 fn1 fn2"]
287+
)
288+
289+
Client("my_account", plugins=plugins).run()
290+
291+
Load/Unload Plugins at Runtime
292+
------------------------------
293+
294+
In the `previous section <#specifying-the-plugins-to-include>`_ we've explained how to specify which plugins to load and
295+
which to ignore before your Client starts. Here we'll show, instead, how to unload and load again a previously
296+
registered plugins at runtime.
297+
298+
Each function decorated with the usual ``on_message`` decorator (or any other decorator that deals with Telegram updates
299+
) will be modified in such a way that, when you reference them later on, they will be actually pointing to a tuple of
300+
*(handler: Handler, group: int)*. The actual callback function is therefore stored inside the handler's *callback*
301+
attribute. Here's an example:
302+
303+
- ``plugins/handlers.py``
304+
305+
.. code-block:: python
306+
:emphasize-lines: 5, 6
307+
308+
@Client.on_message(Filters.text & Filters.private)
309+
def echo(client, message):
310+
message.reply(message.text)
311+
312+
print(echo)
313+
print(echo[0].callback)
314+
315+
- Printing ``echo`` will show something like ``(<MessageHandler object at 0x10e3abc50>, 0)``.
316+
317+
- Printing ``echo[0].callback``, that is, the *callback* attribute of the first eleent of the tuple, which is an
318+
Handler, will reveal the actual callback ``<function echo at 0x10e3b6598>``.
319+
320+
Unloading
321+
^^^^^^^^^
322+
323+
In order to unload a plugin, or any other handler, all you need to do is obtain a reference to it (by importing the
324+
relevant module) and call :meth:`remove_handler <pyrogram.Client.remove_handler>` Client's method with your function
325+
name preceded by the star ``*`` operator as argument. Example:
326+
327+
- ``main.py``
328+
329+
.. code-block:: python
330+
331+
from plugins.handlers import echo
332+
333+
...
334+
335+
app.remove_handler(*echo)
336+
337+
The star ``*`` operator is used to unpack the tuple into positional arguments so that *remove_handler* will receive
338+
exactly what is needed. The same could have been achieved with:
339+
340+
.. code-block:: python
341+
342+
handler, group = echo
343+
app.remove_handler(handler, group)
344+
345+
Loading
346+
^^^^^^^
347+
348+
Similarly to the unloading process, in order to load again a previously unloaded plugin you do the same, but this time
349+
using :meth:`add_handler <pyrogram.Client.add_handler>` instead. Example:
350+
351+
- ``main.py``
352+
353+
.. code-block:: python
354+
355+
from plugins.handlers import echo
356+
357+
...
358+
359+
app.add_handler(*echo)

0 commit comments

Comments
 (0)