Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ., ./lib, ./plugin directories to path for Python plugins #3110

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from

Conversation

Yusyuriv
Copy link
Member

@Yusyuriv Yusyuriv commented Dec 5, 2024

The problem

Currently, due to Flow Launcher using embedded Python by default, plugin directory doesn't get added to path. This causes inconvenience for plugin developers. The plugin development documentation for Python suggests using ./lib for installing dependencies and ./plugin for actual plugin code, but developers can't import anything from there because these directories are not in Python's path. They have to add them to path manually in every plugin before they can start importing their code. For example, FlowYouTube plugin:

import sys
import os

plugindir = os.path.abspath(os.path.dirname(__file__))
sys.path.append(plugindir)
sys.path.append(os.path.join(plugindir, "lib"))
sys.path.append(os.path.join(plugindir, "plugin"))

# Only now you can import and use your actual code
from plugin.main import FlowYouTube

The solution

We can't simply add these paths to PYTHONPATH env variable before starting the plugin because we're using embedded Python. It ignores this variable. Flow Launcher users also might be using their own Python installations, so we can't use any solution that would apply only to our embedded Python. So the easiest solution I came up with was instead of launching the files directly (like python path/to/file.py), run some code that Flow Launcher generates, adding these paths to Python's path, and only then run the actual plugin file via runpy:

StartInfo.ArgumentList.Add("-c");
StartInfo.ArgumentList.Add(
    $"""
     import sys
     sys.path.append(r'{rootDirectory}')
     sys.path.append(r'{libDirectory}')
     sys.path.append(r'{pluginDirectory}')
     
     import runpy
     runpy.run_path(r'{filePath}', None, '__main__')
     """
);

Backwards compatibility

I tested this change with several plugins, with them still manually adding directories to path and with them simply importing things. From my tests it seemed like they all continued working normally. Worst case scenario I can see is that either:

  • a plugin will have non-existent directories added to path, which shouldn't have any effect
  • or a plugin will have the same directories added to path twice, which also shouldn't have any effect

Now, to the plugins I tried running. I tested the listed plugins before removing the code marked red in the code blocks and after. They all continued working like before.

v1

GitHub Quick Launcher

No changes here, it's released as a .pyz file.

FlowYouTube

- import sys
- import os

- plugindir = os.path.abspath(os.path.dirname(__file__))
- sys.path.append(plugindir)
- sys.path.append(os.path.join(plugindir, "lib"))
- sys.path.append(os.path.join(plugindir, "plugin"))

from plugin.main import FlowYouTube

if __name__ == "__main__":
    FlowYouTube()

Search-MDI

# -*- coding: utf-8 -*-
- import sys
- import os

- plugindir = os.path.abspath(os.path.dirname(__file__))
- sys.path.append(plugindir)
- sys.path.append(os.path.join(plugindir, "lib"))
- sys.path.append(os.path.join(plugindir, "plugin"))

from plugin.main import MDI

if __name__ == "__main__":
    MDI()

ChatGPT

# -*- coding: utf-8 -*-

- import sys
- import os

- parent_folder_path = os.path.abspath(os.path.dirname(__file__))
- sys.path.append(parent_folder_path)
- sys.path.append(os.path.join(parent_folder_path, "lib"))
- sys.path.append(os.path.join(parent_folder_path, "plugin"))

from plugin.main import ChatGPT


if __name__ == "__main__":
    ChatGPT()

v2

My unreleased plugin I use to test my plugin API v2 library

- import sys
- import os

- rootDirectory = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
- libDirectory = os.path.join(rootDirectory, 'lib')
- pluginDirectory = os.path.join(rootDirectory, 'plugin')

- sys.path.append(rootDirectory)
- sys.path.append(libDirectory)
- sys.path.append(pluginDirectory)

from plugin import Plugin

class MyPlugin(Plugin):
    pass

Notes

While doing this I encountered a problem. v1 plugins expect requests to be sent as the third argument. I couldn't make -c <code> work after the request because I think it was thinking that the request JSON was file name, so I had to move -c <code> to be the first two arguments. However, Python stops processing arguments after it encounters -c <code>, so I couldn't specify -B after it, and I couldn't specify -B before because -c <code> takes up two arguments and the third argument must be the request. So I replaced -B flag usage with the PYTHONDONTWRITEBYTECODE environment variable that does the same thing.

@github-actions github-actions bot added this to the 1.19.5 milestone Dec 5, 2024

This comment has been minimized.

This comment has been minimized.

@Yusyuriv Yusyuriv marked this pull request as ready for review December 5, 2024 11:43
Copy link

gitstream-cm bot commented Dec 5, 2024

🥷 Code experts: taooceros

taooceros has most 🧠 knowledge in the files.

See details

Flow.Launcher.Core/Plugin/PythonPlugin.cs

Knowledge based on git-blame:
taooceros: 21%

Flow.Launcher.Core/Plugin/PythonPluginV2.cs

Knowledge based on git-blame:

To learn more about /:\ gitStream - Visit our Docs

Copy link

gitstream-cm bot commented Dec 5, 2024

Be a legend 🏆 by adding a before and after screenshot of the changes you made, especially if they are around UI/UX.

Copy link
Contributor

coderabbitai bot commented Dec 5, 2024

📝 Walkthrough

Walkthrough

The pull request introduces modifications to the PythonPlugin and PythonPluginV2 classes within the Flow.Launcher.Core namespace. Key changes include setting the PYTHONDONTWRITEBYTECODE environment variable to prevent the generation of .pyc files and altering the execution logic for Python scripts. The InitAsync method has been updated to dynamically adjust the sys.path for easier module imports, enhancing the flexibility of script execution.

Changes

File Path Change Summary
Flow.Launcher.Core/Plugin/PythonPlugin.cs Added PYTHONDONTWRITEBYTECODE environment variable; modified InitAsync for .py file execution.
Flow.Launcher.Core/Plugin/PythonPluginV2.cs Set PYTHONDONTWRITEBYTECODE in constructor; updated InitAsync for executing .py files with modified sys.path.

Suggested labels

bug, kind/i18n

Suggested reviewers

  • jjw24

Poem

🐇 In the land of code where rabbits play,
Python scripts hop in a new, bright way.
With paths set right, they dance and glide,
No bytecode mess, just joy and pride!
So let us cheer for changes made,
In the world of plugins, a grand parade! 🎉


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Outside diff range and nitpick comments (4)
Flow.Launcher.Core/Plugin/PythonPluginV2.cs (1)

50-52: Insert paths at the beginning of sys.path for correct module precedence

Appending to sys.path places your directories at the end, which may cause conflicts if there are modules with the same names elsewhere in the path. Inserting them at the beginning ensures your modules take precedence.

Modify the code to insert paths at the beginning:

- sys.path.append(r'{rootDirectory}')
- sys.path.append(r'{libDirectory}')
- sys.path.append(r'{pluginDirectory}')
+ sys.path.insert(0, r'{pluginDirectory}')
+ sys.path.insert(0, r'{libDirectory}')
+ sys.path.insert(0, r'{rootDirectory}')
Flow.Launcher.Core/Plugin/PythonPlugin.cs (3)

1-1: Confirm the necessity of adding using System;

The using System; directive has been added. Please verify if this is required, as it might be redundant if unused in the code.


71-73: Insert paths at the beginning of sys.path for correct module precedence

Appending directories to sys.path may lead to unintended module resolutions if other installed modules have the same names. Inserting your directories at the beginning ensures that your plugin modules are found first.

Modify the code as follows:

- sys.path.append(r'{rootDirectory}')
- sys.path.append(r'{libDirectory}')
- sys.path.append(r'{pluginDirectory}')
+ sys.path.insert(0, r'{pluginDirectory}')
+ sys.path.insert(0, r'{libDirectory}')
+ sys.path.insert(0, r'{rootDirectory}')

86-89: Reconsider the use of the -B flag for clarity

Although you're keeping the -B flag to maintain argument positions, it might cause confusion since it's no longer necessary due to the PYTHONDONTWRITEBYTECODE environment variable. Consider using an explicit placeholder to maintain argument positions without adding redundant flags.

Suggested change:

- _startInfo.ArgumentList.Add("-B");
- _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);
+ _startInfo.ArgumentList.Add("");  // Placeholder to maintain argument positions
+ _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath);

Add a comment to explain the placeholder:

// Adding an empty string as a placeholder to keep the argument positions consistent
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL

📥 Commits

Reviewing files that changed from the base of the PR and between 120b7c7 and d5dd7b4.

📒 Files selected for processing (2)
  • Flow.Launcher.Core/Plugin/PythonPlugin.cs (3 hunks)
  • Flow.Launcher.Core/Plugin/PythonPluginV2.cs (1 hunks)
🔇 Additional comments (4)
Flow.Launcher.Core/Plugin/PythonPluginV2.cs (2)

29-29: Setting PYTHONDONTWRITEBYTECODE to prevent .pyc file generation is appropriate

This change ensures that Python does not generate .pyc files, which can contain location-specific information that hinders portability.


34-63: Ensure proper handling of paths with spaces or special characters

When constructing the code string passed via the -c argument, ensure that paths (rootDirectory, libDirectory, pluginDirectory, filePath) are properly escaped or quoted to handle spaces and special characters. This prevents potential execution errors if plugin directories have such characters.

To verify, consider testing the plugin with directory paths that contain spaces or special characters.

Flow.Launcher.Core/Plugin/PythonPlugin.cs (2)

29-31: Setting PYTHONDONTWRITEBYTECODE to prevent .pyc file generation is appropriate

This change ensures that Python does not generate .pyc files, which can contain location-specific information that hinders portability.


53-95: Ensure proper handling of paths with spaces or special characters

Similar to PythonPluginV2, when passing the code via the -c argument, ensure that all paths are correctly escaped or quoted to handle any spaces or special characters.

Test the plugin with directory paths containing spaces or special characters to ensure it operates correctly.

@Garulf
Copy link
Member

Garulf commented Dec 5, 2024

Thanks for making this, I'll try and take a look this weekend when I have more time.

@jjw24 jjw24 modified the milestones: 1.19.5, 1.20.0 Dec 7, 2024
@jjw24
Copy link
Member

jjw24 commented Dec 19, 2024

LGTM. We will want to update how those path are handled in the ci here before we remove them from documentation going forward.

@jjw24
Copy link
Member

jjw24 commented Dec 19, 2024

@Garulf do you still want to do more testing?

@cibere
Copy link
Contributor

cibere commented Dec 19, 2024

Would it make sense to also add the win32 package paths to path? Incase they are using the pywin32 package? If they are using the package, this would be needed (so the question is should the plugin dev do it or flow), since pywin32 uses a pth file that doesn't get used with the lib folder

@jjw24
Copy link
Member

jjw24 commented Dec 20, 2024

I believe so, thoughts @Yusyuriv ?

@Yusyuriv
Copy link
Member Author

I don't think we should add package-specific things to the path because most of the plugin devs won't be using those packages. I added lib and plugin directories because that's a de facto standard and those two directories are mentioned in the docs. But I'm not opposed to it if others decide that it's needed. @Garulf?

@Garulf
Copy link
Member

Garulf commented Dec 22, 2024

Sorry for the late reply. I think pywin32 can be very useful for a Python plugin developer. However its unique packaging can cause a lot of friction.

I think it would be very helpful to add a path for this package as well.

@jjw24
Copy link
Member

jjw24 commented Jan 21, 2025

Hi @Yusyuriv have you got time to add this in or do you want to separate this change out as a todo later issue?

@Yusyuriv
Copy link
Member Author

Sure, I'll add it sometime this week.

Copy link

github-actions bot commented Feb 2, 2025

@check-spelling-bot Report

🔴 Please review

See the 📂 files view, the 📜action log, or 📝 job summary for details.

❌ Errors Count
❌ forbidden-pattern 23
⚠️ ignored-expect-variant 1
⚠️ non-alpha-in-dictionary 19

See ❌ Event descriptions for more information.

Forbidden patterns 🙅 (1)

In order to address this, you could change the content to not match the forbidden patterns (comments before forbidden patterns may help explain why they're forbidden), add patterns for acceptable instances, or adjust the forbidden patterns themselves.

These forbidden patterns matched content:

Pattern

\b[Nn]o[nt][- ]existent\b
If the flagged items are 🤯 false positives

If items relate to a ...

  • binary file (or some other file you wouldn't want to check at all).

    Please add a file path to the excludes.txt file matching the containing file.

    File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

    ^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

  • well-formed pattern.

    If you can write a pattern that would match it,
    try adding it to the patterns.txt file.

    Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

    Note that patterns can't match multiline strings.

@Yusyuriv
Copy link
Member Author

Yusyuriv commented Feb 2, 2025

Done. As far as I know, these are the only two directories that needed to be added:

  • lib/win32/lib
  • lib/win32

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
Flow.Launcher.Core/Plugin/PythonPlugin.cs (1)

54-86: Consider extracting common Python initialization code.

The Python initialization code is duplicated between PythonPlugin and PythonPluginV2. Consider extracting this into a shared helper class.

Create a new helper class:

internal static class PythonPluginHelper
{
    public static string GetPythonInitCode(string rootDirectory, string executeFilePath)
    {
        var libDirectory = Path.Combine(rootDirectory, "lib");
        var libPyWin32Directory = Path.Combine(libDirectory, "win32");
        var libPyWin32LibDirectory = Path.Combine(libPyWin32Directory, "lib");
        var pluginDirectory = Path.Combine(rootDirectory, "plugin");

        return $"""
               import sys, os
               paths = [
                   r'{rootDirectory}',
                   r'{libDirectory}',
                   r'{libPyWin32LibDirectory}',
                   r'{libPyWin32Directory}',
                   r'{pluginDirectory}'
               ]
               for path in paths:
                   if os.path.exists(path):
                       sys.path.append(path)
               
               import runpy
               runpy.run_path(r'{executeFilePath}', None, '__main__')
               """;
    }
}
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5dd7b4 and bb7900c.

📒 Files selected for processing (2)
  • Flow.Launcher.Core/Plugin/PythonPlugin.cs (3 hunks)
  • Flow.Launcher.Core/Plugin/PythonPluginV2.cs (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: gitStream workflow automation
  • GitHub Check: gitStream.cm
  • GitHub Check: gitStream.cm
🔇 Additional comments (5)
Flow.Launcher.Core/Plugin/PythonPluginV2.cs (3)

29-29: LGTM! Environment variable addition is a good approach.

Using PYTHONDONTWRITEBYTECODE environment variable is a more reliable approach than using the -B flag, as it ensures consistent behavior across different Python execution methods.


63-67: LGTM! Fallback for non-Python files.

The fallback for non-.py files maintains backward compatibility.


34-62: Verify directory existence before adding to sys.path.

While the approach to modify sys.path is good, we should verify that these directories exist before adding them to avoid potential issues with non-existent paths.

Consider adding existence checks:

 StartInfo.ArgumentList.Add(
     $"""
      import sys
      import os
-     sys.path.append(r'{rootDirectory}')
-     sys.path.append(r'{libDirectory}')
-     sys.path.append(r'{libPyWin32LibDirectory}')
-     sys.path.append(r'{libPyWin32Directory}')
-     sys.path.append(r'{pluginDirectory}')
+     paths = [
+         r'{rootDirectory}',
+         r'{libDirectory}',
+         r'{libPyWin32LibDirectory}',
+         r'{libPyWin32Directory}',
+         r'{pluginDirectory}'
+     ]
+     for path in paths:
+         if os.path.exists(path):
+             sys.path.append(path)
      
      import runpy
      runpy.run_path(r'{filePath}', None, '__main__')
      """
 );
Flow.Launcher.Core/Plugin/PythonPlugin.cs (2)

29-31: LGTM! Well-documented environment variable addition.

Good documentation explaining why we prevent .pyc file generation.


90-98: LGTM! Well-documented compatibility handling.

Good explanation of why the -B flag is retained for compatibility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants