circup 2.3.0__tar.gz → 2.4.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-2.4.0}/PKG-INFO +1 -1
- {circup-2.3.0 → circup-2.4.0}/circup/backends.py +5 -7
- {circup-2.3.0 → circup-2.4.0}/circup/bundle.py +97 -38
- {circup-2.3.0 → circup-2.4.0}/circup/command_utils.py +101 -47
- {circup-2.3.0 → circup-2.4.0}/circup/commands.py +93 -27
- {circup-2.3.0 → circup-2.4.0}/circup/module.py +1 -9
- {circup-2.3.0 → circup-2.4.0}/circup/shared.py +4 -1
- {circup-2.3.0 → circup-2.4.0/circup.egg-info}/PKG-INFO +1 -1
- {circup-2.3.0 → circup-2.4.0}/tests/test_circup.py +40 -14
- {circup-2.3.0 → circup-2.4.0}/.github/ISSUE_TEMPLATE.md +0 -0
- {circup-2.3.0 → circup-2.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {circup-2.3.0 → circup-2.4.0}/.github/workflows/build.yml +0 -0
- {circup-2.3.0 → circup-2.4.0}/.github/workflows/release.yml +0 -0
- {circup-2.3.0 → circup-2.4.0}/.gitignore +0 -0
- {circup-2.3.0 → circup-2.4.0}/.isort.cfg +0 -0
- {circup-2.3.0 → circup-2.4.0}/.pre-commit-config.yaml +0 -0
- {circup-2.3.0 → circup-2.4.0}/.pylintrc +0 -0
- {circup-2.3.0 → circup-2.4.0}/CODE_OF_CONDUCT.rst +0 -0
- {circup-2.3.0 → circup-2.4.0}/CODE_OF_CONDUCT.rst.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/CONTRIBUTING.rst +0 -0
- {circup-2.3.0 → circup-2.4.0}/CONTRIBUTING.rst.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/LICENSE +0 -0
- {circup-2.3.0 → circup-2.4.0}/LICENSES/CC-BY-4.0.txt +0 -0
- {circup-2.3.0 → circup-2.4.0}/LICENSES/MIT.txt +0 -0
- {circup-2.3.0 → circup-2.4.0}/LICENSES/Unlicense.txt +0 -0
- {circup-2.3.0 → circup-2.4.0}/README.rst +0 -0
- {circup-2.3.0 → circup-2.4.0}/README.rst.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/circup/__init__.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/circup/config/bundle_config.json +0 -0
- {circup-2.3.0 → circup-2.4.0}/circup/config/bundle_config.json.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/circup/lazy_metadata.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/circup/logging.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/circup/wwshell/README.rst +0 -0
- {circup-2.3.0 → circup-2.4.0}/circup/wwshell/README.rst.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/circup/wwshell/__init__.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/circup/wwshell/commands.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/circup.egg-info/SOURCES.txt +0 -0
- {circup-2.3.0 → circup-2.4.0}/circup.egg-info/dependency_links.txt +0 -0
- {circup-2.3.0 → circup-2.4.0}/circup.egg-info/entry_points.txt +0 -0
- {circup-2.3.0 → circup-2.4.0}/circup.egg-info/requires.txt +0 -0
- {circup-2.3.0 → circup-2.4.0}/circup.egg-info/top_level.txt +0 -0
- {circup-2.3.0 → circup-2.4.0}/docs/_static/favicon.ico +0 -0
- {circup-2.3.0 → circup-2.4.0}/docs/_static/favicon.ico.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/docs/conf.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/docs/index.rst +0 -0
- {circup-2.3.0 → circup-2.4.0}/docs/index.rst.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/docs/logo.png +0 -0
- {circup-2.3.0 → circup-2.4.0}/docs/logo.png.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/optional_requirements.txt +0 -0
- {circup-2.3.0 → circup-2.4.0}/optional_requirements.txt.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/pyproject.toml +0 -0
- {circup-2.3.0 → circup-2.4.0}/readthedocs.yml +0 -0
- {circup-2.3.0 → circup-2.4.0}/requirements.txt +0 -0
- {circup-2.3.0 → circup-2.4.0}/requirements.txt.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/setup.cfg +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/__init__.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/bad_module/__init__.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/bad_module/my_module.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/bad_python.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/bundle.json +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/bundle.json.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/device.json +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/device.json.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/dir_module/__init__.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/dir_module/my_module.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/import_styles.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/local_module.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/local_module_cp7.mpy +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/local_module_cp7.mpy.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mock_device/apps/test_app/import_styles.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mock_device/apps/test_app/import_styles_sub.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mock_device/boot_out.txt +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mock_device/boot_out.txt.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mock_device/import_styles.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mock_device/import_styles_sub.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mock_device/lib/adafruit_waveform/.gitkeep +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mock_device_2/.gitignore +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mock_device_2/boot_out.txt +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mock_device_2/boot_out.txt.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mock_device_2/code.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mock_device_2/import_styles_sub.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mock_device_2/package/__init__.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mock_device_2/package/other.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mount_exists.txt +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mount_exists.txt.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mount_missing.txt +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/mount_missing.txt.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/remote_module.py +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/test_bundle_config.json +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/test_bundle_config.json.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/test_bundle_config_local.json +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/test_bundle_config_local.json.license +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/test_module.mpy +0 -0
- {circup-2.3.0 → circup-2.4.0}/tests/test_module.mpy.license +0 -0
|
@@ -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:
|
|
@@ -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)
|
|
@@ -920,9 +920,7 @@ class DiskBackend(Backend):
|
|
|
920
920
|
# Must be a directory based module.
|
|
921
921
|
module_name = os.path.basename(os.path.dirname(metadata["path"]))
|
|
922
922
|
|
|
923
|
-
|
|
924
|
-
bundle_platform = "{}mpy".format(major_version)
|
|
925
|
-
bundle_path = os.path.join(bundle.lib_dir(bundle_platform), module_name)
|
|
923
|
+
bundle_path = os.path.join(bundle.lib_dir(), module_name)
|
|
926
924
|
if os.path.isdir(bundle_path):
|
|
927
925
|
target_path = os.path.join(self.library_path, module_name)
|
|
928
926
|
# Copy the directory.
|
|
@@ -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),
|
|
@@ -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):
|
|
@@ -354,40 +397,39 @@ def find_modules(backend, bundles_list):
|
|
|
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,9 +449,9 @@ 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
457
|
with open(meta_saved, "r", encoding="utf-8") as f:
|
|
@@ -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)
|
|
@@ -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)
|
|
@@ -24,7 +24,11 @@ import requests
|
|
|
24
24
|
|
|
25
25
|
from circup.backends import WebBackend, DiskBackend
|
|
26
26
|
from circup.logging import logger, log_formatter, LOGFILE
|
|
27
|
-
from circup.shared import
|
|
27
|
+
from circup.shared import (
|
|
28
|
+
BOARDLESS_COMMANDS,
|
|
29
|
+
SUPPORTED_PLATFORMS,
|
|
30
|
+
get_latest_release_from_url,
|
|
31
|
+
)
|
|
28
32
|
from circup.bundle import Bundle
|
|
29
33
|
from circup.command_utils import (
|
|
30
34
|
get_device_path,
|
|
@@ -38,6 +42,7 @@ from circup.command_utils import (
|
|
|
38
42
|
get_dependencies,
|
|
39
43
|
get_bundles_local_dict,
|
|
40
44
|
parse_cli_bundle_tags,
|
|
45
|
+
pretty_supported_cpy_versions,
|
|
41
46
|
save_local_bundles,
|
|
42
47
|
get_bundles_dict,
|
|
43
48
|
completion_for_example,
|
|
@@ -68,6 +73,16 @@ from circup.command_utils import (
|
|
|
68
73
|
" You can optionally set an environment variable CIRCUP_WEBWORKFLOW_PASSWORD"
|
|
69
74
|
" instead of passing this argument. If both exist the CLI arg takes precedent.",
|
|
70
75
|
)
|
|
76
|
+
@click.option(
|
|
77
|
+
"--offline",
|
|
78
|
+
is_flag=True,
|
|
79
|
+
help="Prevents Circup from accessing the internet for any reason. "
|
|
80
|
+
"Without this flag, Circup will fail with an error if it needs to access "
|
|
81
|
+
"the network and the network is not available. With this flag, Circup "
|
|
82
|
+
"will attempt to proceed without the network if possible. Circup will "
|
|
83
|
+
"only use bundles downloaded locally even if there might be newer "
|
|
84
|
+
"versions available.",
|
|
85
|
+
)
|
|
71
86
|
@click.option(
|
|
72
87
|
"--timeout",
|
|
73
88
|
default=30,
|
|
@@ -95,6 +110,13 @@ from circup.command_utils import (
|
|
|
95
110
|
"version values provided here will override any pinned values from the "
|
|
96
111
|
"pyproject.toml.",
|
|
97
112
|
)
|
|
113
|
+
@click.option(
|
|
114
|
+
"--allow-unsupported",
|
|
115
|
+
is_flag=True,
|
|
116
|
+
help="Allow using a device with a version of CircuitPython that is no longer "
|
|
117
|
+
"supported. Using an unsupported version of CircuitPython is generally not "
|
|
118
|
+
"recommended because libraries may not work with it.",
|
|
119
|
+
)
|
|
98
120
|
@click.version_option(
|
|
99
121
|
prog_name="Circup",
|
|
100
122
|
message="%(prog)s, A CircuitPython module updater. Version %(version)s",
|
|
@@ -107,10 +129,12 @@ def main( # pylint: disable=too-many-locals
|
|
|
107
129
|
host,
|
|
108
130
|
port,
|
|
109
131
|
password,
|
|
132
|
+
offline,
|
|
110
133
|
timeout,
|
|
111
134
|
board_id,
|
|
112
135
|
cpy_version,
|
|
113
136
|
bundle_versions,
|
|
137
|
+
allow_unsupported,
|
|
114
138
|
): # pragma: no cover
|
|
115
139
|
"""
|
|
116
140
|
A tool to manage and update libraries on a CircuitPython device.
|
|
@@ -121,6 +145,7 @@ def main( # pylint: disable=too-many-locals
|
|
|
121
145
|
ctx.obj["BUNDLE_TAGS"] = (
|
|
122
146
|
parse_cli_bundle_tags(bundle_versions) if len(bundle_versions) > 0 else None
|
|
123
147
|
)
|
|
148
|
+
Bundle.offline = offline
|
|
124
149
|
|
|
125
150
|
if password is None:
|
|
126
151
|
password = os.getenv("CIRCUP_WEBWORKFLOW_PASSWORD")
|
|
@@ -177,20 +202,22 @@ def main( # pylint: disable=too-many-locals
|
|
|
177
202
|
|
|
178
203
|
logger.info("### Started Circup ###")
|
|
179
204
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
205
|
+
if offline:
|
|
206
|
+
logger.info(
|
|
207
|
+
"'--offline' flag present, all update checks requiring the network will be skipped."
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
# If a newer version of circup is available, print a message.
|
|
211
|
+
logger.info("Checking for a newer version of circup")
|
|
212
|
+
version = get_circup_version()
|
|
213
|
+
if version:
|
|
214
|
+
update_checker.update_check("circup", version)
|
|
185
215
|
|
|
186
216
|
# stop early if the command is boardless
|
|
187
217
|
if ctx.invoked_subcommand in BOARDLESS_COMMANDS or "--help" in sys.argv:
|
|
188
218
|
return
|
|
189
219
|
|
|
190
220
|
ctx.obj["DEVICE_PATH"] = device_path
|
|
191
|
-
latest_version = get_latest_release_from_url(
|
|
192
|
-
"https://github.com/adafruit/circuitpython/releases/latest", logger
|
|
193
|
-
)
|
|
194
221
|
|
|
195
222
|
if device_path is None or not ctx.obj["backend"].is_device_present():
|
|
196
223
|
click.secho("Could not find a connected CircuitPython device.", fg="red")
|
|
@@ -201,27 +228,54 @@ def main( # pylint: disable=too-many-locals
|
|
|
201
228
|
if board_id is None or cpy_version is None
|
|
202
229
|
else (cpy_version, board_id)
|
|
203
230
|
)
|
|
231
|
+
major_version = cpy_version.split(".")[0]
|
|
232
|
+
bundle_platform = "{}mpy".format(major_version)
|
|
233
|
+
ctx.obj["DEVICE_PLATFORM_VERSION"] = bundle_platform
|
|
204
234
|
click.echo(
|
|
205
235
|
"Found device {} at {}, running CircuitPython {}.".format(
|
|
206
236
|
board_id, device_path, cpy_version
|
|
207
237
|
)
|
|
208
238
|
)
|
|
209
|
-
|
|
210
|
-
|
|
239
|
+
|
|
240
|
+
if not offline:
|
|
241
|
+
latest_version = get_latest_release_from_url(
|
|
242
|
+
"https://github.com/adafruit/circuitpython/releases/latest", logger
|
|
243
|
+
)
|
|
244
|
+
try:
|
|
245
|
+
if VersionInfo.parse(cpy_version) < VersionInfo.parse(latest_version):
|
|
246
|
+
click.secho(
|
|
247
|
+
"A newer version of CircuitPython ({}) is available.".format(
|
|
248
|
+
latest_version
|
|
249
|
+
),
|
|
250
|
+
fg="green",
|
|
251
|
+
)
|
|
252
|
+
if board_id:
|
|
253
|
+
url_download = f"https://circuitpython.org/board/{board_id}"
|
|
254
|
+
else:
|
|
255
|
+
url_download = "https://circuitpython.org/downloads"
|
|
256
|
+
click.secho("Get it here: {}".format(url_download), fg="green")
|
|
257
|
+
except ValueError as ex:
|
|
258
|
+
logger.warning("CircuitPython has incorrect semver value.")
|
|
259
|
+
logger.warning(ex)
|
|
260
|
+
|
|
261
|
+
if not bundle_platform in SUPPORTED_PLATFORMS:
|
|
262
|
+
click.secho(
|
|
263
|
+
"The version of CircuitPython on the device is no longer supported.",
|
|
264
|
+
fg="yellow" if allow_unsupported else "red",
|
|
265
|
+
)
|
|
266
|
+
if allow_unsupported:
|
|
211
267
|
click.secho(
|
|
212
|
-
"
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
fg="green",
|
|
268
|
+
"It is recommended to update to a supported version "
|
|
269
|
+
f"({pretty_supported_cpy_versions()}) to ensure compatability.",
|
|
270
|
+
fg="yellow",
|
|
216
271
|
)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
logger.warning(ex)
|
|
272
|
+
else:
|
|
273
|
+
click.echo(
|
|
274
|
+
f"If you would like to continue to use version {cpy_version} of CircuitPython, "
|
|
275
|
+
"pass the '--allow-unsupported' flag with this command. Otherwise, update to a "
|
|
276
|
+
f"supported version ({pretty_supported_cpy_versions()}) to ensure compatability.",
|
|
277
|
+
)
|
|
278
|
+
sys.exit(1)
|
|
225
279
|
|
|
226
280
|
|
|
227
281
|
@main.command()
|
|
@@ -282,7 +336,10 @@ def list_cli(ctx): # pragma: no cover
|
|
|
282
336
|
modules = [
|
|
283
337
|
m.row
|
|
284
338
|
for m in find_modules(
|
|
285
|
-
ctx.obj["backend"],
|
|
339
|
+
ctx.obj["backend"],
|
|
340
|
+
get_bundles_list(
|
|
341
|
+
ctx.obj["BUNDLE_TAGS"], ctx.obj["DEVICE_PLATFORM_VERSION"]
|
|
342
|
+
),
|
|
286
343
|
)
|
|
287
344
|
if m.outofdate
|
|
288
345
|
]
|
|
@@ -359,7 +416,10 @@ def install(
|
|
|
359
416
|
|
|
360
417
|
# pylint: disable=too-many-branches
|
|
361
418
|
# TODO: Ensure there's enough space on the device
|
|
362
|
-
|
|
419
|
+
platform_version = ctx.obj["DEVICE_PLATFORM_VERSION"] if not pyext else None
|
|
420
|
+
available_modules = get_bundle_versions(
|
|
421
|
+
get_bundles_list(ctx.obj["BUNDLE_TAGS"], platform_version)
|
|
422
|
+
)
|
|
363
423
|
mod_names = {}
|
|
364
424
|
for module, metadata in available_modules.items():
|
|
365
425
|
mod_names[module.replace(".py", "").lower()] = metadata
|
|
@@ -392,7 +452,7 @@ def install(
|
|
|
392
452
|
upgrade,
|
|
393
453
|
)
|
|
394
454
|
|
|
395
|
-
if stubs:
|
|
455
|
+
if stubs and not Bundle.offline:
|
|
396
456
|
# Check we are in a virtual environment
|
|
397
457
|
if not is_virtual_env_active():
|
|
398
458
|
if is_global_install_ok is None:
|
|
@@ -581,7 +641,9 @@ def update(ctx, update_all): # pragma: no cover
|
|
|
581
641
|
"""
|
|
582
642
|
logger.info("Update")
|
|
583
643
|
# Grab current modules.
|
|
584
|
-
bundles_list = get_bundles_list(
|
|
644
|
+
bundles_list = get_bundles_list(
|
|
645
|
+
ctx.obj["BUNDLE_TAGS"], ctx.obj["DEVICE_PLATFORM_VERSION"]
|
|
646
|
+
)
|
|
585
647
|
installed_modules = find_modules(ctx.obj["backend"], bundles_list)
|
|
586
648
|
modules_to_update = [m for m in installed_modules if m.outofdate]
|
|
587
649
|
|
|
@@ -728,6 +790,10 @@ def bundle_add(ctx, bundle):
|
|
|
728
790
|
)
|
|
729
791
|
return
|
|
730
792
|
|
|
793
|
+
if Bundle.offline:
|
|
794
|
+
click.secho("Cannot add new bundle when '--offline' flag is present.", fg="red")
|
|
795
|
+
return
|
|
796
|
+
|
|
731
797
|
bundles_dict = get_bundles_local_dict()
|
|
732
798
|
modified = False
|
|
733
799
|
for bundle_repo in bundle:
|
|
@@ -79,16 +79,8 @@ class Module:
|
|
|
79
79
|
self.max_version = compatibility[1]
|
|
80
80
|
# Figure out the bundle path.
|
|
81
81
|
self.bundle_path = None
|
|
82
|
-
if self.mpy:
|
|
83
|
-
# Byte compiled, now check CircuitPython version.
|
|
84
|
-
|
|
85
|
-
major_version = self.backend.get_circuitpython_version()[0].split(".")[0]
|
|
86
|
-
bundle_platform = "{}mpy".format(major_version)
|
|
87
|
-
else:
|
|
88
|
-
# Regular Python
|
|
89
|
-
bundle_platform = "py"
|
|
90
82
|
# module path in the bundle
|
|
91
|
-
search_path = bundle.lib_dir(
|
|
83
|
+
search_path = bundle.lib_dir(source=not self.mpy)
|
|
92
84
|
if self.file:
|
|
93
85
|
self.bundle_path = os.path.join(search_path, self.file)
|
|
94
86
|
else:
|
|
@@ -22,7 +22,10 @@ BAD_FILE_FORMAT = "Invalid"
|
|
|
22
22
|
DATA_DIR = appdirs.user_data_dir(appname="circup", appauthor="adafruit")
|
|
23
23
|
|
|
24
24
|
#: Module formats list (and the other form used in github files)
|
|
25
|
-
PLATFORMS = {"py": "py", "9mpy": "9.x-mpy", "10mpy": "10.x-mpy"}
|
|
25
|
+
PLATFORMS = {"py": "py", "8mpy": "8.x-mpy", "9mpy": "9.x-mpy", "10mpy": "10.x-mpy"}
|
|
26
|
+
|
|
27
|
+
#: CircuitPython platforms that are currently supported.
|
|
28
|
+
SUPPORTED_PLATFORMS = ["9mpy", "10mpy"]
|
|
26
29
|
|
|
27
30
|
#: Timeout for requests calls like get()
|
|
28
31
|
REQUESTS_TIMEOUT = 30
|
|
@@ -51,7 +51,6 @@ from circup.command_utils import (
|
|
|
51
51
|
pyproject_bundle_versions,
|
|
52
52
|
tags_data_load,
|
|
53
53
|
)
|
|
54
|
-
from circup.shared import PLATFORMS
|
|
55
54
|
from circup.module import Module
|
|
56
55
|
from circup.logging import logger
|
|
57
56
|
|
|
@@ -98,14 +97,15 @@ def test_Bundle_lib_dir():
|
|
|
98
97
|
Check the return of Bundle.lib_dir with a test tag.
|
|
99
98
|
"""
|
|
100
99
|
bundle = circup.Bundle(TEST_BUNDLE_NAME)
|
|
100
|
+
bundle.platform = "9mpy"
|
|
101
101
|
with mock.patch.object(bundle, "_available", ["TESTTAG"]):
|
|
102
102
|
assert bundle.current_tag == "TESTTAG"
|
|
103
|
-
assert bundle.lib_dir(
|
|
103
|
+
assert bundle.lib_dir(source=True) == (
|
|
104
104
|
circup.shared.DATA_DIR + "/"
|
|
105
105
|
"adafruit/adafruit-circuitpython-bundle-py/"
|
|
106
106
|
"adafruit-circuitpython-bundle-py-TESTTAG/lib"
|
|
107
107
|
)
|
|
108
|
-
assert bundle.lib_dir(
|
|
108
|
+
assert bundle.lib_dir() == (
|
|
109
109
|
circup.shared.DATA_DIR + "/"
|
|
110
110
|
"adafruit/adafruit-circuitpython-bundle-9mpy/"
|
|
111
111
|
"adafruit-circuitpython-bundle-9.x-mpy-TESTTAG/lib"
|
|
@@ -943,7 +943,7 @@ def test_ensure_latest_bundle_no_bundle_data():
|
|
|
943
943
|
):
|
|
944
944
|
bundle = circup.Bundle(TEST_BUNDLE_NAME)
|
|
945
945
|
ensure_latest_bundle(bundle)
|
|
946
|
-
mock_gb.assert_called_once_with(bundle, "12345")
|
|
946
|
+
mock_gb.assert_called_once_with(bundle, "12345", "py")
|
|
947
947
|
assert mock_json.dump.call_count == 1 # Current version saved to file.
|
|
948
948
|
|
|
949
949
|
|
|
@@ -968,7 +968,7 @@ def test_ensure_latest_bundle_bad_bundle_data():
|
|
|
968
968
|
tags = tags_data_load()
|
|
969
969
|
bundle.available_tags = tags.get(bundle.key, [])
|
|
970
970
|
ensure_latest_bundle(bundle)
|
|
971
|
-
mock_gb.assert_called_once_with(bundle, "12345")
|
|
971
|
+
mock_gb.assert_called_once_with(bundle, "12345", "py")
|
|
972
972
|
|
|
973
973
|
assert mock_logger.error.call_count == 1
|
|
974
974
|
assert mock_logger.exception.call_count == 1
|
|
@@ -987,7 +987,7 @@ def test_ensure_latest_bundle_to_update():
|
|
|
987
987
|
mock_json.load.return_value = {TEST_BUNDLE_NAME: "12345"}
|
|
988
988
|
bundle = circup.Bundle(TEST_BUNDLE_NAME)
|
|
989
989
|
ensure_latest_bundle(bundle)
|
|
990
|
-
mock_gb.assert_called_once_with(bundle, "54321")
|
|
990
|
+
mock_gb.assert_called_once_with(bundle, "54321", "py")
|
|
991
991
|
assert mock_json.dump.call_count == 1 # Current version saved to file.
|
|
992
992
|
|
|
993
993
|
|
|
@@ -1015,7 +1015,7 @@ def test_ensure_latest_bundle_to_update_http_error():
|
|
|
1015
1015
|
tags = tags_data_load()
|
|
1016
1016
|
bundle.available_tags = tags.get(bundle.key, [])
|
|
1017
1017
|
ensure_latest_bundle(bundle)
|
|
1018
|
-
mock_gb.assert_called_once_with(bundle, "54321")
|
|
1018
|
+
mock_gb.assert_called_once_with(bundle, "54321", "py")
|
|
1019
1019
|
assert bundle.current_tag == "67890"
|
|
1020
1020
|
assert mock_json.dump.call_count == 0 # not saved.
|
|
1021
1021
|
assert mock_click.call_count == 2 # friendly message.
|
|
@@ -1045,7 +1045,7 @@ def test_ensure_pinned_bundle_to_exit_http_error():
|
|
|
1045
1045
|
tags = tags_data_load()
|
|
1046
1046
|
bundle.available_tags = tags.get(bundle.key, [])
|
|
1047
1047
|
ensure_pinned_bundle(bundle)
|
|
1048
|
-
mock_gb.assert_called_once_with(bundle, "54321")
|
|
1048
|
+
mock_gb.assert_called_once_with(bundle, "54321", "py")
|
|
1049
1049
|
assert mock_json.dump.call_count == 0 # not saved.
|
|
1050
1050
|
assert mock_click.call_count == 2 # friendly message.
|
|
1051
1051
|
mock_exit.assert_called_once_with(1)
|
|
@@ -1074,6 +1074,34 @@ def test_ensure_latest_bundle_no_update():
|
|
|
1074
1074
|
assert mock_logger.info.call_count == 2
|
|
1075
1075
|
|
|
1076
1076
|
|
|
1077
|
+
def test_ensure_bundle_tag_fallback_to_source():
|
|
1078
|
+
"""
|
|
1079
|
+
If a compiled platform download fails, fallback to the source version.
|
|
1080
|
+
"""
|
|
1081
|
+
tags_data = {TEST_BUNDLE_NAME: ["12345"]}
|
|
1082
|
+
with mock.patch("circup.Bundle.latest_tag", "54321"), mock.patch(
|
|
1083
|
+
"circup.os.path.isfile",
|
|
1084
|
+
return_value=True,
|
|
1085
|
+
), mock.patch("circup.command_utils.open"), mock.patch(
|
|
1086
|
+
"circup.command_utils.get_bundle",
|
|
1087
|
+
side_effect=[None, requests.exceptions.HTTPError("404")],
|
|
1088
|
+
) as mock_gb, mock.patch(
|
|
1089
|
+
"circup.command_utils.json"
|
|
1090
|
+
) as mock_json, mock.patch(
|
|
1091
|
+
"circup.click.secho"
|
|
1092
|
+
):
|
|
1093
|
+
mock_json.load.return_value = tags_data
|
|
1094
|
+
bundle = circup.Bundle(TEST_BUNDLE_NAME)
|
|
1095
|
+
bundle.platform = "10mpy"
|
|
1096
|
+
tags = tags_data_load()
|
|
1097
|
+
bundle.available_tags = tags.get(bundle.key, [])
|
|
1098
|
+
ensure_latest_bundle(bundle)
|
|
1099
|
+
mock_gb.assert_called_with(bundle, "54321", "10mpy")
|
|
1100
|
+
assert bundle.current_tag == "54321"
|
|
1101
|
+
assert bundle.platform is None
|
|
1102
|
+
assert mock_json.dump.call_count == 1
|
|
1103
|
+
|
|
1104
|
+
|
|
1077
1105
|
def test_get_bundle():
|
|
1078
1106
|
"""
|
|
1079
1107
|
Ensure the expected calls are made to get the referenced bundle and the
|
|
@@ -1095,20 +1123,18 @@ def test_get_bundle():
|
|
|
1095
1123
|
"circup.command_utils.zipfile"
|
|
1096
1124
|
) as mock_zipfile, mock.patch(
|
|
1097
1125
|
"circup.Bundle.add_tag"
|
|
1098
|
-
)
|
|
1126
|
+
):
|
|
1099
1127
|
mock_click.progressbar = mock_progress
|
|
1100
1128
|
mock_requests.get().status_code = mock_requests.codes.ok
|
|
1101
1129
|
mock_requests.get.reset_mock()
|
|
1102
1130
|
tag = "12345"
|
|
1103
1131
|
bundle = circup.Bundle(TEST_BUNDLE_NAME)
|
|
1104
|
-
get_bundle(bundle, tag)
|
|
1105
|
-
|
|
1106
|
-
_bundle_count = len(PLATFORMS)
|
|
1132
|
+
get_bundle(bundle, tag, "py")
|
|
1133
|
+
_bundle_count = 1
|
|
1107
1134
|
assert mock_requests.get.call_count == _bundle_count
|
|
1108
1135
|
assert mock_open.call_count == _bundle_count
|
|
1109
1136
|
assert mock_zipfile.ZipFile.call_count == _bundle_count
|
|
1110
1137
|
assert mock_zipfile.ZipFile().__enter__().extractall.call_count == _bundle_count
|
|
1111
|
-
assert mock_add_tag.call_count == 1
|
|
1112
1138
|
|
|
1113
1139
|
|
|
1114
1140
|
def test_get_bundle_network_error():
|
|
@@ -1127,7 +1153,7 @@ def test_get_bundle_network_error():
|
|
|
1127
1153
|
tag = "12345"
|
|
1128
1154
|
with pytest.raises(Exception) as ex:
|
|
1129
1155
|
bundle = circup.Bundle(TEST_BUNDLE_NAME)
|
|
1130
|
-
get_bundle(bundle, tag)
|
|
1156
|
+
get_bundle(bundle, tag, "py")
|
|
1131
1157
|
assert ex.value.args[0] == "Bang!"
|
|
1132
1158
|
url = (
|
|
1133
1159
|
"https://github.com/" + TEST_BUNDLE_NAME + "/releases/download"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|