5
\$\begingroup\$

All of the other Python IRC packages I've seen out there are overly complicated for simple tasks, and they don't abstract away much of the complexity of the protocol and asynchronicity. I've taken a crack at a better interface, but I'm new to the IRC protocol. It runs under Python2 and 3. How's it look?

"""
simple_irc: a simple, Pythonic IRC interface.

This module contains 2 classes: an IRC adapter, and a message wrapper.

Basic usage:
  >>> irc = simple_irc.IRC('mynick', '#python', 'irc.myserver.net')
  >>> for msg in irc: print(msg.sender + ': ' + msg)
  somebody: Hello, simple_irc!
  somebody_else: Oh hai there!
  >>> irc.write('Hi guys!')
"""

import socket, threading, time, sys
if sys.version_info.major < 3:
  import Queue
else: import queue as Queue

_wait = .01

class IRC(object):
  '''A simple IRC interface that can handle simultaneous reading and writing.
  Its interface is similar to those in the io module.'''

  def __init__(self, nick, channel, network, port=6667, future=False,
               mode=2, realname='Python simpleirc bot'):
    '''Initialize an IRC connection. By default, does not return until
    the connection is established.

      nick: the nickname to connect with
      channel: the name of the channel to connect to (e.g. "#python")
      network: the network to connect to (e.g. "irc.freenode.net")
      port: the port to connect on
      future: if True, will not connect until open() is called
      mode: bitmask of initial user mode. Only 'w' (2) and 'i' (4) are available.
      realname: real name field. Can be any string.
    '''

    self.nick = nick
    self.channel = channel
    self.network = network
    self.port = port
    self.mode = mode
    self.realname = realname

    self._readqueue = Queue.Queue()
    self._writequeue = Queue.Queue()
    self._closed = True

    if not future:
      self.open()


  def open(self):
    '''Open the connection. Does not return until the connection has been established.'''
    if not self._closed:
      raise IOError(self + " is already open")

    self._soc = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    self._soc.connect((self.network, self.port))
    self._soc.send('NICK {}\r\n'.format(self.nick).encode('UTF-8'))
    self._soc.send('USER {} {} * :{}\r\n'.format(self.nick, self.mode, self.realname).encode('UTF-8'))
    self._soc.send('JOIN {}\r\n'.format(self.channel).encode('UTF-8'))

    while True:
      #Read through all of the opening garbage
      data = self._soc.recv(4096).decode('UTF-8')

      #Error messages don't follow the same format as others, 
      #so we parse the code out early
      code = data.split()[1]
      if code.isdigit() and 400<=int(code)<=499:  
        raise ValueError(data.split(':')[2])
      elif "End of /NAMES list." in data:   #This is the last message before it's all good for some reason
        break

    self._soc.settimeout(_wait)   #the threads use this for nonblocking io
    self._closed = False

    tread = threading.Thread(target=self._reader)
    tread.daemon = True
    tread.start()

    twrite = threading.Thread(target=self._writer)
    twrite.daemon = True
    twrite.start()


  def _reader(self):
    '''Message reader thread'''
    while not self._closed:
      try: data = self._soc.recv(4096).decode('UTF-8')
      except socket.timeout: continue

      if 'PING' in data:  #so we don't get booted
        self._soc.send ('PONG ' + data.split()[1] + '\r\n')
      else:
        self._readqueue.put(message(data))

  def _writer(self):
    '''Message writer thread'''
    while not self._closed:
      time.sleep(_wait)
      if not self._writequeue.empty():
        msg = self._writequeue.get()
        self._writequeue.task_done()
        self._soc.send("PRIVMSG {} :{}\r\n".format(self.channel, msg).encode('UTF-8'))


  def __next__(self):
    msg = self.read()
    if msg is None:
      raise StopIteration
    else:
      return msg

  #Python2 compatability
  next = __next__

  def __iter__(self):
    return iter(self.read, None)

  def read(self):
    '''Returns the oldest unread message, or None if there are no unread messages.'''
    if self._readqueue.empty():
      return None
    else:
      msg = self._readqueue.get()
      self._readqueue.task_done()
      return msg

  def readall(self, limit=None):
    '''Returns a list of at most <limit> unread messages.
    If limit is unspecified, returns all unread messages.'''
    i = 0
    l = []
    for m in self:  #self is an iterable!
      l.append(m)
      i += 1
      if limit is not None and i>=limit:
        break

    return l

  def write(self, msg):
    '''Write a single message to the connected channel'''
    self._writequeue.put(msg)

  def writeall(self, msgs):
    '''Write all messages in an iterable to the connected channel'''
    for m in msgs:
      self.write(m)


  def close(self):
    '''Close the IRC connection'''
    self._closed = True
    time.sleep(2*_wait)   #wait for threads to terminate
    self._soc.send("QUIT\r\n".encode('UTF-8'))
    self._soc.close()
    del self._writequeue, self._readqueue

  @property
  def closed(self):
    '''True if the IRC connection is closed'''
    return self._closed


  def __enter__(self):
    return self

  def __exit__(self, exception_type, exception_value, traceback):
    self.close()
    return True



class message(str):
  '''An IRC message.

  Properties:
  - sender: the sender
  - hostname: the hostname of the sender
  '''
  def __new__(cls, raw):
    self = str.__new__(cls, raw.split(':')[2].rstrip())
    raw = raw.split(':')
    self.sender = raw[1].split('!')[0]
    self.hostname = raw[1].split()[0].split('@')[1]
    return self
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

It's hard to read this code because it doesn't follow PEP8. The violations that stick in the eye the most:

  • Indentation should be 4 spaces
  • Class names should follow CamelCase (message violates that)
  • Do break lines at :, for example in else: import queue as Queue
  • Put spaces around operators, for example 400 <= int(code) <= 499 instead of 400<=int(code)<=499
  • Put a single blank line in front of method declarations of a class
  • Put a space after #comment in comments

There is a pep8 command line utility in a package with the same name. I recommend to install it and run against your script and correct all reported violations.

Some other tips:

  • You do self._soc.send("........".encode('UTF-8')) a lot. It would be better to add a helper method so that you can do self.send("........") instead.
    • I recommend the same for the self._soc.recv calls, even though you only do that twice, for now.
  • IRC is not a great name for a bot. It doesn't have a single clear purpose as a class should. It looks headed to become a God class, doing everything. And why should it be iterable?
  • Avoid single letter variable names like l in the readall method. The letter l is probably one of the worst possible single letter variables, as it's easy mistake it for the number 1, or capital I. In this example I would recommend items instead.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.