circup 2.2.6__tar.gz → 2.4.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 (95) hide show
  1. {circup-2.2.6/circup.egg-info → circup-2.4.0}/PKG-INFO +1 -1
  2. {circup-2.2.6 → circup-2.4.0}/circup/backends.py +5 -7
  3. circup-2.4.0/circup/bundle.py +280 -0
  4. {circup-2.2.6 → circup-2.4.0}/circup/command_utils.py +260 -67
  5. {circup-2.2.6 → circup-2.4.0}/circup/commands.py +175 -39
  6. {circup-2.2.6 → circup-2.4.0}/circup/module.py +1 -9
  7. {circup-2.2.6 → circup-2.4.0}/circup/shared.py +4 -25
  8. {circup-2.2.6 → circup-2.4.0/circup.egg-info}/PKG-INFO +1 -1
  9. {circup-2.2.6 → circup-2.4.0}/tests/test_circup.py +203 -34
  10. circup-2.2.6/circup/bundle.py +0 -170
  11. {circup-2.2.6 → circup-2.4.0}/.github/ISSUE_TEMPLATE.md +0 -0
  12. {circup-2.2.6 → circup-2.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  13. {circup-2.2.6 → circup-2.4.0}/.github/workflows/build.yml +0 -0
  14. {circup-2.2.6 → circup-2.4.0}/.github/workflows/release.yml +0 -0
  15. {circup-2.2.6 → circup-2.4.0}/.gitignore +0 -0
  16. {circup-2.2.6 → circup-2.4.0}/.isort.cfg +0 -0
  17. {circup-2.2.6 → circup-2.4.0}/.pre-commit-config.yaml +0 -0
  18. {circup-2.2.6 → circup-2.4.0}/.pylintrc +0 -0
  19. {circup-2.2.6 → circup-2.4.0}/CODE_OF_CONDUCT.rst +0 -0
  20. {circup-2.2.6 → circup-2.4.0}/CODE_OF_CONDUCT.rst.license +0 -0
  21. {circup-2.2.6 → circup-2.4.0}/CONTRIBUTING.rst +0 -0
  22. {circup-2.2.6 → circup-2.4.0}/CONTRIBUTING.rst.license +0 -0
  23. {circup-2.2.6 → circup-2.4.0}/LICENSE +0 -0
  24. {circup-2.2.6 → circup-2.4.0}/LICENSES/CC-BY-4.0.txt +0 -0
  25. {circup-2.2.6 → circup-2.4.0}/LICENSES/MIT.txt +0 -0
  26. {circup-2.2.6 → circup-2.4.0}/LICENSES/Unlicense.txt +0 -0
  27. {circup-2.2.6 → circup-2.4.0}/README.rst +0 -0
  28. {circup-2.2.6 → circup-2.4.0}/README.rst.license +0 -0
  29. {circup-2.2.6 → circup-2.4.0}/circup/__init__.py +0 -0
  30. {circup-2.2.6 → circup-2.4.0}/circup/config/bundle_config.json +0 -0
  31. {circup-2.2.6 → circup-2.4.0}/circup/config/bundle_config.json.license +0 -0
  32. {circup-2.2.6 → circup-2.4.0}/circup/lazy_metadata.py +0 -0
  33. {circup-2.2.6 → circup-2.4.0}/circup/logging.py +0 -0
  34. {circup-2.2.6 → circup-2.4.0}/circup/wwshell/README.rst +0 -0
  35. {circup-2.2.6 → circup-2.4.0}/circup/wwshell/README.rst.license +0 -0
  36. {circup-2.2.6 → circup-2.4.0}/circup/wwshell/__init__.py +0 -0
  37. {circup-2.2.6 → circup-2.4.0}/circup/wwshell/commands.py +0 -0
  38. {circup-2.2.6 → circup-2.4.0}/circup.egg-info/SOURCES.txt +0 -0
  39. {circup-2.2.6 → circup-2.4.0}/circup.egg-info/dependency_links.txt +0 -0
  40. {circup-2.2.6 → circup-2.4.0}/circup.egg-info/entry_points.txt +0 -0
  41. {circup-2.2.6 → circup-2.4.0}/circup.egg-info/requires.txt +0 -0
  42. {circup-2.2.6 → circup-2.4.0}/circup.egg-info/top_level.txt +0 -0
  43. {circup-2.2.6 → circup-2.4.0}/docs/_static/favicon.ico +0 -0
  44. {circup-2.2.6 → circup-2.4.0}/docs/_static/favicon.ico.license +0 -0
  45. {circup-2.2.6 → circup-2.4.0}/docs/conf.py +0 -0
  46. {circup-2.2.6 → circup-2.4.0}/docs/index.rst +0 -0
  47. {circup-2.2.6 → circup-2.4.0}/docs/index.rst.license +0 -0
  48. {circup-2.2.6 → circup-2.4.0}/docs/logo.png +0 -0
  49. {circup-2.2.6 → circup-2.4.0}/docs/logo.png.license +0 -0
  50. {circup-2.2.6 → circup-2.4.0}/optional_requirements.txt +0 -0
  51. {circup-2.2.6 → circup-2.4.0}/optional_requirements.txt.license +0 -0
  52. {circup-2.2.6 → circup-2.4.0}/pyproject.toml +0 -0
  53. {circup-2.2.6 → circup-2.4.0}/readthedocs.yml +0 -0
  54. {circup-2.2.6 → circup-2.4.0}/requirements.txt +0 -0
  55. {circup-2.2.6 → circup-2.4.0}/requirements.txt.license +0 -0
  56. {circup-2.2.6 → circup-2.4.0}/setup.cfg +0 -0
  57. {circup-2.2.6 → circup-2.4.0}/tests/__init__.py +0 -0
  58. {circup-2.2.6 → circup-2.4.0}/tests/bad_module/__init__.py +0 -0
  59. {circup-2.2.6 → circup-2.4.0}/tests/bad_module/my_module.py +0 -0
  60. {circup-2.2.6 → circup-2.4.0}/tests/bad_python.py +0 -0
  61. {circup-2.2.6 → circup-2.4.0}/tests/bundle.json +0 -0
  62. {circup-2.2.6 → circup-2.4.0}/tests/bundle.json.license +0 -0
  63. {circup-2.2.6 → circup-2.4.0}/tests/device.json +0 -0
  64. {circup-2.2.6 → circup-2.4.0}/tests/device.json.license +0 -0
  65. {circup-2.2.6 → circup-2.4.0}/tests/dir_module/__init__.py +0 -0
  66. {circup-2.2.6 → circup-2.4.0}/tests/dir_module/my_module.py +0 -0
  67. {circup-2.2.6 → circup-2.4.0}/tests/import_styles.py +0 -0
  68. {circup-2.2.6 → circup-2.4.0}/tests/local_module.py +0 -0
  69. {circup-2.2.6 → circup-2.4.0}/tests/local_module_cp7.mpy +0 -0
  70. {circup-2.2.6 → circup-2.4.0}/tests/local_module_cp7.mpy.license +0 -0
  71. {circup-2.2.6 → circup-2.4.0}/tests/mock_device/apps/test_app/import_styles.py +0 -0
  72. {circup-2.2.6 → circup-2.4.0}/tests/mock_device/apps/test_app/import_styles_sub.py +0 -0
  73. {circup-2.2.6 → circup-2.4.0}/tests/mock_device/boot_out.txt +0 -0
  74. {circup-2.2.6 → circup-2.4.0}/tests/mock_device/boot_out.txt.license +0 -0
  75. {circup-2.2.6 → circup-2.4.0}/tests/mock_device/import_styles.py +0 -0
  76. {circup-2.2.6 → circup-2.4.0}/tests/mock_device/import_styles_sub.py +0 -0
  77. {circup-2.2.6 → circup-2.4.0}/tests/mock_device/lib/adafruit_waveform/.gitkeep +0 -0
  78. {circup-2.2.6 → circup-2.4.0}/tests/mock_device_2/.gitignore +0 -0
  79. {circup-2.2.6 → circup-2.4.0}/tests/mock_device_2/boot_out.txt +0 -0
  80. {circup-2.2.6 → circup-2.4.0}/tests/mock_device_2/boot_out.txt.license +0 -0
  81. {circup-2.2.6 → circup-2.4.0}/tests/mock_device_2/code.py +0 -0
  82. {circup-2.2.6 → circup-2.4.0}/tests/mock_device_2/import_styles_sub.py +0 -0
  83. {circup-2.2.6 → circup-2.4.0}/tests/mock_device_2/package/__init__.py +0 -0
  84. {circup-2.2.6 → circup-2.4.0}/tests/mock_device_2/package/other.py +0 -0
  85. {circup-2.2.6 → circup-2.4.0}/tests/mount_exists.txt +0 -0
  86. {circup-2.2.6 → circup-2.4.0}/tests/mount_exists.txt.license +0 -0
  87. {circup-2.2.6 → circup-2.4.0}/tests/mount_missing.txt +0 -0
  88. {circup-2.2.6 → circup-2.4.0}/tests/mount_missing.txt.license +0 -0
  89. {circup-2.2.6 → circup-2.4.0}/tests/remote_module.py +0 -0
  90. {circup-2.2.6 → circup-2.4.0}/tests/test_bundle_config.json +0 -0
  91. {circup-2.2.6 → circup-2.4.0}/tests/test_bundle_config.json.license +0 -0
  92. {circup-2.2.6 → circup-2.4.0}/tests/test_bundle_config_local.json +0 -0
  93. {circup-2.2.6 → circup-2.4.0}/tests/test_bundle_config_local.json.license +0 -0
  94. {circup-2.2.6 → circup-2.4.0}/tests/test_module.mpy +0 -0
  95. {circup-2.2.6 → circup-2.4.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.2.6
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
@@ -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.
@@ -0,0 +1,280 @@
1
+ # SPDX-FileCopyrightText: 2019 Nicholas Tollervey, 2024 Tim Cocks, written for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ """
5
+ Class that represents a specific release of a Bundle.
6
+ """
7
+ import os
8
+ import sys
9
+
10
+ import click
11
+ import requests
12
+
13
+ from semver import VersionInfo
14
+
15
+ from circup.shared import (
16
+ DATA_DIR,
17
+ PLATFORMS,
18
+ REQUESTS_TIMEOUT,
19
+ get_latest_release_from_url,
20
+ )
21
+
22
+ from circup.logging import logger
23
+
24
+
25
+ class Bundle: # pylint: disable=too-many-instance-attributes
26
+ """
27
+ All the links and file names for a bundle
28
+ """
29
+
30
+ #: Avoid requests to the internet
31
+ offline = False
32
+
33
+ def __init__(self, repo):
34
+ """
35
+ Initialise a Bundle created from its github info.
36
+ Construct all the strings in one place.
37
+
38
+ :param str repo: Repository string for github: "user/repository"
39
+ """
40
+ vendor, bundle_id = repo.split("/")
41
+ bundle_id = bundle_id.lower().replace("_", "-")
42
+ self.key = repo
43
+ #
44
+ self.url = "https://github.com/" + repo
45
+ self.basename = bundle_id + "-{platform}-{tag}"
46
+ self.urlzip = self.basename + ".zip"
47
+ self.dir = os.path.join(DATA_DIR, vendor, bundle_id + "-{platform}")
48
+ self.zip = os.path.join(DATA_DIR, bundle_id + "-{platform}.zip")
49
+ self.url_format = self.url + "/releases/download/{tag}/" + self.urlzip
50
+ # tag
51
+ self._current = None
52
+ self._latest = None
53
+ self.pinned_tag = None
54
+ self._available = []
55
+ #
56
+ self.platform = None
57
+
58
+ def lib_dir(self, source=False):
59
+ """
60
+ This bundle's lib directory for the bundle's source or compiled version.
61
+
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.
67
+ """
68
+ tag = self.current_tag
69
+ platform = "py" if source or not self.platform else self.platform
70
+ return os.path.join(
71
+ self.dir.format(platform=platform),
72
+ self.basename.format(platform=PLATFORMS[platform], tag=tag),
73
+ "lib",
74
+ )
75
+
76
+ def examples_dir(self, source=False):
77
+ """
78
+ This bundle's examples directory for the bundle's source or compiled
79
+ version.
80
+
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.
86
+ """
87
+ tag = self.current_tag
88
+ platform = "py" if source or not self.platform else self.platform
89
+ return os.path.join(
90
+ self.dir.format(platform=platform),
91
+ self.basename.format(platform=PLATFORMS[platform], tag=tag),
92
+ "examples",
93
+ )
94
+
95
+ def requirements_for(self, library_name, toml_file=False):
96
+ """
97
+ The requirements file for this library.
98
+
99
+ :param str library_name: The name of the library.
100
+ :return: The path to the requirements.txt file.
101
+ """
102
+ platform = "py"
103
+ tag = self.current_tag
104
+ found_file = os.path.join(
105
+ self.dir.format(platform=platform),
106
+ self.basename.format(platform=PLATFORMS[platform], tag=tag),
107
+ "requirements",
108
+ library_name,
109
+ "requirements.txt" if not toml_file else "pyproject.toml",
110
+ )
111
+ if os.path.isfile(found_file):
112
+ with open(found_file, "r", encoding="utf-8") as read_this:
113
+ return read_this.read()
114
+ return None
115
+
116
+ @property
117
+ def current_tag(self):
118
+ """
119
+ The current tag for the project. If the tag hasn't been explicitly set
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.
123
+
124
+ :return: The current tag value for the project.
125
+ """
126
+ if self._current is None:
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
+
139
+ return self._current
140
+
141
+ @current_tag.setter
142
+ def current_tag(self, tag):
143
+ """
144
+ Set the current tag (after updating).
145
+
146
+ :param str tag: The new value for the current tag.
147
+ """
148
+ self._current = tag
149
+
150
+ @property
151
+ def latest_tag(self):
152
+ """
153
+ Lazy find the value of the latest tag for the bundle.
154
+
155
+ :return: The most recent tag value for the project.
156
+ """
157
+ if self._latest is None:
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
+ )
164
+ return self._latest
165
+
166
+ @property
167
+ def available_tags(self):
168
+ """
169
+ The locally available tags to use for the project.
170
+
171
+ :return: All tags available for the project.
172
+ """
173
+ return tuple(self._available)
174
+
175
+ @available_tags.setter
176
+ def available_tags(self, tags):
177
+ """
178
+ Set the available tags.
179
+
180
+ :param str|list tags: The new value for the locally available tags.
181
+ """
182
+ if isinstance(tags, str):
183
+ tags = [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
193
+
194
+ def add_tag(self, tag: str) -> None:
195
+ """
196
+ Add a tag to the list of available tags.
197
+
198
+ This will add the tag if it isn't already present in the list of
199
+ available tags. The tag will be added so that the list is sorted in an
200
+ increasing order. This ensures that that last tag is always the latest.
201
+
202
+ :param str tag: The tag to add to the list of available tags.
203
+ """
204
+ if tag in self._available:
205
+ # The tag is already stored for some reason, lets not add it again
206
+ return
207
+
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)
225
+
226
+ def validate(self):
227
+ """
228
+ Test the existence of the expected URL (not the content)
229
+ """
230
+ tag = self.latest_tag
231
+ if not tag or tag == "releases":
232
+ if "--verbose" in sys.argv:
233
+ click.secho(f' Invalid tag "{tag}"', fg="red")
234
+ return False
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
243
+ return True
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
+
261
+ def __repr__(self):
262
+ """
263
+ Helps with log files.
264
+
265
+ :return: A repr of a dictionary containing the Bundles's metadata.
266
+ """
267
+ return repr(
268
+ {
269
+ "key": self.key,
270
+ "url": self.url,
271
+ "urlzip": self.urlzip,
272
+ "dir": self.dir,
273
+ "zip": self.zip,
274
+ "url_format": self.url_format,
275
+ "current": self._current,
276
+ "latest": self._latest,
277
+ "pinned": self.pinned_tag,
278
+ "available": self._available,
279
+ }
280
+ )