diff --git a/mu/logic.py b/mu/logic.py index d4566fb04..3faa58a65 100644 --- a/mu/logic.py +++ b/mu/logic.py @@ -361,6 +361,13 @@ def read_and_decode(filepath): return text, newline +def get_pathname(self): + """ + Returns the pathname of the currently edited file + """ + return self.view.current_tab.path + + def extract_envars(raw): """ Returns a list of environment variables given a string containing diff --git a/mu/modes/circuitpython.py b/mu/modes/circuitpython.py index 7572d7fe0..e5048447c 100644 --- a/mu/modes/circuitpython.py +++ b/mu/modes/circuitpython.py @@ -17,13 +17,16 @@ along with this program. If not, see . """ import os +import time import ctypes import logging +from shutil import copyfile from subprocess import check_output from mu.modes.base import MicroPythonMode from mu.modes.api import ADAFRUIT_APIS, SHARED_APIS from mu.interface.panes import CHARTS from mu.logic import Device +from mu.logic import get_pathname from adafruit_board_toolkit import circuitpython_serial logger = logging.getLogger(__name__) @@ -137,13 +140,22 @@ def actions(self): is a name (also used to identify the icon) , description, and handler. """ buttons = [ + { + "name": "run", + "display_name": _("Run"), + "description": _( + "Save and run your current file on CIRCUITPY." + ), + "handler": self.run, + "shortcut": "CTRL+Shift+R", + }, { "name": "serial", "display_name": _("Serial"), "description": _("Open a serial connection to your device."), "handler": self.toggle_repl, "shortcut": "CTRL+Shift+U", - } + }, ] if CHARTS: buttons.append( @@ -246,11 +258,9 @@ def get_volume_name(disk_name): try: for disk in "ABCDEFGHIJKLMNOPQRSTUVWXYZ": path = "{}:\\".format(disk) - if ( - os.path.exists(path) - and get_volume_name(path) == "CIRCUITPY" - ): - return path + if os.path.exists(path): + if get_volume_name(path) == "CIRCUITPY": + return path finally: ctypes.windll.kernel32.SetErrorMode(old_mode) else: @@ -279,6 +289,68 @@ def get_volume_name(disk_name): self.connected = False return wd + def workspace_dir_cp(self): + """ + Is the file currently being edited located on CIRCUITPY. + """ + return "CIRCUITPY" in str(get_pathname(self)) + + def workspace_cp_avail(self): + """ + Is CIRCUITPY available. + """ + return "CIRCUITPY" in str(self.workspace_dir()) + + def run_circuitpython_lib_copy(self, pathname, dst_dir): + """ + Optionally copy lib files to CIRCUITPY. + """ + lib_dir = os.path.dirname(pathname) + "/lib" + if not os.path.isdir(lib_dir): + return + replace_cnt = 0 + for root, dirs, files in os.walk(lib_dir): + for filename in files: + src_lib = lib_dir + "/" + filename + dst_lib_dir = dst_dir + "/lib" + dst_lib = dst_lib_dir + "/" + filename + if not os.path.exists(dst_lib): + replace_lib = True + else: + src_tm = time.ctime(os.path.getmtime(src_lib)) + dst_tm = time.ctime(os.path.getmtime(dst_lib)) + replace_lib = src_tm > dst_tm + if replace_lib: + if replace_cnt == 0: + if not os.path.exists(dst_lib_dir): + os.makedirs(dst_lib_dir) + copyfile(src_lib, dst_lib) + replace_cnt = replace_cnt + 1 + # let libraries load before copying source main source file + if replace_cnt > 0: + time.sleep(4) + + def run(self, event): + """ + Save the file and copy to CIRCUITPY if not already there and available. + """ + self.editor.save() + + if not self.workspace_dir_cp() and self.workspace_cp_avail(): + pathname = get_pathname(self) + if pathname: + dst_dir = self.workspace_dir() + if pathname.find("/lib/") == -1: + dst = dst_dir + "/code.py" + else: + dst = dst_dir + "/lib/" + os.path.basename(pathname) + + # copy library files on to device if not working on the device + # self.run_circuitpython_lib_copy(pathname, dst_dir) + + # copy edited source file on to device + copyfile(pathname, dst) + def compatible_board(self, port): """Use adafruit_board_toolkit to find out whether a board is running CircuitPython. The toolkit sees if the CDC Interface name is appropriate. diff --git a/tests/modes/test_circuitpython.py b/tests/modes/test_circuitpython.py index 73f9cacbe..6f1c953cc 100644 --- a/tests/modes/test_circuitpython.py +++ b/tests/modes/test_circuitpython.py @@ -24,11 +24,13 @@ def test_circuitpython_mode(): with mock.patch("mu.modes.circuitpython.CHARTS", True): actions = am.actions() - assert len(actions) == 2 - assert actions[0]["name"] == "serial" - assert actions[0]["handler"] == am.toggle_repl - assert actions[1]["name"] == "plotter" - assert actions[1]["handler"] == am.toggle_plotter + assert len(actions) == 3 + assert actions[0]["name"] == "run" + assert actions[0]["handler"] == am.run + assert actions[1]["name"] == "serial" + assert actions[1]["handler"] == am.toggle_repl + assert actions[2]["name"] == "plotter" + assert actions[2]["handler"] == am.toggle_plotter assert "code" not in am.module_names @@ -41,9 +43,11 @@ def test_circuitpython_mode_no_charts(): am = CircuitPythonMode(editor, view) with mock.patch("mu.modes.circuitpython.CHARTS", False): actions = am.actions() - assert len(actions) == 1 - assert actions[0]["name"] == "serial" - assert actions[0]["handler"] == am.toggle_repl + assert len(actions) == 2 + assert actions[0]["name"] == "run" + assert actions[0]["handler"] == am.run + assert actions[1]["name"] == "serial" + assert actions[1]["handler"] == am.toggle_repl def test_workspace_dir_posix_exists(): diff --git a/tests/test_logic.py b/tests/test_logic.py index 8cbabd8aa..d6dfca07a 100644 --- a/tests/test_logic.py +++ b/tests/test_logic.py @@ -449,6 +449,13 @@ def test_get_settings_path(): mock_func.assert_called_once_with("settings.json") +def test_get_pathname(): + mock_func = mock.MagicMock(return_value="CIRCUITPY") + with mock.patch("mu.logic.get_pathname", mock_func): + assert mu.logic.get_pathname() == "CIRCUITPY" + mock_func.assert_called_once_with() + + def test_extract_envars(): """ Given a correct textual representation, get the expected list