circup 2.3.0__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 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 pyext:
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
- major_version = self.get_circuitpython_version()[0].split(".")[0]
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
- major_version = self.get_circuitpython_version()[0].split(".")[0]
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,6 +10,8 @@ import sys
10
10
  import click
11
11
  import requests
12
12
 
13
+ from semver import VersionInfo
14
+
13
15
  from circup.shared import (
14
16
  DATA_DIR,
15
17
  PLATFORMS,
@@ -20,11 +22,14 @@ from circup.shared import (
20
22
  from circup.logging import logger
21
23
 
22
24
 
23
- class Bundle:
25
+ class Bundle: # pylint: disable=too-many-instance-attributes
24
26
  """
25
27
  All the links and file names for a bundle
26
28
  """
27
29
 
30
+ #: Avoid requests to the internet
31
+ offline = False
32
+
28
33
  def __init__(self, repo):
29
34
  """
30
35
  Initialise a Bundle created from its github info.
@@ -47,29 +52,40 @@ class Bundle:
47
52
  self._latest = None
48
53
  self.pinned_tag = None
49
54
  self._available = []
55
+ #
56
+ self.platform = None
50
57
 
51
- def lib_dir(self, platform):
58
+ def lib_dir(self, source=False):
52
59
  """
53
- This bundle's lib directory for the platform.
60
+ This bundle's lib directory for the bundle's source or compiled version.
54
61
 
55
- :param str platform: The platform identifier (py/6mpy/...).
56
- :return: The path to the lib directory for the platform.
62
+ :param bool source: Whether to return the path to the source lib
63
+ directory or to :py:attr:`self.platform`'s lib directory. If `source` is
64
+ `False` but :py:attr:`self.platform` is None, the source lib directory
65
+ will be returned instead.
66
+ :return: The path to the lib directory.
57
67
  """
58
68
  tag = self.current_tag
69
+ platform = "py" if source or not self.platform else self.platform
59
70
  return os.path.join(
60
71
  self.dir.format(platform=platform),
61
72
  self.basename.format(platform=PLATFORMS[platform], tag=tag),
62
73
  "lib",
63
74
  )
64
75
 
65
- def examples_dir(self, platform):
76
+ def examples_dir(self, source=False):
66
77
  """
67
- This bundle's examples directory for the platform.
78
+ This bundle's examples directory for the bundle's source or compiled
79
+ version.
68
80
 
69
- :param str platform: The platform identifier (py/6mpy/...).
70
- :return: The path to the examples directory for the platform.
81
+ :param bool source: Whether to return the path to the source examples
82
+ directory or to :py:attr:`self.platform`'s examples directory. If
83
+ `source` is `False` but :py:attr:`self.platform` is None, the source
84
+ examples directory will be returned instead.
85
+ :return: The path to the examples directory.
71
86
  """
72
87
  tag = self.current_tag
88
+ platform = "py" if source or not self.platform else self.platform
73
89
  return os.path.join(
74
90
  self.dir.format(platform=platform),
75
91
  self.basename.format(platform=PLATFORMS[platform], tag=tag),
@@ -101,18 +117,25 @@ class Bundle:
101
117
  def current_tag(self):
102
118
  """
103
119
  The current tag for the project. If the tag hasn't been explicitly set
104
- this will be the pinned tag, if one is set. If there is no pinned tag,
105
- this will be the latest available tag that is locally available.
120
+ this will be the pinned tag, if one is set and it is available. If there
121
+ is no pinned tag, this will be the latest available tag that is locally
122
+ available.
106
123
 
107
124
  :return: The current tag value for the project.
108
125
  """
109
126
  if self._current is None:
110
- 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
- )
127
+ if self.pinned_tag:
128
+ self._current = (
129
+ self.pinned_tag if self.pinned_tag in self._available else None
130
+ )
131
+ else:
132
+ self._current = (
133
+ # This represents the latest version locally available
134
+ self._available[-1]
135
+ if len(self._available) > 0
136
+ else None
137
+ )
138
+
116
139
  return self._current
117
140
 
118
141
  @current_tag.setter
@@ -132,9 +155,12 @@ class Bundle:
132
155
  :return: The most recent tag value for the project.
133
156
  """
134
157
  if self._latest is None:
135
- self._latest = get_latest_release_from_url(
136
- self.url + "/releases/latest", logger
137
- )
158
+ if self.offline:
159
+ self._latest = self._available[-1] if len(self._available) > 0 else None
160
+ else:
161
+ self._latest = get_latest_release_from_url(
162
+ self.url + "/releases/latest", logger
163
+ )
138
164
  return self._latest
139
165
 
140
166
  @property
@@ -155,7 +181,15 @@ class Bundle:
155
181
  """
156
182
  if isinstance(tags, str):
157
183
  tags = [tags]
158
- self._available = sorted(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
159
193
 
160
194
  def add_tag(self, tag: str) -> None:
161
195
  """
@@ -171,34 +205,59 @@ class Bundle:
171
205
  # The tag is already stored for some reason, lets not add it again
172
206
  return
173
207
 
174
- 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)
208
+ try:
209
+ version_tag = self.parse_version(tag)
210
+
211
+ for rev_i, available_tag in enumerate(reversed(self._available)):
212
+ available_version_tag = self.parse_version(available_tag)
213
+ if version_tag > available_version_tag:
214
+ i = len(self._available) - rev_i
215
+ self._available.insert(i, tag)
216
+ break
217
+ else:
218
+ self._available.insert(0, tag)
219
+ except ValueError as ex:
220
+ logger.warning(
221
+ "Bundle tag '%s' is not a valid tag, cannot order by version.", tag
222
+ )
223
+ logger.warning(ex)
224
+ self._available.append(tag)
181
225
 
182
226
  def validate(self):
183
227
  """
184
- Test the existence of the expected URLs (not their content)
228
+ Test the existence of the expected URL (not the content)
185
229
  """
186
230
  tag = self.latest_tag
187
231
  if not tag or tag == "releases":
188
232
  if "--verbose" in sys.argv:
189
233
  click.secho(f' Invalid tag "{tag}"', fg="red")
190
234
  return False
191
- for platform in PLATFORMS.values():
192
- url = self.url_format.format(platform=platform, tag=tag)
193
- r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
194
- # pylint: disable=no-member
195
- if r.status_code != requests.codes.ok:
196
- if "--verbose" in sys.argv:
197
- click.secho(f" Unable to find {os.path.split(url)[1]}", fg="red")
198
- return False
199
- # pylint: enable=no-member
235
+ url = self.url_format.format(platform="py", tag=tag)
236
+ r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
237
+ # pylint: disable=no-member
238
+ if r.status_code != requests.codes.ok:
239
+ if "--verbose" in sys.argv:
240
+ click.secho(f" Unable to find {os.path.split(url)[1]}", fg="red")
241
+ return False
242
+ # pylint: enable=no-member
200
243
  return True
201
244
 
245
+ @staticmethod
246
+ def parse_version(tag: str) -> VersionInfo:
247
+ """
248
+ Parse a tag to get a VersionInfo object.
249
+
250
+ `VersionInfo` objects are useful for ordering the tags from oldest to
251
+ newest in :py:attr:`self.available_tags`. The tags are stripped of a
252
+ leading 'v' (if one is present) and minor and patch components are
253
+ optional. This is to allow more flexibility with how a bundle is
254
+ versioned.
255
+
256
+ :param str tag: The tag to parse.
257
+ :return: A `VersionInfo` object parsed from the tag.
258
+ """
259
+ return VersionInfo.parse(tag.removeprefix("v"), optional_minor_and_patch=True)
260
+
202
261
  def __repr__(self):
203
262
  """
204
263
  Helps with log files.
circup/command_utils.py CHANGED
@@ -23,6 +23,7 @@ import click
23
23
  from circup.shared import (
24
24
  PLATFORMS,
25
25
  REQUESTS_TIMEOUT,
26
+ SUPPORTED_PLATFORMS,
26
27
  _get_modules_file,
27
28
  BUNDLE_CONFIG_OVERWRITE,
28
29
  BUNDLE_CONFIG_FILE,
@@ -145,29 +146,71 @@ def ensure_bundle_tag(bundle, tag):
145
146
 
146
147
  :return: If the bundle is available.
147
148
  """
148
- do_update = False
149
+ if tag is None:
150
+ logger.warning("Bundle version requested is 'None'.")
151
+ return False
152
+
153
+ do_update_source = False
154
+ do_update_compiled = False
149
155
  if tag in bundle.available_tags:
150
- for platform in PLATFORMS:
151
- # missing directories (new platform added on an existing install
152
- # or side effect of pytest or network errors)
153
- do_update = do_update or not os.path.isdir(bundle.lib_dir(platform))
156
+ # missing directories (new platform added on an existing install
157
+ # or side effect of pytest or network errors)
158
+ # Check for the source
159
+ do_update_source = not os.path.isdir(bundle.lib_dir(source=True))
160
+ do_update_compiled = bundle.platform is not None and not os.path.isdir(
161
+ bundle.lib_dir(source=False)
162
+ )
154
163
  else:
155
- do_update = True
164
+ do_update_source = True
165
+ do_update_compiled = bundle.platform is not None
166
+
167
+ if not (do_update_source or do_update_compiled):
168
+ logger.info("Current bundle version available (%s).", tag)
169
+ return True
156
170
 
157
- if do_update:
158
- logger.info("New version available (%s).", tag)
171
+ if Bundle.offline:
172
+ if do_update_source: # pylint: disable=no-else-return
173
+ logger.info(
174
+ "Bundle version not available but skipping update in offline mode."
175
+ )
176
+ return False
177
+ else:
178
+ logger.info(
179
+ "Bundle platform not available. Falling back to source (.py) files in offline mode."
180
+ )
181
+ bundle.platform = None
182
+ return True
183
+
184
+ logger.info("New version available (%s).", tag)
185
+ if do_update_source:
159
186
  try:
160
- get_bundle(bundle, tag)
161
- tags_data_save_tags(bundle.key, bundle.available_tags)
187
+ get_bundle(bundle, tag, "py")
162
188
  except requests.exceptions.HTTPError as ex:
163
189
  click.secho(
164
- f"There was a problem downloading the {bundle.key} bundle.", fg="red"
190
+ f"There was a problem downloading the 'py' platform for the '{bundle.key}' bundle.",
191
+ fg="red",
165
192
  )
166
193
  logger.exception(ex)
167
- return False
168
- else:
169
- logger.info("Current bundle version available (%s).", tag)
170
- return True
194
+ return False # Bundle isn't available
195
+ bundle.add_tag(tag)
196
+ tags_data_save_tags(bundle.key, bundle.available_tags)
197
+
198
+ if do_update_compiled:
199
+ try:
200
+ get_bundle(bundle, tag, bundle.platform)
201
+ except requests.exceptions.HTTPError as ex:
202
+ click.secho(
203
+ (
204
+ f"There was a problem downloading the '{bundle.platform}' platform for the "
205
+ f"'{bundle.key}' bundle.\nFalling back to source (.py) files."
206
+ ),
207
+ fg="red",
208
+ )
209
+ logger.exception(ex)
210
+ bundle.platform = None # Compiled isn't available, source is good
211
+ bundle.current_tag = tag
212
+
213
+ return True # bundle is available
171
214
 
172
215
 
173
216
  def ensure_latest_bundle(bundle):
@@ -354,40 +397,39 @@ def find_modules(backend, bundles_list):
354
397
  # pylint: enable=broad-except,too-many-locals
355
398
 
356
399
 
357
- def get_bundle(bundle, tag):
400
+ def get_bundle(bundle, tag, platform):
358
401
  """
359
402
  Downloads and extracts the version of the bundle with the referenced tag.
360
403
  The resulting zip file is saved on the local filesystem.
361
404
 
362
405
  :param Bundle bundle: the target Bundle object.
363
406
  :param str tag: The GIT tag to use to download the bundle.
407
+ :param str platform: The platform string (i.e. '10mpy').
364
408
  """
365
- click.echo(f"Downloading bundles for {bundle.key} ({tag}).")
366
- for platform, github_string in PLATFORMS.items():
367
- # Report the platform: "8.x-mpy", etc.
368
- click.echo(f"{github_string}:")
369
- url = bundle.url_format.format(platform=github_string, tag=tag)
370
- logger.info("Downloading bundle: %s", url)
371
- r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
372
- # pylint: disable=no-member
373
- if r.status_code != requests.codes.ok:
374
- logger.warning("Unable to connect to %s", url)
375
- r.raise_for_status()
376
- # pylint: enable=no-member
377
- total_size = int(r.headers.get("Content-Length"))
378
- temp_zip = bundle.zip.format(platform=platform)
379
- with click.progressbar(
380
- r.iter_content(1024), label="Extracting:", length=total_size
381
- ) as pbar, open(temp_zip, "wb") as zip_fp:
382
- for chunk in pbar:
383
- zip_fp.write(chunk)
384
- pbar.update(len(chunk))
385
- logger.info("Saved to %s", temp_zip)
386
- temp_dir = bundle.dir.format(platform=platform)
387
- with zipfile.ZipFile(temp_zip, "r") as zfile:
388
- zfile.extractall(temp_dir)
389
- bundle.add_tag(tag)
390
- bundle.current_tag = tag
409
+ click.echo(f"Downloading '{platform}' bundle for {bundle.key} ({tag}).")
410
+ github_string = PLATFORMS[platform]
411
+ # Report the platform: "8.x-mpy", etc.
412
+ click.echo(f"{github_string}:")
413
+ url = bundle.url_format.format(platform=github_string, tag=tag)
414
+ logger.info("Downloading bundle: %s", url)
415
+ r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
416
+ # pylint: disable=no-member
417
+ if r.status_code != requests.codes.ok:
418
+ logger.warning("Unable to connect to %s", url)
419
+ r.raise_for_status()
420
+ # pylint: enable=no-member
421
+ total_size = int(r.headers.get("Content-Length"))
422
+ temp_zip = bundle.zip.format(platform=platform)
423
+ with click.progressbar(
424
+ r.iter_content(1024), label="Extracting:", length=total_size
425
+ ) as pbar, open(temp_zip, "wb") as zip_fp:
426
+ for chunk in pbar:
427
+ zip_fp.write(chunk)
428
+ pbar.update(len(chunk))
429
+ logger.info("Saved to %s", temp_zip)
430
+ temp_dir = bundle.dir.format(platform=platform)
431
+ with zipfile.ZipFile(temp_zip, "r") as zfile:
432
+ zfile.extractall(temp_dir)
391
433
  click.echo("\nOK\n")
392
434
 
393
435
 
@@ -407,9 +449,9 @@ def get_bundle_examples(bundles_list, avoid_download=False):
407
449
 
408
450
  try:
409
451
  for bundle in bundles_list:
410
- if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
452
+ if not avoid_download or not os.path.isdir(bundle.lib_dir(source=True)):
411
453
  ensure_bundle(bundle)
412
- path = bundle.examples_dir("py")
454
+ path = bundle.examples_dir(source=True)
413
455
  meta_saved = os.path.join(path, "../bundle_examples.json")
414
456
  if os.path.exists(meta_saved):
415
457
  with open(meta_saved, "r", encoding="utf-8") as f:
@@ -455,9 +497,9 @@ def get_bundle_versions(bundles_list, avoid_download=False):
455
497
  """
456
498
  all_the_modules = dict()
457
499
  for bundle in bundles_list:
458
- if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
500
+ if not avoid_download or not os.path.isdir(bundle.lib_dir(source=True)):
459
501
  ensure_bundle(bundle)
460
- path = bundle.lib_dir("py")
502
+ path = bundle.lib_dir(source=True)
461
503
  path_modules = _get_modules_file(path, logger)
462
504
  for name, module in path_modules.items():
463
505
  module["bundle"] = bundle
@@ -504,12 +546,14 @@ def get_bundles_local_dict():
504
546
  return dict()
505
547
 
506
548
 
507
- def get_bundles_list(bundle_tags):
549
+ def get_bundles_list(bundle_tags, platform_version=None):
508
550
  """
509
551
  Retrieve the list of bundles from the config dictionary.
510
552
 
511
553
  :param Dict[str,str]|None bundle_tags: Pinned bundle tags. These override
512
554
  any tags found in the pyproject.toml.
555
+ :param str platform_version: The platform version needed for the current
556
+ device.
513
557
  :return: List of supported bundles as Bundle objects.
514
558
  """
515
559
  bundle_config = get_bundles_dict()
@@ -524,6 +568,7 @@ def get_bundles_list(bundle_tags):
524
568
 
525
569
  bundles_list = [Bundle(bundle_config[b]) for b in bundle_config]
526
570
  for bundle in bundles_list:
571
+ bundle.platform = platform_version
527
572
  bundle.available_tags = tags.get(bundle.key, [])
528
573
  if pinned_tags is not None:
529
574
  bundle.pinned_tag = pinned_tags.get(bundle.key)
@@ -994,3 +1039,12 @@ def parse_cli_bundle_tags(bundle_tags_cli):
994
1039
  if len(item) == 2:
995
1040
  bundle_tags[item[0].strip()] = item[1].strip()
996
1041
  return bundle_tags if len(bundle_tags) > 0 else None
1042
+
1043
+
1044
+ def pretty_supported_cpy_versions():
1045
+ """Return a user friendly string of the supported CircuitPython versions."""
1046
+ supported_cpy = [
1047
+ PLATFORMS[platform].split("-", maxsplit=1)[0]
1048
+ for platform in SUPPORTED_PLATFORMS
1049
+ ]
1050
+ return ", ".join(supported_cpy)
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 BOARDLESS_COMMANDS, get_latest_release_from_url
27
+ from circup.shared import (
28
+ BOARDLESS_COMMANDS,
29
+ SUPPORTED_PLATFORMS,
30
+ get_latest_release_from_url,
31
+ )
28
32
  from circup.bundle import Bundle
29
33
  from circup.command_utils import (
30
34
  get_device_path,
@@ -38,6 +42,7 @@ from circup.command_utils import (
38
42
  get_dependencies,
39
43
  get_bundles_local_dict,
40
44
  parse_cli_bundle_tags,
45
+ pretty_supported_cpy_versions,
41
46
  save_local_bundles,
42
47
  get_bundles_dict,
43
48
  completion_for_example,
@@ -68,6 +73,16 @@ from circup.command_utils import (
68
73
  " You can optionally set an environment variable CIRCUP_WEBWORKFLOW_PASSWORD"
69
74
  " instead of passing this argument. If both exist the CLI arg takes precedent.",
70
75
  )
76
+ @click.option(
77
+ "--offline",
78
+ is_flag=True,
79
+ help="Prevents Circup from accessing the internet for any reason. "
80
+ "Without this flag, Circup will fail with an error if it needs to access "
81
+ "the network and the network is not available. With this flag, Circup "
82
+ "will attempt to proceed without the network if possible. Circup will "
83
+ "only use bundles downloaded locally even if there might be newer "
84
+ "versions available.",
85
+ )
71
86
  @click.option(
72
87
  "--timeout",
73
88
  default=30,
@@ -95,6 +110,13 @@ from circup.command_utils import (
95
110
  "version values provided here will override any pinned values from the "
96
111
  "pyproject.toml.",
97
112
  )
113
+ @click.option(
114
+ "--allow-unsupported",
115
+ is_flag=True,
116
+ help="Allow using a device with a version of CircuitPython that is no longer "
117
+ "supported. Using an unsupported version of CircuitPython is generally not "
118
+ "recommended because libraries may not work with it.",
119
+ )
98
120
  @click.version_option(
99
121
  prog_name="Circup",
100
122
  message="%(prog)s, A CircuitPython module updater. Version %(version)s",
@@ -107,10 +129,12 @@ def main( # pylint: disable=too-many-locals
107
129
  host,
108
130
  port,
109
131
  password,
132
+ offline,
110
133
  timeout,
111
134
  board_id,
112
135
  cpy_version,
113
136
  bundle_versions,
137
+ allow_unsupported,
114
138
  ): # pragma: no cover
115
139
  """
116
140
  A tool to manage and update libraries on a CircuitPython device.
@@ -121,6 +145,7 @@ def main( # pylint: disable=too-many-locals
121
145
  ctx.obj["BUNDLE_TAGS"] = (
122
146
  parse_cli_bundle_tags(bundle_versions) if len(bundle_versions) > 0 else None
123
147
  )
148
+ Bundle.offline = offline
124
149
 
125
150
  if password is None:
126
151
  password = os.getenv("CIRCUP_WEBWORKFLOW_PASSWORD")
@@ -177,20 +202,22 @@ def main( # pylint: disable=too-many-locals
177
202
 
178
203
  logger.info("### Started Circup ###")
179
204
 
180
- # If a newer version of circup is available, print a message.
181
- logger.info("Checking for a newer version of circup")
182
- version = get_circup_version()
183
- if version:
184
- update_checker.update_check("circup", version)
205
+ if offline:
206
+ logger.info(
207
+ "'--offline' flag present, all update checks requiring the network will be skipped."
208
+ )
209
+ else:
210
+ # If a newer version of circup is available, print a message.
211
+ logger.info("Checking for a newer version of circup")
212
+ version = get_circup_version()
213
+ if version:
214
+ update_checker.update_check("circup", version)
185
215
 
186
216
  # stop early if the command is boardless
187
217
  if ctx.invoked_subcommand in BOARDLESS_COMMANDS or "--help" in sys.argv:
188
218
  return
189
219
 
190
220
  ctx.obj["DEVICE_PATH"] = device_path
191
- latest_version = get_latest_release_from_url(
192
- "https://github.com/adafruit/circuitpython/releases/latest", logger
193
- )
194
221
 
195
222
  if device_path is None or not ctx.obj["backend"].is_device_present():
196
223
  click.secho("Could not find a connected CircuitPython device.", fg="red")
@@ -201,27 +228,54 @@ def main( # pylint: disable=too-many-locals
201
228
  if board_id is None or cpy_version is None
202
229
  else (cpy_version, board_id)
203
230
  )
231
+ major_version = cpy_version.split(".")[0]
232
+ bundle_platform = "{}mpy".format(major_version)
233
+ ctx.obj["DEVICE_PLATFORM_VERSION"] = bundle_platform
204
234
  click.echo(
205
235
  "Found device {} at {}, running CircuitPython {}.".format(
206
236
  board_id, device_path, cpy_version
207
237
  )
208
238
  )
209
- try:
210
- if VersionInfo.parse(cpy_version) < VersionInfo.parse(latest_version):
239
+
240
+ if not offline:
241
+ latest_version = get_latest_release_from_url(
242
+ "https://github.com/adafruit/circuitpython/releases/latest", logger
243
+ )
244
+ try:
245
+ if VersionInfo.parse(cpy_version) < VersionInfo.parse(latest_version):
246
+ click.secho(
247
+ "A newer version of CircuitPython ({}) is available.".format(
248
+ latest_version
249
+ ),
250
+ fg="green",
251
+ )
252
+ if board_id:
253
+ url_download = f"https://circuitpython.org/board/{board_id}"
254
+ else:
255
+ url_download = "https://circuitpython.org/downloads"
256
+ click.secho("Get it here: {}".format(url_download), fg="green")
257
+ except ValueError as ex:
258
+ logger.warning("CircuitPython has incorrect semver value.")
259
+ logger.warning(ex)
260
+
261
+ if not bundle_platform in SUPPORTED_PLATFORMS:
262
+ click.secho(
263
+ "The version of CircuitPython on the device is no longer supported.",
264
+ fg="yellow" if allow_unsupported else "red",
265
+ )
266
+ if allow_unsupported:
211
267
  click.secho(
212
- "A newer version of CircuitPython ({}) is available.".format(
213
- latest_version
214
- ),
215
- fg="green",
268
+ "It is recommended to update to a supported version "
269
+ f"({pretty_supported_cpy_versions()}) to ensure compatability.",
270
+ fg="yellow",
216
271
  )
217
- if board_id:
218
- url_download = f"https://circuitpython.org/board/{board_id}"
219
- else:
220
- url_download = "https://circuitpython.org/downloads"
221
- click.secho("Get it here: {}".format(url_download), fg="green")
222
- except ValueError as ex:
223
- logger.warning("CircuitPython has incorrect semver value.")
224
- logger.warning(ex)
272
+ else:
273
+ click.echo(
274
+ f"If you would like to continue to use version {cpy_version} of CircuitPython, "
275
+ "pass the '--allow-unsupported' flag with this command. Otherwise, update to a "
276
+ f"supported version ({pretty_supported_cpy_versions()}) to ensure compatability.",
277
+ )
278
+ sys.exit(1)
225
279
 
226
280
 
227
281
  @main.command()
@@ -282,7 +336,10 @@ def list_cli(ctx): # pragma: no cover
282
336
  modules = [
283
337
  m.row
284
338
  for m in find_modules(
285
- ctx.obj["backend"], get_bundles_list(ctx.obj["BUNDLE_TAGS"])
339
+ ctx.obj["backend"],
340
+ get_bundles_list(
341
+ ctx.obj["BUNDLE_TAGS"], ctx.obj["DEVICE_PLATFORM_VERSION"]
342
+ ),
286
343
  )
287
344
  if m.outofdate
288
345
  ]
@@ -359,7 +416,10 @@ def install(
359
416
 
360
417
  # pylint: disable=too-many-branches
361
418
  # TODO: Ensure there's enough space on the device
362
- available_modules = get_bundle_versions(get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
419
+ platform_version = ctx.obj["DEVICE_PLATFORM_VERSION"] if not pyext else None
420
+ available_modules = get_bundle_versions(
421
+ get_bundles_list(ctx.obj["BUNDLE_TAGS"], platform_version)
422
+ )
363
423
  mod_names = {}
364
424
  for module, metadata in available_modules.items():
365
425
  mod_names[module.replace(".py", "").lower()] = metadata
@@ -392,7 +452,7 @@ def install(
392
452
  upgrade,
393
453
  )
394
454
 
395
- if stubs:
455
+ if stubs and not Bundle.offline:
396
456
  # Check we are in a virtual environment
397
457
  if not is_virtual_env_active():
398
458
  if is_global_install_ok is None:
@@ -581,7 +641,9 @@ def update(ctx, update_all): # pragma: no cover
581
641
  """
582
642
  logger.info("Update")
583
643
  # Grab current modules.
584
- bundles_list = get_bundles_list(ctx.obj["BUNDLE_TAGS"])
644
+ bundles_list = get_bundles_list(
645
+ ctx.obj["BUNDLE_TAGS"], ctx.obj["DEVICE_PLATFORM_VERSION"]
646
+ )
585
647
  installed_modules = find_modules(ctx.obj["backend"], bundles_list)
586
648
  modules_to_update = [m for m in installed_modules if m.outofdate]
587
649
 
@@ -728,6 +790,10 @@ def bundle_add(ctx, bundle):
728
790
  )
729
791
  return
730
792
 
793
+ if Bundle.offline:
794
+ click.secho("Cannot add new bundle when '--offline' flag is present.", fg="red")
795
+ return
796
+
731
797
  bundles_dict = get_bundles_local_dict()
732
798
  modified = False
733
799
  for bundle_repo in bundle:
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(bundle_platform)
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
@@ -22,7 +22,10 @@ BAD_FILE_FORMAT = "Invalid"
22
22
  DATA_DIR = appdirs.user_data_dir(appname="circup", appauthor="adafruit")
23
23
 
24
24
  #: Module formats list (and the other form used in github files)
25
- PLATFORMS = {"py": "py", "9mpy": "9.x-mpy", "10mpy": "10.x-mpy"}
25
+ PLATFORMS = {"py": "py", "8mpy": "8.x-mpy", "9mpy": "9.x-mpy", "10mpy": "10.x-mpy"}
26
+
27
+ #: CircuitPython platforms that are currently supported.
28
+ SUPPORTED_PLATFORMS = ["9mpy", "10mpy"]
26
29
 
27
30
  #: Timeout for requests calls like get()
28
31
  REQUESTS_TIMEOUT = 30
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: circup
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: A tool to manage/update libraries on CircuitPython devices.
5
5
  Author-email: Adafruit Industries <circuitpython@adafruit.com>
6
6
  License: MIT License
@@ -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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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=eQ_PxMaDD_XKfxXMXcpzSnhHjx5oJh1YXZq00o5smiM,7075
4
- circup/command_utils.py,sha256=_DfnRKinXflkPt-CGaIg_3nBZ6Q2J9yRFS9LXC73k3U,35180
5
- circup/commands.py,sha256=vZ4iui7IpLIkeaO_LQ5JzIqCPkfcUbrbUmICcLrl9aE,31494
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=O3_iA1mIsoVPObTzv5tfPCqejJSZitTWb0RMYIEM87w,8635
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.3.0.dist-info/licenses/LICENSE,sha256=bVlIMmSL_pqLCqae4hzixy9pYXD808IbgsMoQXTNLBk,1076
17
- circup-2.3.0.dist-info/METADATA,sha256=0PRlXlC9q3DFjK27uGkOPIj3tNYxbhi7ciaL8epvyKQ,13617
18
- circup-2.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
- circup-2.3.0.dist-info/entry_points.txt,sha256=FjTmwYD_ApgLRGifUrr_Ui1voW6fEzodQc3DKNzoAPc,69
20
- circup-2.3.0.dist-info/top_level.txt,sha256=Qx6E0eZgSBE10ciNKsLZx8-TTy_9fEVZh7NLmn24KcU,7
21
- circup-2.3.0.dist-info/RECORD,,