sopel-irc / sopel

:robot::speech_balloon: An easy-to-use and highly extensible IRC Bot framework. Formerly Willie.
https://sopel.chat
Other
950 stars 405 forks source link

version: fallback behavior if no module- or file-level `__version__` #2498

Open dgw opened 11 months ago

dgw commented 11 months ago

One can .version pluginname for any loaded plugin, but e.g. a single-file module stored in ~/.sopel/plugins/pluginname.py will simply return [version] pluginname (unknown) if it doesn't contain a __version__ member.

I propose that, at least for single-file plugins, the plugin machinery should fall back on the file modification timestamp in lieu of a version number.* It'd probably be reasonable to format the file's modification time as yyyy.mm.dd, and ignore hours or smaller units.**


* It gets trickier for Folder or Namespace Package plugins, but we're discouraging the use of those now.
** Our Sphinx theme for 8.x, Furo, is even versioned this way on PyPI, though packaging strips the tags' leading zeroes. I'm sure someone will come along and say we should just use the Unix timestamp as the version number, but come on.
SnoopJ commented 11 months ago

Here's a small patch to version that would implement the desired behavior, although it relies on accessing Sopel._plugins directly, so it will need some support work to do it the Right Way™, either adding the modification time to the plugin metadata or adding a mechanism for retrieving the plugin from the bot.

click for patch ```diff diff --git a/sopel/modules/version.py b/sopel/modules/version.py index 536eaa83..ad2bca5e 100644 --- a/sopel/modules/version.py +++ b/sopel/modules/version.py @@ -9,6 +9,7 @@ https://sopel.chat from __future__ import annotations import datetime +import importlib.util import os import platform @@ -51,6 +52,13 @@ def version(bot, trigger): meta = bot.get_plugin_meta(plugin) if meta["version"] is None: version = "(unknown)" + # TODO: this uses implementation details of Sopel ;( + spec = importlib.util.find_spec(bot._plugins[plugin].module_name) + if spec.origin: + mtime = os.stat(spec.origin).st_mtime + timestamp = datetime.datetime.fromtimestamp(mtime).strftime("%d %b %Y") + version += " — last modified {ts}".format(ts=timestamp) else: version = "v" + str(meta["version"]) ```

Output before patch:

15:40 <testibot> [version] 8ball (unknown)
15:40 <SnoopJ> !reload version

Output after patch:

15:40 <SnoopJ> !version 8ball
15:40 <testibot> [version] 8ball (unknown) — last modified 02 Apr 2023

(note: I prefer using %b where possible to avoid possible confusion over date order but I don't feel super strongly about that, it could just as easily be %Y.%m.%d)