From c524ddeb1a4ef98f3dea902aa4139d97291fa296 Mon Sep 17 00:00:00 2001 From: ilia Date: Thu, 16 Nov 2023 21:21:10 +0300 Subject: [PATCH] done turtle thread --- .gitignore | 2 ++ README.md | 4 +-- turtle/README.md | 11 ++++++++ turtle/controls/device.py | 2 +- turtle/equipment/turtle_device.py | 44 +++++++++++++++-------------- turtle/noblocking_turtle_shell.py | 46 +++++++++++++++---------------- 6 files changed, 62 insertions(+), 47 deletions(-) create mode 100644 turtle/README.md diff --git a/.gitignore b/.gitignore index 68bc17f..84917c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.code-workspace + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/README.md b/README.md index bcbe7c0..f4acb38 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,6 @@ extensions = [ - [assignment](https://sciprogcentre.github.io/green-courses/pages/advanced_python/2023/hw/03.html) - [solution](./turtle) -## Turtle Async (4th homework) +## Turtle Thread (4th homework) - [assignment](https://sciprogcentre.github.io/green-courses/pages/advanced_python/2023/hw/04.html) -- [solution](./turtle_async) +- [solution](./turtle) diff --git a/turtle/README.md b/turtle/README.md new file mode 100644 index 0000000..eac55a0 --- /dev/null +++ b/turtle/README.md @@ -0,0 +1,11 @@ +# NonBlocking Turtle Shell +## Problem statement +Create a [non blocking shell](https://sciprogcentre.github.io/green-courses/pages/advanced_python/2023/hw/04.html) for Python Turtle. +## Test +I added two shortcut methods for testing: +- `le` = `execute left 1000` - spinning by 1000$^\circ$ +- `fw` = `execute forward 100` - go forward by 100px + +`le` takes a lot of time. The shell is not locked while turtle is spinning and you +can enter other commands (e.g. `fw`, `le`). The turtle will do them all +step-by-step. \ No newline at end of file diff --git a/turtle/controls/device.py b/turtle/controls/device.py index 6d1b6ea..770d3bd 100644 --- a/turtle/controls/device.py +++ b/turtle/controls/device.py @@ -69,7 +69,7 @@ class SynchronyDevice(Device): @abstractmethod def execute(self, action_name: str, *args, **kwargs): """Execute action `action_name`, using `args` and `kwargs` as action argument.""" - pass + raise DeviceError @abstractmethod def read(self, trait_name: str) -> Any: diff --git a/turtle/equipment/turtle_device.py b/turtle/equipment/turtle_device.py index 27ee251..737dcd3 100644 --- a/turtle/equipment/turtle_device.py +++ b/turtle/equipment/turtle_device.py @@ -1,9 +1,6 @@ from turtle import Turtle from typing import Optional, Collection, Any -from controls.device import SynchronyDevice -from controls.device import TraitDescriptor -from controls.device import ActionDescriptor -from controls.device import DeviceError +from controls.device import * import inspect @@ -14,7 +11,7 @@ class TurtleDevice(SynchronyDevice): super().open() def close(self): - self.turtle.clear() + self.turtle.reset() super().close() def trait_descriptors(self) -> Collection[TraitDescriptor]: @@ -47,8 +44,12 @@ class TurtleDevice(SynchronyDevice): """Execute action `action_name`, using `args` and `kwargs` as action argument.""" action = self.get_descriptors()["actions"].get(action_name) if action: - return getattr(self.turtle, action_name)(*args, **kwargs) - super().execute() + self._state = DeviceLifecycleState.CLOSE + getattr(self.turtle, action_name)(*args, **kwargs) + self._state = DeviceLifecycleState.OPEN + return + else: + raise DeviceError("No such action: `{}`".format(action_name)) def invalidate(self, trait_name: str): """Invalidate logical state of trait `trait_name`""" @@ -60,26 +61,27 @@ class TurtleDevice(SynchronyDevice): descriptors = dict(actions=dict(), traits=dict()) for k, v in self.turtle.__dict__.items(): if not k.startswith("_"): - descriptors["traits"][k] = TraitDescriptor(k, - inspect.getdoc(v), - readable=True, - writable=False) + descriptors["traits"][k] = TraitDescriptor( + k, inspect.getdoc(v), readable=True, writable=False + ) for m_name, member in inspect.getmembers(Turtle): - if m_name.startswith("_"): continue - if m_name.lower() != m_name: continue + if m_name.startswith("_"): + continue + if m_name.lower() != m_name: + continue doc = inspect.getdoc(member) - if doc is None: continue + if doc is None: + continue if not inspect.isfunction(member): - descriptors["traits"][m_name] = TraitDescriptor(m_name, - doc, - readable=True, - writable=False) + descriptors["traits"][m_name] = TraitDescriptor( + m_name, doc, readable=True, writable=False + ) else: sig = inspect.signature(member) params_dict = dict(sig.parameters) - descriptors["actions"][m_name] = ActionDescriptor(m_name, - arguments=params_dict, - info=doc) + descriptors["actions"][m_name] = ActionDescriptor( + m_name, arguments=params_dict, info=doc + ) return descriptors diff --git a/turtle/noblocking_turtle_shell.py b/turtle/noblocking_turtle_shell.py index 035fcd5..45b416b 100644 --- a/turtle/noblocking_turtle_shell.py +++ b/turtle/noblocking_turtle_shell.py @@ -3,7 +3,6 @@ import threading from queue import Queue -import turtle from equipment.turtle_device import TurtleDevice @@ -12,6 +11,7 @@ class TurtleDeviceThread(threading.Thread): def __init__(self): super().__init__() self.device = TurtleDevice() + self.queue = Queue[tuple]() self.device.open() @@ -20,6 +20,9 @@ class TurtleDeviceThread(threading.Thread): def run(self): while True: action, args, kwargs = self.queue.get() + if action == "exit": + self.queue.task_done() + break self.device.execute(action, *args, **kwargs) self.queue.task_done() @@ -41,33 +44,30 @@ class NoBlockingTurtleShell(cmd.Cmd): return self.turtle_thread.device def do_execute(self, arg): - print(arg) - command, number = tuple(arg.split()) - assert number.isdecimal() - self.turtle_thread.add_task(command, int(number)) + command_and_args = arg.split() + if len(command_and_args) == 1: + self.turtle_thread.add_task(command) + return + command = command_and_args[0] + args = tuple(int(c) if c.isdecimal() else c for c in command_and_args[1:]) + self.turtle_thread.add_task(command, *args) + + def do_fw(self, arg): + self.do_execute("forward 100") + + def do_le(self, arg): + self.do_execute("left 1000") def do_exit(self, arg): - self.turtle_device.close() - self.close() + self.turtle_thread.add_task("exit") + return True - def precmd(self, line): - line = line.lower() - if self.file and "playback" not in line: - print(line, file=self.file) - return line - - def close(self): - if self.file: - self.file.close() - self.file = None - - -import tkinter if __name__ == "__main__": turtle_thread = TurtleDeviceThread() # TODO(Homework 4: Correct start thread) turtle_thread.daemon = True - turtle_thread.start() - NoBlockingTurtleShell(turtle_thread).cmdloop() - turtle_thread.join() + thread_shell = threading.Thread(target=NoBlockingTurtleShell(turtle_thread).cmdloop) + thread_shell.start() + turtle_thread.run() + thread_shell.join()