Commit e6e29591 authored by Jason Madden's avatar Jason Madden

Basic support for CPython 3.8.0b4.

Still needs the specific networking test classes added, but all the basics pass for me. Lets see about CI.
parent d95f526c
...@@ -42,6 +42,7 @@ env: ...@@ -42,6 +42,7 @@ env:
- TRAVIS_PYTHON_VERSION=3.5 - TRAVIS_PYTHON_VERSION=3.5
- TRAVIS_PYTHON_VERSION=3.6 - TRAVIS_PYTHON_VERSION=3.6
- TRAVIS_PYTHON_VERSION=3.7 - TRAVIS_PYTHON_VERSION=3.7
- TRAVIS_PYTHON_VERSION=3.8
- TRAVIS_PYTHON_VERSION=pypy2.7 - TRAVIS_PYTHON_VERSION=pypy2.7
- TRAVIS_PYTHON_VERSION=pypy3.6 - TRAVIS_PYTHON_VERSION=pypy3.6
- TRAVIS_PYTHON_VERSION=2.7 GEVENTSETUP_EMBED=0 - TRAVIS_PYTHON_VERSION=2.7 GEVENTSETUP_EMBED=0
...@@ -144,6 +145,8 @@ jobs: ...@@ -144,6 +145,8 @@ jobs:
script: ccache -s script: ccache -s
before_script: true before_script: true
after_success: true after_success: true
- <<: *build-gevent
env: TRAVIS_PYTHON_VERSION=3.8
- <<: *build-gevent - <<: *build-gevent
env: TRAVIS_PYTHON_VERSION=3.5 env: TRAVIS_PYTHON_VERSION=3.5
- <<: *build-gevent - <<: *build-gevent
...@@ -278,6 +281,14 @@ jobs: ...@@ -278,6 +281,14 @@ jobs:
env: TRAVIS_PYTHON_VERSION=3.7 env: TRAVIS_PYTHON_VERSION=3.7
name: pure37 name: pure37
# 3.8
- <<: *test-libuv-jobs
env: TRAVIS_PYTHON_VERSION=3.8
name: libuv36
- <<: *test-libev-jobs
env: TRAVIS_PYTHON_VERSION=3.8
name: libev-cffi36
# 2.7, no-embed. Run the tests that exercise the libraries we # 2.7, no-embed. Run the tests that exercise the libraries we
# linked to. # linked to.
......
...@@ -12,6 +12,10 @@ ...@@ -12,6 +12,10 @@
- Add an ``--module`` option to ``gevent.monkey`` allowing to run a Python - Add an ``--module`` option to ``gevent.monkey`` allowing to run a Python
module rather than a script. See :pr:`1440`. module rather than a script. See :pr:`1440`.
- Add support for CPython 3.8.0b4.
- Improve the way joining the main thread works on Python 3.
1.5a1 (2019-05-02) 1.5a1 (2019-05-02)
================== ==================
......
...@@ -15,7 +15,12 @@ from gevent import monkey; monkey.patch_all() ...@@ -15,7 +15,12 @@ from gevent import monkey; monkey.patch_all()
import sys import sys
import re import re
import traceback import traceback
from cgi import escape
try:
from cgi import escape
except ImportError:
# Python 3.8 removed this API
from html import escape
try: try:
import urllib2 import urllib2
......
...@@ -12,8 +12,9 @@ requires = [ ...@@ -12,8 +12,9 @@ requires = [
# 0.28 is faster, and (important!) lets us specify the target module # 0.28 is faster, and (important!) lets us specify the target module
# name to be created so that we can have both foo.py and _foo.so # name to be created so that we can have both foo.py and _foo.so
# at the same time. 0.29 fixes some issues with Python 3.7, # at the same time. 0.29 fixes some issues with Python 3.7,
# and adds the 3str mode for transition to Python 3. # and adds the 3str mode for transition to Python 3. 0.29.12+ is
"Cython >= 0.29.7", # required for Python 3.8
"Cython >= 0.29.13",
# See version requirements in setup.py # See version requirements in setup.py
"cffi >= 1.12.3 ; platform_python_implementation == 'CPython'", "cffi >= 1.12.3 ; platform_python_implementation == 'CPython'",
# Python 3.7 requires at least 0.4.14, which is ABI incompatible with earlier # Python 3.7 requires at least 0.4.14, which is ABI incompatible with earlier
......
...@@ -99,6 +99,9 @@ for var in "$@"; do ...@@ -99,6 +99,9 @@ for var in "$@"; do
3.7) 3.7)
install 3.7.2 python3.7 3.7.d install 3.7.2 python3.7 3.7.d
;; ;;
3.8)
install 3.8.0b4 python3.8 3.8.d
;;
pypy2.7) pypy2.7)
install pypy2.7-7.1.0 pypy2.7 pypy2.7.d install pypy2.7-7.1.0 pypy2.7 pypy2.7.d
;; ;;
......
...@@ -13,7 +13,7 @@ cdef class Semaphore(AbstractLinkable): ...@@ -13,7 +13,7 @@ cdef class Semaphore(AbstractLinkable):
# threadpool uses it # threadpool uses it
cpdef _start_notify(self) cpdef _start_notify(self)
cpdef int wait(self, object timeout=*) except -1000 cpdef int wait(self, object timeout=*) except -1000
cpdef bint acquire(self, int blocking=*, object timeout=*) except -1000 cpdef bint acquire(self, bint blocking=*, object timeout=*) except -1000
cpdef __enter__(self) cpdef __enter__(self)
cpdef __exit__(self, object t, object v, object tb) cpdef __exit__(self, object t, object v, object tb)
......
...@@ -21,7 +21,7 @@ PY38 = sys.version_info[:2] >= (3, 8) ...@@ -21,7 +21,7 @@ PY38 = sys.version_info[:2] >= (3, 8)
PYPY = hasattr(sys, 'pypy_version_info') PYPY = hasattr(sys, 'pypy_version_info')
WIN = sys.platform.startswith("win") WIN = sys.platform.startswith("win")
LINUX = sys.platform.startswith('linux') LINUX = sys.platform.startswith('linux')
OSX = sys.platform == 'darwin' OSX = MAC = sys.platform == 'darwin'
PURE_PYTHON = PYPY or os.getenv('PURE_PYTHON') PURE_PYTHON = PYPY or os.getenv('PURE_PYTHON')
......
...@@ -6,6 +6,7 @@ from __future__ import absolute_import ...@@ -6,6 +6,7 @@ from __future__ import absolute_import
# Our import magic sadly makes this warning useless # Our import magic sadly makes this warning useless
# pylint: disable=undefined-variable # pylint: disable=undefined-variable
import sys
from gevent import _socketcommon from gevent import _socketcommon
from gevent._util import copy_globals from gevent._util import copy_globals
......
...@@ -69,13 +69,19 @@ __py3_imports__ = [ ...@@ -69,13 +69,19 @@ __py3_imports__ = [
__imports__.extend(__py3_imports__) __imports__.extend(__py3_imports__)
import time import time
import sys
from gevent._hub_local import get_hub_noargs as get_hub from gevent._hub_local import get_hub_noargs as get_hub
from gevent._compat import string_types, integer_types, PY3 from gevent._compat import string_types, integer_types, PY3
from gevent._compat import PY38
from gevent._compat import WIN as is_windows
from gevent._compat import OSX as is_macos
from gevent._util import copy_globals from gevent._util import copy_globals
is_windows = sys.platform == 'win32' if PY38:
is_macos = sys.platform == 'darwin' __imports__.extend([
'create_server',
'has_dualstack_ipv6',
])
# pylint:disable=no-name-in-module,unused-import # pylint:disable=no-name-in-module,unused-import
if is_windows: if is_windows:
......
...@@ -668,6 +668,22 @@ def patch_thread(threading=True, _threading_local=True, Event=True, logging=True ...@@ -668,6 +668,22 @@ def patch_thread(threading=True, _threading_local=True, Event=True, logging=True
raise RuntimeError("Cannot join current thread") raise RuntimeError("Cannot join current thread")
if thread_greenlet is not None and thread_greenlet.dead: if thread_greenlet is not None and thread_greenlet.dead:
return return
# You may ask: Why not call thread_greenlet.join()?
# Well, in the one case we actually have a greenlet, it's the
# low-level greenlet.greenlet object for the main thread, which
# doesn't have a join method.
#
# You may ask: Why not become the main greenlet's *parent*
# so you can get notified when it finishes? Because you can't
# create a greenlet cycle (the current greenlet is a descendent
# of the parent), and nor can you set a greenlet's parent to None,
# so there can only ever be one greenlet with a parent of None: the main
# greenlet, the one we need to watch.
#
# You may ask: why not swizzle out the problematic lock on the main thread
# into a gevent friendly lock? Well, the interpreter actually depends on that
# for the main thread in threading._shutdown; see below.
if not thread.is_alive(): if not thread.is_alive():
return return
...@@ -700,8 +716,34 @@ def patch_thread(threading=True, _threading_local=True, Event=True, logging=True ...@@ -700,8 +716,34 @@ def patch_thread(threading=True, _threading_local=True, Event=True, logging=True
if orig_current_thread == threading_mod.main_thread(): if orig_current_thread == threading_mod.main_thread():
main_thread = threading_mod.main_thread() main_thread = threading_mod.main_thread()
_greenlet = main_thread._greenlet = greenlet.getcurrent() _greenlet = main_thread._greenlet = greenlet.getcurrent()
main_thread.__real_tstate_lock = main_thread._tstate_lock
main_thread.join = make_join_func(main_thread, _greenlet)
# The interpreter will call threading._shutdown
# when the main thread exits and is about to
# go away. It is called *in* the main thread. This
# is a perfect place to notify other greenlets that
# the main thread is done. We do this by overriding the
# lock of the main thread during operation, and only restoring
# it to the native blocking version at shutdown time
# (the interpreter also has a reference to this lock in a
# C data structure).
main_thread._tstate_lock = threading_mod.Lock()
main_thread._tstate_lock.acquire()
orig_shutdown = threading_mod._shutdown
def _shutdown():
# Release anyone trying to join() me,
# and let us switch to them.
if main_thread._tstate_lock:
main_thread._tstate_lock.release()
from gevent import sleep
sleep()
# The only truly blocking native shutdown lock to
# acquire should be our own (hopefully)
main_thread._tstate_lock = main_thread.__real_tstate_lock
main_thread.__real_tstate_lock = None
orig_shutdown()
threading_mod._shutdown = _shutdown
# Patch up the ident of the main thread to match. This # Patch up the ident of the main thread to match. This
# matters if threading was imported before monkey-patching # matters if threading was imported before monkey-patching
......
...@@ -385,6 +385,12 @@ if hasattr(os, 'fork'): ...@@ -385,6 +385,12 @@ if hasattr(os, 'fork'):
# we're not watching it # we're not watching it
return _waitpid(pid, options) return _waitpid(pid, options)
def _watch_child(pid, callback=None, loop=None, ref=False):
loop = loop or get_hub().loop
watcher = loop.child(pid, ref=ref)
_watched_children[pid] = watcher
watcher.start(_on_child, watcher, callback)
def fork_and_watch(callback=None, loop=None, ref=False, fork=fork_gevent): def fork_and_watch(callback=None, loop=None, ref=False, fork=fork_gevent):
""" """
Fork a child process and start a child watcher for it in the parent process. Fork a child process and start a child watcher for it in the parent process.
...@@ -413,10 +419,7 @@ if hasattr(os, 'fork'): ...@@ -413,10 +419,7 @@ if hasattr(os, 'fork'):
pid = fork() pid = fork()
if pid: if pid:
# parent # parent
loop = loop or get_hub().loop _watch_child(pid, callback, loop, ref)
watcher = loop.child(pid, ref=ref)
_watched_children[pid] = watcher
watcher.start(_on_child, watcher, callback)
return pid return pid
__extensions__.append('fork_and_watch') __extensions__.append('fork_and_watch')
...@@ -474,6 +477,23 @@ if hasattr(os, 'fork'): ...@@ -474,6 +477,23 @@ if hasattr(os, 'fork'):
# take any args to match fork_and_watch # take any args to match fork_and_watch
return forkpty_and_watch(*args, **kwargs) return forkpty_and_watch(*args, **kwargs)
__implements__.append("waitpid") __implements__.append("waitpid")
if hasattr(os, 'posix_spawn'):
_raw_posix_spawn = os.posix_spawn
_raw_posix_spawnp = os.posix_spawnp
def posix_spawn(*args, **kwargs):
pid = _raw_posix_spawn(*args, **kwargs)
_watch_child(pid)
return pid
def posix_spawnp(*args, **kwargs):
pid = _raw_posix_spawnp(*args, **kwargs)
_watch_child(pid)
return pid
__implements__.append("posix_spawn")
__implements__.append("posix_spawnp")
else: else:
def fork(): def fork():
""" """
...@@ -503,6 +523,7 @@ if hasattr(os, 'fork'): ...@@ -503,6 +523,7 @@ if hasattr(os, 'fork'):
else: else:
__implements__.remove('fork') __implements__.remove('fork')
__imports__ = copy_globals(os, globals(), __imports__ = copy_globals(os, globals(),
names_to_ignore=__implements__ + __extensions__, names_to_ignore=__implements__ + __extensions__,
dunder_names_to_keep=()) dunder_names_to_keep=())
......
...@@ -47,6 +47,7 @@ from gevent._compat import PY3 ...@@ -47,6 +47,7 @@ from gevent._compat import PY3
from gevent._compat import PY35 from gevent._compat import PY35
from gevent._compat import PY36 from gevent._compat import PY36
from gevent._compat import PY37 from gevent._compat import PY37
from gevent._compat import PY38
from gevent._compat import reraise from gevent._compat import reraise
from gevent._compat import fspath from gevent._compat import fspath
from gevent._compat import fsencode from gevent._compat import fsencode
...@@ -69,6 +70,25 @@ if PY3 and not sys.platform.startswith('win32'): ...@@ -69,6 +70,25 @@ if PY3 and not sys.platform.startswith('win32'):
__implements__.append("_posixsubprocess") __implements__.append("_posixsubprocess")
_posixsubprocess = None _posixsubprocess = None
if PY38:
# Using os.posix_spawn() to start subprocesses
# bypasses our child watchers on certain operating systems,
# and with certain library versions. Possibly the right
# fix is to monkey-patch os.posix_spawn like we do os.fork?
# These have no effect, they're just here to match the stdlib.
# TODO: When available, given a monkey patch on them, I think
# we ought to be able to use them if the stdlib has identified them
# as suitable.
__implements__.extend([
'_use_posix_spawn',
'_USE_POSIX_SPAWN'
])
def _use_posix_spawn():
return False
_USE_POSIX_SPAWN = False
# Some symbols we define that we expect to export; # Some symbols we define that we expect to export;
# useful for static analysis # useful for static analysis
PIPE = "PIPE should be imported" PIPE = "PIPE should be imported"
...@@ -1720,3 +1740,15 @@ def run(*popenargs, **kwargs): ...@@ -1720,3 +1740,15 @@ def run(*popenargs, **kwargs):
raise _with_stdout_stderr(CalledProcessError(retcode, process.args, stdout), stderr) raise _with_stdout_stderr(CalledProcessError(retcode, process.args, stdout), stderr)
return CompletedProcess(process.args, retcode, stdout, stderr) return CompletedProcess(process.args, retcode, stdout, stderr)
def _gevent_did_monkey_patch(*_args):
# Beginning on 3.8 on Mac, the 'spawn' method became the default
# start method. That doesn't fire fork watchers and we can't
# easily patch to make it do so: multiprocessing uses the private
# c accelerated _subprocess module to implement this. Instead we revert
# back to using fork.
from gevent._compat import MAC
if MAC:
import multiprocessing
if hasattr(multiprocessing, 'set_start_method'):
multiprocessing.set_start_method('fork', force=True)
...@@ -480,17 +480,26 @@ def _setup_environ(debug=False): ...@@ -480,17 +480,26 @@ def _setup_environ(debug=False):
if 'GEVENT_DEBUG' not in os.environ and debug: if 'GEVENT_DEBUG' not in os.environ and debug:
os.environ['GEVENT_DEBUG'] = 'debug' os.environ['GEVENT_DEBUG'] = 'debug'
if 'PYTHONTRACEMALLOC' not in os.environ: if 'PYTHONTRACEMALLOC' not in os.environ and debug:
# This slows the tests down quite a bit. Reserve
# for debugging.
os.environ['PYTHONTRACEMALLOC'] = '10' os.environ['PYTHONTRACEMALLOC'] = '10'
if 'PYTHONDEVMODE' not in os.environ: if 'PYTHONDEVMODE' not in os.environ:
# Python 3.7 # Python 3.7 and above.
os.environ['PYTHONDEVMODE'] = '1' os.environ['PYTHONDEVMODE'] = '1'
if 'PYTHONMALLOC' not in os.environ: if 'PYTHONMALLOC' not in os.environ and debug:
# Python 3.6 # Python 3.6 and above.
# This slows the tests down some, but
# can detect memory corruption. Unfortunately
# it can also be flaky, especially in pre-release
# versions of Python (e.g., lots of crashes on Python 3.8b4).
os.environ['PYTHONMALLOC'] = 'debug' os.environ['PYTHONMALLOC'] = 'debug'
if sys.version_info.releaselevel != 'final' and not debug:
os.environ['PYTHONMALLOC'] = 'default'
os.environ['PYTHONDEVMODE'] = ''
def main(): def main():
......
from __future__ import print_function from __future__ import print_function
import gevent.monkey import gevent.monkey
gevent.monkey.patch_all() gevent.monkey.patch_all()
import gevent
import os
import os
import multiprocessing import multiprocessing
import gevent
from gevent._compat import MAC
hub = gevent.get_hub() hub = gevent.get_hub()
pid = os.getpid() pid = os.getpid()
newpid = None newpid = None
...@@ -46,6 +48,7 @@ def test(): ...@@ -46,6 +48,7 @@ def test():
if __name__ == '__main__': if __name__ == '__main__':
# Must call for Windows to fork properly; the fork can't be in the top-level # Must call for Windows to fork properly; the fork can't be in the top-level
multiprocessing.freeze_support() multiprocessing.freeze_support()
# fork watchers weren't firing in multi-threading processes. # fork watchers weren't firing in multi-threading processes.
# This test is designed to prove that they are. # This test is designed to prove that they are.
# However, it fails on Windows: The fork watcher never runs! # However, it fails on Windows: The fork watcher never runs!
......
...@@ -33,6 +33,7 @@ try: ...@@ -33,6 +33,7 @@ try:
from test.support import verbose from test.support import verbose
except ImportError: except ImportError:
from test.test_support import verbose from test.test_support import verbose
import random import random
import re import re
import sys import sys
...@@ -46,7 +47,7 @@ import unittest ...@@ -46,7 +47,7 @@ import unittest
import weakref import weakref
from gevent.tests import lock_tests from gevent.tests import lock_tests
verbose = False
# A trivial mutable counter. # A trivial mutable counter.
def skipDueToHang(cls): def skipDueToHang(cls):
...@@ -132,7 +133,7 @@ class ThreadTests(unittest.TestCase): ...@@ -132,7 +133,7 @@ class ThreadTests(unittest.TestCase):
print('waiting for all tasks to complete') print('waiting for all tasks to complete')
for t in threads: for t in threads:
t.join(NUMTASKS) t.join(NUMTASKS)
self.assertFalse(t.is_alive()) self.assertFalse(t.is_alive(), t.__dict__)
if hasattr(t, 'ident'): if hasattr(t, 'ident'):
self.assertNotEqual(t.ident, 0) self.assertNotEqual(t.ident, 0)
self.assertFalse(t.ident is None) self.assertFalse(t.ident is None)
...@@ -351,28 +352,33 @@ class ThreadTests(unittest.TestCase): ...@@ -351,28 +352,33 @@ class ThreadTests(unittest.TestCase):
# Issue 1722344 # Issue 1722344
# Raising SystemExit skipped threading._shutdown # Raising SystemExit skipped threading._shutdown
import subprocess import subprocess
p = subprocess.Popen([sys.executable, "-W", "ignore", "-c", """if 1: script = """if 1:
%s %s
import threading import threading
from time import sleep from time import sleep
def child(): def child():
sleep(1) sleep(0.3)
# As a non-daemon thread we SHOULD wake up and nothing # As a non-daemon thread we SHOULD wake up and nothing
# should be torn down yet # should be torn down yet
print("Woke up, sleep function is: %%r" %% sleep) print("Woke up, sleep function is: %%s.%%s" %% (sleep.__module__, sleep.__name__))
threading.Thread(target=child).start() threading.Thread(target=child).start()
raise SystemExit raise SystemExit
""" % setup_4], """ % setup_4
p = subprocess.Popen([sys.executable, "-W", "ignore", "-c", script],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE) stderr=subprocess.PIPE)
stdout, stderr = p.communicate() stdout, stderr = p.communicate()
stdout = stdout.strip() stdout = stdout.strip()
stdout = stdout.decode('utf-8') stdout = stdout.decode('utf-8')
stderr = stderr.decode('utf-8') 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(
'Woke up, sleep function is: gevent.hub.sleep',
stdout)
# On Python 2, importing pkg_resources tends to result in some 'ImportWarning' # 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... # being printed to stderr about packages missing __init__.py; the -W ignore is...
# ignored. # ignored.
...@@ -410,7 +416,7 @@ class ThreadTests(unittest.TestCase): ...@@ -410,7 +416,7 @@ class ThreadTests(unittest.TestCase):
self.should_raise = should_raise self.should_raise = should_raise
self.thread = threading.Thread(target=self._run, self.thread = threading.Thread(target=self._run,
args=(self,), args=(self,),
kwargs={'yet_another': self}) kwargs={'_yet_another': self})
self.thread.start() self.thread.start()
def _run(self, _other_ref, _yet_another): def _run(self, _other_ref, _yet_another):
...@@ -463,7 +469,7 @@ class ThreadJoinOnShutdown(unittest.TestCase): ...@@ -463,7 +469,7 @@ class ThreadJoinOnShutdown(unittest.TestCase):
t = threading.Thread(target=joiningfunc, t = threading.Thread(target=joiningfunc,
args=(threading.current_thread(),)) args=(threading.current_thread(),))
t.start() t.start()
time.sleep(0.1) time.sleep(0.2)
print('end of main') print('end of main')
""" """
self._run_and_join(script) self._run_and_join(script)
......
...@@ -41,6 +41,12 @@ else: ...@@ -41,6 +41,12 @@ else:
'interrupt_main', 'interrupt_main',
'start_new' 'start_new'
] ]
if sys.version_info[:2] >= (3, 8):
# We can't actually produce a value that "may be used
# to identify this particular thread system-wide", right?
# Even if we could, I imagine people will want to pass this to
# non-Python (native) APIs, so we shouldn't mess with it.
__imports__.append('get_native_id')
error = __thread__.error error = __thread__.error
......
...@@ -160,41 +160,25 @@ else: ...@@ -160,41 +160,25 @@ else:
if PY3: if PY3:
# XXX: Issue 18808 breaks us on Python 3.4+. # XXX: Issue 18808 breaks us on Python 3.4+.
# Thread objects now expect a callback from the interpreter itself # Thread objects now expect a callback from the interpreter itself
# (threadmodule.c:release_sentinel). Because this never happens # (threadmodule.c:release_sentinel) when the C-level PyThreadState
# object is being deallocated. Because this never happens
# when a greenlet exits, join() and friends will block forever. # when a greenlet exits, join() and friends will block forever.
# The solution below involves capturing the greenlet when it is # Fortunately this is easy to fix: just ensure that the allocation of the
# started and deferring the known broken methods to it. # lock, _set_sentinel, creates a *gevent* lock, and release it when
# we're done. The main _shutdown code is in Python and deals with
# this gracefully.
class Thread(__threading__.Thread): class Thread(__threading__.Thread):
_greenlet = None
def is_alive(self):
return bool(self._greenlet)
isAlive = is_alive
def _set_tstate_lock(self): def _set_tstate_lock(self):
self._greenlet = getcurrent() super(Thread, self)._set_tstate_lock()
greenlet = getcurrent()
def run(self): greenlet.rawlink(self.__greenlet_finished)
try:
super(Thread, self).run() def __greenlet_finished(self, _):
finally: if self._tstate_lock:
# avoid ref cycles, but keep in __dict__ so we can self._tstate_lock.release()
# distinguish the started/never-started case self._stop()
self._greenlet = None
self._stop() # mark as finished
def join(self, timeout=None):
if '_greenlet' not in self.__dict__:
raise RuntimeError("Cannot join an inactive thread")
if self._greenlet is None:
return
self._greenlet.join(timeout=timeout)
def _wait_for_tstate_lock(self, *args, **kwargs):
# pylint:disable=arguments-differ
raise NotImplementedError()
__implements__.append('Thread') __implements__.append('Thread')
...@@ -203,6 +187,8 @@ if PY3: ...@@ -203,6 +187,8 @@ if PY3:
__implements__.append('Timer') __implements__.append('Timer')
_set_sentinel = allocate_lock
__implements__.append('_set_sentinel')
# The main thread is patched up with more care # The main thread is patched up with more care
# in _gevent_will_monkey_patch # in _gevent_will_monkey_patch
......
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