How to update Sphinx options during the build#

As part of the pydata-sphinx-theme we have a few settings that auto-enable extensions and configure them on behalf of the user. It has always been mysterious to me how to do this properly during the Sphinx build. It’s easy to configure things with ahead of time, but what if you want to manually set a value during the build?

I finally figured it out, so documenting the process here.

Use the builder-inited event#

Define a Sphinx event for builder-inited. This will trigger after the builder has been selected, but before the environent is finalized for the build. This should be a function that takes a single (app) parameter.

See also

See the Sphinx Core Events documentation for more information about Sphinx’s events system.

Use app._raw_config to find the user-provided config#

Use the app._raw_config object to detect user-given config. This is first written when Sphinx is initialized and should be a good indication of what the user provided.

This is useful if you only want to over-ride something if the user didn’t set it themselves. However, the app.config object will have all of the config options, including defaults.

Update configuration options with the app.config object#

Update app.config.keyname. You can set and update configuration values directly with app.config.keyname = "foo". The way Sphinx does this is by directly setting app.config.__dict__["keyname"] = "foo", but this doesn’t seem to be necessary and I’m not sure why they do it that way.

app.config.values has the defaults!

It might seem like app.config.values is the right place for this, but that isn't the case. app.config.values` has the defaults for Sphinx, and if you directly replace items, then Sphinx will behave unpredictably. Here’s where Sphinx configures these defaults

Update HTML theme options with app.builder.theme_options#

Update app.builder.theme_options. Many people (including myself) incorrectly try to update app.config.html_theme_options during a build event. But this doesn’t do anything because the’ve already been copied over to app.builder.theme_options early in the build process. Annoyingly, the copied dictionary in app.builder.theme_options does not point to the same points in memory as app.config.html_theme_options.

This copy action is done here.

An example#

Here’s an example of the whole process in action:

# This function will update a single configuration value
def update_config(app):
   # Check if a value was provided by the user
   if "foo" in app.config._raw_values:
      # Update a config value
      app.config.__dict__["foo"] = "bar"

   # Update an HTML theme value
   app.builder.theme_options["foo2"] = "bar2"

# Register the above function to be called during the builder-inited phase
def setup(app):
  app.connect("builder-inited", update_config)