circup 1.7.0__tar.gz → 1.8.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.8.0}/PKG-INFO +8 -5
  2. {circup-1.7.0 → circup-1.8.0}/README.rst +3 -0
  3. {circup-1.7.0 → circup-1.8.0}/circup/backends.py +75 -23
  4. {circup-1.7.0 → circup-1.8.0}/circup/bundle.py +14 -0
  5. {circup-1.7.0 → circup-1.8.0}/circup/command_utils.py +54 -0
  6. {circup-1.7.0 → circup-1.8.0}/circup/commands.py +53 -2
  7. {circup-1.7.0 → circup-1.8.0/circup.egg-info}/PKG-INFO +8 -5
  8. {circup-1.7.0 → circup-1.8.0}/.github/ISSUE_TEMPLATE.md +0 -0
  9. {circup-1.7.0 → circup-1.8.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  10. {circup-1.7.0 → circup-1.8.0}/.github/workflows/build.yml +0 -0
  11. {circup-1.7.0 → circup-1.8.0}/.github/workflows/release.yml +0 -0
  12. {circup-1.7.0 → circup-1.8.0}/.gitignore +0 -0
  13. {circup-1.7.0 → circup-1.8.0}/.isort.cfg +0 -0
  14. {circup-1.7.0 → circup-1.8.0}/.pre-commit-config.yaml +0 -0
  15. {circup-1.7.0 → circup-1.8.0}/.pylintrc +0 -0
  16. {circup-1.7.0 → circup-1.8.0}/CODE_OF_CONDUCT.rst +0 -0
  17. {circup-1.7.0 → circup-1.8.0}/CODE_OF_CONDUCT.rst.license +0 -0
  18. {circup-1.7.0 → circup-1.8.0}/CONTRIBUTING.rst +0 -0
  19. {circup-1.7.0 → circup-1.8.0}/CONTRIBUTING.rst.license +0 -0
  20. {circup-1.7.0 → circup-1.8.0}/LICENSE +0 -0
  21. {circup-1.7.0 → circup-1.8.0}/LICENSES/CC-BY-4.0.txt +0 -0
  22. {circup-1.7.0 → circup-1.8.0}/LICENSES/MIT.txt +0 -0
  23. {circup-1.7.0 → circup-1.8.0}/LICENSES/Unlicense.txt +0 -0
  24. {circup-1.7.0 → circup-1.8.0}/README.rst.license +0 -0
  25. {circup-1.7.0 → circup-1.8.0}/circup/__init__.py +0 -0
  26. {circup-1.7.0 → circup-1.8.0}/circup/config/bundle_config.json +0 -0
  27. {circup-1.7.0 → circup-1.8.0}/circup/config/bundle_config.json.license +0 -0
  28. {circup-1.7.0 → circup-1.8.0}/circup/logging.py +0 -0
  29. {circup-1.7.0 → circup-1.8.0}/circup/module.py +0 -0
  30. {circup-1.7.0 → circup-1.8.0}/circup/shared.py +0 -0
  31. {circup-1.7.0 → circup-1.8.0}/circup.egg-info/SOURCES.txt +0 -0
  32. {circup-1.7.0 → circup-1.8.0}/circup.egg-info/dependency_links.txt +0 -0
  33. {circup-1.7.0 → circup-1.8.0}/circup.egg-info/entry_points.txt +0 -0
  34. {circup-1.7.0 → circup-1.8.0}/circup.egg-info/requires.txt +4 -4
  35. {circup-1.7.0 → circup-1.8.0}/circup.egg-info/top_level.txt +0 -0
  36. {circup-1.7.0 → circup-1.8.0}/docs/_static/favicon.ico +0 -0
  37. {circup-1.7.0 → circup-1.8.0}/docs/_static/favicon.ico.license +0 -0
  38. {circup-1.7.0 → circup-1.8.0}/docs/conf.py +0 -0
  39. {circup-1.7.0 → circup-1.8.0}/docs/index.rst +0 -0
  40. {circup-1.7.0 → circup-1.8.0}/docs/index.rst.license +0 -0
  41. {circup-1.7.0 → circup-1.8.0}/docs/logo.png +0 -0
  42. {circup-1.7.0 → circup-1.8.0}/docs/logo.png.license +0 -0
  43. {circup-1.7.0 → circup-1.8.0}/optional_requirements.txt +0 -0
  44. {circup-1.7.0 → circup-1.8.0}/optional_requirements.txt.license +0 -0
  45. {circup-1.7.0 → circup-1.8.0}/readthedocs.yml +0 -0
  46. {circup-1.7.0 → circup-1.8.0}/requirements.txt +0 -0
  47. {circup-1.7.0 → circup-1.8.0}/requirements.txt.license +0 -0
  48. {circup-1.7.0 → circup-1.8.0}/setup.cfg +0 -0
  49. {circup-1.7.0 → circup-1.8.0}/setup.py +0 -0
  50. {circup-1.7.0 → circup-1.8.0}/tests/__init__.py +0 -0
  51. {circup-1.7.0 → circup-1.8.0}/tests/bad_module/__init__.py +0 -0
  52. {circup-1.7.0 → circup-1.8.0}/tests/bad_module/my_module.py +0 -0
  53. {circup-1.7.0 → circup-1.8.0}/tests/bad_python.py +0 -0
  54. {circup-1.7.0 → circup-1.8.0}/tests/bundle.json +0 -0
  55. {circup-1.7.0 → circup-1.8.0}/tests/bundle.json.license +0 -0
  56. {circup-1.7.0 → circup-1.8.0}/tests/device.json +0 -0
  57. {circup-1.7.0 → circup-1.8.0}/tests/device.json.license +0 -0
  58. {circup-1.7.0 → circup-1.8.0}/tests/dir_module/__init__.py +0 -0
  59. {circup-1.7.0 → circup-1.8.0}/tests/dir_module/my_module.py +0 -0
  60. {circup-1.7.0 → circup-1.8.0}/tests/import_styles.py +0 -0
  61. {circup-1.7.0 → circup-1.8.0}/tests/local_module.py +0 -0
  62. {circup-1.7.0 → circup-1.8.0}/tests/local_module_cp7.mpy +0 -0
  63. {circup-1.7.0 → circup-1.8.0}/tests/local_module_cp7.mpy.license +0 -0
  64. {circup-1.7.0 → circup-1.8.0}/tests/mock_device/boot_out.txt +0 -0
  65. {circup-1.7.0 → circup-1.8.0}/tests/mock_device/boot_out.txt.license +0 -0
  66. {circup-1.7.0 → circup-1.8.0}/tests/mock_device/lib/adafruit_waveform/.gitkeep +0 -0
  67. {circup-1.7.0 → circup-1.8.0}/tests/mount_exists.txt +0 -0
  68. {circup-1.7.0 → circup-1.8.0}/tests/mount_exists.txt.license +0 -0
  69. {circup-1.7.0 → circup-1.8.0}/tests/mount_missing.txt +0 -0
  70. {circup-1.7.0 → circup-1.8.0}/tests/mount_missing.txt.license +0 -0
  71. {circup-1.7.0 → circup-1.8.0}/tests/remote_module.py +0 -0
  72. {circup-1.7.0 → circup-1.8.0}/tests/test_bundle_config.json +0 -0
  73. {circup-1.7.0 → circup-1.8.0}/tests/test_bundle_config.json.license +0 -0
  74. {circup-1.7.0 → circup-1.8.0}/tests/test_bundle_config_local.json +0 -0
  75. {circup-1.7.0 → circup-1.8.0}/tests/test_bundle_config_local.json.license +0 -0
  76. {circup-1.7.0 → circup-1.8.0}/tests/test_circup.py +0 -0
  77. {circup-1.7.0 → circup-1.8.0}/tests/test_module.mpy +0 -0
  78. {circup-1.7.0 → circup-1.8.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.8.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
@@ -59,14 +59,14 @@ Requires-Dist: twine; extra == "dev"
59
59
  Provides-Extra: all
60
60
  Requires-Dist: wheel; extra == "all"
61
61
  Requires-Dist: pytest-faulthandler; extra == "all"
62
- Requires-Dist: pytest-random-order>=1.0.0; extra == "all"
62
+ Requires-Dist: coverage; extra == "all"
63
63
  Requires-Dist: pylint; extra == "all"
64
64
  Requires-Dist: pytest; extra == "all"
65
- Requires-Dist: coverage; extra == "all"
66
- Requires-Dist: black; extra == "all"
67
- Requires-Dist: pytest-cov; extra == "all"
68
65
  Requires-Dist: twine; extra == "all"
69
66
  Requires-Dist: sphinx; extra == "all"
67
+ Requires-Dist: black; extra == "all"
68
+ Requires-Dist: pytest-random-order>=1.0.0; extra == "all"
69
+ Requires-Dist: pytest-cov; 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,13 +78,13 @@ 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):
89
88
  """
90
89
  To be overridden by subclass
91
90
  """
@@ -93,7 +92,7 @@ class Backend:
93
92
 
94
93
  # pylint: disable=too-many-locals,too-many-branches,too-many-arguments,too-many-nested-blocks
95
94
  def install_module(
96
- self, device_path, device_modules, name, pyext, mod_names
95
+ self, device_path, device_modules, name, pyext, mod_names, upgrade=False
97
96
  ): # pragma: no cover
98
97
  """
99
98
  Finds a connected device and installs a given module name if it
@@ -108,14 +107,27 @@ class Backend:
108
107
  source or from a pre-compiled module
109
108
  :param mod_names: Dictionary of metadata from modules that can be generated
110
109
  with get_bundle_versions()
110
+ :param bool upgrade: Upgrade the specified modules if they're already installed.
111
111
  """
112
112
  if not name:
113
113
  click.echo("No module name(s) provided.")
114
114
  elif name in mod_names:
115
115
  # Grab device modules to check if module already installed
116
116
  if name in device_modules:
117
- click.echo("'{}' is already installed.".format(name))
118
- return
117
+ if not upgrade:
118
+ # skip already installed modules if no -upgrade flag
119
+ click.echo("'{}' is already installed.".format(name))
120
+ return
121
+
122
+ # uninstall the module before installing
123
+ name = name.lower()
124
+ _mod_names = {}
125
+ for module_item, _metadata in device_modules.items():
126
+ _mod_names[module_item.replace(".py", "").lower()] = _metadata
127
+ if name in _mod_names:
128
+ _metadata = _mod_names[name]
129
+ module_path = _metadata["path"]
130
+ self.uninstall(device_path, module_path)
119
131
 
120
132
  library_path = (
121
133
  os.path.join(device_path, self.LIB_DIR_PATH)
@@ -159,10 +171,10 @@ class Backend:
159
171
 
160
172
  if pyext:
161
173
  # Use Python source for module.
162
- self._install_module_py(metadata)
174
+ self.install_module_py(metadata)
163
175
  else:
164
176
  # Use pre-compiled mpy modules.
165
- self._install_module_mpy(bundle, metadata)
177
+ self.install_module_mpy(bundle, metadata)
166
178
  click.echo("Installed '{}'.".format(name))
167
179
  else:
168
180
  click.echo("Unknown module named, '{}'.".format(name))
@@ -219,6 +231,12 @@ class Backend:
219
231
  board_id = ""
220
232
  return circuit_python, board_id
221
233
 
234
+ def file_exists(self, filepath):
235
+ """
236
+ To be overriden by subclass
237
+ """
238
+ raise NotImplementedError
239
+
222
240
 
223
241
  def _writeable_error():
224
242
  click.secho(
@@ -250,7 +268,8 @@ class WebBackend(Backend):
250
268
  else "Could not find or connect to specified device"
251
269
  ) from exc
252
270
 
253
- self.LIB_DIR_PATH = "fs/lib/"
271
+ self.FS_PATH = "fs/"
272
+ self.LIB_DIR_PATH = f"{self.FS_PATH}lib/"
254
273
  self.host = host
255
274
  self.password = password
256
275
  self.device_location = f"http://:{self.password}@{self.host}"
@@ -260,14 +279,20 @@ class WebBackend(Backend):
260
279
  self.library_path = self.device_location + "/" + self.LIB_DIR_PATH
261
280
  self.timeout = timeout
262
281
 
263
- def install_file_http(self, source):
282
+ def install_file_http(self, source, location=None):
264
283
  """
265
284
  Install file to device using web workflow.
266
285
  :param source source file.
286
+ :param location the location on the device to copy the source
287
+ directory in to. If omitted is CIRCUITPY/lib/ used.
267
288
  """
268
289
  file_name = source.split(os.path.sep)
269
290
  file_name = file_name[-2] if file_name[-1] == "" else file_name[-1]
270
- target = self.device_location + "/" + self.LIB_DIR_PATH + file_name
291
+
292
+ if location is None:
293
+ target = self.device_location + "/" + self.LIB_DIR_PATH + file_name
294
+ else:
295
+ target = self.device_location + "/" + self.FS_PATH + location + file_name
271
296
 
272
297
  auth = HTTPBasicAuth("", self.password)
273
298
 
@@ -277,14 +302,19 @@ class WebBackend(Backend):
277
302
  _writeable_error()
278
303
  r.raise_for_status()
279
304
 
280
- def install_dir_http(self, source):
305
+ def install_dir_http(self, source, location=None):
281
306
  """
282
307
  Install directory to device using web workflow.
283
308
  :param source source directory.
309
+ :param location the location on the device to copy the source
310
+ directory in to. If omitted is CIRCUITPY/lib/ used.
284
311
  """
285
312
  mod_name = source.split(os.path.sep)
286
313
  mod_name = mod_name[-2] if mod_name[-1] == "" else mod_name[-1]
287
- target = self.device_location + "/" + self.LIB_DIR_PATH + mod_name
314
+ if location is None:
315
+ target = self.device_location + "/" + self.LIB_DIR_PATH + mod_name
316
+ else:
317
+ target = self.device_location + "/" + self.FS_PATH + location + mod_name
288
318
  target = target + "/" if target[:-1] != "/" else target
289
319
  url = urlparse(target)
290
320
  auth = HTTPBasicAuth("", url.password)
@@ -490,7 +520,7 @@ class WebBackend(Backend):
490
520
  _writeable_error()
491
521
  r.raise_for_status()
492
522
 
493
- def _install_module_mpy(self, bundle, metadata):
523
+ def install_module_mpy(self, bundle, metadata):
494
524
  """
495
525
  :param bundle library bundle.
496
526
  :param library_path library path
@@ -514,7 +544,7 @@ class WebBackend(Backend):
514
544
  raise IOError("Cannot find compiled version of module.")
515
545
 
516
546
  # pylint: enable=too-many-locals,too-many-branches
517
- def _install_module_py(self, metadata):
547
+ def install_module_py(self, metadata, location=None):
518
548
  """
519
549
  :param library_path library path
520
550
  :param metadata dictionary.
@@ -522,10 +552,10 @@ class WebBackend(Backend):
522
552
 
523
553
  source_path = metadata["path"] # Path to Python source version.
524
554
  if os.path.isdir(source_path):
525
- self.install_dir_http(source_path)
555
+ self.install_dir_http(source_path, location=location)
526
556
 
527
557
  else:
528
- self.install_file_http(source_path)
558
+ self.install_file_http(source_path, location=location)
529
559
 
530
560
  def get_auto_file_path(self, auto_file_path):
531
561
  """
@@ -560,6 +590,18 @@ class WebBackend(Backend):
560
590
  """
561
591
  self._update_http(module)
562
592
 
593
+ def file_exists(self, filepath):
594
+ """
595
+ return True if the file exists, otherwise False.
596
+ """
597
+ auth = HTTPBasicAuth("", self.password)
598
+ resp = requests.get(
599
+ self.get_file_path(filepath), auth=auth, timeout=self.timeout
600
+ )
601
+ if resp.status_code == 200:
602
+ return True
603
+ return False
604
+
563
605
  def _update_http(self, module):
564
606
  """
565
607
  Update the module using web workflow.
@@ -733,10 +775,9 @@ class DiskBackend(Backend):
733
775
  if not os.path.exists(library_path): # pragma: no cover
734
776
  os.makedirs(library_path)
735
777
 
736
- def _install_module_mpy(self, bundle, metadata):
778
+ def install_module_mpy(self, bundle, metadata):
737
779
  """
738
780
  :param bundle library bundle.
739
- :param library_path library path
740
781
  :param metadata dictionary.
741
782
  """
742
783
  module_name = os.path.basename(metadata["path"]).replace(".py", ".mpy")
@@ -763,21 +804,26 @@ class DiskBackend(Backend):
763
804
  raise IOError("Cannot find compiled version of module.")
764
805
 
765
806
  # pylint: enable=too-many-locals,too-many-branches
766
- def _install_module_py(self, metadata):
807
+ def install_module_py(self, metadata, location=None):
767
808
  """
768
- :param library_path library path
769
809
  :param metadata dictionary.
810
+ :param location the location on the device to copy the py module to.
811
+ If omitted is CIRCUITPY/lib/ used.
770
812
  """
813
+ if location is None:
814
+ location = self.library_path
815
+ else:
816
+ location = os.path.join(self.device_location, location)
771
817
 
772
818
  source_path = metadata["path"] # Path to Python source version.
773
819
  if os.path.isdir(source_path):
774
820
  target = os.path.basename(os.path.dirname(source_path))
775
- target_path = os.path.join(self.library_path, target)
821
+ target_path = os.path.join(location, target)
776
822
  # Copy the directory.
777
823
  shutil.copytree(source_path, target_path)
778
824
  else:
779
825
  target = os.path.basename(source_path)
780
- target_path = os.path.join(self.library_path, target)
826
+ target_path = os.path.join(location, target)
781
827
  # Copy file.
782
828
  shutil.copyfile(source_path, target_path)
783
829
 
@@ -825,6 +871,12 @@ class DiskBackend(Backend):
825
871
  os.remove(module.path)
826
872
  shutil.copyfile(module.bundle_path, module.path)
827
873
 
874
+ def file_exists(self, filepath):
875
+ """
876
+ return True if the file exists, otherwise False.
877
+ """
878
+ return os.path.exists(os.path.join(self.device_location, filepath))
879
+
828
880
  def get_file_path(self, filename):
829
881
  """
830
882
  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.
@@ -93,6 +93,23 @@ def completion_for_install(ctx, param, incomplete):
93
93
  return sorted(module_names)
94
94
 
95
95
 
96
+ def completion_for_example(ctx, param, incomplete):
97
+ """
98
+ Returns the list of available modules for the command line tab-completion
99
+ with the ``circup example`` command.
100
+ """
101
+ # pylint: disable=unused-argument, consider-iterating-dictionary
102
+ available_examples = get_bundle_examples(get_bundles_list(), avoid_download=True)
103
+
104
+ matching_examples = [
105
+ example_path
106
+ for example_path in available_examples.keys()
107
+ if example_path.startswith(incomplete)
108
+ ]
109
+
110
+ return sorted(matching_examples)
111
+
112
+
96
113
  def ensure_latest_bundle(bundle):
97
114
  """
98
115
  Ensure that there's a copy of the latest library bundle available so circup
@@ -290,6 +307,43 @@ def get_bundle(bundle, tag):
290
307
  click.echo("\nOK\n")
291
308
 
292
309
 
310
+ def get_bundle_examples(bundles_list, avoid_download=False):
311
+ """
312
+ Return a dictionary of metadata from examples in the all of the bundles
313
+ specified by bundles_list argument.
314
+
315
+ :param List[Bundle] bundles_list: List of supported bundles as Bundle objects.
316
+ :param bool avoid_download: if True, download the bundle only if missing.
317
+ :return: A dictionary of metadata about the examples available in the
318
+ library bundle.
319
+ """
320
+ # pylint: disable=too-many-nested-blocks
321
+ all_the_examples = dict()
322
+
323
+ try:
324
+ for bundle in bundles_list:
325
+ if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
326
+ ensure_latest_bundle(bundle)
327
+ path = bundle.examples_dir("py")
328
+ path_examples = _get_modules_file(path, logger)
329
+ for lib_name, lib_metadata in path_examples.items():
330
+ for _dir_level in os.walk(lib_metadata["path"]):
331
+ for _file in _dir_level[2]:
332
+ _parts = _dir_level[0].split(os.path.sep)
333
+ _lib_name_index = _parts.index(lib_name)
334
+ _dirs = _parts[_lib_name_index:]
335
+ if _dirs[-1] == "":
336
+ _dirs.pop(-1)
337
+ slug = f"{os.path.sep}".join(_dirs + [_file.replace(".py", "")])
338
+ all_the_examples[slug] = os.path.join(_dir_level[0], _file)
339
+
340
+ except NotADirectoryError:
341
+ # Bundle does not have new style examples directory
342
+ # so we cannot include its examples.
343
+ pass
344
+ return all_the_examples
345
+
346
+
293
347
  def get_bundle_versions(bundles_list, avoid_download=False):
294
348
  """
295
349
  Returns a dictionary of metadata from modules in the latest known release
@@ -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,7 +296,9 @@ 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
@@ -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.8.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
@@ -59,14 +59,14 @@ Requires-Dist: twine; extra == "dev"
59
59
  Provides-Extra: all
60
60
  Requires-Dist: wheel; extra == "all"
61
61
  Requires-Dist: pytest-faulthandler; extra == "all"
62
- Requires-Dist: pytest-random-order>=1.0.0; extra == "all"
62
+ Requires-Dist: coverage; extra == "all"
63
63
  Requires-Dist: pylint; extra == "all"
64
64
  Requires-Dist: pytest; extra == "all"
65
- Requires-Dist: coverage; extra == "all"
66
- Requires-Dist: black; extra == "all"
67
- Requires-Dist: pytest-cov; extra == "all"
68
65
  Requires-Dist: twine; extra == "all"
69
66
  Requires-Dist: sphinx; extra == "all"
67
+ Requires-Dist: black; extra == "all"
68
+ Requires-Dist: pytest-random-order>=1.0.0; extra == "all"
69
+ Requires-Dist: pytest-cov; 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...
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
@@ -12,14 +12,14 @@ importlib_metadata
12
12
  [all]
13
13
  wheel
14
14
  pytest-faulthandler
15
- pytest-random-order>=1.0.0
15
+ coverage
16
16
  pylint
17
17
  pytest
18
- coverage
19
- black
20
- pytest-cov
21
18
  twine
22
19
  sphinx
20
+ black
21
+ pytest-random-order>=1.0.0
22
+ pytest-cov
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
File without changes