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

Plugin Logic Proposal #75

Merged
merged 9 commits into from
Jul 18, 2024
Merged

Plugin Logic Proposal #75

merged 9 commits into from
Jul 18, 2024

Conversation

kwokcb
Copy link
Contributor

@kwokcb kwokcb commented Jun 17, 2024

Proposal

This is an initial proposed design for allow users to create compartmentalized plug-ins without having to override the main Qt application.

Design

  • Add in a simple QuiltiXPlugin class. For now this has the minimal of:
    • An identifier string
    • The plugin handler
  • Add in a simple QuiltiXPluginManager which is just a list of QuiltiXPlugin.
  • Access is provided to the editor as well as the package ROOT.
  • There are no constraints on what can and cannot be performed as is the case now when overriding the entire application.
  • It is up to the implementer to know the UI structure (also no change).

image

Discovery

  • A plugin is contained in a folder with a plugin.py file which contains a module with a install_plugin entry point.
  • One or more of these folders can reside under the following plugin "roots":
    • The plugins folder under the root of the Python distribution.
    • The folder specified by QUILTIX_PLUGIN_FOLDER

Example

  • I've migrated the JSON MaterialX logic to a materialxjson folder under a new sample_plugins folder.
  • Setting the env variable will allow this plug-in to be loaded.
  • The plug-in has read/write to JSON file and a "show as text" menu options.
    image

@manuelkoester, @RichardFrangenberg : Let me know what you think of this. It's naturally very basic to start with.
If something like this is reasonable, I'd like to move on to adding glTF support this way. Thanks!

logger = logging.getLogger(__name__)

class QuiltiXPlugin():
def __init__(self):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

More fields can be added. It's simple for now with a key and implementation.


def install_plugins(self):
self.plugins = []
plugin_roots = [os.path.join(self.root, "plugins"), os.getenv("QUILTIX_PLUGIN_FOLDER", "")]
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This gives an initial package and env var location.

spec.loader.exec_module(module)

# Call module install_plugin function if it exists
if hasattr(module, "install_plugin"):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A specific entry point is looked for.

except ImportError:
logger.error("materialxjson.core not found")

class QuiltiX_JSON_serializer():
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is no encapsulation of the core code as is the case with the current way of just overriding the entire application.

@manuelkoester
Copy link
Member

Hey Bernard! Sorry for not getting to this earlier.
Thanks for this great PR. I would like to add some changes to it if you don't mind. It's mostly about making things a little bit more pythonic and maybe setting it up in a way where there could be hooks at multiple stages of startup, so plugins could decide when to run what code.
I hope to be able to get to it soon.

@kwokcb
Copy link
Contributor Author

kwokcb commented Jul 10, 2024

Hi @manuelkoester ,
Feel free to make whatever changes you want. I just wanted to put something up to see what you thought about opening this up. Just curious what stages of startup would be available ? Thanks for looking at this.

@manuelkoester
Copy link
Member

Hey Bernard!

I've gone ahead and changed the plugin system quite a bit. It is now using pluggy in the backend, which also powers pytest. https://pluggy.readthedocs.io/en/stable/
This allows for multiple hooks, basically allowing us to hook into QuiltiX at different stages.
I've already went ahead and implemented some of these hooks.

I also changed the discovery a tiny bit, which allows for setting multiple paths in the QUILTIX_PLUGIN_FOLDER env var. For now I also removed looking into a "local" QuiltiX plugin dir.
Pluggy also makes it very simple defining new hooks, calling them in our code and for plugins to use them.

Comment on lines 66 to 89
class QuiltixHookspecs:
@hookspec
def after_ui_init(self, editor: "quiltix.QuiltiXWindow"):
"""
:param editor: The QuiltiX Window
"""

@hookspec
def before_ui_init(self, editor: "quiltix.QuiltiXWindow"):
"""
:param editor: The QuiltiX Window
"""

@hookspec
def before_mx_import(self):
"""
This allows any code to execute before MaterialX gets imported
"""

@hookspec
def before_pxr_import(self):
"""
This allows any code to execute before OpenUSD's pxr gets imported
"""
Copy link
Member

Choose a reason for hiding this comment

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

These hookspecs specify the hooks, and what arguments the individual hooks get supplied.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Similar comment to the before_mx_import. Seems like it would be better to have as an after_pxr_import. Also maybe it's better to call this after_usd_import ?

Copy link
Member

@manuelkoester manuelkoester Jul 17, 2024

Choose a reason for hiding this comment

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

Personally I prefer pxr, as AFAIK it is responsible for setting the env vars and startup. Also Usd at least in this context is just another submodule of pxr next to things like Gf, UsdGeom and the likes.

Comment on lines 45 to 49
if hasattr(module, PLUGIN_ID_FUNCTION_NAME):
plugin_id_function = getattr(module, PLUGIN_ID_FUNCTION_NAME)
plugin_id_name = plugin_id_function()
self.register(module, plugin_id_name)
logger.info(f"Registered plugin '{plugin_id_name} at {module.__file__}")
Copy link
Member

Choose a reason for hiding this comment

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

For now plugins are required to provide the plugin_id function in their plugin.py so it gets picked up by the Manager.

plugin_manager.load_plugins_from_environment_variable()
plugin_manager.add_hookspecs(qx_plugin.QuiltixHookspecs)

plugin_manager.hook.before_mx_import()
Copy link
Member

Choose a reason for hiding this comment

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

This is how the hooks actually get triggered.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think it would be more useful to have an after MaterialX imported then the plugin can use the loaded version and if desired. Could cut down on overhead and also enhance consistent usage of the same runtime.

Copy link
Member

Choose a reason for hiding this comment

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

These named this way so users can theoretically still modify environment variables before MaterialX or the likes get initialized and read from them.
But yeah it would probably make sense to also add the after_ versions.

Comment on lines 158 to 160
@qx_plugin.hookimpl
def after_ui_init(editor: "quiltix.QuiltiXWindow"):
editor.json_serializer = QuiltiX_JSON_serializer(editor, constants.ROOT)
Copy link
Member

Choose a reason for hiding this comment

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

This is how and where we use the after_ui_init hook that get's triggered by the Manager.

Copy link
Member

@RichardFrangenberg RichardFrangenberg left a comment

Choose a reason for hiding this comment

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

Very nice and interesting approach. Feels very clean!

src/QuiltiX/qx_plugin.py Outdated Show resolved Hide resolved
src/QuiltiX/qx_plugin.py Outdated Show resolved Hide resolved
try:
import materialxjson.core as jsoncore
except ImportError:
logger.error("materialxjson.core not found")
Copy link
Member

Choose a reason for hiding this comment

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

This plugin isn't usable without jsoncore. Would it make sense to stop loading the plugin at this point and somehow disable it after logging the error?
Some logic for plugins to define requirements, which need to be met before the plugin can be loaded, could be handy. This could be used to define plugins, which only work on a specific OS or with a specific MaterialX versions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi @RichardFrangenberg
There used to be a validity check before the refactor. I've added this back in with an explicit `is_valid()1 check on the plugin. It leaves it up to the plugin to decide where it can work.

Copy link
Member

Choose a reason for hiding this comment

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

I've changed it so a plugin doesn't need to implement is_valid, but it can choose to do so.

sample_plugins/materialjson/plugin.py Outdated Show resolved Hide resolved
src/QuiltiX/qx_plugin.py Outdated Show resolved Hide resolved
@manuelkoester
Copy link
Member

Hey Bernard! I hope it is okay if I push over your changes with some of the things that I have started yesterday evening while chatting with Richard. It will functionally be very similar to yours however!

@manuelkoester
Copy link
Member

@kwokcb @RichardFrangenberg please feel free to leave any comments/feedback! Should be nearly done. The tiny test in tests\test_plugins.py also succeeds without any additional setup of env vars.

@kwokcb
Copy link
Contributor Author

kwokcb commented Jul 17, 2024

@manuelkoester Thanks for adding the pre and post. Looks good to me.
BTW, I created a 1.38.9 version of the dependent package if you want to test it out running. pip install materialxjson==1.38.9.

@manuelkoester
Copy link
Member

Nice! Works perfectly! I'll now merge this in. Thank you very much for the first implementation @kwokcb . There's very little we had to add here <3

@manuelkoester manuelkoester merged commit e34ebfe into PrismPipeline:main Jul 18, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants