Index: Doc/library/asynchat.rst =================================================================== --- Doc/library/asynchat.rst (revision 59392) +++ Doc/library/asynchat.rst (working copy) @@ -76,9 +76,9 @@ .. method:: async_chat.collect_incoming_data(data) - Called with *data* holding an arbitrary amount of received data. The - default method, which must be overridden, raises a - :exc:`NotImplementedError` exception. + Called with *data* holding an arbitrary amount of received data in + the form of a bytes object. The default method, which must be + overridden, raises a :exc:`NotImplementedError` exception. .. method:: async_chat.discard_buffers() @@ -89,10 +89,12 @@ .. method:: async_chat.found_terminator() - Called when the incoming data stream matches the termination condition set - by :meth:`set_terminator`. The default method, which must be overridden, - raises a :exc:`NotImplementedError` exception. The buffered input data - should be available via an instance attribute. + Called when the incoming data stream matches the termination + condition set by :meth:`set_terminator` or + :meth:`set_terminator_str`. The default method, which must be + overridden, raises a :exc:`NotImplementedError` + exception. :meth:`collect_incoming_data` should have stored the + data in some accesible location. .. method:: async_chat.get_terminator() @@ -127,13 +129,39 @@ .. method:: async_chat.push(data) - Creates a :class:`simple_producer` object (*see below*) containing the data - and pushes it on to the channel's ``producer_fifo`` to ensure its - transmission. This is all you need to do to have the channel write the - data out to the network, although it is possible to use your own producers - in more complex schemes to implement encryption and chunking, for example. + Creates a :class:`simple_producer` object (*see below*) containing + the data and pushes it on to the channel's ``producer_fifo`` to + ensure its transmission, where *data* can be interpreted as a bytes + object. This is all you need to do to have the channel write the + data out to the network, although it is possible to use your own + producers in more complex schemes to implement encryption and + chunking, for example. +.. method:: async_chat.push_str(data, encoding) + + Creates a :class:`simple_producer` object (*see below*) containing + the data and pushes it on to the channel's ``producer_fifo`` to + ensure its transmission, where *data* can be encoded into a bytes + object via the specified encoding. This is all you need to do to + have the channel write the data out to the network, although it is + possible to use your own producers in more complex schemes to + implement encryption and chunking, for example. + + It is wise to be aware the of endian behavior of your chosen + encoding, especially when communicating between computers of + different architectures. + + +.. method:: async_chat.push_iterable(iterable) + + Creates a :class:`iterator_producer` object (*see below*) wrapping + iter(*iterable*) and pushes it on to the channel's + ``producer_fifo`` to ensure its transmission, where *iterable* + results in a series of bytes objects. This is all you need to do + to have the channel write the data out to the network. + + .. method:: async_chat.push_with_producer(producer) Takes a producer object and adds it to the producer fifo associated with @@ -165,11 +193,11 @@ +-----------+---------------------------------------------+ | term | Description | +===========+=============================================+ - | *string* | Will call :meth:`found_terminator` when the | - | | string is found in the input stream | + | *bytes* | Will call :meth:`found_terminator` when the | + | | byte sequence is found in the input stream | +-----------+---------------------------------------------+ | *integer* | Will call :meth:`found_terminator` when the | - | | indicated number of characters have been | + | | indicated number of bytes have been | | | received | +-----------+---------------------------------------------+ | ``None`` | The channel continues to collect data | @@ -180,6 +208,18 @@ by the channel after :meth:`found_terminator` is called. +.. method:: async_chat.set_terminator_str(term, encoding) + + Sets the terminating condition to be recognized on the channel. + ``term`` is encoded into a bytes object using the ``encoding``, + after which it is treated just as a bytes object passed to + :meth:`set_terminator`. + + It is wise to be aware the of endian behavior of your chosen + encoding, especially when communicating between computers of + different architectures. + + .. method:: async_chat.writable() Should return ``True`` as long as items remain on the producer fifo, or the @@ -203,6 +243,19 @@ empty string. +.. class:: iterator_producer(iterator) + + A :class:`iterator_producer` takes an iterator and adapts it to the + asynchat producer interface. The objects returned by the iterator + must be valid to pass into the ``bytes`` constructor. + + +.. method:: iterator_producer.more() + + Produces the next chunk of information resulting from the iterator, + or an b'' if the iterator raises :exc:`StopIteration` + + .. class:: fifo([list=None]) Each channel maintains a :class:`fifo` holding data which has been pushed @@ -271,8 +324,8 @@ self.addr = addr self.sessions = sessions self.ibuffer = [] - self.obuffer = "" - self.set_terminator("\r\n\r\n") + self.obuffer = b"" + self.set_terminator(b"\r\n\r\n") self.reading_headers = True self.handling = False self.cgi_data = None @@ -285,10 +338,10 @@ def found_terminator(self): if self.reading_headers: self.reading_headers = False - self.parse_headers("".join(self.ibuffer)) + self.parse_headers(b"".join(self.ibuffer)) self.ibuffer = [] - if self.op.upper() == "POST": - clen = self.headers.getheader("content-length") + if self.op.upper() == b"POST": + clen = self.headers.getheader(b"content-length") self.set_terminator(int(clen)) else: self.handling = True @@ -296,7 +349,7 @@ self.handle_request() elif not self.handling: self.set_terminator(None) # browsers sometimes over-send - self.cgi_data = parse(self.headers, "".join(self.ibuffer)) + self.cgi_data = parse(self.headers, b"".join(self.ibuffer)) self.handling = True self.ibuffer = [] self.handle_request() Index: Lib/asynchat.py =================================================================== --- Lib/asynchat.py (revision 59392) +++ Lib/asynchat.py (working copy) @@ -32,18 +32,17 @@ the common internet protocols - smtp, nntp, ftp, etc..). The handle_read() method looks at the input stream for the current -'terminator' (usually '\r\n' for single-line responses, '\r\n.\r\n' -for multi-line output), calling self.found_terminator() on its -receipt. +'terminator', calling self.found_terminator() on its receipt. for example: -Say you build an async nntp client using this class. At the start -of the connection, you'll have self.terminator set to '\r\n', in -order to process the single-line greeting. Just before issuing a -'LIST' command you'll set it to '\r\n.\r\n'. The output of the LIST -command will be accumulated (using your own 'collect_incoming_data' -method) up to the terminator, and then control will be returned to -you - by calling your self.found_terminator() method. +Say you build an async nntp client using this class. At the start of +the connection, you'll have self.terminator set to b'\r\n', in order +to process the single-line greeting. Just before issuing a LIST +command you'll set it to b'\r\n.\r\n'. The output of the LIST command +will be accumulated (using your own 'collect_incoming_data' method) up +to the terminator, and then control will be returned to you - by +calling your self.found_terminator() method. + """ import sys @@ -52,31 +51,128 @@ from collections import deque class async_chat (asyncore.dispatcher): + """This is an abstract class. You must derive from this class, and add - the two methods collect_incoming_data() and found_terminator()""" + the two methods collect_incoming_data() and found_terminator(). + The set_terminator(term) or set_terminator_str(term, encoding) + method almost always should be called before entering + asyncore.loop. + + In addition to overriding methods, children may override the + variables `ac_in_buffer_size` and `ac_out_buffer_size`, which + deterine the number of bytes that may be stored in the incoming + and outgoing buffers, respectively. Changing these will not + affect functionality, but may alter performance. The default + value of each is 4096. + + """ + # these are overridable defaults ac_in_buffer_size = 4096 ac_out_buffer_size = 4096 def __init__ (self, conn=None): + """Constructor, should be called by child's __init__ + + Optional arguments: + conn -- An already initialized socket. + + conn, if provided and not None, will be used as the socket for + this channel. If conn is provided, create_socket and its + related methods need not be called. + + """ + self.ac_in_buffer = b'' self.ac_out_buffer = b'' self.producer_fifo = fifo() asyncore.dispatcher.__init__ (self, conn) def collect_incoming_data(self, data): + """Store received data for later processing + + Arguments: + data -- A bytes object representing some received data. + + collect_incoming_data must be overridden in a child class for + async_chat to be useful. All it needs to do in most cases is + record the data; `found_terminator` is the proper place to do + most processing. + + """ raise NotImplementedError("must be implemented in subclass") def found_terminator(self): + """Process a complete section of data received from the socket + + found_terminator must be overridden in a child class for + async_chat to be useful. It is called when the terminator is + found in the received data stream, after calling + `collect_incoming_data` with all bytes prior to the terminator + in the stream. The terminator is defined by the most recent + call to `set_terminator` or `set_terminator_str`. + + + """ raise NotImplementedError("must be implemented in subclass") def set_terminator (self, term): - "Set the input delimiter. Can be a fixed string of any length, an integer, or None" - self.terminator = term + """Set the input delimiter. + Arguments: + term -- Can be a bytes object, an integer, or None. + + If term is None, async_chat will never call + found_terminator. This is occasionally useful, if + `collect_incoming_data` performs all necessary handling of the + data. + + If term is an int object, async_chat will read that many bytes + from the data stream, and then call `found_terminator`. This + is useful for reading protocol sections of known length. + + If term is neither an int nor None, it will be passed to the + bytes object constructor. async_chat will look for that + sequence of bytes in the incoming data stream, and call + `found_terminator` when it is discovered. This is useful for + reading protocol sections of varying or unknown length. + + set_terminator may be called as many times as you like. This + is quite often useful -- for example in HTTP you might read + the headers by doing set_terminator(b'\n\n') and then extract + the content length from the headers and do + set_terminator(content_length) to read the page data. + + """ + + if term is None: + self.terminator = None + elif isinstance(term, int): + self.terminator = term + else: + self.terminator = bytes(term) + + def set_terminator_str(self, term, encoding): + """Set the input delimiter to an encoded string + + Arguments: + term -- A str object. + encoding -- The name of a codec. + + This method is like `set_terminator`, except that it sets the + terminator to a sequence of bytes representing term in the + given encoding. + + It is very important to be aware of byte ordering when using + this function. Some encodings produce endian-sentive results. + + """ + self.set_terminator(term.encode(encoding)) + def get_terminator (self): + """Return the current terminator""" return self.terminator # grab some more data from the socket, @@ -85,6 +181,7 @@ # if found, transition to the next state. def handle_read (self): + """asyncore function. Overriding children must invoke.""" try: data = self.recv (self.ac_in_buffer_size) @@ -92,9 +189,8 @@ self.handle_error() return - if isinstance(data, str): - data = data.encode('ascii') - self.ac_in_buffer = self.ac_in_buffer + bytes(data) + data = bytes(data) + self.ac_in_buffer = self.ac_in_buffer + data # Continue to search for self.terminator in self.ac_in_buffer, # while calling self.collect_incoming_data. The while loop @@ -108,7 +204,7 @@ # no terminator, collect it all self.collect_incoming_data (self.ac_in_buffer) self.ac_in_buffer = b'' - elif isinstance(terminator, int) or isinstance(terminator, int): + elif isinstance(terminator, int): # numeric terminator n = terminator if lb < n: @@ -129,8 +225,6 @@ # 3) end of buffer does not match any prefix: # collect data terminator_len = len(terminator) - if isinstance(terminator, str): - terminator = terminator.encode('ascii') index = self.ac_in_buffer.find(terminator) if index != -1: # we found the terminator @@ -155,16 +249,73 @@ self.ac_in_buffer = b'' def handle_write (self): + """asyncore function. Overriding children must invoke.""" self.initiate_send () def handle_close (self): + """asyncore function. Overriding children must invoke.""" self.close() def push (self, data): - self.producer_fifo.push (simple_producer (data)) + """Place data in the outgoing queue. + + Arguments: + data -- The expression bytes(data) must be meaningful + + Appends bytes(data) to the sequence of bytes to be sent + through the socket. Each time asyncore.loop discovers that + the socket is writable, some or all of the bytes in that queue + will be transmitted. + + """ + self.producer_fifo.push(simple_producer(bytes(data))) self.initiate_send() + def push_str(self, data, encoding): + """Place a string in the outgoing queue. + + Arguments: + data -- The expression data.encode(encoding) must be meaningful + + Appends bytes(data.encode(encoding)) to the sequence of bytes + to be sent through the socket. Each time asyncore.loop + discovers that the socket is writable, some or all of the + bytes in that queue will be transmitted. + + It is very important to be aware of byte ordering when using + this function. Some encodings produce endian-sentive results. + + """ + self.push(data.encode(encoding)) + + def push_iterable(self, iterable): + """Place the iterated items in the outgoing queue + + Arguments: + iterable -- [bytes(x) for x in iterable] must be meaningful + + Appends bytes(x) for each x in iterator to the sequence of + bytes to be sent through the socket. Each time asyncore.loop + discovers that the socket is writable, some or all of the + bytes in that queue will be transmitted. + + """ + self.producer_fifo.push(iterator_producer(iter(iterable))) + self.initiate_send() + def push_with_producer (self, producer): + """Place a producer in the outgoing queue. + + Arguments: + producer -- The expression producer.more() must be meaningful + + Repeatedly appends bytes(producer.more()) to the sequence of + bytes to be sent through the socket, until + bool(producer.more()) is False. Each time asyncore.loop + discovers that the socket is writable, some or all of the + bytes in that queue will be transmitted. + + """ self.producer_fifo.push (producer) self.initiate_send() @@ -189,6 +340,7 @@ # refill the outgoing buffer by calling the more() method # of the first producer in the queue def refill_buffer (self): + """Internal function. Do not invoke or override.""" while 1: if len(self.producer_fifo): p = self.producer_fifo.first() @@ -199,16 +351,12 @@ self.producer_fifo.pop() self.close() return - elif isinstance(p, str) or isinstance(p, bytes): - if isinstance(p, str): - p = p.encode('ascii') + elif isinstance(p, bytes): self.producer_fifo.pop() self.ac_out_buffer = self.ac_out_buffer + p return data = p.more() if data: - if isinstance(data, str): - data = data.encode('ascii') self.ac_out_buffer = self.ac_out_buffer + bytes(data) return else: @@ -217,6 +365,7 @@ return def initiate_send (self): + """Internal function. Do not invoke or override.""" obs = self.ac_out_buffer_size # try to refill the buffer if (len (self.ac_out_buffer) < obs): @@ -234,6 +383,12 @@ return def discard_buffers (self): + """Discard all incoming and outgoing data. + + It is generally wise to avoid the use of this function, as it + results in data loss. This is rarely useful. + + """ # Emergencies only! self.ac_in_buffer = b'' self.ac_out_buffer = b'' @@ -243,8 +398,31 @@ class simple_producer: + """Produce bytes for transmission. + + This is a very basic producer, used to implement + `async_chat.push`; the only extra functionality it provides is to + break the data up into blocks of arbitrary size, such that each + call to more produces a block of data no larger than the requested + size. + + """ + def __init__ (self, data, buffer_size=512): - self.data = data + """Initialize the producer with a byte sequence + + Arguments: + data -- bytes(data) must be meaningful. + + Optional arguments: + buffer_size -- A number, defaults to 512 + + The simple_producer will pop and return blocks of `data` of + length min(`buffer_size`, len(`data`)) until data is empty. + + """ + + self.data = bytes(data) self.buffer_size = buffer_size def more (self): @@ -257,6 +435,36 @@ self.data = b'' return result +class iterator_producer: + + """Produce bytes for transmission. + + iterator_producer is a wrapper around Python iterators, adapting + them to the older producer interface defined by this module. Using + iterator_producer, any iterator that produces a sequence of bytes + objects can be used as a producer. + + `async_chat.push_iterable` is a helper function which adds an + instance of iterator_producer to the sending queue. + + """ + + def __init__(self, itr): + """Initialize the producer with an iterator + + Arguments: + itr -- An iterator for which [bytes(x) for x in itr] works + + """ + + self.itr = itr + + def more(self): + try: + return self.itr.__next__() + except StopIteration: + return b'' + class fifo: def __init__ (self, list=None): if not list: @@ -298,6 +506,17 @@ # regex: 14035/s def find_prefix_at_end (haystack, needle): + """Returns > 0 if the beginning of needle is at the end of haystack + + Arguments: + haystack -- A bytes object. + needle -- A bytes object. + + Given haystack, see if any prefix of needle is at its end. This + assumes an exact match has already been checked. Return the + number of characters matched. + + """ l = len(needle) - 1 while l and not haystack.endswith(needle[:l]): l -= 1 Index: Lib/smtpd.py =================================================================== --- Lib/smtpd.py (revision 59392) +++ Lib/smtpd.py (working copy) @@ -123,12 +123,12 @@ self.__fqdn = socket.getfqdn() self.__peer = conn.getpeername() print('Peer:', repr(self.__peer), file=DEBUGSTREAM) - self.push('220 %s %s' % (self.__fqdn, __version__)) - self.set_terminator('\r\n') + self.push_str('220 %s %s' % (self.__fqdn, __version__), 'ascii') + self.set_terminator(b'\r\n') # Overrides base class for convenience def push(self, msg): - asynchat.async_chat.push(self, msg + '\r\n') + asynchat.async_chat.push(self, msg + b'\r\n') # Implementation of base class abstract method def collect_incoming_data(self, data): @@ -141,7 +141,7 @@ self.__line = [] if self.__state == self.COMMAND: if not line: - self.push('500 Error: bad syntax') + self.push_str('500 Error: bad syntax', 'ascii') return method = None i = line.find(' ') @@ -153,13 +153,13 @@ arg = line[i+1:].strip() method = getattr(self, 'smtp_' + command, None) if not method: - self.push('502 Error: command "%s" not implemented' % command) + self.push_str('502 Error: command "%s" not implemented' % command, 'ascii') return method(arg) return else: if self.__state != self.DATA: - self.push('451 Internal confusion') + self.push_str('451 Internal confusion', 'ascii') return # Remove extraneous carriage returns and de-transparency according # to RFC 821, Section 4.5.2. @@ -177,32 +177,32 @@ self.__rcpttos = [] self.__mailfrom = None self.__state = self.COMMAND - self.set_terminator('\r\n') + self.set_terminator(b'\r\n') if not status: - self.push('250 Ok') + self.push_str('250 Ok', 'ascii') else: - self.push(status) + self.push_str(status, 'ascii') # SMTP and ESMTP commands def smtp_HELO(self, arg): if not arg: - self.push('501 Syntax: HELO hostname') + self.push_str('501 Syntax: HELO hostname', 'ascii') return if self.__greeting: - self.push('503 Duplicate HELO/EHLO') + self.push_str('503 Duplicate HELO/EHLO', 'ascii') else: self.__greeting = arg - self.push('250 %s' % self.__fqdn) + self.push_str('250 %s' % self.__fqdn, 'ascii') def smtp_NOOP(self, arg): if arg: - self.push('501 Syntax: NOOP') + self.push_str('501 Syntax: NOOP', 'ascii') else: - self.push('250 Ok') + self.push_str('250 Ok', 'ascii') def smtp_QUIT(self, arg): # args is ignored - self.push('221 Bye') + self.push_str('221 Bye', 'ascii') self.close_when_done() # factored @@ -223,49 +223,49 @@ print('===> MAIL', arg, file=DEBUGSTREAM) address = self.__getaddr('FROM:', arg) if arg else None if not address: - self.push('501 Syntax: MAIL FROM:
') + self.push_str('501 Syntax: MAIL FROM:
', 'ascii') return if self.__mailfrom: - self.push('503 Error: nested MAIL command') + self.push_str('503 Error: nested MAIL command', 'ascii') return self.__mailfrom = address print('sender:', self.__mailfrom, file=DEBUGSTREAM) - self.push('250 Ok') + self.push_str('250 Ok', 'ascii') def smtp_RCPT(self, arg): print('===> RCPT', arg, file=DEBUGSTREAM) if not self.__mailfrom: - self.push('503 Error: need MAIL command') + self.push_str('503 Error: need MAIL command', 'ascii') return address = self.__getaddr('TO:', arg) if arg else None if not address: - self.push('501 Syntax: RCPT TO:
') + self.push_str('501 Syntax: RCPT TO:
', 'ascii') return self.__rcpttos.append(address) print('recips:', self.__rcpttos, file=DEBUGSTREAM) - self.push('250 Ok') + self.push_str('250 Ok', 'ascii') def smtp_RSET(self, arg): if arg: - self.push('501 Syntax: RSET') + self.push_str('501 Syntax: RSET', 'ascii') return # Resets the sender, recipients, and data, but not the greeting self.__mailfrom = None self.__rcpttos = [] self.__data = '' self.__state = self.COMMAND - self.push('250 Ok') + self.push_str('250 Ok', 'ascii') def smtp_DATA(self, arg): if not self.__rcpttos: - self.push('503 Error: need RCPT command') + self.push_str('503 Error: need RCPT command', 'ascii') return if arg: - self.push('501 Syntax: DATA') + self.push_str('501 Syntax: DATA', 'ascii') return self.__state = self.DATA - self.set_terminator('\r\n.\r\n') - self.push('354 End data with .') + self.set_terminator(b'\r\n.\r\n') + self.push_str('354 End data with .', 'ascii') Index: Lib/asyncore.py =================================================================== --- Lib/asyncore.py (revision 59392) +++ Lib/asyncore.py (working copy) @@ -178,6 +178,23 @@ poll3 = poll2 # Alias for backward compatibility def loop(timeout=30.0, use_poll=False, map=None, count=None): + """The main event-handling loop of asyncore. + + Optional arguments: + timeout -- Number representing seconds, default 30.0 + use_poll -- Use poll instead of select if available, default False + map -- The map of channels to process, default is use built-in map + count -- number of times to cycle through the loop, default infinity + + Note: Setting timeout will not cause loop to terminate after a + fixed amount of time. Rather, it determines how long each select + or poll call will block. To cause loop to terminate after a fixed + amount of time, pass an integer count. The loop will then + terminate after approximately count * timeout seconds, if the + select or poll calls block. + + """ + if map is None: map = socket_map @@ -197,6 +214,25 @@ class dispatcher: + """The abstract base class that users of asyncore should inherit + from. + + A dispatcher object handles a single socket, processing connect, + accept, close, read and write events as defined by the child + handle_connect, handle_accept, handle_close, handle_read and + handle_write methods, respectively. The child may also define + handle_expt to handle exceptions raised during the communication + process. + + dispatcher implements many socket methods, so children can send, + recv, accept, bind, listen, etc within as if self were a socket. + dispatcher also has a create_socket method for creating the + internal socket, if necessary. + + Children should always invoke dispatcher.__init__. + + """ + debug = False connected = False accepting = False @@ -204,6 +240,22 @@ addr = None def __init__(self, sock=None, map=None): + """Initialize a dispatcher. + + Children should always invoke dispatcher.__init__ + + Optional arguments: + sock -- A socket to communicate through, default None + map -- A map to register in, default is use built-in map + + If sock is None, create_socket and either connect or + listen/bind must be called to create and initialize the + socket. + + If map is None, the asyncore global map is used. + + """ + if map is None: self._map = socket_map else: @@ -238,12 +290,24 @@ return '<%s at %#x>' % (' '.join(status), id(self)) def add_channel(self, map=None): + """Adds the dispatcher to the map. + + Called automatically. + + """ + #self.log_info('adding channel %s' % self) if map is None: map = self._map map[self._fileno] = self def del_channel(self, map=None): + """Removes the dispatcher from the map. + + Called automatically by close. + + """ + fd = self._fileno if map is None: map = self._map @@ -253,6 +317,18 @@ self._fileno = None def create_socket(self, family, type): + """Creates and initializes the internal socket. + + Arguments: + family -- socket module AF_* constant + type -- socket module SOCK_* constant + + If an initialized socket is passed to the constructor's conn + argument, or to the set_socket method, there is no need to + call this method. + + """ + self.family_and_type = family, type self.socket = socket.socket(family, type) self.socket.setblocking(0) @@ -260,12 +336,29 @@ self.add_channel() def set_socket(self, sock, map=None): + """Sets the internal socket + + Arguments: + sock -- An initialized socket + + Optional arguments: + map -- The map to use, default is the built-in map + + """ + self.socket = sock ## self.__dict__['socket'] = sock self._fileno = sock.fileno() self.add_channel(map) def set_reuse_addr(self): + """Sets the socket.SO_REUSEADDR socket option, is possible + + This will cause the socket to attempt to re-use a server + port. There is no guarantee of success. + + """ + # try to re-use a server port if possible try: self.socket.setsockopt( @@ -283,9 +376,31 @@ # ================================================== def readable(self): + """Determine whether read events should be generated for this + dispatcher. + + While readable returns False, read events will not be + generated for this dispatcher, and handle_read and + handle_accept will not be called. handle_connect will be + called only if triggered by a write event. + + Default is to return True. + + """ return True def writable(self): + """Determine whether write events should be generated for this + dispatcher. + + While writable returns False, write events will not be + generated for this dispatcher, and handle_write not be called. + handle_connect will be called only if triggered by a read + event. + + Default is to return True. + + """ return True # ================================================== @@ -293,16 +408,19 @@ # ================================================== def listen(self, num): + """Behaves as the similarly-named socket method""" self.accepting = True if os.name == 'nt' and num > 5: num = 1 return self.socket.listen(num) def bind(self, addr): + """Behaves as the similarly-named socket method""" self.addr = addr return self.socket.bind(addr) def connect(self, address): + """Behaves as the similarly-named socket method""" self.connected = False err = self.socket.connect_ex(address) # XXX Should interpret Winsock return values @@ -316,6 +434,7 @@ raise socket.error(err, errorcode[err]) def accept(self): + """Behaves as the similarly-named socket method""" # XXX can return either an address pair or None try: conn, addr = self.socket.accept() @@ -327,6 +446,7 @@ raise def send(self, data): + """Behaves as the similarly-named socket method""" try: result = self.socket.send(data) return result @@ -338,6 +458,7 @@ return 0 def recv(self, buffer_size): + """Behaves as the similarly-named socket method""" try: data = self.socket.recv(buffer_size) if not data: @@ -356,6 +477,7 @@ raise def close(self): + """Behaves as the similarly-named socket method""" self.del_channel() self.socket.close() @@ -376,6 +498,7 @@ print('%s: %s' % (type, message)) def handle_read_event(self): + """Internal method, do not override.""" if self.accepting: # for an accepting socket, getting a read implies # that we are connected @@ -390,6 +513,7 @@ self.handle_read() def handle_write_event(self): + """Internal method, do not override.""" # getting a write implies that we are connected if not self.connected: self.handle_connect() @@ -397,9 +521,11 @@ self.handle_write() def handle_expt_event(self): + """Internal method, do not override.""" self.handle_expt() def handle_error(self): + """Internal method, do not override.""" nil, t, v, tbinfo = compact_traceback() # sometimes a user repr method will crash. @@ -420,21 +546,66 @@ self.close() def handle_expt(self): + """Called to handle an exception event. + + Children may override this method to implement exception + processing. + + """ self.log_info('unhandled exception', 'warning') def handle_read(self): + """Called to handle a read event. + + When this method is called, a call to self.recv will return + data without blocking. Children may override this method to + implement data reception. + + """ self.log_info('unhandled read event', 'warning') def handle_write(self): + """Called to handle a write event. + + When this method is called, a call to self.send will transmit + data without blocking. Children may override this method to + implement data transmission. + + """ self.log_info('unhandled write event', 'warning') def handle_connect(self): + """Called to handle a connect event. + + When this method is called, the socket has just been + connected. Calls to self.send and self.recv will succeed + without blocking. Children may override this method. + + """ self.log_info('unhandled connect event', 'warning') def handle_accept(self): + """Called to handle an accept event. + + When this method is called, a call to self.accept will return + a new socket without blocking. Children may override this + method to implement server socket listening. + + Note: This event will not be triggered unless listen and bind + have previously been called. Passing a listening socket into + the constructor or set_socket will not work. + + """ self.log_info('unhandled accept event', 'warning') def handle_close(self): + """Called to handle a close event. + + When this method is called, it is time to call self.close. + Children may override this method to implement end-of-life + behavior. + + """ self.log_info('unhandled close event', 'warning') self.close() @@ -444,7 +615,20 @@ # --------------------------------------------------------------------------- class dispatcher_with_send(dispatcher): + """A very basic buffered output implementation on top of + dispatcher. + The send method of this class queues data that are passed to it, + and the handle_write method transmits data from the queue across + the network. Thus you can simply call send with however much data + you like, and dispatcher_with_send will see to it that all of the + data are sent. + + The asynchat module provides a more capable implementation of + this, along with assorted other features. + + """ + def __init__(self, sock=None, map=None): dispatcher.__init__(self, sock, map) self.out_buffer = b'' Index: Lib/test/test_smtplib.py =================================================================== --- Lib/test/test_smtplib.py (revision 59392) +++ Lib/test/test_smtplib.py (working copy) @@ -300,12 +300,12 @@ # Simulated SMTP channel & server class SimSMTPChannel(smtpd.SMTPChannel): def smtp_EHLO(self, arg): - resp = '250-testhost\r\n' \ - '250-EXPN\r\n' \ - '250-SIZE 20000000\r\n' \ - '250-STARTTLS\r\n' \ - '250-DELIVERBY\r\n' \ - '250 HELP' + resp = b'250-testhost\r\n' \ + b'250-EXPN\r\n' \ + b'250-SIZE 20000000\r\n' \ + b'250-STARTTLS\r\n' \ + b'250-DELIVERBY\r\n' \ + b'250 HELP' self.push(resp) def smtp_VRFY(self, arg): @@ -314,9 +314,9 @@ raw_addr = email.utils.parseaddr(arg)[1] quoted_addr = smtplib.quoteaddr(arg) if raw_addr in sim_users: - self.push('250 %s %s' % (sim_users[raw_addr], quoted_addr)) + self.push_str('250 %s %s' % (sim_users[raw_addr], quoted_addr), 'ascii') else: - self.push('550 No such user: %s' % arg) + self.push_str('550 No such user: %s' % arg, 'ascii') def smtp_EXPN(self, arg): # print '\nsmtp_EXPN(%r)\n' % arg @@ -327,11 +327,11 @@ for n, user_email in enumerate(user_list): quoted_addr = smtplib.quoteaddr(user_email) if n < len(user_list) - 1: - self.push('250-%s %s' % (sim_users[user_email], quoted_addr)) + self.push_str('250-%s %s' % (sim_users[user_email], quoted_addr), 'ascii') else: - self.push('250 %s %s' % (sim_users[user_email], quoted_addr)) + self.push_str('250 %s %s' % (sim_users[user_email], quoted_addr), 'ascii') else: - self.push('550 No access for you!') + self.push_str('550 No access for you!', 'ascii') class SimSMTPServer(smtpd.SMTPServer): Index: Lib/test/test_asynchat.py =================================================================== --- Lib/test/test_asynchat.py (revision 59392) +++ Lib/test/test_asynchat.py (working copy) @@ -49,12 +49,11 @@ class echo_client(asynchat.async_chat): - def __init__(self, terminator): + def __init__(self): asynchat.async_chat.__init__(self) self.contents = [] self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.connect((HOST, PORT)) - self.set_terminator(terminator) self.buffer = b"" def handle_connect(self): @@ -88,41 +87,74 @@ s.chunk_size = server_chunk s.start() time.sleep(0.5) # Give server time to initialize - c = echo_client(term) + c = echo_client() + c.set_terminator(term) c.push(b"hello ") - c.push(bytes("world%s" % term, "ascii")) - c.push(bytes("I'm not dead yet!%s" % term, "ascii")) + c.push(b"world" + term) + c.push(b"I'm not dead yet!" + term) c.push(SERVER_QUIT) asyncore.loop(use_poll=self.usepoll, count=300, timeout=.01) s.join() self.assertEqual(c.contents, [b"hello world", b"I'm not dead yet!"]) + def line_terminator_check_str(self, term, server_chunk): + s = echo_server() + s.chunk_size = server_chunk + s.start() + time.sleep(0.5) # Give server time to initialize + c = echo_client() + c.set_terminator_str(term, 'utf-8') + c.push_str("\u03C8hello ", 'utf-8') + c.push_str("world%s" % term, "utf-8") + c.push_str("I'm not dead yet!%s" % term, "utf-8") + c.push(SERVER_QUIT) + asyncore.loop(use_poll=self.usepoll, count=300, timeout=.01) + s.join() + + self.assertEqual(c.contents, ["\u03C8hello world".encode('utf-8'), "I'm not dead yet!".encode('utf-8')]) + # the line terminator tests below check receiving variously-sized # chunks back from the server in order to exercise all branches of # async_chat.handle_read def test_line_terminator1(self): + # test one-byte terminator + for l in (1,2,3): + self.line_terminator_check(b'\n', l) + + def test_line_terminator2(self): + # test two-byte terminator + for l in (1,2,3): + self.line_terminator_check(b'\r\n', l) + + def test_line_terminator3(self): + # test three-byte terminator + for l in (1,2,3): + self.line_terminator_check(b'qqq', l) + + def test_line_terminator1_str(self): # test one-character terminator for l in (1,2,3): - self.line_terminator_check('\n', l) + self.line_terminator_check_str('\u03C9', l) - def test_line_terminator2(self): + def test_line_terminator2_str(self): # test two-character terminator for l in (1,2,3): - self.line_terminator_check('\r\n', l) + self.line_terminator_check_str('\u03C9\u03C9', l) - def test_line_terminator3(self): + def test_line_terminator3_str(self): # test three-character terminator for l in (1,2,3): - self.line_terminator_check('qqq', l) + self.line_terminator_check_str('\u03C9\u03C9\u03C9', l) def numeric_terminator_check(self, termlen): # Try reading a fixed number of bytes s = echo_server() s.start() time.sleep(0.5) # Give server time to initialize - c = echo_client(termlen) + c = echo_client() + c.set_terminator(termlen) data = b"hello world, I'm not dead yet!\n" c.push(data) c.push(SERVER_QUIT) @@ -144,7 +176,8 @@ s = echo_server() s.start() time.sleep(0.5) # Give server time to initialize - c = echo_client(None) + c = echo_client() + c.set_terminator(None) data = b"hello world, I'm not dead yet!\n" c.push(data) c.push(SERVER_QUIT) @@ -158,7 +191,8 @@ s = echo_server() s.start() time.sleep(0.5) # Give server time to initialize - c = echo_client(b'\n') + c = echo_client() + c.set_terminator(b'\n') data = b"hello world\nI'm not dead yet!\n" p = asynchat.simple_producer(data+SERVER_QUIT, buffer_size=8) c.push_with_producer(p) @@ -167,11 +201,28 @@ self.assertEqual(c.contents, [b"hello world", b"I'm not dead yet!"]) - def test_string_producer(self): + def test_iterator_producer(self): s = echo_server() s.start() time.sleep(0.5) # Give server time to initialize - c = echo_client(b'\n') + c = echo_client() + c.set_terminator(b'\n') + def producer(): + yield b'hello world\n' + yield b"I'm not dead yet!\n" + yield SERVER_QUIT + c.push_iterable(producer()) + asyncore.loop(use_poll=self.usepoll, count=300, timeout=.01) + s.join() + + self.assertEqual(c.contents, [b"hello world", b"I'm not dead yet!"]) + + def test_bytes_producer(self): + s = echo_server() + s.start() + time.sleep(0.5) # Give server time to initialize + c = echo_client() + c.set_terminator(b'\n') data = b"hello world\nI'm not dead yet!\n" c.push_with_producer(data+SERVER_QUIT) asyncore.loop(use_poll=self.usepoll, count=300, timeout=.01) @@ -184,7 +235,8 @@ s = echo_server() s.start() time.sleep(0.5) # Give server time to initialize - c = echo_client(b'\n') + c = echo_client() + c.set_terminator(b'\n') c.push(b"hello world\n\nI'm not dead yet!\n") c.push(SERVER_QUIT) asyncore.loop(use_poll=self.usepoll, count=300, timeout=.01) @@ -197,7 +249,8 @@ s = echo_server() s.start() time.sleep(0.5) # Give server time to initialize - c = echo_client(b'\n') + c = echo_client() + c.set_terminator(b'\n') c.push(b"hello world\nI'm not dead yet!\n") c.push(SERVER_QUIT) c.close_when_done() @@ -216,8 +269,8 @@ class TestHelperFunctions(unittest.TestCase): def test_find_prefix_at_end(self): - self.assertEqual(asynchat.find_prefix_at_end("qwerty\r", "\r\n"), 1) - self.assertEqual(asynchat.find_prefix_at_end("qwertydkjf", "\r\n"), 0) + self.assertEqual(asynchat.find_prefix_at_end(b"qwerty\r", b"\r\n"), 1) + self.assertEqual(asynchat.find_prefix_at_end(b"qwertydkjf", b"\r\n"), 0) class TestFifo(unittest.TestCase): def test_basic(self):