summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexey Stepanov <penguinolog@users.noreply.github.com>2023-04-21 15:12:51 +0200
committerGitHub <noreply@github.com>2023-04-21 15:12:51 +0200
commit350ee5c47ff565d3b0d25b1c3cbddbfb2a0a2a4f (patch)
tree78625ff9d88095e21fb2e972bfbdb767c6e7fc11
parenta53fd346d22e67d5e42c29a4d5c3498d598443c9 (diff)
downloadurwid-350ee5c47ff565d3b0d25b1c3cbddbfb2a0a2a4f.tar.gz
[BREAKING CHANGE] Fixes: #90 Remove idle emulation from asyncio event loop (#541)
* [BREAKING CHANGE] Fix: #90 Remove idle emulation from asyncio event loop Re-implement abandoned PR #418 * Fix "not hashable `AttrSpec`" and it's instance creation price (use `__slots__`) `AttrSpec` instances may be created in huge amount, with slots this process consume less resources. * `Terminal` is always created with event loop, if not provided -> `SelectEventLoop` is used * Fixed `TornadoEventLoop` & `AsyncioEventLoop` logic (Tornado IOLoop is asyncio based) For extra details see original PR. * Make `AttrSpec` immutable and hash-reusable * Update IDLE callback comment * Update urwid/display_common.py Co-authored-by: Ian Ward <ian@excess.org> --------- Co-authored-by: Aleksei Stepanov <alekseis@nvidia.com> Co-authored-by: Ian Ward <ian@excess.org>
-rwxr-xr-xurwid/display_common.py118
-rw-r--r--urwid/event_loop/asyncio_loop.py83
-rw-r--r--urwid/event_loop/tornado_loop.py77
-rw-r--r--urwid/tests/test_event_loops.py5
-rw-r--r--urwid/vterm.py18
5 files changed, 196 insertions, 105 deletions
diff --git a/urwid/display_common.py b/urwid/display_common.py
index 30ece96..afbf60f 100755
--- a/urwid/display_common.py
+++ b/urwid/display_common.py
@@ -36,7 +36,7 @@ from urwid import signals
from urwid.util import StoppingContext, int_scale
if typing.TYPE_CHECKING:
- from typing_extensions import Literal
+ from typing_extensions import Literal, Self
# for replacing unprintable bytes with '?'
UNPRINTABLE_TRANS_TABLE = b"?" * 32 + bytes(range(32, 256))
@@ -487,6 +487,8 @@ class AttrSpecError(Exception):
class AttrSpec:
+ __slots__ = ("__value",)
+
def __init__(self, fg: str, bg: str, colors: Literal[1, 16, 88, 256, 16777216] = 256) -> None:
"""
fg -- a string containing a comma-separated foreground color
@@ -545,70 +547,102 @@ class AttrSpec:
"""
if colors not in (1, 16, 88, 256, 2**24):
raise AttrSpecError(f'invalid number of colors ({colors:d}).')
- self._value = 0 | _HIGH_88_COLOR * (colors == 88) | _HIGH_TRUE_COLOR * (colors == 2**24)
- self.foreground = fg
- self.background = bg
+ self.__value = 0 | _HIGH_88_COLOR * (colors == 88) | _HIGH_TRUE_COLOR * (colors == 2 ** 24)
+ self.__set_foreground(fg)
+ self.__set_background(bg)
if self.colors > colors:
raise AttrSpecError(
f'foreground/background ({fg!r}/{bg!r}) '
f'require more colors than have been specified ({colors:d}).'
)
+ def copy_modified(
+ self,
+ fg: str | None = None,
+ bg: str | None = None,
+ colors: Literal[1, 16, 88, 256, 16777216] | None = None,
+ ) -> Self:
+ if fg is None:
+ foreground = self.foreground
+ else:
+ foreground = fg
+
+ if bg is None:
+ background = self.background
+ else:
+ background = bg
+
+ if colors is None:
+ new_colors = self.colors
+ else:
+ new_colors = colors
+
+ return self.__class__(foreground, background, new_colors)
+
+ def __hash__(self) -> int:
+ """Instance is immutable and hashable."""
+ return hash((self.__class__, self.__value))
+
+ @property
+ def _value(self) -> int:
+ """Read-only value access."""
+ return self.__value
+
@property
def foreground_basic(self) -> bool:
- return self._value & _FG_BASIC_COLOR != 0
+ return self.__value & _FG_BASIC_COLOR != 0
@property
def foreground_high(self) -> bool:
- return self._value & _FG_HIGH_COLOR != 0
+ return self.__value & _FG_HIGH_COLOR != 0
@property
def foreground_true(self) -> bool:
- return self._value & _FG_TRUE_COLOR != 0
+ return self.__value & _FG_TRUE_COLOR != 0
@property
def foreground_number(self) -> int:
- return self._value & _FG_COLOR_MASK
+ return self.__value & _FG_COLOR_MASK
@property
def background_basic(self) -> bool:
- return self._value & _BG_BASIC_COLOR != 0
+ return self.__value & _BG_BASIC_COLOR != 0
@property
def background_high(self) -> bool:
- return self._value & _BG_HIGH_COLOR != 0
+ return self.__value & _BG_HIGH_COLOR != 0
@property
def background_true(self) -> bool:
- return self._value & _BG_TRUE_COLOR != 0
+ return self.__value & _BG_TRUE_COLOR != 0
@property
def background_number(self) -> int:
- return (self._value & _BG_COLOR_MASK) >> _BG_SHIFT
+ return (self.__value & _BG_COLOR_MASK) >> _BG_SHIFT
@property
def italics(self) -> bool:
- return self._value & _ITALICS != 0
+ return self.__value & _ITALICS != 0
@property
def bold(self) -> bool:
- return self._value & _BOLD != 0
+ return self.__value & _BOLD != 0
@property
def underline(self) -> bool:
- return self._value & _UNDERLINE != 0
+ return self.__value & _UNDERLINE != 0
@property
def blink(self) -> bool:
- return self._value & _BLINK != 0
+ return self.__value & _BLINK != 0
@property
def standout(self) -> bool:
- return self._value & _STANDOUT != 0
+ return self.__value & _STANDOUT != 0
@property
def strikethrough(self) -> bool:
- return self._value & _STRIKETHROUGH != 0
+ return self.__value & _STRIKETHROUGH != 0
@property
def colors(self) -> int:
@@ -617,13 +651,13 @@ class AttrSpec:
Returns 256, 88, 16 or 1.
"""
- if self._value & _HIGH_88_COLOR:
+ if self.__value & _HIGH_88_COLOR:
return 88
- if self._value & (_BG_HIGH_COLOR | _FG_HIGH_COLOR):
+ if self.__value & (_BG_HIGH_COLOR | _FG_HIGH_COLOR):
return 256
- if self._value & (_BG_TRUE_COLOR | _FG_TRUE_COLOR):
+ if self.__value & (_BG_TRUE_COLOR | _FG_TRUE_COLOR):
return 2**24
- if self._value & (_BG_BASIC_COLOR | _FG_BASIC_COLOR):
+ if self.__value & (_BG_BASIC_COLOR | _FG_BASIC_COLOR):
return 16
return 1
@@ -671,8 +705,7 @@ class AttrSpec:
+ ',strikethrough' * self.strikethrough
)
- @foreground.setter
- def foreground(self, foreground: str) -> None:
+ def __set_foreground(self, foreground: str) -> None:
color = None
flags = 0
# handle comma-separated foreground
@@ -691,10 +724,10 @@ class AttrSpec:
elif part in _BASIC_COLORS:
scolor = _BASIC_COLORS.index(part)
flags |= _FG_BASIC_COLOR
- elif self._value & _HIGH_88_COLOR:
+ elif self.__value & _HIGH_88_COLOR:
scolor = _parse_color_88(part)
flags |= _FG_HIGH_COLOR
- elif self._value & _HIGH_TRUE_COLOR:
+ elif self.__value & _HIGH_TRUE_COLOR:
scolor = _parse_color_true(part)
flags |= _FG_TRUE_COLOR
else:
@@ -708,7 +741,7 @@ class AttrSpec:
color = scolor
if color is None:
color = 0
- self._value = (self._value & ~_FG_MASK) | color | flags
+ self.__value = (self.__value & ~_FG_MASK) | color | flags
def _foreground(self) -> str:
warnings.warn(
@@ -719,15 +752,6 @@ class AttrSpec:
)
return self.foreground
- def _set_foreground(self, foreground: str) -> None:
- warnings.warn(
- f"Method `{self.__class__.__name__}._set_foreground` is deprecated, "
- f"please use property `{self.__class__.__name__}.foreground`",
- DeprecationWarning,
- stacklevel=2,
- )
- self.foreground = foreground
-
@property
def background(self) -> str:
"""Return the background color."""
@@ -735,24 +759,23 @@ class AttrSpec:
return 'default'
if self.background_basic:
return _BASIC_COLORS[self.background_number]
- if self._value & _HIGH_88_COLOR:
+ if self.__value & _HIGH_88_COLOR:
return _color_desc_88(self.background_number)
if self.colors == 2**24:
return _color_desc_true(self.background_number)
return _color_desc_256(self.background_number)
- @background.setter
- def background(self, background: str) -> None:
+ def __set_background(self, background: str) -> None:
flags = 0
if background in ('', 'default'):
color = 0
elif background in _BASIC_COLORS:
color = _BASIC_COLORS.index(background)
flags |= _BG_BASIC_COLOR
- elif self._value & _HIGH_88_COLOR:
+ elif self.__value & _HIGH_88_COLOR:
color = _parse_color_88(background)
flags |= _BG_HIGH_COLOR
- elif self._value & _HIGH_TRUE_COLOR:
+ elif self.__value & _HIGH_TRUE_COLOR:
color = _parse_color_true(background)
flags |= _BG_TRUE_COLOR
else:
@@ -760,7 +783,7 @@ class AttrSpec:
flags |= _BG_HIGH_COLOR
if color is None:
raise AttrSpecError(f"Unrecognised color specification in background ({background!r})")
- self._value = (self._value & ~_BG_MASK) | (color << _BG_SHIFT) | flags
+ self.__value = (self.__value & ~_BG_MASK) | (color << _BG_SHIFT) | flags
def _background(self) -> str:
warnings.warn(
@@ -771,15 +794,6 @@ class AttrSpec:
)
return self.background
- def _set_background(self, background: str) -> None:
- warnings.warn(
- f"Method `{self.__class__.__name__}._set_background` is deprecated, "
- f"please use property `{self.__class__.__name__}.background`",
- DeprecationWarning,
- stacklevel=2,
- )
- self.background = background
-
def get_rgb_values(self):
"""
Return (fg_red, fg_green, fg_blue, bg_red, bg_green, bg_blue) color
@@ -817,7 +831,7 @@ class AttrSpec:
return vals + _COLOR_VALUES_256[self.background_number]
def __eq__(self, other: typing.Any) -> bool:
- return isinstance(other, AttrSpec) and self._value == other._value
+ return isinstance(other, AttrSpec) and self.__value == other.__value
def __ne__(self, other: typing.Any) -> bool:
return not self == other
diff --git a/urwid/event_loop/asyncio_loop.py b/urwid/event_loop/asyncio_loop.py
index e8f1522..fce9472 100644
--- a/urwid/event_loop/asyncio_loop.py
+++ b/urwid/event_loop/asyncio_loop.py
@@ -24,11 +24,18 @@
from __future__ import annotations
import asyncio
+import functools
import typing
from collections.abc import Callable
from .abstract_loop import EventLoop, ExitMainLoop
+if typing.TYPE_CHECKING:
+ from typing_extensions import ParamSpec
+
+ _Spec = ParamSpec("_Spec")
+ _T = typing.TypeVar("_T")
+
__all__ = ("AsyncioEventLoop",)
@@ -36,11 +43,17 @@ class AsyncioEventLoop(EventLoop):
"""
Event loop based on the standard library ``asyncio`` module.
+ .. note::
+ If you make any changes to the urwid state outside of it
+ handling input or responding to alarms (for example, from asyncio.Task
+ running in background), and wish the screen to be
+ redrawn, you must call :meth:`MainLoop.draw_screen` method of the
+ main loop manually.
+ A good way to do this::
+ asyncio.get_event_loop().call_soon(main_loop.draw_screen)
"""
_we_started_event_loop = False
- _idle_emulation_delay = 1.0/30 # a short time (in seconds)
-
def __init__(self, *, loop: asyncio.AbstractEventLoop | None = None, **kwargs) -> None:
if loop:
self._loop: asyncio.AbstractEventLoop = loop
@@ -49,6 +62,33 @@ class AsyncioEventLoop(EventLoop):
self._exc: BaseException | None = None
+ self._idle_asyncio_handle: asyncio.TimerHandle | None = None
+ self._idle_handle: int = 0
+ self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
+
+ def _also_call_idle(self, callback: Callable[_Spec, _T]) -> Callable[_Spec, _T]:
+ """
+ Wrap the callback to also call _entering_idle.
+ """
+
+ @functools.wraps(callback)
+ def wrapper(*args: _Spec.args, **kwargs: _Spec.kwargs) -> _T:
+ if not self._idle_asyncio_handle:
+ self._idle_asyncio_handle = self._loop.call_later(0, self._entering_idle)
+ return callback(*args, **kwargs)
+
+ return wrapper
+
+ def _entering_idle(self) -> None:
+ """
+ Call all the registered idle callbacks.
+ """
+ try:
+ for callback in self._idle_callbacks.values():
+ callback()
+ finally:
+ self._idle_asyncio_handle = None
+
def alarm(self, seconds: float | int, callback: Callable[[], typing.Any]) -> asyncio.TimerHandle:
"""
Call callback() a given time from now. No parameters are
@@ -59,7 +99,7 @@ class AsyncioEventLoop(EventLoop):
seconds -- time in seconds to wait before calling callback
callback -- function to call from event loop
"""
- return self._loop.call_later(seconds, callback)
+ return self._loop.call_later(seconds, self._also_call_idle(callback))
def remove_alarm(self, handle) -> bool:
"""
@@ -86,7 +126,7 @@ class AsyncioEventLoop(EventLoop):
fd -- file descriptor to watch for input
callback -- function to call when input is available
"""
- self._loop.add_reader(fd, callback)
+ self._loop.add_reader(fd, self._also_call_idle(callback))
return fd
def remove_watch_file(self, handle: int) -> bool:
@@ -97,38 +137,41 @@ class AsyncioEventLoop(EventLoop):
"""
return self._loop.remove_reader(handle)
- def enter_idle(self, callback: Callable[[], typing.Any]) -> list[asyncio.TimerHandle]:
+ def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
"""
Add a callback for entering idle.
- Returns a handle that may be passed to remove_idle()
+ Returns a handle that may be passed to remove_enter_idle()
"""
# XXX there's no such thing as "idle" in most event loops; this fakes
- # it the same way as Twisted, by scheduling the callback to be called
- # repeatedly
- mutable_handle: list[asyncio.TimerHandle] = []
-
- def faux_idle_callback():
- callback()
- mutable_handle[0] = self._loop.call_later(self._idle_emulation_delay, faux_idle_callback)
+ # it by adding extra callback to the timer and file watch callbacks.
+ self._idle_handle += 1
+ self._idle_callbacks[self._idle_handle] = callback
+ return self._idle_handle
- mutable_handle.append(self._loop.call_later(self._idle_emulation_delay, faux_idle_callback))
-
- return mutable_handle
-
- def remove_enter_idle(self, handle) -> bool:
+ def remove_enter_idle(self, handle: int) -> bool:
"""
Remove an idle callback.
Returns True if the handle was removed.
"""
- # `handle` is just a list containing the current actual handle
- return self.remove_alarm(handle[0])
+ try:
+ del self._idle_callbacks[handle]
+ except KeyError:
+ return False
+ return True
def _exception_handler(self, loop: asyncio.AbstractEventLoop, context):
exc = context.get('exception')
if exc:
loop.stop()
+
+ if self._idle_asyncio_handle:
+ # clean it up to prevent old callbacks
+ # from messing things up if loop is restarted
+ self._idle_asyncio_handle.cancel()
+ self._idle_asyncio_handle = None
+
if not isinstance(exc, ExitMainLoop):
# Store the exc_info so we can re-raise after the loop stops
self._exc = exc
diff --git a/urwid/event_loop/tornado_loop.py b/urwid/event_loop/tornado_loop.py
index 72f4635..0d9a5b3 100644
--- a/urwid/event_loop/tornado_loop.py
+++ b/urwid/event_loop/tornado_loop.py
@@ -47,25 +47,49 @@ class TornadoEventLoop(EventLoop):
""" This is an Urwid-specific event loop to plug into its MainLoop.
It acts as an adaptor for Tornado's IOLoop which does all
heavy lifting except idle-callbacks.
-
- Notice, since Tornado has no concept of idle callbacks we
- monkey patch ioloop._impl.poll() function to be able to detect
- potential idle periods.
"""
- _idle_emulation_delay = 1.0 / 30 # a short time (in seconds)
def __init__(self, loop: ioloop.IOLoop | None = None) -> None:
if loop:
self._loop: ioloop.IOLoop = loop
else:
- self._loop = ioloop.IOLoop.instance()
+ self._loop = ioloop.IOLoop.current() # TODO(Aleksei): Switch to the syncio.EventLoop as tornado >= 6.0 !
self._pending_alarms: dict[object, int] = {}
self._watch_handles: dict[int, int] = {} # {<watch_handle> : <file_descriptor>}
self._max_watch_handle: int = 0
self._exc: BaseException | None = None
+ self._idle_asyncio_handle: object | None = None
+ self._idle_handle: int = 0
+ self._idle_callbacks: dict[int, Callable[[], typing.Any]] = {}
+
+ def _also_call_idle(self, callback: Callable[_Spec, _T]) -> Callable[_Spec, _T]:
+ """
+ Wrap the callback to also call _entering_idle.
+ """
+
+ @functools.wraps(callback)
+ def wrapper(*args: _Spec.args, **kwargs: _Spec.kwargs) -> _T:
+ if not self._idle_asyncio_handle:
+ self._idle_asyncio_handle = self._loop.call_later(0, self._entering_idle)
+ return callback(*args, **kwargs)
+
+ return wrapper
+
+ def _entering_idle(self) -> None:
+ """
+ Call all the registered idle callbacks.
+ """
+ try:
+ for callback in self._idle_callbacks.values():
+ callback()
+ finally:
+ self._idle_asyncio_handle = None
+
def alarm(self, seconds: float | int, callback: Callable[[], typing.Any]):
+ @self._also_call_idle
+ @functools.wraps(callback)
def wrapped() -> None:
try:
del self._pending_alarms[handle]
@@ -77,7 +101,7 @@ class TornadoEventLoop(EventLoop):
self._pending_alarms[handle] = 1
return handle
- def remove_alarm(self, handle) -> bool:
+ def remove_alarm(self, handle: object) -> bool:
self._loop.remove_timeout(handle)
try:
del self._pending_alarms[handle]
@@ -87,6 +111,7 @@ class TornadoEventLoop(EventLoop):
return True
def watch_file(self, fd: int, callback: Callable[[], _T]) -> int:
+ @self._also_call_idle
def handler(_fd: int, _events: int) -> None:
self.handle_exit(callback)()
@@ -104,34 +129,29 @@ class TornadoEventLoop(EventLoop):
self._loop.remove_handler(fd)
return True
- def enter_idle(self, callback: Callable[[], typing.Any]) -> list[object]:
+ def enter_idle(self, callback: Callable[[], typing.Any]) -> int:
"""
Add a callback for entering idle.
Returns a handle that may be passed to remove_idle()
"""
# XXX there's no such thing as "idle" in most event loops; this fakes
- # it the same way as Twisted, by scheduling the callback to be called
- # repeatedly
- mutable_handle: list[object] = []
-
- def faux_idle_callback():
- callback()
- mutable_handle[0] = self._loop.call_later(self._idle_emulation_delay, faux_idle_callback)
-
- mutable_handle.append(self._loop.call_later(self._idle_emulation_delay, faux_idle_callback))
-
- # asyncio used as backend, real type comes from asyncio_loop.call_later
- return mutable_handle
+ # it by adding extra callback to the timer and file watch callbacks.
+ self._idle_handle += 1
+ self._idle_callbacks[self._idle_handle] = callback
+ return self._idle_handle
- def remove_enter_idle(self, handle) -> bool:
+ def remove_enter_idle(self, handle: int) -> bool:
"""
Remove an idle callback.
Returns True if the handle was removed.
"""
- # `handle` is just a list containing the current actual handle
- return self.remove_alarm(handle[0])
+ try:
+ del self._idle_callbacks[handle]
+ except KeyError:
+ return False
+ return True
def handle_exit(self, f: Callable[_Spec, _T]) -> Callable[_Spec, _T | Literal[False]]:
@functools.wraps(f)
@@ -139,10 +159,17 @@ class TornadoEventLoop(EventLoop):
try:
return f(*args, **kwargs)
except ExitMainLoop:
- self._loop.stop()
+ pass # handled later
except Exception as exc:
self._exc = exc
- self._loop.stop()
+
+ if self._idle_asyncio_handle:
+ # clean it up to prevent old callbacks
+ # from messing things up if loop is restarted
+ self._loop.remove_timeout(self._idle_asyncio_handle)
+ self._idle_asyncio_handle = None
+
+ self._loop.stop()
return False
return wrapper
diff --git a/urwid/tests/test_event_loops.py b/urwid/tests/test_event_loops.py
index fac7f72..b8ce257 100644
--- a/urwid/tests/test_event_loops.py
+++ b/urwid/tests/test_event_loops.py
@@ -71,12 +71,13 @@ class EventLoopTestMixin:
if self._expected_idle_handle is not None:
self.assertEqual(idle_handle, 1)
evl.run()
+ self.assertTrue("waiting" in out, out)
self.assertTrue("hello" in out, out)
self.assertTrue("clean exit" in out, out)
handle = evl.watch_file(rd, exit_clean)
del out[:]
evl.run()
- self.assertEqual(out, ["clean exit"])
+ self.assertEqual(["clean exit"], out)
self.assertTrue(evl.remove_watch_file(handle))
handle = evl.alarm(0, exit_error)
self.assertRaises(ZeroDivisionError, evl.run)
@@ -208,7 +209,7 @@ class AsyncioEventLoopTest(unittest.TestCase, EventLoopTestMixin):
result = 1 / 0 # Simulate error in coroutine
return result
- asyncio.ensure_future(error_coro())
+ asyncio.ensure_future(error_coro(), loop=asyncio.get_event_loop_policy().get_event_loop())
self.assertRaises(ZeroDivisionError, evl.run)
diff --git a/urwid/vterm.py b/urwid/vterm.py
index 486fc01..6922c7f 100644
--- a/urwid/vterm.py
+++ b/urwid/vterm.py
@@ -48,6 +48,7 @@ from urwid.canvas import Canvas
from urwid.display_common import _BASIC_COLORS, AttrSpec, RealTerminal
from urwid.escape import ALT_DEC_SPECIAL_CHARS, DEC_SPECIAL_CHARS
from urwid.widget import BOX, Widget
+from urwid import event_loop
if typing.TYPE_CHECKING:
from typing_extensions import Literal
@@ -1123,13 +1124,15 @@ class TermCanvas(Canvas):
fg = None
else:
fg = self.attrspec.foreground_number
- if fg >= 8: fg -= 8
+ if fg >= 8:
+ fg -= 8
if 'default' in self.attrspec.background:
bg = None
else:
bg = self.attrspec.background_number
- if bg >= 8: bg -= 8
+ if bg >= 8:
+ bg -= 8
for attr in ('bold', 'underline', 'blink', 'standout'):
if not getattr(self.attrspec, attr):
@@ -1154,10 +1157,10 @@ class TermCanvas(Canvas):
attrs = [fg.strip() for fg in attrspec.foreground.split(',')]
if 'standout' in attrs and undo:
attrs.remove('standout')
- attrspec.foreground = ','.join(attrs)
+ attrspec = attrspec.copy_modified(fg=','.join(attrs))
elif 'standout' not in attrs and not undo:
attrs.append('standout')
- attrspec.foreground = ','.join(attrs)
+ attrspec = attrspec.copy_modified(fg=','.join(attrs))
return attrspec
def reverse_video(self, undo: bool = False) -> None:
@@ -1366,7 +1369,7 @@ class Terminal(Widget):
self,
command: Sequence[str] | None,
env: Mapping[str, str] | Iterable[Sequence[str]] | None = None,
- main_loop=None,
+ main_loop: event_loop.EventLoop | None = None,
escape_sequence: str | None = None,
encoding: str = 'utf-8',
):
@@ -1418,7 +1421,10 @@ class Terminal(Widget):
self.term_modes = TermModes()
- self.main_loop = main_loop
+ if main_loop is not None:
+ self.main_loop = main_loop
+ else:
+ self.main_loop = event_loop.SelectEventLoop()
self.master = None
self.pid = None