Skip to content

Commit

Permalink
Stop using chess.engine.EventLoopPolicy
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasf committed Jul 27, 2023
1 parent 8214d97 commit 7acc7cf
Show file tree
Hide file tree
Showing 5 changed files with 2 additions and 103 deletions.
80 changes: 2 additions & 78 deletions chess/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,77 +42,8 @@
MANAGED_OPTIONS = ["uci_chess960", "uci_variant", "multipv", "ponder"]


class EventLoopPolicy(asyncio.AbstractEventLoopPolicy):
"""
An event loop policy for thread-local event loops and child watchers.
Ensures each event loop is capable of spawning and watching subprocesses,
even when not running on the main thread.
Windows: Uses :class:`~asyncio.ProactorEventLoop`.
Unix: Uses :class:`~asyncio.SelectorEventLoop`. If available,
:class:`~asyncio.PidfdChildWatcher` is used to detect subprocess
termination (Python 3.9+ on Linux 5.3+). Otherwise, the default child
watcher is used on the main thread and relatively slow eager polling
is used on all other threads.
"""
class _Local(threading.local):
loop: Optional[asyncio.AbstractEventLoop] = None
set_called = False
watcher: Optional[asyncio.AbstractChildWatcher] = None

def __init__(self) -> None:
self._local = self._Local()

def get_event_loop(self) -> asyncio.AbstractEventLoop:
if self._local.loop is None and not self._local.set_called and threading.current_thread() is threading.main_thread():
self.set_event_loop(self.new_event_loop())
if self._local.loop is None:
raise RuntimeError(f"no current event loop in thread {threading.current_thread().name!r}")
return self._local.loop

def set_event_loop(self, loop: Optional[asyncio.AbstractEventLoop]) -> None:
assert loop is None or isinstance(loop, asyncio.AbstractEventLoop)
self._local.set_called = True
self._local.loop = loop
if self._local.watcher is not None:
self._local.watcher.attach_loop(loop)

def new_event_loop(self) -> asyncio.AbstractEventLoop:
return asyncio.ProactorEventLoop() if sys.platform == "win32" else asyncio.SelectorEventLoop() # type: ignore

def get_child_watcher(self) -> asyncio.AbstractChildWatcher:
if self._local.watcher is None:
self._local.watcher = self._init_watcher()
self._local.watcher.attach_loop(self._local.loop)
return self._local.watcher

def set_child_watcher(self, watcher: Optional[asyncio.AbstractChildWatcher]) -> None:
assert watcher is None or isinstance(watcher, asyncio.AbstractChildWatcher)
if self._local.watcher is not None:
self._local.watcher.close()
self._local.watcher = watcher

def _init_watcher(self) -> asyncio.AbstractChildWatcher:
if sys.platform == "win32":
raise NotImplementedError

try:
os.close(os.pidfd_open(os.getpid()))
watcher: asyncio.AbstractChildWatcher = asyncio.PidfdChildWatcher()
LOGGER.debug("Using PidfdChildWatcher")
return watcher
except (AttributeError, OSError):
# Before Python 3.9 or before Linux 5.3 or the syscall is not
# permitted.
pass

if threading.current_thread() is threading.main_thread():
LOGGER.debug("Using SafeChildWatcher")
return asyncio.SafeChildWatcher()
else:
LOGGER.debug("Using ThreadedChildWatcher")
return asyncio.ThreadedChildWatcher()
# No longer needed, but alias kept around for compatibility.
EventLoopPolicy = asyncio.DefaultEventLoopPolicy


def run_in_background(coroutine: Callable[[concurrent.futures.Future[T]], Coroutine[Any, Any, None]], *, name: Optional[str] = None, debug: bool = False, _policy_lock: threading.Lock = threading.Lock()) -> T:
Expand All @@ -122,16 +53,9 @@ def run_in_background(coroutine: Callable[[concurrent.futures.Future[T]], Corout
Blocks on *future* and returns the result as soon as it is resolved.
The coroutine and all remaining tasks continue running in the background
until complete.
Note: This installs a :class:`chess.engine.EventLoopPolicy` for the entire
process.
"""
assert asyncio.iscoroutinefunction(coroutine)

with _policy_lock:
if not isinstance(asyncio.get_event_loop_policy(), EventLoopPolicy):
asyncio.set_event_loop_policy(EventLoopPolicy())

future: concurrent.futures.Future[T] = concurrent.futures.Future()

def background() -> None:
Expand Down
6 changes: 0 additions & 6 deletions docs/engine.rst
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ Example: Let Stockfish play against itself, 100 milliseconds per move.
await engine.quit()
asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())
.. autoclass:: chess.engine.Protocol
Expand Down Expand Up @@ -126,7 +125,6 @@ Example:
await engine.quit()
asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())
.. autoclass:: chess.engine.Protocol
Expand Down Expand Up @@ -189,7 +187,6 @@ Example: Stream information from the engine and stop on an arbitrary condition.
await engine.quit()
asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())
.. autoclass:: chess.engine.Protocol
Expand Down Expand Up @@ -243,7 +240,6 @@ Options
# [...]
asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())
.. autoclass:: chess.engine.Protocol
Expand Down Expand Up @@ -315,5 +311,3 @@ Reference

.. autoclass:: chess.engine.SimpleAnalysisResult
:members:

.. autofunction:: chess.engine.EventLoopPolicy
1 change: 0 additions & 1 deletion examples/bratko_kopec/bratko_kopec.py
Original file line number Diff line number Diff line change
Expand Up @@ -152,5 +152,4 @@ async def main() -> None:


if __name__ == "__main__":
asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())
1 change: 0 additions & 1 deletion fuzz/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@


logging.getLogger("chess.engine").setLevel(logging.CRITICAL)
asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())


@PythonFuzz
Expand Down
17 changes: 0 additions & 17 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3114,7 +3114,6 @@ async def main():
await protocol.ping()
mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

def test_uci_debug(self):
Expand All @@ -3130,7 +3129,6 @@ async def main():
protocol.debug(False)
mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

def test_uci_go(self):
Expand Down Expand Up @@ -3173,7 +3171,6 @@ async def main():
self.assertEqual(result.ponder, None)
mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

def test_iota_log(self):
Expand All @@ -3196,7 +3193,6 @@ async def main():
await protocol.play(board, chess.engine.Limit(time=5.0))
mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

def test_uci_analyse_mode(self):
Expand Down Expand Up @@ -3243,7 +3239,6 @@ async def main():
self.assertEqual(best.ponder, chess.Move.from_uci("e7e5"))
mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

def test_uci_play_after_analyse(self):
Expand Down Expand Up @@ -3271,7 +3266,6 @@ async def main():

mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

def test_uci_ponderhit(self):
Expand Down Expand Up @@ -3387,7 +3381,6 @@ async def main():

mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

def test_uci_info(self):
Expand Down Expand Up @@ -3474,7 +3467,6 @@ async def main():
await protocol.send_game_result(checkmate_board)
mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

def test_hiarcs_bestmove(self):
Expand All @@ -3499,7 +3491,6 @@ async def main():
self.assertEqual(result.info["string"], "keep double space")
mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

def test_xboard_options(self):
Expand Down Expand Up @@ -3562,7 +3553,6 @@ async def main():
await protocol.configure({"buttonvar": None})
mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

def test_xboard_replay(self):
Expand Down Expand Up @@ -3624,7 +3614,6 @@ async def main():
self.assertEqual(result.move, board.parse_san("d4"))
mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

def test_xboard_opponent(self):
Expand Down Expand Up @@ -3682,7 +3671,6 @@ async def main():
result = await protocol.play(board, limit, game="bad game", opponent=bad_opponent)
mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

def test_xboard_result(self):
Expand Down Expand Up @@ -3772,7 +3760,6 @@ async def main():
await protocol.send_game_result(material_board)
mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

def test_xboard_analyse(self):
Expand Down Expand Up @@ -3810,7 +3797,6 @@ async def main():
self.assertEqual(info["pv"], [chess.Move.from_uci(move) for move in ["f7f6", "e2e4", "e7e6"]])
mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

def test_xboard_level(self):
Expand Down Expand Up @@ -3855,7 +3841,6 @@ async def main():
self.assertEqual(result.move, chess.Move.from_uci("d2d4"))
mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

def test_xboard_error(self):
Expand All @@ -3874,7 +3859,6 @@ async def main():

mock.assert_done()

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

@catchAndSkip(FileNotFoundError, "need /bin/bash")
Expand All @@ -3886,7 +3870,6 @@ async def main():
self.assertNotEqual(results[0], None)
self.assertNotEqual(results[1], None)

asyncio.set_event_loop_policy(chess.engine.EventLoopPolicy())
asyncio.run(main())

@catchAndSkip(FileNotFoundError, "need /bin/bash")
Expand Down

0 comments on commit 7acc7cf

Please sign in to comment.