[issue16853] add a Selector to the select module

Charles-François Natali report at bugs.python.org
Mon Jan 7 20:06:37 CET 2013


Charles-François Natali added the comment:

It works fine on Linux.

I'm attaching the latest version that should hopefully fix the second
failure, as a standalone diff and a diff for Tulip.
It also adds a SELECT_CONNECT, and - untested - kqueue support.

----------
Added file: http://bugs.python.org/file28615/tulip-selectors-2.diff
Added file: http://bugs.python.org/file28616/selector-8.diff

_______________________________________
Python tracker <report at bugs.python.org>
<http://bugs.python.org/issue16853>
_______________________________________
-------------- next part --------------
diff --git a/runtests.py b/runtests.py
--- a/runtests.py
+++ b/runtests.py
@@ -2,9 +2,12 @@
 
 # Originally written by Beech Horn (for NDB).
 
+import logging
 import sys
 import unittest
 
+##logging.basicConfig(level=logging.DEBUG)
+
 
 def load_tests():
   mods = ['events', 'futures', 'tasks']
diff --git a/tulip/events_test.py b/tulip/events_test.py
--- a/tulip/events_test.py
+++ b/tulip/events_test.py
@@ -10,6 +10,7 @@
 from . import events
 from . import transports
 from . import protocols
+from . import selectors
 from . import unix_events
 
 
@@ -37,8 +38,8 @@
 class EventLoopTestsMixin:
 
     def setUp(self):
-        pollster = self.POLLSTER_CLASS()
-        event_loop = unix_events.UnixEventLoop(pollster)
+        selector = self.SELECTOR_CLASS()
+        event_loop = unix_events.UnixEventLoop(selector)
         events.set_event_loop(event_loop)
 
     def testRun(self):
@@ -229,24 +230,24 @@
         el.run_once()
 
 
-if hasattr(select, 'kqueue'):
+if hasattr(selectors, 'KqueueSelector'):
     class KqueueEventLoopTests(EventLoopTestsMixin, unittest.TestCase):
-        POLLSTER_CLASS = unix_events.KqueuePollster
+        SELECTOR_CLASS = selectors.KqueueSelector
 
 
-if hasattr(select, 'epoll'):
+if hasattr(selectors, 'EpollSelector'):
     class EPollEventLoopTests(EventLoopTestsMixin, unittest.TestCase):
-        POLLSTER_CLASS = unix_events.EPollPollster
+        SELECTOR_CLASS = selectors.EpollSelector
 
 
-if hasattr(select, 'poll'):
+if hasattr(selectors, 'PollSelector'):
     class PollEventLoopTests(EventLoopTestsMixin, unittest.TestCase):
-        POLLSTER_CLASS = unix_events.PollPollster
+        SELECTOR_CLASS = selectors.PollSelector
 
 
 # Should always exist.
 class SelectEventLoopTests(EventLoopTestsMixin, unittest.TestCase):
-    POLLSTER_CLASS = unix_events.SelectPollster
+    SELECTOR_CLASS = selectors.SelectSelector
 
 
 class HandlerTests(unittest.TestCase):
diff --git a/tulip/selectors.py b/tulip/selectors.py
new file mode 100644
--- /dev/null
+++ b/tulip/selectors.py
@@ -0,0 +1,347 @@
+"""Select module.
+
+This module supports asynchronous I/O on multiple file descriptors.
+"""
+
+
+from select import *    # XXX
+
+
+# generic events, that must be mapped to implementation-specific ones
+# read event
+SELECT_IN  = (1 << 0)
+# write event
+SELECT_OUT = (1 << 1)
+# connect event
+SELECT_CONNECT = SELECT_OUT
+
+
+def _fileobj_to_fd(fileobj):
+    """Return a file descriptor from a file object.
+
+    Parameters:
+    fileobj -- file descriptor, or any object with a `fileno()` method
+
+    Returns:
+    corresponding file descriptor
+    """
+    if isinstance(fileobj, int):
+        fd = fileobj
+    else:
+        try:
+            fd = int(fileobj.fileno())
+        except (ValueError, TypeError):
+            raise ValueError("Invalid file object: {!r}".format(fileobj))
+    return fd
+
+
+class _Key:
+    """Object used internally to associate a file object to its backing file
+    descriptor, selected event mask and attached data."""
+
+    def __init__(self, fileobj, events, data=None):
+        self.fileobj = fileobj
+        self.fd = _fileobj_to_fd(fileobj)
+        self.events = events
+        self.data = data
+
+
+class _BaseSelector:
+    """Base selector class.
+
+    A selector supports registering file objects to be monitored for specific
+    I/O events.
+
+    A file object is a file descriptor or any object with a `fileno()` method.
+    An arbitrary object can be attached to the file object, which can be used
+    for example to store context information, a callback, etc.
+
+    A selector can use various implementations (select(), poll(), epoll()...)
+    depending on the platform. The default `Selector` class uses the most
+    performant implementation on the current platform.
+    """
+
+    def __init__(self):
+        # this maps file descriptors to keys
+        self._fd_to_key = {}
+        # this maps file objects to keys - for fast (un)registering
+        self._fileobj_to_key = {}
+
+    def register(self, fileobj, events, data=None):
+        """Register a file object.
+
+        Parameters:
+        fileobj -- file object
+        events  -- events to monitor (bitwise mask of SELECT_IN|SELECT_OUT)
+        data    -- attached data
+        """
+        if (not events) or (events & ~(SELECT_IN|SELECT_OUT)):
+            raise ValueError("Invalid events: {}".format(events))
+
+        if fileobj in self._fileobj_to_key:
+            raise ValueError("{!r} is already registered".format(fileobj))
+
+        key = _Key(fileobj, events, data)
+        self._fd_to_key[key.fd] = key
+        self._fileobj_to_key[fileobj] = key
+        return key
+
+    def unregister(self, fileobj):
+        """Unregister a file object.
+
+        Parameters:
+        fileobj -- file object
+        """
+        try:
+            key = self._fileobj_to_key[fileobj]
+            del self._fd_to_key[key.fd]
+            del self._fileobj_to_key[fileobj]
+        except KeyError:
+            raise ValueError("{!r} is not registered".format(fileobj))
+        return key
+
+    def modify(self, fileobj, events, data=None):
+        """Change a registered file object monitored events or attached data.
+
+        Parameters:
+        fileobj -- file object
+        events  -- events to monitor (bitwise mask of SELECT_IN|SELECT_OUT)
+        data    -- attached data
+        """
+        self.unregister(fileobj)
+        self.register(fileobj, events, data)
+
+    def select(self, timeout=None):
+        """Perform the actual selection, until some monitored file objects are
+        ready or a timeout expires.
+        
+        Parameters:
+        timeout -- if timeout > 0, this specifies the maximum wait time, in
+                   seconds
+                   if timeout == 0, the select() call won't block, and will
+                   report the currently ready file objects
+                   if timeout is None, select() will block until a monitored
+                   file object becomes ready
+
+        Returns:
+        list of (fileobj, events, attached data) for ready file objects
+        `events` is a bitwise mask of SELECT_IN|SELECT_OUT
+        """
+        raise NotImplementedError()
+
+    def close(self):
+        """Close the selector.
+
+        This must be called to make sure that any underlying resource is freed.
+        """
+        self._fd_to_key.clear()
+        self._fileobj_to_key.clear()
+
+    def get_info(self, fileobj):
+        """Return information about a registered file object.
+
+        Returns:
+        (events, data) associated to this file object
+
+        Raises KeyError if the file object is not registered.
+        """
+        try:
+            key = self._fileobj_to_key[fileobj]
+        except KeyError:
+            raise KeyError("{} is not registered".format(fileobj))
+        return key.events, key.data
+
+    def registered_count(self):
+        """Return the number of registered file objects.
+
+        Returns:
+        number of currently registered file objects
+        """
+        return len(self._fd_to_key)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *args):
+        self.close()
+
+    def _key_from_fd(self, fd):
+        """Return the key associated to a given file descriptor.
+
+        Parameters:
+        fd -- file descriptor
+
+        Returns:
+        corresponding key
+        """
+        try:
+            return self._fd_to_key[fd]
+        except KeyError:
+            raise RuntimeError("No key found for fd {}".format(fd))
+
+
+class SelectSelector(_BaseSelector):
+    """Select-based selector."""
+
+    def __init__(self):
+        super().__init__()
+        self._readers = set()
+        self._writers = set()
+
+    def register(self, fileobj, events, data=None):
+        key = super().register(fileobj, events, data)
+        if events & SELECT_IN:
+            self._readers.add(key.fd)
+        if events & SELECT_OUT:
+            self._writers.add(key.fd)
+
+    def unregister(self, fileobj):
+        key = super().unregister(fileobj)
+        self._readers.discard(key.fd)
+        self._writers.discard(key.fd)
+
+    def select(self, timeout=None):
+        r, w, _ = select(self._readers, self._writers, [], timeout)
+        r = set(r)
+        w = set(w)
+        ready = []
+        for fd in r | w:
+            events = 0
+            if fd in r:
+                events |= SELECT_IN
+            if fd in w:
+                events |= SELECT_OUT
+
+            key = self._key_from_fd(fd)
+            ready.append((key.fileobj, events, key.data))
+        return ready
+
+
+if 'poll' in globals():
+
+    class PollSelector(_BaseSelector):
+        """Poll-based selector."""
+    
+        def __init__(self):
+            super().__init__()
+            self._poll = poll()
+    
+        def register(self, fileobj, events, data=None):
+            key = super().register(fileobj, events, data)
+            poll_events = 0
+            if events & SELECT_IN:
+                poll_events |= POLLIN
+            if events & SELECT_OUT:
+                poll_events |= POLLOUT
+            self._poll.register(key.fd, poll_events)
+    
+        def unregister(self, fileobj):
+            key = super().unregister(fileobj)
+            self._poll.unregister(key.fd)
+    
+        def select(self, timeout=None):
+            timeout = None if timeout is None else int(1000 * timeout)
+            ready = []
+            for fd, event in self._poll.poll(timeout):
+                events = 0
+                if event & ~POLLIN:
+                    events |= SELECT_OUT
+                if event & ~POLLOUT:
+                    events |= SELECT_IN
+    
+                key = self._key_from_fd(fd)
+                ready.append((key.fileobj, events, key.data))
+            return ready
+
+
+if 'epoll' in globals():
+
+    class EpollSelector(_BaseSelector):
+        """Epoll-based selector."""
+    
+        def __init__(self):
+            super().__init__()
+            self._epoll = epoll()
+    
+        def register(self, fileobj, events, data=None):
+            key = super().register(fileobj, events, data)
+            epoll_events = 0
+            if events & SELECT_IN:
+                epoll_events |= EPOLLIN
+            if events & SELECT_OUT:
+                epoll_events |= EPOLLOUT
+            self._epoll.register(key.fd, epoll_events)
+    
+        def unregister(self, fileobj):
+            key = super().unregister(fileobj)
+            self._epoll.unregister(key.fd)
+    
+        def select(self, timeout=None):
+            timeout = -1 if timeout is None else timeout
+            max_ev = self.registered_count()
+            ready = []
+            for fd, event in self._epoll.poll(timeout, max_ev):
+                events = 0
+                if event & ~EPOLLIN:
+                    events |= SELECT_OUT
+                if event & ~EPOLLOUT:
+                    events |= SELECT_IN
+    
+                key = self._key_from_fd(fd)
+                ready.append((key.fileobj, events, key.data))
+            return ready
+    
+        def close(self):
+            super().close()
+            self._epoll.close()
+
+
+if 'kqueue' in globals():
+
+    class KqueueSelector(_BaseSelector):
+        """Kqueue-based selector."""
+    
+        def __init__(self):
+            super().__init__()
+            self._kqueue = kqueue()
+    
+        def register(self, fileobj, events, data=None):
+            key = super().register(fileobj, events, data)
+            if events & SELECT_IN:
+                kev = select.kevent(fd, select.KQ_FILTER_READ, select.KQ_EV_ADD)
+                self._kqueue.control([kev], 0, 0)
+            if events & SELECT_OUT:
+                kev = select.kevent(fd, select.KQ_FILTER_WRITE, select.KQ_EV_ADD)
+                self._kqueue.control([kev], 0, 0)
+    
+        def select(self, timeout=None):
+            max_ev = self.registered_count()
+            ready = []
+            for kev in self._kqueue.control(None, max_ev, timeout):
+                fd = kev.ident
+                flag = kev.filter
+                events = 0
+                if flag == select.KQ_FILTER_READ:
+                    events |= SELECT_IN
+                if flag == select.KQ_FILTER_WRITE:
+                    events |= SELECT_OUT
+    
+                key = self._key_from_fd(fd)
+                ready.append((key.fileobj, events, key.data))
+            return events
+    
+        def close(self):
+            super().close()
+            self._kqueue.close()
+
+
+# Choose the best implementation: roughly, epoll|kqueue > poll > select.
+# select() also can't accept a FD > FD_SETSIZE (usually around 1024)
+if 'KqueueSelector' in globals():
+    Selector = KqueueSelector
+elif 'EpollSelector' in globals():
+    Selector = EpollSelector
+elif 'PollSelector' in globals():
+    Selector = PollSelector
+else:
+    Selector = SelectSelector
diff --git a/tulip/unix_events.py b/tulip/unix_events.py
--- a/tulip/unix_events.py
+++ b/tulip/unix_events.py
@@ -1,10 +1,8 @@
 """UNIX event loop and related classes.
 
-NOTE: The Pollster classes are not part of the published API.
-
-The event loop can be broken up into a pollster (the part responsible
+The event loop can be broken up into a selector (the part responsible
 for telling us when file descriptors are ready) and the event loop
-proper, which wraps a pollster with functionality for scheduling
+proper, which wraps a selector with functionality for scheduling
 callbacks, immediately or at a given time in the future.
 
 Whenever a public API takes a callback, subsequent positional
@@ -13,22 +11,6 @@
 Keyword arguments for the callback are not supported; this is a
 conscious design decision, leaving the door open for keyword arguments
 to modify the meaning of the API call itself.
-
-There are several implementations of the pollster part, several using
-esoteric system calls that exist only on some platforms.  These are:
-
-- kqueue (most BSD systems)
-- epoll (newer Linux systems)
-- poll (most UNIX systems)
-- select (all UNIX systems, and Windows)
-
-NOTE: We don't use select on systems where any of the others is
-available, because select performs poorly as the number of file
-descriptors goes up.  The ranking is roughly:
-
-  1. kqueue, epoll, IOCP (best for each platform)
-  2. poll (linear in number of file descriptors polled)
-  3. select (linear in max number of file descriptors supported)
 """
 
 import collections
@@ -46,6 +28,7 @@
 from . import events
 from . import futures
 from . import protocols
+from . import selectors
 from . import tasks
 from . import transports
 
@@ -73,352 +56,6 @@
 _MAX_WORKERS = 5
 
 
-class PollsterBase:
-    """Base class for all polling implementations.
-
-    This defines an interface to register and unregister readers and
-    writers for specific file descriptors, and an interface to get a
-    list of events.  There's also an interface to check whether any
-    readers or writers are currently registered.
-    """
-
-    def __init__(self):
-        super().__init__()
-        self.readers = {}  # {fd: handler, ...}.
-        self.writers = {}  # {fd: handler, ...}.
-
-    def pollable(self):
-        """Return the number readers and writers currently registered."""
-        # The event loop needs the number since it must subtract one for
-        # the self-pipe.
-        return len(self.readers) + len(self.writers)
-
-    # Subclasses are expected to extend the add/remove methods.
-
-    def register_reader(self, fd, handler):
-        """Add or update a reader for a file descriptor."""
-        self.readers[fd] = handler
-
-    def register_writer(self, fd, handler):
-        """Add or update a writer for a file descriptor."""
-        self.writers[fd] = handler
-
-    def unregister_reader(self, fd):
-        """Remove the reader for a file descriptor."""
-        del self.readers[fd]
-
-    def unregister_writer(self, fd):
-        """Remove the writer for a file descriptor."""
-        del self.writers[fd]
-
-    def register_connector(self, fd, handler):
-        """Add or update a connector for a file descriptor."""
-        # On Unix a connector is the same as a writer.
-        self.register_writer(fd, handler)
-
-    def unregister_connector(self, fd):
-        """Remove the connector for a file descriptor."""
-        # On Unix a connector is the same as a writer.
-        self.unregister_writer(fd)
-
-    def poll(self, timeout=None):
-        """Poll for I/O events.  A subclass must implement this.
-
-        If timeout is omitted or None, this blocks until at least one
-        event is ready.  Otherwise, timeout gives a maximum time to
-        wait (an int of float in seconds) -- the method returns as
-        soon as at least one event is ready or when the timeout is
-        expired.  For a non-blocking poll, pass 0.
-
-        The return value is a list of events; it is empty when the
-        timeout expired before any events were ready.  Each event
-        is a handler previously passed to register_reader/writer().
-        """
-        raise NotImplementedError
-
-
-if sys.platform != 'win32':
-
-    class SelectPollster(PollsterBase):
-        """Pollster implementation using select."""
-
-        def poll(self, timeout=None):
-            readable, writable, _ = select.select(self.readers, self.writers,
-                                                  [], timeout)
-            events = []
-            events += (self.readers[fd] for fd in readable)
-            events += (self.writers[fd] for fd in writable)
-            return events
-
-else:
-
-    class SelectPollster(PollsterBase):
-        """Pollster implementation using select."""
-
-        def __init__(self):
-            super().__init__()
-            self.exceptionals = {}
-
-        def poll(self, timeout=None):
-            # Failed connections are reported as exceptional but not writable.
-            readable, writable, exceptional = select.select(
-                self.readers, self.writers, self.exceptionals, timeout)
-            writable = set(writable).union(exceptional)
-            events = []
-            events += (self.readers[fd] for fd in readable)
-            events += (self.writers[fd] for fd in writable)
-            return events
-
-        def register_connector(self, fd, token):
-            self.register_writer(fd, token)
-            self.exceptionals[fd] = token
-
-        def unregister_connector(self, fd):
-            self.unregister_writer(fd)
-            try:
-                del self.exceptionals[fd]
-            except KeyError:
-                # remove_connector() does not check fd in self.exceptionals.
-                pass
-
-
-class PollPollster(PollsterBase):
-    """Pollster implementation using poll."""
-
-    def __init__(self):
-        super().__init__()
-        self._poll = select.poll()
-
-    def _update(self, fd):
-        assert isinstance(fd, int), fd
-        flags = 0
-        if fd in self.readers:
-            flags |= select.POLLIN
-        if fd in self.writers:
-            flags |= select.POLLOUT
-        if flags:
-            self._poll.register(fd, flags)
-        else:
-            self._poll.unregister(fd)
-
-    def register_reader(self, fd, handler):
-        super().register_reader(fd, handler)
-        self._update(fd)
-
-    def register_writer(self, fd, handler):
-        super().register_writer(fd, handler)
-        self._update(fd)
-
-    def unregister_reader(self, fd):
-        super().unregister_reader(fd)
-        self._update(fd)
-
-    def unregister_writer(self, fd):
-        super().unregister_writer(fd)
-        self._update(fd)
-
-    def poll(self, timeout=None):
-        # Timeout is in seconds, but poll() takes milliseconds.
-        msecs = None if timeout is None else int(round(1000 * timeout))
-        events = []
-        for fd, flags in self._poll.poll(msecs):
-            if flags & ~select.POLLOUT:
-                if fd in self.readers:
-                    events.append(self.readers[fd])
-            if flags & ~select.POLLIN:
-                if fd in self.writers:
-                    events.append(self.writers[fd])
-        return events
-
-
-if sys.platform == 'win32':
-
-    class WindowsPollPollster(PollPollster):
-        """Pollster implementation using WSAPoll.
-
-        WSAPoll is only available on Windows Vista and later.  Python
-        does not currently support WSAPoll, but there is a patch
-        available at http://bugs.python.org/issue16507.
-        """
-
-        # REAP_PERIOD is the maximum wait before checking for failed
-        # connections.  This is necessary because WSAPoll() does notify us
-        # of failed connections.  See
-        #     daniel.haxx.se/blog/2012/10/10/wsapoll-is-broken/
-        REAP_PERIOD = 5.0
-
-        # FD_SETSIZE is maximum number of sockets in an fd_set
-        FD_SETSIZE = 512
-
-        def __init__(self):
-            super().__init__()
-            self.exceptionals = {}
-
-        def register_connector(self, fd, token):
-            self.register_writer(fd, token)
-            self.exceptionals[fd] = token
-
-        def unregister_connector(self, fd):
-            self.unregister_writer(fd)
-            try:
-                del self.exceptionals[fd]
-            except KeyError:
-                # remove_connector() does not check fd in self.exceptionals.
-                pass
-
-        def _get_failed_connector_events(self):
-            fds = []
-            remaining = list(self.exceptionals)
-            while remaining:
-                fds += select.select([], [], remaining[:self.FD_SETSIZE], 0)[2]
-                del remaining[:self.FD_SETSIZE]
-            return [(fd, select.POLLOUT) for fd in fds]
-
-        def poll(self, timeout=None):
-            if not self.exceptionals:
-                msecs = None if timeout is None else int(round(1000 * timeout))
-                polled = self._poll.poll(msecs)
-
-            elif timeout is None:
-                polled = None
-                while not polled:
-                    polled = (self._get_failed_connector_events() or
-                              self._poll.poll(self.REAP_PERIOD))
-
-            elif timeout == 0:
-                polled = (self._get_failed_connector_events() or
-                          self._poll.poll(0))
-
-            else:
-                start = time.monotonic()
-                deadline = start + timeout
-                polled = None
-                while timeout >= 0:
-                    msecs = int(round(1000 * min(self.REAP_PERIOD, timeout)))
-                    polled = (self._get_failed_connector_events() or
-                              self._poll.poll(self.REAP_PERIOD))
-                    if polled:
-                        break
-                    timemout = deadline - time.monotonic()
-
-            events = []
-            for fd, flags in polled:
-                if flags & ~select.POLLOUT:
-                    if fd in self.readers:
-                        events.append(self.readers[fd])
-                if flags & ~select.POLLIN:
-                    if fd in self.writers:
-                        events.append(self.writers[fd])
-            return events
-
-    PollPollster = WindowsPollPollster
-
-
-class EPollPollster(PollsterBase):
-    """Pollster implementation using epoll."""
-
-    def __init__(self):
-        super().__init__()
-        self._epoll = select.epoll()
-
-    def _update(self, fd):
-        assert isinstance(fd, int), fd
-        eventmask = 0
-        if fd in self.readers:
-            eventmask |= select.EPOLLIN
-        if fd in self.writers:
-            eventmask |= select.EPOLLOUT
-        if eventmask:
-            try:
-                self._epoll.register(fd, eventmask)
-            except IOError:
-                self._epoll.modify(fd, eventmask)
-        else:
-            self._epoll.unregister(fd)
-
-    def register_reader(self, fd, handler):
-        super().register_reader(fd, handler)
-        self._update(fd)
-
-    def register_writer(self, fd, handler):
-        super().register_writer(fd, handler)
-        self._update(fd)
-
-    def unregister_reader(self, fd):
-        super().unregister_reader(fd)
-        self._update(fd)
-
-    def unregister_writer(self, fd):
-        super().unregister_writer(fd)
-        self._update(fd)
-
-    def poll(self, timeout=None):
-        if timeout is None:
-            timeout = -1  # epoll.poll() uses -1 to mean "wait forever".
-        events = []
-        for fd, eventmask in self._epoll.poll(timeout):
-            if eventmask & ~select.EPOLLOUT:
-                if fd in self.readers:
-                    events.append(self.readers[fd])
-            if eventmask & ~select.EPOLLIN:
-                if fd in self.writers:
-                    events.append(self.writers[fd])
-        return events
-
-
-class KqueuePollster(PollsterBase):
-    """Pollster implementation using kqueue."""
-
-    def __init__(self):
-        super().__init__()
-        self._kqueue = select.kqueue()
-
-    def register_reader(self, fd, handler):
-        if fd not in self.readers:
-            kev = select.kevent(fd, select.KQ_FILTER_READ, select.KQ_EV_ADD)
-            self._kqueue.control([kev], 0, 0)
-        return super().register_reader(fd, handler)
-
-    def register_writer(self, fd, handler):
-        if fd not in self.writers:
-            kev = select.kevent(fd, select.KQ_FILTER_WRITE, select.KQ_EV_ADD)
-            self._kqueue.control([kev], 0, 0)
-        return super().register_writer(fd, handler)
-
-    def unregister_reader(self, fd):
-        super().unregister_reader(fd)
-        kev = select.kevent(fd, select.KQ_FILTER_READ, select.KQ_EV_DELETE)
-        self._kqueue.control([kev], 0, 0)
-
-    def unregister_writer(self, fd):
-        super().unregister_writer(fd)
-        kev = select.kevent(fd, select.KQ_FILTER_WRITE, select.KQ_EV_DELETE)
-        self._kqueue.control([kev], 0, 0)
-
-    def poll(self, timeout=None):
-        events = []
-        max_ev = len(self.readers) + len(self.writers)
-        for kev in self._kqueue.control(None, max_ev, timeout):
-            fd = kev.ident
-            flag = kev.filter
-            if flag == select.KQ_FILTER_READ and fd in self.readers:
-                events.append(self.readers[fd])
-            elif flag == select.KQ_FILTER_WRITE and fd in self.writers:
-                events.append(self.writers[fd])
-        return events
-
-
-# Pick the best pollster class for the platform.
-if hasattr(select, 'kqueue'):
-    best_pollster = KqueuePollster
-elif hasattr(select, 'epoll'):
-    best_pollster = EPollPollster
-elif hasattr(select, 'poll'):
-    best_pollster = PollPollster
-else:
-    best_pollster = SelectPollster
-
-
 class _StopError(BaseException):
     """Raised to stop the event loop."""
 
@@ -433,12 +70,13 @@
     See events.EventLoop for API specification.
     """
 
-    def __init__(self, pollster=None):
+    def __init__(self, selector=None):
         super().__init__()
-        if pollster is None:
-            logging.info('Using pollster: %s', best_pollster.__name__)
-            pollster = best_pollster()
-        self._pollster = pollster
+        if selector is None:
+            # pick the best selector class for the platform
+            selector = selectors.Selector()
+            logging.info('Using selector: %s', selector.__name__)
+        self._selector = selector
         self._ready = collections.deque()
         self._scheduled = []
         self._everytime = []
@@ -465,7 +103,9 @@
 
         TODO: Give this a timeout too?
         """
-        while self._ready or self._scheduled or self._pollster.pollable() > 1:
+        while (self._ready or
+               self._scheduled or
+               self._selector.registered_count() > 1):
             try:
                 self._run_once()
             except _StopError:
@@ -702,36 +342,83 @@
     def add_reader(self, fd, callback, *args):
         """Add a reader callback.  Return a Handler instance."""
         handler = events.Handler(None, callback, args)
-        self._pollster.register_reader(fd, handler)
+        try:
+            mask, (reader, writer, connector) = self._selector.get_info(fd)
+        except KeyError:
+            self._selector.register(fd, selectors.SELECT_IN,
+                                    (handler, None, None))
+        else:
+            self._selector.modify(fd, mask | selectors.SELECT_IN,
+                                  (handler, writer, connector))
+        
         return handler
 
     def remove_reader(self, fd):
         """Remove a reader callback."""
-        if fd in self._pollster.readers:
-            self._pollster.unregister_reader(fd)
+        try:
+            mask, (reader, writer, connector) = self._selector.get_info(fd)
+        except KeyError:
+            pass
+        else:
+            mask &= ~selectors.SELECT_IN
+            if not mask:
+                self._selector.unregister(fd)
+            else:
+                self._selector.modify(fd, mask, (None, writer, connector))
 
     def add_writer(self, fd, callback, *args):
         """Add a writer callback.  Return a Handler instance."""
         handler = events.Handler(None, callback, args)
-        self._pollster.register_writer(fd, handler)
+        try:
+            mask, (reader, writer, connector) = self._selector.get_info(fd)
+        except KeyError:
+            self._selector.register(fd, selectors.SELECT_OUT,
+                                    (None, handler, None))
+        else:
+            self._selector.modify(fd, mask | selectors.SELECT_OUT,
+                                  (reader, handler, connector))
         return handler
 
     def remove_writer(self, fd):
         """Remove a writer callback."""
-        if fd in self._pollster.writers:
-            self._pollster.unregister_writer(fd)
+        try:
+            mask, (reader, writer, connector) = self._selector.get_info(fd)
+        except KeyError:
+            pass
+        else:
+            mask &= ~selectors.SELECT_OUT
+            if not mask:
+                self._selector.unregister(fd)
+            else:
+                self._selector.modify(fd, mask, (reader, None, connector))
 
     def add_connector(self, fd, callback, *args):
         """Add a connector callback.  Return a Handler instance."""
-        dcall = events.Handler(None, callback, args)
-        self._pollster.register_connector(fd, dcall)
-        return dcall
+        # XXX As long as SELECT_CONNECT == SELECT_OUT, set the handler
+        # as both writer and connector.
+        handler = events.Handler(None, callback, args)
+        try:
+            mask, (reader, writer, connector) = self._selector.get_info(fd)
+        except KeyError:
+            self._selector.register(fd, selectors.SELECT_CONNECT,
+                                    (None, handler, handler))
+        else:
+            self._selector.modify(fd, mask | selectors.SELECT_CONNECT,
+                                  (reader, handler, handler))
+        return handler
 
     def remove_connector(self, fd):
         """Remove a connector callback."""
-        # Every connector fd is in self._pollsters.writers.
-        if fd in self._pollster.writers:
-            self._pollster.unregister_connector(fd)
+        try:
+            mask, (reader, writer, connector) = self._selector.get_info(fd)
+        except KeyError:
+            pass
+        else:
+            mask &= ~selectors.SELECT_CONNECT
+            if not mask:
+                self._selector.unregister(fd)
+            else:
+                self._selector.modify(fd, mask, (reader, None, None))
 
     def sock_recv(self, sock, n):
         """XXX"""
@@ -743,7 +430,7 @@
         fd = sock.fileno()
         if registered:
             # Remove the callback early.  It should be rare that the
-            # pollster says the fd is ready but the call still returns
+            # selector says the fd is ready but the call still returns
             # EAGAIN, and I am willing to take a hit in that case in
             # order to simplify the common case.
             self.remove_reader(fd)
@@ -876,10 +563,10 @@
         while self._scheduled and self._scheduled[0].cancelled:
             heapq.heappop(self._scheduled)
 
-        # Inspect the poll queue.  If there's exactly one pollable
+        # Inspect the poll queue.  If there's exactly one selectable
         # file descriptor, it's the self-pipe, and if there's nothing
         # scheduled, we should ignore it.
-        if self._pollster.pollable() > 1 or self._scheduled:
+        if self._selector.registered_count() > 1 or self._scheduled:
             if self._ready:
                 timeout = 0
             elif self._scheduled:
@@ -892,7 +579,7 @@
                     timeout = min(timeout, deadline)
 
             t0 = time.monotonic()
-            events = self._pollster.poll(timeout)
+            event_list = self._selector.select(timeout)
             t1 = time.monotonic()
             argstr = '' if timeout is None else ' %.3f' % timeout
             if t1-t0 >= 1:
@@ -900,8 +587,13 @@
             else:
                 level = logging.DEBUG
             logging.log(level, 'poll%s took %.3f seconds', argstr, t1-t0)
-            for handler in events:
-                self._add_callback(handler)
+            for fileobj, mask, (reader, writer, connector) in event_list:
+                if mask & selectors.SELECT_IN and reader is not None:
+                    self._add_callback(reader)
+                if mask & selectors.SELECT_OUT and writer is not None:
+                    self._add_callback(writer)
+                elif mask & selectors.SELECT_CONNECT and connector is not None:
+                    self._add_callback(connector)
 
         # Handle 'later' callbacks that are ready.
         now = time.monotonic()
-------------- next part --------------
diff --git a/Lib/select.py b/Lib/select.py
new file mode 100644
--- /dev/null
+++ b/Lib/select.py
@@ -0,0 +1,347 @@
+"""Select module.
+
+This module supports asynchronous I/O on multiple file descriptors.
+"""
+
+
+from _select import *
+
+
+# generic events, that must be mapped to implementation-specific ones
+# read event
+SELECT_IN  = (1 << 0)
+# write event
+SELECT_OUT = (1 << 1)
+# connect event
+SELECT_CONNECT = SELECT_OUT
+
+
+def _fileobj_to_fd(fileobj):
+    """Return a file descriptor from a file object.
+
+    Parameters:
+    fileobj -- file descriptor, or any object with a `fileno()` method
+
+    Returns:
+    corresponding file descriptor
+    """
+    if isinstance(fileobj, int):
+        fd = fileobj
+    else:
+        try:
+            fd = int(fileobj.fileno())
+        except (ValueError, TypeError):
+            raise ValueError("Invalid file object: {!r}".format(fileobj))
+    return fd
+
+
+class _Key:
+    """Object used internally to associate a file object to its backing file
+    descriptor, selected event mask and attached data."""
+
+    def __init__(self, fileobj, events, data=None):
+        self.fileobj = fileobj
+        self.fd = _fileobj_to_fd(fileobj)
+        self.events = events
+        self.data = data
+
+
+class _BaseSelector:
+    """Base selector class.
+
+    A selector supports registering file objects to be monitored for specific
+    I/O events.
+
+    A file object is a file descriptor or any object with a `fileno()` method.
+    An arbitrary object can be attached to the file object, which can be used
+    for example to store context information, a callback, etc.
+
+    A selector can use various implementations (select(), poll(), epoll()...)
+    depending on the platform. The default `Selector` class uses the most
+    performant implementation on the current platform.
+    """
+
+    def __init__(self):
+        # this maps file descriptors to keys
+        self._fd_to_key = {}
+        # this maps file objects to keys - for fast (un)registering
+        self._fileobj_to_key = {}
+
+    def register(self, fileobj, events, data=None):
+        """Register a file object.
+
+        Parameters:
+        fileobj -- file object
+        events  -- events to monitor (bitwise mask of SELECT_IN|SELECT_OUT)
+        data    -- attached data
+        """
+        if (not events) or (events & ~(SELECT_IN|SELECT_OUT)):
+            raise ValueError("Invalid events: {}".format(events))
+
+        if fileobj in self._fileobj_to_key:
+            raise ValueError("{!r} is already registered".format(fileobj))
+
+        key = _Key(fileobj, events, data)
+        self._fd_to_key[key.fd] = key
+        self._fileobj_to_key[fileobj] = key
+        return key
+
+    def unregister(self, fileobj):
+        """Unregister a file object.
+
+        Parameters:
+        fileobj -- file object
+        """
+        try:
+            key = self._fileobj_to_key[fileobj]
+            del self._fd_to_key[key.fd]
+            del self._fileobj_to_key[fileobj]
+        except KeyError:
+            raise ValueError("{!r} is not registered".format(fileobj))
+        return key
+
+    def modify(self, fileobj, events, data=None):
+        """Change a registered file object monitored events or attached data.
+
+        Parameters:
+        fileobj -- file object
+        events  -- events to monitor (bitwise mask of SELECT_IN|SELECT_OUT)
+        data    -- attached data
+        """
+        self.unregister(fileobj)
+        self.register(fileobj, events, data)
+
+    def select(self, timeout=None):
+        """Perform the actual selection, until some monitored file objects are
+        ready or a timeout expires.
+        
+        Parameters:
+        timeout -- if timeout > 0, this specifies the maximum wait time, in
+                   seconds
+                   if timeout == 0, the select() call won't block, and will
+                   report the currently ready file objects
+                   if timeout is None, select() will block until a monitored
+                   file object becomes ready
+
+        Returns:
+        list of (fileobj, events, attached data) for ready file objects
+        `events` is a bitwise mask of SELECT_IN|SELECT_OUT
+        """
+        raise NotImplementedError()
+
+    def close(self):
+        """Close the selector.
+
+        This must be called to make sure that any underlying resource is freed.
+        """
+        self._fd_to_key.clear()
+        self._fileobj_to_key.clear()
+
+    def get_info(self, fileobj):
+        """Return information about a registered file object.
+
+        Returns:
+        (events, data) associated to this file object
+
+        Raises KeyError if the file object is not registered.
+        """
+        try:
+            key = self._fileobj_to_key[fileobj]
+        except KeyError:
+            raise KeyError("{} is not registered".format(fileobj))
+        return key.events, key.data
+
+    def registered_count(self):
+        """Return the number of registered file objects.
+
+        Returns:
+        number of currently registered file objects
+        """
+        return len(self._fd_to_key)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *args):
+        self.close()
+
+    def _key_from_fd(self, fd):
+        """Return the key associated to a given file descriptor.
+
+        Parameters:
+        fd -- file descriptor
+
+        Returns:
+        corresponding key
+        """
+        try:
+            return self._fd_to_key[fd]
+        except KeyError:
+            raise RuntimeError("No key found for fd {}".format(fd))
+
+
+class SelectSelector(_BaseSelector):
+    """Select-based selector."""
+
+    def __init__(self):
+        super().__init__()
+        self._readers = set()
+        self._writers = set()
+
+    def register(self, fileobj, events, data=None):
+        key = super().register(fileobj, events, data)
+        if events & SELECT_IN:
+            self._readers.add(key.fd)
+        if events & SELECT_OUT:
+            self._writers.add(key.fd)
+
+    def unregister(self, fileobj):
+        key = super().unregister(fileobj)
+        self._readers.discard(key.fd)
+        self._writers.discard(key.fd)
+
+    def select(self, timeout=None):
+        r, w, _ = select(self._readers, self._writers, [], timeout)
+        r = set(r)
+        w = set(w)
+        ready = []
+        for fd in r | w:
+            events = 0
+            if fd in r:
+                events |= SELECT_IN
+            if fd in w:
+                events |= SELECT_OUT
+
+            key = self._key_from_fd(fd)
+            ready.append((key.fileobj, events, key.data))
+        return ready
+
+
+if 'poll' in globals():
+
+    class PollSelector(_BaseSelector):
+        """Poll-based selector."""
+    
+        def __init__(self):
+            super().__init__()
+            self._poll = poll()
+    
+        def register(self, fileobj, events, data=None):
+            key = super().register(fileobj, events, data)
+            poll_events = 0
+            if events & SELECT_IN:
+                poll_events |= POLLIN
+            if events & SELECT_OUT:
+                poll_events |= POLLOUT
+            self._poll.register(key.fd, poll_events)
+    
+        def unregister(self, fileobj):
+            key = super().unregister(fileobj)
+            self._poll.unregister(key.fd)
+    
+        def select(self, timeout=None):
+            timeout = None if timeout is None else int(1000 * timeout)
+            ready = []
+            for fd, event in self._poll.poll(timeout):
+                events = 0
+                if event & ~POLLIN:
+                    events |= SELECT_OUT
+                if event & ~POLLOUT:
+                    events |= SELECT_IN
+    
+                key = self._key_from_fd(fd)
+                ready.append((key.fileobj, events, key.data))
+            return ready
+
+
+if 'epoll' in globals():
+
+    class EpollSelector(_BaseSelector):
+        """Epoll-based selector."""
+    
+        def __init__(self):
+            super().__init__()
+            self._epoll = epoll()
+    
+        def register(self, fileobj, events, data=None):
+            key = super().register(fileobj, events, data)
+            epoll_events = 0
+            if events & SELECT_IN:
+                epoll_events |= EPOLLIN
+            if events & SELECT_OUT:
+                epoll_events |= EPOLLOUT
+            self._epoll.register(key.fd, epoll_events)
+    
+        def unregister(self, fileobj):
+            key = super().unregister(fileobj)
+            self._epoll.unregister(key.fd)
+    
+        def select(self, timeout=None):
+            timeout = -1 if timeout is None else timeout
+            max_ev = self.registered_count()
+            ready = []
+            for fd, event in self._epoll.poll(timeout, max_ev):
+                events = 0
+                if event & ~EPOLLIN:
+                    events |= SELECT_OUT
+                if event & ~EPOLLOUT:
+                    events |= SELECT_IN
+    
+                key = self._key_from_fd(fd)
+                ready.append((key.fileobj, events, key.data))
+            return ready
+    
+        def close(self):
+            super().close()
+            self._epoll.close()
+
+
+if 'kqueue' in globals():
+
+    class KqueueSelector(_BaseSelector):
+        """Kqueue-based selector."""
+    
+        def __init__(self):
+            super().__init__()
+            self._kqueue = kqueue()
+    
+        def register(self, fileobj, events, data=None):
+            key = super().register(fileobj, events, data)
+            if events & SELECT_IN:
+                kev = select.kevent(fd, select.KQ_FILTER_READ, select.KQ_EV_ADD)
+                self._kqueue.control([kev], 0, 0)
+            if events & SELECT_OUT:
+                kev = select.kevent(fd, select.KQ_FILTER_WRITE, select.KQ_EV_ADD)
+                self._kqueue.control([kev], 0, 0)
+    
+        def select(self, timeout=None):
+            max_ev = self.registered_count()
+            ready = []
+            for kev in self._kqueue.control(None, max_ev, timeout):
+                fd = kev.ident
+                flag = kev.filter
+                events = 0
+                if flag == select.KQ_FILTER_READ:
+                    events |= SELECT_IN
+                if flag == select.KQ_FILTER_WRITE:
+                    events |= SELECT_OUT
+    
+                key = self._key_from_fd(fd)
+                ready.append((key.fileobj, events, key.data))
+            return events
+    
+        def close(self):
+            super().close()
+            self._kqueue.close()
+
+
+# Choose the best implementation: roughly, epoll|kqueue > poll > select.
+# select() also can't accept a FD > FD_SETSIZE (usually around 1024)
+if 'KqueueSelector' in globals():
+    Selector = KqueueSelector
+elif 'EpollSelector' in globals():
+    Selector = EpollSelector
+elif 'PollSelector' in globals():
+    Selector = PollSelector
+else:
+    Selector = SelectSelector
diff --git a/Lib/test/test_select.py b/Lib/test/test_select.py
--- a/Lib/test/test_select.py
+++ b/Lib/test/test_select.py
@@ -1,10 +1,25 @@
 import errno
 import os
+import random
 import select
 import sys
 import unittest
 from test import support
 
+try:
+    import resource
+except ImportError:
+    resource = None
+
+
+def find_ready_matching(ready, flag):
+    match = []
+    for fd, mode, data in ready:
+        if mode & flag:
+            match.append(fd)
+    return match
+
+
 @unittest.skipIf((sys.platform[:3]=='win'),
                  "can't easily test on this system")
 class SelectTestCase(unittest.TestCase):
@@ -75,9 +90,260 @@
         a[:] = [F()] * 10
         self.assertEqual(select.select([], a, []), ([], a[:5], []))
 
+
+class BasicSelectorTestCase(unittest.TestCase):
+
+    def test_constants(self):
+        select.SELECT_IN
+        select.SELECT_OUT
+
+
+class BaseSelectorTestCase(unittest.TestCase):
+
+    def test_error_conditions(self):
+        s = self.SELECTOR()
+        self.assertRaises(TypeError, s.register)
+        self.assertRaises(TypeError, s.register, 0)
+        self.assertRaises(ValueError, s.register, 0, 18)
+        self.assertRaises(TypeError, s.unregister, 0, 1)
+        self.assertRaises(TypeError, s.modify, 0)
+        self.assertRaises(TypeError, s.select, 0, 1)
+
+    def test_basic(self):
+        with self.SELECTOR() as s:
+            rd, wr = os.pipe()
+            wro = os.fdopen(os.dup(wr), "wb")
+            self.addCleanup(os.close, rd)
+            self.addCleanup(os.close, wr)
+            self.addCleanup(wro.close)
+
+            # test without attached data
+            s.register(wr, select.SELECT_OUT)
+            self.assertEqual(set(((wr, select.SELECT_OUT, None),)), set(s.select()))
+
+            # test with attached data
+            s.unregister(wr)
+            s.register(wr, select.SELECT_OUT, sys.stdin)
+            self.assertEqual(set(((wr, select.SELECT_OUT, sys.stdin),)), set(s.select()))
+
+            # test with file object
+            s.register(wro, select.SELECT_OUT)
+            self.assertEqual(set(((wro, select.SELECT_OUT, None),
+                                 (wr, select.SELECT_OUT, sys.stdin))), set(s.select()))
+            s.unregister(wro)
+
+            # modify
+            s.modify(wr, select.SELECT_OUT, sys.stdout)
+            self.assertEqual(set(((wr, select.SELECT_OUT, sys.stdout),)), set(s.select()))
+
+            # test timeout
+            s.register(rd, select.SELECT_IN, sys.stdin)
+            s.unregister(wr)
+            self.assertFalse(s.select(0.1))
+            s.register(wr, select.SELECT_OUT)
+            self.assertEqual(set(((wr, select.SELECT_OUT, None),)),
+                             set(s.select(0.1)))
+
+            # registering twice should raise an error
+            self.assertRaises(ValueError, s.register, wr, select.SELECT_OUT)
+
+            # test get_info()
+            self.assertEquals((select.SELECT_OUT, None), s.get_info(wr))
+            self.assertEquals((select.SELECT_IN, sys.stdin), s.get_info(rd))
+            self.assertRaises(KeyError, s.get_info, wro)
+
+            s.unregister(rd)
+            s.unregister(wr)
+
+            # test registered_count()
+            self.assertEquals(0, s.registered_count())
+            s.register(rd, select.SELECT_IN)
+            s.register(wr, select.SELECT_OUT)
+            self.assertEquals(2, s.registered_count())
+            s.modify(wr, select.SELECT_OUT, sys.stdout)
+            self.assertEquals(2, s.registered_count())
+            s.unregister(rd)
+            s.unregister(wr)
+            self.assertEquals(0, s.registered_count())
+
+            # unregistering twice should raise an error
+            self.assertRaises(ValueError, s.unregister, wr)
+
+    def test_selector(self):
+        s = self.SELECTOR()
+        self.addCleanup(s.close)
+
+        NUM_PIPES = 12
+        MSG = b" This is a test."
+        MSG_LEN = len(MSG)
+        readers = []
+        writers = []
+        r2w = {}
+        w2r = {}
+
+        for i in range(NUM_PIPES):
+            rd, wr = os.pipe()
+            s.register(rd, select.SELECT_IN)
+            s.register(wr, select.SELECT_OUT)
+            readers.append(rd)
+            writers.append(wr)
+            r2w[rd] = wr
+            w2r[wr] = rd
+
+        bufs = []
+
+        while writers:
+            ready = s.select()
+            ready_writers = find_ready_matching(ready, select.SELECT_OUT)
+            if not ready_writers:
+                self.fail("no pipes ready for writing")
+            wr = random.choice(ready_writers)
+            os.write(wr, MSG)
+
+            ready = s.select()
+            ready_readers = find_ready_matching(ready, select.SELECT_IN)
+            if not ready_readers:
+                self.fail("no pipes ready for reading")
+            self.assertEqual([w2r[wr]], ready_readers)
+            rd = ready_readers[0]
+            buf = os.read(rd, MSG_LEN)
+            self.assertEqual(len(buf), MSG_LEN)
+            bufs.append(buf)
+            os.close(r2w[rd]) ; os.close(rd)
+            s.unregister(r2w[rd])
+            s.unregister(rd)
+            writers.remove(r2w[rd])
+
+        self.assertEqual(bufs, [MSG] * NUM_PIPES)
+
+    def test_timeout(self):
+        s = self.SELECTOR()
+        self.addCleanup(s.close)
+
+        cmd = 'for i in 0 1 2 3 4 5 6 7 8 9; do echo testing...; sleep 1; done'
+        p = os.popen(cmd, 'r')
+        s.register(p.fileno(), select.SELECT_IN, p)
+
+        for tout in (0, 1, 2, 4, 8, 16) + (None,)*10:
+            if support.verbose:
+                print('timeout =', tout)
+
+            ready = s.select(tout)
+            if not ready:
+                continue
+            eof_seen = False
+            for fileobj, evt, data in ready:
+                if fileobj == p.fileno() and evt & select.SELECT_IN:
+                    while True:
+                        line = p.readline()
+                        if support.verbose:
+                            print(repr(line))
+                        if not line:
+                            if support.verbose:
+                                print('EOF')
+                            eof_seen = True
+                            break
+            if not eof_seen:
+                self.fail('Unexpected return values from select(): %r' % ready)
+        p.close()
+
+    def test_below_fd_setsize(self):
+        # No implementation should have a problem with less than FD_SETSIZE file
+        # descriptors. To be conservative, let's say 64.
+        NUM_FDS = 64
+
+        r, w = os.pipe()
+        self.addCleanup(os.close, r)
+        self.addCleanup(os.close, w)
+
+        with self.SELECTOR() as s:
+            for i in range(NUM_FDS):
+                fd = os.dup(w)
+                self.addCleanup(os.close, fd)
+
+                s.register(fd, select.SELECT_OUT)
+            self.assertEquals(NUM_FDS, len(s.select()))
+
+
+class ScalableSelectorTestCase:
+
+    # a mixin to test selector scalability
+
+    @unittest.skipUnless(resource, "Test needs resource module")
+    def test_above_fd_setsize(self):
+        # A scalable implementation should have no problem with more than
+        # FD_SETSIZE file descriptors. Since we don't know the value, we just
+        # try to set the soft RLIMIT_NOFILE to the hard RLIMIT_NOFILE ceiling.
+        try:
+            soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
+            resource.setrlimit(resource.RLIMIT_NOFILE, (hard, hard))
+            self.addCleanup(resource.setrlimit, resource.RLIMIT_NOFILE,
+                            (soft, hard))
+            NUM_FDS = hard
+        except OSError:
+            NUM_FDS = soft
+
+        # guard for already allocated FDs (stdin, stdout...)
+        NUM_FDS -= 16
+
+        r, w = os.pipe()
+        self.addCleanup(os.close, r)
+        self.addCleanup(os.close, w)
+
+        with self.SELECTOR() as s:
+            for i in range(NUM_FDS):
+                try:
+                    fd = os.dup(w)
+                except OSError as e:
+                    if e.errno == errno.EMFILE:
+                        # too many FD, skip
+                        self.skipTest("FD limit reached")
+                    raise
+
+                self.addCleanup(os.close, fd)
+                s.register(fd, select.SELECT_OUT)
+
+            self.assertEquals(NUM_FDS, len(s.select()))
+
+
+ at unittest.skipIf((sys.platform[:3]=='win'),
+                 "can't easily test on this system")
+class SelectorTestCase(BaseSelectorTestCase):
+
+    SELECTOR = select.Selector
+
+
+class SelectSelectorTestCase(BaseSelectorTestCase):
+
+    SELECTOR = select.SelectSelector
+
+
+ at unittest.skipUnless(hasattr(select, 'PollSelector'), "Test needs select.poll()")
+class PollSelectorTestCase(BaseSelectorTestCase, ScalableSelectorTestCase):
+
+    SELECTOR = getattr(select, 'PollSelector', None)
+
+
+ at unittest.skipUnless(hasattr(select, 'EpollSelector'), "Test needs select.epoll()")
+class EpollSelectorTestCase(BaseSelectorTestCase, ScalableSelectorTestCase):
+
+    SELECTOR = getattr(select, 'EpollSelector', None)
+
+
+ at unittest.skipUnless(hasattr(select, 'KqueueSelector'), "Test needs select.kqueue()")
+class KqueueSelectorTestCase(BaseSelectorTestCase, ScalableSelectorTestCase):
+
+    SELECTOR = getattr(select, 'KqueueSelector', None)
+
+
 def test_main():
-    support.run_unittest(SelectTestCase)
+    tests = [SelectTestCase]
+    tests.extend([BasicSelectorTestCase, SelectorTestCase,
+                  SelectSelectorTestCase, PollSelectorTestCase,
+                  EpollSelectorTestCase, KqueueSelectorTestCase])
+    support.run_unittest(*tests)
     support.reap_children()
 
+
 if __name__ == "__main__":
     test_main()
diff --git a/Modules/selectmodule.c b/Modules/selectmodule.c
--- a/Modules/selectmodule.c
+++ b/Modules/selectmodule.c
@@ -2129,7 +2129,7 @@
 
 static struct PyModuleDef selectmodule = {
     PyModuleDef_HEAD_INIT,
-    "select",
+    "_select",
     module_doc,
     -1,
     select_methods,
@@ -2143,7 +2143,7 @@
 
 
 PyMODINIT_FUNC
-PyInit_select(void)
+PyInit__select(void)
 {
     PyObject *m;
     m = PyModule_Create(&selectmodule);
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -623,7 +623,7 @@
             missing.append('spwd')
 
         # select(2); not on ancient System V
-        exts.append( Extension('select', ['selectmodule.c']) )
+        exts.append( Extension('_select', ['selectmodule.c']) )
 
         # Fred Drake's interface to the Python parser
         exts.append( Extension('parser', ['parsermodule.c']) )


More information about the Python-bugs-list mailing list