Merge branch 'network-config' into netconf-and-ircmsgs-channel

This commit is contained in:
Valentin Lorentz 2019-08-24 15:39:10 +02:00
commit 81968d9970
7 changed files with 201 additions and 42 deletions

View File

@ -151,10 +151,10 @@ class Config(callbacks.Plugin):
def _list(self, irc, group): def _list(self, irc, group):
L = [] L = []
for (vname, v) in group._children.items(): 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: irc.isChannel(vname) and not v._children:
continue continue
if hasattr(v, 'channelValue') and v.channelValue: if hasattr(v, '_channelValue') and v._channelValue:
vname = '#' + vname vname = '#' + vname
if v._added and not all(irc.isChannel, v._added): if v._added and not all(irc.isChannel, v._added):
vname = '@' + vname vname = '@' + vname
@ -198,11 +198,20 @@ class Config(callbacks.Plugin):
irc.reply(_('There were no matching configuration variables.')) irc.reply(_('There were no matching configuration variables.'))
search = wrap(search, ['lowered']) # XXX compose with withoutSpaces? 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 ' ' value = str(group) or ' '
if addChannel and irc.isChannel(msg.args[0]) and not irc.nested: if addGlobal and not irc.nested:
s = str(group.get(msg.args[0])) value = _(
value = _('Global: %s; %s: %s') % (value, msg.args[0], s) '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 hasattr(group, 'value'):
if not group._private: if not group._private:
return (value, None) return (value, None)
@ -230,28 +239,44 @@ class Config(callbacks.Plugin):
irc.errorNoCapability(capability, Raise=True) irc.errorNoCapability(capability, Raise=True)
@internationalizeDocstring @internationalizeDocstring
def channel(self, irc, msg, args, channels, group, value): def channel(self, irc, msg, args, network, channels, group, value):
"""[<channel>] <name> [<value>] """[<network>] [<channel>] <name> [<value>]
If <value> is given, sets the channel configuration variable for <name> 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 configuration value of <name>. <channel> is only necessary if the
message isn't sent in the channel itself. More than one channel may message isn't sent in the channel itself. More than one channel may
be given at once by separating them with commas.""" be given at once by separating them with commas.
if not group.channelValue: <network> defaults to the current network."""
if not group._channelValue:
irc.error(_('That configuration variable is not a channel-specific ' irc.error(_('That configuration variable is not a channel-specific '
'configuration variable.')) 'configuration variable.'))
return return
if value is not None: if value is not None:
for channel in channels: for channel in channels:
assert irc.isChannel(channel) 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) 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() irc.replySuccess()
else: else:
if network == '*':
network = None
values = [] values = []
private = None private = None
for channel in channels: 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)) values.append((channel, value))
if private_value: if private_value:
private = True private = True
@ -261,7 +286,8 @@ class Config(callbacks.Plugin):
for (channel, value) in values])) for (channel, value) in values]))
else: else:
irc.reply(values[0][1]) irc.reply(values[0][1])
channel = wrap(channel, ['channels', 'settableConfigVar', channel = wrap(channel, [optional(first(('literal', '*'), 'networkIrc')),
'channels', 'settableConfigVar',
additional('text')]) additional('text')])
@internationalizeDocstring @internationalizeDocstring
@ -276,7 +302,10 @@ class Config(callbacks.Plugin):
self._setValue(irc, msg, group, value) self._setValue(irc, msg, group, value)
irc.replySuccess() irc.replySuccess()
else: 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) irc.reply(value, private=private)
config = wrap(config, ['settableConfigVar', additional('text')]) config = wrap(config, ['settableConfigVar', additional('text')])

View File

@ -133,13 +133,13 @@ class ConfigTestCase(ChannelPluginTestCase):
'^Completely: Error: ', '^Completely: Error: ',
frm=self.prefix3) frm=self.prefix3)
self.assertResponse('config plugins.Config.%s' % var_name, 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, self.assertNotRegexp('config channel plugins.Config.%s 1' % var_name,
'^Completely: Error: ', '^Completely: Error: ',
frm=self.prefix3) frm=self.prefix3)
self.assertResponse('config plugins.Config.%s' % var_name, self.assertResponse('config plugins.Config.%s' % var_name,
'Global: 0; #test: 1') 'Global: 0; #test @ test: 1')
def testOpNonEditable(self): def testOpNonEditable(self):
var_name = 'testOpNonEditable' + random_string() var_name = 'testOpNonEditable' + random_string()
@ -154,18 +154,18 @@ class ConfigTestCase(ChannelPluginTestCase):
'^Completely: Error: ', '^Completely: Error: ',
frm=self.prefix3) frm=self.prefix3)
self.assertResponse('config plugins.Config.%s' % var_name, 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, self.assertRegexp('config channel plugins.Config.%s 1' % var_name,
'^Completely: Error: ', '^Completely: Error: ',
frm=self.prefix3) frm=self.prefix3)
self.assertResponse('config plugins.Config.%s' % var_name, 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, self.assertNotRegexp('config channel plugins.Config.%s 1' % var_name,
'^Completely: Error: ') '^Completely: Error: ')
self.assertResponse('config plugins.Config.%s' % var_name, self.assertResponse('config plugins.Config.%s' % var_name,
'Global: 0; #test: 1') 'Global: 0; #test @ test: 1')
def testChannel(self): def testChannel(self):
self.assertResponse('config reply.whenAddressedBy.strings ^', 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 #testchan1 reply.whenAddressedBy.strings', '.')
self.assertResponse('config channel #testchan2 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: # 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.protocols.irc.throttleTime: 0
supybot.reply.whenAddressedBy.chars: @ supybot.reply.whenAddressedBy.chars: @
supybot.networks.test.server: should.not.need.this 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.nick: test
supybot.databases.users.allowUnregistration: True supybot.databases.users.allowUnregistration: True
""" % {'base_dir': os.getcwd()}) """ % {'base_dir': os.getcwd()})

View File

@ -1391,33 +1391,33 @@ class PluginMixin(BasePlugin, irclib.IrcCallback):
else: else:
self.__parent.__call__(irc, msg) 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() plugin = self.name()
group = conf.supybot.plugins.get(plugin) group = conf.supybot.plugins.get(plugin)
names = registry.split(name) names = registry.split(name)
for name in names: for name in names:
group = group.get(name) 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: if value:
group = group.getSpecific(network=network, channel=channel)
return group() return group()
else: else:
return group return group
def setRegistryValue(self, name, value, channel=None): def setRegistryValue(self, name, value, channel=None, network=None):
plugin = self.name() plugin = self.name()
group = conf.supybot.plugins.get(plugin) group = conf.supybot.plugins.get(plugin)
names = registry.split(name) names = registry.split(name)
for name in names: for name in names:
group = group.get(name) group = group.get(name)
if channel is None: if network:
group.setValue(value) group = group.get(':' + network)
else: if channel:
group.get(channel).setValue(value) group = group.get(channel)
group.setValue(value)
def userValue(self, name, prefixOrName, default=None): def userValue(self, name, prefixOrName, default=None):
try: try:

View File

@ -83,12 +83,14 @@ def registerGroup(Group, name, group=None, **kwargs):
return Group.register(name, group) return Group.register(name, group)
def registerGlobalValue(group, name, value): def registerGlobalValue(group, name, value):
value.channelValue = False value._networkValue = False
value._channelValue = False
return group.register(name, value) return group.register(name, value)
def registerChannelValue(group, name, value, opSettable=True): def registerChannelValue(group, name, value, opSettable=True):
value._supplyDefault = True value._supplyDefault = True
value.channelValue = True value._networkValue = True
value._channelValue = True
value._opSettable = opSettable value._opSettable = opSettable
g = group.register(name, value) g = group.register(name, value)
gname = g._name.lower() 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): if name.lower().startswith(gname) and len(gname) < len(name):
name = name[len(gname)+1:] # +1 for . name = name[len(gname)+1:] # +1 for .
parts = registry.split(name) parts = registry.split(name)
if len(parts) == 1 and parts[0] and ircutils.isChannel(parts[0]): if len(parts) == 2 and parts[0] and parts[0].startswith(':') \
# This gets the channel values so they always persist. 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])() g.get(parts[0])()
def registerPlugin(name, currentValue=None, public=True): 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) s = '%r is not a valid entry in %r' % (attr, self._name)
raise NonExistentRegistryEntry(s) raise NonExistentRegistryEntry(s)
def __makeChild(self, attr, s): def _makeChild(self, attr, s):
v = self.__class__(self._default, self._help) v = self.__class__(self._default, self._help)
v.set(s) v.set(s)
v._wasSet = False v._wasSet = False
@ -225,10 +225,12 @@ class Group(object):
return attr in self._children return attr in self._children
def __getattr__(self, attr): 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] return self._children[attr]
elif self._supplyDefault: elif self._supplyDefault:
return self.__makeChild(attr, str(self)) return self._makeChild(attr, str(self))
else: else:
self.__nonExistentEntry(attr) self.__nonExistentEntry(attr)
@ -253,7 +255,7 @@ class Group(object):
parts = split(rest) parts = split(rest)
if len(parts) == 1 and parts[0] == name: if len(parts) == 1 and parts[0] == name:
try: try:
self.__makeChild(name, v) self._makeChild(name, v)
except InvalidRegistryValue: except InvalidRegistryValue:
# It's probably supposed to be registered later. # It's probably supposed to be registered later.
pass pass
@ -328,7 +330,7 @@ class Value(Group):
"""Invalid registry value. If you're getting this message, report it, """Invalid registry value. If you're getting this message, report it,
because we forgot to put a proper help string here.""" because we forgot to put a proper help string here."""
__slots__ = ('__parent', '_default', '_showDefault', '_help', '_callbacks', __slots__ = ('__parent', '_default', '_showDefault', '_help', '_callbacks',
'value', 'channelValue', '_opSettable') 'value', '_networkValue', '_channelValue', '_opSettable')
def __init__(self, default, help, setDefault=True, def __init__(self, default, help, setDefault=True,
showDefault=True, **kwargs): showDefault=True, **kwargs):
self.__parent = super(Value, self) self.__parent = super(Value, self)
@ -340,6 +342,24 @@ class Value(Group):
if setDefault: if setDefault:
self.setValue(default) 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): def error(self, value=_NoValueGiven):
if hasattr(self, 'errormsg') and value is not _NoValueGiven: if hasattr(self, 'errormsg') and value is not _NoValueGiven:
try: try:
@ -356,6 +376,58 @@ class Value(Group):
e.value = self e.value = self
raise e 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): def setName(self, *args):
if self._name == 'unset': if self._name == 'unset':
self._lastModified = 0 self._lastModified = 0

View File

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