question about code organization issues for personal common libraries and multiple plug-ins

64 views
Skip to first unread message

HaveF HaveF

unread,
Dec 23, 2023, 9:23:39 PM12/23/23
to leo-editor
Hi, Leo Lovers,

Upon your suggestions to learn more about Leo, I examined the LeoPyRef file, and learned the g.pdb() method for an in-depth exploration, which was great. I like it!

I now have an idea and seek your input. I realized that in various Leo files, I frequently employ same specific functions such as fun1, fun2, and so on.

Investigating further, I discovered a plugin that could help me with that. The steps are as follows: 

1. Develop a plugin named personal_common_functions and incorporate these functions into it.
2. Once the plugin is developed, activate it.
3. Create script within a Leo node, similar to the example below:

```
fun1 = g.getPluginModule('leo.plugins.personal_common_functions').fun1
fun2 = g.getPluginModule('leo.plugins.personal_common_functions').fun2

...
... utilize fun1 and fun2 ...
...
```

Execute the script by pressing Ctrl-B. This addresses the initial issue.

However, a new challenge arises: if I have multiple plugins, such as plugin1 and plugin2, and they both rely on the personal_common_functions plugin, is it possible to activate the personal_common_functions plugin and call fun1 within plugin1 and plugin2 using g.getPluginModule('leo.plugins.personal_common_functions').fun1?

is this the best practice for code organization?

Thanks!

 

Thomas Passin

unread,
Dec 23, 2023, 10:37:39 PM12/23/23
to leo-editor
It sounds more complex than necessary to me.  I'm not sure what kind of functions you have in mind.  But there are several ways to attach functions globally.  One way is that g has a user dict, whose name I forget right now.  Let's call it g.userdict, though that's probably not its real name. Then you can write a script the assigns

def func1():
    # body of function

g.userdict['func1') = func1
# etc

Or you can just assign functions to g itself:
g.func1 = func1

For example, my mind map commands apply a monkey patch to c.  The code that constructs a mind map checks to see if the monkey patch has been applied and if not it applies it.  This look like:

# Check if we have not been run before or if the rendering
# device has been changed
_render_dev = c.config.getString('mmap_render_dev')
if not hasattr(c, 'render_dev') or c.render_dev != _render_dev:
    # Install our patch
    c.executeMinibufferCommand('mmap-monkeypatch')
    c.render_dev = _render_dev

c.build_mmap(data)


In the monkey patch subtree:

"""This command adds a function to the outline commander c that is
used by the top-level mind mapping commands.  This function
generates the svg output for a mindmap and renders it in
the specified rendering pane or program.  The rendering
"device" is specified by the setting "mmap_render_dev".
"""
@command mmap-monkeypatch
# ...
def build_mmap(data):
    """Parse data and build mindmap."""
    data = data.replace('&', '+').replace('<', ' ')
    svg = create_map(IndentedParser, data)

    find_or_create_target(c, TARGET_NODE, svg)
    render_with_device(svg)

    # Let others know body has changed.
    editor = c.frame.body.wrapper.widget
    doc = editor.document()
    doc.setModified(True)

c.build_mmap = build_mmap

You can put all these function definitions in a node or nodes and install them by running the node with CTRL-b.  Or if there are so many that you want to put them in a module, you can import them from the module, but it doesn't have to be a plugin.  You could put it into, for example, ~/.leo/functions.  You can add that location to the Python system path, then import your function definition module from there.

So there are many possibilities for adding your own functions and command that can be stored right in subtrees in myLeoSettings.leo, or in a module, without even needing a plugin.  Or a plugin could use the same techniques to install your functions. If you want to write them as Leo minibuffer commands, you can create menu items for them, so that you can access them from Leo's menubar (you do that in myLeoSettings.leo, or in particular outlines).

Just look for the simplest thing that could work, and go on from there.   I have all the code to run my browser bookmarks system right in a subtree in the outline that stores the bookmarks themselves. I prototyped the code to highlight the current line, and  the code to draw a right margin line, in subtrees in the workbook.  Here's how the highlighting code gets installed (in the prototype;  the highlighter code that Leo actually uses was derived from this but is part of the core code so it doesn't need to be installed each time Leo is run):

class Hiliter:
    def __init__(self, cc):
        self.c = cc
        w = cc.frame.body.wrapper
        self.editor = w.widget
    # .....

# Install the highlighter in all commanders
for cc in g.app.commanders():
    cc.hiliter = Hiliter(cc)
    cc.hiliter.editor.cursorPositionChanged.connect(cc.hiliter.highlightCurrentLine)


Presto!  All outlines now have current line highlighting installed.

HaveF HaveF

unread,
Dec 23, 2023, 11:12:26 PM12/23/23
to leo-e...@googlegroups.com
You can put all these function definitions in a node or nodes and install them by running the node with CTRL-b.  Or if there are so many that you want to put them in a module, you can import them from the module, but it doesn't have to be a plugin.  You could put it into, for example, ~/.leo/functions.  You can add that location to the Python system path, then import your function definition module from there.

So there are many possibilities for adding your own functions and command that can be stored right in subtrees in myLeoSettings.leo, or in a module, without even needing a plugin.  Or a plugin could use the same techniques to install your functions. If you want to write them as Leo minibuffer commands, you can create menu items for them, so that you can access them from Leo's menubar (you do that in myLeoSettings.leo, or in particular outlines).

Hi, Thomas, Thanks again for your detailed advice!
 
For now, there is nothing special for my personal_common_functions. Just some wraps like

def get_clipboard():
return g.app.gui.getTextFromClipboard()

Or some stupid hard codes(like, I used hard code when ask API key...Ocp-Apim-Subscription-Key):

def translate_text(text, to_lang='zh-Hans', from_lang='en'):
# Declare httpx client
client = httpx.Client(proxies=os.getenv("https_proxy"))
# Define the Translator Text API endpoint

# Add required headers to API request
headers = {
'Ocp-Apim-Subscription-Key': 'simple hard code',
'Ocp-Apim-Subscription-Region': 'eastasia',
'Content-type': 'application/json',
'X-ClientTraceId': str(uuid.uuid4())
}

# Add required parameters to API request
params = {
'api-version': '3.0',
'from': from_lang,
'to': to_lang
}

# Package the text to be translated into a JSON object
body = [{'text' : text}]

# Make the API request
response = client.post(f"{endpoint}/translate", headers=headers, params=params, json=body)

# Check the status of the request
if response.status_code == 200:
# On success, return the translated text
return response.json()[0]['translations'][0]['text']
else:
# On failure, return a message indicating the error
return f"Translation request failed with status code {response.status_code}"

Yes, I know I can put them in myLeoSettings.leo or ~/.leo/functions, but I plan to refine them or add tests in future. So I think plugin is the right place?(correct me if I'm wrong)

If my plugins and some leo node scripts rely on these personal_common_functions, I believe putting them all to the plugin system is a reasonable choice? Maybe I'm wrong.

 
class Hiliter:
    def __init__(self, cc):
        self.c = cc
        w = cc.frame.body.wrapper
        self.editor = w.widget
    # .....

# Install the highlighter in all commanders
for cc in g.app.commanders():
    cc.hiliter = Hiliter(cc)
    cc.hiliter.editor.cursorPositionChanged.connect(cc.hiliter.highlightCurrentLine)
an interesting function!

 
--
--
Sincerely,

HaveF

Thomas Passin

unread,
Dec 24, 2023, 12:25:47 AM12/24/23
to leo-editor
I don't have a large objection to creating a plugin.  One thing you will have to decide is where the plugin should live.  You don't want it to be in the Leo codebase since it's personal.  Leo will load plugins from  sys.path if you list them in the @enabled-plugins setting if you give the module name without the ".py" extension.  So you have to get your plugin on sys.path.  Here is how I do it (using Windows paths).  In Lib.sitepackages I place a .pth file, custom.pth. This file walks up the the Python directory in %APPDATA%\Python:

# Look in AppData\Roaming\Python for other files and packages.
..\..\

In this Python directory I created pycustomize\leo. Now I can import a module from that directory:

from pycustomize.leo import my_plugin

I decided to set up the location of these files (I have some others in pycustomizeI) like this so they wouldn't need to be copied to each new Python install.  I only need to remember to copy custom.pth to the new install.

I recently wrote a tiny test plugin that can install scripts and functions (see, I've just been teaching myself how to do the same things you are asking about).  In this case I was monkeypatching the g.openUNLFile() method. Here's its contents:

"""A Leo plugin to run scripts at startup.

Methods changed by this plugin:
    - g.openUNLFile()
"""
import leo.core.leoGlobals as g

def new_open_unl(c, s):
    print('the new openUNLFile', s, c)
    return g.old_openUNLFile(c, s)

def onCreate(tag, keywords):
    pass
def init():
    """Return True if the plugin has loaded successfully."""
    if g.unitTesting:
        return False
    print('hello from the monkey-patcher')
    # Register the handlers...
    # g.registerHandler('after-create-leo-frame', onCreate)
    g.old_openUNLFile = g.openUNLFile
    g.openUNLFile = new_open_unl

    g.plugin_signon(__name__)
    return True


HaveF HaveF

unread,
Dec 24, 2023, 12:35:02 AM12/24/23
to leo-e...@googlegroups.com
One thing you will have to decide is where the plugin should live.  You don't want it to be in the Leo codebase since it's personal.  Leo will load plugins from  sys.path if you list them in the @enabled-plugins setting if you give the module name without the ".py" extension.  So you have to get your plugin on sys.path.  Here is how I do it (using Windows paths).  In Lib.sitepackages I place a .pth file, custom.pth. This file walks up the the Python directory in %APPDATA%\Python:

# Look in AppData\Roaming\Python for other files and packages.
..\..\

In this Python directory I created pycustomize\leo. Now I can import a module from that directory:

from pycustomize.leo import my_plugin

I decided to set up the location of these files (I have some others in pycustomizeI) like this so they wouldn't need to be copied to each new Python install.  I only need to remember to copy custom.pth to the new install.

 Nice tips. Thanks, Thomas!

Edward K. Ream

unread,
Dec 24, 2023, 5:50:42 AM12/24/23
to leo-e...@googlegroups.com
On Sat, Dec 23, 2023 at 8:23 PM HaveF HaveF <iamap...@gmail.com> wrote:

> I frequently employ the same specific functions such as fun1, fun2, and so on.

Here is the easy (and flexible!) way:

- Define a shared @command f1 node in your myLeoSetting.leo for your function f1.
- Execute the function with: c.doCommandByName("f1").

Yes, a custom plugin can inject functions into the 'g' namespace.

I suggest using a prefix (like havef_) to ensure the new functions can't conflict with existing global functions. The plugin would be something like this (not tested):

from leo.core import leoGlobals as g

<< define your functions >>

def init() -> bool:
    g.havef_f1 = f1
    g.havef_f2 = f2

HTH.

Edward

HaveF HaveF

unread,
Dec 24, 2023, 7:05:11 AM12/24/23
to leo-editor
Here is the easy (and flexible!) way:

- Define a shared @command f1 node in your myLeoSetting.leo for your function f1.
- Execute the function with: c.doCommandByName("f1").

I'm going to do a bit of a balance between plugins or the way which you said.

For now, Thomas's approach to 'place a .pth file in Lib.sitepackages' and plugins way seems the most plausible if I consider writing some future tests for some of my custom functions.

Thank you, Thomas and Edward, for your suggestions.


The best thing about Leo is that if you want add some simple functions, just use @command F1 node is enough.

 

Thomas Passin

unread,
Dec 24, 2023, 8:11:46 AM12/24/23
to leo-editor
On Sunday, December 24, 2023 at 5:50:42 AM UTC-5 Edward K. Ream wrote:

- Define a shared @command f1 node in your myLeoSetting.leo for your function f1.
- Execute the function with: c.doCommandByName("f1").

That's how most of my custom things work. 
Reply all
Reply to author
Forward
0 new messages