circup 2.2.6__py3-none-any.whl → 2.3.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/bundle.py CHANGED
@@ -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
- Lazy load current cached tag from the BUNDLE_DATA json file.
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 cached tag value for the project.
107
+ :return: The current tag value for the project.
105
108
  """
106
109
  if self._current is None:
107
- self._current = tags_data_load(logger).get(self.key, "0")
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 cached tag (after updating).
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
  )
circup/command_utils.py CHANGED
@@ -12,10 +12,10 @@ import os
12
12
 
13
13
  from subprocess import check_output
14
14
  import sys
15
- import shutil
16
15
  import zipfile
17
16
  import json
18
17
  import re
18
+ from pathlib import Path
19
19
  import toml
20
20
  import requests
21
21
  import click
@@ -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,7 +120,9 @@ 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(get_bundles_list(), avoid_download=True)
123
+ available_examples = get_bundle_examples(
124
+ get_bundles_list(None), avoid_download=True
125
+ )
125
126
  matching_examples = []
126
127
  for term in incomplete:
127
128
  _examples = [
@@ -134,17 +135,18 @@ def completion_for_example(ctx, param, incomplete):
134
135
  return sorted(matching_examples)
135
136
 
136
137
 
137
- def ensure_latest_bundle(bundle):
138
+ def ensure_bundle_tag(bundle, tag):
138
139
  """
139
- Ensure that there's a copy of the latest library bundle available so circup
140
- can check the metadata contained therein.
140
+ Ensure that there's a copy of the library bundle with the version referenced
141
+ by the tag.
141
142
 
142
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.
143
147
  """
144
- logger.info("Checking library updates for %s.", bundle.key)
145
- tag = bundle.latest_tag
146
148
  do_update = False
147
- if tag == bundle.current_tag:
149
+ if tag in bundle.available_tags:
148
150
  for platform in PLATFORMS:
149
151
  # missing directories (new platform added on an existing install
150
152
  # or side effect of pytest or network errors)
@@ -156,19 +158,78 @@ def ensure_latest_bundle(bundle):
156
158
  logger.info("New version available (%s).", tag)
157
159
  try:
158
160
  get_bundle(bundle, tag)
159
- tags_data_save_tag(bundle.key, tag)
161
+ tags_data_save_tags(bundle.key, bundle.available_tags)
160
162
  except requests.exceptions.HTTPError as ex:
161
- # See #20 for reason for this
162
163
  click.secho(
163
- (
164
- "There was a problem downloading that platform bundle. "
165
- "Skipping and using existing download if available."
166
- ),
167
- fg="red",
164
+ f"There was a problem downloading the {bundle.key} bundle.", fg="red"
168
165
  )
169
166
  logger.exception(ex)
167
+ return False
170
168
  else:
171
- logger.info("Current bundle up to date %s.", tag)
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}).")
185
+ else:
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)
172
233
 
173
234
 
174
235
  def find_device():
@@ -301,7 +362,7 @@ def get_bundle(bundle, tag):
301
362
  :param Bundle bundle: the target Bundle object.
302
363
  :param str tag: The GIT tag to use to download the bundle.
303
364
  """
304
- click.echo(f"Downloading latest bundles for {bundle.key} ({tag}).")
365
+ click.echo(f"Downloading bundles for {bundle.key} ({tag}).")
305
366
  for platform, github_string in PLATFORMS.items():
306
367
  # Report the platform: "8.x-mpy", etc.
307
368
  click.echo(f"{github_string}:")
@@ -323,10 +384,9 @@ def get_bundle(bundle, tag):
323
384
  pbar.update(len(chunk))
324
385
  logger.info("Saved to %s", temp_zip)
325
386
  temp_dir = bundle.dir.format(platform=platform)
326
- if os.path.isdir(temp_dir):
327
- shutil.rmtree(temp_dir)
328
387
  with zipfile.ZipFile(temp_zip, "r") as zfile:
329
388
  zfile.extractall(temp_dir)
389
+ bundle.add_tag(tag)
330
390
  bundle.current_tag = tag
331
391
  click.echo("\nOK\n")
332
392
 
@@ -348,7 +408,7 @@ def get_bundle_examples(bundles_list, avoid_download=False):
348
408
  try:
349
409
  for bundle in bundles_list:
350
410
  if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
351
- ensure_latest_bundle(bundle)
411
+ ensure_bundle(bundle)
352
412
  path = bundle.examples_dir("py")
353
413
  meta_saved = os.path.join(path, "../bundle_examples.json")
354
414
  if os.path.exists(meta_saved):
@@ -383,9 +443,10 @@ def get_bundle_examples(bundles_list, avoid_download=False):
383
443
 
384
444
  def get_bundle_versions(bundles_list, avoid_download=False):
385
445
  """
386
- Returns a dictionary of metadata from modules in the latest known release
387
- of the library bundle. Uses the Python version (rather than the compiled
388
- version) of the library modules.
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.
389
450
 
390
451
  :param List[Bundle] bundles_list: List of supported bundles as Bundle objects.
391
452
  :param bool avoid_download: if True, download the bundle only if missing.
@@ -395,7 +456,7 @@ def get_bundle_versions(bundles_list, avoid_download=False):
395
456
  all_the_modules = dict()
396
457
  for bundle in bundles_list:
397
458
  if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
398
- ensure_latest_bundle(bundle)
459
+ ensure_bundle(bundle)
399
460
  path = bundle.lib_dir("py")
400
461
  path_modules = _get_modules_file(path, logger)
401
462
  for name, module in path_modules.items():
@@ -443,14 +504,29 @@ def get_bundles_local_dict():
443
504
  return dict()
444
505
 
445
506
 
446
- def get_bundles_list():
507
+ def get_bundles_list(bundle_tags):
447
508
  """
448
509
  Retrieve the list of bundles from the config dictionary.
449
510
 
511
+ :param Dict[str,str]|None bundle_tags: Pinned bundle tags. These override
512
+ any tags found in the pyproject.toml.
450
513
  :return: List of supported bundles as Bundle objects.
451
514
  """
452
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
+
453
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)
454
530
  logger.info("Using bundles: %s", ", ".join(b.key for b in bundles_list))
455
531
  return bundles_list
456
532
 
@@ -613,15 +689,38 @@ def save_local_bundles(bundles_data):
613
689
  os.unlink(BUNDLE_CONFIG_LOCAL)
614
690
 
615
691
 
616
- def tags_data_save_tag(key, tag):
692
+ def tags_data_load():
693
+ """
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):
617
716
  """
618
- Add or change the saved tag value for a bundle.
717
+ Add or change the saved available tags value for a bundle.
619
718
 
620
719
  :param str key: The bundle's identifier/key.
621
- :param str tag: The new tag for the bundle.
720
+ :param List[str] tags: The new tags for the bundle.
622
721
  """
623
- tags_data = tags_data_load(logger)
624
- tags_data[key] = tag
722
+ tags_data = tags_data_load()
723
+ tags_data[key] = tags
625
724
  with open(BUNDLE_DATA, "w", encoding="utf-8") as data:
626
725
  json.dump(tags_data, data)
627
726
 
@@ -855,3 +954,43 @@ def is_virtual_env_active():
855
954
  virtual environment, regardless how circup is installed.
856
955
  """
857
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
circup/commands.py CHANGED
@@ -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, verbose, path, host, port, password, timeout, board_id, cpy_version
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(ctx.obj["backend"], get_bundles_list())
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
@@ -440,7 +465,7 @@ def example(ctx, examples, op_list, rename, overwrite):
440
465
  else:
441
466
  click.echo("Available example libraries:")
442
467
  available_examples = get_bundle_examples(
443
- get_bundles_list(), avoid_download=True
468
+ get_bundles_list(ctx.obj["BUNDLE_TAGS"]), avoid_download=True
444
469
  )
445
470
  lib_names = {
446
471
  str(key.split(os.path.sep)[0]): value
@@ -451,7 +476,7 @@ def example(ctx, examples, op_list, rename, overwrite):
451
476
 
452
477
  for example_arg in examples:
453
478
  available_examples = get_bundle_examples(
454
- get_bundles_list(), avoid_download=True
479
+ get_bundles_list(ctx.obj["BUNDLE_TAGS"]), avoid_download=True
455
480
  )
456
481
  if example_arg in available_examples:
457
482
  filename = available_examples[example_arg].split(os.path.sep)[-1]
@@ -485,14 +510,15 @@ def example(ctx, examples, op_list, rename, overwrite):
485
510
 
486
511
  @main.command()
487
512
  @click.argument("match", required=False, nargs=1)
488
- def show(match): # pragma: no cover
513
+ @click.pass_context
514
+ def show(ctx, match): # pragma: no cover
489
515
  """
490
516
  Show a list of available modules in the bundle. These are modules which
491
517
  *could* be installed on the device.
492
518
 
493
519
  If MATCH is specified only matching modules will be listed.
494
520
  """
495
- available_modules = get_bundle_versions(get_bundles_list())
521
+ available_modules = get_bundle_versions(get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
496
522
  module_names = sorted([m.replace(".py", "") for m in available_modules])
497
523
  if match is not None:
498
524
  match = match.lower()
@@ -555,7 +581,7 @@ def update(ctx, update_all): # pragma: no cover
555
581
  """
556
582
  logger.info("Update")
557
583
  # Grab current modules.
558
- bundles_list = get_bundles_list()
584
+ bundles_list = get_bundles_list(ctx.obj["BUNDLE_TAGS"])
559
585
  installed_modules = find_modules(ctx.obj["backend"], bundles_list)
560
586
  modules_to_update = [m for m in installed_modules if m.outofdate]
561
587
 
@@ -654,13 +680,14 @@ def update(ctx, update_all): # pragma: no cover
654
680
 
655
681
  @main.command("bundle-show")
656
682
  @click.option("--modules", is_flag=True, help="List all the modules per bundle.")
657
- def bundle_show(modules):
683
+ @click.pass_context
684
+ def bundle_show(ctx, modules):
658
685
  """
659
- Show the list of bundles, default and local, with URL, current version
660
- 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.
661
688
  """
662
689
  local_bundles = get_bundles_local_dict().values()
663
- bundles = get_bundles_list()
690
+ bundles = get_bundles_list(ctx.obj["BUNDLE_TAGS"])
664
691
  available_modules = get_bundle_versions(bundles)
665
692
 
666
693
  for bundle in bundles:
@@ -669,7 +696,13 @@ def bundle_show(modules):
669
696
  else:
670
697
  click.secho(bundle.key, fg="green")
671
698
  click.echo(" " + bundle.url)
672
- click.echo(" version = " + bundle.current_tag)
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))
673
706
  if modules:
674
707
  click.echo("Modules:")
675
708
  for name, mod in sorted(available_modules.items()):
@@ -739,7 +772,7 @@ def bundle_add(ctx, bundle):
739
772
  # save the bundles list
740
773
  save_local_bundles(bundles_dict)
741
774
  # update and get the new bundles for the first time
742
- get_bundle_versions(get_bundles_list())
775
+ get_bundle_versions(get_bundles_list(ctx.obj["BUNDLE_TAGS"]))
743
776
 
744
777
 
745
778
  @main.command("bundle-remove")
@@ -788,3 +821,40 @@ def bundle_remove(bundle, reset):
788
821
  )
789
822
  if modified:
790
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.")
circup/shared.py CHANGED
@@ -9,7 +9,6 @@ and Backend class functions.
9
9
  import glob
10
10
  import os
11
11
  import re
12
- import json
13
12
  import importlib.resources
14
13
  import appdirs
15
14
  import requests
@@ -192,29 +191,6 @@ def extract_metadata(path, logger):
192
191
  return result
193
192
 
194
193
 
195
- def tags_data_load(logger):
196
- """
197
- Load the list of the version tags of the bundles on disk.
198
-
199
- :return: a dict() of tags indexed by Bundle identifiers/keys.
200
- """
201
- tags_data = None
202
- try:
203
- with open(BUNDLE_DATA, encoding="utf-8") as data:
204
- try:
205
- tags_data = json.load(data)
206
- except json.decoder.JSONDecodeError as ex:
207
- # Sometimes (why?) the JSON file becomes corrupt. In which case
208
- # log it and carry on as if setting up for first time.
209
- logger.error("Could not parse %s", BUNDLE_DATA)
210
- logger.exception(ex)
211
- except FileNotFoundError:
212
- pass
213
- if not isinstance(tags_data, dict):
214
- tags_data = {}
215
- return tags_data
216
-
217
-
218
194
  def get_latest_release_from_url(url, logger):
219
195
  """
220
196
  Find the tag name of the latest release by using HTTP HEAD and decoding the redirect.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: circup
3
- Version: 2.2.6
3
+ Version: 2.3.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
@@ -1,21 +1,21 @@
1
1
  circup/__init__.py,sha256=9A98U3DyA14tm-7_d88fGSlLfEKz9T_85t8kXkErqRg,662
2
2
  circup/backends.py,sha256=g9Q9xCGZidwsEDL2Ga2cm50YYB54IiqlKUPcxj-pWZA,40008
3
- circup/bundle.py,sha256=FEP4F470aJtwmm8jgTM3DgR3dj5SVwbX1tbyIRKVHn8,5327
4
- circup/command_utils.py,sha256=_bYFfsfXoy6ERyTSmqt-RTNuIJN9ilYolKGTF42ebyk,30676
5
- circup/commands.py,sha256=3clk6B6oF5noA31TnuCyzIHNxxFcsoxBCVBOoy-K8fo,29082
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
6
  circup/lazy_metadata.py,sha256=69VidxGGWE13QwAAtMCPNTXTsQ2q5dJvMtclw4YaqEY,3764
7
7
  circup/logging.py,sha256=hu4v8ljkXo8ru-cqs0W3PU-xEVvTO_qqMKDJM18OXbQ,1115
8
8
  circup/module.py,sha256=33_kdy5BZn6COyIjAFZMpw00rTtPiryQZWFXQkMF8FY,7435
9
- circup/shared.py,sha256=rribEZdoeyZRHxLiezyrDH0vb8DIP4bGOfMpRJm9M9w,9405
9
+ circup/shared.py,sha256=O3_iA1mIsoVPObTzv5tfPCqejJSZitTWb0RMYIEM87w,8635
10
10
  circup/config/bundle_config.json,sha256=zzpmfy0jD7TxpOOw2P_gqIISuMBG0enb_f3_VneQ5mI,135
11
11
  circup/config/bundle_config.json.license,sha256=OOHNqDsViGFhmG9z8J0o98hYmub1CkYKiZB96Php6KE,80
12
12
  circup/wwshell/README.rst,sha256=M_jFP0hwOcngF0RdosdeqmVOISNcPzyjTW3duzIu9A8,3617
13
13
  circup/wwshell/README.rst.license,sha256=GhA0SoZGP7CReDam-JJk_UtIQIpQaZWQFzR26YSuMm4,107
14
14
  circup/wwshell/__init__.py,sha256=CAPZiYrouWboyPx4KiWLBG_vf_n0MmArGqaFyTXGKWk,398
15
15
  circup/wwshell/commands.py,sha256=-I5l7XeoDmvWWuZg5wHdt9qe__SBQ1EGmKwCDTBMeus,7454
16
- circup-2.2.6.dist-info/licenses/LICENSE,sha256=bVlIMmSL_pqLCqae4hzixy9pYXD808IbgsMoQXTNLBk,1076
17
- circup-2.2.6.dist-info/METADATA,sha256=bBGt9xAlnkAhdypwfT63GgbGSjfzee1JwHJ5FFC2Euw,13617
18
- circup-2.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
- circup-2.2.6.dist-info/entry_points.txt,sha256=FjTmwYD_ApgLRGifUrr_Ui1voW6fEzodQc3DKNzoAPc,69
20
- circup-2.2.6.dist-info/top_level.txt,sha256=Qx6E0eZgSBE10ciNKsLZx8-TTy_9fEVZh7NLmn24KcU,7
21
- circup-2.2.6.dist-info/RECORD,,
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,,
File without changes