Hatchling plugins

Hello,
I have a couple of questions regarding the changes for Indico 3.3.4.
First, as per the new internal change comment:

  • Indico and plugin wheels are now built using hatchling instead of setuptools, and package metadata is specified using pyproject.toml. Developers who want to build their own plugins need to switch from setup.py and/or setup.cfg to pyproject.toml as well (#6477)

question: What’s the best way of doing this?

I installed hatch via homebrew and ran under the plugin folder:
hatch new --init
It created the pyproject.toml file but building the plugin wheel failed with some errors.
I got it work by comparing my file to those under indico-plugins-cern and making the appropiate changes.

Second question:
when I run:
./bin/maintenance/build-wheel.py plugin --add-version-suffix my_plugin
the wheel file created has no suffix, e.g.:
indico_plugin_my_plugin-2.0-py3-none-any.whl

am I missing something?
Cheers

You should not (and do not need to) install hatch (which is a build frontend, while hatchling is a build backend).

Literally all you have to do is getting rid of setup.{py,cfg} and MANIFEST.in, and add a pyproject.toml instead.

You can check e.g. this PR to see how plugins are updated.

Thanks for the response.
I moved the created wheel to our server and did a “pip install pi_customization”
but now I get this error:

RuntimeError: Assets for plugin pi_customization have not been built:

Traceback (most recent call last):
  File "/opt/indico/.venv/lib/python3.12/site-packages/indico/core/plugins/__init__.py", line 192, in _do_inject
    return self.manifest[name]
           ~~~~~~~~~~~~~^^^^^^
TypeError: 'NoneType' object is not subscriptable

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/opt/indico/.venv/lib/python3.12/site-packages/flask/app.py", line 880, in full_dispatch_request
    rv = self.dispatch_request()
         ^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/flask/app.py", line 865, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)  # type: ignore[no-any-return]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/flask_pluginengine/util.py", line 190, in wrapped
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/indico/web/flask/util.py", line 80, in wrapper
    return obj().process()
           ^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/indico/web/rh.py", line 307, in process
    res = self._do_process()
          ^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/indico/web/rh.py", line 275, in _do_process
    rv = self._process()
         ^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/pi_customization/controllers.py", line 120, in _process
    return self._prepare_template(form)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/pi_customization/controllers.py", line 97, in _prepare_template
    return WPSeminarManagement.render_template('seminar_management.html', form=form,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/indico/web/views.py", line 129, in render_template
    return cls(g.rh, *wp_args, **context).display()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/indico/web/views.py", line 269, in display
    injected_bundles = values_from_signal(signals.plugin.inject_bundle.send(self.__class__), as_list=True,
                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/blinker/base.py", line 279, in send
    result = receiver(sender, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/flask_pluginengine/util.py", line 190, in wrapped
    return func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/indico/core/plugins/__init__.py", line 203, in _func
    return _do_inject(sender)
           ^^^^^^^^^^^^^^^^^^
  File "/opt/indico/.venv/lib/python3.12/site-packages/indico/core/plugins/__init__.py", line 194, in _do_inject
    raise RuntimeError(f'Assets for plugin {self.name} have not been built')

I ran had ran the build assets script:

./bin/maintenance/build-assets.py plugin ~/dev/indico/custom/plugins/pi_customization --dev --watch

  • share the output of tree in your plugin’s root directory so I can see if something is wrong with your structure
  • share your pyproject.toml content
  • check static/dist/ folder inside your plugin’s package directory - it should contain whatever assets your plugin has after building them
  • building a plugin via build-wheel.py also builds assets by default, you should NOT have the build-assets.py script running in parallel (if something triggers a build there you’d pollute your wheel with the output from its dev build in addition to the release build files)

Tree:

README.md
indico_plugin_pi_customization.egg-info
pi_customization
pyproject.toml
url_map.json
webpack-bundles.json

pyproject.toml

[project]
name = "indico-plugin-pi-customization"
version = "2.0"
readme = "README.md"
license = "MIT"
requires-python = ">=3.12.2, <3.13"
authors = [
    { name = "PI Software Team", email = "me@pitp.ca" },
]
classifiers = [
    "Environment :: Plugins",
    "Environment :: Web Environment",
    "License :: OSI Approved :: MIT License",
    "Programming Language :: Python :: 3.12",
]
dependencies = [
    "indico-patcher>=0.2.0",
    "indico>=3.3.dev0",
    "markdownify>=0.12.1",
]

[project.entry-points."indico.plugins"]
pi_customization = "pi_customization.plugin:PICustomization"

[project.urls]
GitHub = "https://github.com/perimeterit/"

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

[tool.hatch.build]
packages = ['pi_customization']
exclude = [
    '*.no-header',
    '.keep',
    # exclude original client sources (they are all included in source maps anyway)
    'indico_*/client/',
    # no need for tests outside development
    'test_snapshots/',
    'tests/',
    '*_test.py',
]
artifacts = [
    'indico_*/translations/**/messages-react.json',
    'indico_*/translations/**/*.mo',
    'indico_*/static/dist/',
]

static/dist looks like this:

image

Can you get a recursive tree? Not sure why it wasn’t recursive for you, mine is (on Linux). Maybe you’re on osx and it’s different there?

Also, can you share the output of unzip -l yourfile.whl to see what’s in your wheel?

Nevermind, I see the problem. You need to adjust the paths in artifacts (and maybe also exclude) to match your pi_customization directory which is of course not covered by indico_* (we assume Indico plugin packages are named indico_<whatever>).

awesome!
Thanks for the tip. It works now on our remote server.
I do have this issue though when running this command:
./bin/maintenance/build-assets.py all-plugins --dev ~/dev/indico/custom/plugins/

plugin: pi_customization
Traceback (most recent call last):
  File "/Users/lrivas/dev/indico/src/./bin/maintenance/build-assets.py", line 270, in <module>
    cli()
  File "/Users/lrivas/dev/indico/env/lib/python3.12/site-packages/click/core.py", line 1157, in __call__
    return self.main(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/lrivas/dev/indico/env/lib/python3.12/site-packages/click/core.py", line 1078, in main
    rv = self.invoke(ctx)
         ^^^^^^^^^^^^^^^^
  File "/Users/lrivas/dev/indico/env/lib/python3.12/site-packages/click/core.py", line 1688, in invoke
    return _process_result(sub_ctx.command.invoke(sub_ctx))
                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/lrivas/dev/indico/env/lib/python3.12/site-packages/click/core.py", line 1434, in invoke
    return ctx.invoke(self.callback, **ctx.params)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/lrivas/dev/indico/env/lib/python3.12/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/lrivas/dev/indico/env/lib/python3.12/site-packages/click/decorators.py", line 33, in new_func
    return f(get_current_context(), *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/lrivas/dev/indico/src/./bin/maintenance/build-assets.py", line 265, in build_all_plugins
    ctx.invoke(build_plugin, plugin_dir=os.path.join(plugins_dir, plugin), dev=dev, clean=clean, watch=False,
  File "/Users/lrivas/dev/indico/env/lib/python3.12/site-packages/click/core.py", line 783, in invoke
    return __callback(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/lrivas/dev/indico/src/./bin/maintenance/build-assets.py", line 222, in build_plugin
    webpack_build_config_file = plugin_dir / 'webpack-build-config.json'
                                ~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
TypeError: unsupported operand type(s) for /: 'str' and 'str'

Ah, that looks like a bug (I never use all-plugins myself and thus forgot to test it…).

Here’s a fix for it (which I’ll also commit to master soon):

diff --git a/bin/maintenance/build-assets.py b/bin/maintenance/build-assets.py
index 2f2ad2c17d..aaeeebb97d 100755
--- a/bin/maintenance/build-assets.py
+++ b/bin/maintenance/build-assets.py
@@ -254,15 +254,15 @@ def build_plugin(plugin_dir: Path, dev, clean, watch, url_root):


 @cli.command('all-plugins', short_help='Builds assets of all plugins in a directory.')
-@click.argument('plugins_dir', type=click.Path(exists=True, file_okay=False, resolve_path=True))
+@click.argument('plugins_dir', type=click.Path(exists=True, file_okay=False, resolve_path=True, path_type=Path))
 @_common_build_options(allow_watch=False)
 @click.pass_context
 def build_all_plugins(ctx, plugins_dir, dev, clean, url_root):
     """Run webpack to build plugin assets."""
-    plugins = sorted(d for d in os.listdir(plugins_dir) if _is_plugin_dir(os.path.join(plugins_dir, d)))
+    plugins = sorted(d for d in plugins_dir.iterdir() if _is_plugin_dir(plugins_dir / d))
     for plugin in plugins:
         step('plugin: {}', plugin)
-        ctx.invoke(build_plugin, plugin_dir=os.path.join(plugins_dir, plugin), dev=dev, clean=clean, watch=False,
+        ctx.invoke(build_plugin, plugin_dir=(plugins_dir / plugin), dev=dev, clean=clean, watch=False,
                    url_root=url_root)


Got it and thanks again.
Cheers