Commit 0f086770 authored by Jason Madden's avatar Jason Madden Committed by GitHub

Merge pull request #1636 from gevent/issue1532

Add gevent.selectors
parents f0e3bb96 54df0039
...@@ -40,6 +40,11 @@ environment: ...@@ -40,6 +40,11 @@ environment:
# a later point release. # a later point release.
# 64-bit # 64-bit
- PYTHON: "C:\\Python27-x64"
PYTHON_VERSION: "2.7.x" # currently 2.7.13
PYTHON_ARCH: "64"
PYTHON_EXE: python
- PYTHON: "C:\\Python38-x64" - PYTHON: "C:\\Python38-x64"
PYTHON_VERSION: "3.8.x" PYTHON_VERSION: "3.8.x"
PYTHON_ARCH: "64" PYTHON_ARCH: "64"
...@@ -50,11 +55,6 @@ environment: ...@@ -50,11 +55,6 @@ environment:
PYTHON_ARCH: "64" PYTHON_ARCH: "64"
PYTHON_EXE: python PYTHON_EXE: python
- PYTHON: "C:\\Python27-x64"
PYTHON_VERSION: "2.7.x" # currently 2.7.13
PYTHON_ARCH: "64"
PYTHON_EXE: python
- PYTHON: "C:\\Python36-x64" - PYTHON: "C:\\Python36-x64"
PYTHON_VERSION: "3.6.x" # currently 3.6.0 PYTHON_VERSION: "3.6.x" # currently 3.6.0
PYTHON_ARCH: "64" PYTHON_ARCH: "64"
......
=======================================================
:mod:`gevent.selectors` -- High-level IO Multiplexing
=======================================================
.. automodule:: gevent.selectors
:members:
Add ``gevent.selectors`` containing ``GeventSelector``. This selector
implementation uses gevent details to attempt to reduce overhead when
polling many file descriptors, only some of which become ready at any
given time.
This is monkey-patched as ``selectors.DefaultSelector`` by default.
This is available on Python 2 if the ``selectors2`` backport is
installed. (This backport is installed automatically using the
``recommended`` extra.) When monkey-patching, ``selectors`` is made
available as an alias to this module.
...@@ -146,7 +146,13 @@ monitor ...@@ -146,7 +146,13 @@ monitor
build on all platforms.) build on all platforms.)
recommended recommended
A shortcut for installing suggested extras together. A shortcut for installing suggested extras together. This includes
the non-test extras defined here, plus:
- `backports.socketpair
<https://pypi.org/project/backports.socketpair/>`_ on Python
2/Windows (beginning with release 20.6.0);
- `selectors2 <https://pypi.org/project/selectors2/>`_ on Python 2 (beginning with release 20.6.0).
test test
Everything needed to run the complete gevent test suite. Everything needed to run the complete gevent test suite.
......
...@@ -315,6 +315,10 @@ EXTRA_MONITOR = [ ...@@ -315,6 +315,10 @@ EXTRA_MONITOR = [
EXTRA_RECOMMENDED = [ EXTRA_RECOMMENDED = [
# We need this at runtime to use the libev-CFFI and libuv backends # We need this at runtime to use the libev-CFFI and libuv backends
CFFI_DEP, CFFI_DEP,
# Backport of selectors module to Python 2
'selectors2 ; python_version == "2.7"',
# Backport of socket.socketpair to Python 2; only needed on Windows
'backports.socketpair ; python_version == "2.7" and sys_platform == "win32"',
] + EXTRA_DNSPYTHON + EXTRA_EVENTS + EXTRA_MONITOR ] + EXTRA_DNSPYTHON + EXTRA_EVENTS + EXTRA_MONITOR
......
...@@ -194,6 +194,10 @@ class AbstractLinkable(object): ...@@ -194,6 +194,10 @@ class AbstractLinkable(object):
# The object itself becomes false in a boolean way as soon # The object itself becomes false in a boolean way as soon
# as this method returns. # as this method returns.
notifier = self._notifier notifier = self._notifier
if notifier is None:
# XXX: How did we get here?
self._check_and_notify()
return
# Early links are allowed to remove later links, and links # Early links are allowed to remove later links, and links
# are allowed to add more links, thus we must not # are allowed to add more links, thus we must not
# make a copy of our the ``_links`` list, we must traverse it and # make a copy of our the ``_links`` list, we must traverse it and
......
...@@ -25,6 +25,7 @@ MAPPING = { ...@@ -25,6 +25,7 @@ MAPPING = {
'gevent.local': '_threading_local', 'gevent.local': '_threading_local',
'gevent.socket': 'socket', 'gevent.socket': 'socket',
'gevent.select': 'select', 'gevent.select': 'select',
'gevent.selectors': 'selectors' if PY3 else 'selectors2',
'gevent.ssl': 'ssl', 'gevent.ssl': 'ssl',
'gevent.thread': '_thread' if PY3 else 'thread', 'gevent.thread': '_thread' if PY3 else 'thread',
'gevent.subprocess': 'subprocess', 'gevent.subprocess': 'subprocess',
......
...@@ -460,7 +460,8 @@ class socket(_socketcommon.SocketMixin): ...@@ -460,7 +460,8 @@ class socket(_socketcommon.SocketMixin):
SocketType = socket SocketType = socket
if hasattr(_socket, 'socketpair'): if hasattr(_socket, 'socketpair'):
# The native, low-level socketpair returns
# low-level objects
def socketpair(family=getattr(_socket, 'AF_UNIX', _socket.AF_INET), def socketpair(family=getattr(_socket, 'AF_UNIX', _socket.AF_INET),
type=_socket.SOCK_STREAM, proto=0): type=_socket.SOCK_STREAM, proto=0):
one, two = _socket.socketpair(family, type, proto) one, two = _socket.socketpair(family, type, proto)
...@@ -469,6 +470,20 @@ if hasattr(_socket, 'socketpair'): ...@@ -469,6 +470,20 @@ if hasattr(_socket, 'socketpair'):
one._drop() one._drop()
two._drop() two._drop()
return result return result
elif hasattr(__socket__, 'socketpair'):
# The high-level backport uses high-level socket APIs. It works
# cooperatively automatically if we're monkey-patched,
# else we must do it ourself.
_orig_socketpair = __socket__.socketpair
def socketpair(family=_socket.AF_INET, type=_socket.SOCK_STREAM, proto=0):
one, two = _orig_socketpair(family, type, proto)
if not isinstance(one, socket):
one = socket(_sock=one)
two = socket(_sock=two)
if PYPY:
one._drop()
two._drop()
return one, two
elif 'socketpair' in __implements__: elif 'socketpair' in __implements__:
__implements__.remove('socketpair') __implements__.remove('socketpair')
......
...@@ -128,6 +128,11 @@ if is_macos: ...@@ -128,6 +128,11 @@ if is_macos:
import _socket import _socket
_realsocket = _socket.socket _realsocket = _socket.socket
import socket as __socket__ import socket as __socket__
try:
# Provide implementation of socket.socketpair on Windows < 3.5.
import backports.socketpair
except ImportError:
pass
_name = _value = None _name = _value = None
__imports__ = copy_globals(__socket__, globals(), __imports__ = copy_globals(__socket__, globals(),
......
...@@ -415,6 +415,20 @@ def patch_module(target_module, source_module, items=None, ...@@ -415,6 +415,20 @@ def patch_module(target_module, source_module, items=None,
return True return True
def _check_availability(name):
"""
Test that the source and target modules for *name* are
available and return them.
:raise ImportError: If the source or target cannot be imported.
:return: The tuple ``(gevent_module, target_module, target_module_name)``
"""
gevent_module = getattr(__import__('gevent.' + name), name)
target_module_name = getattr(gevent_module, '__target__', name)
target_module = __import__(target_module_name)
return gevent_module, target_module, target_module_name
def _patch_module(name, def _patch_module(name,
items=None, items=None,
_warnings=None, _warnings=None,
...@@ -423,9 +437,7 @@ def _patch_module(name, ...@@ -423,9 +437,7 @@ def _patch_module(name,
_notify_did_subscribers=True, _notify_did_subscribers=True,
_call_hooks=True): _call_hooks=True):
gevent_module = getattr(__import__('gevent.' + name), name) gevent_module, target_module, target_module_name = _check_availability(name)
module_name = getattr(gevent_module, '__target__', name)
target_module = __import__(module_name)
patch_module(target_module, gevent_module, items=items, patch_module(target_module, gevent_module, items=items,
_warnings=_warnings, _patch_kwargs=_patch_kwargs, _warnings=_warnings, _patch_kwargs=_patch_kwargs,
...@@ -451,7 +463,7 @@ def _patch_module(name, ...@@ -451,7 +463,7 @@ def _patch_module(name,
_notify_will_subscribers=False, _notify_will_subscribers=False,
_notify_did_subscribers=False, _notify_did_subscribers=False,
_call_hooks=False) _call_hooks=False)
saved[alternate_name] = saved[module_name] saved[alternate_name] = saved[target_module_name]
return gevent_module, target_module return gevent_module, target_module
...@@ -1013,20 +1025,48 @@ def patch_select(aggressive=True): ...@@ -1013,20 +1025,48 @@ def patch_select(aggressive=True):
and :func:`select.poll` with :class:`gevent.select.poll` (where available). and :func:`select.poll` with :class:`gevent.select.poll` (where available).
If ``aggressive`` is true (the default), also remove other If ``aggressive`` is true (the default), also remove other
blocking functions from :mod:`select` and (on Python 3.4 and blocking functions from :mod:`select` .
above) :mod:`selectors`:
- :func:`select.epoll` - :func:`select.epoll`
- :func:`select.kqueue` - :func:`select.kqueue`
- :func:`select.kevent` - :func:`select.kevent`
- :func:`select.devpoll` (Python 3.5+) - :func:`select.devpoll` (Python 3.5+)
"""
_patch_module('select',
_patch_kwargs={'aggressive': aggressive})
@_ignores_DoNotPatch
def patch_selectors(aggressive=True):
"""
Replace :class:`selectors.DefaultSelector` with
:class:`gevent.selectors.GeventSelector`.
If ``aggressive`` is true (the default), also remove other
blocking classes :mod:`selectors`:
- :class:`selectors.EpollSelector` - :class:`selectors.EpollSelector`
- :class:`selectors.KqueueSelector` - :class:`selectors.KqueueSelector`
- :class:`selectors.DevpollSelector` (Python 3.5+) - :class:`selectors.DevpollSelector` (Python 3.5+)
On Python 2, the :mod:`selectors2` module is used instead
of :mod:`selectors` if it is available. If this module cannot
be imported, no patching is done and :mod:`gevent.selectors` is
not available.
In :func:`patch_all`, the *select* argument controls both this function
and :func:`patch_select`.
.. versionadded:: NEXT
""" """
_patch_module('select', try:
_check_availability('selectors')
except ImportError: # pragma: no cover
return
_patch_module('selectors',
_patch_kwargs={'aggressive': aggressive}) _patch_kwargs={'aggressive': aggressive})
@_ignores_DoNotPatch @_ignores_DoNotPatch
def patch_subprocess(): def patch_subprocess():
""" """
...@@ -1178,6 +1218,7 @@ def patch_all(socket=True, dns=True, time=True, select=True, thread=True, os=Tru ...@@ -1178,6 +1218,7 @@ def patch_all(socket=True, dns=True, time=True, select=True, thread=True, os=Tru
patch_socket(dns=dns, aggressive=aggressive) patch_socket(dns=dns, aggressive=aggressive)
if select: if select:
patch_select(aggressive=aggressive) patch_select(aggressive=aggressive)
patch_selectors(aggressive=aggressive)
if ssl: if ssl:
patch_ssl(_warnings=_warnings, _first_time=first_time) patch_ssl(_warnings=_warnings, _first_time=first_time)
if subprocess: if subprocess:
......
...@@ -258,6 +258,25 @@ class poll(object): ...@@ -258,6 +258,25 @@ class poll(object):
def modify(self, fd, eventmask): def modify(self, fd, eventmask):
self.register(fd, eventmask) self.register(fd, eventmask)
def _get_started_watchers(self, watcher_cb):
watchers = []
io = self.loop.io
MAXPRI = self.loop.MAXPRI
try:
for fd, flags in iteritems(self.fds):
watcher = io(fd, flags)
watchers.append(watcher)
watcher.priority = MAXPRI
watcher.start(watcher_cb, fd, pass_events=True)
except:
for awatcher in watchers:
awatcher.stop()
awatcher.close()
raise
return watchers
def poll(self, timeout=None): def poll(self, timeout=None):
""" """
poll the registered fds. poll the registered fds.
...@@ -270,15 +289,8 @@ class poll(object): ...@@ -270,15 +289,8 @@ class poll(object):
i.e., block. This was always the case with libev. i.e., block. This was always the case with libev.
""" """
result = PollResult() result = PollResult()
watchers = [] watchers = self._get_started_watchers(result.add_event)
io = self.loop.io
MAXPRI = self.loop.MAXPRI
try: try:
for fd, flags in iteritems(self.fds):
watcher = io(fd, flags)
watchers.append(watcher)
watcher.priority = MAXPRI
watcher.start(result.add_event, fd, pass_events=True)
if timeout is not None: if timeout is not None:
if timeout < 0: if timeout < 0:
# The docs for python say that an omitted timeout, # The docs for python say that an omitted timeout,
...@@ -320,7 +332,6 @@ class poll(object): ...@@ -320,7 +332,6 @@ class poll(object):
def _gevent_do_monkey_patch(patch_request): def _gevent_do_monkey_patch(patch_request):
aggressive = patch_request.patch_kwargs['aggressive'] aggressive = patch_request.patch_kwargs['aggressive']
target_mod = patch_request.target_module
patch_request.default_patch_items() patch_request.default_patch_items()
...@@ -334,55 +345,3 @@ def _gevent_do_monkey_patch(patch_request): ...@@ -334,55 +345,3 @@ def _gevent_do_monkey_patch(patch_request):
'kevent', 'kevent',
'devpoll', 'devpoll',
) )
if patch_request.PY3:
# TODO: Do we need to broadcast events about patching the selectors
# package? If so, must be careful to deal with DoNotPatch exceptions.
# Python 3 wants to use `select.select` as a member function,
# leading to this error in selectors.py (because
# gevent.select.select is not a builtin and doesn't get the
# magic auto-static that they do):
#
# r, w, _ = self._select(self._readers, self._writers, [], timeout)
# TypeError: select() takes from 3 to 4 positional arguments but 5 were given
#
# Note that this obviously only happens if selectors was
# imported after we had patched select; but there is a code
# path that leads to it being imported first (but now we've
# patched select---so we can't compare them identically). It also doesn't
# happen on Windows, because they define a normal method for _select, to work around
# some weirdness in the handling of the third argument.
orig_select_select = patch_request.get_original('select', 'select')
assert target_mod.select is not orig_select_select
selectors = __import__('selectors')
if selectors.SelectSelector._select in (target_mod.select, orig_select_select):
def _select(self, *args, **kwargs): # pylint:disable=unused-argument
return select(*args, **kwargs)
selectors.SelectSelector._select = _select
_select._gevent_monkey = True # prove for test cases
# Python 3.7 refactors the poll-like selectors to use a common
# base class and capture a reference to select.poll, etc, at
# import time. selectors tends to get imported early
# (importing 'platform' does it: platform -> subprocess -> selectors),
# so we need to clean that up.
if hasattr(selectors, 'PollSelector') and hasattr(selectors.PollSelector, '_selector_cls'):
selectors.PollSelector._selector_cls = poll
if aggressive:
# If `selectors` had already been imported before we removed
# select.epoll|kqueue|devpoll, these may have been defined in terms
# of those functions. They'll fail at runtime.
patch_request.remove_item(
selectors,
'EpollSelector',
'KqueueSelector',
'DevpollSelector',
)
selectors.DefaultSelector = getattr(
selectors,
'PollSelector',
selectors.SelectSelector
)
# Copyright (c) 2020 gevent contributors.
"""
This module provides :class:`GeventSelector`, a high-level IO
multiplexing mechanism. This is aliased to :class:`DefaultSelector`.
This module provides the same API as the selectors defined in :mod:`selectors`.
On Python 2, this module is only available if the `selectors2
<https://pypi.org/project/selectors2/>`_ backport is installed.
.. versionadded:: NEXT
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
from collections import defaultdict
try:
import selectors as __selectors__
except ImportError:
# Probably on Python 2. Do we have the backport?
import selectors2 as __selectors__
__target__ = 'selectors2'
from gevent.hub import _get_hub_noargs as get_hub
from gevent import sleep
from gevent._compat import iteritems
from gevent._compat import itervalues
from gevent._util import copy_globals
from gevent._util import Lazy
from gevent.event import Event
from gevent.select import _EV_READ
from gevent.select import _EV_WRITE
__implements__ = [
'DefaultSelector',
]
__extra__ = [
'GeventSelector',
]
__all__ = __implements__ + __extra__
__imports__ = copy_globals(
__selectors__, globals(),
names_to_ignore=__all__,
# Copy __all__; __all__ is defined by selectors2 but not Python 3.
dunder_names_to_keep=('__all__',)
)
_POLL_ALL = _EV_READ | _EV_WRITE
EVENT_READ = __selectors__.EVENT_READ
EVENT_WRITE = __selectors__.EVENT_WRITE
_ALL_EVENTS = EVENT_READ | EVENT_WRITE
SelectorKey = __selectors__.SelectorKey
# In 3.4 and selectors2, BaseSelector is a concrete
# class that can be called. In 3.5 and later, it's an
# ABC, with the real implementation being
# passed to _BaseSelectorImpl.
_BaseSelectorImpl = getattr(
__selectors__,
'_BaseSelectorImpl',
__selectors__.BaseSelector
)
class GeventSelector(_BaseSelectorImpl):
"""
A selector implementation using gevent primitives.
This is a type of :class:`selectors.BaseSelector`, so the documentation
for that class applies here.
.. caution::
As the base class indicates, it is critically important to
unregister file objects before closing them. (Or close the selector
they are registered with before closing them.) Failure to do so
may crash the process or have other unintended results.
"""
# Notes on the approach:
#
# It's easy to wrap a selector implementation around
# ``gevent.select.poll``; in fact that's what happens by default
# when monkey-patching in Python 3. But the problem with that is
# each call to ``selector.select()`` will result in creating and
# then destroying new kernel-level polling resources, as nothing
# in ``gevent.select`` can keep watchers around (because the underlying
# file could be closed at any time). This ends up producing a large
# number of syscalls that are unnecessary.
#
# So here, we take advantage of the fact that it is documented and
# required that files not be closed while they are registered.
# This lets us persist watchers. Indeed, it lets us continually
# accrue events in the background before a call to ``select()`` is even
# made. We can take advantage of this to return results immediately, without
# a syscall, if we have them.
#
# We create watchers in ``register()`` and destroy them in
# ``unregister()``. They do not get started until the first call
# to ``select()``, though. Once they are started, they don't get
# stopped until they deliver an event.
# Lifecycle:
# register() -> inactive_watchers
# select() -> inactive_watchers -> active_watchers;
# active_watchers -> inactive_watchers
def __init__(self, hub=None):
if hub is not None:
self.hub = hub
# {fd: watcher}
self._active_watchers = {}
self._inactive_watchers = {}
# {fd: EVENT_READ|EVENT_WRITE}
self._accumulated_events = defaultdict(int)
self._ready = Event()
super(GeventSelector, self).__init__()
def __callback(self, events, fd):
if events > 0:
cur_event_for_fd = self._accumulated_events[fd]
if events & _EV_READ:
cur_event_for_fd |= EVENT_READ
if events & _EV_WRITE:
cur_event_for_fd |= EVENT_WRITE
self._accumulated_events[fd] = cur_event_for_fd
self._ready.set()
@Lazy
def hub(self): # pylint:disable=method-hidden
return get_hub()
def register(self, fileobj, events, data=None):
key = _BaseSelectorImpl.register(self, fileobj, events, data)
if events == _ALL_EVENTS:
flags = _POLL_ALL
elif events == EVENT_READ:
flags = _EV_READ
else:
flags = _EV_WRITE
loop = self.hub.loop
io = loop.io
MAXPRI = loop.MAXPRI
self._inactive_watchers[key.fd] = watcher = io(key.fd, flags)
watcher.priority = MAXPRI
return key
def unregister(self, fileobj):
key = _BaseSelectorImpl.unregister(self, fileobj)
if key.fd in self._active_watchers:
watcher = self._active_watchers.pop(key.fd)
else:
watcher = self._inactive_watchers.pop(key.fd)
watcher.stop()
watcher.close()
self._accumulated_events.pop(key.fd, None)
return key
# XXX: Can we implement ``modify`` more efficiently than
# ``unregister()``+``register()``? We could detect the no-change
# case and do nothing; recent versions of the standard library
# do that.
def select(self, timeout=None):
"""
Poll for I/O.
Note that, like the built-in selectors, this will block
indefinitely if no timeout is given and no files have been
registered.
"""
# timeout > 0 : block seconds
# timeout <= 0 : No blocking.
# timeout = None: Block forever
# Event.wait doesn't deal with negative values
if timeout is not None and timeout < 0:
timeout = 0
# Start any watchers that need started. Note that they may
# not actually get a chance to do anything yet if we already had
# events set.
for fd, watcher in iteritems(self._inactive_watchers):
watcher.start(self.__callback, fd, pass_events=True)
self._active_watchers.update(self._inactive_watchers)
self._inactive_watchers.clear()
# The _ready event is either already set (in which case
# there are some results waiting in _accumulated_events) or
# not set, in which case we have to block. But to make the two cases
# behave the same, we will always yield to the event loop.
if self._ready.is_set():
sleep()
self._ready.wait(timeout)
self._ready.clear()
# TODO: If we have nothing ready, but they ask us not to block,
# should we make an effort to actually spin the event loop and let
# it check for events?
result = []
for fd, event in iteritems(self._accumulated_events):
key = self._key_from_fd(fd)
watcher = self._active_watchers.pop(fd)
## The below is taken without comment from
## https://github.com/gevent/gevent/pull/1523/files and
## hasn't been checked:
#
# Since we are emulating an epoll object within another epoll object,
# once a watcher has fired, we must deactivate it until poll is called
# next. If we did not, someone else could call, e.g., gevent.time.sleep
# and any unconsumed bytes on our watched fd would prevent the process
# from sleeping correctly.
watcher.stop()
if key:
result.append((key, event & key.events))
self._inactive_watchers[fd] = watcher
else: # pragma: no cover
# If the key was gone, then somehow we've been unregistered.
# Don't put it back in inactive, close it.
watcher.close()
self._accumulated_events.clear()
return result
def close(self):
for d in self._active_watchers, self._inactive_watchers:
if d is None:
continue # already closed
for watcher in itervalues(d):
watcher.stop()
watcher.close()
self._active_watchers = self._inactive_watchers = None
self._accumulated_events = None
self.hub = None
_BaseSelectorImpl.close(self)
DefaultSelector = GeventSelector
def _gevent_do_monkey_patch(patch_request):
aggressive = patch_request.patch_kwargs['aggressive']
target_mod = patch_request.target_module
patch_request.default_patch_items()
import sys
if 'selectors' not in sys.modules:
# Py2: Make 'import selectors' work
sys.modules['selectors'] = sys.modules[__name__]
# Python 3 wants to use `select.select` as a member function,
# leading to this error in selectors.py (because
# gevent.select.select is not a builtin and doesn't get the
# magic auto-static that they do):
#
# r, w, _ = self._select(self._readers, self._writers, [], timeout)
# TypeError: select() takes from 3 to 4 positional arguments but 5 were given
#
# Note that this obviously only happens if selectors was
# imported after we had patched select; but there is a code
# path that leads to it being imported first (but now we've
# patched select---so we can't compare them identically). It also doesn't
# happen on Windows, because they define a normal method for _select, to work around
# some weirdness in the handling of the third argument.
#
# The backport doesn't have that.
orig_select_select = patch_request.get_original('select', 'select')
assert target_mod.select is not orig_select_select
selectors = __selectors__
SelectSelector = selectors.SelectSelector
if hasattr(SelectSelector, '_select') and SelectSelector._select in (
target_mod.select, orig_select_select
):
from gevent.select import select
def _select(self, *args, **kwargs): # pylint:disable=unused-argument
return select(*args, **kwargs)
selectors.SelectSelector._select = _select
_select._gevent_monkey = True # prove for test cases
if aggressive:
# If `selectors` had already been imported before we removed
# select.epoll|kqueue|devpoll, these may have been defined in terms
# of those functions. They'll fail at runtime.
patch_request.remove_item(
selectors,
'EpollSelector',
'KqueueSelector',
'DevpollSelector',
)
selectors.DefaultSelector = DefaultSelector
# Python 3.7 refactors the poll-like selectors to use a common
# base class and capture a reference to select.poll, etc, at
# import time. selectors tends to get imported early
# (importing 'platform' does it: platform -> subprocess -> selectors),
# so we need to clean that up.
if hasattr(selectors, 'PollSelector') and hasattr(selectors.PollSelector, '_selector_cls'):
from gevent.select import poll
selectors.PollSelector._selector_cls = poll
...@@ -259,14 +259,6 @@ class Definitions(DefinitionsBase): ...@@ -259,14 +259,6 @@ class Definitions(DefinitionsBase):
when=APPVEYOR & PY3 when=APPVEYOR & PY3
) )
test__socketpair = Ignored(
"""
Py35 added socket.socketpair, all other releases
are missing it. No reason to even test it.
""",
when=WIN & PY2
)
test_ftplib = Flaky( test_ftplib = Flaky(
r""" r"""
could be a problem of appveyor - not sure could be a problem of appveyor - not sure
......
...@@ -174,7 +174,7 @@ class AbstractTestMixin(object): ...@@ -174,7 +174,7 @@ class AbstractTestMixin(object):
return return
if self.__implements__ is not None and self.stdlib_module is None: if self.__implements__ is not None and self.stdlib_module is None:
raise AssertionError( raise AssertionError(
'%s (%r) has __implements__ (%s) but no stdlib counterpart (%s)' '%s (%r) has __implements__ (%s) but no stdlib counterpart module exists (%s)'
% (self.modname, self.module, self.__implements__, self.stdlib_name)) % (self.modname, self.module, self.__implements__, self.stdlib_name))
@skip_if_no_stdlib_counterpart @skip_if_no_stdlib_counterpart
......
...@@ -4,22 +4,22 @@ try: ...@@ -4,22 +4,22 @@ try:
# things up properly if the order is wrong. # things up properly if the order is wrong.
import selectors import selectors
except ImportError: except ImportError:
selectors = None import selectors2 as selectors
import socket
import gevent
from gevent.monkey import patch_all from gevent.monkey import patch_all
import gevent.testing as greentest import gevent.testing as greentest
patch_all() patch_all()
@greentest.skipIf( from gevent.selectors import DefaultSelector
selectors is None, from gevent.selectors import GeventSelector
"selectors module not present" from gevent.tests.test__selectors import SelectorTestMixin
)
class TestSelectors(greentest.TestCase): class TestSelectors(SelectorTestMixin, greentest.TestCase):
@greentest.skipOnPy2(
'selectors2 backport does not use _select'
)
@greentest.skipOnWindows( @greentest.skipOnWindows(
"SelectSelector._select is a normal function on Windows" "SelectSelector._select is a normal function on Windows"
) )
...@@ -28,45 +28,17 @@ class TestSelectors(greentest.TestCase): ...@@ -28,45 +28,17 @@ class TestSelectors(greentest.TestCase):
_select = selectors.SelectSelector._select _select = selectors.SelectSelector._select
self.assertIn('_gevent_monkey', dir(_select)) self.assertIn('_gevent_monkey', dir(_select))
@greentest.skipUnless( def test_default(self):
hasattr(selectors, 'PollSelector'),
"Needs gevent.select.poll"
)
def test_poll_is_default(self):
# Depending on the order of imports, gevent.select.poll may be defined but # Depending on the order of imports, gevent.select.poll may be defined but
# selectors.PollSelector may not be defined. # selectors.PollSelector may not be defined.
# https://github.com/gevent/gevent/issues/1466 # https://github.com/gevent/gevent/issues/1466
self.assertIs(selectors.DefaultSelector, selectors.PollSelector) self.assertIs(DefaultSelector, GeventSelector)
self.assertIs(selectors.DefaultSelector, GeventSelector)
def _check_selector(self, sel):
def read(conn, _mask):
data = conn.recv(100) # Should be ready
if data:
conn.send(data) # Hope it won't block
sel.unregister(conn)
conn.close()
def run_selector_once():
events = sel.select()
for key, mask in events:
key.data(key.fileobj, mask)
sock1, sock2 = socket.socketpair()
try:
sel.register(sock1, selectors.EVENT_READ, read)
glet = gevent.spawn(run_selector_once)
DATA = b'abcdef'
sock2.send(DATA)
data = sock2.recv(50)
self.assertEqual(data, DATA)
finally:
sel.close()
sock1.close()
sock2.close()
glet.join(10)
self.assertTrue(glet.ready())
def test_import_selectors(self):
# selectors can always be imported once monkey-patched. On Python 2,
# this is an alias for gevent.selectors.
__import__('selectors')
def _make_test(name, kind): # pylint:disable=no-self-argument def _make_test(name, kind): # pylint:disable=no-self-argument
if kind is None: if kind is None:
...@@ -74,7 +46,7 @@ class TestSelectors(greentest.TestCase): ...@@ -74,7 +46,7 @@ class TestSelectors(greentest.TestCase):
self.skipTest(name + ' is not defined') self.skipTest(name + ' is not defined')
else: else:
def m(self, k=kind): def m(self, k=kind):
sel = k() with k() as sel:
self._check_selector(sel) self._check_selector(sel)
m.__name__ = 'test_selector_' + name m.__name__ = 'test_selector_' + name
return m return m
...@@ -90,8 +62,13 @@ class TestSelectors(greentest.TestCase): ...@@ -90,8 +62,13 @@ class TestSelectors(greentest.TestCase):
'DevpollSelector', 'DevpollSelector',
'PollSelector', 'PollSelector',
'SelectSelector', 'SelectSelector',
GeventSelector,
): ):
if not isinstance(SelKindName, type):
SelKind = getattr(selectors, SelKindName, None) SelKind = getattr(selectors, SelKindName, None)
else:
SelKind = SelKindName
SelKindName = SelKind.__name__
m = _make_test(SelKindName, SelKind) m = _make_test(SelKindName, SelKind)
locals()[m.__name__] = m locals()[m.__name__] = m
...@@ -100,5 +77,6 @@ class TestSelectors(greentest.TestCase): ...@@ -100,5 +77,6 @@ class TestSelectors(greentest.TestCase):
del _make_test del _make_test
if __name__ == '__main__': if __name__ == '__main__':
greentest.main() greentest.main()
# Tests for gevent.selectors in its native form, without
# monkey-patching.
import gevent
from gevent import socket
from gevent import selectors
import gevent.testing as greentest
class SelectorTestMixin(object):
@staticmethod
def run_selector_once(sel, timeout=3):
# Run in a background greenlet, leaving the main
# greenlet free to send data.
events = sel.select(timeout=timeout)
for key, mask in events:
key.data(sel, key.fileobj, mask)
gevent.sleep()
unregister_after_send = True
def read_from_ready_socket_and_reply(self, selector, conn, _events):
data = conn.recv(100) # Should be ready
if data:
conn.send(data) # Hope it won't block
# Must unregister before we close.
if self.unregister_after_send:
selector.unregister(conn)
conn.close()
def _check_selector(self, sel):
server, client = socket.socketpair()
try:
sel.register(server, selectors.EVENT_READ, self.read_from_ready_socket_and_reply)
glet = gevent.spawn(self.run_selector_once, sel)
DATA = b'abcdef'
client.send(DATA)
data = client.recv(50) # here is probably where we yield to the event loop
self.assertEqual(data, DATA)
finally:
sel.close()
server.close()
client.close()
glet.join(10)
self.assertTrue(glet.ready())
class GeventSelectorTest(SelectorTestMixin,
greentest.TestCase):
def test_select_using_socketpair(self):
# Basic test.
with selectors.GeventSelector() as sel:
self._check_selector(sel)
def test_select_many_sockets(self):
try:
AF_UNIX = socket.AF_UNIX
except AttributeError:
AF_UNIX = None
pairs = [socket.socketpair() for _ in range(10)]
try:
server_sel = selectors.GeventSelector()
client_sel = selectors.GeventSelector()
for i, pair in enumerate(pairs):
server, client = pair
server_sel.register(server, selectors.EVENT_READ,
self.read_from_ready_socket_and_reply)
client_sel.register(client, selectors.EVENT_READ, i)
# Prime them all to be ready at once.
data = str(i).encode('ascii')
client.send(data)
# Read and reply to all the clients..
# Everyone should be ready, so we ask not to block.
# The call to gevent.idle() is there to make sure that
# all event loop implementations (looking at you, libuv)
# get a chance to poll for IO. Without it, libuv
# doesn't find any results here.
# Not blocking only works for AF_UNIX sockets, though.
# If we got AF_INET (Windows) the data may need some time to
# traverse through the layers.
gevent.idle()
self.run_selector_once(
server_sel,
timeout=-1 if pairs[0][0].family == AF_UNIX else 3)
found = 0
for key, _ in client_sel.select(timeout=3):
expected = str(key.data).encode('ascii')
data = key.fileobj.recv(50)
self.assertEqual(data, expected)
found += 1
self.assertEqual(found, len(pairs))
finally:
server_sel.close()
client_sel.close()
for pair in pairs:
for s in pair:
s.close()
if __name__ == '__main__':
greentest.main()
...@@ -308,6 +308,7 @@ class TestDefaultSpawn(TestCase): ...@@ -308,6 +308,7 @@ class TestDefaultSpawn(TestCase):
with self.assertRaises(TypeError): with self.assertRaises(TypeError):
self.ServerClass(self.get_listener(), backlog=25) self.ServerClass(self.get_listener(), backlog=25)
@greentest.skipOnLibuvOnCIOnPyPy("Sometimes times out")
def test_backlog_is_accepted_for_address(self): def test_backlog_is_accepted_for_address(self):
self.server = self.ServerSubClass((greentest.DEFAULT_BIND_ADDR, 0), backlog=25) self.server = self.ServerSubClass((greentest.DEFAULT_BIND_ADDR, 0), backlog=25)
self.assertConnectionRefused() self.assertConnectionRefused()
......
...@@ -15,6 +15,8 @@ class TestSocketpair(unittest.TestCase): ...@@ -15,6 +15,8 @@ class TestSocketpair(unittest.TestCase):
self.assertEqual(msg, read) self.assertEqual(msg, read)
y.close() y.close()
@unittest.skipUnless(hasattr(socket, 'fromfd'),
'Needs socket.fromfd')
def test_fromfd(self): def test_fromfd(self):
msg = b'hello world' msg = b'hello world'
x, y = socket.socketpair() x, y = socket.socketpair()
......
...@@ -438,7 +438,7 @@ class TestMaxsize(TestCase): ...@@ -438,7 +438,7 @@ class TestMaxsize(TestCase):
pool.spawn(sleep, 0.2) pool.spawn(sleep, 0.2)
pool.spawn(sleep, 0.3) pool.spawn(sleep, 0.3)
gevent.sleep(0.2) gevent.sleep(0.2)
self.assertEqual(pool.size, 3) self.assertGreaterEqual(pool.size, 2)
pool.maxsize = 0 pool.maxsize = 0
gevent.sleep(0.2) gevent.sleep(0.2)
self.assertEqualFlakyRaceCondition(pool.size, 0) self.assertEqualFlakyRaceCondition(pool.size, 0)
......
...@@ -743,8 +743,9 @@ else: ...@@ -743,8 +743,9 @@ else:
future = self._threadpool.spawn(fn, *args, **kwargs) future = self._threadpool.spawn(fn, *args, **kwargs)
return _FutureProxy(future) return _FutureProxy(future)
def shutdown(self, wait=True): def shutdown(self, wait=True, **kwargs): # pylint:disable=arguments-differ
super(ThreadPoolExecutor, self).shutdown(wait) # In 3.9, this added ``cancel_futures=False``
super(ThreadPoolExecutor, self).shutdown(wait, **kwargs)
# XXX: We don't implement wait properly # XXX: We don't implement wait properly
kill = getattr(self._threadpool, 'kill', None) kill = getattr(self._threadpool, 'kill', None)
if kill: # pylint:disable=using-constant-test if kill: # pylint:disable=using-constant-test
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment