Seafoam

Advanced Pelican: Self-Configuring Themes, and Seafoam 2.6.0 Released

I’ve just released a groundbreaking version of my website theme Seafoam; I believe this is the first Pelican theme designed to configure itself!

The root of this magic is that the theme has actually been packaged as a namespace plugin for Pelican. If you’re running a sufficiently recent version of Pelican (i.e. v4.5 or newer), Pelican will automatically load the Seafoam plugin; if you’re using an older version of Pelican (or other, non-namespace plugins), you’ll need to add the Seafoam plugin to you configuration:

# pelicanconf.py

PLUGINS = [
    "pelican.plugins.seafoam",
    # others, as needed/desired
]

# the rest of your configuration file

Unless you need to change Seafoam‘s behaviour from its defaults, you shouldn’t need do any further configuration!

Shiny! How Do I Do This For My Theme?

Start With a Namespace Plugin

A namespace plugin works its magic by have the code files in the right folder (i.e. “namespace”). For Pelican, we want those code files under pelican/plugins. So our file layout looks like this:

<project root>
  +- pelican
  |    `- plugins
  |         `- my_theme
  |              `- __init__.py
  `- setup.py

Of particular note is that both the pelican and plugins directories have no Python code directly in them.

Add Your Theme Files

For this article, I’m going to treat the actual creation of a theme as out-of-scope and assume you already have a theme. Your theme will consist of a templates folder, containing the Jinja21 templates with .html file extensions, and possibly a static folder containing the theme’s static assets (images, CSS, and JavaScript). Add these as a subfolder of your theme. So now our file layout looks like this (actual static files not shown):

<project root>
  +- pelican
  |    `- plugins
  |         `- my_theme
  |              + static
  |              |   +- css
  |              |   +- js
  |              |   `- images
  |              +- templates
  |              |    +- index.html
  |              |    `- <other templates> 
  |              `- __init__.py
  `- setup.py

Find Our Templates

Next, we’re going to add a function to our theme’s __init__.py file that will point to the location of our theme:

# pelican/plugins/my_theme/__init__.py

from pathlib import Path

def get_path():
    # Theme directory is defined as our parent directory
    return str(Path(__file__).resolve().parent)

If we didn’t do anything more, in our pelicanconf.py, we could:

# pelicanconf.py
from pelican.plugins import my_theme

THEME = my_theme.get_path()

(And this is exactly how Seafoam worked previously.)

Automatically Activate Our Theme

Now we get into the real magic, to which there are two parts: 1) a function to set the theme, and 2) hook that function into Pelican’s signals.

The function to set the theme is rather simple, but note that we set pelican_obj.theme directly rather than pelican_obj.settings["THEME"], as this setting appears to already have been read.

# pelican/plugins/my_theme/__init__.py

def set_theme(pelican_obj):
    pelican_obj.theme = get_path()

If we wanted to, we could extend the above function to massage Pelican’s settings (this is where Seafoam sets its defaults).

For signals, these are “hooks” that allow you to run custom code as Pelican goes through the process of generating your site. We’re going to hook into the very first signal that Pelican offers:

# pelican/plugins/my_theme/__init__.py

from pelican import signals

def register():
    signals.initialized.connect(set_theme)

Complete Python Code

Adding a couple of niceties (logging, a version number) and above, we end up with something like this:

# pelican/plugins/my_theme/__init__.py

import logging
from pathlib import Path

from pelican import signals

__version__ = "1.0.0"

logger = logging.get_logger(__name__)

def get_path():
    return str(Path(__file__).resolve().parent)

def set_theme(pelican_obj):
    pelican_obj.theme = get_path()
    logger.debug("[My Theme] Theme set!")

def register():
    signals.initialized.connect(set_theme)

Tips for Publishing

When I went to publish the updated package to PyPI, I found I had to switch from setuptools.find_packages() to setuptools.find_namespace_packages() in my theme’s setup.py. I also had to add the non-Python files to my MANIFEST.in file:

# MANIFEST.in

recursive-include pelican/plugins/seafoam/static *.*
recursive-include pelican/plugins/seafoam/templates *.*

Finally, be sure to include the Framework :: Pelican :: Themes PyPI classifier so others can find your work!

If I missed something in this mini-tutorial, but sure to leave a comment or send me an email so I can expand/fix it.

Upgrading

Upgrading should is straight forward; I haven’t broken anything on purpose since v2.0.0 came out. With this release several previously mandatory settings are now optional to specify and so you can remove them if you want, but keeping them in place shouldn’t cause any problems.

To install or to upgrade, you can use pip:

pip install seafoam --upgrade

If you’re already running Pelican v4.5 (or newer) and only using namespace plugins, then the required plugins will automatically load. However, most will have to update your pelicanconf.py to point to the new plugin names:

# pelicanconf.py

PLUGINS = [
    "pelican.plugins.seafoam",
    "pelican.plugins.jinja_filters",  # <-- optional now
    "pelican.plugins.image_process",  # <-- optional now
    # others, as desired...
]

All other settings, if it was just to apply the default settings for Seafoam, can now be removed.

To be clear, Seafoam still supports Pelican 3 (i.e. you don’t need to upgrade to Pelican 4.5 quite yet).

Development Issues

This is just a section where I make some notes on the issues I ran into while working on the code.

PEP517 & PEP518 are accepted extensions to Python dealing with the ability to specify a “build backend”, i.e. how does a given module package itself up for distribution. The way to do this is to add a few lines to your pyproject.toml file (the new place to collect settings and metadata about your Python project) like so:

# pyproject.toml

[build-system]
requires = ["setuptools >= 40.6.0", "wheel"]
build-backend = "setuptools.build_meta"

I figured I would do this to this latest version of Seafoam as this is the way of the future there really wasn’t any obvious downside. However, I discovered a downside: when you go to build such a package from source, it wants to create a isolated environment for doing so, and as part of that, it wants to download setuptools and wheel (really, whatever you specify in the build-system key) and doesn’t seem to rely on either the system libraries or the system’s pip download cache. Normally, this wouldn’t be an issue, but if you try and install such a package from the Test PyPI server, it also tries to download and install the build requirements from there too! And many packages, including setuptools are never posted to the Test PyPI server.

There may be a workaround, but I have somewhat limited internet at the moment so I haven’t been able to research the issue. I should probably raise an issue, somewhere….

Summary: Source distributions with a PEP517/518 setuptools build system are unable to be installed from the Test PyPI server.

Update (July 7th): The answer is to call pip with --no-build-isolation. This relies on the environment being installed into to provide the needed packages to actually do the install, and as a byproduct allows offline installs and installs when the “main” package is hosted on the Test PyPI server.

See this Stack Overflow answer, which points to the terse to the point of being cryptic pip documentation.

Future Plans

This release actually resolves mosts of the future plans I’d written for the v2.5 release. At this point, further development will likely be tied to my website demands directly.

One thing I thought about doing was to make the tag pages (called “labels” on this site) more powerful by adding the ability to add a custom text header to them. For example, the current “homepage” link for Seafoam is given as such a tag page (http://blog.minchin.ca/label/seafoam/) and this would be handy to do for all my Python projects, as most of the project history is already collected here as blog posts. I haven’t begun to look at this, so I’m not sure if it could be done from the Jinja templates alone, or if it would require adjusting the Pelican output with an extention, either as part of Seafoam or a separate plugin.

Also, as I wrote previously, there are long term plans to upgrade to Bootstrap 5, but nothing started there.

Changelog

See my previous post for earlier changelog entries.

Version 2.6.0 — July 5, 2021

  • feature add internal plugin. This will allow the theme to automatically configure and activate itself. Should significantly reduced installation complexity. You may be able to completely remove the configuration you have in place for the plugin.
  • feature include Seafoam version in source HTML of generated sites
  • bug adjust HTML to add the .table class where needed, rather than applying the formatting to all HTML tables. Effectively a re-work of v.2.4.7.
  • support add screenshots. See issues #1 and #18.
  • support updated setup.py. Include tempalate and static files at new location.
  • support no longer include raw LESS files in distributions or in generated sites.
  • support now also requires beautifulsoup4 and semantic_version

  1. Jinja version 3 has now been released, so what does this package go by now? Jinja3? Jinja2 v3? plain old Jinja? In any case, jinja2 seems to install version 3 off of PyPI. 


Other posts


Other posts under Seafoam



Comments

There are no comments yet. Will you add the first one?

Add a Comment

You can use the Markdown syntax to format your comment.

or alternately, send me your thoughts at minchinweb [at] gmail.com

Comment Atom Feed (for this post)