circup 2.2.6__py3-none-any.whl → 2.4.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 +5 -7
- circup/bundle.py +138 -28
- circup/command_utils.py +260 -67
- circup/commands.py +175 -39
- circup/module.py +1 -9
- circup/shared.py +4 -25
- {circup-2.2.6.dist-info → circup-2.4.0.dist-info}/METADATA +1 -1
- circup-2.4.0.dist-info/RECORD +21 -0
- {circup-2.2.6.dist-info → circup-2.4.0.dist-info}/WHEEL +1 -1
- circup-2.2.6.dist-info/RECORD +0 -21
- {circup-2.2.6.dist-info → circup-2.4.0.dist-info}/entry_points.txt +0 -0
- {circup-2.2.6.dist-info → circup-2.4.0.dist-info}/licenses/LICENSE +0 -0
- {circup-2.2.6.dist-info → circup-2.4.0.dist-info}/top_level.txt +0 -0
circup/backends.py
CHANGED
|
@@ -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.
|
circup/bundle.py
CHANGED
|
@@ -10,22 +10,26 @@ 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,
|
|
16
18
|
REQUESTS_TIMEOUT,
|
|
17
|
-
tags_data_load,
|
|
18
19
|
get_latest_release_from_url,
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
from circup.logging import logger
|
|
22
23
|
|
|
23
24
|
|
|
24
|
-
class Bundle:
|
|
25
|
+
class Bundle: # pylint: disable=too-many-instance-attributes
|
|
25
26
|
"""
|
|
26
27
|
All the links and file names for a bundle
|
|
27
28
|
"""
|
|
28
29
|
|
|
30
|
+
#: Avoid requests to the internet
|
|
31
|
+
offline = False
|
|
32
|
+
|
|
29
33
|
def __init__(self, repo):
|
|
30
34
|
"""
|
|
31
35
|
Initialise a Bundle created from its github info.
|
|
@@ -46,29 +50,42 @@ class Bundle:
|
|
|
46
50
|
# tag
|
|
47
51
|
self._current = None
|
|
48
52
|
self._latest = None
|
|
53
|
+
self.pinned_tag = None
|
|
54
|
+
self._available = []
|
|
55
|
+
#
|
|
56
|
+
self.platform = None
|
|
49
57
|
|
|
50
|
-
def lib_dir(self,
|
|
58
|
+
def lib_dir(self, source=False):
|
|
51
59
|
"""
|
|
52
|
-
This bundle's lib directory for the
|
|
60
|
+
This bundle's lib directory for the bundle's source or compiled version.
|
|
53
61
|
|
|
54
|
-
:param
|
|
55
|
-
|
|
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.
|
|
56
67
|
"""
|
|
57
68
|
tag = self.current_tag
|
|
69
|
+
platform = "py" if source or not self.platform else self.platform
|
|
58
70
|
return os.path.join(
|
|
59
71
|
self.dir.format(platform=platform),
|
|
60
72
|
self.basename.format(platform=PLATFORMS[platform], tag=tag),
|
|
61
73
|
"lib",
|
|
62
74
|
)
|
|
63
75
|
|
|
64
|
-
def examples_dir(self,
|
|
76
|
+
def examples_dir(self, source=False):
|
|
65
77
|
"""
|
|
66
|
-
This bundle's examples directory for the
|
|
78
|
+
This bundle's examples directory for the bundle's source or compiled
|
|
79
|
+
version.
|
|
67
80
|
|
|
68
|
-
:param
|
|
69
|
-
|
|
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.
|
|
70
86
|
"""
|
|
71
87
|
tag = self.current_tag
|
|
88
|
+
platform = "py" if source or not self.platform else self.platform
|
|
72
89
|
return os.path.join(
|
|
73
90
|
self.dir.format(platform=platform),
|
|
74
91
|
self.basename.format(platform=PLATFORMS[platform], tag=tag),
|
|
@@ -99,21 +116,34 @@ class Bundle:
|
|
|
99
116
|
@property
|
|
100
117
|
def current_tag(self):
|
|
101
118
|
"""
|
|
102
|
-
|
|
119
|
+
The current tag for the project. If the tag hasn't been explicitly set
|
|
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.
|
|
103
123
|
|
|
104
|
-
:return: The current
|
|
124
|
+
:return: The current tag value for the project.
|
|
105
125
|
"""
|
|
106
126
|
if self._current is None:
|
|
107
|
-
|
|
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
|
+
|
|
108
139
|
return self._current
|
|
109
140
|
|
|
110
141
|
@current_tag.setter
|
|
111
142
|
def current_tag(self, tag):
|
|
112
143
|
"""
|
|
113
|
-
Set the current
|
|
144
|
+
Set the current tag (after updating).
|
|
114
145
|
|
|
115
146
|
:param str tag: The new value for the current tag.
|
|
116
|
-
:return: The current cached tag value for the project.
|
|
117
147
|
"""
|
|
118
148
|
self._current = tag
|
|
119
149
|
|
|
@@ -125,31 +155,109 @@ class Bundle:
|
|
|
125
155
|
:return: The most recent tag value for the project.
|
|
126
156
|
"""
|
|
127
157
|
if self._latest is None:
|
|
128
|
-
self.
|
|
129
|
-
self.
|
|
130
|
-
|
|
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
|
+
)
|
|
131
164
|
return self._latest
|
|
132
165
|
|
|
166
|
+
@property
|
|
167
|
+
def available_tags(self):
|
|
168
|
+
"""
|
|
169
|
+
The locally available tags to use for the project.
|
|
170
|
+
|
|
171
|
+
:return: All tags available for the project.
|
|
172
|
+
"""
|
|
173
|
+
return tuple(self._available)
|
|
174
|
+
|
|
175
|
+
@available_tags.setter
|
|
176
|
+
def available_tags(self, tags):
|
|
177
|
+
"""
|
|
178
|
+
Set the available tags.
|
|
179
|
+
|
|
180
|
+
:param str|list tags: The new value for the locally available tags.
|
|
181
|
+
"""
|
|
182
|
+
if isinstance(tags, str):
|
|
183
|
+
tags = [tags]
|
|
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
|
|
193
|
+
|
|
194
|
+
def add_tag(self, tag: str) -> None:
|
|
195
|
+
"""
|
|
196
|
+
Add a tag to the list of available tags.
|
|
197
|
+
|
|
198
|
+
This will add the tag if it isn't already present in the list of
|
|
199
|
+
available tags. The tag will be added so that the list is sorted in an
|
|
200
|
+
increasing order. This ensures that that last tag is always the latest.
|
|
201
|
+
|
|
202
|
+
:param str tag: The tag to add to the list of available tags.
|
|
203
|
+
"""
|
|
204
|
+
if tag in self._available:
|
|
205
|
+
# The tag is already stored for some reason, lets not add it again
|
|
206
|
+
return
|
|
207
|
+
|
|
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)
|
|
225
|
+
|
|
133
226
|
def validate(self):
|
|
134
227
|
"""
|
|
135
|
-
Test the existence of the expected
|
|
228
|
+
Test the existence of the expected URL (not the content)
|
|
136
229
|
"""
|
|
137
230
|
tag = self.latest_tag
|
|
138
231
|
if not tag or tag == "releases":
|
|
139
232
|
if "--verbose" in sys.argv:
|
|
140
233
|
click.secho(f' Invalid tag "{tag}"', fg="red")
|
|
141
234
|
return False
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
if
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
# 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
|
|
151
243
|
return True
|
|
152
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
|
+
|
|
153
261
|
def __repr__(self):
|
|
154
262
|
"""
|
|
155
263
|
Helps with log files.
|
|
@@ -166,5 +274,7 @@ class Bundle:
|
|
|
166
274
|
"url_format": self.url_format,
|
|
167
275
|
"current": self._current,
|
|
168
276
|
"latest": self._latest,
|
|
277
|
+
"pinned": self.pinned_tag,
|
|
278
|
+
"available": self._available,
|
|
169
279
|
}
|
|
170
280
|
)
|
circup/command_utils.py
CHANGED
|
@@ -12,10 +12,10 @@ import os
|
|
|
12
12
|
|
|
13
13
|
from subprocess import check_output
|
|
14
14
|
import sys
|
|
15
|
-
import shutil
|
|
16
15
|
import zipfile
|
|
17
16
|
import json
|
|
18
17
|
import re
|
|
18
|
+
from pathlib import Path
|
|
19
19
|
import toml
|
|
20
20
|
import requests
|
|
21
21
|
import click
|
|
@@ -23,13 +23,13 @@ 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,
|
|
29
30
|
BUNDLE_CONFIG_LOCAL,
|
|
30
31
|
BUNDLE_DATA,
|
|
31
32
|
NOT_MCU_LIBRARIES,
|
|
32
|
-
tags_data_load,
|
|
33
33
|
)
|
|
34
34
|
from circup.logging import logger
|
|
35
35
|
from circup.module import Module
|
|
@@ -106,7 +106,7 @@ def completion_for_install(ctx, param, incomplete):
|
|
|
106
106
|
with the ``circup install`` command.
|
|
107
107
|
"""
|
|
108
108
|
# pylint: disable=unused-argument
|
|
109
|
-
available_modules = get_bundle_versions(get_bundles_list(), avoid_download=True)
|
|
109
|
+
available_modules = get_bundle_versions(get_bundles_list(None), avoid_download=True)
|
|
110
110
|
module_names = {m.replace(".py", "") for m in available_modules}
|
|
111
111
|
if incomplete:
|
|
112
112
|
module_names = [name for name in module_names if name.startswith(incomplete)]
|
|
@@ -121,7 +121,9 @@ def completion_for_example(ctx, param, incomplete):
|
|
|
121
121
|
"""
|
|
122
122
|
|
|
123
123
|
# pylint: disable=unused-argument, consider-iterating-dictionary
|
|
124
|
-
available_examples = get_bundle_examples(
|
|
124
|
+
available_examples = get_bundle_examples(
|
|
125
|
+
get_bundles_list(None), avoid_download=True
|
|
126
|
+
)
|
|
125
127
|
matching_examples = []
|
|
126
128
|
for term in incomplete:
|
|
127
129
|
_examples = [
|
|
@@ -134,41 +136,143 @@ def completion_for_example(ctx, param, incomplete):
|
|
|
134
136
|
return sorted(matching_examples)
|
|
135
137
|
|
|
136
138
|
|
|
137
|
-
def
|
|
139
|
+
def ensure_bundle_tag(bundle, tag):
|
|
138
140
|
"""
|
|
139
|
-
Ensure that there's a copy of the
|
|
140
|
-
|
|
141
|
+
Ensure that there's a copy of the library bundle with the version referenced
|
|
142
|
+
by the tag.
|
|
141
143
|
|
|
142
144
|
:param Bundle bundle: the target Bundle object.
|
|
145
|
+
:param tag: the target bundle's tag (version).
|
|
146
|
+
|
|
147
|
+
:return: If the bundle is available.
|
|
143
148
|
"""
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
155
|
+
if tag in bundle.available_tags:
|
|
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
|
+
)
|
|
152
163
|
else:
|
|
153
|
-
|
|
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
|
|
170
|
+
|
|
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:
|
|
186
|
+
try:
|
|
187
|
+
get_bundle(bundle, tag, "py")
|
|
188
|
+
except requests.exceptions.HTTPError as ex:
|
|
189
|
+
click.secho(
|
|
190
|
+
f"There was a problem downloading the 'py' platform for the '{bundle.key}' bundle.",
|
|
191
|
+
fg="red",
|
|
192
|
+
)
|
|
193
|
+
logger.exception(ex)
|
|
194
|
+
return False # Bundle isn't available
|
|
195
|
+
bundle.add_tag(tag)
|
|
196
|
+
tags_data_save_tags(bundle.key, bundle.available_tags)
|
|
154
197
|
|
|
155
|
-
if
|
|
156
|
-
logger.info("New version available (%s).", tag)
|
|
198
|
+
if do_update_compiled:
|
|
157
199
|
try:
|
|
158
|
-
get_bundle(bundle, tag)
|
|
159
|
-
tags_data_save_tag(bundle.key, tag)
|
|
200
|
+
get_bundle(bundle, tag, bundle.platform)
|
|
160
201
|
except requests.exceptions.HTTPError as ex:
|
|
161
|
-
# See #20 for reason for this
|
|
162
202
|
click.secho(
|
|
163
203
|
(
|
|
164
|
-
"There was a problem downloading
|
|
165
|
-
"
|
|
204
|
+
f"There was a problem downloading the '{bundle.platform}' platform for the "
|
|
205
|
+
f"'{bundle.key}' bundle.\nFalling back to source (.py) files."
|
|
166
206
|
),
|
|
167
207
|
fg="red",
|
|
168
208
|
)
|
|
169
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
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def ensure_latest_bundle(bundle):
|
|
217
|
+
"""
|
|
218
|
+
Ensure that there's a copy of the latest library bundle available so circup
|
|
219
|
+
can check the metadata contained therein.
|
|
220
|
+
|
|
221
|
+
:param Bundle bundle: the target Bundle object.
|
|
222
|
+
"""
|
|
223
|
+
logger.info("Checking library updates for %s.", bundle.key)
|
|
224
|
+
tag = bundle.latest_tag
|
|
225
|
+
is_available = ensure_bundle_tag(bundle, tag)
|
|
226
|
+
if is_available:
|
|
227
|
+
click.echo(f"Using latest bundle for {bundle.key} ({tag}).")
|
|
170
228
|
else:
|
|
171
|
-
|
|
229
|
+
if bundle.current_tag is None:
|
|
230
|
+
# See issue #20 for reason for this
|
|
231
|
+
click.secho("Please try again in a moment.", fg="red")
|
|
232
|
+
sys.exit(1)
|
|
233
|
+
else:
|
|
234
|
+
# See PR #184 for reason for this
|
|
235
|
+
click.secho(
|
|
236
|
+
f"Skipping and using existing bundle for {bundle.key} ({bundle.current_tag}).",
|
|
237
|
+
fg="red",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def ensure_pinned_bundle(bundle):
|
|
242
|
+
"""
|
|
243
|
+
Ensure that there's a copy of the pinned library bundle available so circup
|
|
244
|
+
can check the metadata contained therein.
|
|
245
|
+
|
|
246
|
+
:param Bundle bundle: the target Bundle object.
|
|
247
|
+
"""
|
|
248
|
+
logger.info("Checking library for %s.", bundle.key)
|
|
249
|
+
tag = bundle.pinned_tag
|
|
250
|
+
is_available = ensure_bundle_tag(bundle, tag)
|
|
251
|
+
if is_available:
|
|
252
|
+
click.echo(f"Using pinned bundle for {bundle.key} ({tag}).")
|
|
253
|
+
else:
|
|
254
|
+
click.secho(
|
|
255
|
+
(
|
|
256
|
+
"Check pinned version to make sure it is correct and check "
|
|
257
|
+
f"{bundle.url} to make sure the version ({tag}) exists."
|
|
258
|
+
),
|
|
259
|
+
fg="red",
|
|
260
|
+
)
|
|
261
|
+
sys.exit(1)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def ensure_bundle(bundle):
|
|
265
|
+
"""
|
|
266
|
+
Ensure that there's a copy of either the pinned library bundle, or if no
|
|
267
|
+
version is pinned, the latest library bundle available so circup can check
|
|
268
|
+
the metadata contained therein.
|
|
269
|
+
|
|
270
|
+
:param Bundle bundle: the target Bundle object.
|
|
271
|
+
"""
|
|
272
|
+
if bundle.pinned_tag is not None:
|
|
273
|
+
ensure_pinned_bundle(bundle)
|
|
274
|
+
else:
|
|
275
|
+
ensure_latest_bundle(bundle)
|
|
172
276
|
|
|
173
277
|
|
|
174
278
|
def find_device():
|
|
@@ -293,41 +397,39 @@ def find_modules(backend, bundles_list):
|
|
|
293
397
|
# pylint: enable=broad-except,too-many-locals
|
|
294
398
|
|
|
295
399
|
|
|
296
|
-
def get_bundle(bundle, tag):
|
|
400
|
+
def get_bundle(bundle, tag, platform):
|
|
297
401
|
"""
|
|
298
402
|
Downloads and extracts the version of the bundle with the referenced tag.
|
|
299
403
|
The resulting zip file is saved on the local filesystem.
|
|
300
404
|
|
|
301
405
|
:param Bundle bundle: the target Bundle object.
|
|
302
406
|
:param str tag: The GIT tag to use to download the bundle.
|
|
407
|
+
:param str platform: The platform string (i.e. '10mpy').
|
|
303
408
|
"""
|
|
304
|
-
click.echo(f"Downloading
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
with zipfile.ZipFile(temp_zip, "r") as zfile:
|
|
329
|
-
zfile.extractall(temp_dir)
|
|
330
|
-
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)
|
|
331
433
|
click.echo("\nOK\n")
|
|
332
434
|
|
|
333
435
|
|
|
@@ -347,9 +449,9 @@ def get_bundle_examples(bundles_list, avoid_download=False):
|
|
|
347
449
|
|
|
348
450
|
try:
|
|
349
451
|
for bundle in bundles_list:
|
|
350
|
-
if not avoid_download or not os.path.isdir(bundle.lib_dir(
|
|
351
|
-
|
|
352
|
-
path = bundle.examples_dir(
|
|
452
|
+
if not avoid_download or not os.path.isdir(bundle.lib_dir(source=True)):
|
|
453
|
+
ensure_bundle(bundle)
|
|
454
|
+
path = bundle.examples_dir(source=True)
|
|
353
455
|
meta_saved = os.path.join(path, "../bundle_examples.json")
|
|
354
456
|
if os.path.exists(meta_saved):
|
|
355
457
|
with open(meta_saved, "r", encoding="utf-8") as f:
|
|
@@ -383,9 +485,10 @@ def get_bundle_examples(bundles_list, avoid_download=False):
|
|
|
383
485
|
|
|
384
486
|
def get_bundle_versions(bundles_list, avoid_download=False):
|
|
385
487
|
"""
|
|
386
|
-
Returns a dictionary of metadata from modules in the
|
|
387
|
-
|
|
388
|
-
version
|
|
488
|
+
Returns a dictionary of metadata from modules in either the pinned release
|
|
489
|
+
if one is present in 'pyproject.toml', or the latest known release of the
|
|
490
|
+
library bundle. Uses the Python version (rather than the compiled version)
|
|
491
|
+
of the library modules.
|
|
389
492
|
|
|
390
493
|
:param List[Bundle] bundles_list: List of supported bundles as Bundle objects.
|
|
391
494
|
:param bool avoid_download: if True, download the bundle only if missing.
|
|
@@ -394,9 +497,9 @@ def get_bundle_versions(bundles_list, avoid_download=False):
|
|
|
394
497
|
"""
|
|
395
498
|
all_the_modules = dict()
|
|
396
499
|
for bundle in bundles_list:
|
|
397
|
-
if not avoid_download or not os.path.isdir(bundle.lib_dir(
|
|
398
|
-
|
|
399
|
-
path = bundle.lib_dir(
|
|
500
|
+
if not avoid_download or not os.path.isdir(bundle.lib_dir(source=True)):
|
|
501
|
+
ensure_bundle(bundle)
|
|
502
|
+
path = bundle.lib_dir(source=True)
|
|
400
503
|
path_modules = _get_modules_file(path, logger)
|
|
401
504
|
for name, module in path_modules.items():
|
|
402
505
|
module["bundle"] = bundle
|
|
@@ -443,14 +546,32 @@ def get_bundles_local_dict():
|
|
|
443
546
|
return dict()
|
|
444
547
|
|
|
445
548
|
|
|
446
|
-
def get_bundles_list():
|
|
549
|
+
def get_bundles_list(bundle_tags, platform_version=None):
|
|
447
550
|
"""
|
|
448
551
|
Retrieve the list of bundles from the config dictionary.
|
|
449
552
|
|
|
553
|
+
:param Dict[str,str]|None bundle_tags: Pinned bundle tags. These override
|
|
554
|
+
any tags found in the pyproject.toml.
|
|
555
|
+
:param str platform_version: The platform version needed for the current
|
|
556
|
+
device.
|
|
450
557
|
:return: List of supported bundles as Bundle objects.
|
|
451
558
|
"""
|
|
452
559
|
bundle_config = get_bundles_dict()
|
|
560
|
+
tags = tags_data_load()
|
|
561
|
+
pyproject = find_pyproject()
|
|
562
|
+
pinned_tags = (
|
|
563
|
+
pyproject_bundle_versions(pyproject) if pyproject is not None else None
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
if bundle_tags is not None:
|
|
567
|
+
pinned_tags = bundle_tags if pinned_tags is None else pinned_tags | bundle_tags
|
|
568
|
+
|
|
453
569
|
bundles_list = [Bundle(bundle_config[b]) for b in bundle_config]
|
|
570
|
+
for bundle in bundles_list:
|
|
571
|
+
bundle.platform = platform_version
|
|
572
|
+
bundle.available_tags = tags.get(bundle.key, [])
|
|
573
|
+
if pinned_tags is not None:
|
|
574
|
+
bundle.pinned_tag = pinned_tags.get(bundle.key)
|
|
454
575
|
logger.info("Using bundles: %s", ", ".join(b.key for b in bundles_list))
|
|
455
576
|
return bundles_list
|
|
456
577
|
|
|
@@ -613,15 +734,38 @@ def save_local_bundles(bundles_data):
|
|
|
613
734
|
os.unlink(BUNDLE_CONFIG_LOCAL)
|
|
614
735
|
|
|
615
736
|
|
|
616
|
-
def
|
|
737
|
+
def tags_data_load():
|
|
738
|
+
"""
|
|
739
|
+
Load the list of the version tags of the bundles on disk.
|
|
740
|
+
|
|
741
|
+
:return: a dict() of tags indexed by Bundle identifiers/keys.
|
|
617
742
|
"""
|
|
618
|
-
|
|
743
|
+
tags_data = None
|
|
744
|
+
try:
|
|
745
|
+
with open(BUNDLE_DATA, encoding="utf-8") as data:
|
|
746
|
+
try:
|
|
747
|
+
tags_data = json.load(data)
|
|
748
|
+
except json.decoder.JSONDecodeError as ex:
|
|
749
|
+
# Sometimes (why?) the JSON file becomes corrupt. In which case
|
|
750
|
+
# log it and carry on as if setting up for first time.
|
|
751
|
+
logger.error("Could not parse %s", BUNDLE_DATA)
|
|
752
|
+
logger.exception(ex)
|
|
753
|
+
except FileNotFoundError:
|
|
754
|
+
pass
|
|
755
|
+
if not isinstance(tags_data, dict):
|
|
756
|
+
tags_data = {}
|
|
757
|
+
return tags_data
|
|
758
|
+
|
|
759
|
+
|
|
760
|
+
def tags_data_save_tags(key, tags):
|
|
761
|
+
"""
|
|
762
|
+
Add or change the saved available tags value for a bundle.
|
|
619
763
|
|
|
620
764
|
:param str key: The bundle's identifier/key.
|
|
621
|
-
:param str
|
|
765
|
+
:param List[str] tags: The new tags for the bundle.
|
|
622
766
|
"""
|
|
623
|
-
tags_data = tags_data_load(
|
|
624
|
-
tags_data[key] =
|
|
767
|
+
tags_data = tags_data_load()
|
|
768
|
+
tags_data[key] = tags
|
|
625
769
|
with open(BUNDLE_DATA, "w", encoding="utf-8") as data:
|
|
626
770
|
json.dump(tags_data, data)
|
|
627
771
|
|
|
@@ -855,3 +999,52 @@ def is_virtual_env_active():
|
|
|
855
999
|
virtual environment, regardless how circup is installed.
|
|
856
1000
|
"""
|
|
857
1001
|
return "VIRTUAL_ENV" in os.environ
|
|
1002
|
+
|
|
1003
|
+
|
|
1004
|
+
def find_pyproject():
|
|
1005
|
+
"""
|
|
1006
|
+
Look for a pyproject.toml in the current directory or its parent directories.
|
|
1007
|
+
|
|
1008
|
+
:return: The path to the pyproject.toml for the project, or None if it
|
|
1009
|
+
couldn't be found.
|
|
1010
|
+
"""
|
|
1011
|
+
logger.info("Looking for pyproject.toml file.")
|
|
1012
|
+
cwd = Path.cwd()
|
|
1013
|
+
candidates = [cwd, cwd.parent]
|
|
1014
|
+
|
|
1015
|
+
for path in candidates:
|
|
1016
|
+
pyproject_file = path / "pyproject.toml"
|
|
1017
|
+
|
|
1018
|
+
if pyproject_file.exists():
|
|
1019
|
+
logger.info("Found pyproject.toml at '%s'", str(pyproject_file))
|
|
1020
|
+
return pyproject_file
|
|
1021
|
+
|
|
1022
|
+
logger.info("No pyproject.toml file found.")
|
|
1023
|
+
return None
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def pyproject_bundle_versions(pyproject_file):
|
|
1027
|
+
"""
|
|
1028
|
+
Check for specified bundle versions.
|
|
1029
|
+
"""
|
|
1030
|
+
pyproject_toml_data = toml.load(pyproject_file)
|
|
1031
|
+
return pyproject_toml_data.get("tool", {}).get("circup", {}).get("bundle-versions")
|
|
1032
|
+
|
|
1033
|
+
|
|
1034
|
+
def parse_cli_bundle_tags(bundle_tags_cli):
|
|
1035
|
+
"""Parse bundle tags that were provided from the command line."""
|
|
1036
|
+
bundle_tags = {}
|
|
1037
|
+
for bundle_tag_item in bundle_tags_cli:
|
|
1038
|
+
item = bundle_tag_item.split("=")
|
|
1039
|
+
if len(item) == 2:
|
|
1040
|
+
bundle_tags[item[0].strip()] = item[1].strip()
|
|
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)
|
circup/commands.py
CHANGED
|
@@ -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,
|
|
@@ -37,6 +41,8 @@ from circup.command_utils import (
|
|
|
37
41
|
libraries_from_auto_file,
|
|
38
42
|
get_dependencies,
|
|
39
43
|
get_bundles_local_dict,
|
|
44
|
+
parse_cli_bundle_tags,
|
|
45
|
+
pretty_supported_cpy_versions,
|
|
40
46
|
save_local_bundles,
|
|
41
47
|
get_bundles_dict,
|
|
42
48
|
completion_for_example,
|
|
@@ -67,6 +73,16 @@ from circup.command_utils import (
|
|
|
67
73
|
" You can optionally set an environment variable CIRCUP_WEBWORKFLOW_PASSWORD"
|
|
68
74
|
" instead of passing this argument. If both exist the CLI arg takes precedent.",
|
|
69
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
|
+
)
|
|
70
86
|
@click.option(
|
|
71
87
|
"--timeout",
|
|
72
88
|
default=30,
|
|
@@ -84,13 +100,41 @@ from circup.command_utils import (
|
|
|
84
100
|
help="Manual CircuitPython version. If provided in combination "
|
|
85
101
|
"with --board-id, it overrides the detected CPy version.",
|
|
86
102
|
)
|
|
103
|
+
@click.option(
|
|
104
|
+
"bundle_versions",
|
|
105
|
+
"--bundle-version",
|
|
106
|
+
multiple=True,
|
|
107
|
+
help="Specify the version to use for a bundle. Include the bundle name and "
|
|
108
|
+
"the version separated by '=', similar to the format of requirements.txt. "
|
|
109
|
+
"This option can be used multiple times for different bundles. Bundle "
|
|
110
|
+
"version values provided here will override any pinned values from the "
|
|
111
|
+
"pyproject.toml.",
|
|
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
|
+
)
|
|
87
120
|
@click.version_option(
|
|
88
121
|
prog_name="Circup",
|
|
89
122
|
message="%(prog)s, A CircuitPython module updater. Version %(version)s",
|
|
90
123
|
)
|
|
91
124
|
@click.pass_context
|
|
92
125
|
def main( # pylint: disable=too-many-locals
|
|
93
|
-
ctx,
|
|
126
|
+
ctx,
|
|
127
|
+
verbose,
|
|
128
|
+
path,
|
|
129
|
+
host,
|
|
130
|
+
port,
|
|
131
|
+
password,
|
|
132
|
+
offline,
|
|
133
|
+
timeout,
|
|
134
|
+
board_id,
|
|
135
|
+
cpy_version,
|
|
136
|
+
bundle_versions,
|
|
137
|
+
allow_unsupported,
|
|
94
138
|
): # pragma: no cover
|
|
95
139
|
"""
|
|
96
140
|
A tool to manage and update libraries on a CircuitPython device.
|
|
@@ -98,6 +142,10 @@ def main( # pylint: disable=too-many-locals
|
|
|
98
142
|
# pylint: disable=too-many-arguments,too-many-branches,too-many-statements,too-many-locals, R0801
|
|
99
143
|
ctx.ensure_object(dict)
|
|
100
144
|
ctx.obj["TIMEOUT"] = timeout
|
|
145
|
+
ctx.obj["BUNDLE_TAGS"] = (
|
|
146
|
+
parse_cli_bundle_tags(bundle_versions) if len(bundle_versions) > 0 else None
|
|
147
|
+
)
|
|
148
|
+
Bundle.offline = offline
|
|
101
149
|
|
|
102
150
|
if password is None:
|
|
103
151
|
password = os.getenv("CIRCUP_WEBWORKFLOW_PASSWORD")
|
|
@@ -154,20 +202,22 @@ def main( # pylint: disable=too-many-locals
|
|
|
154
202
|
|
|
155
203
|
logger.info("### Started Circup ###")
|
|
156
204
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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)
|
|
162
215
|
|
|
163
216
|
# stop early if the command is boardless
|
|
164
217
|
if ctx.invoked_subcommand in BOARDLESS_COMMANDS or "--help" in sys.argv:
|
|
165
218
|
return
|
|
166
219
|
|
|
167
220
|
ctx.obj["DEVICE_PATH"] = device_path
|
|
168
|
-
latest_version = get_latest_release_from_url(
|
|
169
|
-
"https://github.com/adafruit/circuitpython/releases/latest", logger
|
|
170
|
-
)
|
|
171
221
|
|
|
172
222
|
if device_path is None or not ctx.obj["backend"].is_device_present():
|
|
173
223
|
click.secho("Could not find a connected CircuitPython device.", fg="red")
|
|
@@ -178,27 +228,54 @@ def main( # pylint: disable=too-many-locals
|
|
|
178
228
|
if board_id is None or cpy_version is None
|
|
179
229
|
else (cpy_version, board_id)
|
|
180
230
|
)
|
|
231
|
+
major_version = cpy_version.split(".")[0]
|
|
232
|
+
bundle_platform = "{}mpy".format(major_version)
|
|
233
|
+
ctx.obj["DEVICE_PLATFORM_VERSION"] = bundle_platform
|
|
181
234
|
click.echo(
|
|
182
235
|
"Found device {} at {}, running CircuitPython {}.".format(
|
|
183
236
|
board_id, device_path, cpy_version
|
|
184
237
|
)
|
|
185
238
|
)
|
|
186
|
-
|
|
187
|
-
|
|
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:
|
|
188
267
|
click.secho(
|
|
189
|
-
"
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
fg="green",
|
|
268
|
+
"It is recommended to update to a supported version "
|
|
269
|
+
f"({pretty_supported_cpy_versions()}) to ensure compatability.",
|
|
270
|
+
fg="yellow",
|
|
193
271
|
)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
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)
|
|
202
279
|
|
|
203
280
|
|
|
204
281
|
@main.command()
|
|
@@ -210,7 +287,7 @@ def freeze(ctx, requirement): # pragma: no cover
|
|
|
210
287
|
device. Option -r saves output to requirements.txt file
|
|
211
288
|
"""
|
|
212
289
|
logger.info("Freeze")
|
|
213
|
-
modules = find_modules(ctx.obj["backend"], get_bundles_list())
|
|
290
|
+
modules = find_modules(ctx.obj["backend"], get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
|
|
214
291
|
if modules:
|
|
215
292
|
output = []
|
|
216
293
|
for module in modules:
|
|
@@ -258,7 +335,12 @@ def list_cli(ctx): # pragma: no cover
|
|
|
258
335
|
|
|
259
336
|
modules = [
|
|
260
337
|
m.row
|
|
261
|
-
for m in find_modules(
|
|
338
|
+
for m in find_modules(
|
|
339
|
+
ctx.obj["backend"],
|
|
340
|
+
get_bundles_list(
|
|
341
|
+
ctx.obj["BUNDLE_TAGS"], ctx.obj["DEVICE_PLATFORM_VERSION"]
|
|
342
|
+
),
|
|
343
|
+
)
|
|
262
344
|
if m.outofdate
|
|
263
345
|
]
|
|
264
346
|
if modules:
|
|
@@ -334,7 +416,10 @@ def install(
|
|
|
334
416
|
|
|
335
417
|
# pylint: disable=too-many-branches
|
|
336
418
|
# TODO: Ensure there's enough space on the device
|
|
337
|
-
|
|
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
|
+
)
|
|
338
423
|
mod_names = {}
|
|
339
424
|
for module, metadata in available_modules.items():
|
|
340
425
|
mod_names[module.replace(".py", "").lower()] = metadata
|
|
@@ -367,7 +452,7 @@ def install(
|
|
|
367
452
|
upgrade,
|
|
368
453
|
)
|
|
369
454
|
|
|
370
|
-
if stubs:
|
|
455
|
+
if stubs and not Bundle.offline:
|
|
371
456
|
# Check we are in a virtual environment
|
|
372
457
|
if not is_virtual_env_active():
|
|
373
458
|
if is_global_install_ok is None:
|
|
@@ -440,7 +525,7 @@ def example(ctx, examples, op_list, rename, overwrite):
|
|
|
440
525
|
else:
|
|
441
526
|
click.echo("Available example libraries:")
|
|
442
527
|
available_examples = get_bundle_examples(
|
|
443
|
-
get_bundles_list(), avoid_download=True
|
|
528
|
+
get_bundles_list(ctx.obj["BUNDLE_TAGS"]), avoid_download=True
|
|
444
529
|
)
|
|
445
530
|
lib_names = {
|
|
446
531
|
str(key.split(os.path.sep)[0]): value
|
|
@@ -451,7 +536,7 @@ def example(ctx, examples, op_list, rename, overwrite):
|
|
|
451
536
|
|
|
452
537
|
for example_arg in examples:
|
|
453
538
|
available_examples = get_bundle_examples(
|
|
454
|
-
get_bundles_list(), avoid_download=True
|
|
539
|
+
get_bundles_list(ctx.obj["BUNDLE_TAGS"]), avoid_download=True
|
|
455
540
|
)
|
|
456
541
|
if example_arg in available_examples:
|
|
457
542
|
filename = available_examples[example_arg].split(os.path.sep)[-1]
|
|
@@ -485,14 +570,15 @@ def example(ctx, examples, op_list, rename, overwrite):
|
|
|
485
570
|
|
|
486
571
|
@main.command()
|
|
487
572
|
@click.argument("match", required=False, nargs=1)
|
|
488
|
-
|
|
573
|
+
@click.pass_context
|
|
574
|
+
def show(ctx, match): # pragma: no cover
|
|
489
575
|
"""
|
|
490
576
|
Show a list of available modules in the bundle. These are modules which
|
|
491
577
|
*could* be installed on the device.
|
|
492
578
|
|
|
493
579
|
If MATCH is specified only matching modules will be listed.
|
|
494
580
|
"""
|
|
495
|
-
available_modules = get_bundle_versions(get_bundles_list())
|
|
581
|
+
available_modules = get_bundle_versions(get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
|
|
496
582
|
module_names = sorted([m.replace(".py", "") for m in available_modules])
|
|
497
583
|
if match is not None:
|
|
498
584
|
match = match.lower()
|
|
@@ -555,7 +641,9 @@ def update(ctx, update_all): # pragma: no cover
|
|
|
555
641
|
"""
|
|
556
642
|
logger.info("Update")
|
|
557
643
|
# Grab current modules.
|
|
558
|
-
bundles_list = get_bundles_list(
|
|
644
|
+
bundles_list = get_bundles_list(
|
|
645
|
+
ctx.obj["BUNDLE_TAGS"], ctx.obj["DEVICE_PLATFORM_VERSION"]
|
|
646
|
+
)
|
|
559
647
|
installed_modules = find_modules(ctx.obj["backend"], bundles_list)
|
|
560
648
|
modules_to_update = [m for m in installed_modules if m.outofdate]
|
|
561
649
|
|
|
@@ -654,13 +742,14 @@ def update(ctx, update_all): # pragma: no cover
|
|
|
654
742
|
|
|
655
743
|
@main.command("bundle-show")
|
|
656
744
|
@click.option("--modules", is_flag=True, help="List all the modules per bundle.")
|
|
657
|
-
|
|
745
|
+
@click.pass_context
|
|
746
|
+
def bundle_show(ctx, modules):
|
|
658
747
|
"""
|
|
659
|
-
Show the list of bundles, default and local, with URL, current version
|
|
660
|
-
and latest version retrieved from the web.
|
|
748
|
+
Show the list of bundles, default and local, with URL, current version,
|
|
749
|
+
available versions, and latest version retrieved from the web.
|
|
661
750
|
"""
|
|
662
751
|
local_bundles = get_bundles_local_dict().values()
|
|
663
|
-
bundles = get_bundles_list()
|
|
752
|
+
bundles = get_bundles_list(ctx.obj["BUNDLE_TAGS"])
|
|
664
753
|
available_modules = get_bundle_versions(bundles)
|
|
665
754
|
|
|
666
755
|
for bundle in bundles:
|
|
@@ -669,7 +758,13 @@ def bundle_show(modules):
|
|
|
669
758
|
else:
|
|
670
759
|
click.secho(bundle.key, fg="green")
|
|
671
760
|
click.echo(" " + bundle.url)
|
|
672
|
-
click.echo(
|
|
761
|
+
click.echo(
|
|
762
|
+
" version = "
|
|
763
|
+
+ bundle.current_tag
|
|
764
|
+
+ (" (pinned)" if bundle.pinned_tag is not None else "")
|
|
765
|
+
)
|
|
766
|
+
click.echo(" available versions:")
|
|
767
|
+
click.echo(" " + "\n ".join(bundle.available_tags))
|
|
673
768
|
if modules:
|
|
674
769
|
click.echo("Modules:")
|
|
675
770
|
for name, mod in sorted(available_modules.items()):
|
|
@@ -695,6 +790,10 @@ def bundle_add(ctx, bundle):
|
|
|
695
790
|
)
|
|
696
791
|
return
|
|
697
792
|
|
|
793
|
+
if Bundle.offline:
|
|
794
|
+
click.secho("Cannot add new bundle when '--offline' flag is present.", fg="red")
|
|
795
|
+
return
|
|
796
|
+
|
|
698
797
|
bundles_dict = get_bundles_local_dict()
|
|
699
798
|
modified = False
|
|
700
799
|
for bundle_repo in bundle:
|
|
@@ -739,7 +838,7 @@ def bundle_add(ctx, bundle):
|
|
|
739
838
|
# save the bundles list
|
|
740
839
|
save_local_bundles(bundles_dict)
|
|
741
840
|
# update and get the new bundles for the first time
|
|
742
|
-
get_bundle_versions(get_bundles_list())
|
|
841
|
+
get_bundle_versions(get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
|
|
743
842
|
|
|
744
843
|
|
|
745
844
|
@main.command("bundle-remove")
|
|
@@ -788,3 +887,40 @@ def bundle_remove(bundle, reset):
|
|
|
788
887
|
)
|
|
789
888
|
if modified:
|
|
790
889
|
save_local_bundles(bundles_local_dict)
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
@main.command()
|
|
893
|
+
@click.pass_context
|
|
894
|
+
def bundle_freeze(ctx): # pragma: no cover
|
|
895
|
+
"""
|
|
896
|
+
Output details of all the bundles for modules found on the connected
|
|
897
|
+
CIRCUITPYTHON device. Copying the output into pyproject.toml will pin the
|
|
898
|
+
bundles.
|
|
899
|
+
"""
|
|
900
|
+
logger.info("Bundle Freeze")
|
|
901
|
+
device_modules = ctx.obj["backend"].get_device_versions()
|
|
902
|
+
if not device_modules:
|
|
903
|
+
click.echo("No modules found on the device.")
|
|
904
|
+
return
|
|
905
|
+
|
|
906
|
+
available_modules = get_bundle_versions(get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
|
|
907
|
+
bundles_used = {}
|
|
908
|
+
for name in device_modules:
|
|
909
|
+
module = available_modules.get(name)
|
|
910
|
+
if module:
|
|
911
|
+
bundle = module["bundle"]
|
|
912
|
+
bundles_used[bundle.key] = bundle.current_tag
|
|
913
|
+
|
|
914
|
+
if bundles_used:
|
|
915
|
+
click.echo(
|
|
916
|
+
"Copy the following lines into your pyproject.toml to pin "
|
|
917
|
+
"the bundles used with modules on the device:\n"
|
|
918
|
+
)
|
|
919
|
+
output = ["[tool.circup.bundle-versions]"]
|
|
920
|
+
for bundle_name, version in bundles_used.items():
|
|
921
|
+
output.append(f'"{bundle_name}" = "{version}"')
|
|
922
|
+
for line in output:
|
|
923
|
+
click.echo(line)
|
|
924
|
+
logger.info(line)
|
|
925
|
+
else:
|
|
926
|
+
click.echo("No bundles used with the modules on the device.")
|
circup/module.py
CHANGED
|
@@ -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:
|
circup/shared.py
CHANGED
|
@@ -9,7 +9,6 @@ and Backend class functions.
|
|
|
9
9
|
import glob
|
|
10
10
|
import os
|
|
11
11
|
import re
|
|
12
|
-
import json
|
|
13
12
|
import importlib.resources
|
|
14
13
|
import appdirs
|
|
15
14
|
import requests
|
|
@@ -23,7 +22,10 @@ BAD_FILE_FORMAT = "Invalid"
|
|
|
23
22
|
DATA_DIR = appdirs.user_data_dir(appname="circup", appauthor="adafruit")
|
|
24
23
|
|
|
25
24
|
#: Module formats list (and the other form used in github files)
|
|
26
|
-
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"]
|
|
27
29
|
|
|
28
30
|
#: Timeout for requests calls like get()
|
|
29
31
|
REQUESTS_TIMEOUT = 30
|
|
@@ -192,29 +194,6 @@ def extract_metadata(path, logger):
|
|
|
192
194
|
return result
|
|
193
195
|
|
|
194
196
|
|
|
195
|
-
def tags_data_load(logger):
|
|
196
|
-
"""
|
|
197
|
-
Load the list of the version tags of the bundles on disk.
|
|
198
|
-
|
|
199
|
-
:return: a dict() of tags indexed by Bundle identifiers/keys.
|
|
200
|
-
"""
|
|
201
|
-
tags_data = None
|
|
202
|
-
try:
|
|
203
|
-
with open(BUNDLE_DATA, encoding="utf-8") as data:
|
|
204
|
-
try:
|
|
205
|
-
tags_data = json.load(data)
|
|
206
|
-
except json.decoder.JSONDecodeError as ex:
|
|
207
|
-
# Sometimes (why?) the JSON file becomes corrupt. In which case
|
|
208
|
-
# log it and carry on as if setting up for first time.
|
|
209
|
-
logger.error("Could not parse %s", BUNDLE_DATA)
|
|
210
|
-
logger.exception(ex)
|
|
211
|
-
except FileNotFoundError:
|
|
212
|
-
pass
|
|
213
|
-
if not isinstance(tags_data, dict):
|
|
214
|
-
tags_data = {}
|
|
215
|
-
return tags_data
|
|
216
|
-
|
|
217
|
-
|
|
218
197
|
def get_latest_release_from_url(url, logger):
|
|
219
198
|
"""
|
|
220
199
|
Find the tag name of the latest release by using HTTP HEAD and decoding the redirect.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
circup/__init__.py,sha256=9A98U3DyA14tm-7_d88fGSlLfEKz9T_85t8kXkErqRg,662
|
|
2
|
+
circup/backends.py,sha256=aH76wEusVCf2IeOCWrypHd2_tBo286TxOBkgYGwXeOI,39869
|
|
3
|
+
circup/bundle.py,sha256=qqDz0MjezKfSngldk8zNahNhPikuOZLAMwBUv7w75Mc,9442
|
|
4
|
+
circup/command_utils.py,sha256=k18vZe0ecAKANoB9SoXh39S-DjE_cC8rpZpr3-gyQ1I,37121
|
|
5
|
+
circup/commands.py,sha256=vAsmWyvTB3sjH4bdfGe9IQxPt3DC925mWdHNRDOeK00,34042
|
|
6
|
+
circup/lazy_metadata.py,sha256=69VidxGGWE13QwAAtMCPNTXTsQ2q5dJvMtclw4YaqEY,3764
|
|
7
|
+
circup/logging.py,sha256=hu4v8ljkXo8ru-cqs0W3PU-xEVvTO_qqMKDJM18OXbQ,1115
|
|
8
|
+
circup/module.py,sha256=ZrgZhBKuDWHfw9FzcXKjBYx-oybbDnH_IAdek3aSGwU,7131
|
|
9
|
+
circup/shared.py,sha256=d1imapk_esiuz6dMLe37Ck8lHW6W6L3djFpH8KBTAMk,8752
|
|
10
|
+
circup/config/bundle_config.json,sha256=zzpmfy0jD7TxpOOw2P_gqIISuMBG0enb_f3_VneQ5mI,135
|
|
11
|
+
circup/config/bundle_config.json.license,sha256=OOHNqDsViGFhmG9z8J0o98hYmub1CkYKiZB96Php6KE,80
|
|
12
|
+
circup/wwshell/README.rst,sha256=M_jFP0hwOcngF0RdosdeqmVOISNcPzyjTW3duzIu9A8,3617
|
|
13
|
+
circup/wwshell/README.rst.license,sha256=GhA0SoZGP7CReDam-JJk_UtIQIpQaZWQFzR26YSuMm4,107
|
|
14
|
+
circup/wwshell/__init__.py,sha256=CAPZiYrouWboyPx4KiWLBG_vf_n0MmArGqaFyTXGKWk,398
|
|
15
|
+
circup/wwshell/commands.py,sha256=-I5l7XeoDmvWWuZg5wHdt9qe__SBQ1EGmKwCDTBMeus,7454
|
|
16
|
+
circup-2.4.0.dist-info/licenses/LICENSE,sha256=bVlIMmSL_pqLCqae4hzixy9pYXD808IbgsMoQXTNLBk,1076
|
|
17
|
+
circup-2.4.0.dist-info/METADATA,sha256=iqTeOcWnEgpLE8jfAK8c05pqg8LRa1t74lnEnoHxuYM,13617
|
|
18
|
+
circup-2.4.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
19
|
+
circup-2.4.0.dist-info/entry_points.txt,sha256=FjTmwYD_ApgLRGifUrr_Ui1voW6fEzodQc3DKNzoAPc,69
|
|
20
|
+
circup-2.4.0.dist-info/top_level.txt,sha256=Qx6E0eZgSBE10ciNKsLZx8-TTy_9fEVZh7NLmn24KcU,7
|
|
21
|
+
circup-2.4.0.dist-info/RECORD,,
|
circup-2.2.6.dist-info/RECORD
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
circup/__init__.py,sha256=9A98U3DyA14tm-7_d88fGSlLfEKz9T_85t8kXkErqRg,662
|
|
2
|
-
circup/backends.py,sha256=g9Q9xCGZidwsEDL2Ga2cm50YYB54IiqlKUPcxj-pWZA,40008
|
|
3
|
-
circup/bundle.py,sha256=FEP4F470aJtwmm8jgTM3DgR3dj5SVwbX1tbyIRKVHn8,5327
|
|
4
|
-
circup/command_utils.py,sha256=_bYFfsfXoy6ERyTSmqt-RTNuIJN9ilYolKGTF42ebyk,30676
|
|
5
|
-
circup/commands.py,sha256=3clk6B6oF5noA31TnuCyzIHNxxFcsoxBCVBOoy-K8fo,29082
|
|
6
|
-
circup/lazy_metadata.py,sha256=69VidxGGWE13QwAAtMCPNTXTsQ2q5dJvMtclw4YaqEY,3764
|
|
7
|
-
circup/logging.py,sha256=hu4v8ljkXo8ru-cqs0W3PU-xEVvTO_qqMKDJM18OXbQ,1115
|
|
8
|
-
circup/module.py,sha256=33_kdy5BZn6COyIjAFZMpw00rTtPiryQZWFXQkMF8FY,7435
|
|
9
|
-
circup/shared.py,sha256=rribEZdoeyZRHxLiezyrDH0vb8DIP4bGOfMpRJm9M9w,9405
|
|
10
|
-
circup/config/bundle_config.json,sha256=zzpmfy0jD7TxpOOw2P_gqIISuMBG0enb_f3_VneQ5mI,135
|
|
11
|
-
circup/config/bundle_config.json.license,sha256=OOHNqDsViGFhmG9z8J0o98hYmub1CkYKiZB96Php6KE,80
|
|
12
|
-
circup/wwshell/README.rst,sha256=M_jFP0hwOcngF0RdosdeqmVOISNcPzyjTW3duzIu9A8,3617
|
|
13
|
-
circup/wwshell/README.rst.license,sha256=GhA0SoZGP7CReDam-JJk_UtIQIpQaZWQFzR26YSuMm4,107
|
|
14
|
-
circup/wwshell/__init__.py,sha256=CAPZiYrouWboyPx4KiWLBG_vf_n0MmArGqaFyTXGKWk,398
|
|
15
|
-
circup/wwshell/commands.py,sha256=-I5l7XeoDmvWWuZg5wHdt9qe__SBQ1EGmKwCDTBMeus,7454
|
|
16
|
-
circup-2.2.6.dist-info/licenses/LICENSE,sha256=bVlIMmSL_pqLCqae4hzixy9pYXD808IbgsMoQXTNLBk,1076
|
|
17
|
-
circup-2.2.6.dist-info/METADATA,sha256=bBGt9xAlnkAhdypwfT63GgbGSjfzee1JwHJ5FFC2Euw,13617
|
|
18
|
-
circup-2.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
19
|
-
circup-2.2.6.dist-info/entry_points.txt,sha256=FjTmwYD_ApgLRGifUrr_Ui1voW6fEzodQc3DKNzoAPc,69
|
|
20
|
-
circup-2.2.6.dist-info/top_level.txt,sha256=Qx6E0eZgSBE10ciNKsLZx8-TTy_9fEVZh7NLmn24KcU,7
|
|
21
|
-
circup-2.2.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|