i need a rundown, stat

This commit is contained in:
iou1name 2018-01-18 23:43:27 -05:00
parent e6e8d544d2
commit 5b1909cac5
10 changed files with 446 additions and 452 deletions

2
bot.py
View File

@ -311,7 +311,7 @@ class Sopel(irc.Bot):
# Now that we've sent the first part, we need to send the rest. Doing # Now that we've sent the first part, we need to send the rest. Doing
# this recursively seems easier to me than iteratively # this recursively seems easier to me than iteratively
if excess: if excess:
self.say(recipient, excess, max_messages - 1) self.say(excess, recipient, max_messages - 1)
def notice(self, text, dest): def notice(self, text, dest):
"""Send an IRC NOTICE to a user or a channel. """Send an IRC NOTICE to a user or a channel.

View File

@ -1,4 +1,5 @@
# coding=utf-8 #! /usr/bin/env python3
# -*- coding: utf-8 -*-
""" """
The config object provides a simplified to access Sopel's configuration file. The config object provides a simplified to access Sopel's configuration file.
The sections of the file are attributes of the object, and the keys in the The sections of the file are attributes of the object, and the keys in the
@ -13,9 +14,6 @@ object is initialized.
.. versionadded:: 6.0.0 .. versionadded:: 6.0.0
""" """
# Copyright 2012-2015, Elsie Powell, embolalia.com
# Copyright © 2012, Elad Alfassa <elad@fedoraproject.org>
# Licensed under the Eiffel Forum License 2.
from __future__ import unicode_literals, absolute_import, print_function, division from __future__ import unicode_literals, absolute_import, print_function, division
@ -25,239 +23,235 @@ from tools import get_input
import loader import loader
import os import os
import sys import sys
if sys.version_info.major < 3: import configparser as ConfigParser
import ConfigParser
else:
basestring = str
import configparser as ConfigParser
import config.core_section import config.core_section
from config.types import StaticSection from config.types import StaticSection
class ConfigurationError(Exception): class ConfigurationError(Exception):
""" Exception type for configuration errors """ """ Exception type for configuration errors """
def __init__(self, value): def __init__(self, value):
self.value = value self.value = value
def __str__(self): def __str__(self):
return 'ConfigurationError: %s' % self.value return 'ConfigurationError: %s' % self.value
class Config(object): class Config(object):
def __init__(self, filename, validate=True): def __init__(self, filename, validate=True):
"""The bot's configuration. """The bot's configuration.
The given filename will be associated with the configuration, and is The given filename will be associated with the configuration, and is
the file which will be written if write() is called. If load is not the file which will be written if write() is called. If load is not
given or True, the configuration object will load the attributes from given or True, the configuration object will load the attributes from
the file at filename. the file at filename.
A few default values will be set here if they are not defined in the A few default values will be set here if they are not defined in the
config file, or a config file is not loaded. They are documented below. config file, or a config file is not loaded. They are documented below.
""" """
self.filename = filename self.filename = filename
"""The config object's associated file, as noted above.""" """The config object's associated file, as noted above."""
self.parser = ConfigParser.RawConfigParser(allow_no_value=True) self.parser = ConfigParser.RawConfigParser(allow_no_value=True)
self.parser.read(self.filename) self.parser.read(self.filename)
self.define_section('core', config.core_section.CoreSection, self.define_section('core', config.core_section.CoreSection,
validate=validate) validate=validate)
self.get = self.parser.get self.get = self.parser.get
@property @property
def homedir(self): def homedir(self):
"""An alias to config.core.homedir""" """An alias to config.core.homedir"""
# Technically it's the other way around, so we can bootstrap filename # Technically it's the other way around, so we can bootstrap filename
# attributes in the core section, but whatever. # attributes in the core section, but whatever.
configured = None configured = None
if self.parser.has_option('core', 'homedir'): if self.parser.has_option('core', 'homedir'):
configured = self.parser.get('core', 'homedir') configured = self.parser.get('core', 'homedir')
if configured: if configured:
return configured return configured
else: else:
return os.path.dirname(self.filename) return os.path.dirname(self.filename)
def save(self): def save(self):
"""Save all changes to the config file.""" """Save all changes to the config file."""
cfgfile = open(self.filename, 'w') cfgfile = open(self.filename, 'w')
self.parser.write(cfgfile) self.parser.write(cfgfile)
cfgfile.flush() cfgfile.flush()
cfgfile.close() cfgfile.close()
def add_section(self, name): def add_section(self, name):
"""Add a section to the config file. """Add a section to the config file.
Returns ``False`` if already exists. Returns ``False`` if already exists.
""" """
try: try:
return self.parser.add_section(name) return self.parser.add_section(name)
except ConfigParser.DuplicateSectionError: except ConfigParser.DuplicateSectionError:
return False return False
def define_section(self, name, cls_, validate=True): def define_section(self, name, cls_, validate=True):
"""Define the available settings in a section. """Define the available settings in a section.
``cls_`` must be a subclass of ``StaticSection``. If the section has ``cls_`` must be a subclass of ``StaticSection``. If the section has
already been defined with a different class, ValueError is raised. already been defined with a different class, ValueError is raised.
If ``validate`` is True, the section's values will be validated, and an If ``validate`` is True, the section's values will be validated, and an
exception raised if they are invalid. This is desirable in a module's exception raised if they are invalid. This is desirable in a module's
setup function, for example, but might not be in the configure function. setup function, for example, but might not be in the configure function.
""" """
if not issubclass(cls_, StaticSection): if not issubclass(cls_, StaticSection):
raise ValueError("Class must be a subclass of StaticSection.") raise ValueError("Class must be a subclass of StaticSection.")
current = getattr(self, name, None) current = getattr(self, name, None)
current_name = str(current.__class__) current_name = str(current.__class__)
new_name = str(cls_) new_name = str(cls_)
if (current is not None and not isinstance(current, self.ConfigSection) if (current is not None and not isinstance(current, self.ConfigSection)
and not current_name == new_name): and not current_name == new_name):
raise ValueError( raise ValueError(
"Can not re-define class for section from {} to {}.".format( "Can not re-define class for section from {} to {}.".format(
current_name, new_name) current_name, new_name)
) )
setattr(self, name, cls_(self, name, validate=validate)) setattr(self, name, cls_(self, name, validate=validate))
class ConfigSection(object): class ConfigSection(object):
"""Represents a section of the config file. """Represents a section of the config file.
Contains all keys in thesection as attributes. Contains all keys in thesection as attributes.
""" """
def __init__(self, name, items, parent): def __init__(self, name, items, parent):
object.__setattr__(self, '_name', name) object.__setattr__(self, '_name', name)
object.__setattr__(self, '_parent', parent) object.__setattr__(self, '_parent', parent)
for item in items: for item in items:
value = item[1].strip() value = item[1].strip()
if not value.lower() == 'none': if not value.lower() == 'none':
if value.lower() == 'false': if value.lower() == 'false':
value = False value = False
object.__setattr__(self, item[0], value) object.__setattr__(self, item[0], value)
def __getattr__(self, name): def __getattr__(self, name):
return None return None
def __setattr__(self, name, value): def __setattr__(self, name, value):
object.__setattr__(self, name, value) object.__setattr__(self, name, value)
if type(value) is list: if type(value) is list:
value = ','.join(value) value = ','.join(value)
self._parent.parser.set(self._name, name, value) self._parent.parser.set(self._name, name, value)
def get_list(self, name): def get_list(self, name):
value = getattr(self, name) value = getattr(self, name)
if not value: if not value:
return [] return []
if isinstance(value, basestring): if isinstance(value, str):
value = value.split(',') value = value.split(',')
# Keep the split value, so we don't have to keep doing this # Keep the split value, so we don't have to keep doing this
setattr(self, name, value) setattr(self, name, value)
return value return value
def __getattr__(self, name): def __getattr__(self, name):
if name in self.parser.sections(): if name in self.parser.sections():
items = self.parser.items(name) items = self.parser.items(name)
section = self.ConfigSection(name, items, self) # Return a section section = self.ConfigSection(name, items, self) # Return a section
setattr(self, name, section) setattr(self, name, section)
return section return section
else: else:
raise AttributeError("%r object has no attribute %r" raise AttributeError("%r object has no attribute %r"
% (type(self).__name__, name)) % (type(self).__name__, name))
def option(self, question, default=False): def option(self, question, default=False):
"""Ask "y/n" and return the corresponding boolean answer. """Ask "y/n" and return the corresponding boolean answer.
Show user in terminal a "y/n" prompt, and return true or false based on Show user in terminal a "y/n" prompt, and return true or false based on
the response. If default is passed as true, the default will be shown the response. If default is passed as true, the default will be shown
as ``[y]``, else it will be ``[n]``. ``question`` should be phrased as as ``[y]``, else it will be ``[n]``. ``question`` should be phrased as
a question, but without a question mark at the end. a question, but without a question mark at the end.
""" """
d = 'n' d = 'n'
if default: if default:
d = 'y' d = 'y'
ans = get_input(question + ' (y/n)? [' + d + '] ') ans = get_input(question + ' (y/n)? [' + d + '] ')
if not ans: if not ans:
ans = d ans = d
return ans.lower() == 'y' return ans.lower() == 'y'
def _modules(self): def _modules(self):
home = os.getcwd() home = os.getcwd()
modules_dir = os.path.join(home, 'modules') modules_dir = os.path.join(home, 'modules')
filenames = sopel.loader.enumerate_modules(self) filenames = sopel.loader.enumerate_modules(self)
os.sys.path.insert(0, modules_dir) os.sys.path.insert(0, modules_dir)
for name, mod_spec in iteritems(filenames): for name, mod_spec in iteritems(filenames):
path, type_ = mod_spec path, type_ = mod_spec
try: try:
module, _ = sopel.loader.load_module(name, path, type_) module, _ = sopel.loader.load_module(name, path, type_)
except Exception as e: except Exception as e:
filename, lineno = sopel.tools.get_raising_file_and_line() filename, lineno = sopel.tools.get_raising_file_and_line()
rel_path = os.path.relpath(filename, os.path.dirname(__file__)) rel_path = os.path.relpath(filename, os.path.dirname(__file__))
raising_stmt = "%s:%d" % (rel_path, lineno) raising_stmt = "%s:%d" % (rel_path, lineno)
stderr("Error loading %s: %s (%s)" % (name, e, raising_stmt)) stderr("Error loading %s: %s (%s)" % (name, e, raising_stmt))
else: else:
if hasattr(module, 'configure'): if hasattr(module, 'configure'):
prompt = name + ' module' prompt = name + ' module'
if module.__doc__: if module.__doc__:
doc = module.__doc__.split('\n', 1)[0] doc = module.__doc__.split('\n', 1)[0]
if doc: if doc:
prompt = doc prompt = doc
prompt = 'Configure {} (y/n)? [n]'.format(prompt) prompt = 'Configure {} (y/n)? [n]'.format(prompt)
do_configure = get_input(prompt) do_configure = get_input(prompt)
do_configure = do_configure and do_configure.lower() == 'y' do_configure = do_configure and do_configure.lower() == 'y'
if do_configure: if do_configure:
module.configure(self) module.configure(self)
self.save() self.save()
def _wizard(section, config=None): def _wizard(section, config=None):
dotdir = os.path.expanduser('~/.sopel') dotdir = os.path.expanduser('~/.sopel')
configpath = os.path.join(dotdir, (config or 'default') + '.cfg') configpath = os.path.join(dotdir, (config or 'default') + '.cfg')
if section == 'all': if section == 'all':
_create_config(configpath) _create_config(configpath)
elif section == 'mod': elif section == 'mod':
_check_dir(False) _check_dir(False)
if not os.path.isfile(configpath): if not os.path.isfile(configpath):
print("No config file found." + print("No config file found." +
" Please make one before configuring these options.") " Please make one before configuring these options.")
sys.exit(1) sys.exit(1)
config = Config(configpath, validate=False) config = Config(configpath, validate=False)
config._modules() config._modules()
def _check_dir(create=True): def _check_dir(create=True):
dotdir = os.path.join(os.path.expanduser('~'), '.sopel') dotdir = os.path.join(os.path.expanduser('~'), '.sopel')
if not os.path.isdir(dotdir): if not os.path.isdir(dotdir):
if create: if create:
print('Creating a config directory at ~/.sopel...') print('Creating a config directory at ~/.sopel...')
try: try:
os.makedirs(dotdir) os.makedirs(dotdir)
except Exception as e: except Exception as e:
print('There was a problem creating %s:' % dotdir, file=sys.stderr) print('There was a problem creating %s:' % dotdir, file=sys.stderr)
print('%s, %s' % (e.__class__, str(e)), file=sys.stderr) print('%s, %s' % (e.__class__, str(e)), file=sys.stderr)
print('Please fix this and then run Sopel again.', file=sys.stderr) print('Please fix this and then run Sopel again.', file=sys.stderr)
sys.exit(1) sys.exit(1)
else: else:
print("No config file found. Please make one before configuring these options.") print("No config file found. Please make one before configuring these options.")
sys.exit(1) sys.exit(1)
def _create_config(configpath): def _create_config(configpath):
_check_dir() _check_dir()
print("Please answer the following questions" + print("Please answer the following questions" +
" to create your configuration file:\n") " to create your configuration file:\n")
try: try:
config = Config(configpath, validate=False) config = Config(configpath, validate=False)
sopel.config.core_section.configure(config) sopel.config.core_section.configure(config)
if config.option( if config.option(
'Would you like to see if there are any modules' 'Would you like to see if there are any modules'
' that need configuring' ' that need configuring'
): ):
config._modules() config._modules()
config.save() config.save()
except Exception: except Exception:
print("Encountered an error while writing the config file." + print("Encountered an error while writing the config file." +
" This shouldn't happen. Check permissions.") " This shouldn't happen. Check permissions.")
raise raise
sys.exit(1) sys.exit(1)
print("Config file written sucessfully!") print("Config file written sucessfully!")

4
irc.py
View File

@ -332,12 +332,12 @@ class Bot(asynchat.async_chat):
LOGGER.error("Could not save traceback from %s to file: %s", trigger.sender, str(e)) LOGGER.error("Could not save traceback from %s to file: %s", trigger.sender, str(e))
if trigger and self.config.core.reply_errors and trigger.sender is not None: if trigger and self.config.core.reply_errors and trigger.sender is not None:
self.msg(trigger.sender, signature) self.say(signature, trigger.sender)
if trigger: if trigger:
LOGGER.error('Exception from {}: {} ({})'.format(trigger.sender, str(signature), trigger.raw)) LOGGER.error('Exception from {}: {} ({})'.format(trigger.sender, str(signature), trigger.raw))
except Exception as e: except Exception as e:
if trigger and self.config.core.reply_errors and trigger.sender is not None: if trigger and self.config.core.reply_errors and trigger.sender is not None:
self.msg(trigger.sender, "Got an error.") self.say("Got an error.", trigger.sender)
if trigger: if trigger:
LOGGER.error('Exception from {}: {} ({})'.format(trigger.sender, str(e), trigger.raw)) LOGGER.error('Exception from {}: {} ({})'.format(trigger.sender, str(e), trigger.raw))

View File

@ -1,5 +1,4 @@
#! /usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" """
Methods for loading modules. Methods for loading modules.
""" """

View File

@ -1,4 +1,3 @@
# coding=utf-8
"""This contains decorators and tools for creating callable plugin functions. """This contains decorators and tools for creating callable plugin functions.
""" """
# Copyright 2013, Ari Koivula, <ari@koivu.la> # Copyright 2013, Ari Koivula, <ari@koivu.la>

View File

@ -1,5 +1,4 @@
#! /usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" """
ASCII ASCII
""" """

View File

@ -1,5 +1,4 @@
#! /usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
""" """
help.py - Sopel Help Module help.py - Sopel Help Module
Copyright 2008, Sean B. Palmer, inamidst.com Copyright 2008, Sean B. Palmer, inamidst.com

View File

@ -70,9 +70,9 @@ def setup(bot):
for oldtime in oldtimes: for oldtime in oldtimes:
for (channel, nick, message) in bot.rdb[oldtime]: for (channel, nick, message) in bot.rdb[oldtime]:
if message: if message:
bot.say(nick + ': ' + message) bot.say(nick + ': ' + message, channel)
else: else:
bot.say(nick + '!') bot.say(nick + '!', channel)
del bot.rdb[oldtime] del bot.rdb[oldtime]
dump_database(bot.rfn, bot.rdb) dump_database(bot.rfn, bot.rdb)
time.sleep(2.5) time.sleep(2.5)

23
modules/rundown.py Executable file
View File

@ -0,0 +1,23 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Redpill on the Bogdanovs.
"""
import os
import random
from module import commands, example
@commands("rundown")
@example(".rundown")
def grog(bot, trigger):
"""
Provides rundown on demand.
"""
if trigger.group(2) in ["-c", "--cabal"]:
with open(os.path.join(bot.config.homedir, "cabaldown.txt"), "r") as file:
data = file.read()
else:
with open(os.path.join(bot.config.homedir, "rundown.txt"), "r") as file:
data = file.read()
bot.say(data)

View File

@ -1,19 +1,10 @@
# coding=utf-8 #! /usr/bin/env python3
"""Useful miscellaneous tools and shortcuts for Sopel modules # -*- coding: utf-8 -*-
"""
Useful miscellaneous tools and shortcuts for Sopel modules
*Availability: 3+* *Availability: 3+*
""" """
# tools.py - Sopel misc tools
# Copyright 2008, Sean B. Palmer, inamidst.com
# Copyright © 2012, Elad Alfassa <elad@fedoraproject.org>
# Copyright 2012, Elsie Powell, embolalia.com
# Licensed under the Eiffel Forum License 2.
# https://sopel.chat
from __future__ import unicode_literals, absolute_import, print_function, division
import sys import sys
import os import os
import re import re
@ -24,82 +15,72 @@ from collections import defaultdict
from tools._events import events # NOQA from tools._events import events # NOQA
if sys.version_info.major >= 3: iteritems = dict.items
raw_input = input itervalues = dict.values
unicode = str iterkeys = dict.keys
iteritems = dict.items
itervalues = dict.values
iterkeys = dict.keys
else:
iteritems = dict.iteritems
itervalues = dict.itervalues
iterkeys = dict.iterkeys
_channel_prefixes = ('#', '&', '+', '!') _channel_prefixes = ('#', '&', '+', '!')
def get_input(prompt): def get_input(prompt):
"""Get decoded input from the terminal (equivalent to python 3's ``input``). """Get decoded input from the terminal (equivalent to python 3's ``input``).
""" """
if sys.version_info.major >= 3: return input(prompt)
return input(prompt)
else:
return raw_input(prompt).decode('utf8')
def get_raising_file_and_line(tb=None): def get_raising_file_and_line(tb=None):
"""Return the file and line number of the statement that raised the tb. """Return the file and line number of the statement that raised the tb.
Returns: (filename, lineno) tuple Returns: (filename, lineno) tuple
""" """
if not tb: if not tb:
tb = sys.exc_info()[2] tb = sys.exc_info()[2]
filename, lineno, _context, _line = traceback.extract_tb(tb)[-1] filename, lineno, _context, _line = traceback.extract_tb(tb)[-1]
return filename, lineno return filename, lineno
def get_command_regexp(prefix, command): def get_command_regexp(prefix, command):
"""Return a compiled regexp object that implements the command.""" """Return a compiled regexp object that implements the command."""
# Escape all whitespace with a single backslash. This ensures that regexp # Escape all whitespace with a single backslash. This ensures that regexp
# in the prefix is treated as it was before the actual regexp was changed # in the prefix is treated as it was before the actual regexp was changed
# to use the verbose syntax. # to use the verbose syntax.
prefix = re.sub(r"(\s)", r"\\\1", prefix) prefix = re.sub(r"(\s)", r"\\\1", prefix)
# This regexp match equivalently and produce the same # This regexp match equivalently and produce the same
# groups 1 and 2 as the old regexp: r'^%s(%s)(?: +(.*))?$' # groups 1 and 2 as the old regexp: r'^%s(%s)(?: +(.*))?$'
# The only differences should be handling all whitespace # The only differences should be handling all whitespace
# like spaces and the addition of groups 3-6. # like spaces and the addition of groups 3-6.
pattern = r""" pattern = r"""
(?:{prefix})({command}) # Command as group 1. (?:{prefix})({command}) # Command as group 1.
(?:\s+ # Whitespace to end command. (?:\s+ # Whitespace to end command.
( # Rest of the line as group 2. ( # Rest of the line as group 2.
(?:(\S+))? # Parameters 1-4 as groups 3-6. (?:(\S+))? # Parameters 1-4 as groups 3-6.
(?:\s+(\S+))? (?:\s+(\S+))?
(?:\s+(\S+))? (?:\s+(\S+))?
(?:\s+(\S+))? (?:\s+(\S+))?
.* # Accept anything after the parameters. .* # Accept anything after the parameters.
# Leave it up to the module to parse # Leave it up to the module to parse
# the line. # the line.
))? # Group 2 must be None, if there are no ))? # Group 2 must be None, if there are no
# parameters. # parameters.
$ # EoL, so there are no partial matches. $ # EoL, so there are no partial matches.
""".format(prefix=prefix, command=command) """.format(prefix=prefix, command=command)
return re.compile(pattern, re.IGNORECASE | re.VERBOSE) return re.compile(pattern, re.IGNORECASE | re.VERBOSE)
def deprecated(old): def deprecated(old):
def new(*args, **kwargs): def new(*args, **kwargs):
print('Function %s is deprecated.' % old.__name__, file=sys.stderr) print('Function %s is deprecated.' % old.__name__, file=sys.stderr)
trace = traceback.extract_stack() trace = traceback.extract_stack()
for line in traceback.format_list(trace[:-1]): for line in traceback.format_list(trace[:-1]):
stderr(line[:-1]) stderr(line[:-1])
return old(*args, **kwargs) return old(*args, **kwargs)
new.__doc__ = old.__doc__ new.__doc__ = old.__doc__
new.__name__ = old.__name__ new.__name__ = old.__name__
return new return new
# from # from
@ -107,144 +88,144 @@ def deprecated(old):
# A simple class to make mutli dimensional dict easy to use # A simple class to make mutli dimensional dict easy to use
class Ddict(dict): class Ddict(dict):
"""Class for multi-dimensional ``dict``. """Class for multi-dimensional ``dict``.
A simple helper class to ease the creation of multi-dimensional ``dict``\s. A simple helper class to ease the creation of multi-dimensional ``dict``\s.
""" """
def __init__(self, default=None): def __init__(self, default=None):
self.default = default self.default = default
def __getitem__(self, key): def __getitem__(self, key):
if key not in self: if key not in self:
self[key] = self.default() self[key] = self.default()
return dict.__getitem__(self, key) return dict.__getitem__(self, key)
class Identifier(unicode): class Identifier(str):
"""A `unicode` subclass which acts appropriately for IRC identifiers. """A `str` subclass which acts appropriately for IRC identifiers.
When used as normal `unicode` objects, case will be preserved. When used as normal `str` objects, case will be preserved.
However, when comparing two Identifier objects, or comparing a Identifier However, when comparing two Identifier objects, or comparing a Identifier
object with a `unicode` object, the comparison will be case insensitive. object with a `str` object, the comparison will be case insensitive.
This case insensitivity includes the case convention conventions regarding This case insensitivity includes the case convention conventions regarding
``[]``, ``{}``, ``|``, ``\\``, ``^`` and ``~`` described in RFC 2812. ``[]``, ``{}``, ``|``, ``\\``, ``^`` and ``~`` described in RFC 2812.
""" """
def __new__(cls, identifier): def __new__(cls, identifier):
# According to RFC2812, identifiers have to be in the ASCII range. # According to RFC2812, identifiers have to be in the ASCII range.
# However, I think it's best to let the IRCd determine that, and we'll # However, I think it's best to let the IRCd determine that, and we'll
# just assume unicode. It won't hurt anything, and is more internally # just assume str. It won't hurt anything, and is more internally
# consistent. And who knows, maybe there's another use case for this # consistent. And who knows, maybe there's another use case for this
# weird case convention. # weird case convention.
s = unicode.__new__(cls, identifier) s = str.__new__(cls, identifier)
s._lowered = Identifier._lower(identifier) s._lowered = Identifier._lower(identifier)
return s return s
def lower(self): def lower(self):
"""Return the identifier converted to lower-case per RFC 2812.""" """Return the identifier converted to lower-case per RFC 2812."""
return self._lowered return self._lowered
@staticmethod @staticmethod
def _lower(identifier): def _lower(identifier):
"""Returns `identifier` in lower case per RFC 2812.""" """Returns `identifier` in lower case per RFC 2812."""
# The tilde replacement isn't needed for identifiers, but is for # The tilde replacement isn't needed for identifiers, but is for
# channels, which may be useful at some point in the future. # channels, which may be useful at some point in the future.
low = identifier.lower().replace('{', '[').replace('}', ']') low = identifier.lower().replace('{', '[').replace('}', ']')
low = low.replace('|', '\\').replace('^', '~') low = low.replace('|', '\\').replace('^', '~')
return low return low
def __repr__(self): def __repr__(self):
return "%s(%r)" % ( return "%s(%r)" % (
self.__class__.__name__, self.__class__.__name__,
self.__str__() self.__str__()
) )
def __hash__(self): def __hash__(self):
return self._lowered.__hash__() return self._lowered.__hash__()
def __lt__(self, other): def __lt__(self, other):
if isinstance(other, Identifier): if isinstance(other, Identifier):
return self._lowered < other._lowered return self._lowered < other._lowered
return self._lowered < Identifier._lower(other) return self._lowered < Identifier._lower(other)
def __le__(self, other): def __le__(self, other):
if isinstance(other, Identifier): if isinstance(other, Identifier):
return self._lowered <= other._lowered return self._lowered <= other._lowered
return self._lowered <= Identifier._lower(other) return self._lowered <= Identifier._lower(other)
def __gt__(self, other): def __gt__(self, other):
if isinstance(other, Identifier): if isinstance(other, Identifier):
return self._lowered > other._lowered return self._lowered > other._lowered
return self._lowered > Identifier._lower(other) return self._lowered > Identifier._lower(other)
def __ge__(self, other): def __ge__(self, other):
if isinstance(other, Identifier): if isinstance(other, Identifier):
return self._lowered >= other._lowered return self._lowered >= other._lowered
return self._lowered >= Identifier._lower(other) return self._lowered >= Identifier._lower(other)
def __eq__(self, other): def __eq__(self, other):
if isinstance(other, Identifier): if isinstance(other, Identifier):
return self._lowered == other._lowered return self._lowered == other._lowered
return self._lowered == Identifier._lower(other) return self._lowered == Identifier._lower(other)
def __ne__(self, other): def __ne__(self, other):
return not (self == other) return not (self == other)
def is_nick(self): def is_nick(self):
"""Returns True if the Identifier is a nickname (as opposed to channel) """Returns True if the Identifier is a nickname (as opposed to channel)
""" """
return self and not self.startswith(_channel_prefixes) return self and not self.startswith(_channel_prefixes)
class OutputRedirect(object): class OutputRedirect(object):
"""Redirect te output to the terminal and a log file. """Redirect te output to the terminal and a log file.
A simplified object used to write to both the terminal and a log file. A simplified object used to write to both the terminal and a log file.
""" """
def __init__(self, logpath, stderr=False, quiet=False): def __init__(self, logpath, stderr=False, quiet=False):
"""Create an object which will to to a file and the terminal. """Create an object which will to to a file and the terminal.
Create an object which will log to the file at ``logpath`` as well as Create an object which will log to the file at ``logpath`` as well as
the terminal. the terminal.
If ``stderr`` is given and true, it will write to stderr rather than If ``stderr`` is given and true, it will write to stderr rather than
stdout. stdout.
If ``quiet`` is given and True, data will be written to the log file If ``quiet`` is given and True, data will be written to the log file
only, but not the terminal. only, but not the terminal.
""" """
self.logpath = logpath self.logpath = logpath
self.stderr = stderr self.stderr = stderr
self.quiet = quiet self.quiet = quiet
def write(self, string): def write(self, string):
"""Write the given ``string`` to the logfile and terminal.""" """Write the given ``string`` to the logfile and terminal."""
if not self.quiet: if not self.quiet:
try: try:
if self.stderr: if self.stderr:
sys.__stderr__.write(string) sys.__stderr__.write(string)
else: else:
sys.__stdout__.write(string) sys.__stdout__.write(string)
except: except:
pass pass
with codecs.open(self.logpath, 'ab', encoding="utf8", with codecs.open(self.logpath, 'ab', encoding="utf8",
errors='xmlcharrefreplace') as logfile: errors='xmlcharrefreplace') as logfile:
try: try:
logfile.write(string) logfile.write(string)
except UnicodeDecodeError: except strDecodeError:
# we got an invalid string, safely encode it to utf-8 # we got an invalid string, safely encode it to utf-8
logfile.write(unicode(string, 'utf8', errors="replace")) logfile.write(str(string, 'utf8', errors="replace"))
def flush(self): def flush(self):
if self.stderr: if self.stderr:
sys.__stderr__.flush() sys.__stderr__.flush()
else: else:
sys.__stdout__.flush() sys.__stdout__.flush()
# These seems to trace back to when we thought we needed a try/except on prints, # These seems to trace back to when we thought we needed a try/except on prints,
@ -252,101 +233,101 @@ class OutputRedirect(object):
# 4.0^H^H^H5.0^H^H^H6.0^H^H^Hsome version when someone can be bothered. # 4.0^H^H^H5.0^H^H^H6.0^H^H^Hsome version when someone can be bothered.
@deprecated @deprecated
def stdout(string): def stdout(string):
print(string) print(string)
def stderr(string): def stderr(string):
"""Print the given ``string`` to stderr. """Print the given ``string`` to stderr.
This is equivalent to ``print >> sys.stderr, string`` This is equivalent to ``print >> sys.stderr, string``
""" """
print(string, file=sys.stderr) print(string, file=sys.stderr)
def check_pid(pid): def check_pid(pid):
"""Check if a process is running with the given ``PID``. """Check if a process is running with the given ``PID``.
*Availability: Only on POSIX systems* *Availability: Only on POSIX systems*
Return ``True`` if there is a process running with the given ``PID``. Return ``True`` if there is a process running with the given ``PID``.
""" """
try: try:
os.kill(pid, 0) os.kill(pid, 0)
except OSError: except OSError:
return False return False
else: else:
return True return True
def get_hostmask_regex(mask): def get_hostmask_regex(mask):
"""Return a compiled `re.RegexObject` for an IRC hostmask""" """Return a compiled `re.RegexObject` for an IRC hostmask"""
mask = re.escape(mask) mask = re.escape(mask)
mask = mask.replace(r'\*', '.*') mask = mask.replace(r'\*', '.*')
return re.compile(mask + '$', re.I) return re.compile(mask + '$', re.I)
class SopelMemory(dict): class SopelMemory(dict):
"""A simple thread-safe dict implementation. """A simple thread-safe dict implementation.
*Availability: 4.0; available as ``Sopel.SopelMemory`` in 3.1.0 - 3.2.0* *Availability: 4.0; available as ``Sopel.SopelMemory`` in 3.1.0 - 3.2.0*
In order to prevent exceptions when iterating over the values and changing In order to prevent exceptions when iterating over the values and changing
them at the same time from different threads, we use a blocking lock on them at the same time from different threads, we use a blocking lock on
``__setitem__`` and ``contains``. ``__setitem__`` and ``contains``.
""" """
def __init__(self, *args): def __init__(self, *args):
dict.__init__(self, *args) dict.__init__(self, *args)
self.lock = threading.Lock() self.lock = threading.Lock()
def __setitem__(self, key, value): def __setitem__(self, key, value):
self.lock.acquire() self.lock.acquire()
result = dict.__setitem__(self, key, value) result = dict.__setitem__(self, key, value)
self.lock.release() self.lock.release()
return result return result
def __contains__(self, key): def __contains__(self, key):
"""Check if a key is in the dict. """Check if a key is in the dict.
It locks it for writes when doing so. It locks it for writes when doing so.
""" """
self.lock.acquire() self.lock.acquire()
result = dict.__contains__(self, key) result = dict.__contains__(self, key)
self.lock.release() self.lock.release()
return result return result
def contains(self, key): def contains(self, key):
"""Backwards compatability with 3.x, use `in` operator instead.""" """Backwards compatability with 3.x, use `in` operator instead."""
return self.__contains__(key) return self.__contains__(key)
class SopelMemoryWithDefault(defaultdict): class SopelMemoryWithDefault(defaultdict):
"""Same as SopelMemory, but subclasses from collections.defaultdict.""" """Same as SopelMemory, but subclasses from collections.defaultdict."""
def __init__(self, *args): def __init__(self, *args):
defaultdict.__init__(self, *args) defaultdict.__init__(self, *args)
self.lock = threading.Lock() self.lock = threading.Lock()
def __setitem__(self, key, value): def __setitem__(self, key, value):
self.lock.acquire() self.lock.acquire()
result = defaultdict.__setitem__(self, key, value) result = defaultdict.__setitem__(self, key, value)
self.lock.release() self.lock.release()
return result return result
def __contains__(self, key): def __contains__(self, key):
"""Check if a key is in the dict. """Check if a key is in the dict.
It locks it for writes when doing so. It locks it for writes when doing so.
""" """
self.lock.acquire() self.lock.acquire()
result = defaultdict.__contains__(self, key) result = defaultdict.__contains__(self, key)
self.lock.release() self.lock.release()
return result return result
def contains(self, key): def contains(self, key):
"""Backwards compatability with 3.x, use `in` operator instead.""" """Backwards compatability with 3.x, use `in` operator instead."""
return self.__contains__(key) return self.__contains__(key)