Override default edx-platform XBlock mappings without forking

:warning: UPDATE: This post is outdated! See later replies about how to use xblock.v1.overrides instead of the hack suggsted in this first post of mine. :warning:

When you create an XBlock, you define an entry point in your setup.py, where you specify the tags you want to associate with it:

    entry_points={
        'xblock.v1': [
            "supercool = my_plugin_pkg.xblocks:SuperCoolBlock"
        ]
    }

But say you want to override the built-in html tag so that HTML content is handled by your own custom XBlock, instead of edx-platform’s HTMLBlock entry. Maybe you’ve got a plugin where you subclass HTMLBlock to add one or two key new behaviors.

You could try to do:

    entry_points={
        'xblock.v1': [
            "html = my_plugin_pkg.xblocks:CustomHtmlBlock"
        ]
    }

But that will cause the XBlock to raise an AmbiguousPluginError when rendering, because it doesn’t know which mapping to use:

That’s actually a pretty reasonable behavior, since anything can install an XBlock, and it’s better to surface the ambiguity immediately, rather than have it just pick one of the classes at random.

But you can force the edx-platform entry to be removed at startup in your private.py settings file (or anything else that executes in process at server startup) with a snippet like this:

# Remove edx-platform's mapping for the "html" XBlock tag if we've installed
# a plugin with our own mapping for that tag.
import pkg_resources
html_mappings = list(pkg_resources.iter_entry_points('xblock.v1', 'html'))
if len(html_mappings) > 1:
    dist = pkg_resources.get_distribution('Open-edX')
    del dist.get_entry_map('xblock.v1')['html']

This doesn’t permanently remove edx-platform’s entry point for the html tag from its installed package. It’s still there, and pkg_resources will read it on startup every time. We’re just removing the in-process dictionary entry for it as part of LMS/Studio startup, so that it’s no longer there by the time the XBlock tag lookup is checking for it.

I’ve tested that it works locally, though I haven’t used it in production. I definitely recommend caution when trying this approach, since the configuration changes will not be obvious for people who might have to debug issues later. Folks who are running their own forks are almost certainly better off modifying their local copies of edx-platform’s setup.py to just directly map to their custom XBlock. But if you really need to override a built-in XBlock and forking is not feasible for you, this alternative might work.

5 Likes

Thanks, this is useful

This no longer works in XBlock v5.0.0. We’ll either need a new workaround, or, better yet, a supported API for overriding builtin block types.

@dave What do you think about replacing this workaround with a new supported entrypoint, "xblock.v1.overrides"?

Across all installed packages, the semantics of the entrypoints would be:

  • Each block type must occur exactly once in xblock.v1. This is the current behavior.
  • Each block type may occur at most once in xblock.v1.overrides.
  • When loading XBlock classes, block type mappings in xblock.v1.overrides take precedence over those in xblock.v1.
  • xblock.v1.overrides may only contain block types which exist in xblock.v1, protecting operators against typos, and avoiding the ambiguity of a block type that is “overridden but doesn’t exist in the first place”.

For example:

# edx-platform/setup.py
setup(
    entry_points={
        'xblock.v1': [
            "html = xmodule.html_block.HtmlBlock",
            ....,
        ],
        ....,
    },
    ....,
)
# my-cool-plugin/setup.py
# Adds a "cool" block and customizes the "html" block
setup(
    entry_points={
        'xblock.v1': [
            "cool = my_cool_plugin.CoolNewBlock",
        ],
        'xblock.v1.overrides': [
            "html = my_cool_plugin.CoolerHtmlBlock",
        ],
        ....,
    },
    ....,
)
2 Likes

This would definitely fix our use case :+1: Happy to help with design / review if you’d like.

I will note that our use case (overriding an XBlock implementation) is only an issue because we need our plugin to override an XBlock that is still baked into edx-platform (e.g. HTML, Video, Problem). If those blocks were themselves plugins, we could more easily replace them with our own implementations, instead of needing a hack or an override.

Adding the override entry point is still probably our best approach now, but I wonder how long this will be needed in the future or if it solves other use cases.

1 Like

There is a (multi-release) project in process to extract the baked-in blocks from edx-platform. But, I believe that this issue would arise regardless of whether the block is baked into edx-platform. When Dave says “built-in” I think he means “built into the pip-installed edx-platform Python environment” rather than “built into the edx-platform codebase”. You could test this by trying to override openassessment instead of html.

This is because Python entry-points are searched across all installed packages rather than in the immediate repo. When you call XBlock.load_class("yada") , it invokes entry_points(group="xblock.v1", identifier="yada"), which scans for yada = YadaClass in the distribution (setup.py) metadata of every package in the current Python env.

The (now-broken) workaround only mentioned the 'Open-edX' (edx-platform) distribution because that is the distribution where the html entrypoint was defined. In the case of openassessment, you’d need to remove it from the ora2 distribution.

1 Like

Ah, gotcha, so this is still a good thing to add anyways :+1:

I created a quick proof of concept which I was able to get working, would love eyes and feedback on this as soon as you’re able, @kmccormick !

1 Like

Yeah, I think this approach is fine, so long as edx-platform itself pinky-swears that it won’t add things here, so it can’t possibly conflict with extensions that do.

2 Likes

It is probably also worth having some way of environmentally controlling if overrides are even allowed, e.g. if the platform doesn’t want to support overriding?

I don’t think we need to add an extra setting to control this. Ultimately, it’s the site operator that would determine the value of that setting and it’s a site operator that would determine whether or not to use xblock.v1.overrides for these replacement block types (when they pip install XBlocks that use it). I think the code can just always look for overrides.

1 Like

xblock.v1.overrides is available in XBlock 5.1.0 thanks to @nsprenkle !

Nathan, could you let us know in this thread once edx-platform is upgraded to XBlock==5.1.0 and the entrypoint is confirmed to be working on edx.org ?