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
andsemantic_version
-
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. ↩
Comments
There are no comments yet. Will you add the first one?