Skip to content

WIP Back to threads #927

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Improve CodeRunner documentation.
  • Loading branch information
thomasballinger committed Oct 7, 2021
commit 06a55384f02f04fc448b82e9baffe53b48aa8814
61 changes: 33 additions & 28 deletions bpython/curtsiesfrontend/coderunner.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""For running Python code that could interrupt itself at any time in order to,
for example, ask for a read on stdin, or a write on stdout
for example, ask for a read on stdin, or a write on stdout.

The CodeRunner spawns a thread to run code in, and that code can block
The CodeRunner spawns a thread to run code in. That code can block
on a queue to ask the main (UI) thread to refresh the display or get
information.
"""
Expand Down Expand Up @@ -54,26 +54,33 @@ def __init__(self, args):


class CodeRunner:
"""Runs user code in an interpreter.

Running code requests a refresh by calling
request_from_main_thread(force_refresh=True), which
suspends execution of the code by blocking on a queue
that the main thread was blocked on.

After load_code() is called with the source code to be run,
the run_code() method should be called to start running the code.
The running code may request screen refreshes and user input
by calling request_from_main_thread.
When this are called, the running source code cedes
control, and the current run_code() method call returns.

The return value of run_code() determines whether the method ought
to be called again to complete execution of the source code.
"""Runs user code in a pausable thread.

>>> cr = CodeRunner()
>>> def get_input():
... print('waiting for a number plz')
... return cr.request_from_main_thread()
...
>>> i = InteractiveInterpreter(locals={'get_input': get_input})
>>> cr.interp = i
>>> cr.load_code('x = get_input(); print(x * 2)')
>>> finished = cr.run_code()
waiting for a number plz
>>> # do something else, user code thread is paused
>>> finished = cr.run_code(for_code=21)
42

As user code executes it can make requests for values or simply
request that the screen be refreshed with `request_from_main_thread()`.
This pauses the user code execution thread and wakes up the main thread,
where run_code() returns whether user code has finished executing.
This is cooperative multitasking: even though there are two threads,
the main thread and the user code thread, the two threads work cede
control to one another like like green threads with no parallelism.

Once the screen refresh has occurred or the requested user input
has been gathered, run_code() should be called again, passing in any
requested user input. This continues until run_code returns Done.
requested user input. This continues until run_code returns True.

The code thread is responsible for telling the main thread
what it wants returned in the next run_code call - CodeRunner
Expand All @@ -98,7 +105,7 @@ def __init__(self, interp=None, request_refresh=lambda: None):
# waiting for response from main thread
self.code_is_waiting = False
# sigint happened while in main thread
self.sigint_happened_in_main_thread = False # TODO rename context to thread
self.sigint_happened_in_main_thread = False
self.orig_sigint_handler = None

@property
Expand Down Expand Up @@ -130,8 +137,8 @@ def run_code(self, for_code=None):
if self.code_thread is None:
assert self.source is not None
self.code_thread = threading.Thread(
target=self._blocking_run_code,
name='codethread')
target=self._blocking_run_code, name="codethread"
)
self.code_thread.daemon = True
if is_main_thread():
self.orig_sigint_handler = signal.getsignal(signal.SIGINT)
Expand All @@ -151,9 +158,7 @@ def run_code(self, for_code=None):
request = self.requests_from_code_thread.get()
logger.debug("request received from code was %r", request)
if not isinstance(request, RequestFromCodeRunner):
raise ValueError(
"Not a valid value from code thread: %r" % request
)
raise ValueError("Not a valid value from code thread: %r" % request)
if isinstance(request, (Wait, Refresh)):
self.code_is_waiting = True
if isinstance(request, Refresh):
Expand Down Expand Up @@ -188,9 +193,9 @@ def _blocking_run_code(self):
except SystemExit as e:
self.requests_from_code_thread.push(SystemExitRequest(*e.args))
return
self.requests_from_code_thread.put(Unfinished()
if unfinished
else Done())
self.requests_from_code_thread.put(
Unfinished() if unfinished else Done()
)

def request_from_main_thread(self, force_refresh=False):
"""Return the argument passed in to .run_code(for_code)
Expand Down
4 changes: 3 additions & 1 deletion bpython/curtsiesfrontend/interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,9 @@ def wait_for_request_or_notify(self):
try:
r = self.request_or_notify_queue.get(True, 1)
except queue.Empty:
raise Exception('Main thread blocked because task thread not calling back')
raise Exception(
"Main thread blocked because task thread not calling back"
)
return r

# interaction interface - should be called from other threads
Expand Down
2 changes: 1 addition & 1 deletion bpython/curtsiesfrontend/repl.py
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,7 @@ def switch(self, task):
"""Runs task in another thread"""
t = threading.Thread(target=task)
t.daemon = True
logging.debug('starting task thread')
logging.debug("starting task thread")
t.start()
self.interact.wait_for_request_or_notify()

Expand Down