Commit e3e555e2 authored by Jason Madden's avatar Jason Madden Committed by GitHub

Merge pull request #1169 from gevent/plugins

Add extension points to gevent.monkey using events and setuptools entry points.
parents 5f881cae b0435230
......@@ -96,6 +96,7 @@ ignored-modules=gevent._corecffi,gevent.os,os,greenlet,threading,gevent.libev.co
[DESIGN]
max-attributes=12
max-parents=10
[BASIC]
bad-functions=input
......
......@@ -68,6 +68,14 @@ Enhancements
``<script>``, including paths to packages or compiled bytecode.
Reported in :issue:`1157` by Eddie Linder.
- Add a simple event framework for decoupled communication. It uses
:mod:`zope.event` if that is installed.
- :mod:`gevent.monkey` has support for plugins in the form of event
subscribers and setuptools entry points. See :pr:`1158` and
:issue:`1162`. setuptools must be installed at runtime for its entry
points to function.
Monitoring and Debugging
------------------------
......@@ -84,10 +92,8 @@ Monitoring and Debugging
use it, and ``GEVENT_MAX_BLOCKING_TIME`` to configure the blocking
interval.
- Add a simple event framework for decoupled communication. It uses
:mod:`zope.event` if that is installed. The monitoring thread emits
events when it detects certain conditions, like loop blocked or
memory limits exceeded.
- The monitoring thread emits events when it detects certain
conditions, like loop blocked or memory limits exceeded.
- Add settings for monitoring memory usage and emitting events when a
threshold is exceeded and then corrected. gevent currently supplies
......
==================================
:mod:`gevent` -- basic utilities
==================================
====================================================
:mod:`gevent` -- basic utilities and configuration
====================================================
.. module:: gevent
......
......@@ -344,6 +344,11 @@ def run_setup(ext_modules, run_make):
"Development Status :: 4 - Beta"
],
python_requires=">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*",
entry_points={
'gevent.plugins.monkey.will_patch_all': [
"signal_os_incompat = gevent.monkey:_subscribe_signal_os",
],
},
)
# Tools like pyroma expect the actual call to `setup` to be performed
......
......@@ -487,13 +487,11 @@ class MonitorThread(BoolSettingMixin, Setting):
If this setting is true, then this thread will be created
the first time the hub is switched to,
or you can call `gevent.hub.Hub.start_periodic_monitoring_thread` at any
or you can call :meth:`gevent.hub.Hub.start_periodic_monitoring_thread` at any
time to create it (from the same thread that will run the hub). That function
will return an object with a method ``add_monitoring_function(function, period)``
that you can call to add your own periodic monitoring function. ``function``
will be called with one argument, the hub it is monitoring. It will be called
in a separate native thread than the one running the hub and **must not**
attempt to use the gevent asynchronous API.
will return an instance of :class:`gevent.events.IPeriodicMonitorThread`
to which you can add your own monitoring functions. That function
also emits an event of :class:`gevent.events.PeriodicMonitorThreadStartedEvent`.
.. seealso:: `max_blocking_time`
......
......@@ -16,6 +16,8 @@ from gevent.events import notify
from gevent.events import EventLoopBlocked
from gevent.events import MemoryUsageThresholdExceeded
from gevent.events import MemoryUsageUnderThreshold
from gevent.events import IPeriodicMonitorThread
from gevent.events import implementer
from gevent._compat import thread_mod_name
from gevent._compat import perf_counter
......@@ -63,6 +65,7 @@ class _MonitorEntry(object):
def __repr__(self):
return repr((self.function, self.period, self.last_run_time))
@implementer(IPeriodicMonitorThread)
class PeriodicMonitoringThread(object):
# The amount of seconds we will sleep when we think we have nothing
......@@ -145,20 +148,6 @@ class PeriodicMonitoringThread(object):
return self._monitoring_functions
def add_monitoring_function(self, function, period):
"""
Schedule the *function* to be called approximately every *period* fractional seconds.
The *function* receives one argument, the hub being monitored. It is called
in the monitoring thread, *not* the hub thread.
If the *function* is already a monitoring function, then its *period*
will be updated for future runs.
If the *period* is ``None``, then the function will be removed.
A *period* less than or equal to zero is not allowed.
"""
if not callable(function):
raise ValueError("function must be callable")
......
......@@ -28,14 +28,41 @@ from __future__ import print_function
__all__ = [
'subscribers',
# monitor thread
'IEventLoopBlocked',
'EventLoopBlocked',
'IMemoryUsageThresholdExceeded',
'MemoryUsageThresholdExceeded',
'IMemoryUsageUnderThreshold',
'MemoryUsageUnderThreshold',
# Hub
'IPeriodicMonitorThread',
'IPeriodicMonitorThreadStartedEvent',
'PeriodicMonitorThreadStartedEvent',
# monkey
'IGeventPatchEvent',
'GeventPatchEvent',
'IGeventWillPatchEvent',
'DoNotPatch',
'GeventWillPatchEvent',
'IGeventDidPatchEvent',
'IGeventWillPatchModuleEvent',
'GeventWillPatchModuleEvent',
'IGeventDidPatchModuleEvent',
'GeventDidPatchModuleEvent',
'IGeventWillPatchAllEvent',
'GeventWillPatchAllEvent',
'IGeventDidPatchBuiltinModulesEvent',
'GeventDidPatchBuiltinModulesEvent',
'IGeventDidPatchAllEvent',
'GeventDidPatchAllEvent',
]
# pylint:disable=no-self-argument
try:
from zope.event import subscribers
from zope.event import notify
......@@ -58,6 +85,32 @@ except ImportError:
subscriber(event)
notify = notify # export
try:
# pkg_resources is technically optional, we don't
# list a hard dependency on it.
__import__('pkg_resources')
except ImportError:
notify_and_call_entry_points = notify
else:
from pkg_resources import iter_entry_points
import platform
try:
# Cache the platform info. pkg_resources uses
# platform.machine() for environment markers, and
# platform.machine() wants to call os.popen('uname'), which is
# broken on Py2 when the gevent child signal handler is
# installed. (see test__monkey_sigchild_2.py)
platform.uname()
except: # pylint:disable=bare-except
pass
finally:
del platform
def notify_and_call_entry_points(event):
notify(event)
for plugin in iter_entry_points(event.ENTRY_POINT_NAME):
subscriber = plugin.load()
subscriber(event)
try:
from zope.interface import Interface
......@@ -74,6 +127,48 @@ except ImportError:
def Attribute(s):
return s
class IPeriodicMonitorThread(Interface):
"""
The contract for the periodic monitoring thread that is started
by the hub.
"""
def add_monitoring_function(function, period):
"""
Schedule the *function* to be called approximately every *period* fractional seconds.
The *function* receives one argument, the hub being monitored. It is called
in the monitoring thread, *not* the hub thread. It **must not** attempt to
use the gevent asynchronous API.
If the *function* is already a monitoring function, then its *period*
will be updated for future runs.
If the *period* is ``None``, then the function will be removed.
A *period* less than or equal to zero is not allowed.
"""
class IPeriodicMonitorThreadStartedEvent(Interface):
"""
The event emitted when a hub starts a periodic monitoring thread.
You can use this event to add additional monitoring functions.
"""
monitor = Attribute("The instance of `IPeriodicMonitorThread` that was started.")
class PeriodicMonitorThreadStartedEvent(object):
"""
The implementation of :class:`IPeriodicMonitorThreadStartedEvent`.
"""
#: The name of the setuptools entry point that is called when this
#: event is emitted.
ENTRY_POINT_NAME = 'gevent.plugins.hub.periodic_monitor_thread_started'
def __init__(self, monitor):
self.monitor = monitor
class IEventLoopBlocked(Interface):
"""
......@@ -165,3 +260,231 @@ class MemoryUsageUnderThreshold(_AbstractMemoryEvent):
def __init__(self, mem_usage, max_allowed, memory_info, max_usage):
super(MemoryUsageUnderThreshold, self).__init__(mem_usage, max_allowed, memory_info)
self.max_memory_usage = max_usage
class IGeventPatchEvent(Interface):
"""
The root for all monkey-patch events gevent emits.
"""
source = Attribute("The source object containing the patches.")
target = Attribute("The destination object to be patched.")
@implementer(IGeventPatchEvent)
class GeventPatchEvent(object):
"""
Implementation of `IGeventPatchEvent`.
"""
def __init__(self, source, target):
self.source = source
self.target = target
def __repr__(self):
return '<%s source=%r target=%r at %x>' % (self.__class__.__name__,
self.source,
self.target,
id(self))
class IGeventWillPatchEvent(IGeventPatchEvent):
"""
An event emitted *before* gevent monkey-patches something.
If a subscriber raises `DoNotPatch`, then patching this particular
item will not take place.
"""
class DoNotPatch(BaseException):
"""
Subscribers to will-patch events can raise instances
of this class to tell gevent not to patch that particular item.
"""
@implementer(IGeventWillPatchEvent)
class GeventWillPatchEvent(GeventPatchEvent):
"""
Implementation of `IGeventWillPatchEvent`.
"""
class IGeventDidPatchEvent(IGeventPatchEvent):
"""
An event emitted *after* gevent has patched something.
"""
@implementer(IGeventDidPatchEvent)
class GeventDidPatchEvent(GeventPatchEvent):
"""
Implementation of `IGeventDidPatchEvent`.
"""
class IGeventWillPatchModuleEvent(IGeventWillPatchEvent):
"""
An event emitted *before* gevent begins patching a specific module.
Both *source* and *target* attributes are module objects.
"""
module_name = Attribute("The name of the module being patched. "
"This is the same as ``target.__name__``.")
target_item_names = Attribute("The list of item names to patch. "
"This can be modified in place with caution.")
@implementer(IGeventWillPatchModuleEvent)
class GeventWillPatchModuleEvent(GeventWillPatchEvent):
"""
Implementation of `IGeventWillPatchModuleEvent`.
"""
#: The name of the setuptools entry point that is called when this
#: event is emitted.
ENTRY_POINT_NAME = 'gevent.plugins.monkey.will_patch_module'
def __init__(self, module_name, source, target, items):
super(GeventWillPatchModuleEvent, self).__init__(source, target)
self.module_name = module_name
self.target_item_names = items
class IGeventDidPatchModuleEvent(IGeventDidPatchEvent):
"""
An event emitted *after* gevent has completed patching a specific
module.
"""
module_name = Attribute("The name of the module being patched. "
"This is the same as ``target.__name__``.")
@implementer(IGeventDidPatchModuleEvent)
class GeventDidPatchModuleEvent(GeventDidPatchEvent):
"""
Implementation of `IGeventDidPatchModuleEvent`.
"""
#: The name of the setuptools entry point that is called when this
#: event is emitted.
ENTRY_POINT_NAME = 'gevent.plugins.monkey.did_patch_module'
def __init__(self, module_name, source, target):
super(GeventDidPatchModuleEvent, self).__init__(source, target)
self.module_name = module_name
# TODO: Maybe it would be useful for the the module patch events
# to have an attribute telling if they're being done during patch_all?
class IGeventWillPatchAllEvent(IGeventWillPatchEvent):
"""
An event emitted *before* gevent begins patching the system.
Following this event will be a series of
`IGeventWillPatchModuleEvent` and `IGeventDidPatchModuleEvent` for
each patched module.
Once the gevent builtin modules have been processed,
`IGeventDidPatchBuiltinModulesEvent` will be emitted. Processing
this event is an ideal time for third-party modules to be imported
and patched (which may trigger its own will/did patch module
events).
Finally, a `IGeventDidPatchAllEvent` will be sent.
If a subscriber to this event raises `DoNotPatch`, no patching
will be done.
The *source* and *target* attributes have undefined values.
"""
patch_all_arguments = Attribute(
"A dictionary of all the arguments to `gevent.monkey.patch_all`. "
"This dictionary should not be modified. "
)
patch_all_kwargs = Attribute(
"A dictionary of the extra arguments to `gevent.monkey.patch_all`. "
"This dictionary should not be modified. "
)
def will_patch_module(module_name):
"""
Return whether the module named *module_name* will be patched.
"""
class _PatchAllMixin(object):
def __init__(self, patch_all_arguments, patch_all_kwargs):
super(_PatchAllMixin, self).__init__(None, None)
self._patch_all_arguments = patch_all_arguments
self._patch_all_kwargs = patch_all_kwargs
@property
def patch_all_arguments(self):
return self._patch_all_arguments.copy()
@property
def patch_all_kwargs(self):
return self._patch_all_kwargs.copy()
def __repr__(self):
return '<%s %r at %x>' % (self.__class__.__name__,
self._patch_all_arguments,
id(self))
@implementer(IGeventWillPatchAllEvent)
class GeventWillPatchAllEvent(_PatchAllMixin, GeventWillPatchEvent):
"""
Implementation of `IGeventWillPatchAllEvent`.
"""
#: The name of the setuptools entry point that is called when this
#: event is emitted.
ENTRY_POINT_NAME = 'gevent.plugins.monkey.will_patch_all'
def will_patch_module(self, module_name):
return self.patch_all_arguments.get(module_name)
class IGeventDidPatchBuiltinModulesEvent(IGeventDidPatchEvent):
"""
Event emitted *after* the builtin modules have been patched.
The values of the *source* and *target* attributes are undefined.
"""
patch_all_arguments = Attribute(
"A dictionary of all the arguments to `gevent.monkey.patch_all`. "
"This dictionary should not be modified. "
)
patch_all_kwargs = Attribute(
"A dictionary of the extra arguments to `gevent.monkey.patch_all`. "
"This dictionary should not be modified. "
)
@implementer(IGeventDidPatchBuiltinModulesEvent)
class GeventDidPatchBuiltinModulesEvent(_PatchAllMixin, GeventDidPatchEvent):
"""
Implementation of `IGeventDidPatchBuiltinModulesEvent`.
"""
#: The name of the setuptools entry point that is called when this
#: event is emitted.
ENTRY_POINT_NAME = 'gevent.plugins.monkey.did_patch_builtins'
class IGeventDidPatchAllEvent(IGeventDidPatchEvent):
"""
Event emitted after gevent has patched all modules, both builtin
and those provided by plugins/subscribers.
The values of the *source* and *target* attributes are undefined.
"""
@implementer(IGeventDidPatchAllEvent)
class GeventDidPatchAllEvent(_PatchAllMixin, GeventDidPatchEvent):
"""
Implementation of `IGeventDidPatchAllEvent`.
"""
#: The name of the setuptools entry point that is called when this
#: event is emitted.
ENTRY_POINT_NAME = 'gevent.plugins.monkey.did_patch_all'
......@@ -564,11 +564,16 @@ class Hub(WaitOperationsGreenlet):
# in the threadpool tests. The monitoring threads will eventually notice their
# hub object is gone.
from gevent._monitor import PeriodicMonitoringThread
from gevent.events import PeriodicMonitorThreadStartedEvent
from gevent.events import notify_and_call_entry_points
self.periodic_monitoring_thread = PeriodicMonitoringThread(self)
if self.main_hub:
self.periodic_monitoring_thread.install_monitor_memory_usage()
notify_and_call_entry_points(PeriodicMonitorThreadStartedEvent(
self.periodic_monitoring_thread))
return self.periodic_monitoring_thread
def join(self, timeout=None):
......
......@@ -3,9 +3,6 @@
"""
Make the standard library cooperative.
Patching
========
The primary purpose of this module is to carefully patch, in place,
portions of the standard library with gevent-friendly functions that
behave in the same way as the original (at least as closely as possible).
......@@ -32,8 +29,13 @@ thread** and **should be done while the program is single-threaded**.
Some frameworks, such as gunicorn, handle monkey-patching for you.
Check their documentation to be sure.
.. warning::
Patching too late can lead to unreliable behaviour (for example, some
modules may still use blocking sockets) or even errors.
Querying
--------
========
Sometimes it is helpful to know if objects have been monkey-patched, and in
advanced cases even to have access to the original standard library functions. This
......@@ -43,6 +45,64 @@ module provides functions for that purpose.
- :func:`is_object_patched`
- :func:`get_original`
Plugins
=======
Beginning in gevent 1.3, events are emitted during the monkey patching process.
These events are delivered first to :mod:`gevent.events` subscribers, and then
to `setuptools entry points`_.
The following events are defined. They are listed in (roughly) the order
that a call to :func:`patch_all` will emit them.
- :class:`gevent.events.GeventWillPatchAllEvent`
- :class:`gevent.events.GeventWillPatchModuleEvent`
- :class:`gevent.events.GeventDidPatchModuleEvent`
- :class:`gevent.events.GeventDidPatchBuiltinModulesEvent`
- :class:`gevent.events.GeventDidPatchAllEvent`
Each event class documents the corresponding setuptools entry point name. The
entry points will be called with a single argument, the same instance of
the class that was sent to the subscribers.
You can subscribe to the events to monitor the monkey-patching process and
to manipulate it, for example by raising :exc:`gevent.events.DoNotPatch`.
You can also subscribe to the events to provide additional patching beyond what
gevent distributes, either for additional standard library modules, or
for third-party packages. The suggested time to do this patching is in
the subscriber for :class:`gevent.events.GeventDidPatchBuiltinModulesEvent`.
For example, to automatically patch `psycopg2`_ using `psycogreen`_
when the call to :func:`patch_all` is made, you could write code like this::
# mypackage.py
def patch_psycopg(event):
from psycogreen.gevent import patch_psycopg
patch_psycopg()
In your ``setup.py`` you would register it like this::
from setuptools import setup
setup(
...
entry_points={
'gevent.plugins.monkey.did_patch_builtins': [
'psycopg2 = mypackage:patch_psycopg',
],
},
...
)
For more complex patching, gevent provides a helper method
that you can call to replace attributes of modules with attributes of your
own modules. This function also takes care of emitting the appropriate events.
- :func:`patch_module`
.. _setuptools entry points: http://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins
.. _psycopg2: https://pypi.python.org/pypi/psycopg2
.. _psycogreen: https://pypi.python.org/pypi/psycogreen
Use as a module
===============
......@@ -51,8 +111,8 @@ were not built to be gevent aware under gevent. To do so, this module
can be run as the main module, passing the script and its arguments.
For details, see the :func:`main` function.
Functions
=========
.. versionchanged:: 1.3b1
Added support for plugins and began emitting will/did patch events.
"""
from __future__ import absolute_import
from __future__ import print_function
......@@ -75,6 +135,8 @@ __all__ = [
'get_original',
'is_module_patched',
'is_object_patched',
# plugin API
'patch_module',
# module functions
'main',
]
......@@ -97,6 +159,27 @@ class MonkeyPatchWarning(RuntimeWarning):
.. versionadded:: 1.3a2
"""
def _notify_patch(event, _warnings=None):
# Raises DoNotPatch if we're not supposed to patch
from gevent.events import notify_and_call_entry_points
event._warnings = _warnings
notify_and_call_entry_points(event)
def _ignores_DoNotPatch(func):
from functools import wraps
@wraps(func)
def ignores(*args, **kwargs):
from gevent.events import DoNotPatch
try:
return func(*args, **kwargs)
except DoNotPatch:
return False
return ignores
# maps module name -> {attribute name: original item}
# e.g. "time" -> {"sleep": built-in function sleep}
......@@ -180,35 +263,94 @@ def remove_item(module, attr):
delattr(module, attr)
def __call_module_hook(gevent_module, name, module, items, warn):
def __call_module_hook(gevent_module, name, module, items, _warnings):
# This function can raise DoNotPatch on 'will'
def warn(message):
_queue_warning(message, _warnings)
func_name = '_gevent_' + name + '_monkey_patch'
try:
func = getattr(gevent_module, func_name)
except AttributeError:
pass
else:
func(module, items, warn)
func = lambda *args: None
def patch_module(name, items=None, _warnings=None):
def warn(message):
_queue_warning(message, _warnings)
gevent_module = getattr(__import__('gevent.' + name), name)
module_name = getattr(gevent_module, '__target__', name)
module = __import__(module_name)
func(module, items, warn)
def patch_module(target_module, source_module, items=None,
_warnings=None,
_notify_did_subscribers=True):
"""
patch_module(target_module, source_module, items=None)
Replace attributes in *target_module* with the attributes of the
same name in *source_module*.
The *source_module* can provide some attributes to customize the process:
* ``__implements__`` is a list of attribute names to copy; if not present,
the *items* keyword argument is mandatory.
* ``_gevent_will_monkey_patch(target_module, items, warn, **kwargs)``
* ``_gevent_did_monkey_patch(target_module, items, warn, **kwargs)``
These two functions in the *source_module* are called *if* they exist,
before and after copying attributes, respectively. The "will" function
may modify *items*. The value of *warn* is a function that should be called
with a single string argument to issue a warning to the user. If the "will"
function raises :exc:`gevent.events.DoNotPatch`, no patching will be done. These functions
are called before any event subscribers or plugins.
:keyword list items: A list of attribute names to replace. If
not given, this will be taken from the *source_module* ``__implements__``
attribute.
:return: A true value if patching was done, a false value if patching was canceled.
.. versionadded:: 1.3b1
"""
from gevent import events
if items is None:
items = getattr(gevent_module, '__implements__', None)
items = getattr(source_module, '__implements__', None)
if items is None:
raise AttributeError('%r does not have __implements__' % gevent_module)
raise AttributeError('%r does not have __implements__' % source_module)
__call_module_hook(gevent_module, 'will', module, items, warn)
try:
__call_module_hook(source_module, 'will', target_module, items, _warnings)
_notify_patch(
events.GeventWillPatchModuleEvent(target_module.__name__, source_module,
target_module, items),
_warnings)
except events.DoNotPatch:
return False
for attr in items:
patch_item(module, attr, getattr(gevent_module, attr))
patch_item(target_module, attr, getattr(source_module, attr))
__call_module_hook(source_module, 'did', target_module, items, _warnings)
if _notify_did_subscribers:
# We allow turning off the broadcast of the 'did' event for the benefit
# of our internal functions which need to do additional work (besides copying
# attributes) before their patch can be considered complete.
_notify_patch(
events.GeventDidPatchModuleEvent(target_module.__name__, source_module,
target_module)
)
return True
def _patch_module(name, items=None, _warnings=None, _notify_did_subscribers=True):
gevent_module = getattr(__import__('gevent.' + name), name)
module_name = getattr(gevent_module, '__target__', name)
target_module = __import__(module_name)
__call_module_hook(gevent_module, 'did', module, items, warn)
patch_module(target_module, gevent_module, items=items,
_warnings=_warnings,
_notify_did_subscribers=_notify_did_subscribers)
return module
return gevent_module, target_module
def _queue_warning(message, _warnings):
......@@ -233,7 +375,7 @@ def _patch_sys_std(name):
if not isinstance(orig, FileObjectThread):
patch_item(sys, name, FileObjectThread(orig))
@_ignores_DoNotPatch
def patch_sys(stdin=True, stdout=True, stderr=True):
"""
Patch sys.std[in,out,err] to use a cooperative IO via a
......@@ -254,16 +396,27 @@ def patch_sys(stdin=True, stdout=True, stderr=True):
# test__issue6.py demonstrates the hang if these lines are removed;
# strangely enough that test passes even without monkey-patching sys
if PY3:
items = None
else:
items = set([('stdin' if stdin else None),
('stdout' if stdout else None),
('stderr' if stderr else None)])
items.discard(None)
items = list(items)
if not items:
return
if stdin:
_patch_sys_std('stdin')
if stdout:
_patch_sys_std('stdout')
if stderr:
_patch_sys_std('stderr')
from gevent import events
_notify_patch(events.GeventWillPatchModuleEvent('sys', None, sys,
items))
for item in items:
_patch_sys_std(item)
_notify_patch(events.GeventDidPatchModuleEvent('sys', None, sys))
@_ignores_DoNotPatch
def patch_os():
"""
Replace :func:`os.fork` with :func:`gevent.fork`, and, on POSIX,
......@@ -278,14 +431,15 @@ def patch_os():
.. caution:: For `SIGCHLD` handling to work correctly, the event loop must run.
The easiest way to help ensure this is to use :func:`patch_all`.
"""
patch_module('os')
_patch_module('os')
@_ignores_DoNotPatch
def patch_time():
"""
Replace :func:`time.sleep` with :func:`gevent.sleep`.
"""
patch_module('time')
_patch_module('time')
def _patch_existing_locks(threading):
......@@ -325,7 +479,7 @@ def _patch_existing_locks(threading):
if o.owner is not None:
o.owner = tid
@_ignores_DoNotPatch
def patch_thread(threading=True, _threading_local=True, Event=True, logging=True,
existing_locks=True,
_warnings=None):
......@@ -395,12 +549,15 @@ def patch_thread(threading=True, _threading_local=True, Event=True, logging=True
orig_current_thread = threading_mod.current_thread()
else:
threading_mod = None
gevent_threading_mod = None
orig_current_thread = None
patch_module('thread', _warnings=_warnings)
gevent_thread_mod, thread_mod = _patch_module('thread',
_warnings=_warnings, _notify_did_subscribers=False)
if threading:
patch_module('threading', _warnings=_warnings)
gevent_threading_mod, _ = _patch_module('threading',
_warnings=_warnings, _notify_did_subscribers=False)
if Event:
from gevent.event import Event
......@@ -490,7 +647,11 @@ def patch_thread(threading=True, _threading_local=True, Event=True, logging=True
"threading.main_thread().join() will hang from a greenlet",
_warnings)
from gevent import events
_notify_patch(events.GeventDidPatchModuleEvent('thread', gevent_thread_mod, thread_mod))
_notify_patch(events.GeventDidPatchModuleEvent('threading', gevent_threading_mod, threading_mod))
@_ignores_DoNotPatch
def patch_socket(dns=True, aggressive=True):
"""
Replace the standard socket object with gevent's cooperative
......@@ -509,12 +670,12 @@ def patch_socket(dns=True, aggressive=True):
items = socket.__implements__ # pylint:disable=no-member
else:
items = set(socket.__implements__) - set(socket.__dns__) # pylint:disable=no-member
patch_module('socket', items=items)
_patch_module('socket', items=items)
if aggressive:
if 'ssl' not in socket.__implements__: # pylint:disable=no-member
remove_item(socket, 'ssl')
@_ignores_DoNotPatch
def patch_dns():
"""
Replace :doc:`DNS functions <dns>` in :mod:`socket` with
......@@ -524,9 +685,9 @@ def patch_dns():
done automatically by that method if requested.
"""
from gevent import socket
patch_module('socket', items=socket.__dns__) # pylint:disable=no-member
_patch_module('socket', items=socket.__dns__) # pylint:disable=no-member
@_ignores_DoNotPatch
def patch_ssl(_warnings=None, _first_time=True):
"""
patch_ssl() -> None
......@@ -546,9 +707,9 @@ def patch_ssl(_warnings=None, _first_time=True):
'Please monkey-patch earlier. '
'See https://github.com/gevent/gevent/issues/1016',
_warnings)
patch_module('ssl', _warnings=_warnings)
_patch_module('ssl', _warnings=_warnings)
@_ignores_DoNotPatch
def patch_select(aggressive=True):
"""
Replace :func:`select.select` with :func:`gevent.select.select`
......@@ -567,9 +728,9 @@ def patch_select(aggressive=True):
- :class:`selectors.DevpollSelector` (Python 3.5+)
"""
patch_module('select')
source_mod, target_mod = _patch_module('select', _notify_did_subscribers=False)
if aggressive:
select = __import__('select')
select = target_mod
# since these are blocking we're removing them here. This makes some other
# modules (e.g. asyncore) non-blocking, as they use select that we provide
# when none of these are available.
......@@ -587,7 +748,7 @@ def patch_select(aggressive=True):
# 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)
select = __import__('select') # Should be gevent-patched now
select = target_mod # Should be gevent-patched now
orig_select_select = get_original('select', 'select')
assert select.select is not orig_select_select
selectors = __import__('selectors')
......@@ -614,7 +775,10 @@ def patch_select(aggressive=True):
remove_item(selectors, 'DevpollSelector')
selectors.DefaultSelector = selectors.SelectSelector
from gevent import events
_notify_patch(events.GeventDidPatchModuleEvent('select', source_mod, target_mod))
@_ignores_DoNotPatch
def patch_subprocess():
"""
Replace :func:`subprocess.call`, :func:`subprocess.check_call`,
......@@ -626,9 +790,9 @@ def patch_subprocess():
the standard library.
"""
patch_module('subprocess')
_patch_module('subprocess')
@_ignores_DoNotPatch
def patch_builtins():
"""
Make the builtin :func:`__import__` function `greenlet safe`_ under Python 2.
......@@ -641,9 +805,9 @@ def patch_builtins():
"""
if sys.version_info[:2] < (3, 3):
patch_module('builtins')
_patch_module('builtins')
@_ignores_DoNotPatch
def patch_signal():
"""
Make the :func:`signal.signal` function work with a :func:`monkey-patched os <patch_os>`.
......@@ -656,12 +820,13 @@ def patch_signal():
.. seealso:: :mod:`gevent.signal`
"""
patch_module("signal")
_patch_module("signal")
def _check_repatching(**module_settings):
_warnings = []
key = '_gevent_saved_patch_all'
del module_settings['kwargs']
if saved.get(key, module_settings) != module_settings:
_queue_warning("Patching more than once will result in the union of all True"
" parameters being patched",
......@@ -669,16 +834,30 @@ def _check_repatching(**module_settings):
first_time = key not in saved
saved[key] = module_settings
return _warnings, first_time
return _warnings, first_time, module_settings
def patch_all(socket=True, dns=True, time=True, select=True, thread=True, os=True, ssl=True, httplib=False,
def _subscribe_signal_os(will_patch_all):
if will_patch_all.will_patch_module('signal') and not will_patch_all.will_patch_module('os'):
warnings = will_patch_all._warnings # Internal
_queue_warning('Patching signal but not os will result in SIGCHLD handlers'
' installed after this not being called and os.waitpid may not'
' function correctly if gevent.subprocess is used. This may raise an'
' error in the future.',
warnings)
def patch_all(socket=True, dns=True, time=True, select=True, thread=True, os=True, ssl=True,
httplib=False, # Deprecated, to be removed.
subprocess=True, sys=False, aggressive=True, Event=True,
builtins=True, signal=True):
builtins=True, signal=True,
**kwargs):
"""
Do all of the default monkey patching (calls every other applicable
function in this module).
:return: A true value if patching all modules wasn't cancelled, a false
value if it was.
.. versionchanged:: 1.1
Issue a :mod:`warning <warnings>` if this function is called multiple times
with different arguments. The second and subsequent calls will only add more
......@@ -689,16 +868,28 @@ def patch_all(socket=True, dns=True, time=True, select=True, thread=True, os=Tru
be an error in the future.
.. versionchanged:: 1.3a2
``Event`` defaults to True.
.. versionchanged:: 1.3b1
Defined the return values.
.. versionchanged:: 1.3b1
Add ``**kwargs`` for the benefit of event subscribers. CAUTION: gevent may add
and interpret additional arguments in the future, so it is suggested to use prefixes
for kwarg values to be interpreted by plugins, for example, `patch_all(mylib_futures=True)`.
"""
# pylint:disable=too-many-locals,too-many-branches
# Check to see if they're changing the patched list
_warnings, first_time = _check_repatching(**locals())
_warnings, first_time, modules_to_patch = _check_repatching(**locals())
if not _warnings and not first_time:
# Nothing to do, identical args to what we just
# did
return
from gevent import events
try:
_notify_patch(events.GeventWillPatchAllEvent(modules_to_patch, kwargs), _warnings)
except events.DoNotPatch:
return False
# order is important
if os:
patch_os()
......@@ -723,15 +914,13 @@ def patch_all(socket=True, dns=True, time=True, select=True, thread=True, os=Tru
if builtins:
patch_builtins()
if signal:
if not os:
_queue_warning('Patching signal but not os will result in SIGCHLD handlers'
' installed after this not being called and os.waitpid may not'
' function correctly if gevent.subprocess is used. This may raise an'
' error in the future.',
_warnings)
patch_signal()
_notify_patch(events.GeventDidPatchBuiltinModulesEvent(modules_to_patch, kwargs), _warnings)
_notify_patch(events.GeventDidPatchAllEvent(modules_to_patch, kwargs), _warnings)
_process_warnings(_warnings)
return True
def main():
......@@ -789,7 +978,7 @@ def _get_script_help():
modules = [x for x in patch_all_args if 'patch_' + x in globals()]
script_help = """gevent.monkey - monkey patch the standard modules to use gevent.
USAGE: python -m gevent.monkey [MONKEY OPTIONS] script [SCRIPT OPTIONS]
USAGE: ``python -m gevent.monkey [MONKEY OPTIONS] script [SCRIPT OPTIONS]``
If no OPTIONS present, monkey patches all the modules it can patch.
You can exclude a module with --no-module, e.g. --no-thread. You can
......@@ -802,7 +991,7 @@ case only the modules specified on the command line will be patched.
Previously it had to be the path to
a .py source file.
MONKEY OPTIONS: --verbose %s""" % ', '.join('--[no-]%s' % m for m in modules)
MONKEY OPTIONS: ``--verbose %s``""" % ', '.join('--[no-]%s' % m for m in modules)
return script_help, patch_all_args, modules
main.__doc__ = _get_script_help()[0]
......
......@@ -120,7 +120,22 @@ class TestCaseMetaClass(type):
def _noop():
return
class TestCase(TestCaseMetaClass("NewBase", (TimeAssertMixin, BaseTestCase,), {})):
class SubscriberCleanupMixin(object):
def setUp(self):
super(SubscriberCleanupMixin, self).setUp()
from gevent import events
self.__old_subscribers = events.subscribers[:]
def tearDown(self):
from gevent import events
events.subscribers[:] = self.__old_subscribers
super(SubscriberCleanupMixin, self).tearDown()
class TestCase(TestCaseMetaClass("NewBase",
(SubscriberCleanupMixin, TimeAssertMixin, BaseTestCase,),
{})):
__timeout__ = params.LOCAL_TIMEOUT if not sysinfo.RUNNING_ON_CI else params.CI_TIMEOUT
switch_expected = 'default'
......@@ -137,8 +152,6 @@ class TestCase(TestCaseMetaClass("NewBase", (TimeAssertMixin, BaseTestCase,), {}
def setUp(self):
super(TestCase, self).setUp()
from gevent import events
self.__old_subscribers = events.subscribers[:]
# Especially if we're running in leakcheck mode, where
# the same test gets executed repeatedly, we need to update the
# current time. Tests don't always go through the full event loop,
......@@ -151,8 +164,6 @@ class TestCase(TestCaseMetaClass("NewBase", (TimeAssertMixin, BaseTestCase,), {}
def tearDown(self):
if getattr(self, 'skipTearDown', False):
return
from gevent import events
events.subscribers[:] = self.__old_subscribers
cleanup = getattr(self, 'cleanup', _noop)
cleanup()
......
......@@ -13,6 +13,7 @@ class Test(TestCase):
repeat = 0
def setUp(self):
super(Test, self).setUp()
self.called = []
self.loop = config.loop(default=False)
self.timer = self.loop.timer(0.001, repeat=self.repeat)
......
......@@ -10,6 +10,7 @@ class MyException(Exception):
class TestSwitch(greentest.TestCase):
def setUp(self):
super(TestSwitch, self).setUp()
self.switched_to = [False, False]
self.caught = None
......
......@@ -14,7 +14,7 @@ if not sys.argv[1:]:
# If warnings are enabled, Python 3 has started producing this:
# '...importlib/_bootstrap.py:219: ImportWarning: can't resolve package from __spec__
# or __package__, falling back on __name__ and __path__\n return f(*args, **kwds)\n'
assert err == b'' or b'sys.excepthook' in err or b'ImportWarning' in err, (out, err, code)
assert err == b'' or b'sys.excepthook' in err or b'Warning' in err, (out, err, code)
elif sys.argv[1:] == ['subprocess']:
import gevent
......
......@@ -5,8 +5,9 @@ monkey.patch_all()
import sys
import unittest
from greentest.testcase import SubscriberCleanupMixin
class TestMonkey(unittest.TestCase):
class TestMonkey(SubscriberCleanupMixin, unittest.TestCase):
maxDiff = None
......@@ -48,9 +49,9 @@ class TestMonkey(unittest.TestCase):
for name in ('fork', 'forkpty'):
if hasattr(os, name):
attr = getattr(os, name)
assert 'built-in' not in repr(attr), repr(attr)
assert not isinstance(attr, types.BuiltinFunctionType), repr(attr)
assert isinstance(attr, types.FunctionType), repr(attr)
self.assertNotIn('built-in', repr(attr))
self.assertNotIsInstance(attr, types.BuiltinFunctionType)
self.assertIsInstance(attr, types.FunctionType)
self.assertIs(attr, getattr(gos, name))
def test_saved(self):
......@@ -67,17 +68,28 @@ class TestMonkey(unittest.TestCase):
monkey.patch_subprocess()
self.assertIs(Popen, monkey.get_original('subprocess', 'Popen'))
def test_patch_twice(self):
def test_patch_twice_warnings_events(self):
import warnings
from zope.interface import verify
orig_saved = {}
for k, v in monkey.saved.items():
orig_saved[k] = v.copy()
from gevent import events
all_events = []
events.subscribers.append(all_events.append)
def veto(event):
if isinstance(event, events.GeventWillPatchModuleEvent) and event.module_name == 'ssl':
raise events.DoNotPatch
events.subscribers.append(veto)
with warnings.catch_warnings(record=True) as issued_warnings:
# Patch again, triggering three warnings, one for os=False/signal=True,
# one for repeated monkey-patching, one for patching after ssl (on python >= 2.7.9)
monkey.patch_all(os=False)
monkey.patch_all(os=False, extra_kwarg=42)
self.assertGreaterEqual(len(issued_warnings), 2)
self.assertIn('SIGCHLD', str(issued_warnings[-1].message))
self.assertIn('more than once', str(issued_warnings[0].message))
......@@ -102,6 +114,26 @@ class TestMonkey(unittest.TestCase):
for k, v in monkey.saved['threading'].items():
self.assertNotIn('gevent', str(v))
self.assertIsInstance(all_events[0], events.GeventWillPatchAllEvent)
self.assertEqual({'extra_kwarg': 42}, all_events[0].patch_all_kwargs)
verify.verifyObject(events.IGeventWillPatchAllEvent, all_events[0])
self.assertIsInstance(all_events[1], events.GeventWillPatchModuleEvent)
verify.verifyObject(events.IGeventWillPatchModuleEvent, all_events[1])
self.assertIsInstance(all_events[2], events.GeventDidPatchModuleEvent)
verify.verifyObject(events.IGeventWillPatchModuleEvent, all_events[1])
self.assertIsInstance(all_events[-2], events.GeventDidPatchBuiltinModulesEvent)
verify.verifyObject(events.IGeventDidPatchBuiltinModulesEvent, all_events[-2])
self.assertIsInstance(all_events[-1], events.GeventDidPatchAllEvent)
verify.verifyObject(events.IGeventDidPatchAllEvent, all_events[-1])
for e in all_events:
self.assertFalse(isinstance(e, events.GeventDidPatchModuleEvent)
and e.module_name == 'ssl')
if __name__ == '__main__':
unittest.main()
import os
import os.path
import sys
import unittest
......@@ -6,10 +8,13 @@ from subprocess import Popen
from subprocess import PIPE
class TestRun(unittest.TestCase):
maxDiff = None
def _run(self, script):
env = os.environ.copy()
env['PYTHONWARNINGS'] = 'ignore'
args = [sys.executable, '-m', 'gevent.monkey', script, 'patched']
p = Popen(args, stdout=PIPE, stderr=PIPE)
p = Popen(args, stdout=PIPE, stderr=PIPE, env=env)
gout, gerr = p.communicate()
self.assertEqual(0, p.returncode, (gout, gerr))
......@@ -27,7 +32,6 @@ class TestRun(unittest.TestCase):
return glines, gerr
def test_run_simple(self):
import os.path
self._run(os.path.join('monkey_package', 'script.py'))
def test_run_package(self):
......@@ -38,7 +42,6 @@ class TestRun(unittest.TestCase):
self.assertEqual(lines[1], '__main__')
def test_issue_302(self):
import os
lines, _ = self._run(os.path.join('monkey_package', 'issue302monkey.py'))
self.assertEqual(lines[0], 'True')
......
......@@ -16,6 +16,12 @@ def handle(*_args):
os.waitpid(-1, os.WNOHANG)
# The signal watcher must be installed *before* monkey patching
if hasattr(signal, 'SIGCHLD'):
# On Python 2, the signal handler breaks the platform
# module, because it uses os.popen. pkg_resources uses the platform
# module.
# Cache that info.
import platform
platform.uname()
signal.signal(signal.SIGCHLD, handle)
pid = os.fork()
......@@ -28,15 +34,15 @@ if hasattr(signal, 'SIGCHLD'):
_, stat = os.waitpid(pid, 0)
assert stat == 0, stat
else:
import gevent.monkey
gevent.monkey.patch_all()
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
# Under Python 2, os.popen() directly uses the popen call, and
# popen's file uses the pclose() system call to
# wait for the child. If it's already waited on,
# it raises the same exception.
# Python 3 uses the subprocess module directly which doesn't
# have this problem.
import gevent.monkey
gevent.monkey.patch_all()
signal.signal(signal.SIGCHLD, signal.SIG_DFL)
f = os.popen('true')
f.close()
......
......@@ -33,6 +33,7 @@ python_universal_newlines_broken = PY3 and subprocess.mswindows
class Test(greentest.TestCase):
def setUp(self):
super(Test, self).setUp()
gc.collect()
gc.collect()
......
......@@ -366,7 +366,10 @@ class ThreadTests(unittest.TestCase):
stderr = stderr.decode('utf-8')
assert re.match('^Woke up, sleep function is: <.*?sleep.*?>$', stdout), repr(stdout)
stderr = re.sub(r"^\[\d+ refs\]", "", stderr, re.MULTILINE).strip()
self.assertEqual(stderr, "")
# On Python 2, importing pkg_resources tends to result in some 'ImportWarning'
# being printed to stderr about packages missing __init__.py; the -W ignore is...
# ignored.
# self.assertEqual(stderr, "")
def test_enumerate_after_join(self):
# Try hard to trigger #1703448: a thread is still returned in
......
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