Skip to content

Commit 9a3417e

Browse files
Register plugins from entry points (#1991)
* Add test data to load plugins from entry points Signed-off-by: Nig3l <nig3lpro@gmail.com> * Load REZ plugins from setuptools entry points Signed-off-by: Nig3l <nig3lpro@gmail.com> * Fix flake8 report Signed-off-by: Nig3l <nig3lpro@gmail.com> * Fix import of importlib metadata in python 3.8, 3.9 Signed-off-by: Nig3l <nig3lpro@gmail.com> * Signed-off-by: Nig3l <nig3lpro@gmail.com> * Remove dupplicated plugin definition Signed-off-by: Nig3l <nig3lpro@gmail.com> * Install test plugin to test data to speed up test Signed-off-by: Nig3l <nig3lpro@gmail.com> * Add try except on load plugin from entry points Signed-off-by: Nig3l <nig3lpro@gmail.com> * Update plugin install doc Signed-off-by: Nig3l <nig3lpro@gmail.com> * use modern build backend, use dist-info, fix tests and simplify file structure. Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> * Update src/rez/tests/util.py Signed-off-by: Jean-Christophe Morin <38703886+JeanChristopheMorinPerso@users.noreply.github.com> * Remove vendored code for safety reason. Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> * Re-add importlib-metadata (this time with the right version to support py 3.7) and add missing dependencies (zipp, typing_extensions) Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> * Make it compatible with python 3.8 and 3.9 Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> * Fix python compat again... Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> * Last fix for real Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> * Fix flake8 Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> * Add new plugin install methods Signed-off-by: Nig3l <nig3lpro@gmail.com> * Organize entrypoints by plugin type to avoid having plugins being registered for every type Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> * Use entrypoint name for plugin name instead of module name Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> * Add debug logs to match the other plugin loading modes Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> * Fix tests Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> * First pass at improving the plugins documentation Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> * Fix test due to missing commit Signed-off-by: Jean-Christophe Morin <38703886+JeanChristopheMorinPerso@users.noreply.github.com> * Improve layout of plugin guide and add a warning about the fact that it's under construction Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> * Re-order things in the plugins doc to make it more user friendly Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> * Clarify the docs a bit more Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> * Expose the plugins key in the configuration page Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> --------- Signed-off-by: Nig3l <nig3lpro@gmail.com> Signed-off-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com> Signed-off-by: Maxime Cots <42718803+Ni-g-3l@users.noreply.github.com> Co-authored-by: Jean-Christophe Morin <jean_christophe_morin@hotmail.com>
1 parent cd8476f commit 9a3417e

30 files changed

+6025
-130
lines changed
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
==========================
2+
Developing your own plugin
3+
==========================
4+
5+
This guide will walk you through writing a rez plugin.
6+
7+
.. warning::
8+
This section is under constructions. The instructions provided might not be
9+
accurate or up-to-date. We welcome contributions!
10+
11+
Structure
12+
=========
13+
14+
If you decide to register your plugin using the :ref:`entry-points method <plugin-entry-points>`, you are free
15+
to structure your plugin however you like.
16+
17+
If you decide to implement your plugins using the ``rezplugins`` namespace package, please
18+
refer to :ref:`rezplugins-structure` to learn about the file structure that you will need to follow.
19+
20+
Registering subcommands
21+
=======================
22+
23+
Optionally, plugins can provide new ``rez`` subcommands.
24+
25+
To register a plugin and expose a new subcommand, the plugin module:
26+
27+
- **MUST** have a module-level docstring (used as the command help)
28+
- **MUST** provide a ``setup_parser()`` function
29+
- **MUST** provide a ``command()`` function
30+
- **MUST** provide a ``register_plugin()`` function
31+
- **SHOULD** have a module-level attribute ``command_behavior``
32+
33+
For example, a plugin named ``foo`` and this is the ``foo.py`` in the plugin type
34+
root directory:
35+
36+
.. code-block:: python
37+
:caption: foo.py
38+
39+
"""The docstring for command help, this is required."""
40+
import argparse
41+
42+
command_behavior = {
43+
"hidden": False, # optional: bool
44+
"arg_mode": None, # optional: None, "passthrough", "grouped"
45+
}
46+
47+
48+
def setup_parser(parser: argparse.ArgumentParser):
49+
parser.add_argument("--hello", action="store_true")
50+
51+
52+
def command(
53+
opts: argparse.Namespace,
54+
parser: argparse.ArgumentParser,
55+
extra_arg_groups: list[list[str]],
56+
):
57+
if opts.hello:
58+
print("world")
59+
60+
61+
def register_plugin():
62+
"""This function is your plugin entry point. Rez will call this function."""
63+
64+
# import here to avoid circular imports.
65+
from rez.command import Command
66+
67+
class CommandFoo(Command):
68+
# This is where you declare the settings the plugin accepts.
69+
schema_dict = {
70+
"str_option": str,
71+
"int_option": int,
72+
}
73+
74+
@classmethod
75+
def name(cls):
76+
return "foo"
77+
78+
return CommandFoo
79+
80+
Install plugins
81+
===============
82+
83+
1. Copy directly to rez install folder
84+
85+
To make your plugin available to rez, you can install it directly under
86+
``src/rezplugins`` (that's called a namespace package).
87+
88+
2. Add the source path to :envvar:`REZ_PLUGIN_PATH`
89+
90+
Add the source path to the ``REZ_PLUGIN_PATH`` environment variable in order to make your plugin available to rez.
91+
92+
3. Add entry points to pyproject.toml
93+
94+
To make your plugin available to rez, you can also create an entry points section in your
95+
``pyproject.toml`` file, that will allow you to install your plugin with ``pip install`` command.
96+
97+
.. code-block:: toml
98+
:caption: pyproject.toml
99+
100+
[build-system]
101+
requires = ["hatchling"]
102+
build-backend = "hatchling.build"
103+
104+
[project]
105+
name = "foo"
106+
version = "0.1.0"
107+
108+
[project.entry-points."rez.plugins"]
109+
foo_cmd = "foo"
110+
111+
4. Create a setup.py
112+
113+
To make your plugin available to rez, you can also create a ``setup.py`` file,
114+
that will allow you to install your plugin with ``pip install`` command.
115+
116+
.. code-block:: python
117+
:caption: setup.py
118+
119+
from setuptools import setup, find_packages
120+
121+
setup(
122+
name="foo",
123+
version="0.1.0",
124+
package_dir={
125+
"foo": "foo"
126+
},
127+
packages=find_packages(where="."),
128+
entry_points={
129+
'rez.plugins': [
130+
'foo_cmd = foo',
131+
]
132+
}
133+
)

docs/source/guides/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ This section contains various user guides.
88
:maxdepth: 1
99

1010
update_to_3
11+
developing_your_own_plugin

docs/source/plugins.rst

Lines changed: 119 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,53 @@ Rez is designed around the concept of plugins. Plugins can be used to extend rez
66

77
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).
88

9+
This page documents what plugins are available, the plugin types and how plugins are discovered.
10+
If you want to learn how to develop a plugin, please refer to :doc:`guides/developing_your_own_plugin`.
11+
12+
.. _plugin-types:
13+
14+
Existing plugin types
15+
=====================
16+
17+
.. table::
18+
:align: left
19+
20+
====================== =========================================================== ==================
21+
Type Base class(es) Top level settings [1]_
22+
====================== =========================================================== ==================
23+
``build_process`` | :class:`rez.build_process.BuildProcess`
24+
| :class:`rez.build_process.BuildProcessHelper` [2]_ No
25+
``build_system`` :class:`rez.build_system.BuildSystem` No
26+
``command`` :class:`rez.command.Command` Yes
27+
``package_repository`` | :class:`rez.package_repository.PackageRepository` No
28+
| :class:`rez.package_resources.PackageFamilyResource`
29+
| :class:`rez.package_resources.PackageResourceHelper`
30+
| :class:`rez.package_resources.VariantResourceHelper` [3]_
31+
``release_hook`` :class:`rez.release_hook.ReleaseHook` Yes
32+
``release_vcs`` :class:`rez.release_vcs.ReleaseVCS` Yes
33+
``shell`` :class:`rez.shells.Shell` No
34+
====================== =========================================================== ==================
35+
36+
.. [1] Top level settings: The concept of top level settings is documented in :ref:`default-settings`.
37+
.. [2] build_process: You have to choose between on of the two classes.
38+
.. [3] package_repository: All 4 classes have to be implemented.
39+
40+
.. _configuring-plugins:
41+
42+
Configuring plugins
43+
===================
44+
45+
Plugins can be configured by adding a ``plugins`` key to your ``rezconfig.py``
46+
like this:
47+
48+
.. code-block:: python
49+
50+
plugins = {
51+
"package_repository": {
52+
"filesystem": {}
53+
}
54+
}
55+
956
List installed plugins
1057
======================
1158

@@ -38,103 +85,104 @@ Currently installed plugins can be queried by running :option:`rez -i`
3885
shell powershell Windows PowerShell 5 loaded
3986
shell pwsh PowerShell Core 6+ loaded
4087
88+
Discovery mechanisms
89+
====================
4190

42-
Configuring plugins
43-
===================
91+
There are three different discovery mechanisms for external/out-of-tree plugins:
4492

45-
Plugins can be configured by adding a ``plugins`` key to your ``rezconfig.py``
46-
like this:
93+
#. :ref:`rezplugins-structure`
94+
#. :ref:`plugin-entry-points`
4795

48-
.. code-block:: python
49-
50-
plugins = {
51-
"filesystem": {}
52-
}
96+
Each of these mechanisms can be used independently or in combination. It is up to you to
97+
decide which discovery mechanism is best for your use case. Each option has pros and cons.
5398

54-
Existing plugin types
55-
=====================
99+
.. _rezplugins-structure:
56100

57-
- :gh-rez:`build_process <src/rezplugins/build_process>`
58-
- :gh-rez:`build_system <src/rezplugins/build_system>`
59-
- :gh-rez:`command <src/rezplugins/command>`
60-
- :gh-rez:`package_repository <src/rezplugins/package_repository>`
61-
- :gh-rez:`release_hook <src/rezplugins/release_hook>`
62-
- :gh-rez:`release_vcs <src/rezplugins/release_vcs>`
63-
- :gh-rez:`shell <src/rezplugins/shell>`
101+
``rezplugins`` structure
102+
------------------------
64103

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

68-
Rez plugins require a specific folder structure as follows:
107+
You need to follow the following file structure:
69108

70109
.. code-block:: text
71110
72-
/plugin_type
73-
/__init__.py (adds plugin path to rez)
74-
/rezconfig.py (defines configuration settings for your plugin)
75-
/plugin_file1.py (your plugin file)
76-
/plugin_file2.py (your plugin file)
77-
etc.
111+
rezplugins/
112+
├── __init__.py
113+
└── <plugin_type>/
114+
├── __init__.py
115+
└── <plugin name>.py
78116
79-
To make your plugin available to rez, you can install them directly under
80-
``src/rezplugins`` (that's called a namespace package) or you can add
81-
the path to :envvar:`REZ_PLUGIN_PATH`.
117+
``<plugin_type>`` refers to types defined in the :ref:`plugin types <plugin-types>` section. ``<plugin_name>`` is the name of your plugin.
118+
The ``rezplugins`` directory is not optional.
82119

83-
Registering subcommands
84-
-----------------------
120+
.. note::
121+
The path(s) MUST point to the directory **above** your ``rezplugins`` directory.
85122

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

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

90-
- MUST have a module-level docstring (used as the command help)
91-
- MUST provide a `setup_parser()` function
92-
- MUST provide a `command()` function
93-
- MUST provide a `register_plugin()` function
94-
- SHOULD have a module-level attribute `command_behavior`
131+
Entry-points
132+
------------
95133

96-
For example, a plugin named 'foo' and this is the ``foo.py`` in the plugin type
97-
root directory:
134+
.. versionadded:: 3.3.0
98135

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

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

105-
command_behavior = {
106-
"hidden": False, # optional: bool
107-
"arg_mode": None, # optional: None, "passthrough", "grouped"
108-
}
140+
* ``rez.plugins.build_process``
141+
* ``rez.plugins.build_system``
142+
* ``rez.plugins.command``
143+
* ``rez.plugins.package_repository``
144+
* ``rez.plugins.release_hook``
145+
* ``rez.plugins.release_vcs``
146+
* ``rez.plugins.shell``
109147

110-
def setup_parser(parser, completions=False):
111-
parser.add_argument("--hello", ...)
148+
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.
112149

113-
def command(opts, parser=None, extra_arg_groups=None):
114-
if opts.hello:
115-
print("world")
150+
.. note::
151+
Unlike the other discovery mechanisms, this method doesn't require any special file structure. It is thus more flexible, less restricting
152+
and easier to use.
116153

117-
class CommandFoo(Command):
118-
# This is where you declare the settings the plugin accepts.
119-
schema_dict = {
120-
"str_option": str,
121-
"int_option": int,
122-
...
123-
}
124-
@classmethod
125-
def name(cls):
126-
return "foo"
154+
.. _default-settings:
155+
156+
Default settings
157+
----------------
158+
159+
You can define default settings for the plugins you write by adding a ``rezconfig.py`` or ``rezconfig.yml``
160+
beside your plugin module. Rez will automatically load these settings.
127161

128-
def register_plugin():
129-
return CommandFoo
162+
This is valid both all the discovery mechanisms.
130163

131-
All new plugin types must define an ``__init__.py`` in their root directory
132-
so that the plugin manager will find them.
164+
Note that the format of that ``rezconfig.py`` or ``rezconfig.yml`` file for plugins is as follows:
133165

134166
.. code-block:: python
135-
:caption: __init__.py
136167
137-
from rez.plugin_managers import extend_path
138-
__path__ = extend_path(__path__, __name__)
168+
top_level_setting = "value"
169+
170+
plugin_name = {
171+
"setting_1": "value1"
172+
}
173+
174+
In this case, the settings for ``plugin_name`` would be available in your plugin as ``self.settings``
175+
and ``top_level_setting`` would be available as ``self.type_settings.top_level_setting``.
176+
177+
.. note::
178+
179+
Not all plugin types support top level settings. Please refer to the table in :ref:`plugin-types` to
180+
see which types support them.
181+
182+
Overriding built-in plugins
183+
===========================
139184

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

188+
This is useful if you want to modify a built-in plugin without having to modify rez's source code.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Metadata-Version: 2.4
2+
Name: baz
3+
Version: 0.1.0
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[rez.plugins.command]
2+
baz_cmd = baz

0 commit comments

Comments
 (0)