Add network-specific config values.

This commit is contained in:
Valentin Lorentz 2019-08-15 12:22:43 +02:00
parent d4cac026d4
commit 4f024cb0b2
7 changed files with 201 additions and 42 deletions

View File

@ -151,10 +151,10 @@ class Config(callbacks.Plugin):
def _list(self, irc, group):
L = []
for (vname, v) in group._children.items():
if hasattr(group, 'channelValue') and group.channelValue and \
if hasattr(group, '_channelValue') and group._channelValue and \
irc.isChannel(vname) and not v._children:
continue
if hasattr(v, 'channelValue') and v.channelValue:
if hasattr(v, '_channelValue') and v._channelValue:
vname = '#' + vname
if v._added and not all(irc.isChannel, v._added):
vname = '@' + vname
@ -198,11 +198,20 @@ class Config(callbacks.Plugin):
irc.reply(_('There were no matching configuration variables.'))
search = wrap(search, ['lowered']) # XXX compose with withoutSpaces?
def _getValue(self, irc, msg, group, addChannel=False):
def _getValue(self, irc, msg, group, network=None, channel=None, addGlobal=False):
global_value = str(group) or ' '
group = group.getSpecific(
network=network.network, channel=channel, check=None)
value = str(group) or ' '
if addChannel and irc.isChannel(msg.args[0]) and not irc.nested:
s = str(group.get(msg.args[0]))
value = _('Global: %s; %s: %s') % (value, msg.args[0], s)
if addGlobal and not irc.nested:
value = _(
'Global: %(global_value)s; '
'%(channel_name)s @ %(network_name)s: %(channel_value)s') % {
'global_value': global_value,
'channel_name': msg.args[0],
'network_name': irc.network,
'channel_value': value,
}
if hasattr(group, 'value'):
if not group._private:
return (value, None)
@ -230,28 +239,44 @@ class Config(callbacks.Plugin):
irc.errorNoCapability(capability, Raise=True)
@internationalizeDocstring
def channel(self, irc, msg, args, channels, group, value):
"""[<channel>] <name> [<value>]
def channel(self, irc, msg, args, network, channels, group, value):
"""[<network>] [<channel>] <name> [<value>]
If <value> is given, sets the channel configuration variable for <name>
to <value> for <channel>. Otherwise, returns the current channel
to <value> for <channel> on the <network>.
Otherwise, returns the current channel
configuration value of <name>. <channel> is only necessary if the
message isn't sent in the channel itself. More than one channel may
be given at once by separating them with commas."""
if not group.channelValue:
be given at once by separating them with commas.
<network> defaults to the current network."""
if not group._channelValue:
irc.error(_('That configuration variable is not a channel-specific '
'configuration variable.'))
return
if value is not None:
for channel in channels:
assert irc.isChannel(channel)
# Sets the non-network-specific value, for forward
# compatibility, ie. this will work even if the owner rolls
# back Limnoria to an older version.
# It's also an easy way to support plugins which are not
# network-aware.
self._setValue(irc, msg, group.get(channel), value)
if network != '*':
# Set the network-specific value
self._setValue(irc, msg, group.get(':' + network.network).get(channel), value)
irc.replySuccess()
else:
if network == '*':
network = None
values = []
private = None
for channel in channels:
(value, private_value) = self._getValue(irc, msg, group.get(channel))
(value, private_value) = \
self._getValue(irc, msg, group, network, channel)
values.append((channel, value))
if private_value:
private = True
@ -261,7 +286,8 @@ class Config(callbacks.Plugin):
for (channel, value) in values]))
else:
irc.reply(values[0][1])
channel = wrap(channel, ['channels', 'settableConfigVar',
channel = wrap(channel, [optional(first(('literal', '*'), 'networkIrc')),
'channels', 'settableConfigVar',
additional('text')])
@internationalizeDocstring
@ -276,7 +302,10 @@ class Config(callbacks.Plugin):
self._setValue(irc, msg, group, value)
irc.replySuccess()
else:
(value, private) = self._getValue(irc, msg, group, addChannel=group.channelValue)
(value, private) = self._getValue(
irc, msg, group, network=irc,
channel=msg.args[0] if irc.isChannel(msg.args[0]) else None,
addGlobal=group._channelValue)
irc.reply(value, private=private)
config = wrap(config, ['settableConfigVar', additional('text')])

View File

@ -133,13 +133,13 @@ class ConfigTestCase(ChannelPluginTestCase):
'^Completely: Error: ',
frm=self.prefix3)
self.assertResponse('config plugins.Config.%s' % var_name,
'Global: 0; #test: 0')
'Global: 0; #test @ test: 0')
self.assertNotRegexp('config channel plugins.Config.%s 1' % var_name,
'^Completely: Error: ',
frm=self.prefix3)
self.assertResponse('config plugins.Config.%s' % var_name,
'Global: 0; #test: 1')
'Global: 0; #test @ test: 1')
def testOpNonEditable(self):
var_name = 'testOpNonEditable' + random_string()
@ -154,18 +154,18 @@ class ConfigTestCase(ChannelPluginTestCase):
'^Completely: Error: ',
frm=self.prefix3)
self.assertResponse('config plugins.Config.%s' % var_name,
'Global: 0; #test: 0')
'Global: 0; #test @ test: 0')
self.assertRegexp('config channel plugins.Config.%s 1' % var_name,
'^Completely: Error: ',
frm=self.prefix3)
self.assertResponse('config plugins.Config.%s' % var_name,
'Global: 0; #test: 0')
'Global: 0; #test @ test: 0')
self.assertNotRegexp('config channel plugins.Config.%s 1' % var_name,
'^Completely: Error: ')
self.assertResponse('config plugins.Config.%s' % var_name,
'Global: 0; #test: 1')
'Global: 0; #test @ test: 1')
def testChannel(self):
self.assertResponse('config reply.whenAddressedBy.strings ^',
@ -181,6 +181,54 @@ class ConfigTestCase(ChannelPluginTestCase):
self.assertResponse('config channel #testchan1 reply.whenAddressedBy.strings', '.')
self.assertResponse('config channel #testchan2 reply.whenAddressedBy.strings', '.')
def testChannelNetwork(self):
irc = self.irc
irc1 = getTestIrc('testnet1')
irc2 = getTestIrc('testnet2')
irc3 = getTestIrc('testnet3')
conf.supybot.reply.whenAddressedBy.strings.get('#test')._wasSet = False
# 1. Set global
self.assertResponse('config reply.whenAddressedBy.strings ^',
'The operation succeeded.')
# 2. Set for current net + #testchan1
self.assertResponse('config channel #testchan1 reply.whenAddressedBy.strings @',
'The operation succeeded.')
# Exact match for #2:
self.assertResponse('config channel #testchan1 reply.whenAddressedBy.strings', '@')
# 3: Set for #testchan1 for all nets:
self.assertNotError('config channel * #testchan1 reply.whenAddressedBy.strings $')
# Still exact match for #2:
self.assertResponse('config channel #testchan1 reply.whenAddressedBy.strings', '@')
# Inherit from *:
self.assertResponse('config channel testnet1 #testchan1 reply.whenAddressedBy.strings', '$')
self.assertResponse('config channel testnet2 #testchan1 reply.whenAddressedBy.strings', '$')
# 4: Set for testnet1 for #testchan1 and #testchan2:
self.assertNotError('config channel testnet1 #testchan1,#testchan2 reply.whenAddressedBy.strings .')
# 5: Set for testnet2 for #testchan1:
self.assertNotError('config channel testnet2 #testchan1 reply.whenAddressedBy.strings :')
# Inherit from global value (nothing was set of current net or current
# chan):
self.assertResponse('config channel reply.whenAddressedBy.strings', '^')
# Still exact match for #2:
self.assertResponse('config channel #testchan1 reply.whenAddressedBy.strings', '@')
self.assertResponse('config channel %s #testchan1 reply.whenAddressedBy.strings' % irc.network, '@')
# Exact match for #4:
self.assertResponse('config channel testnet1 #testchan1 reply.whenAddressedBy.strings', '.')
self.assertResponse('config channel testnet1 #testchan2 reply.whenAddressedBy.strings', '.')
# Inherit from #5, which set for #testchan1 on all nets
self.assertResponse('config channel testnet3 #testchan1 reply.whenAddressedBy.strings', ':')
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:

View File

@ -59,6 +59,9 @@ supybot.log.plugins.individualLogfiles: False
supybot.protocols.irc.throttleTime: 0
supybot.reply.whenAddressedBy.chars: @
supybot.networks.test.server: should.not.need.this
supybot.networks.testnet1.server: should.not.need.this
supybot.networks.testnet2.server: should.not.need.this
supybot.networks.testnet3.server: should.not.need.this
supybot.nick: test
supybot.databases.users.allowUnregistration: True
""" % {'base_dir': os.getcwd()})

View File

@ -1391,33 +1391,33 @@ class PluginMixin(BasePlugin, irclib.IrcCallback):
else:
self.__parent.__call__(irc, msg)
def registryValue(self, name, channel=None, value=True):
def registryValue(self, name, channel=None, network=None, value=True):
if isinstance(network, bool):
# Network-unaware plugin that uses 'value' as a positional
# argument.
(network, value) = (value, network)
plugin = self.name()
group = conf.supybot.plugins.get(plugin)
names = registry.split(name)
for name in names:
group = group.get(name)
if channel is not None:
if ircutils.isChannel(channel):
group = group.get(channel)
else:
self.log.debug('%s: registryValue got channel=%r', plugin,
channel)
if value:
group = group.getSpecific(network=network, channel=channel)
return group()
else:
return group
def setRegistryValue(self, name, value, channel=None):
def setRegistryValue(self, name, value, channel=None, network=None):
plugin = self.name()
group = conf.supybot.plugins.get(plugin)
names = registry.split(name)
for name in names:
group = group.get(name)
if channel is None:
group.setValue(value)
else:
group.get(channel).setValue(value)
if network:
group = group.get(':' + network)
if channel:
group = group.get(channel)
group.setValue(value)
def userValue(self, name, prefixOrName, default=None):
try:

View File

@ -83,12 +83,14 @@ def registerGroup(Group, name, group=None, **kwargs):
return Group.register(name, group)
def registerGlobalValue(group, name, value):
value.channelValue = False
value._networkValue = False
value._channelValue = False
return group.register(name, value)
def registerChannelValue(group, name, value, opSettable=True):
value._supplyDefault = True
value.channelValue = True
value._networkValue = True
value._channelValue = True
value._opSettable = opSettable
g = group.register(name, value)
gname = g._name.lower()
@ -96,8 +98,13 @@ def registerChannelValue(group, name, value, opSettable=True):
if name.lower().startswith(gname) and len(gname) < len(name):
name = name[len(gname)+1:] # +1 for .
parts = registry.split(name)
if len(parts) == 1 and parts[0] and ircutils.isChannel(parts[0]):
# This gets the channel values so they always persist.
if len(parts) == 2 and parts[0] and parts[0].startswith(':') \
and parts[1] and ircutils.isChannel(parts[1]):
# This gets the network+channel values so they always persist.
g.get(parts[0])()
g.get(parts[0]).get(parts[1])()
elif len(parts) == 1 and parts[0] and ircutils.isChannel(parts[0]):
# Old-style variant of the above, without a network
g.get(parts[0])()
def registerPlugin(name, currentValue=None, public=True):

View File

@ -212,7 +212,7 @@ class Group(object):
s = '%r is not a valid entry in %r' % (attr, self._name)
raise NonExistentRegistryEntry(s)
def __makeChild(self, attr, s):
def _makeChild(self, attr, s):
v = self.__class__(self._default, self._help)
v.set(s)
v._wasSet = False
@ -225,10 +225,12 @@ class Group(object):
return attr in self._children
def __getattr__(self, attr):
if attr in self._children:
if attr.startswith('_'):
object.__getattr__(self, attr)
elif attr in self._children:
return self._children[attr]
elif self._supplyDefault:
return self.__makeChild(attr, str(self))
return self._makeChild(attr, str(self))
else:
self.__nonExistentEntry(attr)
@ -253,7 +255,7 @@ class Group(object):
parts = split(rest)
if len(parts) == 1 and parts[0] == name:
try:
self.__makeChild(name, v)
self._makeChild(name, v)
except InvalidRegistryValue:
# It's probably supposed to be registered later.
pass
@ -328,7 +330,7 @@ class Value(Group):
"""Invalid registry value. If you're getting this message, report it,
because we forgot to put a proper help string here."""
__slots__ = ('__parent', '_default', '_showDefault', '_help', '_callbacks',
'value', 'channelValue', '_opSettable')
'value', '_networkValue', '_channelValue', '_opSettable')
def __init__(self, default, help, setDefault=True,
showDefault=True, **kwargs):
self.__parent = super(Value, self)
@ -340,6 +342,24 @@ class Value(Group):
if setDefault:
self.setValue(default)
def _makeChild(self, attr, s):
v = self.__class__(self._default, self._help)
v.set(s)
v._wasSet = False
if self._networkValue and self._channelValue:
# If this is both a network-specific and channel-specific value,
# then the child is (only) channel-specific.
v._networkValue = False
v._channelValue = True
v._supplyDefault = True
else:
# Otherwise, the child is neither network-specific or
# channel-specific.
v._supplyDefault = False
v._help = '' # Clear this so it doesn't print a bazillion times.
self.register(attr, v)
return v
def error(self, value=_NoValueGiven):
if hasattr(self, 'errormsg') and value is not _NoValueGiven:
try:
@ -356,6 +376,58 @@ class Value(Group):
e.value = self
raise e
def getSpecific(self, network=None, channel=None, check=True):
"""Gets the network-specific and/or channel-specific value of this
Value.
If `check=True` (the default), this will raise an error if `network`
(resp. `channel`) is provided but this Value is not network-specific
(resp. channel-specific). If `check=False`, then `network` and/or
`channel` may be silently ignored.
"""
if network and not self._networkValue:
if check:
raise NonExistentRegistryEntry('%s is not network-specific' %
self._name)
else:
network = None
if channel and not self._channelValue:
if check:
raise NonExistentRegistryEntry('%s is not channel-specific' %
self._name)
else:
channel = None
if network and channel:
# The complicated case. We want a net+chan specific value,
# which may come in three different ways:
#
# 1. it was set explicitely net+chan
# 2. it's inherited from a net specific value (which may itself be
# inherited from the base value)
# 3. it's inherited from the chan specific value (which is not a
# actually a parent in the registry tree, but we need this to
# load configuration from old bots).
#
# The choice between 2 and 3 is done by checking which of the
# net-specific and chan-specific values was set explicitely by
# a user/admin. In case both were, the net-specific value is used
# (there is no particular reason for this, I just think it makes
# more sense).
network_value = self.get(':' + network)
network_channel_value = network_value.get(channel)
channel_value = self.get(channel)
if network_value._wasSet or network_channel_value._wasSet:
# cases 1 and 2
return network_channel_value
else:
# case 3
return channel_value
elif network:
return self.get(':' + network)
elif channel:
return self.get(channel)
else:
return self
def setName(self, *args):
if self._name == 'unset':
self._lastModified = 0

View File

@ -96,8 +96,8 @@ def retry(tries=3):
return newf
return decorator
def getTestIrc():
irc = irclib.Irc('test')
def getTestIrc(name='test'):
irc = irclib.Irc(name)
# Gotta clear the connect messages (USER, NICK, etc.)
while irc.takeMsg():
pass