circup 2.1.2__py3-none-any.whl → 2.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- circup/backends.py +51 -19
- circup/command_utils.py +165 -11
- circup/commands.py +5 -25
- {circup-2.1.2.dist-info → circup-2.2.0.dist-info}/METADATA +3 -3
- {circup-2.1.2.dist-info → circup-2.2.0.dist-info}/RECORD +9 -9
- {circup-2.1.2.dist-info → circup-2.2.0.dist-info}/WHEEL +1 -1
- {circup-2.1.2.dist-info → circup-2.2.0.dist-info}/entry_points.txt +0 -0
- {circup-2.1.2.dist-info → circup-2.2.0.dist-info/licenses}/LICENSE +0 -0
- {circup-2.1.2.dist-info → circup-2.2.0.dist-info}/top_level.txt +0 -0
circup/backends.py
CHANGED
|
@@ -233,6 +233,12 @@ class Backend:
|
|
|
233
233
|
"""
|
|
234
234
|
raise NotImplementedError
|
|
235
235
|
|
|
236
|
+
def get_file_content(self, target_file):
|
|
237
|
+
"""
|
|
238
|
+
To be overridden by subclass
|
|
239
|
+
"""
|
|
240
|
+
raise NotImplementedError
|
|
241
|
+
|
|
236
242
|
def get_free_space(self):
|
|
237
243
|
"""
|
|
238
244
|
To be overridden by subclass
|
|
@@ -618,6 +624,20 @@ class WebBackend(Backend):
|
|
|
618
624
|
f"Downloaded File: {os.path.join(location_to_paste, file_name)}"
|
|
619
625
|
)
|
|
620
626
|
|
|
627
|
+
def get_file_content(self, target_file):
|
|
628
|
+
"""
|
|
629
|
+
Get the content of a file from the MCU drive
|
|
630
|
+
:param target_file: The file on the MCU to download
|
|
631
|
+
:return:
|
|
632
|
+
"""
|
|
633
|
+
auth = HTTPBasicAuth("", self.password)
|
|
634
|
+
with self.session.get(
|
|
635
|
+
self.FS_URL + target_file, timeout=self.timeout, auth=auth
|
|
636
|
+
) as r:
|
|
637
|
+
if r.status_code == 404:
|
|
638
|
+
return None
|
|
639
|
+
return r.content # .decode("utf8")
|
|
640
|
+
|
|
621
641
|
def install_module_mpy(self, bundle, metadata):
|
|
622
642
|
"""
|
|
623
643
|
:param bundle library bundle.
|
|
@@ -655,19 +675,6 @@ class WebBackend(Backend):
|
|
|
655
675
|
else:
|
|
656
676
|
self.install_file_http(source_path, location=location)
|
|
657
677
|
|
|
658
|
-
def get_auto_file_path(self, auto_file_path):
|
|
659
|
-
"""
|
|
660
|
-
Make a local temp copy of the --auto file from the device.
|
|
661
|
-
Returns the path to the local copy.
|
|
662
|
-
"""
|
|
663
|
-
url = auto_file_path
|
|
664
|
-
auth = HTTPBasicAuth("", self.password)
|
|
665
|
-
with self.session.get(url, auth=auth, timeout=self.timeout) as r:
|
|
666
|
-
r.raise_for_status()
|
|
667
|
-
with open(LOCAL_CODE_PY_COPY, "w", encoding="utf-8") as f:
|
|
668
|
-
f.write(r.text)
|
|
669
|
-
return LOCAL_CODE_PY_COPY
|
|
670
|
-
|
|
671
678
|
def uninstall(self, device_path, module_path):
|
|
672
679
|
"""
|
|
673
680
|
Uninstall given module on device using REST API.
|
|
@@ -958,12 +965,6 @@ class DiskBackend(Backend):
|
|
|
958
965
|
# Copy file.
|
|
959
966
|
shutil.copyfile(source_path, target_path)
|
|
960
967
|
|
|
961
|
-
def get_auto_file_path(self, auto_file_path):
|
|
962
|
-
"""
|
|
963
|
-
Returns the path on the device to the file to be read for --auto.
|
|
964
|
-
"""
|
|
965
|
-
return auto_file_path
|
|
966
|
-
|
|
967
968
|
def uninstall(self, device_path, module_path):
|
|
968
969
|
"""
|
|
969
970
|
Uninstall module using local file system.
|
|
@@ -1014,6 +1015,18 @@ class DiskBackend(Backend):
|
|
|
1014
1015
|
"""
|
|
1015
1016
|
return os.path.join(self.device_location, filename)
|
|
1016
1017
|
|
|
1018
|
+
def get_file_content(self, target_file):
|
|
1019
|
+
"""
|
|
1020
|
+
Get the content of a file from the MCU drive
|
|
1021
|
+
:param target_file: The file on the MCU to download
|
|
1022
|
+
:return:
|
|
1023
|
+
"""
|
|
1024
|
+
file_path = self.get_file_path(target_file)
|
|
1025
|
+
if os.path.exists(file_path):
|
|
1026
|
+
with open(file_path, "rb") as file:
|
|
1027
|
+
return file.read()
|
|
1028
|
+
return None
|
|
1029
|
+
|
|
1017
1030
|
def is_device_present(self):
|
|
1018
1031
|
"""
|
|
1019
1032
|
returns True if the device is currently connected
|
|
@@ -1027,3 +1040,22 @@ class DiskBackend(Backend):
|
|
|
1027
1040
|
# pylint: disable=unused-variable
|
|
1028
1041
|
_, total, free = shutil.disk_usage(self.device_location)
|
|
1029
1042
|
return free
|
|
1043
|
+
|
|
1044
|
+
def list_dir(self, dirpath):
|
|
1045
|
+
"""
|
|
1046
|
+
Returns the list of files located in the given dirpath.
|
|
1047
|
+
"""
|
|
1048
|
+
files_list = []
|
|
1049
|
+
files = os.listdir(os.path.join(self.device_location, dirpath))
|
|
1050
|
+
for file_name in files:
|
|
1051
|
+
file = os.path.join(self.device_location, dirpath, file_name)
|
|
1052
|
+
stat = os.stat(file)
|
|
1053
|
+
files_list.append(
|
|
1054
|
+
{
|
|
1055
|
+
"name": file_name,
|
|
1056
|
+
"directory": os.path.isdir(file),
|
|
1057
|
+
"modified_ns": stat.st_mtime_ns,
|
|
1058
|
+
"file_size": stat.st_size,
|
|
1059
|
+
}
|
|
1060
|
+
)
|
|
1061
|
+
return files_list
|
circup/command_utils.py
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
Functions called from commands in order to provide behaviors and return information.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import ast
|
|
8
9
|
import ctypes
|
|
9
10
|
import glob
|
|
10
11
|
import os
|
|
@@ -16,7 +17,6 @@ import zipfile
|
|
|
16
17
|
import json
|
|
17
18
|
import re
|
|
18
19
|
import toml
|
|
19
|
-
import findimports
|
|
20
20
|
import requests
|
|
21
21
|
import click
|
|
22
22
|
|
|
@@ -41,6 +41,25 @@ WARNING_IGNORE_MODULES = (
|
|
|
41
41
|
"circuitpython-typing",
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
+
CODE_FILES = [
|
|
45
|
+
"code.txt",
|
|
46
|
+
"code.py",
|
|
47
|
+
"main.py",
|
|
48
|
+
"main.txt",
|
|
49
|
+
"code.txt.py",
|
|
50
|
+
"code.py.txt",
|
|
51
|
+
"code.txt.txt",
|
|
52
|
+
"code.py.py",
|
|
53
|
+
"main.txt.py",
|
|
54
|
+
"main.py.txt",
|
|
55
|
+
"main.txt.txt",
|
|
56
|
+
"main.py.py",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class CodeParsingException(Exception):
|
|
61
|
+
"""Exception thrown when parsing code with ast fails"""
|
|
62
|
+
|
|
44
63
|
|
|
45
64
|
def clean_library_name(assumed_library_name):
|
|
46
65
|
"""
|
|
@@ -605,23 +624,158 @@ def tags_data_save_tag(key, tag):
|
|
|
605
624
|
json.dump(tags_data, data)
|
|
606
625
|
|
|
607
626
|
|
|
608
|
-
def
|
|
627
|
+
def imports_from_code(full_content):
|
|
609
628
|
"""
|
|
610
629
|
Parse the given code.py file and return the imported libraries
|
|
630
|
+
Note that it's impossible at that level to differentiate between
|
|
631
|
+
import module.property and import module.submodule, so we try both
|
|
611
632
|
|
|
612
|
-
:param str
|
|
633
|
+
:param str full_content: Code to read imports from
|
|
634
|
+
:param str module_name: Name of the module the code is from
|
|
613
635
|
:return: sequence of library names
|
|
614
636
|
"""
|
|
615
|
-
# pylint: disable=
|
|
637
|
+
# pylint: disable=too-many-branches
|
|
616
638
|
try:
|
|
617
|
-
|
|
618
|
-
except
|
|
619
|
-
|
|
620
|
-
|
|
639
|
+
par = ast.parse(full_content)
|
|
640
|
+
except (SyntaxError, ValueError) as err:
|
|
641
|
+
raise CodeParsingException(err) from err
|
|
642
|
+
|
|
643
|
+
imports = set()
|
|
644
|
+
for thing in ast.walk(par):
|
|
645
|
+
# import module and import module.submodule
|
|
646
|
+
if isinstance(thing, ast.Import):
|
|
647
|
+
for alias in thing.names:
|
|
648
|
+
imports.add(alias.name)
|
|
649
|
+
# from x import y
|
|
650
|
+
if isinstance(thing, ast.ImportFrom):
|
|
651
|
+
if thing.module:
|
|
652
|
+
# from [.][.]module import names
|
|
653
|
+
module = ("." * thing.level) + thing.module
|
|
654
|
+
imports.add(module)
|
|
655
|
+
for alias in thing.names:
|
|
656
|
+
imports.add(".".join([module, alias.name]))
|
|
657
|
+
else:
|
|
658
|
+
# from . import names
|
|
659
|
+
for alias in thing.names:
|
|
660
|
+
imports.add(alias.name)
|
|
661
|
+
|
|
662
|
+
# import parent modules (in practice it's the __init__.py)
|
|
663
|
+
for name in list(imports):
|
|
664
|
+
if "*" in name:
|
|
665
|
+
imports.remove(name)
|
|
666
|
+
continue
|
|
667
|
+
names = name.split(".")
|
|
668
|
+
for i in range(len(names)):
|
|
669
|
+
module = ".".join(names[: i + 1])
|
|
670
|
+
if module:
|
|
671
|
+
imports.add(module)
|
|
672
|
+
|
|
673
|
+
return sorted(imports)
|
|
674
|
+
|
|
675
|
+
|
|
676
|
+
def get_all_imports(
|
|
677
|
+
backend, auto_file_content, mod_names, current_module, visited=None
|
|
678
|
+
):
|
|
679
|
+
"""
|
|
680
|
+
Recursively retrieve imports from files on the backend
|
|
681
|
+
|
|
682
|
+
:param Backend backend: The current backend object
|
|
683
|
+
:param str auto_file_content: Content of the python file to analyse
|
|
684
|
+
:param list mod_names: Lits of supported bundle mod names
|
|
685
|
+
:param str current_module: Name of the call context module if recursive call
|
|
686
|
+
:param set visited: Modules previously visited
|
|
687
|
+
:return: sequence of library names
|
|
688
|
+
"""
|
|
689
|
+
if visited is None:
|
|
690
|
+
visited = set()
|
|
691
|
+
visited.add(current_module)
|
|
692
|
+
|
|
693
|
+
requested_installs = []
|
|
694
|
+
try:
|
|
695
|
+
imports = imports_from_code(auto_file_content)
|
|
696
|
+
except CodeParsingException as err:
|
|
697
|
+
click.secho(f"Error parsing {current_module}:\n {err}", fg="red")
|
|
621
698
|
sys.exit(2)
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
699
|
+
|
|
700
|
+
for install in imports:
|
|
701
|
+
if install in visited:
|
|
702
|
+
continue
|
|
703
|
+
if install in mod_names:
|
|
704
|
+
requested_installs.append(install)
|
|
705
|
+
else:
|
|
706
|
+
# relative module paths
|
|
707
|
+
if install.startswith(".."):
|
|
708
|
+
install_module = ".".join(current_module.split(".")[:-2])
|
|
709
|
+
install_module = install_module + "." + install[2:]
|
|
710
|
+
elif install.startswith("."):
|
|
711
|
+
install_module = ".".join(current_module.split(".")[:-1])
|
|
712
|
+
install_module = install_module + "." + install[1:]
|
|
713
|
+
else:
|
|
714
|
+
install_module = install
|
|
715
|
+
# possible files for the module: .py or __init__.py (if directory)
|
|
716
|
+
file_name = os.path.join(*install_module.split(".")) + ".py"
|
|
717
|
+
exists = backend.file_exists(file_name)
|
|
718
|
+
if not exists:
|
|
719
|
+
file_name = os.path.join(*install_module.split("."), "__init__.py")
|
|
720
|
+
exists = backend.file_exists(file_name)
|
|
721
|
+
if not exists:
|
|
722
|
+
continue
|
|
723
|
+
install_module += ".__init__"
|
|
724
|
+
# get the content and parse it recursively
|
|
725
|
+
auto_file_content = backend.get_file_content(file_name)
|
|
726
|
+
if auto_file_content:
|
|
727
|
+
sub_imports = get_all_imports(
|
|
728
|
+
backend, auto_file_content, mod_names, install_module, visited
|
|
729
|
+
)
|
|
730
|
+
requested_installs.extend(sub_imports)
|
|
731
|
+
|
|
732
|
+
return sorted(requested_installs)
|
|
733
|
+
# [r for r in requested_installs if r in mod_names]
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def libraries_from_auto_file(backend, auto_file, mod_names):
|
|
737
|
+
"""
|
|
738
|
+
Parse the input auto_file path and/or use the workflow to find the most
|
|
739
|
+
appropriate code.py script. Then return the list of imports
|
|
740
|
+
|
|
741
|
+
:param Backend backend: The current backend object
|
|
742
|
+
:param str auto_file: Path of the candidate auto file or None
|
|
743
|
+
:return: sequence of library names
|
|
744
|
+
"""
|
|
745
|
+
# find the current main file based on Circuitpython's rules
|
|
746
|
+
if auto_file is None:
|
|
747
|
+
root_files = [
|
|
748
|
+
file["name"] for file in backend.list_dir("") if not file["directory"]
|
|
749
|
+
]
|
|
750
|
+
for main_file in CODE_FILES:
|
|
751
|
+
if main_file in root_files:
|
|
752
|
+
auto_file = main_file
|
|
753
|
+
break
|
|
754
|
+
# still no code file found
|
|
755
|
+
if auto_file is None:
|
|
756
|
+
click.secho(
|
|
757
|
+
"No default code file found. See valid names:\n"
|
|
758
|
+
"https://docs.circuitpython.org/en/latest/README.html#behavior",
|
|
759
|
+
fg="red",
|
|
760
|
+
)
|
|
761
|
+
sys.exit(1)
|
|
762
|
+
|
|
763
|
+
# pass a local file with "./" or "../"
|
|
764
|
+
is_relative = auto_file.split(os.sep)[0] in [os.path.curdir, os.path.pardir]
|
|
765
|
+
if os.path.isabs(auto_file) or is_relative:
|
|
766
|
+
with open(auto_file, "r", encoding="UTF8") as fp:
|
|
767
|
+
auto_file_content = fp.read()
|
|
768
|
+
else:
|
|
769
|
+
auto_file_content = backend.get_file_content(auto_file)
|
|
770
|
+
|
|
771
|
+
if auto_file_content is None:
|
|
772
|
+
click.secho(f"Auto file not found: {auto_file}", fg="red")
|
|
773
|
+
sys.exit(1)
|
|
774
|
+
|
|
775
|
+
# from file name to module name (in case it's in a subpackage)
|
|
776
|
+
click.secho(f"Finding imports from: {auto_file}", fg="green")
|
|
777
|
+
current_module = auto_file.rstrip(".py").replace(os.path.sep, ".")
|
|
778
|
+
return get_all_imports(backend, auto_file_content, mod_names, current_module)
|
|
625
779
|
|
|
626
780
|
|
|
627
781
|
def get_device_path(host, port, password, path):
|
circup/commands.py
CHANGED
|
@@ -34,7 +34,7 @@ from circup.command_utils import (
|
|
|
34
34
|
completion_for_install,
|
|
35
35
|
get_bundle_versions,
|
|
36
36
|
libraries_from_requirements,
|
|
37
|
-
|
|
37
|
+
libraries_from_auto_file,
|
|
38
38
|
get_dependencies,
|
|
39
39
|
get_bundles_local_dict,
|
|
40
40
|
save_local_bundles,
|
|
@@ -342,32 +342,12 @@ def install(
|
|
|
342
342
|
requirements_txt = rfile.read()
|
|
343
343
|
requested_installs = libraries_from_requirements(requirements_txt)
|
|
344
344
|
elif auto or auto_file:
|
|
345
|
-
|
|
346
|
-
auto_file
|
|
347
|
-
|
|
348
|
-
# pass a local file with "./" or "../"
|
|
349
|
-
is_relative = not isinstance(ctx.obj["backend"], WebBackend) or auto_file.split(
|
|
350
|
-
os.sep
|
|
351
|
-
)[0] in [os.path.curdir, os.path.pardir]
|
|
352
|
-
if not os.path.isabs(auto_file) and not is_relative:
|
|
353
|
-
auto_file = ctx.obj["backend"].get_file_path(auto_file or "code.py")
|
|
354
|
-
|
|
355
|
-
auto_file_path = ctx.obj["backend"].get_auto_file_path(auto_file)
|
|
356
|
-
print(f"Auto file path: {auto_file_path}")
|
|
357
|
-
if not os.path.isfile(auto_file_path):
|
|
358
|
-
# fell through to here when run from random folder on windows - ask backend.
|
|
359
|
-
new_auto_file = ctx.obj["backend"].get_file_path(auto_file)
|
|
360
|
-
if os.path.isfile(new_auto_file):
|
|
361
|
-
auto_file = new_auto_file
|
|
362
|
-
auto_file_path = ctx.obj["backend"].get_auto_file_path(auto_file)
|
|
363
|
-
print(f"Auto file path: {auto_file_path}")
|
|
364
|
-
else:
|
|
365
|
-
click.secho(f"Auto file not found: {auto_file}", fg="red")
|
|
366
|
-
sys.exit(1)
|
|
367
|
-
|
|
368
|
-
requested_installs = libraries_from_code_py(auto_file_path, mod_names)
|
|
345
|
+
requested_installs = libraries_from_auto_file(
|
|
346
|
+
ctx.obj["backend"], auto_file, mod_names
|
|
347
|
+
)
|
|
369
348
|
else:
|
|
370
349
|
requested_installs = modules
|
|
350
|
+
|
|
371
351
|
requested_installs = sorted(set(requested_installs))
|
|
372
352
|
click.echo(f"Searching for dependencies for: {requested_installs}")
|
|
373
353
|
to_install = get_dependencies(requested_installs, mod_names=mod_names)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: circup
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: A tool to manage/update libraries on CircuitPython devices.
|
|
5
5
|
Author-email: Adafruit Industries <circuitpython@adafruit.com>
|
|
6
6
|
License: MIT License
|
|
@@ -47,7 +47,6 @@ Description-Content-Type: text/x-rst
|
|
|
47
47
|
License-File: LICENSE
|
|
48
48
|
Requires-Dist: appdirs
|
|
49
49
|
Requires-Dist: Click
|
|
50
|
-
Requires-Dist: findimports
|
|
51
50
|
Requires-Dist: requests
|
|
52
51
|
Requires-Dist: semver
|
|
53
52
|
Requires-Dist: toml
|
|
@@ -57,6 +56,7 @@ Requires-Dist: pytest; extra == "optional"
|
|
|
57
56
|
Requires-Dist: pytest-cov; extra == "optional"
|
|
58
57
|
Requires-Dist: pytest-faulthandler; extra == "optional"
|
|
59
58
|
Requires-Dist: pytest-random-order; extra == "optional"
|
|
59
|
+
Dynamic: license-file
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
Circup
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
circup/__init__.py,sha256=9A98U3DyA14tm-7_d88fGSlLfEKz9T_85t8kXkErqRg,662
|
|
2
|
-
circup/backends.py,sha256=
|
|
2
|
+
circup/backends.py,sha256=g9Q9xCGZidwsEDL2Ga2cm50YYB54IiqlKUPcxj-pWZA,40008
|
|
3
3
|
circup/bundle.py,sha256=FEP4F470aJtwmm8jgTM3DgR3dj5SVwbX1tbyIRKVHn8,5327
|
|
4
|
-
circup/command_utils.py,sha256=
|
|
5
|
-
circup/commands.py,sha256=
|
|
4
|
+
circup/command_utils.py,sha256=8CaCy34EQcrDwcnXbyRvTZ-aUU-yCJ0Om8V1QuRNSbA,29392
|
|
5
|
+
circup/commands.py,sha256=1UQ8EONlcEFQsytPO3LAHLcNHZIO0Fi5XCU47quzt08,27436
|
|
6
6
|
circup/logging.py,sha256=hu4v8ljkXo8ru-cqs0W3PU-xEVvTO_qqMKDJM18OXbQ,1115
|
|
7
7
|
circup/module.py,sha256=33_kdy5BZn6COyIjAFZMpw00rTtPiryQZWFXQkMF8FY,7435
|
|
8
8
|
circup/shared.py,sha256=qD7eN2KJcLsfhi8xMqWiNaE_LT0ueOG_G15_5m47gMo,8923
|
|
@@ -12,9 +12,9 @@ circup/wwshell/README.rst,sha256=M_jFP0hwOcngF0RdosdeqmVOISNcPzyjTW3duzIu9A8,361
|
|
|
12
12
|
circup/wwshell/README.rst.license,sha256=GhA0SoZGP7CReDam-JJk_UtIQIpQaZWQFzR26YSuMm4,107
|
|
13
13
|
circup/wwshell/__init__.py,sha256=CAPZiYrouWboyPx4KiWLBG_vf_n0MmArGqaFyTXGKWk,398
|
|
14
14
|
circup/wwshell/commands.py,sha256=-I5l7XeoDmvWWuZg5wHdt9qe__SBQ1EGmKwCDTBMeus,7454
|
|
15
|
-
circup-2.
|
|
16
|
-
circup-2.
|
|
17
|
-
circup-2.
|
|
18
|
-
circup-2.
|
|
19
|
-
circup-2.
|
|
20
|
-
circup-2.
|
|
15
|
+
circup-2.2.0.dist-info/licenses/LICENSE,sha256=bVlIMmSL_pqLCqae4hzixy9pYXD808IbgsMoQXTNLBk,1076
|
|
16
|
+
circup-2.2.0.dist-info/METADATA,sha256=CAUjxIk4DKD_CXJ28nMv9q4WybfiFZAn7FJkkG61Bg4,13617
|
|
17
|
+
circup-2.2.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
|
18
|
+
circup-2.2.0.dist-info/entry_points.txt,sha256=FjTmwYD_ApgLRGifUrr_Ui1voW6fEzodQc3DKNzoAPc,69
|
|
19
|
+
circup-2.2.0.dist-info/top_level.txt,sha256=Qx6E0eZgSBE10ciNKsLZx8-TTy_9fEVZh7NLmn24KcU,7
|
|
20
|
+
circup-2.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|