Skip to content

Commit

Permalink
Merge pull request #216 from python-greenlet/issue215
Browse files Browse the repository at this point in the history
Add extra information to the repr of greenlets.
  • Loading branch information
jamadden committed Nov 20, 2020
2 parents 6986123 + 79c1edd commit cf89224
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 21 deletions.
3 changes: 3 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
alter that at compile time have been removed (no combination other
than the defaults was ever tested). This helps define a
stable ABI.
- The repr of greenlet objects now includes extra information about
its state. This is purely informative and the details are subject to
change. See `issue 215 <https://github.com/python-greenlet/greenlet/issues/215>`_.


0.4.17 (2020-09-22)
Expand Down
55 changes: 53 additions & 2 deletions src/greenlet/greenlet.c
Original file line number Diff line number Diff line change
Expand Up @@ -1301,10 +1301,17 @@ green_setdict(PyGreenlet* self, PyObject* val, void* c)
return 0;
}

static int
_green_not_dead(PyGreenlet* self)
{
return PyGreenlet_ACTIVE(self) || !PyGreenlet_STARTED(self);
}


static PyObject*
green_getdead(PyGreenlet* self, void* c)
{
if (PyGreenlet_ACTIVE(self) || !PyGreenlet_STARTED(self)) {
if (_green_not_dead(self)) {
Py_RETURN_FALSE;
}
else {
Expand Down Expand Up @@ -1512,6 +1519,50 @@ green_getstate(PyGreenlet* self)
return NULL;
}

static PyObject*
green_repr(PyGreenlet* self)
{
/*
Return a string like
<greenlet.greenlet at 0xdeadbeef [current][active started]|dead main>
The handling of greenlets across threads is not super good.
We mostly use the internal definitions of these terms, but they
generally should make sense to users as well.
*/
PyObject* result;
int never_started = !PyGreenlet_STARTED(self) && !PyGreenlet_ACTIVE(self);

if (!STATE_OK) {
return NULL;
}

if (_green_not_dead(self)) {
result = PyUnicode_FromFormat(
"<%s object at %p (otid=%p)%s%s%s%s>",
Py_TYPE(self)->tp_name,
self,
self->run_info,
ts_current == self
? " current"
: (PyGreenlet_STARTED(self) ? " suspended" : ""),
PyGreenlet_ACTIVE(self) ? " active" : "",
never_started ? " pending" : " started",
PyGreenlet_MAIN(self) ? " main" : ""
);
}
else {
/* main greenlets never really appear dead. */
result = PyUnicode_FromFormat(
"<%s object at %p (otid=%p) dead>",
Py_TYPE(self)->tp_name,
self,
self->run_info
);
}
return result;
}

/*****************************************************************************
* C interface
*
Expand Down Expand Up @@ -1662,7 +1713,7 @@ PyTypeObject PyGreenlet_Type = {
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_compare */
0, /* tp_repr */
(reprfunc)green_repr, /* tp_repr */
&green_as_number, /* tp_as _number*/
0, /* tp_as _sequence*/
0, /* tp_as _mapping*/
Expand Down
91 changes: 72 additions & 19 deletions src/greenlet/tests/test_greenlet.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,10 @@
import time
import threading
import unittest
from abc import ABCMeta, abstractmethod

from greenlet import greenlet

try:
from abc import ABCMeta, abstractmethod
except ImportError:
ABCMeta = None
abstractmethod = None


class SomeError(Exception):
pass
Expand Down Expand Up @@ -469,22 +464,22 @@ def switchapply():
g = greenlet(switchapply)
self.assertEqual(g.switch(), kwargs)

if ABCMeta is not None and abstractmethod is not None:
def test_abstract_subclasses(self):
AbstractSubclass = ABCMeta(
'AbstractSubclass',
(greenlet,),
{'run': abstractmethod(lambda self: None)})

class BadSubclass(AbstractSubclass):
pass
def test_abstract_subclasses(self):
AbstractSubclass = ABCMeta(
'AbstractSubclass',
(greenlet,),
{'run': abstractmethod(lambda self: None)})

class GoodSubclass(AbstractSubclass):
def run(self):
pass
class BadSubclass(AbstractSubclass):
pass

GoodSubclass() # should not raise
self.assertRaises(TypeError, BadSubclass)
class GoodSubclass(AbstractSubclass):
def run(self):
pass

GoodSubclass() # should not raise
self.assertRaises(TypeError, BadSubclass)

def test_implicit_parent_with_threads(self):
if not gc.isenabled():
Expand Down Expand Up @@ -540,3 +535,61 @@ def __init__(self):
for i in range(5):
if attempt():
break

class TestRepr(unittest.TestCase):

def assertEndsWith(self, got, suffix):
self.assertTrue(got.endswith(suffix), (got, suffix))

def test_main_while_running(self):
r = repr(greenlet.getcurrent())
self.assertEndsWith(r, " current active started main>")

def test_main_in_background(self):
main = greenlet.getcurrent()
def run():
return repr(main)

g = greenlet(run)
r = g.switch()
self.assertEndsWith(r, ' suspended active started main>')

def test_initial(self):
r = repr(greenlet())
self.assertEndsWith(r, ' pending>')

def test_main_from_other_thread(self):
main = greenlet.getcurrent()

class T(threading.Thread):
original_main = thread_main = None
main_glet = None
def run(self):
self.original_main = repr(main)
self.main_glet = greenlet.getcurrent()
self.thread_main = repr(self.main_glet)

t = T()
t.start()
t.join(10)

self.assertEndsWith(t.original_main, ' suspended active started main>')
self.assertEndsWith(t.thread_main, ' current active started main>')

r = repr(t.main_glet)
# main greenlets, even from dead threads, never really appear dead
# TODO: Can we find a better way to differentiate that?
assert not t.main_glet.dead
self.assertEndsWith(r, ' suspended active started main>')


def test_dead(self):
g = greenlet(lambda: None)
g.switch()
self.assertEndsWith(repr(g), ' dead>')
self.assertNotIn('suspended', repr(g))
self.assertNotIn('started', repr(g))
self.assertNotIn('active', repr(g))

if __name__ == '__main__':
unittest.main()

0 comments on commit cf89224

Please sign in to comment.