circup 2.3.0__tar.gz → 3.0.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.
Files changed (94) hide show
  1. {circup-2.3.0/circup.egg-info → circup-3.0.0}/PKG-INFO +2 -2
  2. {circup-2.3.0 → circup-3.0.0}/circup/backends.py +11 -14
  3. {circup-2.3.0 → circup-3.0.0}/circup/bundle.py +98 -39
  4. {circup-2.3.0 → circup-3.0.0}/circup/command_utils.py +106 -52
  5. {circup-2.3.0 → circup-3.0.0}/circup/commands.py +106 -44
  6. {circup-2.3.0 → circup-3.0.0}/circup/lazy_metadata.py +2 -1
  7. {circup-2.3.0 → circup-3.0.0}/circup/module.py +1 -9
  8. {circup-2.3.0 → circup-3.0.0}/circup/shared.py +5 -2
  9. {circup-2.3.0 → circup-3.0.0}/circup/wwshell/commands.py +2 -2
  10. {circup-2.3.0 → circup-3.0.0/circup.egg-info}/PKG-INFO +2 -2
  11. {circup-2.3.0 → circup-3.0.0}/pyproject.toml +1 -1
  12. {circup-2.3.0 → circup-3.0.0}/tests/test_circup.py +43 -17
  13. {circup-2.3.0 → circup-3.0.0}/.github/ISSUE_TEMPLATE.md +0 -0
  14. {circup-2.3.0 → circup-3.0.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  15. {circup-2.3.0 → circup-3.0.0}/.github/workflows/build.yml +0 -0
  16. {circup-2.3.0 → circup-3.0.0}/.github/workflows/release.yml +0 -0
  17. {circup-2.3.0 → circup-3.0.0}/.gitignore +0 -0
  18. {circup-2.3.0 → circup-3.0.0}/.isort.cfg +0 -0
  19. {circup-2.3.0 → circup-3.0.0}/.pre-commit-config.yaml +0 -0
  20. {circup-2.3.0 → circup-3.0.0}/.pylintrc +0 -0
  21. {circup-2.3.0 → circup-3.0.0}/CODE_OF_CONDUCT.rst +0 -0
  22. {circup-2.3.0 → circup-3.0.0}/CODE_OF_CONDUCT.rst.license +0 -0
  23. {circup-2.3.0 → circup-3.0.0}/CONTRIBUTING.rst +0 -0
  24. {circup-2.3.0 → circup-3.0.0}/CONTRIBUTING.rst.license +0 -0
  25. {circup-2.3.0 → circup-3.0.0}/LICENSE +0 -0
  26. {circup-2.3.0 → circup-3.0.0}/LICENSES/CC-BY-4.0.txt +0 -0
  27. {circup-2.3.0 → circup-3.0.0}/LICENSES/MIT.txt +0 -0
  28. {circup-2.3.0 → circup-3.0.0}/LICENSES/Unlicense.txt +0 -0
  29. {circup-2.3.0 → circup-3.0.0}/README.rst +0 -0
  30. {circup-2.3.0 → circup-3.0.0}/README.rst.license +0 -0
  31. {circup-2.3.0 → circup-3.0.0}/circup/__init__.py +0 -0
  32. {circup-2.3.0 → circup-3.0.0}/circup/config/bundle_config.json +0 -0
  33. {circup-2.3.0 → circup-3.0.0}/circup/config/bundle_config.json.license +0 -0
  34. {circup-2.3.0 → circup-3.0.0}/circup/logging.py +0 -0
  35. {circup-2.3.0 → circup-3.0.0}/circup/wwshell/README.rst +0 -0
  36. {circup-2.3.0 → circup-3.0.0}/circup/wwshell/README.rst.license +0 -0
  37. {circup-2.3.0 → circup-3.0.0}/circup/wwshell/__init__.py +0 -0
  38. {circup-2.3.0 → circup-3.0.0}/circup.egg-info/SOURCES.txt +0 -0
  39. {circup-2.3.0 → circup-3.0.0}/circup.egg-info/dependency_links.txt +0 -0
  40. {circup-2.3.0 → circup-3.0.0}/circup.egg-info/entry_points.txt +0 -0
  41. {circup-2.3.0 → circup-3.0.0}/circup.egg-info/requires.txt +0 -0
  42. {circup-2.3.0 → circup-3.0.0}/circup.egg-info/top_level.txt +0 -0
  43. {circup-2.3.0 → circup-3.0.0}/docs/_static/favicon.ico +0 -0
  44. {circup-2.3.0 → circup-3.0.0}/docs/_static/favicon.ico.license +0 -0
  45. {circup-2.3.0 → circup-3.0.0}/docs/conf.py +0 -0
  46. {circup-2.3.0 → circup-3.0.0}/docs/index.rst +0 -0
  47. {circup-2.3.0 → circup-3.0.0}/docs/index.rst.license +0 -0
  48. {circup-2.3.0 → circup-3.0.0}/docs/logo.png +0 -0
  49. {circup-2.3.0 → circup-3.0.0}/docs/logo.png.license +0 -0
  50. {circup-2.3.0 → circup-3.0.0}/optional_requirements.txt +0 -0
  51. {circup-2.3.0 → circup-3.0.0}/optional_requirements.txt.license +0 -0
  52. {circup-2.3.0 → circup-3.0.0}/readthedocs.yml +0 -0
  53. {circup-2.3.0 → circup-3.0.0}/requirements.txt +0 -0
  54. {circup-2.3.0 → circup-3.0.0}/requirements.txt.license +0 -0
  55. {circup-2.3.0 → circup-3.0.0}/setup.cfg +0 -0
  56. {circup-2.3.0 → circup-3.0.0}/tests/__init__.py +0 -0
  57. {circup-2.3.0 → circup-3.0.0}/tests/bad_module/__init__.py +0 -0
  58. {circup-2.3.0 → circup-3.0.0}/tests/bad_module/my_module.py +0 -0
  59. {circup-2.3.0 → circup-3.0.0}/tests/bad_python.py +0 -0
  60. {circup-2.3.0 → circup-3.0.0}/tests/bundle.json +0 -0
  61. {circup-2.3.0 → circup-3.0.0}/tests/bundle.json.license +0 -0
  62. {circup-2.3.0 → circup-3.0.0}/tests/device.json +0 -0
  63. {circup-2.3.0 → circup-3.0.0}/tests/device.json.license +0 -0
  64. {circup-2.3.0 → circup-3.0.0}/tests/dir_module/__init__.py +0 -0
  65. {circup-2.3.0 → circup-3.0.0}/tests/dir_module/my_module.py +0 -0
  66. {circup-2.3.0 → circup-3.0.0}/tests/import_styles.py +0 -0
  67. {circup-2.3.0 → circup-3.0.0}/tests/local_module.py +0 -0
  68. {circup-2.3.0 → circup-3.0.0}/tests/local_module_cp7.mpy +0 -0
  69. {circup-2.3.0 → circup-3.0.0}/tests/local_module_cp7.mpy.license +0 -0
  70. {circup-2.3.0 → circup-3.0.0}/tests/mock_device/apps/test_app/import_styles.py +0 -0
  71. {circup-2.3.0 → circup-3.0.0}/tests/mock_device/apps/test_app/import_styles_sub.py +0 -0
  72. {circup-2.3.0 → circup-3.0.0}/tests/mock_device/boot_out.txt +0 -0
  73. {circup-2.3.0 → circup-3.0.0}/tests/mock_device/boot_out.txt.license +0 -0
  74. {circup-2.3.0 → circup-3.0.0}/tests/mock_device/import_styles.py +0 -0
  75. {circup-2.3.0 → circup-3.0.0}/tests/mock_device/import_styles_sub.py +0 -0
  76. {circup-2.3.0 → circup-3.0.0}/tests/mock_device/lib/adafruit_waveform/.gitkeep +0 -0
  77. {circup-2.3.0 → circup-3.0.0}/tests/mock_device_2/.gitignore +0 -0
  78. {circup-2.3.0 → circup-3.0.0}/tests/mock_device_2/boot_out.txt +0 -0
  79. {circup-2.3.0 → circup-3.0.0}/tests/mock_device_2/boot_out.txt.license +0 -0
  80. {circup-2.3.0 → circup-3.0.0}/tests/mock_device_2/code.py +0 -0
  81. {circup-2.3.0 → circup-3.0.0}/tests/mock_device_2/import_styles_sub.py +0 -0
  82. {circup-2.3.0 → circup-3.0.0}/tests/mock_device_2/package/__init__.py +0 -0
  83. {circup-2.3.0 → circup-3.0.0}/tests/mock_device_2/package/other.py +0 -0
  84. {circup-2.3.0 → circup-3.0.0}/tests/mount_exists.txt +0 -0
  85. {circup-2.3.0 → circup-3.0.0}/tests/mount_exists.txt.license +0 -0
  86. {circup-2.3.0 → circup-3.0.0}/tests/mount_missing.txt +0 -0
  87. {circup-2.3.0 → circup-3.0.0}/tests/mount_missing.txt.license +0 -0
  88. {circup-2.3.0 → circup-3.0.0}/tests/remote_module.py +0 -0
  89. {circup-2.3.0 → circup-3.0.0}/tests/test_bundle_config.json +0 -0
  90. {circup-2.3.0 → circup-3.0.0}/tests/test_bundle_config.json.license +0 -0
  91. {circup-2.3.0 → circup-3.0.0}/tests/test_bundle_config_local.json +0 -0
  92. {circup-2.3.0 → circup-3.0.0}/tests/test_bundle_config_local.json.license +0 -0
  93. {circup-2.3.0 → circup-3.0.0}/tests/test_module.mpy +0 -0
  94. {circup-2.3.0 → circup-3.0.0}/tests/test_module.mpy.license +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: circup
3
- Version: 2.3.0
3
+ Version: 3.0.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
@@ -42,7 +42,7 @@ Classifier: Programming Language :: Python :: 3.11
42
42
  Classifier: Topic :: Education
43
43
  Classifier: Topic :: Software Development :: Embedded Systems
44
44
  Classifier: Topic :: System :: Software Distribution
45
- Requires-Python: >=3.9
45
+ Requires-Python: >=3.10
46
46
  Description-Content-Type: text/x-rst
47
47
  License-File: LICENSE
48
48
  Requires-Dist: appdirs
@@ -139,7 +139,7 @@ class Backend:
139
139
  if name in device_modules:
140
140
  if not upgrade:
141
141
  # skip already installed modules if no -upgrade flag
142
- click.echo("'{}' is already installed.".format(name))
142
+ click.echo(f"'{name}' is already installed.")
143
143
  return
144
144
 
145
145
  # uninstall the module before installing
@@ -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:
@@ -205,9 +207,9 @@ class Backend:
205
207
  self.install_module_mpy(bundle, metadata)
206
208
  else:
207
209
  self.copy_file(metadata["path"], "lib")
208
- click.echo("Installed '{}'.".format(name))
210
+ click.echo(f"Installed '{name}'.")
209
211
  else:
210
- click.echo("Unknown module named, '{}'.".format(name))
212
+ click.echo(f"Unknown module named, '{name}'.")
211
213
 
212
214
  # def libraries_from_imports(self, code_py, mod_names):
213
215
  # """
@@ -303,7 +305,7 @@ class WebBackend(Backend):
303
305
  socket.getaddrinfo(host, 80, proto=socket.IPPROTO_TCP)
304
306
  except socket.gaierror as exc:
305
307
  raise RuntimeError(
306
- "Invalid host: {}.".format(host) + " You should remove the 'http://'"
308
+ f"Invalid host: {host}." + " You should remove the 'http://'"
307
309
  if "http://" in host or "https://" in host
308
310
  else "Could not find or connect to specified device"
309
311
  ) from exc
@@ -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)
@@ -659,7 +659,7 @@ class WebBackend(Backend):
659
659
  self.install_file_http(bundle_path)
660
660
 
661
661
  else:
662
- raise IOError("Cannot find compiled version of module.")
662
+ raise OSError("Cannot find compiled version of module.")
663
663
 
664
664
  # pylint: enable=too-many-locals,too-many-branches
665
665
  def install_module_py(self, metadata, location=None):
@@ -862,7 +862,6 @@ class DiskBackend(Backend):
862
862
  try:
863
863
  with open(
864
864
  os.path.join(self.device_location, "boot_out.txt"),
865
- "r",
866
865
  encoding="utf-8",
867
866
  ) as boot:
868
867
  boot_out_contents = boot.read()
@@ -920,9 +919,7 @@ class DiskBackend(Backend):
920
919
  # Must be a directory based module.
921
920
  module_name = os.path.basename(os.path.dirname(metadata["path"]))
922
921
 
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)
922
+ bundle_path = os.path.join(bundle.lib_dir(), module_name)
926
923
  if os.path.isdir(bundle_path):
927
924
  target_path = os.path.join(self.library_path, module_name)
928
925
  # Copy the directory.
@@ -936,7 +933,7 @@ class DiskBackend(Backend):
936
933
  # Copy file.
937
934
  shutil.copyfile(bundle_path, target_path)
938
935
  else:
939
- raise IOError("Cannot find compiled version of module.")
936
+ raise OSError("Cannot find compiled version of module.")
940
937
 
941
938
  # pylint: enable=too-many-locals,too-many-branches
942
939
  def install_module_py(self, metadata, location=None):
@@ -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),
@@ -93,7 +109,7 @@ class Bundle:
93
109
  "requirements.txt" if not toml_file else "pyproject.toml",
94
110
  )
95
111
  if os.path.isfile(found_file):
96
- with open(found_file, "r", encoding="utf-8") as read_this:
112
+ with open(found_file, encoding="utf-8") as read_this:
97
113
  return read_this.read()
98
114
  return None
99
115
 
@@ -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.
@@ -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):
@@ -286,7 +329,7 @@ def find_device():
286
329
  old_mode = ctypes.windll.kernel32.SetErrorMode(1)
287
330
  try:
288
331
  for disk in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
289
- path = "{}:\\".format(disk)
332
+ path = f"{disk}:\\"
290
333
  if os.path.exists(path) and get_volume_name(path) == "CIRCUITPY":
291
334
  device_dir = path
292
335
  # Report only the FIRST device found.
@@ -295,7 +338,7 @@ def find_device():
295
338
  ctypes.windll.kernel32.SetErrorMode(old_mode)
296
339
  else:
297
340
  # No support for unknown operating systems.
298
- raise NotImplementedError('OS "{}" not supported.'.format(os.name))
341
+ raise NotImplementedError(f'OS "{os.name}" not supported.')
299
342
  logger.info("Found device: %s", device_dir)
300
343
  return device_dir
301
344
 
@@ -349,45 +392,44 @@ def find_modules(backend, bundles_list):
349
392
  # If it's not possible to get the device and bundle metadata, bail out
350
393
  # with a friendly message and indication of what's gone wrong.
351
394
  logger.exception(ex)
352
- click.echo("There was a problem: {}".format(ex))
395
+ click.echo(f"There was a problem: {ex}")
353
396
  sys.exit(1)
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,12 +449,12 @@ 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
- with open(meta_saved, "r", encoding="utf-8") as f:
457
+ with open(meta_saved, encoding="utf-8") as f:
416
458
  bundle_examples = json.load(f)
417
459
  all_the_examples.update(bundle_examples)
418
460
  bundle_examples.clear()
@@ -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)
@@ -884,7 +929,7 @@ def libraries_from_auto_file(backend, auto_file, mod_names):
884
929
  # pass a local file with "./" or "../"
885
930
  is_relative = auto_file.split(os.sep)[0] in [os.path.curdir, os.path.pardir]
886
931
  if os.path.isabs(auto_file) or is_relative:
887
- with open(auto_file, "r", encoding="UTF8") as fp:
932
+ with open(auto_file, encoding="UTF8") as fp:
888
933
  auto_file_content = fp.read()
889
934
  else:
890
935
  auto_file_content = backend.get_file_content(auto_file)
@@ -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)