circup 1.7.0__tar.gz → 1.9.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 (78) hide show
  1. {circup-1.7.0/circup.egg-info → circup-1.9.0}/PKG-INFO +10 -7
  2. {circup-1.7.0 → circup-1.9.0}/README.rst +3 -0
  3. {circup-1.7.0 → circup-1.9.0}/circup/backends.py +136 -37
  4. {circup-1.7.0 → circup-1.9.0}/circup/bundle.py +14 -0
  5. {circup-1.7.0 → circup-1.9.0}/circup/command_utils.py +78 -13
  6. {circup-1.7.0 → circup-1.9.0}/circup/commands.py +54 -3
  7. {circup-1.7.0 → circup-1.9.0/circup.egg-info}/PKG-INFO +10 -7
  8. {circup-1.7.0 → circup-1.9.0}/tests/import_styles.py +1 -1
  9. {circup-1.7.0 → circup-1.9.0}/.github/ISSUE_TEMPLATE.md +0 -0
  10. {circup-1.7.0 → circup-1.9.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  11. {circup-1.7.0 → circup-1.9.0}/.github/workflows/build.yml +0 -0
  12. {circup-1.7.0 → circup-1.9.0}/.github/workflows/release.yml +0 -0
  13. {circup-1.7.0 → circup-1.9.0}/.gitignore +0 -0
  14. {circup-1.7.0 → circup-1.9.0}/.isort.cfg +0 -0
  15. {circup-1.7.0 → circup-1.9.0}/.pre-commit-config.yaml +0 -0
  16. {circup-1.7.0 → circup-1.9.0}/.pylintrc +0 -0
  17. {circup-1.7.0 → circup-1.9.0}/CODE_OF_CONDUCT.rst +0 -0
  18. {circup-1.7.0 → circup-1.9.0}/CODE_OF_CONDUCT.rst.license +0 -0
  19. {circup-1.7.0 → circup-1.9.0}/CONTRIBUTING.rst +0 -0
  20. {circup-1.7.0 → circup-1.9.0}/CONTRIBUTING.rst.license +0 -0
  21. {circup-1.7.0 → circup-1.9.0}/LICENSE +0 -0
  22. {circup-1.7.0 → circup-1.9.0}/LICENSES/CC-BY-4.0.txt +0 -0
  23. {circup-1.7.0 → circup-1.9.0}/LICENSES/MIT.txt +0 -0
  24. {circup-1.7.0 → circup-1.9.0}/LICENSES/Unlicense.txt +0 -0
  25. {circup-1.7.0 → circup-1.9.0}/README.rst.license +0 -0
  26. {circup-1.7.0 → circup-1.9.0}/circup/__init__.py +0 -0
  27. {circup-1.7.0 → circup-1.9.0}/circup/config/bundle_config.json +0 -0
  28. {circup-1.7.0 → circup-1.9.0}/circup/config/bundle_config.json.license +0 -0
  29. {circup-1.7.0 → circup-1.9.0}/circup/logging.py +0 -0
  30. {circup-1.7.0 → circup-1.9.0}/circup/module.py +0 -0
  31. {circup-1.7.0 → circup-1.9.0}/circup/shared.py +0 -0
  32. {circup-1.7.0 → circup-1.9.0}/circup.egg-info/SOURCES.txt +0 -0
  33. {circup-1.7.0 → circup-1.9.0}/circup.egg-info/dependency_links.txt +0 -0
  34. {circup-1.7.0 → circup-1.9.0}/circup.egg-info/entry_points.txt +0 -0
  35. {circup-1.7.0 → circup-1.9.0}/circup.egg-info/requires.txt +6 -6
  36. {circup-1.7.0 → circup-1.9.0}/circup.egg-info/top_level.txt +0 -0
  37. {circup-1.7.0 → circup-1.9.0}/docs/_static/favicon.ico +0 -0
  38. {circup-1.7.0 → circup-1.9.0}/docs/_static/favicon.ico.license +0 -0
  39. {circup-1.7.0 → circup-1.9.0}/docs/conf.py +0 -0
  40. {circup-1.7.0 → circup-1.9.0}/docs/index.rst +0 -0
  41. {circup-1.7.0 → circup-1.9.0}/docs/index.rst.license +0 -0
  42. {circup-1.7.0 → circup-1.9.0}/docs/logo.png +0 -0
  43. {circup-1.7.0 → circup-1.9.0}/docs/logo.png.license +0 -0
  44. {circup-1.7.0 → circup-1.9.0}/optional_requirements.txt +0 -0
  45. {circup-1.7.0 → circup-1.9.0}/optional_requirements.txt.license +0 -0
  46. {circup-1.7.0 → circup-1.9.0}/readthedocs.yml +0 -0
  47. {circup-1.7.0 → circup-1.9.0}/requirements.txt +0 -0
  48. {circup-1.7.0 → circup-1.9.0}/requirements.txt.license +0 -0
  49. {circup-1.7.0 → circup-1.9.0}/setup.cfg +0 -0
  50. {circup-1.7.0 → circup-1.9.0}/setup.py +0 -0
  51. {circup-1.7.0 → circup-1.9.0}/tests/__init__.py +0 -0
  52. {circup-1.7.0 → circup-1.9.0}/tests/bad_module/__init__.py +0 -0
  53. {circup-1.7.0 → circup-1.9.0}/tests/bad_module/my_module.py +0 -0
  54. {circup-1.7.0 → circup-1.9.0}/tests/bad_python.py +0 -0
  55. {circup-1.7.0 → circup-1.9.0}/tests/bundle.json +0 -0
  56. {circup-1.7.0 → circup-1.9.0}/tests/bundle.json.license +0 -0
  57. {circup-1.7.0 → circup-1.9.0}/tests/device.json +0 -0
  58. {circup-1.7.0 → circup-1.9.0}/tests/device.json.license +0 -0
  59. {circup-1.7.0 → circup-1.9.0}/tests/dir_module/__init__.py +0 -0
  60. {circup-1.7.0 → circup-1.9.0}/tests/dir_module/my_module.py +0 -0
  61. {circup-1.7.0 → circup-1.9.0}/tests/local_module.py +0 -0
  62. {circup-1.7.0 → circup-1.9.0}/tests/local_module_cp7.mpy +0 -0
  63. {circup-1.7.0 → circup-1.9.0}/tests/local_module_cp7.mpy.license +0 -0
  64. {circup-1.7.0 → circup-1.9.0}/tests/mock_device/boot_out.txt +0 -0
  65. {circup-1.7.0 → circup-1.9.0}/tests/mock_device/boot_out.txt.license +0 -0
  66. {circup-1.7.0 → circup-1.9.0}/tests/mock_device/lib/adafruit_waveform/.gitkeep +0 -0
  67. {circup-1.7.0 → circup-1.9.0}/tests/mount_exists.txt +0 -0
  68. {circup-1.7.0 → circup-1.9.0}/tests/mount_exists.txt.license +0 -0
  69. {circup-1.7.0 → circup-1.9.0}/tests/mount_missing.txt +0 -0
  70. {circup-1.7.0 → circup-1.9.0}/tests/mount_missing.txt.license +0 -0
  71. {circup-1.7.0 → circup-1.9.0}/tests/remote_module.py +0 -0
  72. {circup-1.7.0 → circup-1.9.0}/tests/test_bundle_config.json +0 -0
  73. {circup-1.7.0 → circup-1.9.0}/tests/test_bundle_config.json.license +0 -0
  74. {circup-1.7.0 → circup-1.9.0}/tests/test_bundle_config_local.json +0 -0
  75. {circup-1.7.0 → circup-1.9.0}/tests/test_bundle_config_local.json.license +0 -0
  76. {circup-1.7.0 → circup-1.9.0}/tests/test_circup.py +0 -0
  77. {circup-1.7.0 → circup-1.9.0}/tests/test_module.mpy +0 -0
  78. {circup-1.7.0 → circup-1.9.0}/tests/test_module.mpy.license +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: circup
3
- Version: 1.7.0
3
+ Version: 1.9.0
4
4
  Summary: A tool to manage/update libraries on CircuitPython devices.
5
5
  Home-page: https://github.com/adafruit/circup
6
6
  Author: Adafruit Industries
@@ -57,16 +57,16 @@ Requires-Dist: sphinx; extra == "dev"
57
57
  Requires-Dist: wheel; extra == "dev"
58
58
  Requires-Dist: twine; extra == "dev"
59
59
  Provides-Extra: all
60
+ Requires-Dist: pytest; extra == "all"
61
+ Requires-Dist: sphinx; extra == "all"
60
62
  Requires-Dist: wheel; extra == "all"
61
- Requires-Dist: pytest-faulthandler; extra == "all"
62
63
  Requires-Dist: pytest-random-order>=1.0.0; extra == "all"
63
- Requires-Dist: pylint; extra == "all"
64
- Requires-Dist: pytest; extra == "all"
64
+ Requires-Dist: pytest-faulthandler; extra == "all"
65
65
  Requires-Dist: coverage; extra == "all"
66
- Requires-Dist: black; extra == "all"
67
- Requires-Dist: pytest-cov; extra == "all"
66
+ Requires-Dist: pylint; extra == "all"
68
67
  Requires-Dist: twine; extra == "all"
69
- Requires-Dist: sphinx; extra == "all"
68
+ Requires-Dist: pytest-cov; extra == "all"
69
+ Requires-Dist: black; extra == "all"
70
70
 
71
71
 
72
72
  CircUp
@@ -165,6 +165,8 @@ To get help, just type the command::
165
165
  --host TEXT Hostname or IP address of a device. Overrides automatic
166
166
  path detection.
167
167
  --password TEXT Password to use for authentication when --host is used.
168
+ --timeout INTEGER Specify the timeout in seconds for any network
169
+ operations.
168
170
  --board-id TEXT Manual Board ID of the CircuitPython device. If provided
169
171
  in combination with --cpy-version, it overrides the
170
172
  detected board ID.
@@ -177,6 +179,7 @@ To get help, just type the command::
177
179
  bundle-add Add bundles to the local bundles list, by "user/repo"...
178
180
  bundle-remove Remove one or more bundles from the local bundles list.
179
181
  bundle-show Show the list of bundles, default and local, with URL,...
182
+ example Copy named example(s) from a bundle onto the device.
180
183
  freeze Output details of all the modules found on the connected...
181
184
  install Install a named module(s) onto the device.
182
185
  list Lists all out of date modules found on the connected...
@@ -95,6 +95,8 @@ To get help, just type the command::
95
95
  --host TEXT Hostname or IP address of a device. Overrides automatic
96
96
  path detection.
97
97
  --password TEXT Password to use for authentication when --host is used.
98
+ --timeout INTEGER Specify the timeout in seconds for any network
99
+ operations.
98
100
  --board-id TEXT Manual Board ID of the CircuitPython device. If provided
99
101
  in combination with --cpy-version, it overrides the
100
102
  detected board ID.
@@ -107,6 +109,7 @@ To get help, just type the command::
107
109
  bundle-add Add bundles to the local bundles list, by "user/repo"...
108
110
  bundle-remove Remove one or more bundles from the local bundles list.
109
111
  bundle-show Show the list of bundles, default and local, with URL,...
112
+ example Copy named example(s) from a bundle onto the device.
110
113
  freeze Output details of all the modules found on the connected...
111
114
  install Install a named module(s) onto the device.
112
115
  list Lists all out of date modules found on the connected...
@@ -16,7 +16,6 @@ import requests
16
16
  from requests.adapters import HTTPAdapter
17
17
  from requests.auth import HTTPBasicAuth
18
18
 
19
-
20
19
  from circup.shared import DATA_DIR, BAD_FILE_FORMAT, extract_metadata, _get_modules_file
21
20
 
22
21
  #: The location to store a local copy of code.py for use with --auto and
@@ -79,21 +78,27 @@ class Backend:
79
78
  """
80
79
  raise NotImplementedError
81
80
 
82
- def _install_module_py(self, metadata):
81
+ def install_module_py(self, metadata, location=None):
83
82
  """
84
83
  To be overridden by subclass
85
84
  """
86
85
  raise NotImplementedError
87
86
 
88
- def _install_module_mpy(self, bundle, metadata):
87
+ def install_module_mpy(self, bundle, metadata):
88
+ """
89
+ To be overridden by subclass
89
90
  """
91
+ raise NotImplementedError
92
+
93
+ def copy_file(self, target_file, location_to_paste):
94
+ """Paste a copy of the specified file at the location given
90
95
  To be overridden by subclass
91
96
  """
92
97
  raise NotImplementedError
93
98
 
94
- # pylint: disable=too-many-locals,too-many-branches,too-many-arguments,too-many-nested-blocks
99
+ # pylint: disable=too-many-locals,too-many-branches,too-many-arguments,too-many-nested-blocks,too-many-statements
95
100
  def install_module(
96
- self, device_path, device_modules, name, pyext, mod_names
101
+ self, device_path, device_modules, name, pyext, mod_names, upgrade=False
97
102
  ): # pragma: no cover
98
103
  """
99
104
  Finds a connected device and installs a given module name if it
@@ -108,23 +113,51 @@ class Backend:
108
113
  source or from a pre-compiled module
109
114
  :param mod_names: Dictionary of metadata from modules that can be generated
110
115
  with get_bundle_versions()
116
+ :param bool upgrade: Upgrade the specified modules if they're already installed.
111
117
  """
118
+ local_path = None
119
+ if os.path.exists(name):
120
+ # local file exists use that.
121
+ local_path = name
122
+ name = local_path.split(os.path.sep)[-1]
123
+ name = name.replace(".py", "").replace(".mpy", "")
124
+ click.echo(f"Installing from local path: {local_path}")
125
+
112
126
  if not name:
113
127
  click.echo("No module name(s) provided.")
114
- elif name in mod_names:
128
+ return
129
+ if name in mod_names or local_path is not None:
130
+
115
131
  # Grab device modules to check if module already installed
116
132
  if name in device_modules:
117
- click.echo("'{}' is already installed.".format(name))
118
- return
119
-
133
+ if not upgrade:
134
+ # skip already installed modules if no -upgrade flag
135
+ click.echo("'{}' is already installed.".format(name))
136
+ return
137
+
138
+ # uninstall the module before installing
139
+ name = name.lower()
140
+ _mod_names = {}
141
+ for module_item, _metadata in device_modules.items():
142
+ _mod_names[module_item.replace(".py", "").lower()] = _metadata
143
+ if name in _mod_names:
144
+ _metadata = _mod_names[name]
145
+ module_path = _metadata["path"]
146
+ self.uninstall(device_path, module_path)
147
+
148
+ new_module_size = 0
120
149
  library_path = (
121
150
  os.path.join(device_path, self.LIB_DIR_PATH)
122
151
  if not isinstance(self, WebBackend)
123
152
  else urljoin(device_path, self.LIB_DIR_PATH)
124
153
  )
125
- metadata = mod_names[name]
126
- bundle = metadata["bundle"]
127
- bundle.size = os.path.getsize(metadata["path"])
154
+ if local_path is None:
155
+ metadata = mod_names[name]
156
+ bundle = metadata["bundle"]
157
+ else:
158
+ metadata = {"path": local_path}
159
+
160
+ new_module_size = os.path.getsize(metadata["path"])
128
161
  if os.path.isdir(metadata["path"]):
129
162
  # pylint: disable=unused-variable
130
163
  for dirpath, dirnames, filenames in os.walk(metadata["path"]):
@@ -132,7 +165,7 @@ class Backend:
132
165
  fp = os.path.join(dirpath, f)
133
166
  try:
134
167
  if not os.path.islink(fp): # Ignore symbolic links
135
- bundle.size += os.path.getsize(fp)
168
+ new_module_size += os.path.getsize(fp)
136
169
  else:
137
170
  self.logger.warning(
138
171
  f"Skipping symbolic link in space calculation: {fp}"
@@ -142,27 +175,29 @@ class Backend:
142
175
  f"Error: {e} - Skipping file in space calculation: {fp}"
143
176
  )
144
177
 
145
- if self.get_free_space() < bundle.size:
178
+ if self.get_free_space() < new_module_size:
146
179
  self.logger.error(
147
180
  f"Aborted installing module {name} - "
148
- f"not enough free space ({bundle.size} < {self.get_free_space()})"
181
+ f"not enough free space ({new_module_size} < {self.get_free_space()})"
149
182
  )
150
183
  click.secho(
151
184
  f"Aborted installing module {name} - "
152
- f"not enough free space ({bundle.size} < {self.get_free_space()})",
185
+ f"not enough free space ({new_module_size} < {self.get_free_space()})",
153
186
  fg="red",
154
187
  )
155
188
  return
156
189
 
157
190
  # Create the library directory first.
158
191
  self._create_library_directory(device_path, library_path)
159
-
160
- if pyext:
161
- # Use Python source for module.
162
- self._install_module_py(metadata)
192
+ if local_path is None:
193
+ if pyext:
194
+ # Use Python source for module.
195
+ self.install_module_py(metadata)
196
+ else:
197
+ # Use pre-compiled mpy modules.
198
+ self.install_module_mpy(bundle, metadata)
163
199
  else:
164
- # Use pre-compiled mpy modules.
165
- self._install_module_mpy(bundle, metadata)
200
+ self.copy_file(metadata["path"], "lib")
166
201
  click.echo("Installed '{}'.".format(name))
167
202
  else:
168
203
  click.echo("Unknown module named, '{}'.".format(name))
@@ -219,6 +254,12 @@ class Backend:
219
254
  board_id = ""
220
255
  return circuit_python, board_id
221
256
 
257
+ def file_exists(self, filepath):
258
+ """
259
+ To be overriden by subclass
260
+ """
261
+ raise NotImplementedError
262
+
222
263
 
223
264
  def _writeable_error():
224
265
  click.secho(
@@ -250,7 +291,8 @@ class WebBackend(Backend):
250
291
  else "Could not find or connect to specified device"
251
292
  ) from exc
252
293
 
253
- self.LIB_DIR_PATH = "fs/lib/"
294
+ self.FS_PATH = "fs/"
295
+ self.LIB_DIR_PATH = f"{self.FS_PATH}lib/"
254
296
  self.host = host
255
297
  self.password = password
256
298
  self.device_location = f"http://:{self.password}@{self.host}"
@@ -260,14 +302,20 @@ class WebBackend(Backend):
260
302
  self.library_path = self.device_location + "/" + self.LIB_DIR_PATH
261
303
  self.timeout = timeout
262
304
 
263
- def install_file_http(self, source):
305
+ def install_file_http(self, source, location=None):
264
306
  """
265
307
  Install file to device using web workflow.
266
308
  :param source source file.
309
+ :param location the location on the device to copy the source
310
+ directory in to. If omitted is CIRCUITPY/lib/ used.
267
311
  """
268
312
  file_name = source.split(os.path.sep)
269
313
  file_name = file_name[-2] if file_name[-1] == "" else file_name[-1]
270
- target = self.device_location + "/" + self.LIB_DIR_PATH + file_name
314
+
315
+ if location is None:
316
+ target = self.device_location + "/" + self.LIB_DIR_PATH + file_name
317
+ else:
318
+ target = self.device_location + "/" + self.FS_PATH + location + file_name
271
319
 
272
320
  auth = HTTPBasicAuth("", self.password)
273
321
 
@@ -277,14 +325,19 @@ class WebBackend(Backend):
277
325
  _writeable_error()
278
326
  r.raise_for_status()
279
327
 
280
- def install_dir_http(self, source):
328
+ def install_dir_http(self, source, location=None):
281
329
  """
282
330
  Install directory to device using web workflow.
283
331
  :param source source directory.
332
+ :param location the location on the device to copy the source
333
+ directory in to. If omitted is CIRCUITPY/lib/ used.
284
334
  """
285
335
  mod_name = source.split(os.path.sep)
286
336
  mod_name = mod_name[-2] if mod_name[-1] == "" else mod_name[-1]
287
- target = self.device_location + "/" + self.LIB_DIR_PATH + mod_name
337
+ if location is None:
338
+ target = self.device_location + "/" + self.LIB_DIR_PATH + mod_name
339
+ else:
340
+ target = self.device_location + "/" + self.FS_PATH + location + mod_name
288
341
  target = target + "/" if target[:-1] != "/" else target
289
342
  url = urlparse(target)
290
343
  auth = HTTPBasicAuth("", url.password)
@@ -490,7 +543,18 @@ class WebBackend(Backend):
490
543
  _writeable_error()
491
544
  r.raise_for_status()
492
545
 
493
- def _install_module_mpy(self, bundle, metadata):
546
+ def copy_file(self, target_file, location_to_paste):
547
+ if os.path.isdir(target_file):
548
+ create_directory_url = urljoin(
549
+ self.device_location,
550
+ "/".join(("fs", location_to_paste, target_file, "")),
551
+ )
552
+ self._create_library_directory(self.device_location, create_directory_url)
553
+ self.install_dir_http(target_file)
554
+ else:
555
+ self.install_file_http(target_file)
556
+
557
+ def install_module_mpy(self, bundle, metadata):
494
558
  """
495
559
  :param bundle library bundle.
496
560
  :param library_path library path
@@ -514,7 +578,7 @@ class WebBackend(Backend):
514
578
  raise IOError("Cannot find compiled version of module.")
515
579
 
516
580
  # pylint: enable=too-many-locals,too-many-branches
517
- def _install_module_py(self, metadata):
581
+ def install_module_py(self, metadata, location=None):
518
582
  """
519
583
  :param library_path library path
520
584
  :param metadata dictionary.
@@ -522,10 +586,10 @@ class WebBackend(Backend):
522
586
 
523
587
  source_path = metadata["path"] # Path to Python source version.
524
588
  if os.path.isdir(source_path):
525
- self.install_dir_http(source_path)
589
+ self.install_dir_http(source_path, location=location)
526
590
 
527
591
  else:
528
- self.install_file_http(source_path)
592
+ self.install_file_http(source_path, location=location)
529
593
 
530
594
  def get_auto_file_path(self, auto_file_path):
531
595
  """
@@ -560,6 +624,18 @@ class WebBackend(Backend):
560
624
  """
561
625
  self._update_http(module)
562
626
 
627
+ def file_exists(self, filepath):
628
+ """
629
+ return True if the file exists, otherwise False.
630
+ """
631
+ auth = HTTPBasicAuth("", self.password)
632
+ resp = requests.get(
633
+ self.get_file_path(filepath), auth=auth, timeout=self.timeout
634
+ )
635
+ if resp.status_code == 200:
636
+ return True
637
+ return False
638
+
563
639
  def _update_http(self, module):
564
640
  """
565
641
  Update the module using web workflow.
@@ -733,10 +809,22 @@ class DiskBackend(Backend):
733
809
  if not os.path.exists(library_path): # pragma: no cover
734
810
  os.makedirs(library_path)
735
811
 
736
- def _install_module_mpy(self, bundle, metadata):
812
+ def copy_file(self, target_file, location_to_paste):
813
+ target_filename = target_file.split(os.path.sep)[-1]
814
+ if os.path.isdir(target_file):
815
+ shutil.copytree(
816
+ target_file,
817
+ os.path.join(self.device_location, location_to_paste, target_filename),
818
+ )
819
+ else:
820
+ shutil.copyfile(
821
+ target_file,
822
+ os.path.join(self.device_location, location_to_paste, target_filename),
823
+ )
824
+
825
+ def install_module_mpy(self, bundle, metadata):
737
826
  """
738
827
  :param bundle library bundle.
739
- :param library_path library path
740
828
  :param metadata dictionary.
741
829
  """
742
830
  module_name = os.path.basename(metadata["path"]).replace(".py", ".mpy")
@@ -763,21 +851,26 @@ class DiskBackend(Backend):
763
851
  raise IOError("Cannot find compiled version of module.")
764
852
 
765
853
  # pylint: enable=too-many-locals,too-many-branches
766
- def _install_module_py(self, metadata):
854
+ def install_module_py(self, metadata, location=None):
767
855
  """
768
- :param library_path library path
769
856
  :param metadata dictionary.
857
+ :param location the location on the device to copy the py module to.
858
+ If omitted is CIRCUITPY/lib/ used.
770
859
  """
860
+ if location is None:
861
+ location = self.library_path
862
+ else:
863
+ location = os.path.join(self.device_location, location)
771
864
 
772
865
  source_path = metadata["path"] # Path to Python source version.
773
866
  if os.path.isdir(source_path):
774
867
  target = os.path.basename(os.path.dirname(source_path))
775
- target_path = os.path.join(self.library_path, target)
868
+ target_path = os.path.join(location, target)
776
869
  # Copy the directory.
777
870
  shutil.copytree(source_path, target_path)
778
871
  else:
779
872
  target = os.path.basename(source_path)
780
- target_path = os.path.join(self.library_path, target)
873
+ target_path = os.path.join(location, target)
781
874
  # Copy file.
782
875
  shutil.copyfile(source_path, target_path)
783
876
 
@@ -825,6 +918,12 @@ class DiskBackend(Backend):
825
918
  os.remove(module.path)
826
919
  shutil.copyfile(module.bundle_path, module.path)
827
920
 
921
+ def file_exists(self, filepath):
922
+ """
923
+ return True if the file exists, otherwise False.
924
+ """
925
+ return os.path.exists(os.path.join(self.device_location, filepath))
926
+
828
927
  def get_file_path(self, filename):
829
928
  """
830
929
  returns the full path on the device to a given file name.
@@ -61,6 +61,20 @@ class Bundle:
61
61
  "lib",
62
62
  )
63
63
 
64
+ def examples_dir(self, platform):
65
+ """
66
+ This bundle's examples directory for the platform.
67
+
68
+ :param str platform: The platform identifier (py/6mpy/...).
69
+ :return: The path to the examples directory for the platform.
70
+ """
71
+ tag = self.current_tag
72
+ return os.path.join(
73
+ self.dir.format(platform=platform),
74
+ self.basename.format(platform=PLATFORMS[platform], tag=tag),
75
+ "examples",
76
+ )
77
+
64
78
  def requirements_for(self, library_name, toml_file=False):
65
79
  """
66
80
  The requirements file for this library.
@@ -6,6 +6,7 @@ Functions called from commands in order to provide behaviors and return informat
6
6
  """
7
7
 
8
8
  import ctypes
9
+ import glob
9
10
  import os
10
11
 
11
12
  from subprocess import check_output
@@ -90,9 +91,27 @@ def completion_for_install(ctx, param, incomplete):
90
91
  module_names = {m.replace(".py", "") for m in available_modules}
91
92
  if incomplete:
92
93
  module_names = [name for name in module_names if name.startswith(incomplete)]
94
+ module_names.extend(glob.glob(f"{incomplete}*"))
93
95
  return sorted(module_names)
94
96
 
95
97
 
98
+ def completion_for_example(ctx, param, incomplete):
99
+ """
100
+ Returns the list of available modules for the command line tab-completion
101
+ with the ``circup example`` command.
102
+ """
103
+ # pylint: disable=unused-argument, consider-iterating-dictionary
104
+ available_examples = get_bundle_examples(get_bundles_list(), avoid_download=True)
105
+
106
+ matching_examples = [
107
+ example_path
108
+ for example_path in available_examples.keys()
109
+ if example_path.startswith(incomplete)
110
+ ]
111
+
112
+ return sorted(matching_examples)
113
+
114
+
96
115
  def ensure_latest_bundle(bundle):
97
116
  """
98
117
  Ensure that there's a copy of the latest library bundle available so circup
@@ -290,6 +309,43 @@ def get_bundle(bundle, tag):
290
309
  click.echo("\nOK\n")
291
310
 
292
311
 
312
+ def get_bundle_examples(bundles_list, avoid_download=False):
313
+ """
314
+ Return a dictionary of metadata from examples in the all of the bundles
315
+ specified by bundles_list argument.
316
+
317
+ :param List[Bundle] bundles_list: List of supported bundles as Bundle objects.
318
+ :param bool avoid_download: if True, download the bundle only if missing.
319
+ :return: A dictionary of metadata about the examples available in the
320
+ library bundle.
321
+ """
322
+ # pylint: disable=too-many-nested-blocks
323
+ all_the_examples = dict()
324
+
325
+ try:
326
+ for bundle in bundles_list:
327
+ if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
328
+ ensure_latest_bundle(bundle)
329
+ path = bundle.examples_dir("py")
330
+ path_examples = _get_modules_file(path, logger)
331
+ for lib_name, lib_metadata in path_examples.items():
332
+ for _dir_level in os.walk(lib_metadata["path"]):
333
+ for _file in _dir_level[2]:
334
+ _parts = _dir_level[0].split(os.path.sep)
335
+ _lib_name_index = _parts.index(lib_name)
336
+ _dirs = _parts[_lib_name_index:]
337
+ if _dirs[-1] == "":
338
+ _dirs.pop(-1)
339
+ slug = f"{os.path.sep}".join(_dirs + [_file.replace(".py", "")])
340
+ all_the_examples[slug] = os.path.join(_dir_level[0], _file)
341
+
342
+ except NotADirectoryError:
343
+ # Bundle does not have new style examples directory
344
+ # so we cannot include its examples.
345
+ pass
346
+ return all_the_examples
347
+
348
+
293
349
  def get_bundle_versions(bundles_list, avoid_download=False):
294
350
  """
295
351
  Returns a dictionary of metadata from modules in the latest known release
@@ -392,6 +448,7 @@ def get_dependencies(*requested_libraries, mod_names, to_install=()):
392
448
  :param list(str) to_install: Modules already selected for installation.
393
449
  :return: tuple of module names to install which we build
394
450
  """
451
+ # pylint: disable=too-many-branches
395
452
  # Internal variables
396
453
  _to_install = to_install
397
454
  _requested_libraries = []
@@ -417,10 +474,14 @@ def get_dependencies(*requested_libraries, mod_names, to_install=()):
417
474
  _requested_libraries.append(canonical_lib_name)
418
475
  except KeyError:
419
476
  if canonical_lib_name not in WARNING_IGNORE_MODULES:
420
- click.secho(
421
- f"WARNING:\n\t{canonical_lib_name} is not a known CircuitPython library.",
422
- fg="yellow",
423
- )
477
+ if os.path.exists(canonical_lib_name):
478
+ _requested_libraries.append(canonical_lib_name)
479
+ else:
480
+ click.secho(
481
+ f"WARNING:\n\t{canonical_lib_name} "
482
+ f"is not a known CircuitPython library.",
483
+ fg="yellow",
484
+ )
424
485
 
425
486
  if not _requested_libraries:
426
487
  # If nothing is requested, we're done
@@ -430,16 +491,20 @@ def get_dependencies(*requested_libraries, mod_names, to_install=()):
430
491
  if library not in _to_install:
431
492
  _to_install = _to_install + (library,)
432
493
  # get the requirements.txt from bundle
433
- bundle = mod_names[library]["bundle"]
434
- requirements_txt = bundle.requirements_for(library)
435
- if requirements_txt:
436
- _requested_libraries.extend(
437
- libraries_from_requirements(requirements_txt)
438
- )
494
+ try:
495
+ bundle = mod_names[library]["bundle"]
496
+ requirements_txt = bundle.requirements_for(library)
497
+ if requirements_txt:
498
+ _requested_libraries.extend(
499
+ libraries_from_requirements(requirements_txt)
500
+ )
439
501
 
440
- circup_dependencies = get_circup_dependencies(bundle, library)
441
- for circup_dependency in circup_dependencies:
442
- _requested_libraries.append(circup_dependency)
502
+ circup_dependencies = get_circup_dependencies(bundle, library)
503
+ for circup_dependency in circup_dependencies:
504
+ _requested_libraries.append(circup_dependency)
505
+ except KeyError:
506
+ # don't check local file for further dependencies
507
+ pass
443
508
 
444
509
  # we've processed this library, remove it from the list
445
510
  _requested_libraries.remove(library)
@@ -38,6 +38,8 @@ from circup.command_utils import (
38
38
  get_bundles_local_dict,
39
39
  save_local_bundles,
40
40
  get_bundles_dict,
41
+ completion_for_example,
42
+ get_bundle_examples,
41
43
  )
42
44
 
43
45
 
@@ -284,6 +286,9 @@ def list_cli(ctx): # pragma: no cover
284
286
  @click.option(
285
287
  "--auto", "-a", is_flag=True, help="Install the modules imported in code.py."
286
288
  )
289
+ @click.option(
290
+ "--upgrade", "-U", is_flag=True, help="Upgrade modules that are already installed."
291
+ )
287
292
  @click.option(
288
293
  "--auto-file",
289
294
  default=None,
@@ -291,11 +296,13 @@ def list_cli(ctx): # pragma: no cover
291
296
  " Also accepts an absolute path or a local ./ path.",
292
297
  )
293
298
  @click.pass_context
294
- def install(ctx, modules, pyext, requirement, auto, auto_file): # pragma: no cover
299
+ def install(
300
+ ctx, modules, pyext, requirement, auto, auto_file, upgrade=False
301
+ ): # pragma: no cover
295
302
  """
296
303
  Install a named module(s) onto the device. Multiple modules
297
304
  can be installed at once by providing more than one module name, each
298
- separated by a space.
305
+ separated by a space. Modules can be from a Bundle or local filepaths.
299
306
  """
300
307
 
301
308
  # TODO: Ensure there's enough space on the device
@@ -343,7 +350,51 @@ def install(ctx, modules, pyext, requirement, auto, auto_file): # pragma: no co
343
350
  click.echo(f"Ready to install: {to_install}\n")
344
351
  for library in to_install:
345
352
  ctx.obj["backend"].install_module(
346
- ctx.obj["DEVICE_PATH"], device_modules, library, pyext, mod_names
353
+ ctx.obj["DEVICE_PATH"],
354
+ device_modules,
355
+ library,
356
+ pyext,
357
+ mod_names,
358
+ upgrade,
359
+ )
360
+
361
+
362
+ @main.command()
363
+ @click.option("--overwrite", is_flag=True, help="Overwrite the file if it exists.")
364
+ @click.argument(
365
+ "examples", required=True, nargs=-1, shell_complete=completion_for_example
366
+ )
367
+ @click.pass_context
368
+ def example(ctx, examples, overwrite):
369
+ """
370
+ Copy named example(s) from a bundle onto the device. Multiple examples
371
+ can be installed at once by providing more than one example name, each
372
+ separated by a space.
373
+ """
374
+
375
+ for example_arg in examples:
376
+ available_examples = get_bundle_examples(
377
+ get_bundles_list(), avoid_download=True
378
+ )
379
+ if example_arg in available_examples:
380
+ filename = available_examples[example_arg].split(os.path.sep)[-1]
381
+
382
+ if overwrite or not ctx.obj["backend"].file_exists(filename):
383
+ click.echo(
384
+ f"{'Copying' if not overwrite else 'Overwriting'}: {filename}"
385
+ )
386
+ ctx.obj["backend"].install_module_py(
387
+ {"path": available_examples[example_arg]}, location=""
388
+ )
389
+ else:
390
+ click.secho(
391
+ f"File: {filename} already exists. Use --overwrite if you wish to replace it.",
392
+ fg="red",
393
+ )
394
+ else:
395
+ click.secho(
396
+ f"Error: {example_arg} was not found in any local bundle examples.",
397
+ fg="red",
347
398
  )
348
399
 
349
400
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: circup
3
- Version: 1.7.0
3
+ Version: 1.9.0
4
4
  Summary: A tool to manage/update libraries on CircuitPython devices.
5
5
  Home-page: https://github.com/adafruit/circup
6
6
  Author: Adafruit Industries
@@ -57,16 +57,16 @@ Requires-Dist: sphinx; extra == "dev"
57
57
  Requires-Dist: wheel; extra == "dev"
58
58
  Requires-Dist: twine; extra == "dev"
59
59
  Provides-Extra: all
60
+ Requires-Dist: pytest; extra == "all"
61
+ Requires-Dist: sphinx; extra == "all"
60
62
  Requires-Dist: wheel; extra == "all"
61
- Requires-Dist: pytest-faulthandler; extra == "all"
62
63
  Requires-Dist: pytest-random-order>=1.0.0; extra == "all"
63
- Requires-Dist: pylint; extra == "all"
64
- Requires-Dist: pytest; extra == "all"
64
+ Requires-Dist: pytest-faulthandler; extra == "all"
65
65
  Requires-Dist: coverage; extra == "all"
66
- Requires-Dist: black; extra == "all"
67
- Requires-Dist: pytest-cov; extra == "all"
66
+ Requires-Dist: pylint; extra == "all"
68
67
  Requires-Dist: twine; extra == "all"
69
- Requires-Dist: sphinx; extra == "all"
68
+ Requires-Dist: pytest-cov; extra == "all"
69
+ Requires-Dist: black; extra == "all"
70
70
 
71
71
 
72
72
  CircUp
@@ -165,6 +165,8 @@ To get help, just type the command::
165
165
  --host TEXT Hostname or IP address of a device. Overrides automatic
166
166
  path detection.
167
167
  --password TEXT Password to use for authentication when --host is used.
168
+ --timeout INTEGER Specify the timeout in seconds for any network
169
+ operations.
168
170
  --board-id TEXT Manual Board ID of the CircuitPython device. If provided
169
171
  in combination with --cpy-version, it overrides the
170
172
  detected board ID.
@@ -177,6 +179,7 @@ To get help, just type the command::
177
179
  bundle-add Add bundles to the local bundles list, by "user/repo"...
178
180
  bundle-remove Remove one or more bundles from the local bundles list.
179
181
  bundle-show Show the list of bundles, default and local, with URL,...
182
+ example Copy named example(s) from a bundle onto the device.
180
183
  freeze Output details of all the modules found on the connected...
181
184
  install Install a named module(s) onto the device.
182
185
  list Lists all out of date modules found on the connected...
@@ -4,5 +4,5 @@
4
4
  # pylint: disable=all
5
5
  import adafruit_bus_device
6
6
  from adafruit_button import Button
7
- import adafruit_esp32spi.adafruit_esp32spi_socket as socket
7
+ from adafruit_esp32spi import adafruit_esp32spi_socketpool
8
8
  import adafruit_hid.consumer_control
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -10,16 +10,16 @@ update_checker
10
10
  importlib_metadata
11
11
 
12
12
  [all]
13
+ pytest
14
+ sphinx
13
15
  wheel
14
- pytest-faulthandler
15
16
  pytest-random-order>=1.0.0
16
- pylint
17
- pytest
17
+ pytest-faulthandler
18
18
  coverage
19
- black
20
- pytest-cov
19
+ pylint
21
20
  twine
22
- sphinx
21
+ pytest-cov
22
+ black
23
23
 
24
24
  [dev]
25
25
  pytest
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes