Skip to content

Register plugin from entry points #1872

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

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
91b3a3c
Add test data to load plugins from entry points
Ni-g-3l Nov 11, 2024
f8a8f49
Load REZ plugins from setuptools entry points
Ni-g-3l Nov 11, 2024
da9e66c
Fix flake8 report
Ni-g-3l Nov 11, 2024
7fbed7c
Fix import of importlib metadata in python 3.8, 3.9
Ni-g-3l Nov 18, 2024
e776bb9
Signed-off-by: Nig3l <[email protected]>
Ni-g-3l Jan 25, 2025
55ed2ce
Remove dupplicated plugin definition
Ni-g-3l Jan 25, 2025
ee6f9bf
Install test plugin to test data to speed up test
Ni-g-3l Feb 2, 2025
e6a3256
Add try except on load plugin from entry points
Ni-g-3l Feb 2, 2025
e30e7cd
Update plugin install doc
Ni-g-3l Feb 2, 2025
0742acf
use modern build backend, use dist-info, fix tests and simplify file …
JeanChristopheMorinPerso Feb 2, 2025
4e6a47a
Update src/rez/tests/util.py
JeanChristopheMorinPerso Feb 2, 2025
25c020a
Remove vendored code for safety reason.
JeanChristopheMorinPerso Feb 2, 2025
9579331
Re-add importlib-metadata (this time with the right version to suppor…
JeanChristopheMorinPerso Feb 2, 2025
ac70f9a
Make it compatible with python 3.8 and 3.9
JeanChristopheMorinPerso Feb 2, 2025
ffbe7f6
Fix python compat again...
JeanChristopheMorinPerso Feb 2, 2025
ab8e162
Last fix for real
JeanChristopheMorinPerso Feb 2, 2025
a7fa195
Fix flake8
JeanChristopheMorinPerso Feb 2, 2025
cab8f04
Add new plugin install methods
Ni-g-3l Feb 3, 2025
9d939f1
Organize entrypoints by plugin type to avoid having plugins being reg…
JeanChristopheMorinPerso Feb 15, 2025
46219f0
Use entrypoint name for plugin name instead of module name
JeanChristopheMorinPerso Feb 15, 2025
fc381b0
Add debug logs to match the other plugin loading modes
JeanChristopheMorinPerso Feb 15, 2025
861e0f1
Fix tests
JeanChristopheMorinPerso Feb 15, 2025
8ed6899
First pass at improving the plugins documentation
JeanChristopheMorinPerso Mar 1, 2025
cfc7599
Merge branch 'main' into feat/register_plugin_from_entry_points
JeanChristopheMorinPerso Mar 1, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 130 additions & 0 deletions docs/source/guides/developing_your_own_plugin.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
==========================
Developing your own plugin
==========================

This guide will walk you through writing a rez plugin.

Structure
=========

If you decide to register your plugin using the :ref:`entry-points method <plugin-entry-points>`, you are free
to structure your plugin however you like.

If you decide to implement your plugins using the ``rezplugins`` namespace package, please
refer to :ref:`rezplugins-structure` to learn about the file structure that you will need to follow.

Registering subcommands
=======================

Optionally, plugins can provide new ``rez`` subcommands.

To register a plugin and expose a new subcommand, the plugin module:

- MUST have a module-level docstring (used as the command help)
- MUST provide a ``setup_parser()`` function
- MUST provide a ``command()`` function
- MUST provide a ``register_plugin()`` function
- SHOULD have a module-level attribute ``command_behavior``

For example, a plugin named ``foo`` and this is the ``foo.py`` in the plugin type
root directory:

.. code-block:: python
:caption: foo.py

"""The docstring for command help, this is required."""
import argparse

command_behavior = {
"hidden": False, # optional: bool
"arg_mode": None, # optional: None, "passthrough", "grouped"
}


def setup_parser(parser: argparse.ArgumentParser):
parser.add_argument("--hello", action="store_true")


def command(
opts: argparse.Namespace,
parser: argparse.ArgumentParser,
extra_arg_groups: list[list[str]],
):
if opts.hello:
print("world")


def register_plugin():
"""This function is your plugin entry point. Rez will call this function."""

# import here to avoid circular imports.
from rez.command import Command

class CommandFoo(Command):
# This is where you declare the settings the plugin accepts.
schema_dict = {
"str_option": str,
"int_option": int,
}

@classmethod
def name(cls):
return "foo"

return CommandFoo

Install plugins

Choose a reason for hiding this comment

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

This section still needs some work. I didn't touch it.

Copy link
Author

Choose a reason for hiding this comment

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

What are the sections that are need improvements ? Have you specific things in mind ?

===============

1. Copy directly to rez install folder

To make your plugin available to rez, you can install it directly under
``src/rezplugins`` (that's called a namespace package).

2. Add the source path to :envvar:`REZ_PLUGIN_PATH`

Add the source path to the REZ_PLUGIN_PATH environment variable in order to make your plugin available to rez.

3. Add entry points to pyproject.toml

To make your plugin available to rez, you can also create an entry points section in your
``pyproject.toml`` file, that will allow you to install your plugin with `pip install` command.

.. code-block:: toml
:caption: pyproject.toml

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "foo"
version = "0.1.0"

[project.entry-points."rez.plugins"]
foo_cmd = "foo"


4. Create a setup.py

To make your plugin available to rez, you can also create a ``setup.py`` file,
that will allow you to install your plugin with `pip install` command.

.. code-block:: python
:caption: setup.py

from setuptools import setup, find_packages

setup(
name="foo",
version="0.1.0",
package_dir={
"foo": "foo"
},
packages=find_packages(where="."),
entry_points={
'rez.plugins': [
'foo_cmd = foo',
]
}
)
1 change: 1 addition & 0 deletions docs/source/guides/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ This section contains various user guides.
:maxdepth: 1

update_to_3
developing_your_own_plugin
165 changes: 105 additions & 60 deletions docs/source/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Rez is designed around the concept of plugins. Plugins can be used to extend rez

Rez comes with built-in plugins that are located at :gh-rez:`src/rezplugins`. New plugins are encouraged to be developed out-of-tree (outside rez).

This page documents what plugins are available, the plugin types and how plugins are discovered.
If you want to learn how to develop a plugin, please refer to :doc:`guides/developing_your_own_plugin`.

List installed plugins
======================

Expand Down Expand Up @@ -51,90 +54,132 @@ like this:
"filesystem": {}
}

.. _plugin-types:

Existing plugin types
=====================

- :gh-rez:`build_process <src/rezplugins/build_process>`
- :gh-rez:`build_system <src/rezplugins/build_system>`
- :gh-rez:`command <src/rezplugins/command>`
- :gh-rez:`package_repository <src/rezplugins/package_repository>`
- :gh-rez:`release_hook <src/rezplugins/release_hook>`
- :gh-rez:`release_vcs <src/rezplugins/release_vcs>`
- :gh-rez:`shell <src/rezplugins/shell>`
.. table::
:align: left

====================== =========================================================== ==================
Type Base class(es) Top level settings [1]_
====================== =========================================================== ==================
``build_process`` | :class:`rez.build_process.BuildProcess`
| :class:`rez.build_process.BuildProcessHelper` [2]_ No
``build_system`` :class:`rez.build_system.BuildSystem` No
``command`` :class:`rez.command.Command` Yes
``package_repository`` | :class:`rez.package_repository.PackageRepository` No
| :class:`rez.package_resources.PackageFamilyResource`
| :class:`rez.package_resources.PackageResourceHelper`
| :class:`rez.package_resources.VariantResourceHelper` [3]_ No
``release_hook`` :class:`rez.release_hook.ReleaseHook` Yes
``release_vcs`` :class:`rez.release_vcs.ReleaseVCS` Yes
``shell`` :class:`rez.shells.Shell` No
====================== =========================================================== ==================

.. [1] Top level settings: The concept of top level settings is documented in :ref:`default-settings`.
.. [2] build_process: You have to choose between on of the two classes.
.. [3] package_repository: All 4 classes have to be implemented.

Discovery mechanisms
====================

There are three different discovery mechanisms for external/out-of-tree plugins:

#. :ref:`rezplugins-structure`
#. :ref:`plugin-entry-points`

Each of these mechanisms can be used independently or in combination. It is up to you to
decide which discovery mechanism is best for your use case. Each option has pros and cons.

.. _rezplugins-structure:

``rezplugins`` structure
------------------------

Developing your own plugin
==========================
This method relies on the ``rezplugins`` namespace package. Use the :data:`plugin_path` setting or
the :envvar:`REZ_PLUGIN_PATH` environment variable to tell rez where to find your plugin(s).

Rez plugins require a specific folder structure as follows:
You need to follow the following file structure:

.. code-block:: text

/plugin_type
/__init__.py (adds plugin path to rez)
/rezconfig.py (defines configuration settings for your plugin)
/plugin_file1.py (your plugin file)
/plugin_file2.py (your plugin file)
etc.
rezplugins/
├── __init__.py
└── <plugin_type>/
├── __init__.py
└── <plugin name>.py

To make your plugin available to rez, you can install them directly under
``src/rezplugins`` (that's called a namespace package) or you can add
the path to :envvar:`REZ_PLUGIN_PATH`.
``<plugin_type>`` refers to types defined in the :ref:`plugin types <plugin-types>` section. ``<plugin_name>`` is the name of your plugin.
The ``rezplugins`` directory is not optional.

Registering subcommands
-----------------------
.. note::
The path(s) MUST point to the directory **above** your ``rezplugins`` directory.

Optionally, plugins can provide new ``rez`` subcommands.
.. note::
Even though ``rezplugins`` is a python package, your sparse copy of
it should not be on the :envvar:`PYTHONPATH`, just the :envvar:`REZ_PLUGIN_PATH`.
This is important because it ensures that rez's copy of
``rezplugins`` is always found first.

To register a plugin and expose a new subcommand, the plugin module:
.. _plugin-entry-points:

- MUST have a module-level docstring (used as the command help)
- MUST provide a `setup_parser()` function
- MUST provide a `command()` function
- MUST provide a `register_plugin()` function
- SHOULD have a module-level attribute `command_behavior`
Entry-points
------------

For example, a plugin named 'foo' and this is the ``foo.py`` in the plugin type
root directory:
.. versionadded:: 3.3.0

.. code-block:: python
:caption: foo.py
Plugins can be discovered by using `Python's entry-points <https://packaging.python.org/en/latest/guides/creating-and-discovering-plugins/#using-package-metadata>`_.

"""The docstring for command help, this is required."""
from rez.command import Command
There is one entry-point per :ref:`plugin type <plugin-types>`:

command_behavior = {
"hidden": False, # optional: bool
"arg_mode": None, # optional: None, "passthrough", "grouped"
}
* ``rez.plugins.build_process``
* ``rez.plugins.build_system``
* ``rez.plugins.command``
* ``rez.plugins.package_repository``
* ``rez.plugins.release_hook``
* ``rez.plugins.release_vcs``
* ``rez.plugins.shell``

This allows a package to define multiple plugins. In fact, a package can contain multiple plugins of the same type and plugins for multiple types.

def setup_parser(parser, completions=False):
parser.add_argument("--hello", ...)
.. note::
Unlike the other discovery mechanisms, this method doesn't require any special file structure. It is thus more flexible, less restricting
and easier to use.

def command(opts, parser=None, extra_arg_groups=None):
if opts.hello:
print("world")
.. _default-settings:

class CommandFoo(Command):
# This is where you declare the settings the plugin accepts.
schema_dict = {
"str_option": str,
"int_option": int,
...
}
@classmethod
def name(cls):
return "foo"
Default settings
================

def register_plugin():
return CommandFoo
You can define default settings for the plugins you write by adding a ``rezconfig.py`` or ``rezconfig.yml``
beside your plugin module. Rez will automatically load these settings.

All new plugin types must define an ``__init__.py`` in their root directory
so that the plugin manager will find them.
This is valid both all the discovery mechanisms.

Note that the format of that ``rezconfig.py`` or ``rezconfig.yml`` file is as follows:

.. code-block:: python
:caption: __init__.py

from rez.plugin_managers import extend_path
__path__ = extend_path(__path__, __name__)
top_level_setting = "value"

plugin_name = {
"setting_1": "value1"
}

In this case, the settings for ``plugin_name`` would be available in your plugin as ``self.settings``
and ``top_level_setting`` would be available as ``self.type_settings.top_level_setting``.

.. note::

Not all plugin types support top level settings. Please refer to the table in :ref:`plugin-types` to
see which types support them.

Overriding built-in plugins
===========================

Built-in plugins can be overridden by installing a plugin with the same name and type.
When rez sees this, it will prioritie your plugin over its built-in plugin.

This is useful if you want to modify a built-in plugin without having to modify rez's source code.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Metadata-Version: 2.4
Name: baz
Version: 0.1.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[rez.plugins]
baz_cmd = baz
38 changes: 38 additions & 0 deletions src/rez/data/tests/extensions/baz/baz/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
baz plugin
"""

from rez.command import Command

# This attribute is optional, default behavior will be applied if not present.
command_behavior = {
"hidden": False, # (bool): default False
"arg_mode": None, # (str): "passthrough", "grouped", default None
}


def setup_parser(parser, completions=False):
parser.add_argument(
"-m", "--message", action="store_true", help="Print message from world."
)


def command(opts, parser=None, extra_arg_groups=None):
from baz import core

if opts.message:
msg = core.get_message_from_baz()
print(msg)
return

print("Please use '-h' flag to see what you can do to this world !")


class BazCommand(Command):
@classmethod
def name(cls):
return "baz_cmd"


def register_plugin():
return BazCommand
4 changes: 4 additions & 0 deletions src/rez/data/tests/extensions/baz/baz/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
def get_message_from_baz():
from rez.config import config
message = config.plugins.command.baz.message
return message
3 changes: 3 additions & 0 deletions src/rez/data/tests/extensions/baz/baz/rezconfig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
baz = {
"message": "welcome to this world."
}
Loading
Loading