From 290fafa6a4662ad883c634beb49c680f1bd009cb Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Sat, 22 Apr 2017 10:38:56 -0700 Subject: [PATCH 1/3] WIP - First draft of moving back to threads. --- bpython/curtsiesfrontend/coderunner.py | 71 ++++++++++++++----------- bpython/curtsiesfrontend/interaction.py | 36 ++++++++----- bpython/curtsiesfrontend/repl.py | 19 +++++-- doc/sphinx/source/contributing.rst | 2 +- 4 files changed, 76 insertions(+), 52 deletions(-) diff --git a/bpython/curtsiesfrontend/coderunner.py b/bpython/curtsiesfrontend/coderunner.py index cddf1169d..f4a6ee169 100644 --- a/bpython/curtsiesfrontend/coderunner.py +++ b/bpython/curtsiesfrontend/coderunner.py @@ -1,17 +1,14 @@ """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 -The CodeRunner spawns a greenlet to run code in, and that code can suspend its -own execution to ask the main greenlet to refresh the display or get +The CodeRunner spawns a thread to run code in, and that code can block +on a queue to ask the main (UI) thread to refresh the display or get information. - -Greenlets are basically threads that can explicitly switch control to each -other. You can replace the word "greenlet" with "thread" in these docs if that -makes more sense to you. """ import code -import greenlet +from six.moves import queue +import threading import logging import signal @@ -21,12 +18,12 @@ class SigintHappened: - """If this class is returned, a SIGINT happened while the main greenlet""" + """If this class is returned, a SIGINT happened while the main thread""" class SystemExitFromCodeRunner(SystemExit): """If this class is returned, a SystemExit happened while in the code - greenlet""" + thread""" class RequestFromCodeRunner: @@ -61,7 +58,8 @@ class CodeRunner: Running code requests a refresh by calling request_from_main_context(force_refresh=True), which - suspends execution of the code and switches back to the main greenlet + 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. @@ -77,10 +75,10 @@ class CodeRunner: has been gathered, run_code() should be called again, passing in any requested user input. This continues until run_code returns Done. - The code greenlet is responsible for telling the main greenlet + The code thread is responsible for telling the main thread what it wants returned in the next run_code call - CodeRunner just passes whatever is passed in to run_code(for_code) to the - code greenlet + code thread. """ def __init__(self, interp=None, request_refresh=lambda: None): @@ -93,20 +91,20 @@ def __init__(self, interp=None, request_refresh=lambda: None): """ self.interp = interp or code.InteractiveInterpreter() self.source = None - self.main_context = greenlet.getcurrent() - self.code_context = None + self.code_thread = None + self.requests_from_code_thread = queue.Queue(maxsize=0) + self.responses_for_code_thread = queue.Queue() self.request_refresh = request_refresh # waiting for response from main thread self.code_is_waiting = False # sigint happened while in main thread - self.sigint_happened_in_main_context = False + self.sigint_happened_in_main_context = False # TODO rename context to thread self.orig_sigint_handler = None @property def running(self): - """Returns greenlet if code has been loaded greenlet has been - started""" - return self.source and self.code_context + """Returns the running thread if code has been loaded and started.""" + return self.source and self.code_thread def load_code(self, source): """Prep code to be run""" @@ -114,28 +112,31 @@ def load_code(self, source): "you shouldn't load code when some is " "already running" ) self.source = source - self.code_context = None + self.code_thread = None def _unload_code(self): """Called when done running code""" self.source = None - self.code_context = None + self.code_thread = None self.code_is_waiting = False def run_code(self, for_code=None): """Returns Truthy values if code finishes, False otherwise - if for_code is provided, send that value to the code greenlet + if for_code is provided, send that value to the code thread if source code is complete, returns "done" if source code is incomplete, returns "unfinished" """ - if self.code_context is None: + if self.code_thread is None: assert self.source is not None - self.code_context = greenlet.greenlet(self._blocking_run_code) + self.code_thread = threading.Thread( + target=self._blocking_run_code, + name='codethread') + self.code_thread.daemon = True if is_main_thread(): self.orig_sigint_handler = signal.getsignal(signal.SIGINT) signal.signal(signal.SIGINT, self.sigint_handler) - request = self.code_context.switch() + self.code_thread.start() else: assert self.code_is_waiting self.code_is_waiting = False @@ -143,14 +144,15 @@ def run_code(self, for_code=None): signal.signal(signal.SIGINT, self.sigint_handler) if self.sigint_happened_in_main_context: self.sigint_happened_in_main_context = False - request = self.code_context.switch(SigintHappened) + self.responses_for_code_thread.put(SigintHappened) else: - request = self.code_context.switch(for_code) + self.responses_for_code_thread.put(for_code) + 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 greenlet: %r" % request + "Not a valid value from code thread: %r" % request ) if isinstance(request, (Wait, Refresh)): self.code_is_waiting = True @@ -170,7 +172,7 @@ def run_code(self, for_code=None): def sigint_handler(self, *args): """SIGINT handler to use while code is running or request being fulfilled""" - if greenlet.getcurrent() is self.code_context: + if threading.current_thread() is self.code_thread: logger.debug("sigint while running user code!") raise KeyboardInterrupt() else: @@ -184,8 +186,11 @@ def _blocking_run_code(self): try: unfinished = self.interp.runsource(self.source) except SystemExit as e: - return SystemExitRequest(*e.args) - return Unfinished() if unfinished else Done() + self.requests_from_code_thread.push(SystemExitRequest(*e.args)) + return + self.requests_from_code_thread.put(Unfinished() + if unfinished + else Done()) def request_from_main_context(self, force_refresh=False): """Return the argument passed in to .run_code(for_code) @@ -193,9 +198,11 @@ def request_from_main_context(self, force_refresh=False): Nothing means calls to run_code must be... ??? """ if force_refresh: - value = self.main_context.switch(Refresh()) + self.requests_from_code_thread.put(Refresh()) + value = self.responses_for_code_thread.get() else: - value = self.main_context.switch(Wait()) + self.requests_from_code_thread.put(Wait()) + value = self.responses_for_code_thread.get() if value is SigintHappened: raise KeyboardInterrupt() return value diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index 51fd28d35..ee6608a80 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -1,5 +1,5 @@ -import greenlet import time +from queue import Queue from curtsies import events from ..translations import _ @@ -43,8 +43,8 @@ def __init__( self.permanent_stack = [] if permanent_text: self.permanent_stack.append(permanent_text) - self.main_context = greenlet.getcurrent() - self.request_context = None + self.response_queue = Queue() + self.request_or_notify_queue = Queue() self.request_refresh = request_refresh self.schedule_refresh = schedule_refresh @@ -102,12 +102,12 @@ def process_event(self, e): elif self.in_prompt and e in ("\n", "\r", "", "Ctrl-m>"): line = self._current_line self.escape() - self.request_context.switch(line) + self.response_queue.put(line) elif self.in_confirm: if e.lower() == _("y"): - self.request_context.switch(True) + self.request_queue.put(True) else: - self.request_context.switch(False) + self.request_queue.put(False) self.escape() else: # add normal character self.add_normal_character(e) @@ -126,6 +126,7 @@ def add_normal_character(self, e): def escape(self): """unfocus from statusbar, clear prompt state, wait for notify call""" + self.wait_for_request_or_notify() self.in_prompt = False self.in_confirm = False self.prompt = "" @@ -148,27 +149,34 @@ def current_line(self): def should_show_message(self): return bool(self.current_line) - # interaction interface - should be called from other greenlets + 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') + return r + + # interaction interface - should be called from other threads def notify(self, msg, n=3, wait_for_keypress=False): - self.request_context = greenlet.getcurrent() self.message_time = n self.message(msg, schedule_refresh=wait_for_keypress) self.waiting_for_refresh = True self.request_refresh() - self.main_context.switch(msg) + self.request_or_notify_queue.push(msg) - # below really ought to be called from greenlets other than main because + ################################### + # below really ought to be called from threads other than main because # they block def confirm(self, q): """Expected to return True or False, given question prompt q""" - self.request_context = greenlet.getcurrent() self.prompt = q self.in_confirm = True - return self.main_context.switch(q) + self.request_or_notify_queue.put(q) + return self.response_queue.get() def file_prompt(self, s): """Expected to return a file name, given""" - self.request_context = greenlet.getcurrent() self.prompt = s self.in_prompt = True - return self.main_context.switch(s) + self.request_or_notify_queue.put(s) + return self.response_queue.get() diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 30fbbb14e..021cb9da1 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -8,6 +8,7 @@ import subprocess import sys import tempfile +import threading import time import unicodedata from enum import Enum @@ -773,15 +774,15 @@ def process_key_event(self, e): elif e in key_dispatch[self.config.redo_key]: # ctrl-g for redo self.redo() elif e in key_dispatch[self.config.save_key]: # ctrl-s for save - greenlet.greenlet(self.write2file).switch() + self.switch(self.write2file) elif e in key_dispatch[self.config.pastebin_key]: # F8 for pastebin - greenlet.greenlet(self.pastebin).switch() + self.switch(self.pastebin) elif e in key_dispatch[self.config.copy_clipboard_key]: - greenlet.greenlet(self.copy2clipboard).switch() + self.switch(self.copy2clipboard) elif e in key_dispatch[self.config.external_editor_key]: self.send_session_to_external_editor() elif e in key_dispatch[self.config.edit_config_key]: - greenlet.greenlet(self.edit_config).switch() + self.switch(self.edit_config) # TODO add PAD keys hack as in bpython.cli elif e in key_dispatch[self.config.edit_current_block_key]: self.send_current_block_to_external_editor() @@ -792,6 +793,14 @@ def process_key_event(self, e): else: self.add_normal_character(e) + def switch(self, task): + """Runs task in another thread""" + t = threading.Thread(target=task) + t.daemon = True + logging.debug('starting task thread') + t.start() + self.interact.wait_for_request_or_notify() + def get_last_word(self): previous_word = _last_word(self.rl_history.entry) @@ -1849,7 +1858,7 @@ def prompt_for_undo(): if n > 0: self.request_undo(n=n) - greenlet.greenlet(prompt_for_undo).switch() + self.switch(prompt_for_undo) def redo(self): if self.redo_stack: diff --git a/doc/sphinx/source/contributing.rst b/doc/sphinx/source/contributing.rst index 84fb2da55..d28d450f2 100644 --- a/doc/sphinx/source/contributing.rst +++ b/doc/sphinx/source/contributing.rst @@ -60,7 +60,7 @@ Next install your development copy of bpython and its dependencies: .. code-block:: bash - $ sudp apt install python3-greenlet python3-pygments python3-requests + $ sudp apt install python3-pygments python3-requests $ sudo apt install python3-watchdog python3-urwid $ sudo apt install python3-sphinx python3-pytest From 25766566581adc9013ddb6ecaf5f68ecb39f3042 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Wed, 6 Oct 2021 21:02:15 -0700 Subject: [PATCH 2/3] trying to finish --- bpython/curtsiesfrontend/coderunner.py | 16 ++++++++-------- bpython/curtsiesfrontend/interaction.py | 5 +++-- bpython/curtsiesfrontend/repl.py | 4 ++-- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/bpython/curtsiesfrontend/coderunner.py b/bpython/curtsiesfrontend/coderunner.py index f4a6ee169..022a03c02 100644 --- a/bpython/curtsiesfrontend/coderunner.py +++ b/bpython/curtsiesfrontend/coderunner.py @@ -57,14 +57,14 @@ class CodeRunner: """Runs user code in an interpreter. Running code requests a refresh by calling - request_from_main_context(force_refresh=True), which + 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_context. + by calling request_from_main_thread. When this are called, the running source code cedes control, and the current run_code() method call returns. @@ -98,7 +98,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_context = False # TODO rename context to thread + self.sigint_happened_in_main_thread = False # TODO rename context to thread self.orig_sigint_handler = None @property @@ -142,8 +142,8 @@ def run_code(self, for_code=None): self.code_is_waiting = False if is_main_thread(): signal.signal(signal.SIGINT, self.sigint_handler) - if self.sigint_happened_in_main_context: - self.sigint_happened_in_main_context = False + if self.sigint_happened_in_main_thread: + self.sigint_happened_in_main_thread = False self.responses_for_code_thread.put(SigintHappened) else: self.responses_for_code_thread.put(for_code) @@ -180,7 +180,7 @@ def sigint_handler(self, *args): "sigint while fulfilling code request sigint handler " "running!" ) - self.sigint_happened_in_main_context = True + self.sigint_happened_in_main_thread = True def _blocking_run_code(self): try: @@ -192,7 +192,7 @@ def _blocking_run_code(self): if unfinished else Done()) - def request_from_main_context(self, force_refresh=False): + def request_from_main_thread(self, force_refresh=False): """Return the argument passed in to .run_code(for_code) Nothing means calls to run_code must be... ??? @@ -223,7 +223,7 @@ def __init__(self, coderunner, on_write, real_fileobj): def write(self, s, *args, **kwargs): self.on_write(s, *args, **kwargs) - return self.coderunner.request_from_main_context(force_refresh=True) + return self.coderunner.request_from_main_thread(force_refresh=True) # Some applications which use curses require that sys.stdout # have a method called fileno. One example is pwntools. This diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index ee6608a80..ebed9f314 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -83,13 +83,14 @@ def process_event(self, e): assert self.in_prompt or self.in_confirm or self.waiting_for_refresh if isinstance(e, RefreshRequestEvent): self.waiting_for_refresh = False - self.request_context.switch() + self.request_or_notify_queue.put(None) + self.response_queue.get() elif isinstance(e, events.PasteEvent): for ee in e.events: # strip control seq self.add_normal_character(ee if len(ee) == 1 else ee[-1]) elif e == "" or isinstance(e, events.SigIntEvent): - self.request_context.switch(False) + self.request_queue.put(False) self.escape() elif e in edit_keys: self.cursor_offset_in_line, self._current_line = edit_keys[e]( diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 021cb9da1..a0409d5fa 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -112,7 +112,7 @@ def process_event(self, e): self.cursor_offset, self.current_line ) elif isinstance(e, events.SigIntEvent): - self.coderunner.sigint_happened_in_main_context = True + self.coderunner.sigint_happened_in_main_thread = True self.has_focus = False self.current_line = "" self.cursor_offset = 0 @@ -164,7 +164,7 @@ def add_input_character(self, e): def readline(self): self.has_focus = True self.repl.send_to_stdin(self.current_line) - value = self.coderunner.request_from_main_context() + value = self.coderunner.request_from_main_thread() self.readline_results.append(value) return value From 06a55384f02f04fc448b82e9baffe53b48aa8814 Mon Sep 17 00:00:00 2001 From: Thomas Ballinger Date: Wed, 6 Oct 2021 23:49:33 -0700 Subject: [PATCH 3/3] Improve CodeRunner documentation. --- bpython/curtsiesfrontend/coderunner.py | 61 +++++++++++++------------ bpython/curtsiesfrontend/interaction.py | 4 +- bpython/curtsiesfrontend/repl.py | 2 +- 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/bpython/curtsiesfrontend/coderunner.py b/bpython/curtsiesfrontend/coderunner.py index 022a03c02..730b3de00 100644 --- a/bpython/curtsiesfrontend/coderunner.py +++ b/bpython/curtsiesfrontend/coderunner.py @@ -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. """ @@ -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 @@ -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 @@ -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) @@ -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): @@ -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) diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index ebed9f314..a0c406ee8 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -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 diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index a0409d5fa..65ab07dd0 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -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() pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy