circup 2.2.5__tar.gz → 2.3.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.2.5/circup.egg-info → circup-2.3.0}/PKG-INFO +1 -1
- {circup-2.2.5 → circup-2.3.0}/circup/bundle.py +57 -6
- {circup-2.2.5 → circup-2.3.0}/circup/command_utils.py +179 -38
- {circup-2.2.5 → circup-2.3.0}/circup/commands.py +100 -17
- {circup-2.2.5 → circup-2.3.0}/circup/shared.py +0 -24
- {circup-2.2.5 → circup-2.3.0/circup.egg-info}/PKG-INFO +1 -1
- {circup-2.2.5 → circup-2.3.0}/tests/test_circup.py +166 -23
- {circup-2.2.5 → circup-2.3.0}/.github/ISSUE_TEMPLATE.md +0 -0
- {circup-2.2.5 → circup-2.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {circup-2.2.5 → circup-2.3.0}/.github/workflows/build.yml +0 -0
- {circup-2.2.5 → circup-2.3.0}/.github/workflows/release.yml +0 -0
- {circup-2.2.5 → circup-2.3.0}/.gitignore +0 -0
- {circup-2.2.5 → circup-2.3.0}/.isort.cfg +0 -0
- {circup-2.2.5 → circup-2.3.0}/.pre-commit-config.yaml +0 -0
- {circup-2.2.5 → circup-2.3.0}/.pylintrc +0 -0
- {circup-2.2.5 → circup-2.3.0}/CODE_OF_CONDUCT.rst +0 -0
- {circup-2.2.5 → circup-2.3.0}/CODE_OF_CONDUCT.rst.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/CONTRIBUTING.rst +0 -0
- {circup-2.2.5 → circup-2.3.0}/CONTRIBUTING.rst.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/LICENSE +0 -0
- {circup-2.2.5 → circup-2.3.0}/LICENSES/CC-BY-4.0.txt +0 -0
- {circup-2.2.5 → circup-2.3.0}/LICENSES/MIT.txt +0 -0
- {circup-2.2.5 → circup-2.3.0}/LICENSES/Unlicense.txt +0 -0
- {circup-2.2.5 → circup-2.3.0}/README.rst +0 -0
- {circup-2.2.5 → circup-2.3.0}/README.rst.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup/__init__.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup/backends.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup/config/bundle_config.json +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup/config/bundle_config.json.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup/lazy_metadata.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup/logging.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup/module.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup/wwshell/README.rst +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup/wwshell/README.rst.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup/wwshell/__init__.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup/wwshell/commands.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup.egg-info/SOURCES.txt +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup.egg-info/dependency_links.txt +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup.egg-info/entry_points.txt +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup.egg-info/requires.txt +0 -0
- {circup-2.2.5 → circup-2.3.0}/circup.egg-info/top_level.txt +0 -0
- {circup-2.2.5 → circup-2.3.0}/docs/_static/favicon.ico +0 -0
- {circup-2.2.5 → circup-2.3.0}/docs/_static/favicon.ico.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/docs/conf.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/docs/index.rst +0 -0
- {circup-2.2.5 → circup-2.3.0}/docs/index.rst.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/docs/logo.png +0 -0
- {circup-2.2.5 → circup-2.3.0}/docs/logo.png.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/optional_requirements.txt +0 -0
- {circup-2.2.5 → circup-2.3.0}/optional_requirements.txt.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/pyproject.toml +0 -0
- {circup-2.2.5 → circup-2.3.0}/readthedocs.yml +0 -0
- {circup-2.2.5 → circup-2.3.0}/requirements.txt +0 -0
- {circup-2.2.5 → circup-2.3.0}/requirements.txt.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/setup.cfg +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/__init__.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/bad_module/__init__.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/bad_module/my_module.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/bad_python.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/bundle.json +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/bundle.json.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/device.json +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/device.json.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/dir_module/__init__.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/dir_module/my_module.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/import_styles.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/local_module.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/local_module_cp7.mpy +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/local_module_cp7.mpy.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mock_device/apps/test_app/import_styles.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mock_device/apps/test_app/import_styles_sub.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mock_device/boot_out.txt +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mock_device/boot_out.txt.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mock_device/import_styles.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mock_device/import_styles_sub.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mock_device/lib/adafruit_waveform/.gitkeep +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mock_device_2/.gitignore +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mock_device_2/boot_out.txt +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mock_device_2/boot_out.txt.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mock_device_2/code.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mock_device_2/import_styles_sub.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mock_device_2/package/__init__.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mock_device_2/package/other.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mount_exists.txt +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mount_exists.txt.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mount_missing.txt +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/mount_missing.txt.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/remote_module.py +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/test_bundle_config.json +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/test_bundle_config.json.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/test_bundle_config_local.json +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/test_bundle_config_local.json.license +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/test_module.mpy +0 -0
- {circup-2.2.5 → circup-2.3.0}/tests/test_module.mpy.license +0 -0
|
@@ -14,7 +14,6 @@ from circup.shared import (
|
|
|
14
14
|
DATA_DIR,
|
|
15
15
|
PLATFORMS,
|
|
16
16
|
REQUESTS_TIMEOUT,
|
|
17
|
-
tags_data_load,
|
|
18
17
|
get_latest_release_from_url,
|
|
19
18
|
)
|
|
20
19
|
|
|
@@ -46,6 +45,8 @@ class Bundle:
|
|
|
46
45
|
# tag
|
|
47
46
|
self._current = None
|
|
48
47
|
self._latest = None
|
|
48
|
+
self.pinned_tag = None
|
|
49
|
+
self._available = []
|
|
49
50
|
|
|
50
51
|
def lib_dir(self, platform):
|
|
51
52
|
"""
|
|
@@ -99,21 +100,27 @@ class Bundle:
|
|
|
99
100
|
@property
|
|
100
101
|
def current_tag(self):
|
|
101
102
|
"""
|
|
102
|
-
|
|
103
|
+
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. If there is no pinned tag,
|
|
105
|
+
this will be the latest available tag that is locally available.
|
|
103
106
|
|
|
104
|
-
:return: The current
|
|
107
|
+
:return: The current tag value for the project.
|
|
105
108
|
"""
|
|
106
109
|
if self._current is None:
|
|
107
|
-
self._current =
|
|
110
|
+
self._current = self.pinned_tag or (
|
|
111
|
+
# This represents the latest version locally available
|
|
112
|
+
self._available[-1]
|
|
113
|
+
if len(self._available) > 0
|
|
114
|
+
else None
|
|
115
|
+
)
|
|
108
116
|
return self._current
|
|
109
117
|
|
|
110
118
|
@current_tag.setter
|
|
111
119
|
def current_tag(self, tag):
|
|
112
120
|
"""
|
|
113
|
-
Set the current
|
|
121
|
+
Set the current tag (after updating).
|
|
114
122
|
|
|
115
123
|
:param str tag: The new value for the current tag.
|
|
116
|
-
:return: The current cached tag value for the project.
|
|
117
124
|
"""
|
|
118
125
|
self._current = tag
|
|
119
126
|
|
|
@@ -130,6 +137,48 @@ class Bundle:
|
|
|
130
137
|
)
|
|
131
138
|
return self._latest
|
|
132
139
|
|
|
140
|
+
@property
|
|
141
|
+
def available_tags(self):
|
|
142
|
+
"""
|
|
143
|
+
The locally available tags to use for the project.
|
|
144
|
+
|
|
145
|
+
:return: All tags available for the project.
|
|
146
|
+
"""
|
|
147
|
+
return tuple(self._available)
|
|
148
|
+
|
|
149
|
+
@available_tags.setter
|
|
150
|
+
def available_tags(self, tags):
|
|
151
|
+
"""
|
|
152
|
+
Set the available tags.
|
|
153
|
+
|
|
154
|
+
:param str|list tags: The new value for the locally available tags.
|
|
155
|
+
"""
|
|
156
|
+
if isinstance(tags, str):
|
|
157
|
+
tags = [tags]
|
|
158
|
+
self._available = sorted(tags)
|
|
159
|
+
|
|
160
|
+
def add_tag(self, tag: str) -> None:
|
|
161
|
+
"""
|
|
162
|
+
Add a tag to the list of available tags.
|
|
163
|
+
|
|
164
|
+
This will add the tag if it isn't already present in the list of
|
|
165
|
+
available tags. The tag will be added so that the list is sorted in an
|
|
166
|
+
increasing order. This ensures that that last tag is always the latest.
|
|
167
|
+
|
|
168
|
+
:param str tag: The tag to add to the list of available tags.
|
|
169
|
+
"""
|
|
170
|
+
if tag in self._available:
|
|
171
|
+
# The tag is already stored for some reason, lets not add it again
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
for rev_i, available_tag in enumerate(reversed(self._available)):
|
|
175
|
+
if int(tag) > int(available_tag):
|
|
176
|
+
i = len(self._available) - rev_i
|
|
177
|
+
self._available.insert(i, tag)
|
|
178
|
+
break
|
|
179
|
+
else:
|
|
180
|
+
self._available.insert(0, tag)
|
|
181
|
+
|
|
133
182
|
def validate(self):
|
|
134
183
|
"""
|
|
135
184
|
Test the existence of the expected URLs (not their content)
|
|
@@ -166,5 +215,7 @@ class Bundle:
|
|
|
166
215
|
"url_format": self.url_format,
|
|
167
216
|
"current": self._current,
|
|
168
217
|
"latest": self._latest,
|
|
218
|
+
"pinned": self.pinned_tag,
|
|
219
|
+
"available": self._available,
|
|
169
220
|
}
|
|
170
221
|
)
|
|
@@ -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
|
|
@@ -29,7 +29,6 @@ from circup.shared import (
|
|
|
29
29
|
BUNDLE_CONFIG_LOCAL,
|
|
30
30
|
BUNDLE_DATA,
|
|
31
31
|
NOT_MCU_LIBRARIES,
|
|
32
|
-
tags_data_load,
|
|
33
32
|
)
|
|
34
33
|
from circup.logging import logger
|
|
35
34
|
from circup.module import Module
|
|
@@ -106,7 +105,7 @@ def completion_for_install(ctx, param, incomplete):
|
|
|
106
105
|
with the ``circup install`` command.
|
|
107
106
|
"""
|
|
108
107
|
# pylint: disable=unused-argument
|
|
109
|
-
available_modules = get_bundle_versions(get_bundles_list(), avoid_download=True)
|
|
108
|
+
available_modules = get_bundle_versions(get_bundles_list(None), avoid_download=True)
|
|
110
109
|
module_names = {m.replace(".py", "") for m in available_modules}
|
|
111
110
|
if incomplete:
|
|
112
111
|
module_names = [name for name in module_names if name.startswith(incomplete)]
|
|
@@ -121,28 +120,33 @@ def completion_for_example(ctx, param, incomplete):
|
|
|
121
120
|
"""
|
|
122
121
|
|
|
123
122
|
# pylint: disable=unused-argument, consider-iterating-dictionary
|
|
124
|
-
available_examples = get_bundle_examples(
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
123
|
+
available_examples = get_bundle_examples(
|
|
124
|
+
get_bundles_list(None), avoid_download=True
|
|
125
|
+
)
|
|
126
|
+
matching_examples = []
|
|
127
|
+
for term in incomplete:
|
|
128
|
+
_examples = [
|
|
129
|
+
example_path
|
|
130
|
+
for example_path in available_examples.keys()
|
|
131
|
+
if term in example_path
|
|
132
|
+
]
|
|
133
|
+
matching_examples.extend(_examples)
|
|
131
134
|
|
|
132
135
|
return sorted(matching_examples)
|
|
133
136
|
|
|
134
137
|
|
|
135
|
-
def
|
|
138
|
+
def ensure_bundle_tag(bundle, tag):
|
|
136
139
|
"""
|
|
137
|
-
Ensure that there's a copy of the
|
|
138
|
-
|
|
140
|
+
Ensure that there's a copy of the library bundle with the version referenced
|
|
141
|
+
by the tag.
|
|
139
142
|
|
|
140
143
|
:param Bundle bundle: the target Bundle object.
|
|
144
|
+
:param tag: the target bundle's tag (version).
|
|
145
|
+
|
|
146
|
+
:return: If the bundle is available.
|
|
141
147
|
"""
|
|
142
|
-
logger.info("Checking library updates for %s.", bundle.key)
|
|
143
|
-
tag = bundle.latest_tag
|
|
144
148
|
do_update = False
|
|
145
|
-
if tag
|
|
149
|
+
if tag in bundle.available_tags:
|
|
146
150
|
for platform in PLATFORMS:
|
|
147
151
|
# missing directories (new platform added on an existing install
|
|
148
152
|
# or side effect of pytest or network errors)
|
|
@@ -154,19 +158,78 @@ def ensure_latest_bundle(bundle):
|
|
|
154
158
|
logger.info("New version available (%s).", tag)
|
|
155
159
|
try:
|
|
156
160
|
get_bundle(bundle, tag)
|
|
157
|
-
|
|
161
|
+
tags_data_save_tags(bundle.key, bundle.available_tags)
|
|
158
162
|
except requests.exceptions.HTTPError as ex:
|
|
159
|
-
# See #20 for reason for this
|
|
160
163
|
click.secho(
|
|
161
|
-
|
|
162
|
-
"There was a problem downloading that platform bundle. "
|
|
163
|
-
"Skipping and using existing download if available."
|
|
164
|
-
),
|
|
165
|
-
fg="red",
|
|
164
|
+
f"There was a problem downloading the {bundle.key} bundle.", fg="red"
|
|
166
165
|
)
|
|
167
166
|
logger.exception(ex)
|
|
167
|
+
return False
|
|
168
|
+
else:
|
|
169
|
+
logger.info("Current bundle version available (%s).", tag)
|
|
170
|
+
return True
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def ensure_latest_bundle(bundle):
|
|
174
|
+
"""
|
|
175
|
+
Ensure that there's a copy of the latest library bundle available so circup
|
|
176
|
+
can check the metadata contained therein.
|
|
177
|
+
|
|
178
|
+
:param Bundle bundle: the target Bundle object.
|
|
179
|
+
"""
|
|
180
|
+
logger.info("Checking library updates for %s.", bundle.key)
|
|
181
|
+
tag = bundle.latest_tag
|
|
182
|
+
is_available = ensure_bundle_tag(bundle, tag)
|
|
183
|
+
if is_available:
|
|
184
|
+
click.echo(f"Using latest bundle for {bundle.key} ({tag}).")
|
|
168
185
|
else:
|
|
169
|
-
|
|
186
|
+
if bundle.current_tag is None:
|
|
187
|
+
# See issue #20 for reason for this
|
|
188
|
+
click.secho("Please try again in a moment.", fg="red")
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
else:
|
|
191
|
+
# See PR #184 for reason for this
|
|
192
|
+
click.secho(
|
|
193
|
+
f"Skipping and using existing bundle for {bundle.key} ({bundle.current_tag}).",
|
|
194
|
+
fg="red",
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def ensure_pinned_bundle(bundle):
|
|
199
|
+
"""
|
|
200
|
+
Ensure that there's a copy of the pinned library bundle available so circup
|
|
201
|
+
can check the metadata contained therein.
|
|
202
|
+
|
|
203
|
+
:param Bundle bundle: the target Bundle object.
|
|
204
|
+
"""
|
|
205
|
+
logger.info("Checking library for %s.", bundle.key)
|
|
206
|
+
tag = bundle.pinned_tag
|
|
207
|
+
is_available = ensure_bundle_tag(bundle, tag)
|
|
208
|
+
if is_available:
|
|
209
|
+
click.echo(f"Using pinned bundle for {bundle.key} ({tag}).")
|
|
210
|
+
else:
|
|
211
|
+
click.secho(
|
|
212
|
+
(
|
|
213
|
+
"Check pinned version to make sure it is correct and check "
|
|
214
|
+
f"{bundle.url} to make sure the version ({tag}) exists."
|
|
215
|
+
),
|
|
216
|
+
fg="red",
|
|
217
|
+
)
|
|
218
|
+
sys.exit(1)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def ensure_bundle(bundle):
|
|
222
|
+
"""
|
|
223
|
+
Ensure that there's a copy of either the pinned library bundle, or if no
|
|
224
|
+
version is pinned, the latest library bundle available so circup can check
|
|
225
|
+
the metadata contained therein.
|
|
226
|
+
|
|
227
|
+
:param Bundle bundle: the target Bundle object.
|
|
228
|
+
"""
|
|
229
|
+
if bundle.pinned_tag is not None:
|
|
230
|
+
ensure_pinned_bundle(bundle)
|
|
231
|
+
else:
|
|
232
|
+
ensure_latest_bundle(bundle)
|
|
170
233
|
|
|
171
234
|
|
|
172
235
|
def find_device():
|
|
@@ -299,7 +362,7 @@ def get_bundle(bundle, tag):
|
|
|
299
362
|
:param Bundle bundle: the target Bundle object.
|
|
300
363
|
:param str tag: The GIT tag to use to download the bundle.
|
|
301
364
|
"""
|
|
302
|
-
click.echo(f"Downloading
|
|
365
|
+
click.echo(f"Downloading bundles for {bundle.key} ({tag}).")
|
|
303
366
|
for platform, github_string in PLATFORMS.items():
|
|
304
367
|
# Report the platform: "8.x-mpy", etc.
|
|
305
368
|
click.echo(f"{github_string}:")
|
|
@@ -321,10 +384,9 @@ def get_bundle(bundle, tag):
|
|
|
321
384
|
pbar.update(len(chunk))
|
|
322
385
|
logger.info("Saved to %s", temp_zip)
|
|
323
386
|
temp_dir = bundle.dir.format(platform=platform)
|
|
324
|
-
if os.path.isdir(temp_dir):
|
|
325
|
-
shutil.rmtree(temp_dir)
|
|
326
387
|
with zipfile.ZipFile(temp_zip, "r") as zfile:
|
|
327
388
|
zfile.extractall(temp_dir)
|
|
389
|
+
bundle.add_tag(tag)
|
|
328
390
|
bundle.current_tag = tag
|
|
329
391
|
click.echo("\nOK\n")
|
|
330
392
|
|
|
@@ -346,7 +408,7 @@ def get_bundle_examples(bundles_list, avoid_download=False):
|
|
|
346
408
|
try:
|
|
347
409
|
for bundle in bundles_list:
|
|
348
410
|
if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
|
|
349
|
-
|
|
411
|
+
ensure_bundle(bundle)
|
|
350
412
|
path = bundle.examples_dir("py")
|
|
351
413
|
meta_saved = os.path.join(path, "../bundle_examples.json")
|
|
352
414
|
if os.path.exists(meta_saved):
|
|
@@ -381,9 +443,10 @@ def get_bundle_examples(bundles_list, avoid_download=False):
|
|
|
381
443
|
|
|
382
444
|
def get_bundle_versions(bundles_list, avoid_download=False):
|
|
383
445
|
"""
|
|
384
|
-
Returns a dictionary of metadata from modules in the
|
|
385
|
-
|
|
386
|
-
version
|
|
446
|
+
Returns a dictionary of metadata from modules in either the pinned release
|
|
447
|
+
if one is present in 'pyproject.toml', or the latest known release of the
|
|
448
|
+
library bundle. Uses the Python version (rather than the compiled version)
|
|
449
|
+
of the library modules.
|
|
387
450
|
|
|
388
451
|
:param List[Bundle] bundles_list: List of supported bundles as Bundle objects.
|
|
389
452
|
:param bool avoid_download: if True, download the bundle only if missing.
|
|
@@ -393,7 +456,7 @@ def get_bundle_versions(bundles_list, avoid_download=False):
|
|
|
393
456
|
all_the_modules = dict()
|
|
394
457
|
for bundle in bundles_list:
|
|
395
458
|
if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
|
|
396
|
-
|
|
459
|
+
ensure_bundle(bundle)
|
|
397
460
|
path = bundle.lib_dir("py")
|
|
398
461
|
path_modules = _get_modules_file(path, logger)
|
|
399
462
|
for name, module in path_modules.items():
|
|
@@ -441,14 +504,29 @@ def get_bundles_local_dict():
|
|
|
441
504
|
return dict()
|
|
442
505
|
|
|
443
506
|
|
|
444
|
-
def get_bundles_list():
|
|
507
|
+
def get_bundles_list(bundle_tags):
|
|
445
508
|
"""
|
|
446
509
|
Retrieve the list of bundles from the config dictionary.
|
|
447
510
|
|
|
511
|
+
:param Dict[str,str]|None bundle_tags: Pinned bundle tags. These override
|
|
512
|
+
any tags found in the pyproject.toml.
|
|
448
513
|
:return: List of supported bundles as Bundle objects.
|
|
449
514
|
"""
|
|
450
515
|
bundle_config = get_bundles_dict()
|
|
516
|
+
tags = tags_data_load()
|
|
517
|
+
pyproject = find_pyproject()
|
|
518
|
+
pinned_tags = (
|
|
519
|
+
pyproject_bundle_versions(pyproject) if pyproject is not None else None
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
if bundle_tags is not None:
|
|
523
|
+
pinned_tags = bundle_tags if pinned_tags is None else pinned_tags | bundle_tags
|
|
524
|
+
|
|
451
525
|
bundles_list = [Bundle(bundle_config[b]) for b in bundle_config]
|
|
526
|
+
for bundle in bundles_list:
|
|
527
|
+
bundle.available_tags = tags.get(bundle.key, [])
|
|
528
|
+
if pinned_tags is not None:
|
|
529
|
+
bundle.pinned_tag = pinned_tags.get(bundle.key)
|
|
452
530
|
logger.info("Using bundles: %s", ", ".join(b.key for b in bundles_list))
|
|
453
531
|
return bundles_list
|
|
454
532
|
|
|
@@ -611,15 +689,38 @@ def save_local_bundles(bundles_data):
|
|
|
611
689
|
os.unlink(BUNDLE_CONFIG_LOCAL)
|
|
612
690
|
|
|
613
691
|
|
|
614
|
-
def
|
|
692
|
+
def tags_data_load():
|
|
615
693
|
"""
|
|
616
|
-
|
|
694
|
+
Load the list of the version tags of the bundles on disk.
|
|
695
|
+
|
|
696
|
+
:return: a dict() of tags indexed by Bundle identifiers/keys.
|
|
697
|
+
"""
|
|
698
|
+
tags_data = None
|
|
699
|
+
try:
|
|
700
|
+
with open(BUNDLE_DATA, encoding="utf-8") as data:
|
|
701
|
+
try:
|
|
702
|
+
tags_data = json.load(data)
|
|
703
|
+
except json.decoder.JSONDecodeError as ex:
|
|
704
|
+
# Sometimes (why?) the JSON file becomes corrupt. In which case
|
|
705
|
+
# log it and carry on as if setting up for first time.
|
|
706
|
+
logger.error("Could not parse %s", BUNDLE_DATA)
|
|
707
|
+
logger.exception(ex)
|
|
708
|
+
except FileNotFoundError:
|
|
709
|
+
pass
|
|
710
|
+
if not isinstance(tags_data, dict):
|
|
711
|
+
tags_data = {}
|
|
712
|
+
return tags_data
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def tags_data_save_tags(key, tags):
|
|
716
|
+
"""
|
|
717
|
+
Add or change the saved available tags value for a bundle.
|
|
617
718
|
|
|
618
719
|
:param str key: The bundle's identifier/key.
|
|
619
|
-
:param str
|
|
720
|
+
:param List[str] tags: The new tags for the bundle.
|
|
620
721
|
"""
|
|
621
|
-
tags_data = tags_data_load(
|
|
622
|
-
tags_data[key] =
|
|
722
|
+
tags_data = tags_data_load()
|
|
723
|
+
tags_data[key] = tags
|
|
623
724
|
with open(BUNDLE_DATA, "w", encoding="utf-8") as data:
|
|
624
725
|
json.dump(tags_data, data)
|
|
625
726
|
|
|
@@ -853,3 +954,43 @@ def is_virtual_env_active():
|
|
|
853
954
|
virtual environment, regardless how circup is installed.
|
|
854
955
|
"""
|
|
855
956
|
return "VIRTUAL_ENV" in os.environ
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
def find_pyproject():
|
|
960
|
+
"""
|
|
961
|
+
Look for a pyproject.toml in the current directory or its parent directories.
|
|
962
|
+
|
|
963
|
+
:return: The path to the pyproject.toml for the project, or None if it
|
|
964
|
+
couldn't be found.
|
|
965
|
+
"""
|
|
966
|
+
logger.info("Looking for pyproject.toml file.")
|
|
967
|
+
cwd = Path.cwd()
|
|
968
|
+
candidates = [cwd, cwd.parent]
|
|
969
|
+
|
|
970
|
+
for path in candidates:
|
|
971
|
+
pyproject_file = path / "pyproject.toml"
|
|
972
|
+
|
|
973
|
+
if pyproject_file.exists():
|
|
974
|
+
logger.info("Found pyproject.toml at '%s'", str(pyproject_file))
|
|
975
|
+
return pyproject_file
|
|
976
|
+
|
|
977
|
+
logger.info("No pyproject.toml file found.")
|
|
978
|
+
return None
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def pyproject_bundle_versions(pyproject_file):
|
|
982
|
+
"""
|
|
983
|
+
Check for specified bundle versions.
|
|
984
|
+
"""
|
|
985
|
+
pyproject_toml_data = toml.load(pyproject_file)
|
|
986
|
+
return pyproject_toml_data.get("tool", {}).get("circup", {}).get("bundle-versions")
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def parse_cli_bundle_tags(bundle_tags_cli):
|
|
990
|
+
"""Parse bundle tags that were provided from the command line."""
|
|
991
|
+
bundle_tags = {}
|
|
992
|
+
for bundle_tag_item in bundle_tags_cli:
|
|
993
|
+
item = bundle_tag_item.split("=")
|
|
994
|
+
if len(item) == 2:
|
|
995
|
+
bundle_tags[item[0].strip()] = item[1].strip()
|
|
996
|
+
return bundle_tags if len(bundle_tags) > 0 else None
|
|
@@ -37,6 +37,7 @@ from circup.command_utils import (
|
|
|
37
37
|
libraries_from_auto_file,
|
|
38
38
|
get_dependencies,
|
|
39
39
|
get_bundles_local_dict,
|
|
40
|
+
parse_cli_bundle_tags,
|
|
40
41
|
save_local_bundles,
|
|
41
42
|
get_bundles_dict,
|
|
42
43
|
completion_for_example,
|
|
@@ -84,13 +85,32 @@ from circup.command_utils import (
|
|
|
84
85
|
help="Manual CircuitPython version. If provided in combination "
|
|
85
86
|
"with --board-id, it overrides the detected CPy version.",
|
|
86
87
|
)
|
|
88
|
+
@click.option(
|
|
89
|
+
"bundle_versions",
|
|
90
|
+
"--bundle-version",
|
|
91
|
+
multiple=True,
|
|
92
|
+
help="Specify the version to use for a bundle. Include the bundle name and "
|
|
93
|
+
"the version separated by '=', similar to the format of requirements.txt. "
|
|
94
|
+
"This option can be used multiple times for different bundles. Bundle "
|
|
95
|
+
"version values provided here will override any pinned values from the "
|
|
96
|
+
"pyproject.toml.",
|
|
97
|
+
)
|
|
87
98
|
@click.version_option(
|
|
88
99
|
prog_name="Circup",
|
|
89
100
|
message="%(prog)s, A CircuitPython module updater. Version %(version)s",
|
|
90
101
|
)
|
|
91
102
|
@click.pass_context
|
|
92
103
|
def main( # pylint: disable=too-many-locals
|
|
93
|
-
ctx,
|
|
104
|
+
ctx,
|
|
105
|
+
verbose,
|
|
106
|
+
path,
|
|
107
|
+
host,
|
|
108
|
+
port,
|
|
109
|
+
password,
|
|
110
|
+
timeout,
|
|
111
|
+
board_id,
|
|
112
|
+
cpy_version,
|
|
113
|
+
bundle_versions,
|
|
94
114
|
): # pragma: no cover
|
|
95
115
|
"""
|
|
96
116
|
A tool to manage and update libraries on a CircuitPython device.
|
|
@@ -98,6 +118,9 @@ def main( # pylint: disable=too-many-locals
|
|
|
98
118
|
# pylint: disable=too-many-arguments,too-many-branches,too-many-statements,too-many-locals, R0801
|
|
99
119
|
ctx.ensure_object(dict)
|
|
100
120
|
ctx.obj["TIMEOUT"] = timeout
|
|
121
|
+
ctx.obj["BUNDLE_TAGS"] = (
|
|
122
|
+
parse_cli_bundle_tags(bundle_versions) if len(bundle_versions) > 0 else None
|
|
123
|
+
)
|
|
101
124
|
|
|
102
125
|
if password is None:
|
|
103
126
|
password = os.getenv("CIRCUP_WEBWORKFLOW_PASSWORD")
|
|
@@ -210,7 +233,7 @@ def freeze(ctx, requirement): # pragma: no cover
|
|
|
210
233
|
device. Option -r saves output to requirements.txt file
|
|
211
234
|
"""
|
|
212
235
|
logger.info("Freeze")
|
|
213
|
-
modules = find_modules(ctx.obj["backend"], get_bundles_list())
|
|
236
|
+
modules = find_modules(ctx.obj["backend"], get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
|
|
214
237
|
if modules:
|
|
215
238
|
output = []
|
|
216
239
|
for module in modules:
|
|
@@ -258,7 +281,9 @@ def list_cli(ctx): # pragma: no cover
|
|
|
258
281
|
|
|
259
282
|
modules = [
|
|
260
283
|
m.row
|
|
261
|
-
for m in find_modules(
|
|
284
|
+
for m in find_modules(
|
|
285
|
+
ctx.obj["backend"], get_bundles_list(ctx.obj["BUNDLE_TAGS"])
|
|
286
|
+
)
|
|
262
287
|
if m.outofdate
|
|
263
288
|
]
|
|
264
289
|
if modules:
|
|
@@ -334,7 +359,7 @@ def install(
|
|
|
334
359
|
|
|
335
360
|
# pylint: disable=too-many-branches
|
|
336
361
|
# TODO: Ensure there's enough space on the device
|
|
337
|
-
available_modules = get_bundle_versions(get_bundles_list())
|
|
362
|
+
available_modules = get_bundle_versions(get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
|
|
338
363
|
mod_names = {}
|
|
339
364
|
for module, metadata in available_modules.items():
|
|
340
365
|
mod_names[module.replace(".py", "").lower()] = metadata
|
|
@@ -415,11 +440,24 @@ def install(
|
|
|
415
440
|
)
|
|
416
441
|
@click.pass_context
|
|
417
442
|
def example(ctx, examples, op_list, rename, overwrite):
|
|
418
|
-
"""
|
|
443
|
+
"""\b
|
|
419
444
|
Copy named example(s) from a bundle onto the device. Multiple examples
|
|
420
445
|
can be installed at once by providing more than one example name, each
|
|
421
|
-
separated by a space.
|
|
446
|
+
separated by a space. Example names are in the form of:
|
|
447
|
+
[short_library_name]/[example_file_name_without_py_extension]
|
|
448
|
+
ex: circup example bmp5xx/bmp5xx_simpletest
|
|
449
|
+
\b
|
|
450
|
+
For a list of all available library short names run:
|
|
451
|
+
circup example --list
|
|
452
|
+
\b
|
|
453
|
+
To search for examples run:
|
|
454
|
+
circup example [searchterm] --list
|
|
455
|
+
ex: circup example mlx --list
|
|
422
456
|
"""
|
|
457
|
+
examples = list(examples)
|
|
458
|
+
for i, cur_example in enumerate(examples):
|
|
459
|
+
if cur_example.startswith("adafruit_"):
|
|
460
|
+
examples[i] = cur_example.replace("adafruit_", "")
|
|
423
461
|
|
|
424
462
|
if op_list:
|
|
425
463
|
if examples:
|
|
@@ -427,7 +465,7 @@ def example(ctx, examples, op_list, rename, overwrite):
|
|
|
427
465
|
else:
|
|
428
466
|
click.echo("Available example libraries:")
|
|
429
467
|
available_examples = get_bundle_examples(
|
|
430
|
-
get_bundles_list(), avoid_download=True
|
|
468
|
+
get_bundles_list(ctx.obj["BUNDLE_TAGS"]), avoid_download=True
|
|
431
469
|
)
|
|
432
470
|
lib_names = {
|
|
433
471
|
str(key.split(os.path.sep)[0]): value
|
|
@@ -438,7 +476,7 @@ def example(ctx, examples, op_list, rename, overwrite):
|
|
|
438
476
|
|
|
439
477
|
for example_arg in examples:
|
|
440
478
|
available_examples = get_bundle_examples(
|
|
441
|
-
get_bundles_list(), avoid_download=True
|
|
479
|
+
get_bundles_list(ctx.obj["BUNDLE_TAGS"]), avoid_download=True
|
|
442
480
|
)
|
|
443
481
|
if example_arg in available_examples:
|
|
444
482
|
filename = available_examples[example_arg].split(os.path.sep)[-1]
|
|
@@ -472,14 +510,15 @@ def example(ctx, examples, op_list, rename, overwrite):
|
|
|
472
510
|
|
|
473
511
|
@main.command()
|
|
474
512
|
@click.argument("match", required=False, nargs=1)
|
|
475
|
-
|
|
513
|
+
@click.pass_context
|
|
514
|
+
def show(ctx, match): # pragma: no cover
|
|
476
515
|
"""
|
|
477
516
|
Show a list of available modules in the bundle. These are modules which
|
|
478
517
|
*could* be installed on the device.
|
|
479
518
|
|
|
480
519
|
If MATCH is specified only matching modules will be listed.
|
|
481
520
|
"""
|
|
482
|
-
available_modules = get_bundle_versions(get_bundles_list())
|
|
521
|
+
available_modules = get_bundle_versions(get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
|
|
483
522
|
module_names = sorted([m.replace(".py", "") for m in available_modules])
|
|
484
523
|
if match is not None:
|
|
485
524
|
match = match.lower()
|
|
@@ -542,7 +581,7 @@ def update(ctx, update_all): # pragma: no cover
|
|
|
542
581
|
"""
|
|
543
582
|
logger.info("Update")
|
|
544
583
|
# Grab current modules.
|
|
545
|
-
bundles_list = get_bundles_list()
|
|
584
|
+
bundles_list = get_bundles_list(ctx.obj["BUNDLE_TAGS"])
|
|
546
585
|
installed_modules = find_modules(ctx.obj["backend"], bundles_list)
|
|
547
586
|
modules_to_update = [m for m in installed_modules if m.outofdate]
|
|
548
587
|
|
|
@@ -641,13 +680,14 @@ def update(ctx, update_all): # pragma: no cover
|
|
|
641
680
|
|
|
642
681
|
@main.command("bundle-show")
|
|
643
682
|
@click.option("--modules", is_flag=True, help="List all the modules per bundle.")
|
|
644
|
-
|
|
683
|
+
@click.pass_context
|
|
684
|
+
def bundle_show(ctx, modules):
|
|
645
685
|
"""
|
|
646
|
-
Show the list of bundles, default and local, with URL, current version
|
|
647
|
-
and latest version retrieved from the web.
|
|
686
|
+
Show the list of bundles, default and local, with URL, current version,
|
|
687
|
+
available versions, and latest version retrieved from the web.
|
|
648
688
|
"""
|
|
649
689
|
local_bundles = get_bundles_local_dict().values()
|
|
650
|
-
bundles = get_bundles_list()
|
|
690
|
+
bundles = get_bundles_list(ctx.obj["BUNDLE_TAGS"])
|
|
651
691
|
available_modules = get_bundle_versions(bundles)
|
|
652
692
|
|
|
653
693
|
for bundle in bundles:
|
|
@@ -656,7 +696,13 @@ def bundle_show(modules):
|
|
|
656
696
|
else:
|
|
657
697
|
click.secho(bundle.key, fg="green")
|
|
658
698
|
click.echo(" " + bundle.url)
|
|
659
|
-
click.echo(
|
|
699
|
+
click.echo(
|
|
700
|
+
" version = "
|
|
701
|
+
+ bundle.current_tag
|
|
702
|
+
+ (" (pinned)" if bundle.pinned_tag is not None else "")
|
|
703
|
+
)
|
|
704
|
+
click.echo(" available versions:")
|
|
705
|
+
click.echo(" " + "\n ".join(bundle.available_tags))
|
|
660
706
|
if modules:
|
|
661
707
|
click.echo("Modules:")
|
|
662
708
|
for name, mod in sorted(available_modules.items()):
|
|
@@ -726,7 +772,7 @@ def bundle_add(ctx, bundle):
|
|
|
726
772
|
# save the bundles list
|
|
727
773
|
save_local_bundles(bundles_dict)
|
|
728
774
|
# update and get the new bundles for the first time
|
|
729
|
-
get_bundle_versions(get_bundles_list())
|
|
775
|
+
get_bundle_versions(get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
|
|
730
776
|
|
|
731
777
|
|
|
732
778
|
@main.command("bundle-remove")
|
|
@@ -775,3 +821,40 @@ def bundle_remove(bundle, reset):
|
|
|
775
821
|
)
|
|
776
822
|
if modified:
|
|
777
823
|
save_local_bundles(bundles_local_dict)
|
|
824
|
+
|
|
825
|
+
|
|
826
|
+
@main.command()
|
|
827
|
+
@click.pass_context
|
|
828
|
+
def bundle_freeze(ctx): # pragma: no cover
|
|
829
|
+
"""
|
|
830
|
+
Output details of all the bundles for modules found on the connected
|
|
831
|
+
CIRCUITPYTHON device. Copying the output into pyproject.toml will pin the
|
|
832
|
+
bundles.
|
|
833
|
+
"""
|
|
834
|
+
logger.info("Bundle Freeze")
|
|
835
|
+
device_modules = ctx.obj["backend"].get_device_versions()
|
|
836
|
+
if not device_modules:
|
|
837
|
+
click.echo("No modules found on the device.")
|
|
838
|
+
return
|
|
839
|
+
|
|
840
|
+
available_modules = get_bundle_versions(get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
|
|
841
|
+
bundles_used = {}
|
|
842
|
+
for name in device_modules:
|
|
843
|
+
module = available_modules.get(name)
|
|
844
|
+
if module:
|
|
845
|
+
bundle = module["bundle"]
|
|
846
|
+
bundles_used[bundle.key] = bundle.current_tag
|
|
847
|
+
|
|
848
|
+
if bundles_used:
|
|
849
|
+
click.echo(
|
|
850
|
+
"Copy the following lines into your pyproject.toml to pin "
|
|
851
|
+
"the bundles used with modules on the device:\n"
|
|
852
|
+
)
|
|
853
|
+
output = ["[tool.circup.bundle-versions]"]
|
|
854
|
+
for bundle_name, version in bundles_used.items():
|
|
855
|
+
output.append(f'"{bundle_name}" = "{version}"')
|
|
856
|
+
for line in output:
|
|
857
|
+
click.echo(line)
|
|
858
|
+
logger.info(line)
|
|
859
|
+
else:
|
|
860
|
+
click.echo("No bundles used with the modules on the device.")
|