diff --git a/plugins/Config/plugin.py b/plugins/Config/plugin.py index a5505bf7a..e0e6fb91c 100644 --- a/plugins/Config/plugin.py +++ b/plugins/Config/plugin.py @@ -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): - """[] [] + def channel(self, irc, msg, args, network, channels, group, value): + """[] [] [] If is given, sets the channel configuration variable for - to for . Otherwise, returns the current channel + to for on the . + Otherwise, returns the current channel configuration value of . 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. + 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')]) diff --git a/plugins/Config/test.py b/plugins/Config/test.py index 7ccd11765..b07f28da7 100644 --- a/plugins/Config/test.py +++ b/plugins/Config/test.py @@ -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: diff --git a/scripts/supybot-test b/scripts/supybot-test index 518d006db..d57b0bc0a 100644 --- a/scripts/supybot-test +++ b/scripts/supybot-test @@ -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()}) diff --git a/src/callbacks.py b/src/callbacks.py index 6d1676e97..17d0af319 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -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: diff --git a/src/conf.py b/src/conf.py index 5b83f2d68..b9cbd9fdb 100644 --- a/src/conf.py +++ b/src/conf.py @@ -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): diff --git a/src/registry.py b/src/registry.py index 16d19c5b0..793aa0724 100644 --- a/src/registry.py +++ b/src/registry.py @@ -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 diff --git a/src/test.py b/src/test.py index a90b4786b..816f85056 100644 --- a/src/test.py +++ b/src/test.py @@ -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