Sopel 8.0 Migration
Version 8.0 brings Sopel into the modern era of Python, finally dropping support for end-of-life Python releases as of December 2023, including Python 2 and several older versions of Python 3. This allows us to make significant improvements under the hood that were held back by these legacy versions, e.g. the IRC connection backend and Sopel’s event system.
Owner/admin usage changes
Updated Python requirements & support policy
Sopel 8.0 requires Python 3.8 or higher. Support for Python 2.7 and 3.3–3.7 has been removed. In exchange, known issues sometimes preventing use of Sopel 7 with Python 3.11+ have been fixed.
During the lifecycle of Sopel 8.x, we will do our best to test against new Python releases as they come. We will continue to test against Python versions that reach end-of-life unless it becomes impractical to do so—in which case the next maintenance release of Sopel will officially drop support for the Python version(s) that can no longer be tested.
CLI changes
Sopel 8 continues the command-line interface overhaul we began in version 7, mostly in the form of removing support for legacy usage from Sopel’s 6.x era.
The bare sopel
command now offers only basic control. Most of its legacy
options have been removed:
Legacy command | Modern command |
---|---|
sopel --quit or sopel -q |
sopel stop |
sopel --kill or sopel -k |
sopel stop --kill or sopel stop -k |
sopel --restart or sopel -r |
sopel restart |
sopel --configure-all or sopel -w |
sopel configure |
sopel --configure-modules |
sopel configure --plugins |
sopel --list or sopel -l |
sopel-config list |
sopel -v |
sopel -V / sopel --version |
sopel --quiet |
n/a; this feature didn’t work anyway |
Ignore system
In Sopel 8, “hostmask” blocks are now called “host” blocks, which reflects the
reality of how those values are used by Sopel. Bot admins’ muscle memories will
have to learn to type e.g. .blocks add host
instead of .blocks add hostmask
.
We intend to add actual “hostmask” blocking in a future Sopel release.
Note that the config file option for “hostmask” blocks already was, and
still is, named host_blocks
. This change only affects interactive blocklist
editing by bot admins via IRC commands.
Configuration & plugin-handling changes
What’s a “module”, doc?
Sopel 8 no longer loads plugins from the <homedir>/modules
directory by default.
To migrate, rename this directory to plugins
or declare it in the extra
field of
the [core]
configuration section.
This is part of our efforts to clarify the difference between a “module” (which is a Python concept) and a “plugin” (which is a special kind of “module” that can extend Sopel with new functionality).
Encrypted IRC by default
Unless its config file specifies different values, Sopel 8 assumes that it
should use_ssl
and connect to the standard encrypted port
6697.
Older versions assumed they should not use_ssl
and use port
6667.
Taming logging to a channel
DEBUG
is no longer a valid choice for the logging_channel_level
setting, and
logging_channel_level
is no longer inherited from the main logging_level
setting. The new default logging_channel_level
is WARNING
.
Having Sopel output its log messages to an IRC channel of your choice can be
convenient, but if DEBUG
logs could be sent to IRC, it was possible to have
Sopel give you too much of a good thing and get stuck. Every debug message
sent to IRC would itself generate multiple additional debug log entries, filling
the bot’s outbound message queue—and the best solution to prevent deadlocks is
to simply prohibit DEBUG
from being used with logging_channel_level
.
Farewell, Phenny/Jenni
The project that eventually became Sopel diverged from Jenni in 2012. After more than a decade of maintaining backward compatibility (in a theoretical sense, at least) with plugins originally written for Jenni and its predecessor Phenny, Sopel 8.0 bids a fond farewell to plugin code from that era.
Beyond the maintenance burden of making sure Sopel could still technically load such old plugins, we felt it was disingenuous to continue “supporting” them when the rest of Sopel’s API has changed and evolved so much that the chance of such old plugins actually still working is now very low indeed. Thus, we’re not dropping support for those decade-old plugins as much as admitting that things have changed a lot and users would be better off seeking (or if necessary, writing) newer plugins with similar functionality.
Sopel 8 plugin changes
Removed built-in plugins
Sopel’s built-in plugins are slowly being migrated out to their own standalone packages. In 8.0, we say farewell to:
help
: now published assopel-help
- Note: Sopel still requires the
sopel-help
package, so it will be installed and available automatically.
- Note: Sopel still requires the
ip
: now published assopel-iplookup
meetbot
: now published assopel-meetbot
py
: now published assopel-py
reddit
: now published assopel-reddit
remind
: now published assopel-remind
Over the longer term, this will help the Sopel project manage plugin maintenance and updates more quickly.
Notable changes to built-in plugins
currency
plugin
The fiat_provider
setting now takes precedence over the fixer_io_key
.
Previously, setting a fixer_io_key
would use the fixer.io
fiat exchange rate
provider regardless of the fiat_provider
setting.
exchangerate.host
is no longer a valid fiat_provider
choice; it has been
replaced by the new default, open.er-api.com
.
reload
plugin
The .update
command has been removed from the reload
plugin. It relied on
running Sopel in a way that we don’t officially support, so we chose to remove
the command.
To replace .update
, if you were using it, we recommend writing an external
script that stops Sopel, updates the components you want, and then
starts Sopel again.
wikipedia
plugin
The old command names (.w
, .wik
, and .wiki
) are no longer used; they were
freed up for use by bespoke plugins. wikipedia
functionality is now invoked by
the .wikipedia
command, or .wp
for short.
The deprecated lang_per_channel
config-file setting has been removed. Chanops
and Sopel admins can set the preferred wiki language for a channel using the
.wpclang
command instead.
Sopel 8 API changes
Changes that could be particularly surprising are marked with a ⚠️ emoji.
⚠️ Command names are now literal
Sopel 8 no longer supports regular expressions in command names, which are now escaped during plugin loading. This change helps plugin developers avoid unintentional interference with the default capture groups for commands, and also clears the way for enhancements to plugin/command management.
Notes in the documentation for @sopel.plugin.command()
list possible Sopel 8-compatible adaptations for a few common use cases.
⚠️ Time-handling changes
trigger.time
is now an “aware” datetime
object, meaning it has a
UTC offset associated with it. Comparison or arithmetic operations between
“aware” and “naïve” datetime
objects are not allowed; code that manipulates
trigger.time
values will need to make sure “naïve” datetime
objects created,
or retrieved from sources, outside the bot are converted to “aware” values.
The fallback format string used by sopel.tools.time.format_time()
changed from
outputting the timezone name (UTC
) to the UTC offset (+0000
). This format
string is used only if there is no preferred format set for the nick, channel,
and/or config passed to the function.
format_time()
’s handling of “aware” and “naïve” datetime
s was also improved,
but those changes should be transparent to both users and plugin developers. Let
us know if you encounter situations where format_time()
behaves surprisingly.
sopel.tools.time.validate_timezone()
now also raises a ValueError
even if
the timezone to be validated is None
. This was previously a special case that
returned None
.
Enums everywhere
(Insert the classic Buzz Lightyear meme here, if you want.)
The following parts of Sopel’s API each became an enumeration of some kind:
sopel.formatting.colors
- Class members’ names and values remain unchanged
sopel.tools.events
- Class members’ names and values remain unchanged
sopel.plugin.OP
,sopel.plugin.VOICE
, etc.- These constants became shortcuts to equivalent members of a new
sopel.privileges.AccessLevel
enumeration - The old constants are not deprecated until we are closer to releasing improved API features around channel access
- These constants became shortcuts to equivalent members of a new
Now that Sopel is squarely in the Python 3 world, using Enum
s where they make
sense will let us gradually improve how some parts of its API fit together in
the long run.
None of these changes are expected to require any immediate code updates for correct function. However, code updates might be required in projects that make use of type-checking.
Identifier casemapping
Any reasonably modern IRC server advertises its method for normalizing nicknames
to every client that connects. Sopel 8 builds on the bot.isupport
mechanism
added in Sopel 7 and makes use of the advertised CASEMAPPING
value when
comparing and normalizing nicknames itself.
To support handling this under the hood, the bot’s config.core.nick
setting is
now stored and returned as a normal str
. It and any other strings that you’d
like to treat as nicknames should be passed through the bot.make_identifier()
method to get the same Identifier
type you’re used to from Sopel 7, configured
to use the IRC server’s advertised casemapping method.
Database changes
bot.db.get_nick_id()
no longer creates a new ID by default if its create
parameter is unspecified.
Plugin code doesn’t usually need to use this method. But if you do use it,
you’ll possibly need to handle a new ValueError
exception when calling this
method on a nick that hasn’t yet been assigned an ID.
IRC event changes
The event names RPL_INVITELIST
and RPL_ENDOFINVITELIST
have been updated per
clarifications from the “Modern IRC” living-specification project.
RPL_INVEXLIST
and RPL_ENDOFINVEXLIST
were added to the list of named events
that Sopel knows about.
Plugin code might need to be updated as a result of these changes, but keep in
mind that different IRC servers mean different things when
they send these events. If in doubt, specify a raw numeric value (e.g. '346'
).
Writing truly cross-network plugins that react to these events should be
undertaken very carefully.
IRC connection status monitoring
bot.connection_registered
now indicates that a connection is open, the
IRC server has accepted the bot as a client, and the bot can send commands.
Previously, this boolean value indicated only that a connection was open.
This is useful to plugins with code that runs without a triggering IRC event but
still outputs to IRC, such as a @sopel.plugin.interval()
job or acting on data
received via a socket. if not bot.connection_registered
, the
output can be skipped, retried after a delay, etc.
More consistent trigger
objects
⚠️ STATUSMSG
handling
Events sent to a channel can be scoped to users with a particular privilege
level, or status, in that channel. IRC servers advertise the availability of
this feature using the STATUSMSG
token in RPL_ISUPPORT
.
In Sopel 7, trigger.sender
included the status prefix, and it was difficult
for plugins to detect and remove it themselves if so desired. Sopel 8.0 removes
the status prefix from trigger.sender
and leaves only the channel name, while
the status prefix (if any) is exposed as trigger.status_prefix
. This change is
useful, for example, to plugins that use trigger.sender
to store or retrieve
channel values in bot.db
.
The bot
passed to a plugin callable triggered by an IRC event automatically
includes the status prefix, if present, in the default destination
parameter
to methods that send a message to IRC. Plugin callables that simply invoke
bot.say()
, bot.reply()
, etc. in direct response to a trigger
and without
overriding the default destination
will most likely get the expected behavior.
sopel-remind
is an example of one kind of plugin that this
change [might surprise][remind-statusmg]. Naïve implementation of such a feature
might simply store (trigger.nick, trigger.sender, parsed_message)
and send
bot.reply(parsed_message, trigger.sender, trigger.nick)
after the specified
amount of time has passed. The trigger.status_prefix
is lost, and the reminder
will be sent to the entire channel instead of only the status-limited subset of
users who could have seen the original command.
Only set the sender
when it makes sense
Sopel 8.0 now only sets trigger.sender
for the following events:
INVITE
JOIN
KICK
MODE
NOTICE
PART
PRIVMSG
TOPIC
In all other cases, the sender
will be None
and plugin code should use the
trigger.args
list to retrieve all information about the event.
For general IRC events like QUIT
, RPL_NAMREPLY
, etc. that don’t happen “in”
a channel or other specific context, the trigger.sender
value that plugin
callables saw in Sopel 7 was unpredictable at best. Writing correct behavior
should be more intuitive with this change.
To have text
, you must first have args
trigger.text
will now be empty (''
) if the event carries no args
.
In Sopel 7, trigger.text
contained the command name (e.g. 'QUIT'
) in these
cases, but that was a bug.
IRC privilege requirement changes
In Sopel 8.0, the @sopel.plugin.require_privilege()
decorator now implies
@sopel.plugin.require_chanmsg()
, and the decorated callable will not run if
triggered outside of a channel. Previously, require_privilege
restrictions
were simply ignored, and the callable would run anyway.
The @sopel.plugin.require_bot_privilege()
decorator also now implies
@sopel.plugin.require_chanmsg()
, with the same associated behavior change.
Capability negotiation rework
The old CapReq
method of asking Sopel to negotiate additional capabilities on
behalf of your plugin has been replaced with a much more robust system based on
the new sopel.plugin.capability
class/decorator.
Plugins can now request capabilities in one of two ways:
"""Sample plugin file"""
from sopel import plugin
# this will register a capability request
CAP_ACCOUNT_TAG = plugin.capability('account-tag')
# this will work as well
@plugin.capability('message-prefix')
def cap_message_prefix(cap_req, bot, acknowledged):
# do something if message-prefix is ACK or NAK
...
The first method is easier if you just want to require a capability. The second is easier if your plugin needs to do something in response to the IRC server’s response to the capability request. See the documentation for more details about plugins and managing capabilities.
The bot’s hostmask
In Sopel 8, accessing bot.hostmask
when the bot lacks sufficient data to
determine its own hostmask will return None
. The behavior in Sopel 7 was to
raise a KeyError
.
Testing tool changes
The MockIRCServer
mock object from Sopel’s pytest
plugin implements
blocking
as a keyword-only argument in Sopel 8. This applies to:
MockIRCServer.channel_joined()
MockIRCServer.join()
MockIRCServer.mode_set()
MockIRCServer.pm()
MockIRCServer.say()
To upgrade test cases using this mock, simply add blocking=
to this parameter
of the method call if it’s missing.
Moved API features
We wanted to organize things better in Sopel 8.0, so some of the API features have moved. The old locations will still work until Sopel 9.0, but you’ll be way ahead of the game if you update your plugins now!
Identifier
type moved fromsopel.tools
tosopel.tools.identifiers
SopelMemory
andSopelMemoryWithDefault
types moved fromsopel.tools
tosopel.tools.memories
sopel.tools.check_pid()
moved tosopel.cli.utils
Removed API features
Previously deprecated parts of Sopel’s API have been removed, including:
bot.memory['url_callbacks']
is no longer created or populated by the bot- Plugins wanting to add URL handlers should use the
@sopel.plugin.url()
or@sopel.plugin.url_lazy()
decorators instead
- Plugins wanting to add URL handlers should use the
bot.privileges
channel information (deprecated since Sopel 6.2; replaced bybot.channels
)bot.msg()
method (usebot.say()
)bot.register()
&bot.unregister()
methods (no public-API replacement)- Plugins typically don’t need to do this manually; if your code did, we advise refactoring it to work within Sopel’s public APIs
sopel.cli.utils.redirect_outputs()
function (replaced by standard logging)sopel.config.types.ConfigSection.get_list()
method (use aListAttribute
)sopel.irc.utils.CapReq.module
attribute- Stopgap migration: switch to the
CapReq
type’splugin
attribute - True migration: use the
sopel.plugin.capability
class;CapReq
will be removed in Sopel 9.0
- Stopgap migration: switch to the
sopel.loader.trim_docstring()
(useinspect.getdoc()
)sopel.test_tools
(use thepytest
plugin + mocks/factories)sopel.tools
members of the internal-use kind, deprecated since Sopel 7.1:compile_rule()
,get_action_command_pattern()
,get_action_command_regexp()
,get_command_pattern()
,get_command_regexp()
,get_nickname_command_pattern()
, &get_nickname_command_regexp()
sopel.tools
members of the useless-in-modern-Python kind:Ddict
class (usecollections.defaultdict
)contains()
methods ofSopelMemory
andSopelMemoryWithDefault
(use thein
operator)get_raising_file_and_line()
(use Sopel’s logging facility for exceptions)stdout()
(use Sopel’s logging facility, orprint()
if you must)
-
sopel.web
(moved tosopel.tools.web
in Sopel 7.0)
Deprecated API features
More small legacy API features have been deprecated in version 8.0:
bot.search_url_callbacks()
(usebot.rules.check_url_callbacks(bot, url)
; will be removed in 9.0)sopel.tools.OutputRedirect
(will be removed in 8.1)sopel.tools.stderr()
(we can’t stress enough: plugins should use a logger, not stdout/stderr output; will be removed in 8.1)
We continue to reorganize, modernize, and improve various parts of Sopel’s API whenever possible. Thank you for your patience!