circup 2.0.1__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/__init__.py +26 -0
- circup/backends.py +957 -0
- circup/bundle.py +170 -0
- circup/command_utils.py +627 -0
- circup/commands.py +723 -0
- circup/config/bundle_config.json +5 -0
- circup/config/bundle_config.json.license +3 -0
- circup/logging.py +33 -0
- circup/module.py +209 -0
- circup/shared.py +218 -0
- circup-2.0.1.dist-info/LICENSE +21 -0
- circup-2.0.1.dist-info/METADATA +357 -0
- circup-2.0.1.dist-info/RECORD +16 -0
- circup-2.0.1.dist-info/WHEEL +5 -0
- circup-2.0.1.dist-info/entry_points.txt +2 -0
- circup-2.0.1.dist-info/top_level.txt +1 -0
circup/command_utils.py
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, 2024 Tim Cocks, written for Adafruit Industries
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
"""
|
|
5
|
+
Functions called from commands in order to provide behaviors and return information.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import ctypes
|
|
9
|
+
import glob
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
from subprocess import check_output
|
|
13
|
+
import sys
|
|
14
|
+
import shutil
|
|
15
|
+
import zipfile
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import toml
|
|
19
|
+
import findimports
|
|
20
|
+
import requests
|
|
21
|
+
import click
|
|
22
|
+
|
|
23
|
+
from circup.shared import (
|
|
24
|
+
PLATFORMS,
|
|
25
|
+
REQUESTS_TIMEOUT,
|
|
26
|
+
_get_modules_file,
|
|
27
|
+
BUNDLE_CONFIG_OVERWRITE,
|
|
28
|
+
BUNDLE_CONFIG_FILE,
|
|
29
|
+
BUNDLE_CONFIG_LOCAL,
|
|
30
|
+
BUNDLE_DATA,
|
|
31
|
+
NOT_MCU_LIBRARIES,
|
|
32
|
+
tags_data_load,
|
|
33
|
+
)
|
|
34
|
+
from circup.logging import logger
|
|
35
|
+
from circup.module import Module
|
|
36
|
+
from circup.bundle import Bundle
|
|
37
|
+
|
|
38
|
+
WARNING_IGNORE_MODULES = (
|
|
39
|
+
"typing-extensions",
|
|
40
|
+
"pyasn1",
|
|
41
|
+
"circuitpython-typing",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def clean_library_name(assumed_library_name):
|
|
46
|
+
"""
|
|
47
|
+
Most CP repos and library names are look like this:
|
|
48
|
+
|
|
49
|
+
repo: Adafruit_CircuitPython_LC709203F
|
|
50
|
+
library: adafruit_lc709203f
|
|
51
|
+
|
|
52
|
+
But some do not and this handles cleaning that up.
|
|
53
|
+
Also cleans up if the pypi or reponame is passed in instead of the
|
|
54
|
+
CP library name.
|
|
55
|
+
|
|
56
|
+
:param str assumed_library_name: An assumed name of a library from user
|
|
57
|
+
or requirements.txt entry
|
|
58
|
+
:return: str proper library name
|
|
59
|
+
"""
|
|
60
|
+
not_standard_names = {
|
|
61
|
+
# Assumed Name : Actual Name
|
|
62
|
+
"adafruit_adafruitio": "adafruit_io",
|
|
63
|
+
"adafruit_asyncio": "asyncio",
|
|
64
|
+
"adafruit_busdevice": "adafruit_bus_device",
|
|
65
|
+
"adafruit_connectionmanager": "adafruit_connection_manager",
|
|
66
|
+
"adafruit_display_button": "adafruit_button",
|
|
67
|
+
"adafruit_neopixel": "neopixel",
|
|
68
|
+
"adafruit_sd": "adafruit_sdcard",
|
|
69
|
+
"adafruit_simpleio": "simpleio",
|
|
70
|
+
"pimoroni_ltr559": "pimoroni_circuitpython_ltr559",
|
|
71
|
+
}
|
|
72
|
+
if "circuitpython" in assumed_library_name:
|
|
73
|
+
# convert repo or pypi name to common library name
|
|
74
|
+
assumed_library_name = (
|
|
75
|
+
assumed_library_name.replace("-circuitpython-", "_")
|
|
76
|
+
.replace("_circuitpython_", "_")
|
|
77
|
+
.replace("-", "_")
|
|
78
|
+
)
|
|
79
|
+
if assumed_library_name in not_standard_names:
|
|
80
|
+
return not_standard_names[assumed_library_name]
|
|
81
|
+
return assumed_library_name
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def completion_for_install(ctx, param, incomplete):
|
|
85
|
+
"""
|
|
86
|
+
Returns the list of available modules for the command line tab-completion
|
|
87
|
+
with the ``circup install`` command.
|
|
88
|
+
"""
|
|
89
|
+
# pylint: disable=unused-argument
|
|
90
|
+
available_modules = get_bundle_versions(get_bundles_list(), avoid_download=True)
|
|
91
|
+
module_names = {m.replace(".py", "") for m in available_modules}
|
|
92
|
+
if incomplete:
|
|
93
|
+
module_names = [name for name in module_names if name.startswith(incomplete)]
|
|
94
|
+
module_names.extend(glob.glob(f"{incomplete}*"))
|
|
95
|
+
return sorted(module_names)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def completion_for_example(ctx, param, incomplete):
|
|
99
|
+
"""
|
|
100
|
+
Returns the list of available modules for the command line tab-completion
|
|
101
|
+
with the ``circup example`` command.
|
|
102
|
+
"""
|
|
103
|
+
# pylint: disable=unused-argument, consider-iterating-dictionary
|
|
104
|
+
available_examples = get_bundle_examples(get_bundles_list(), avoid_download=True)
|
|
105
|
+
|
|
106
|
+
matching_examples = [
|
|
107
|
+
example_path
|
|
108
|
+
for example_path in available_examples.keys()
|
|
109
|
+
if example_path.startswith(incomplete)
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
return sorted(matching_examples)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def ensure_latest_bundle(bundle):
|
|
116
|
+
"""
|
|
117
|
+
Ensure that there's a copy of the latest library bundle available so circup
|
|
118
|
+
can check the metadata contained therein.
|
|
119
|
+
|
|
120
|
+
:param Bundle bundle: the target Bundle object.
|
|
121
|
+
"""
|
|
122
|
+
logger.info("Checking library updates for %s.", bundle.key)
|
|
123
|
+
tag = bundle.latest_tag
|
|
124
|
+
do_update = False
|
|
125
|
+
if tag == bundle.current_tag:
|
|
126
|
+
for platform in PLATFORMS:
|
|
127
|
+
# missing directories (new platform added on an existing install
|
|
128
|
+
# or side effect of pytest or network errors)
|
|
129
|
+
do_update = do_update or not os.path.isdir(bundle.lib_dir(platform))
|
|
130
|
+
else:
|
|
131
|
+
do_update = True
|
|
132
|
+
|
|
133
|
+
if do_update:
|
|
134
|
+
logger.info("New version available (%s).", tag)
|
|
135
|
+
try:
|
|
136
|
+
get_bundle(bundle, tag)
|
|
137
|
+
tags_data_save_tag(bundle.key, tag)
|
|
138
|
+
except requests.exceptions.HTTPError as ex:
|
|
139
|
+
# See #20 for reason for this
|
|
140
|
+
click.secho(
|
|
141
|
+
(
|
|
142
|
+
"There was a problem downloading that platform bundle. "
|
|
143
|
+
"Skipping and using existing download if available."
|
|
144
|
+
),
|
|
145
|
+
fg="red",
|
|
146
|
+
)
|
|
147
|
+
logger.exception(ex)
|
|
148
|
+
else:
|
|
149
|
+
logger.info("Current bundle up to date %s.", tag)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def find_device():
|
|
153
|
+
"""
|
|
154
|
+
Return the location on the filesystem for the connected CircuitPython device.
|
|
155
|
+
This is based upon how Mu discovers this information.
|
|
156
|
+
|
|
157
|
+
:return: The path to the device on the local filesystem.
|
|
158
|
+
"""
|
|
159
|
+
device_dir = None
|
|
160
|
+
# Attempt to find the path on the filesystem that represents the plugged in
|
|
161
|
+
# CIRCUITPY board.
|
|
162
|
+
if os.name == "posix":
|
|
163
|
+
# Linux / OSX
|
|
164
|
+
for mount_command in ["mount", "/sbin/mount"]:
|
|
165
|
+
try:
|
|
166
|
+
mount_output = check_output(mount_command).splitlines()
|
|
167
|
+
mounted_volumes = [x.split()[2] for x in mount_output]
|
|
168
|
+
for volume in mounted_volumes:
|
|
169
|
+
if volume.endswith(b"CIRCUITPY"):
|
|
170
|
+
device_dir = volume.decode("utf-8")
|
|
171
|
+
except FileNotFoundError:
|
|
172
|
+
continue
|
|
173
|
+
elif os.name == "nt":
|
|
174
|
+
# Windows
|
|
175
|
+
|
|
176
|
+
def get_volume_name(disk_name):
|
|
177
|
+
"""
|
|
178
|
+
Each disk or external device connected to windows has an attribute
|
|
179
|
+
called "volume name". This function returns the volume name for the
|
|
180
|
+
given disk/device.
|
|
181
|
+
|
|
182
|
+
Based upon answer given here: http://stackoverflow.com/a/12056414
|
|
183
|
+
"""
|
|
184
|
+
vol_name_buf = ctypes.create_unicode_buffer(1024)
|
|
185
|
+
ctypes.windll.kernel32.GetVolumeInformationW(
|
|
186
|
+
ctypes.c_wchar_p(disk_name),
|
|
187
|
+
vol_name_buf,
|
|
188
|
+
ctypes.sizeof(vol_name_buf),
|
|
189
|
+
None,
|
|
190
|
+
None,
|
|
191
|
+
None,
|
|
192
|
+
None,
|
|
193
|
+
0,
|
|
194
|
+
)
|
|
195
|
+
return vol_name_buf.value
|
|
196
|
+
|
|
197
|
+
#
|
|
198
|
+
# In certain circumstances, volumes are allocated to USB
|
|
199
|
+
# storage devices which cause a Windows popup to raise if their
|
|
200
|
+
# volume contains no media. Wrapping the check in SetErrorMode
|
|
201
|
+
# with SEM_FAILCRITICALERRORS (1) prevents this popup.
|
|
202
|
+
#
|
|
203
|
+
old_mode = ctypes.windll.kernel32.SetErrorMode(1)
|
|
204
|
+
try:
|
|
205
|
+
for disk in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
|
|
206
|
+
path = "{}:\\".format(disk)
|
|
207
|
+
if os.path.exists(path) and get_volume_name(path) == "CIRCUITPY":
|
|
208
|
+
device_dir = path
|
|
209
|
+
# Report only the FIRST device found.
|
|
210
|
+
break
|
|
211
|
+
finally:
|
|
212
|
+
ctypes.windll.kernel32.SetErrorMode(old_mode)
|
|
213
|
+
else:
|
|
214
|
+
# No support for unknown operating systems.
|
|
215
|
+
raise NotImplementedError('OS "{}" not supported.'.format(os.name))
|
|
216
|
+
logger.info("Found device: %s", device_dir)
|
|
217
|
+
return device_dir
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def find_modules(backend, bundles_list):
|
|
221
|
+
"""
|
|
222
|
+
Extracts metadata from the connected device and available bundles and
|
|
223
|
+
returns this as a list of Module instances representing the modules on the
|
|
224
|
+
device.
|
|
225
|
+
|
|
226
|
+
:param Backend backend: Backend with the device connection.
|
|
227
|
+
:param List[Bundle] bundles_list: List of supported bundles as Bundle objects.
|
|
228
|
+
:return: A list of Module instances describing the current state of the
|
|
229
|
+
modules on the connected device.
|
|
230
|
+
"""
|
|
231
|
+
# pylint: disable=broad-except,too-many-locals
|
|
232
|
+
try:
|
|
233
|
+
device_modules = backend.get_device_versions()
|
|
234
|
+
bundle_modules = get_bundle_versions(bundles_list)
|
|
235
|
+
result = []
|
|
236
|
+
for key, device_metadata in device_modules.items():
|
|
237
|
+
|
|
238
|
+
if key in bundle_modules:
|
|
239
|
+
path = device_metadata["path"]
|
|
240
|
+
bundle_metadata = bundle_modules[key]
|
|
241
|
+
repo = bundle_metadata.get("__repo__")
|
|
242
|
+
bundle = bundle_metadata.get("bundle")
|
|
243
|
+
device_version = device_metadata.get("__version__")
|
|
244
|
+
bundle_version = bundle_metadata.get("__version__")
|
|
245
|
+
mpy = device_metadata["mpy"]
|
|
246
|
+
compatibility = device_metadata.get("compatibility", (None, None))
|
|
247
|
+
module_name = (
|
|
248
|
+
path.split(os.sep)[-1]
|
|
249
|
+
if not path.endswith(os.sep)
|
|
250
|
+
else path[:-1].split(os.sep)[-1] + os.sep
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
m = Module(
|
|
254
|
+
module_name,
|
|
255
|
+
backend,
|
|
256
|
+
repo,
|
|
257
|
+
device_version,
|
|
258
|
+
bundle_version,
|
|
259
|
+
mpy,
|
|
260
|
+
bundle,
|
|
261
|
+
compatibility,
|
|
262
|
+
)
|
|
263
|
+
result.append(m)
|
|
264
|
+
return result
|
|
265
|
+
except Exception as ex:
|
|
266
|
+
# If it's not possible to get the device and bundle metadata, bail out
|
|
267
|
+
# with a friendly message and indication of what's gone wrong.
|
|
268
|
+
logger.exception(ex)
|
|
269
|
+
click.echo("There was a problem: {}".format(ex))
|
|
270
|
+
sys.exit(1)
|
|
271
|
+
# pylint: enable=broad-except,too-many-locals
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def get_bundle(bundle, tag):
|
|
275
|
+
"""
|
|
276
|
+
Downloads and extracts the version of the bundle with the referenced tag.
|
|
277
|
+
The resulting zip file is saved on the local filesystem.
|
|
278
|
+
|
|
279
|
+
:param Bundle bundle: the target Bundle object.
|
|
280
|
+
:param str tag: The GIT tag to use to download the bundle.
|
|
281
|
+
"""
|
|
282
|
+
click.echo(f"Downloading latest bundles for {bundle.key} ({tag}).")
|
|
283
|
+
for platform, github_string in PLATFORMS.items():
|
|
284
|
+
# Report the platform: "8.x-mpy", etc.
|
|
285
|
+
click.echo(f"{github_string}:")
|
|
286
|
+
url = bundle.url_format.format(platform=github_string, tag=tag)
|
|
287
|
+
logger.info("Downloading bundle: %s", url)
|
|
288
|
+
r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
|
|
289
|
+
# pylint: disable=no-member
|
|
290
|
+
if r.status_code != requests.codes.ok:
|
|
291
|
+
logger.warning("Unable to connect to %s", url)
|
|
292
|
+
r.raise_for_status()
|
|
293
|
+
# pylint: enable=no-member
|
|
294
|
+
total_size = int(r.headers.get("Content-Length"))
|
|
295
|
+
temp_zip = bundle.zip.format(platform=platform)
|
|
296
|
+
with click.progressbar(
|
|
297
|
+
r.iter_content(1024), label="Extracting:", length=total_size
|
|
298
|
+
) as pbar, open(temp_zip, "wb") as zip_fp:
|
|
299
|
+
for chunk in pbar:
|
|
300
|
+
zip_fp.write(chunk)
|
|
301
|
+
pbar.update(len(chunk))
|
|
302
|
+
logger.info("Saved to %s", temp_zip)
|
|
303
|
+
temp_dir = bundle.dir.format(platform=platform)
|
|
304
|
+
if os.path.isdir(temp_dir):
|
|
305
|
+
shutil.rmtree(temp_dir)
|
|
306
|
+
with zipfile.ZipFile(temp_zip, "r") as zfile:
|
|
307
|
+
zfile.extractall(temp_dir)
|
|
308
|
+
bundle.current_tag = tag
|
|
309
|
+
click.echo("\nOK\n")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def get_bundle_examples(bundles_list, avoid_download=False):
|
|
313
|
+
"""
|
|
314
|
+
Return a dictionary of metadata from examples in the all of the bundles
|
|
315
|
+
specified by bundles_list argument.
|
|
316
|
+
|
|
317
|
+
:param List[Bundle] bundles_list: List of supported bundles as Bundle objects.
|
|
318
|
+
:param bool avoid_download: if True, download the bundle only if missing.
|
|
319
|
+
:return: A dictionary of metadata about the examples available in the
|
|
320
|
+
library bundle.
|
|
321
|
+
"""
|
|
322
|
+
# pylint: disable=too-many-nested-blocks
|
|
323
|
+
all_the_examples = dict()
|
|
324
|
+
|
|
325
|
+
try:
|
|
326
|
+
for bundle in bundles_list:
|
|
327
|
+
if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
|
|
328
|
+
ensure_latest_bundle(bundle)
|
|
329
|
+
path = bundle.examples_dir("py")
|
|
330
|
+
path_examples = _get_modules_file(path, logger)
|
|
331
|
+
for lib_name, lib_metadata in path_examples.items():
|
|
332
|
+
for _dir_level in os.walk(lib_metadata["path"]):
|
|
333
|
+
for _file in _dir_level[2]:
|
|
334
|
+
_parts = _dir_level[0].split(os.path.sep)
|
|
335
|
+
_lib_name_index = _parts.index(lib_name)
|
|
336
|
+
_dirs = _parts[_lib_name_index:]
|
|
337
|
+
if _dirs[-1] == "":
|
|
338
|
+
_dirs.pop(-1)
|
|
339
|
+
slug = f"{os.path.sep}".join(_dirs + [_file.replace(".py", "")])
|
|
340
|
+
all_the_examples[slug] = os.path.join(_dir_level[0], _file)
|
|
341
|
+
|
|
342
|
+
except NotADirectoryError:
|
|
343
|
+
# Bundle does not have new style examples directory
|
|
344
|
+
# so we cannot include its examples.
|
|
345
|
+
pass
|
|
346
|
+
return all_the_examples
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def get_bundle_versions(bundles_list, avoid_download=False):
|
|
350
|
+
"""
|
|
351
|
+
Returns a dictionary of metadata from modules in the latest known release
|
|
352
|
+
of the library bundle. Uses the Python version (rather than the compiled
|
|
353
|
+
version) of the library modules.
|
|
354
|
+
|
|
355
|
+
:param List[Bundle] bundles_list: List of supported bundles as Bundle objects.
|
|
356
|
+
:param bool avoid_download: if True, download the bundle only if missing.
|
|
357
|
+
:return: A dictionary of metadata about the modules available in the
|
|
358
|
+
library bundle.
|
|
359
|
+
"""
|
|
360
|
+
all_the_modules = dict()
|
|
361
|
+
for bundle in bundles_list:
|
|
362
|
+
if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
|
|
363
|
+
ensure_latest_bundle(bundle)
|
|
364
|
+
path = bundle.lib_dir("py")
|
|
365
|
+
path_modules = _get_modules_file(path, logger)
|
|
366
|
+
for name, module in path_modules.items():
|
|
367
|
+
module["bundle"] = bundle
|
|
368
|
+
if name not in all_the_modules: # here we decide the order of priority
|
|
369
|
+
all_the_modules[name] = module
|
|
370
|
+
return all_the_modules
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def get_bundles_dict():
|
|
374
|
+
"""
|
|
375
|
+
Retrieve the dictionary from BUNDLE_CONFIG_FILE (JSON).
|
|
376
|
+
Put the local dictionary in front, so it gets priority.
|
|
377
|
+
It's a dictionary of bundle string identifiers.
|
|
378
|
+
|
|
379
|
+
:return: Combined dictionaries from the config files.
|
|
380
|
+
"""
|
|
381
|
+
bundle_dict = get_bundles_local_dict()
|
|
382
|
+
try:
|
|
383
|
+
with open(BUNDLE_CONFIG_OVERWRITE, "rb") as bundle_config_json:
|
|
384
|
+
bundle_config = json.load(bundle_config_json)
|
|
385
|
+
except (FileNotFoundError, json.decoder.JSONDecodeError):
|
|
386
|
+
with open(BUNDLE_CONFIG_FILE, "rb") as bundle_config_json:
|
|
387
|
+
bundle_config = json.load(bundle_config_json)
|
|
388
|
+
for name, bundle in bundle_config.items():
|
|
389
|
+
if bundle not in bundle_dict.values():
|
|
390
|
+
bundle_dict[name] = bundle
|
|
391
|
+
return bundle_dict
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def get_bundles_local_dict():
|
|
395
|
+
"""
|
|
396
|
+
Retrieve the local bundles from BUNDLE_CONFIG_LOCAL (JSON).
|
|
397
|
+
|
|
398
|
+
:return: Raw dictionary from the config file(s).
|
|
399
|
+
"""
|
|
400
|
+
try:
|
|
401
|
+
with open(BUNDLE_CONFIG_LOCAL, "rb") as bundle_config_json:
|
|
402
|
+
bundle_config = json.load(bundle_config_json)
|
|
403
|
+
if not isinstance(bundle_config, dict) or not bundle_config:
|
|
404
|
+
logger.error("Local bundle list invalid. Skipped.")
|
|
405
|
+
raise FileNotFoundError("Bad local bundle list")
|
|
406
|
+
return bundle_config
|
|
407
|
+
except (FileNotFoundError, json.decoder.JSONDecodeError):
|
|
408
|
+
return dict()
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def get_bundles_list():
|
|
412
|
+
"""
|
|
413
|
+
Retrieve the list of bundles from the config dictionary.
|
|
414
|
+
|
|
415
|
+
:return: List of supported bundles as Bundle objects.
|
|
416
|
+
"""
|
|
417
|
+
bundle_config = get_bundles_dict()
|
|
418
|
+
bundles_list = [Bundle(bundle_config[b]) for b in bundle_config]
|
|
419
|
+
logger.info("Using bundles: %s", ", ".join(b.key for b in bundles_list))
|
|
420
|
+
return bundles_list
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def get_circup_version():
|
|
424
|
+
"""Return the version of circup that is running. If not available, return None.
|
|
425
|
+
|
|
426
|
+
:return: Current version of circup, or None.
|
|
427
|
+
"""
|
|
428
|
+
try:
|
|
429
|
+
from importlib import metadata # pylint: disable=import-outside-toplevel
|
|
430
|
+
except ImportError:
|
|
431
|
+
try:
|
|
432
|
+
import importlib_metadata as metadata # pylint: disable=import-outside-toplevel
|
|
433
|
+
except ImportError:
|
|
434
|
+
return None
|
|
435
|
+
try:
|
|
436
|
+
return metadata.version("circup")
|
|
437
|
+
except metadata.PackageNotFoundError:
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def get_dependencies(*requested_libraries, mod_names, to_install=()):
|
|
442
|
+
"""
|
|
443
|
+
Return a list of other CircuitPython libraries required by the given list
|
|
444
|
+
of libraries
|
|
445
|
+
|
|
446
|
+
:param tuple requested_libraries: The libraries to search for dependencies
|
|
447
|
+
:param object mod_names: All the modules metadata from bundle
|
|
448
|
+
:param list(str) to_install: Modules already selected for installation.
|
|
449
|
+
:return: tuple of module names to install which we build
|
|
450
|
+
"""
|
|
451
|
+
# pylint: disable=too-many-branches
|
|
452
|
+
# Internal variables
|
|
453
|
+
_to_install = to_install
|
|
454
|
+
_requested_libraries = []
|
|
455
|
+
_rl = requested_libraries[0]
|
|
456
|
+
|
|
457
|
+
if not requested_libraries[0]:
|
|
458
|
+
# If nothing is requested, we're done
|
|
459
|
+
return _to_install
|
|
460
|
+
|
|
461
|
+
for lib_name in _rl:
|
|
462
|
+
lower_lib_name = lib_name.lower()
|
|
463
|
+
if lower_lib_name in NOT_MCU_LIBRARIES:
|
|
464
|
+
logger.info(
|
|
465
|
+
"Skipping %s. It is not for microcontroller installs.", lib_name
|
|
466
|
+
)
|
|
467
|
+
else:
|
|
468
|
+
# Canonicalize, with some exceptions:
|
|
469
|
+
# adafruit-circuitpython-something => adafruit_something
|
|
470
|
+
canonical_lib_name = clean_library_name(lower_lib_name)
|
|
471
|
+
try:
|
|
472
|
+
# Don't process any names we can't find in mod_names
|
|
473
|
+
mod_names[canonical_lib_name] # pylint: disable=pointless-statement
|
|
474
|
+
_requested_libraries.append(canonical_lib_name)
|
|
475
|
+
except KeyError:
|
|
476
|
+
if canonical_lib_name not in WARNING_IGNORE_MODULES:
|
|
477
|
+
if os.path.exists(canonical_lib_name):
|
|
478
|
+
_requested_libraries.append(canonical_lib_name)
|
|
479
|
+
else:
|
|
480
|
+
click.secho(
|
|
481
|
+
f"WARNING:\n\t{canonical_lib_name} "
|
|
482
|
+
f"is not a known CircuitPython library.",
|
|
483
|
+
fg="yellow",
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
if not _requested_libraries:
|
|
487
|
+
# If nothing is requested, we're done
|
|
488
|
+
return _to_install
|
|
489
|
+
|
|
490
|
+
for library in list(_requested_libraries):
|
|
491
|
+
if library not in _to_install:
|
|
492
|
+
_to_install = _to_install + (library,)
|
|
493
|
+
# get the requirements.txt from bundle
|
|
494
|
+
try:
|
|
495
|
+
bundle = mod_names[library]["bundle"]
|
|
496
|
+
requirements_txt = bundle.requirements_for(library)
|
|
497
|
+
if requirements_txt:
|
|
498
|
+
_requested_libraries.extend(
|
|
499
|
+
libraries_from_requirements(requirements_txt)
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
circup_dependencies = get_circup_dependencies(bundle, library)
|
|
503
|
+
for circup_dependency in circup_dependencies:
|
|
504
|
+
_requested_libraries.append(circup_dependency)
|
|
505
|
+
except KeyError:
|
|
506
|
+
# don't check local file for further dependencies
|
|
507
|
+
pass
|
|
508
|
+
|
|
509
|
+
# we've processed this library, remove it from the list
|
|
510
|
+
_requested_libraries.remove(library)
|
|
511
|
+
|
|
512
|
+
return get_dependencies(
|
|
513
|
+
tuple(_requested_libraries), mod_names=mod_names, to_install=_to_install
|
|
514
|
+
)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def get_circup_dependencies(bundle, library):
|
|
518
|
+
"""
|
|
519
|
+
Get the list of circup dependencies from pyproject.toml
|
|
520
|
+
e.g.
|
|
521
|
+
[circup]
|
|
522
|
+
circup_dependencies = ["dependency_name_here"]
|
|
523
|
+
|
|
524
|
+
:param bundle: The Bundle to look within
|
|
525
|
+
:param library: The Library to find pyproject.toml for and get dependencies from
|
|
526
|
+
|
|
527
|
+
:return: The list of dependency libraries that were found
|
|
528
|
+
"""
|
|
529
|
+
try:
|
|
530
|
+
pyproj_toml = bundle.requirements_for(library, toml_file=True)
|
|
531
|
+
if pyproj_toml:
|
|
532
|
+
pyproj_toml_data = toml.loads(pyproj_toml)
|
|
533
|
+
dependencies = pyproj_toml_data["circup"]["circup_dependencies"]
|
|
534
|
+
if isinstance(dependencies, list):
|
|
535
|
+
return dependencies
|
|
536
|
+
|
|
537
|
+
if isinstance(dependencies, str):
|
|
538
|
+
return (dependencies,)
|
|
539
|
+
|
|
540
|
+
return tuple()
|
|
541
|
+
|
|
542
|
+
except KeyError:
|
|
543
|
+
# no circup_dependencies in pyproject.toml
|
|
544
|
+
return tuple()
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def libraries_from_requirements(requirements):
|
|
548
|
+
"""
|
|
549
|
+
Clean up supplied requirements.txt and turn into tuple of CP libraries
|
|
550
|
+
|
|
551
|
+
:param str requirements: A string version of a requirements.txt
|
|
552
|
+
:return: tuple of library names
|
|
553
|
+
"""
|
|
554
|
+
libraries = ()
|
|
555
|
+
for line in requirements.split("\n"):
|
|
556
|
+
line = line.lower().strip()
|
|
557
|
+
if line.startswith("#") or line == "":
|
|
558
|
+
# skip comments
|
|
559
|
+
pass
|
|
560
|
+
else:
|
|
561
|
+
# Remove everything after any pip style version specifiers
|
|
562
|
+
line = re.split("[<>=~[;]", line)[0].strip()
|
|
563
|
+
libraries = libraries + (line,)
|
|
564
|
+
return libraries
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def save_local_bundles(bundles_data):
|
|
568
|
+
"""
|
|
569
|
+
Save the list of local bundles to the settings.
|
|
570
|
+
|
|
571
|
+
:param str key: The bundle's identifier/key.
|
|
572
|
+
"""
|
|
573
|
+
if len(bundles_data) > 0:
|
|
574
|
+
with open(BUNDLE_CONFIG_LOCAL, "w", encoding="utf-8") as data:
|
|
575
|
+
json.dump(bundles_data, data)
|
|
576
|
+
else:
|
|
577
|
+
if os.path.isfile(BUNDLE_CONFIG_LOCAL):
|
|
578
|
+
os.unlink(BUNDLE_CONFIG_LOCAL)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def tags_data_save_tag(key, tag):
|
|
582
|
+
"""
|
|
583
|
+
Add or change the saved tag value for a bundle.
|
|
584
|
+
|
|
585
|
+
:param str key: The bundle's identifier/key.
|
|
586
|
+
:param str tag: The new tag for the bundle.
|
|
587
|
+
"""
|
|
588
|
+
tags_data = tags_data_load(logger)
|
|
589
|
+
tags_data[key] = tag
|
|
590
|
+
with open(BUNDLE_DATA, "w", encoding="utf-8") as data:
|
|
591
|
+
json.dump(tags_data, data)
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def libraries_from_code_py(code_py, mod_names):
|
|
595
|
+
"""
|
|
596
|
+
Parse the given code.py file and return the imported libraries
|
|
597
|
+
|
|
598
|
+
:param str code_py: Full path of the code.py file
|
|
599
|
+
:return: sequence of library names
|
|
600
|
+
"""
|
|
601
|
+
# pylint: disable=broad-except
|
|
602
|
+
try:
|
|
603
|
+
found_imports = findimports.find_imports(code_py)
|
|
604
|
+
except Exception as ex: # broad exception because anything could go wrong
|
|
605
|
+
logger.exception(ex)
|
|
606
|
+
click.secho('Unable to read the auto file: "{}"'.format(str(ex)), fg="red")
|
|
607
|
+
sys.exit(2)
|
|
608
|
+
# pylint: enable=broad-except
|
|
609
|
+
imports = [info.name.split(".", 1)[0] for info in found_imports]
|
|
610
|
+
return [r for r in imports if r in mod_names]
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def get_device_path(host, password, path):
|
|
614
|
+
"""
|
|
615
|
+
:param host Hostname or IP address.
|
|
616
|
+
:param password REST API password.
|
|
617
|
+
:param path File system path.
|
|
618
|
+
:return device URL or None if the device cannot be found.
|
|
619
|
+
"""
|
|
620
|
+
if path:
|
|
621
|
+
device_path = path
|
|
622
|
+
elif host:
|
|
623
|
+
# pylint: enable=no-member
|
|
624
|
+
device_path = f"http://:{password}@" + host
|
|
625
|
+
else:
|
|
626
|
+
device_path = find_device()
|
|
627
|
+
return device_path
|