diff --git a/circup/backends.py b/circup/backends.py index 56758f9..914e0b5 100644 --- a/circup/backends.py +++ b/circup/backends.py @@ -90,7 +90,13 @@ class Backend: """ raise NotImplementedError - # pylint: disable=too-many-locals,too-many-branches,too-many-arguments,too-many-nested-blocks + def copy_file(self, target_file, location_to_paste): + """Paste a copy of the specified file at the location given + To be overridden by subclass + """ + raise NotImplementedError + + # pylint: disable=too-many-locals,too-many-branches,too-many-arguments,too-many-nested-blocks,too-many-statements def install_module( self, device_path, device_modules, name, pyext, mod_names, upgrade=False ): # pragma: no cover @@ -109,9 +115,19 @@ class Backend: with get_bundle_versions() :param bool upgrade: Upgrade the specified modules if they're already installed. """ + local_path = None + if os.path.exists(name): + # local file exists use that. + local_path = name + name = local_path.split(os.path.sep)[-1] + name = name.replace(".py", "").replace(".mpy", "") + click.echo(f"Installing from local path: {local_path}") + if not name: click.echo("No module name(s) provided.") - elif name in mod_names: + return + if name in mod_names or local_path is not None: + # Grab device modules to check if module already installed if name in device_modules: if not upgrade: @@ -129,14 +145,19 @@ class Backend: module_path = _metadata["path"] self.uninstall(device_path, module_path) + new_module_size = 0 library_path = ( os.path.join(device_path, self.LIB_DIR_PATH) if not isinstance(self, WebBackend) else urljoin(device_path, self.LIB_DIR_PATH) ) - metadata = mod_names[name] - bundle = metadata["bundle"] - bundle.size = os.path.getsize(metadata["path"]) + if local_path is None: + metadata = mod_names[name] + bundle = metadata["bundle"] + else: + metadata = {"path": local_path} + + new_module_size = os.path.getsize(metadata["path"]) if os.path.isdir(metadata["path"]): # pylint: disable=unused-variable for dirpath, dirnames, filenames in os.walk(metadata["path"]): @@ -144,7 +165,7 @@ class Backend: fp = os.path.join(dirpath, f) try: if not os.path.islink(fp): # Ignore symbolic links - bundle.size += os.path.getsize(fp) + new_module_size += os.path.getsize(fp) else: self.logger.warning( f"Skipping symbolic link in space calculation: {fp}" @@ -154,27 +175,29 @@ class Backend: f"Error: {e} - Skipping file in space calculation: {fp}" ) - if self.get_free_space() < bundle.size: + if self.get_free_space() < new_module_size: self.logger.error( f"Aborted installing module {name} - " - f"not enough free space ({bundle.size} < {self.get_free_space()})" + f"not enough free space ({new_module_size} < {self.get_free_space()})" ) click.secho( f"Aborted installing module {name} - " - f"not enough free space ({bundle.size} < {self.get_free_space()})", + f"not enough free space ({new_module_size} < {self.get_free_space()})", fg="red", ) return # Create the library directory first. self._create_library_directory(device_path, library_path) - - if pyext: - # Use Python source for module. - self.install_module_py(metadata) + if local_path is None: + if pyext: + # Use Python source for module. + self.install_module_py(metadata) + else: + # Use pre-compiled mpy modules. + self.install_module_mpy(bundle, metadata) else: - # Use pre-compiled mpy modules. - self.install_module_mpy(bundle, metadata) + self.copy_file(metadata["path"], "lib") click.echo("Installed '{}'.".format(name)) else: click.echo("Unknown module named, '{}'.".format(name)) @@ -520,6 +543,17 @@ class WebBackend(Backend): _writeable_error() r.raise_for_status() + def copy_file(self, target_file, location_to_paste): + if os.path.isdir(target_file): + create_directory_url = urljoin( + self.device_location, + "/".join(("fs", location_to_paste, target_file, "")), + ) + self._create_library_directory(self.device_location, create_directory_url) + self.install_dir_http(target_file) + else: + self.install_file_http(target_file) + def install_module_mpy(self, bundle, metadata): """ :param bundle library bundle. @@ -775,6 +809,19 @@ class DiskBackend(Backend): if not os.path.exists(library_path): # pragma: no cover os.makedirs(library_path) + def copy_file(self, target_file, location_to_paste): + target_filename = target_file.split(os.path.sep)[-1] + if os.path.isdir(target_file): + shutil.copytree( + target_file, + os.path.join(self.device_location, location_to_paste, target_filename), + ) + else: + shutil.copyfile( + target_file, + os.path.join(self.device_location, location_to_paste, target_filename), + ) + def install_module_mpy(self, bundle, metadata): """ :param bundle library bundle. diff --git a/circup/command_utils.py b/circup/command_utils.py index d495673..e366736 100644 --- a/circup/command_utils.py +++ b/circup/command_utils.py @@ -6,6 +6,7 @@ Functions called from commands in order to provide behaviors and return informat """ import ctypes +import glob import os from subprocess import check_output @@ -90,6 +91,7 @@ def completion_for_install(ctx, param, incomplete): module_names = {m.replace(".py", "") for m in available_modules} if incomplete: module_names = [name for name in module_names if name.startswith(incomplete)] + module_names.extend(glob.glob(f"{incomplete}*")) return sorted(module_names) @@ -446,6 +448,7 @@ def get_dependencies(*requested_libraries, mod_names, to_install=()): :param list(str) to_install: Modules already selected for installation. :return: tuple of module names to install which we build """ + # pylint: disable=too-many-branches # Internal variables _to_install = to_install _requested_libraries = [] @@ -471,10 +474,14 @@ def get_dependencies(*requested_libraries, mod_names, to_install=()): _requested_libraries.append(canonical_lib_name) except KeyError: if canonical_lib_name not in WARNING_IGNORE_MODULES: - click.secho( - f"WARNING:\n\t{canonical_lib_name} is not a known CircuitPython library.", - fg="yellow", - ) + if os.path.exists(canonical_lib_name): + _requested_libraries.append(canonical_lib_name) + else: + click.secho( + f"WARNING:\n\t{canonical_lib_name} " + f"is not a known CircuitPython library.", + fg="yellow", + ) if not _requested_libraries: # If nothing is requested, we're done @@ -484,16 +491,20 @@ def get_dependencies(*requested_libraries, mod_names, to_install=()): if library not in _to_install: _to_install = _to_install + (library,) # get the requirements.txt from bundle - bundle = mod_names[library]["bundle"] - requirements_txt = bundle.requirements_for(library) - if requirements_txt: - _requested_libraries.extend( - libraries_from_requirements(requirements_txt) - ) + try: + bundle = mod_names[library]["bundle"] + requirements_txt = bundle.requirements_for(library) + if requirements_txt: + _requested_libraries.extend( + libraries_from_requirements(requirements_txt) + ) - circup_dependencies = get_circup_dependencies(bundle, library) - for circup_dependency in circup_dependencies: - _requested_libraries.append(circup_dependency) + circup_dependencies = get_circup_dependencies(bundle, library) + for circup_dependency in circup_dependencies: + _requested_libraries.append(circup_dependency) + except KeyError: + # don't check local file for further dependencies + pass # we've processed this library, remove it from the list _requested_libraries.remove(library) diff --git a/circup/commands.py b/circup/commands.py index ce8280e..179956e 100644 --- a/circup/commands.py +++ b/circup/commands.py @@ -302,7 +302,7 @@ def install( """ Install a named module(s) onto the device. Multiple modules can be installed at once by providing more than one module name, each - separated by a space. + separated by a space. Modules can be from a Bundle or local filepaths. """ # TODO: Ensure there's enough space on the device