circup 2.3.0__tar.gz → 3.0.0__tar.gz
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-2.3.0/circup.egg-info → circup-3.0.0}/PKG-INFO +2 -2
- {circup-2.3.0 → circup-3.0.0}/circup/backends.py +11 -14
- {circup-2.3.0 → circup-3.0.0}/circup/bundle.py +98 -39
- {circup-2.3.0 → circup-3.0.0}/circup/command_utils.py +106 -52
- {circup-2.3.0 → circup-3.0.0}/circup/commands.py +106 -44
- {circup-2.3.0 → circup-3.0.0}/circup/lazy_metadata.py +2 -1
- {circup-2.3.0 → circup-3.0.0}/circup/module.py +1 -9
- {circup-2.3.0 → circup-3.0.0}/circup/shared.py +5 -2
- {circup-2.3.0 → circup-3.0.0}/circup/wwshell/commands.py +2 -2
- {circup-2.3.0 → circup-3.0.0/circup.egg-info}/PKG-INFO +2 -2
- {circup-2.3.0 → circup-3.0.0}/pyproject.toml +1 -1
- {circup-2.3.0 → circup-3.0.0}/tests/test_circup.py +43 -17
- {circup-2.3.0 → circup-3.0.0}/.github/ISSUE_TEMPLATE.md +0 -0
- {circup-2.3.0 → circup-3.0.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {circup-2.3.0 → circup-3.0.0}/.github/workflows/build.yml +0 -0
- {circup-2.3.0 → circup-3.0.0}/.github/workflows/release.yml +0 -0
- {circup-2.3.0 → circup-3.0.0}/.gitignore +0 -0
- {circup-2.3.0 → circup-3.0.0}/.isort.cfg +0 -0
- {circup-2.3.0 → circup-3.0.0}/.pre-commit-config.yaml +0 -0
- {circup-2.3.0 → circup-3.0.0}/.pylintrc +0 -0
- {circup-2.3.0 → circup-3.0.0}/CODE_OF_CONDUCT.rst +0 -0
- {circup-2.3.0 → circup-3.0.0}/CODE_OF_CONDUCT.rst.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/CONTRIBUTING.rst +0 -0
- {circup-2.3.0 → circup-3.0.0}/CONTRIBUTING.rst.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/LICENSE +0 -0
- {circup-2.3.0 → circup-3.0.0}/LICENSES/CC-BY-4.0.txt +0 -0
- {circup-2.3.0 → circup-3.0.0}/LICENSES/MIT.txt +0 -0
- {circup-2.3.0 → circup-3.0.0}/LICENSES/Unlicense.txt +0 -0
- {circup-2.3.0 → circup-3.0.0}/README.rst +0 -0
- {circup-2.3.0 → circup-3.0.0}/README.rst.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/circup/__init__.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/circup/config/bundle_config.json +0 -0
- {circup-2.3.0 → circup-3.0.0}/circup/config/bundle_config.json.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/circup/logging.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/circup/wwshell/README.rst +0 -0
- {circup-2.3.0 → circup-3.0.0}/circup/wwshell/README.rst.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/circup/wwshell/__init__.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/circup.egg-info/SOURCES.txt +0 -0
- {circup-2.3.0 → circup-3.0.0}/circup.egg-info/dependency_links.txt +0 -0
- {circup-2.3.0 → circup-3.0.0}/circup.egg-info/entry_points.txt +0 -0
- {circup-2.3.0 → circup-3.0.0}/circup.egg-info/requires.txt +0 -0
- {circup-2.3.0 → circup-3.0.0}/circup.egg-info/top_level.txt +0 -0
- {circup-2.3.0 → circup-3.0.0}/docs/_static/favicon.ico +0 -0
- {circup-2.3.0 → circup-3.0.0}/docs/_static/favicon.ico.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/docs/conf.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/docs/index.rst +0 -0
- {circup-2.3.0 → circup-3.0.0}/docs/index.rst.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/docs/logo.png +0 -0
- {circup-2.3.0 → circup-3.0.0}/docs/logo.png.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/optional_requirements.txt +0 -0
- {circup-2.3.0 → circup-3.0.0}/optional_requirements.txt.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/readthedocs.yml +0 -0
- {circup-2.3.0 → circup-3.0.0}/requirements.txt +0 -0
- {circup-2.3.0 → circup-3.0.0}/requirements.txt.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/setup.cfg +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/__init__.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/bad_module/__init__.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/bad_module/my_module.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/bad_python.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/bundle.json +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/bundle.json.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/device.json +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/device.json.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/dir_module/__init__.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/dir_module/my_module.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/import_styles.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/local_module.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/local_module_cp7.mpy +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/local_module_cp7.mpy.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mock_device/apps/test_app/import_styles.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mock_device/apps/test_app/import_styles_sub.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mock_device/boot_out.txt +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mock_device/boot_out.txt.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mock_device/import_styles.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mock_device/import_styles_sub.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mock_device/lib/adafruit_waveform/.gitkeep +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mock_device_2/.gitignore +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mock_device_2/boot_out.txt +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mock_device_2/boot_out.txt.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mock_device_2/code.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mock_device_2/import_styles_sub.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mock_device_2/package/__init__.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mock_device_2/package/other.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mount_exists.txt +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mount_exists.txt.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mount_missing.txt +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/mount_missing.txt.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/remote_module.py +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/test_bundle_config.json +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/test_bundle_config.json.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/test_bundle_config_local.json +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/test_bundle_config_local.json.license +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/test_module.mpy +0 -0
- {circup-2.3.0 → circup-3.0.0}/tests/test_module.mpy.license +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: circup
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.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
|
|
@@ -42,7 +42,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
42
42
|
Classifier: Topic :: Education
|
|
43
43
|
Classifier: Topic :: Software Development :: Embedded Systems
|
|
44
44
|
Classifier: Topic :: System :: Software Distribution
|
|
45
|
-
Requires-Python: >=3.
|
|
45
|
+
Requires-Python: >=3.10
|
|
46
46
|
Description-Content-Type: text/x-rst
|
|
47
47
|
License-File: LICENSE
|
|
48
48
|
Requires-Dist: appdirs
|
|
@@ -139,7 +139,7 @@ class Backend:
|
|
|
139
139
|
if name in device_modules:
|
|
140
140
|
if not upgrade:
|
|
141
141
|
# skip already installed modules if no -upgrade flag
|
|
142
|
-
click.echo("'{}' is already installed."
|
|
142
|
+
click.echo(f"'{name}' is already installed.")
|
|
143
143
|
return
|
|
144
144
|
|
|
145
145
|
# uninstall the module before installing
|
|
@@ -197,7 +197,9 @@ class Backend:
|
|
|
197
197
|
# Create the library directory first.
|
|
198
198
|
self.create_directory(device_path, library_path)
|
|
199
199
|
if local_path is None:
|
|
200
|
-
if
|
|
200
|
+
# Fallback to the source version (py) if the bundle doesn't have
|
|
201
|
+
# a compiled version (mpy)
|
|
202
|
+
if pyext or bundle.platform is None:
|
|
201
203
|
# Use Python source for module.
|
|
202
204
|
self.install_module_py(metadata)
|
|
203
205
|
else:
|
|
@@ -205,9 +207,9 @@ class Backend:
|
|
|
205
207
|
self.install_module_mpy(bundle, metadata)
|
|
206
208
|
else:
|
|
207
209
|
self.copy_file(metadata["path"], "lib")
|
|
208
|
-
click.echo("Installed '{}'."
|
|
210
|
+
click.echo(f"Installed '{name}'.")
|
|
209
211
|
else:
|
|
210
|
-
click.echo("Unknown module named, '{}'."
|
|
212
|
+
click.echo(f"Unknown module named, '{name}'.")
|
|
211
213
|
|
|
212
214
|
# def libraries_from_imports(self, code_py, mod_names):
|
|
213
215
|
# """
|
|
@@ -303,7 +305,7 @@ class WebBackend(Backend):
|
|
|
303
305
|
socket.getaddrinfo(host, 80, proto=socket.IPPROTO_TCP)
|
|
304
306
|
except socket.gaierror as exc:
|
|
305
307
|
raise RuntimeError(
|
|
306
|
-
"Invalid host: {}."
|
|
308
|
+
f"Invalid host: {host}." + " You should remove the 'http://'"
|
|
307
309
|
if "http://" in host or "https://" in host
|
|
308
310
|
else "Could not find or connect to specified device"
|
|
309
311
|
) from exc
|
|
@@ -648,9 +650,7 @@ class WebBackend(Backend):
|
|
|
648
650
|
if not module_name:
|
|
649
651
|
# Must be a directory based module.
|
|
650
652
|
module_name = os.path.basename(os.path.dirname(metadata["path"]))
|
|
651
|
-
|
|
652
|
-
bundle_platform = "{}mpy".format(major_version)
|
|
653
|
-
bundle_path = os.path.join(bundle.lib_dir(bundle_platform), module_name)
|
|
653
|
+
bundle_path = os.path.join(bundle.lib_dir(), module_name)
|
|
654
654
|
if os.path.isdir(bundle_path):
|
|
655
655
|
|
|
656
656
|
self.install_dir_http(bundle_path)
|
|
@@ -659,7 +659,7 @@ class WebBackend(Backend):
|
|
|
659
659
|
self.install_file_http(bundle_path)
|
|
660
660
|
|
|
661
661
|
else:
|
|
662
|
-
raise
|
|
662
|
+
raise OSError("Cannot find compiled version of module.")
|
|
663
663
|
|
|
664
664
|
# pylint: enable=too-many-locals,too-many-branches
|
|
665
665
|
def install_module_py(self, metadata, location=None):
|
|
@@ -862,7 +862,6 @@ class DiskBackend(Backend):
|
|
|
862
862
|
try:
|
|
863
863
|
with open(
|
|
864
864
|
os.path.join(self.device_location, "boot_out.txt"),
|
|
865
|
-
"r",
|
|
866
865
|
encoding="utf-8",
|
|
867
866
|
) as boot:
|
|
868
867
|
boot_out_contents = boot.read()
|
|
@@ -920,9 +919,7 @@ class DiskBackend(Backend):
|
|
|
920
919
|
# Must be a directory based module.
|
|
921
920
|
module_name = os.path.basename(os.path.dirname(metadata["path"]))
|
|
922
921
|
|
|
923
|
-
|
|
924
|
-
bundle_platform = "{}mpy".format(major_version)
|
|
925
|
-
bundle_path = os.path.join(bundle.lib_dir(bundle_platform), module_name)
|
|
922
|
+
bundle_path = os.path.join(bundle.lib_dir(), module_name)
|
|
926
923
|
if os.path.isdir(bundle_path):
|
|
927
924
|
target_path = os.path.join(self.library_path, module_name)
|
|
928
925
|
# Copy the directory.
|
|
@@ -936,7 +933,7 @@ class DiskBackend(Backend):
|
|
|
936
933
|
# Copy file.
|
|
937
934
|
shutil.copyfile(bundle_path, target_path)
|
|
938
935
|
else:
|
|
939
|
-
raise
|
|
936
|
+
raise OSError("Cannot find compiled version of module.")
|
|
940
937
|
|
|
941
938
|
# pylint: enable=too-many-locals,too-many-branches
|
|
942
939
|
def install_module_py(self, metadata, location=None):
|
|
@@ -10,6 +10,8 @@ import sys
|
|
|
10
10
|
import click
|
|
11
11
|
import requests
|
|
12
12
|
|
|
13
|
+
from semver import VersionInfo
|
|
14
|
+
|
|
13
15
|
from circup.shared import (
|
|
14
16
|
DATA_DIR,
|
|
15
17
|
PLATFORMS,
|
|
@@ -20,11 +22,14 @@ from circup.shared import (
|
|
|
20
22
|
from circup.logging import logger
|
|
21
23
|
|
|
22
24
|
|
|
23
|
-
class Bundle:
|
|
25
|
+
class Bundle: # pylint: disable=too-many-instance-attributes
|
|
24
26
|
"""
|
|
25
27
|
All the links and file names for a bundle
|
|
26
28
|
"""
|
|
27
29
|
|
|
30
|
+
#: Avoid requests to the internet
|
|
31
|
+
offline = False
|
|
32
|
+
|
|
28
33
|
def __init__(self, repo):
|
|
29
34
|
"""
|
|
30
35
|
Initialise a Bundle created from its github info.
|
|
@@ -47,29 +52,40 @@ class Bundle:
|
|
|
47
52
|
self._latest = None
|
|
48
53
|
self.pinned_tag = None
|
|
49
54
|
self._available = []
|
|
55
|
+
#
|
|
56
|
+
self.platform = None
|
|
50
57
|
|
|
51
|
-
def lib_dir(self,
|
|
58
|
+
def lib_dir(self, source=False):
|
|
52
59
|
"""
|
|
53
|
-
This bundle's lib directory for the
|
|
60
|
+
This bundle's lib directory for the bundle's source or compiled version.
|
|
54
61
|
|
|
55
|
-
:param
|
|
56
|
-
|
|
62
|
+
:param bool source: Whether to return the path to the source lib
|
|
63
|
+
directory or to :py:attr:`self.platform`'s lib directory. If `source` is
|
|
64
|
+
`False` but :py:attr:`self.platform` is None, the source lib directory
|
|
65
|
+
will be returned instead.
|
|
66
|
+
:return: The path to the lib directory.
|
|
57
67
|
"""
|
|
58
68
|
tag = self.current_tag
|
|
69
|
+
platform = "py" if source or not self.platform else self.platform
|
|
59
70
|
return os.path.join(
|
|
60
71
|
self.dir.format(platform=platform),
|
|
61
72
|
self.basename.format(platform=PLATFORMS[platform], tag=tag),
|
|
62
73
|
"lib",
|
|
63
74
|
)
|
|
64
75
|
|
|
65
|
-
def examples_dir(self,
|
|
76
|
+
def examples_dir(self, source=False):
|
|
66
77
|
"""
|
|
67
|
-
This bundle's examples directory for the
|
|
78
|
+
This bundle's examples directory for the bundle's source or compiled
|
|
79
|
+
version.
|
|
68
80
|
|
|
69
|
-
:param
|
|
70
|
-
|
|
81
|
+
:param bool source: Whether to return the path to the source examples
|
|
82
|
+
directory or to :py:attr:`self.platform`'s examples directory. If
|
|
83
|
+
`source` is `False` but :py:attr:`self.platform` is None, the source
|
|
84
|
+
examples directory will be returned instead.
|
|
85
|
+
:return: The path to the examples directory.
|
|
71
86
|
"""
|
|
72
87
|
tag = self.current_tag
|
|
88
|
+
platform = "py" if source or not self.platform else self.platform
|
|
73
89
|
return os.path.join(
|
|
74
90
|
self.dir.format(platform=platform),
|
|
75
91
|
self.basename.format(platform=PLATFORMS[platform], tag=tag),
|
|
@@ -93,7 +109,7 @@ class Bundle:
|
|
|
93
109
|
"requirements.txt" if not toml_file else "pyproject.toml",
|
|
94
110
|
)
|
|
95
111
|
if os.path.isfile(found_file):
|
|
96
|
-
with open(found_file,
|
|
112
|
+
with open(found_file, encoding="utf-8") as read_this:
|
|
97
113
|
return read_this.read()
|
|
98
114
|
return None
|
|
99
115
|
|
|
@@ -101,18 +117,25 @@ class Bundle:
|
|
|
101
117
|
def current_tag(self):
|
|
102
118
|
"""
|
|
103
119
|
The current tag for the project. If the tag hasn't been explicitly set
|
|
104
|
-
this will be the pinned tag, if one is set
|
|
105
|
-
this will be the latest available tag that is locally
|
|
120
|
+
this will be the pinned tag, if one is set and it is available. If there
|
|
121
|
+
is no pinned tag, this will be the latest available tag that is locally
|
|
122
|
+
available.
|
|
106
123
|
|
|
107
124
|
:return: The current tag value for the project.
|
|
108
125
|
"""
|
|
109
126
|
if self._current is None:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
127
|
+
if self.pinned_tag:
|
|
128
|
+
self._current = (
|
|
129
|
+
self.pinned_tag if self.pinned_tag in self._available else None
|
|
130
|
+
)
|
|
131
|
+
else:
|
|
132
|
+
self._current = (
|
|
133
|
+
# This represents the latest version locally available
|
|
134
|
+
self._available[-1]
|
|
135
|
+
if len(self._available) > 0
|
|
136
|
+
else None
|
|
137
|
+
)
|
|
138
|
+
|
|
116
139
|
return self._current
|
|
117
140
|
|
|
118
141
|
@current_tag.setter
|
|
@@ -132,9 +155,12 @@ class Bundle:
|
|
|
132
155
|
:return: The most recent tag value for the project.
|
|
133
156
|
"""
|
|
134
157
|
if self._latest is None:
|
|
135
|
-
self.
|
|
136
|
-
self.
|
|
137
|
-
|
|
158
|
+
if self.offline:
|
|
159
|
+
self._latest = self._available[-1] if len(self._available) > 0 else None
|
|
160
|
+
else:
|
|
161
|
+
self._latest = get_latest_release_from_url(
|
|
162
|
+
self.url + "/releases/latest", logger
|
|
163
|
+
)
|
|
138
164
|
return self._latest
|
|
139
165
|
|
|
140
166
|
@property
|
|
@@ -155,7 +181,15 @@ class Bundle:
|
|
|
155
181
|
"""
|
|
156
182
|
if isinstance(tags, str):
|
|
157
183
|
tags = [tags]
|
|
158
|
-
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
tags = sorted(tags, key=self.parse_version)
|
|
187
|
+
except ValueError as ex:
|
|
188
|
+
logger.warning(
|
|
189
|
+
"Bundle '%s' has invalid tags, cannot order by version.", self.key
|
|
190
|
+
)
|
|
191
|
+
logger.warning(ex)
|
|
192
|
+
self._available = tags
|
|
159
193
|
|
|
160
194
|
def add_tag(self, tag: str) -> None:
|
|
161
195
|
"""
|
|
@@ -171,34 +205,59 @@ class Bundle:
|
|
|
171
205
|
# The tag is already stored for some reason, lets not add it again
|
|
172
206
|
return
|
|
173
207
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
208
|
+
try:
|
|
209
|
+
version_tag = self.parse_version(tag)
|
|
210
|
+
|
|
211
|
+
for rev_i, available_tag in enumerate(reversed(self._available)):
|
|
212
|
+
available_version_tag = self.parse_version(available_tag)
|
|
213
|
+
if version_tag > available_version_tag:
|
|
214
|
+
i = len(self._available) - rev_i
|
|
215
|
+
self._available.insert(i, tag)
|
|
216
|
+
break
|
|
217
|
+
else:
|
|
218
|
+
self._available.insert(0, tag)
|
|
219
|
+
except ValueError as ex:
|
|
220
|
+
logger.warning(
|
|
221
|
+
"Bundle tag '%s' is not a valid tag, cannot order by version.", tag
|
|
222
|
+
)
|
|
223
|
+
logger.warning(ex)
|
|
224
|
+
self._available.append(tag)
|
|
181
225
|
|
|
182
226
|
def validate(self):
|
|
183
227
|
"""
|
|
184
|
-
Test the existence of the expected
|
|
228
|
+
Test the existence of the expected URL (not the content)
|
|
185
229
|
"""
|
|
186
230
|
tag = self.latest_tag
|
|
187
231
|
if not tag or tag == "releases":
|
|
188
232
|
if "--verbose" in sys.argv:
|
|
189
233
|
click.secho(f' Invalid tag "{tag}"', fg="red")
|
|
190
234
|
return False
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
# pylint: enable=no-member
|
|
235
|
+
url = self.url_format.format(platform="py", tag=tag)
|
|
236
|
+
r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
|
|
237
|
+
# pylint: disable=no-member
|
|
238
|
+
if r.status_code != requests.codes.ok:
|
|
239
|
+
if "--verbose" in sys.argv:
|
|
240
|
+
click.secho(f" Unable to find {os.path.split(url)[1]}", fg="red")
|
|
241
|
+
return False
|
|
242
|
+
# pylint: enable=no-member
|
|
200
243
|
return True
|
|
201
244
|
|
|
245
|
+
@staticmethod
|
|
246
|
+
def parse_version(tag: str) -> VersionInfo:
|
|
247
|
+
"""
|
|
248
|
+
Parse a tag to get a VersionInfo object.
|
|
249
|
+
|
|
250
|
+
`VersionInfo` objects are useful for ordering the tags from oldest to
|
|
251
|
+
newest in :py:attr:`self.available_tags`. The tags are stripped of a
|
|
252
|
+
leading 'v' (if one is present) and minor and patch components are
|
|
253
|
+
optional. This is to allow more flexibility with how a bundle is
|
|
254
|
+
versioned.
|
|
255
|
+
|
|
256
|
+
:param str tag: The tag to parse.
|
|
257
|
+
:return: A `VersionInfo` object parsed from the tag.
|
|
258
|
+
"""
|
|
259
|
+
return VersionInfo.parse(tag.removeprefix("v"), optional_minor_and_patch=True)
|
|
260
|
+
|
|
202
261
|
def __repr__(self):
|
|
203
262
|
"""
|
|
204
263
|
Helps with log files.
|
|
@@ -23,6 +23,7 @@ import click
|
|
|
23
23
|
from circup.shared import (
|
|
24
24
|
PLATFORMS,
|
|
25
25
|
REQUESTS_TIMEOUT,
|
|
26
|
+
SUPPORTED_PLATFORMS,
|
|
26
27
|
_get_modules_file,
|
|
27
28
|
BUNDLE_CONFIG_OVERWRITE,
|
|
28
29
|
BUNDLE_CONFIG_FILE,
|
|
@@ -145,29 +146,71 @@ def ensure_bundle_tag(bundle, tag):
|
|
|
145
146
|
|
|
146
147
|
:return: If the bundle is available.
|
|
147
148
|
"""
|
|
148
|
-
|
|
149
|
+
if tag is None:
|
|
150
|
+
logger.warning("Bundle version requested is 'None'.")
|
|
151
|
+
return False
|
|
152
|
+
|
|
153
|
+
do_update_source = False
|
|
154
|
+
do_update_compiled = False
|
|
149
155
|
if tag in bundle.available_tags:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
156
|
+
# missing directories (new platform added on an existing install
|
|
157
|
+
# or side effect of pytest or network errors)
|
|
158
|
+
# Check for the source
|
|
159
|
+
do_update_source = not os.path.isdir(bundle.lib_dir(source=True))
|
|
160
|
+
do_update_compiled = bundle.platform is not None and not os.path.isdir(
|
|
161
|
+
bundle.lib_dir(source=False)
|
|
162
|
+
)
|
|
154
163
|
else:
|
|
155
|
-
|
|
164
|
+
do_update_source = True
|
|
165
|
+
do_update_compiled = bundle.platform is not None
|
|
166
|
+
|
|
167
|
+
if not (do_update_source or do_update_compiled):
|
|
168
|
+
logger.info("Current bundle version available (%s).", tag)
|
|
169
|
+
return True
|
|
156
170
|
|
|
157
|
-
if
|
|
158
|
-
|
|
171
|
+
if Bundle.offline:
|
|
172
|
+
if do_update_source: # pylint: disable=no-else-return
|
|
173
|
+
logger.info(
|
|
174
|
+
"Bundle version not available but skipping update in offline mode."
|
|
175
|
+
)
|
|
176
|
+
return False
|
|
177
|
+
else:
|
|
178
|
+
logger.info(
|
|
179
|
+
"Bundle platform not available. Falling back to source (.py) files in offline mode."
|
|
180
|
+
)
|
|
181
|
+
bundle.platform = None
|
|
182
|
+
return True
|
|
183
|
+
|
|
184
|
+
logger.info("New version available (%s).", tag)
|
|
185
|
+
if do_update_source:
|
|
159
186
|
try:
|
|
160
|
-
get_bundle(bundle, tag)
|
|
161
|
-
tags_data_save_tags(bundle.key, bundle.available_tags)
|
|
187
|
+
get_bundle(bundle, tag, "py")
|
|
162
188
|
except requests.exceptions.HTTPError as ex:
|
|
163
189
|
click.secho(
|
|
164
|
-
f"There was a problem downloading the {bundle.key} bundle.",
|
|
190
|
+
f"There was a problem downloading the 'py' platform for the '{bundle.key}' bundle.",
|
|
191
|
+
fg="red",
|
|
165
192
|
)
|
|
166
193
|
logger.exception(ex)
|
|
167
|
-
return False
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
194
|
+
return False # Bundle isn't available
|
|
195
|
+
bundle.add_tag(tag)
|
|
196
|
+
tags_data_save_tags(bundle.key, bundle.available_tags)
|
|
197
|
+
|
|
198
|
+
if do_update_compiled:
|
|
199
|
+
try:
|
|
200
|
+
get_bundle(bundle, tag, bundle.platform)
|
|
201
|
+
except requests.exceptions.HTTPError as ex:
|
|
202
|
+
click.secho(
|
|
203
|
+
(
|
|
204
|
+
f"There was a problem downloading the '{bundle.platform}' platform for the "
|
|
205
|
+
f"'{bundle.key}' bundle.\nFalling back to source (.py) files."
|
|
206
|
+
),
|
|
207
|
+
fg="red",
|
|
208
|
+
)
|
|
209
|
+
logger.exception(ex)
|
|
210
|
+
bundle.platform = None # Compiled isn't available, source is good
|
|
211
|
+
bundle.current_tag = tag
|
|
212
|
+
|
|
213
|
+
return True # bundle is available
|
|
171
214
|
|
|
172
215
|
|
|
173
216
|
def ensure_latest_bundle(bundle):
|
|
@@ -286,7 +329,7 @@ def find_device():
|
|
|
286
329
|
old_mode = ctypes.windll.kernel32.SetErrorMode(1)
|
|
287
330
|
try:
|
|
288
331
|
for disk in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
|
|
289
|
-
path = "{}:\\"
|
|
332
|
+
path = f"{disk}:\\"
|
|
290
333
|
if os.path.exists(path) and get_volume_name(path) == "CIRCUITPY":
|
|
291
334
|
device_dir = path
|
|
292
335
|
# Report only the FIRST device found.
|
|
@@ -295,7 +338,7 @@ def find_device():
|
|
|
295
338
|
ctypes.windll.kernel32.SetErrorMode(old_mode)
|
|
296
339
|
else:
|
|
297
340
|
# No support for unknown operating systems.
|
|
298
|
-
raise NotImplementedError('OS "{}" not supported.'
|
|
341
|
+
raise NotImplementedError(f'OS "{os.name}" not supported.')
|
|
299
342
|
logger.info("Found device: %s", device_dir)
|
|
300
343
|
return device_dir
|
|
301
344
|
|
|
@@ -349,45 +392,44 @@ def find_modules(backend, bundles_list):
|
|
|
349
392
|
# If it's not possible to get the device and bundle metadata, bail out
|
|
350
393
|
# with a friendly message and indication of what's gone wrong.
|
|
351
394
|
logger.exception(ex)
|
|
352
|
-
click.echo("There was a problem: {}"
|
|
395
|
+
click.echo(f"There was a problem: {ex}")
|
|
353
396
|
sys.exit(1)
|
|
354
397
|
# pylint: enable=broad-except,too-many-locals
|
|
355
398
|
|
|
356
399
|
|
|
357
|
-
def get_bundle(bundle, tag):
|
|
400
|
+
def get_bundle(bundle, tag, platform):
|
|
358
401
|
"""
|
|
359
402
|
Downloads and extracts the version of the bundle with the referenced tag.
|
|
360
403
|
The resulting zip file is saved on the local filesystem.
|
|
361
404
|
|
|
362
405
|
:param Bundle bundle: the target Bundle object.
|
|
363
406
|
:param str tag: The GIT tag to use to download the bundle.
|
|
407
|
+
:param str platform: The platform string (i.e. '10mpy').
|
|
364
408
|
"""
|
|
365
|
-
click.echo(f"Downloading
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
bundle.add_tag(tag)
|
|
390
|
-
bundle.current_tag = tag
|
|
409
|
+
click.echo(f"Downloading '{platform}' bundle for {bundle.key} ({tag}).")
|
|
410
|
+
github_string = PLATFORMS[platform]
|
|
411
|
+
# Report the platform: "8.x-mpy", etc.
|
|
412
|
+
click.echo(f"{github_string}:")
|
|
413
|
+
url = bundle.url_format.format(platform=github_string, tag=tag)
|
|
414
|
+
logger.info("Downloading bundle: %s", url)
|
|
415
|
+
r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
|
|
416
|
+
# pylint: disable=no-member
|
|
417
|
+
if r.status_code != requests.codes.ok:
|
|
418
|
+
logger.warning("Unable to connect to %s", url)
|
|
419
|
+
r.raise_for_status()
|
|
420
|
+
# pylint: enable=no-member
|
|
421
|
+
total_size = int(r.headers.get("Content-Length"))
|
|
422
|
+
temp_zip = bundle.zip.format(platform=platform)
|
|
423
|
+
with click.progressbar(
|
|
424
|
+
r.iter_content(1024), label="Extracting:", length=total_size
|
|
425
|
+
) as pbar, open(temp_zip, "wb") as zip_fp:
|
|
426
|
+
for chunk in pbar:
|
|
427
|
+
zip_fp.write(chunk)
|
|
428
|
+
pbar.update(len(chunk))
|
|
429
|
+
logger.info("Saved to %s", temp_zip)
|
|
430
|
+
temp_dir = bundle.dir.format(platform=platform)
|
|
431
|
+
with zipfile.ZipFile(temp_zip, "r") as zfile:
|
|
432
|
+
zfile.extractall(temp_dir)
|
|
391
433
|
click.echo("\nOK\n")
|
|
392
434
|
|
|
393
435
|
|
|
@@ -407,12 +449,12 @@ def get_bundle_examples(bundles_list, avoid_download=False):
|
|
|
407
449
|
|
|
408
450
|
try:
|
|
409
451
|
for bundle in bundles_list:
|
|
410
|
-
if not avoid_download or not os.path.isdir(bundle.lib_dir(
|
|
452
|
+
if not avoid_download or not os.path.isdir(bundle.lib_dir(source=True)):
|
|
411
453
|
ensure_bundle(bundle)
|
|
412
|
-
path = bundle.examples_dir(
|
|
454
|
+
path = bundle.examples_dir(source=True)
|
|
413
455
|
meta_saved = os.path.join(path, "../bundle_examples.json")
|
|
414
456
|
if os.path.exists(meta_saved):
|
|
415
|
-
with open(meta_saved,
|
|
457
|
+
with open(meta_saved, encoding="utf-8") as f:
|
|
416
458
|
bundle_examples = json.load(f)
|
|
417
459
|
all_the_examples.update(bundle_examples)
|
|
418
460
|
bundle_examples.clear()
|
|
@@ -455,9 +497,9 @@ def get_bundle_versions(bundles_list, avoid_download=False):
|
|
|
455
497
|
"""
|
|
456
498
|
all_the_modules = dict()
|
|
457
499
|
for bundle in bundles_list:
|
|
458
|
-
if not avoid_download or not os.path.isdir(bundle.lib_dir(
|
|
500
|
+
if not avoid_download or not os.path.isdir(bundle.lib_dir(source=True)):
|
|
459
501
|
ensure_bundle(bundle)
|
|
460
|
-
path = bundle.lib_dir(
|
|
502
|
+
path = bundle.lib_dir(source=True)
|
|
461
503
|
path_modules = _get_modules_file(path, logger)
|
|
462
504
|
for name, module in path_modules.items():
|
|
463
505
|
module["bundle"] = bundle
|
|
@@ -504,12 +546,14 @@ def get_bundles_local_dict():
|
|
|
504
546
|
return dict()
|
|
505
547
|
|
|
506
548
|
|
|
507
|
-
def get_bundles_list(bundle_tags):
|
|
549
|
+
def get_bundles_list(bundle_tags, platform_version=None):
|
|
508
550
|
"""
|
|
509
551
|
Retrieve the list of bundles from the config dictionary.
|
|
510
552
|
|
|
511
553
|
:param Dict[str,str]|None bundle_tags: Pinned bundle tags. These override
|
|
512
554
|
any tags found in the pyproject.toml.
|
|
555
|
+
:param str platform_version: The platform version needed for the current
|
|
556
|
+
device.
|
|
513
557
|
:return: List of supported bundles as Bundle objects.
|
|
514
558
|
"""
|
|
515
559
|
bundle_config = get_bundles_dict()
|
|
@@ -524,6 +568,7 @@ def get_bundles_list(bundle_tags):
|
|
|
524
568
|
|
|
525
569
|
bundles_list = [Bundle(bundle_config[b]) for b in bundle_config]
|
|
526
570
|
for bundle in bundles_list:
|
|
571
|
+
bundle.platform = platform_version
|
|
527
572
|
bundle.available_tags = tags.get(bundle.key, [])
|
|
528
573
|
if pinned_tags is not None:
|
|
529
574
|
bundle.pinned_tag = pinned_tags.get(bundle.key)
|
|
@@ -884,7 +929,7 @@ def libraries_from_auto_file(backend, auto_file, mod_names):
|
|
|
884
929
|
# pass a local file with "./" or "../"
|
|
885
930
|
is_relative = auto_file.split(os.sep)[0] in [os.path.curdir, os.path.pardir]
|
|
886
931
|
if os.path.isabs(auto_file) or is_relative:
|
|
887
|
-
with open(auto_file,
|
|
932
|
+
with open(auto_file, encoding="UTF8") as fp:
|
|
888
933
|
auto_file_content = fp.read()
|
|
889
934
|
else:
|
|
890
935
|
auto_file_content = backend.get_file_content(auto_file)
|
|
@@ -994,3 +1039,12 @@ def parse_cli_bundle_tags(bundle_tags_cli):
|
|
|
994
1039
|
if len(item) == 2:
|
|
995
1040
|
bundle_tags[item[0].strip()] = item[1].strip()
|
|
996
1041
|
return bundle_tags if len(bundle_tags) > 0 else None
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
def pretty_supported_cpy_versions():
|
|
1045
|
+
"""Return a user friendly string of the supported CircuitPython versions."""
|
|
1046
|
+
supported_cpy = [
|
|
1047
|
+
PLATFORMS[platform].split("-", maxsplit=1)[0]
|
|
1048
|
+
for platform in SUPPORTED_PLATFORMS
|
|
1049
|
+
]
|
|
1050
|
+
return ", ".join(supported_cpy)
|