circup 2.1.1__tar.gz → 2.2.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 (89) hide show
  1. {circup-2.1.1 → circup-2.2.0}/.github/workflows/build.yml +1 -1
  2. {circup-2.1.1/circup.egg-info → circup-2.2.0}/PKG-INFO +3 -3
  3. {circup-2.1.1 → circup-2.2.0}/circup/backends.py +51 -19
  4. {circup-2.1.1 → circup-2.2.0}/circup/command_utils.py +165 -11
  5. {circup-2.1.1 → circup-2.2.0}/circup/commands.py +5 -25
  6. {circup-2.1.1 → circup-2.2.0}/circup/shared.py +1 -1
  7. {circup-2.1.1 → circup-2.2.0/circup.egg-info}/PKG-INFO +3 -3
  8. {circup-2.1.1 → circup-2.2.0}/circup.egg-info/SOURCES.txt +8 -1
  9. {circup-2.1.1 → circup-2.2.0}/circup.egg-info/requires.txt +0 -1
  10. {circup-2.1.1 → circup-2.2.0}/requirements.txt +0 -1
  11. {circup-2.1.1 → circup-2.2.0}/tests/import_styles.py +3 -0
  12. circup-2.2.0/tests/mock_device/boot_out.txt +3 -0
  13. circup-2.2.0/tests/mock_device/import_styles_sub.py +5 -0
  14. circup-2.2.0/tests/mock_device_2/.gitignore +4 -0
  15. circup-2.2.0/tests/mock_device_2/boot_out.txt.license +3 -0
  16. circup-2.2.0/tests/mock_device_2/code.py +7 -0
  17. circup-2.2.0/tests/mock_device_2/package/__init__.py +6 -0
  18. circup-2.2.0/tests/mock_device_2/package/other.py +5 -0
  19. {circup-2.1.1 → circup-2.2.0}/tests/test_circup.py +163 -12
  20. {circup-2.1.1 → circup-2.2.0}/.github/ISSUE_TEMPLATE.md +0 -0
  21. {circup-2.1.1 → circup-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  22. {circup-2.1.1 → circup-2.2.0}/.github/workflows/release.yml +0 -0
  23. {circup-2.1.1 → circup-2.2.0}/.gitignore +0 -0
  24. {circup-2.1.1 → circup-2.2.0}/.isort.cfg +0 -0
  25. {circup-2.1.1 → circup-2.2.0}/.pre-commit-config.yaml +0 -0
  26. {circup-2.1.1 → circup-2.2.0}/.pylintrc +0 -0
  27. {circup-2.1.1 → circup-2.2.0}/CODE_OF_CONDUCT.rst +0 -0
  28. {circup-2.1.1 → circup-2.2.0}/CODE_OF_CONDUCT.rst.license +0 -0
  29. {circup-2.1.1 → circup-2.2.0}/CONTRIBUTING.rst +0 -0
  30. {circup-2.1.1 → circup-2.2.0}/CONTRIBUTING.rst.license +0 -0
  31. {circup-2.1.1 → circup-2.2.0}/LICENSE +0 -0
  32. {circup-2.1.1 → circup-2.2.0}/LICENSES/CC-BY-4.0.txt +0 -0
  33. {circup-2.1.1 → circup-2.2.0}/LICENSES/MIT.txt +0 -0
  34. {circup-2.1.1 → circup-2.2.0}/LICENSES/Unlicense.txt +0 -0
  35. {circup-2.1.1 → circup-2.2.0}/README.rst +0 -0
  36. {circup-2.1.1 → circup-2.2.0}/README.rst.license +0 -0
  37. {circup-2.1.1 → circup-2.2.0}/circup/__init__.py +0 -0
  38. {circup-2.1.1 → circup-2.2.0}/circup/bundle.py +0 -0
  39. {circup-2.1.1 → circup-2.2.0}/circup/config/bundle_config.json +0 -0
  40. {circup-2.1.1 → circup-2.2.0}/circup/config/bundle_config.json.license +0 -0
  41. {circup-2.1.1 → circup-2.2.0}/circup/logging.py +0 -0
  42. {circup-2.1.1 → circup-2.2.0}/circup/module.py +0 -0
  43. {circup-2.1.1 → circup-2.2.0}/circup/wwshell/README.rst +0 -0
  44. {circup-2.1.1 → circup-2.2.0}/circup/wwshell/README.rst.license +0 -0
  45. {circup-2.1.1 → circup-2.2.0}/circup/wwshell/__init__.py +0 -0
  46. {circup-2.1.1 → circup-2.2.0}/circup/wwshell/commands.py +0 -0
  47. {circup-2.1.1 → circup-2.2.0}/circup.egg-info/dependency_links.txt +0 -0
  48. {circup-2.1.1 → circup-2.2.0}/circup.egg-info/entry_points.txt +0 -0
  49. {circup-2.1.1 → circup-2.2.0}/circup.egg-info/top_level.txt +0 -0
  50. {circup-2.1.1 → circup-2.2.0}/docs/_static/favicon.ico +0 -0
  51. {circup-2.1.1 → circup-2.2.0}/docs/_static/favicon.ico.license +0 -0
  52. {circup-2.1.1 → circup-2.2.0}/docs/conf.py +0 -0
  53. {circup-2.1.1 → circup-2.2.0}/docs/index.rst +0 -0
  54. {circup-2.1.1 → circup-2.2.0}/docs/index.rst.license +0 -0
  55. {circup-2.1.1 → circup-2.2.0}/docs/logo.png +0 -0
  56. {circup-2.1.1 → circup-2.2.0}/docs/logo.png.license +0 -0
  57. {circup-2.1.1 → circup-2.2.0}/optional_requirements.txt +0 -0
  58. {circup-2.1.1 → circup-2.2.0}/optional_requirements.txt.license +0 -0
  59. {circup-2.1.1 → circup-2.2.0}/pyproject.toml +0 -0
  60. {circup-2.1.1 → circup-2.2.0}/readthedocs.yml +0 -0
  61. {circup-2.1.1 → circup-2.2.0}/requirements.txt.license +0 -0
  62. {circup-2.1.1 → circup-2.2.0}/setup.cfg +0 -0
  63. {circup-2.1.1 → circup-2.2.0}/tests/__init__.py +0 -0
  64. {circup-2.1.1 → circup-2.2.0}/tests/bad_module/__init__.py +0 -0
  65. {circup-2.1.1 → circup-2.2.0}/tests/bad_module/my_module.py +0 -0
  66. {circup-2.1.1 → circup-2.2.0}/tests/bad_python.py +0 -0
  67. {circup-2.1.1 → circup-2.2.0}/tests/bundle.json +0 -0
  68. {circup-2.1.1 → circup-2.2.0}/tests/bundle.json.license +0 -0
  69. {circup-2.1.1 → circup-2.2.0}/tests/device.json +0 -0
  70. {circup-2.1.1 → circup-2.2.0}/tests/device.json.license +0 -0
  71. {circup-2.1.1 → circup-2.2.0}/tests/dir_module/__init__.py +0 -0
  72. {circup-2.1.1 → circup-2.2.0}/tests/dir_module/my_module.py +0 -0
  73. {circup-2.1.1 → circup-2.2.0}/tests/local_module.py +0 -0
  74. {circup-2.1.1 → circup-2.2.0}/tests/local_module_cp7.mpy +0 -0
  75. {circup-2.1.1 → circup-2.2.0}/tests/local_module_cp7.mpy.license +0 -0
  76. {circup-2.1.1 → circup-2.2.0}/tests/mock_device/boot_out.txt.license +0 -0
  77. {circup-2.1.1 → circup-2.2.0}/tests/mock_device/lib/adafruit_waveform/.gitkeep +0 -0
  78. {circup-2.1.1/tests/mock_device → circup-2.2.0/tests/mock_device_2}/boot_out.txt +0 -0
  79. {circup-2.1.1 → circup-2.2.0}/tests/mount_exists.txt +0 -0
  80. {circup-2.1.1 → circup-2.2.0}/tests/mount_exists.txt.license +0 -0
  81. {circup-2.1.1 → circup-2.2.0}/tests/mount_missing.txt +0 -0
  82. {circup-2.1.1 → circup-2.2.0}/tests/mount_missing.txt.license +0 -0
  83. {circup-2.1.1 → circup-2.2.0}/tests/remote_module.py +0 -0
  84. {circup-2.1.1 → circup-2.2.0}/tests/test_bundle_config.json +0 -0
  85. {circup-2.1.1 → circup-2.2.0}/tests/test_bundle_config.json.license +0 -0
  86. {circup-2.1.1 → circup-2.2.0}/tests/test_bundle_config_local.json +0 -0
  87. {circup-2.1.1 → circup-2.2.0}/tests/test_bundle_config_local.json.license +0 -0
  88. {circup-2.1.1 → circup-2.2.0}/tests/test_module.mpy +0 -0
  89. {circup-2.1.1 → circup-2.2.0}/tests/test_module.mpy.license +0 -0
@@ -16,7 +16,7 @@ jobs:
16
16
  run: echo "$GITHUB_CONTEXT"
17
17
  - name: Translate Repo Name For Build Tools filename_prefix
18
18
  id: repo-name
19
- run: echo ::set-output name=repo-name::circup
19
+ run: echo "repo-name=circup" >> $GITHUB_OUTPUT
20
20
  - name: Set up Python 3.11
21
21
  uses: actions/setup-python@v1
22
22
  with:
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: circup
3
- Version: 2.1.1
3
+ Version: 2.2.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
@@ -47,7 +47,6 @@ Description-Content-Type: text/x-rst
47
47
  License-File: LICENSE
48
48
  Requires-Dist: appdirs
49
49
  Requires-Dist: Click
50
- Requires-Dist: findimports
51
50
  Requires-Dist: requests
52
51
  Requires-Dist: semver
53
52
  Requires-Dist: toml
@@ -57,6 +56,7 @@ Requires-Dist: pytest; extra == "optional"
57
56
  Requires-Dist: pytest-cov; extra == "optional"
58
57
  Requires-Dist: pytest-faulthandler; extra == "optional"
59
58
  Requires-Dist: pytest-random-order; extra == "optional"
59
+ Dynamic: license-file
60
60
 
61
61
 
62
62
  Circup
@@ -233,6 +233,12 @@ class Backend:
233
233
  """
234
234
  raise NotImplementedError
235
235
 
236
+ def get_file_content(self, target_file):
237
+ """
238
+ To be overridden by subclass
239
+ """
240
+ raise NotImplementedError
241
+
236
242
  def get_free_space(self):
237
243
  """
238
244
  To be overridden by subclass
@@ -618,6 +624,20 @@ class WebBackend(Backend):
618
624
  f"Downloaded File: {os.path.join(location_to_paste, file_name)}"
619
625
  )
620
626
 
627
+ def get_file_content(self, target_file):
628
+ """
629
+ Get the content of a file from the MCU drive
630
+ :param target_file: The file on the MCU to download
631
+ :return:
632
+ """
633
+ auth = HTTPBasicAuth("", self.password)
634
+ with self.session.get(
635
+ self.FS_URL + target_file, timeout=self.timeout, auth=auth
636
+ ) as r:
637
+ if r.status_code == 404:
638
+ return None
639
+ return r.content # .decode("utf8")
640
+
621
641
  def install_module_mpy(self, bundle, metadata):
622
642
  """
623
643
  :param bundle library bundle.
@@ -655,19 +675,6 @@ class WebBackend(Backend):
655
675
  else:
656
676
  self.install_file_http(source_path, location=location)
657
677
 
658
- def get_auto_file_path(self, auto_file_path):
659
- """
660
- Make a local temp copy of the --auto file from the device.
661
- Returns the path to the local copy.
662
- """
663
- url = auto_file_path
664
- auth = HTTPBasicAuth("", self.password)
665
- with self.session.get(url, auth=auth, timeout=self.timeout) as r:
666
- r.raise_for_status()
667
- with open(LOCAL_CODE_PY_COPY, "w", encoding="utf-8") as f:
668
- f.write(r.text)
669
- return LOCAL_CODE_PY_COPY
670
-
671
678
  def uninstall(self, device_path, module_path):
672
679
  """
673
680
  Uninstall given module on device using REST API.
@@ -958,12 +965,6 @@ class DiskBackend(Backend):
958
965
  # Copy file.
959
966
  shutil.copyfile(source_path, target_path)
960
967
 
961
- def get_auto_file_path(self, auto_file_path):
962
- """
963
- Returns the path on the device to the file to be read for --auto.
964
- """
965
- return auto_file_path
966
-
967
968
  def uninstall(self, device_path, module_path):
968
969
  """
969
970
  Uninstall module using local file system.
@@ -1014,6 +1015,18 @@ class DiskBackend(Backend):
1014
1015
  """
1015
1016
  return os.path.join(self.device_location, filename)
1016
1017
 
1018
+ def get_file_content(self, target_file):
1019
+ """
1020
+ Get the content of a file from the MCU drive
1021
+ :param target_file: The file on the MCU to download
1022
+ :return:
1023
+ """
1024
+ file_path = self.get_file_path(target_file)
1025
+ if os.path.exists(file_path):
1026
+ with open(file_path, "rb") as file:
1027
+ return file.read()
1028
+ return None
1029
+
1017
1030
  def is_device_present(self):
1018
1031
  """
1019
1032
  returns True if the device is currently connected
@@ -1027,3 +1040,22 @@ class DiskBackend(Backend):
1027
1040
  # pylint: disable=unused-variable
1028
1041
  _, total, free = shutil.disk_usage(self.device_location)
1029
1042
  return free
1043
+
1044
+ def list_dir(self, dirpath):
1045
+ """
1046
+ Returns the list of files located in the given dirpath.
1047
+ """
1048
+ files_list = []
1049
+ files = os.listdir(os.path.join(self.device_location, dirpath))
1050
+ for file_name in files:
1051
+ file = os.path.join(self.device_location, dirpath, file_name)
1052
+ stat = os.stat(file)
1053
+ files_list.append(
1054
+ {
1055
+ "name": file_name,
1056
+ "directory": os.path.isdir(file),
1057
+ "modified_ns": stat.st_mtime_ns,
1058
+ "file_size": stat.st_size,
1059
+ }
1060
+ )
1061
+ return files_list
@@ -5,6 +5,7 @@
5
5
  Functions called from commands in order to provide behaviors and return information.
6
6
  """
7
7
 
8
+ import ast
8
9
  import ctypes
9
10
  import glob
10
11
  import os
@@ -16,7 +17,6 @@ import zipfile
16
17
  import json
17
18
  import re
18
19
  import toml
19
- import findimports
20
20
  import requests
21
21
  import click
22
22
 
@@ -41,6 +41,25 @@ WARNING_IGNORE_MODULES = (
41
41
  "circuitpython-typing",
42
42
  )
43
43
 
44
+ CODE_FILES = [
45
+ "code.txt",
46
+ "code.py",
47
+ "main.py",
48
+ "main.txt",
49
+ "code.txt.py",
50
+ "code.py.txt",
51
+ "code.txt.txt",
52
+ "code.py.py",
53
+ "main.txt.py",
54
+ "main.py.txt",
55
+ "main.txt.txt",
56
+ "main.py.py",
57
+ ]
58
+
59
+
60
+ class CodeParsingException(Exception):
61
+ """Exception thrown when parsing code with ast fails"""
62
+
44
63
 
45
64
  def clean_library_name(assumed_library_name):
46
65
  """
@@ -605,23 +624,158 @@ def tags_data_save_tag(key, tag):
605
624
  json.dump(tags_data, data)
606
625
 
607
626
 
608
- def libraries_from_code_py(code_py, mod_names):
627
+ def imports_from_code(full_content):
609
628
  """
610
629
  Parse the given code.py file and return the imported libraries
630
+ Note that it's impossible at that level to differentiate between
631
+ import module.property and import module.submodule, so we try both
611
632
 
612
- :param str code_py: Full path of the code.py file
633
+ :param str full_content: Code to read imports from
634
+ :param str module_name: Name of the module the code is from
613
635
  :return: sequence of library names
614
636
  """
615
- # pylint: disable=broad-except
637
+ # pylint: disable=too-many-branches
616
638
  try:
617
- found_imports = findimports.find_imports(code_py)
618
- except Exception as ex: # broad exception because anything could go wrong
619
- logger.exception(ex)
620
- click.secho('Unable to read the auto file: "{}"'.format(str(ex)), fg="red")
639
+ par = ast.parse(full_content)
640
+ except (SyntaxError, ValueError) as err:
641
+ raise CodeParsingException(err) from err
642
+
643
+ imports = set()
644
+ for thing in ast.walk(par):
645
+ # import module and import module.submodule
646
+ if isinstance(thing, ast.Import):
647
+ for alias in thing.names:
648
+ imports.add(alias.name)
649
+ # from x import y
650
+ if isinstance(thing, ast.ImportFrom):
651
+ if thing.module:
652
+ # from [.][.]module import names
653
+ module = ("." * thing.level) + thing.module
654
+ imports.add(module)
655
+ for alias in thing.names:
656
+ imports.add(".".join([module, alias.name]))
657
+ else:
658
+ # from . import names
659
+ for alias in thing.names:
660
+ imports.add(alias.name)
661
+
662
+ # import parent modules (in practice it's the __init__.py)
663
+ for name in list(imports):
664
+ if "*" in name:
665
+ imports.remove(name)
666
+ continue
667
+ names = name.split(".")
668
+ for i in range(len(names)):
669
+ module = ".".join(names[: i + 1])
670
+ if module:
671
+ imports.add(module)
672
+
673
+ return sorted(imports)
674
+
675
+
676
+ def get_all_imports(
677
+ backend, auto_file_content, mod_names, current_module, visited=None
678
+ ):
679
+ """
680
+ Recursively retrieve imports from files on the backend
681
+
682
+ :param Backend backend: The current backend object
683
+ :param str auto_file_content: Content of the python file to analyse
684
+ :param list mod_names: Lits of supported bundle mod names
685
+ :param str current_module: Name of the call context module if recursive call
686
+ :param set visited: Modules previously visited
687
+ :return: sequence of library names
688
+ """
689
+ if visited is None:
690
+ visited = set()
691
+ visited.add(current_module)
692
+
693
+ requested_installs = []
694
+ try:
695
+ imports = imports_from_code(auto_file_content)
696
+ except CodeParsingException as err:
697
+ click.secho(f"Error parsing {current_module}:\n {err}", fg="red")
621
698
  sys.exit(2)
622
- # pylint: enable=broad-except
623
- imports = [info.name.split(".", 1)[0] for info in found_imports]
624
- return [r for r in imports if r in mod_names]
699
+
700
+ for install in imports:
701
+ if install in visited:
702
+ continue
703
+ if install in mod_names:
704
+ requested_installs.append(install)
705
+ else:
706
+ # relative module paths
707
+ if install.startswith(".."):
708
+ install_module = ".".join(current_module.split(".")[:-2])
709
+ install_module = install_module + "." + install[2:]
710
+ elif install.startswith("."):
711
+ install_module = ".".join(current_module.split(".")[:-1])
712
+ install_module = install_module + "." + install[1:]
713
+ else:
714
+ install_module = install
715
+ # possible files for the module: .py or __init__.py (if directory)
716
+ file_name = os.path.join(*install_module.split(".")) + ".py"
717
+ exists = backend.file_exists(file_name)
718
+ if not exists:
719
+ file_name = os.path.join(*install_module.split("."), "__init__.py")
720
+ exists = backend.file_exists(file_name)
721
+ if not exists:
722
+ continue
723
+ install_module += ".__init__"
724
+ # get the content and parse it recursively
725
+ auto_file_content = backend.get_file_content(file_name)
726
+ if auto_file_content:
727
+ sub_imports = get_all_imports(
728
+ backend, auto_file_content, mod_names, install_module, visited
729
+ )
730
+ requested_installs.extend(sub_imports)
731
+
732
+ return sorted(requested_installs)
733
+ # [r for r in requested_installs if r in mod_names]
734
+
735
+
736
+ def libraries_from_auto_file(backend, auto_file, mod_names):
737
+ """
738
+ Parse the input auto_file path and/or use the workflow to find the most
739
+ appropriate code.py script. Then return the list of imports
740
+
741
+ :param Backend backend: The current backend object
742
+ :param str auto_file: Path of the candidate auto file or None
743
+ :return: sequence of library names
744
+ """
745
+ # find the current main file based on Circuitpython's rules
746
+ if auto_file is None:
747
+ root_files = [
748
+ file["name"] for file in backend.list_dir("") if not file["directory"]
749
+ ]
750
+ for main_file in CODE_FILES:
751
+ if main_file in root_files:
752
+ auto_file = main_file
753
+ break
754
+ # still no code file found
755
+ if auto_file is None:
756
+ click.secho(
757
+ "No default code file found. See valid names:\n"
758
+ "https://docs.circuitpython.org/en/latest/README.html#behavior",
759
+ fg="red",
760
+ )
761
+ sys.exit(1)
762
+
763
+ # pass a local file with "./" or "../"
764
+ is_relative = auto_file.split(os.sep)[0] in [os.path.curdir, os.path.pardir]
765
+ if os.path.isabs(auto_file) or is_relative:
766
+ with open(auto_file, "r", encoding="UTF8") as fp:
767
+ auto_file_content = fp.read()
768
+ else:
769
+ auto_file_content = backend.get_file_content(auto_file)
770
+
771
+ if auto_file_content is None:
772
+ click.secho(f"Auto file not found: {auto_file}", fg="red")
773
+ sys.exit(1)
774
+
775
+ # from file name to module name (in case it's in a subpackage)
776
+ click.secho(f"Finding imports from: {auto_file}", fg="green")
777
+ current_module = auto_file.rstrip(".py").replace(os.path.sep, ".")
778
+ return get_all_imports(backend, auto_file_content, mod_names, current_module)
625
779
 
626
780
 
627
781
  def get_device_path(host, port, password, path):
@@ -34,7 +34,7 @@ from circup.command_utils import (
34
34
  completion_for_install,
35
35
  get_bundle_versions,
36
36
  libraries_from_requirements,
37
- libraries_from_code_py,
37
+ libraries_from_auto_file,
38
38
  get_dependencies,
39
39
  get_bundles_local_dict,
40
40
  save_local_bundles,
@@ -342,32 +342,12 @@ def install(
342
342
  requirements_txt = rfile.read()
343
343
  requested_installs = libraries_from_requirements(requirements_txt)
344
344
  elif auto or auto_file:
345
- if auto_file is None:
346
- auto_file = "code.py"
347
- print(f"Auto file: {auto_file}")
348
- # pass a local file with "./" or "../"
349
- is_relative = not isinstance(ctx.obj["backend"], WebBackend) or auto_file.split(
350
- os.sep
351
- )[0] in [os.path.curdir, os.path.pardir]
352
- if not os.path.isabs(auto_file) and not is_relative:
353
- auto_file = ctx.obj["backend"].get_file_path(auto_file or "code.py")
354
-
355
- auto_file_path = ctx.obj["backend"].get_auto_file_path(auto_file)
356
- print(f"Auto file path: {auto_file_path}")
357
- if not os.path.isfile(auto_file_path):
358
- # fell through to here when run from random folder on windows - ask backend.
359
- new_auto_file = ctx.obj["backend"].get_file_path(auto_file)
360
- if os.path.isfile(new_auto_file):
361
- auto_file = new_auto_file
362
- auto_file_path = ctx.obj["backend"].get_auto_file_path(auto_file)
363
- print(f"Auto file path: {auto_file_path}")
364
- else:
365
- click.secho(f"Auto file not found: {auto_file}", fg="red")
366
- sys.exit(1)
367
-
368
- requested_installs = libraries_from_code_py(auto_file_path, mod_names)
345
+ requested_installs = libraries_from_auto_file(
346
+ ctx.obj["backend"], auto_file, mod_names
347
+ )
369
348
  else:
370
349
  requested_installs = modules
350
+
371
351
  requested_installs = sorted(set(requested_installs))
372
352
  click.echo(f"Searching for dependencies for: {requested_installs}")
373
353
  to_install = get_dependencies(requested_installs, mod_names=mod_names)
@@ -21,7 +21,7 @@ BAD_FILE_FORMAT = "Invalid"
21
21
  DATA_DIR = appdirs.user_data_dir(appname="circup", appauthor="adafruit")
22
22
 
23
23
  #: Module formats list (and the other form used in github files)
24
- PLATFORMS = {"py": "py", "8mpy": "8.x-mpy", "9mpy": "9.x-mpy"}
24
+ PLATFORMS = {"py": "py", "9mpy": "9.x-mpy"}
25
25
 
26
26
  #: Timeout for requests calls like get()
27
27
  REQUESTS_TIMEOUT = 30
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: circup
3
- Version: 2.1.1
3
+ Version: 2.2.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
@@ -47,7 +47,6 @@ Description-Content-Type: text/x-rst
47
47
  License-File: LICENSE
48
48
  Requires-Dist: appdirs
49
49
  Requires-Dist: Click
50
- Requires-Dist: findimports
51
50
  Requires-Dist: requests
52
51
  Requires-Dist: semver
53
52
  Requires-Dist: toml
@@ -57,6 +56,7 @@ Requires-Dist: pytest; extra == "optional"
57
56
  Requires-Dist: pytest-cov; extra == "optional"
58
57
  Requires-Dist: pytest-faulthandler; extra == "optional"
59
58
  Requires-Dist: pytest-random-order; extra == "optional"
59
+ Dynamic: license-file
60
60
 
61
61
 
62
62
  Circup
@@ -77,4 +77,11 @@ tests/dir_module/__init__.py
77
77
  tests/dir_module/my_module.py
78
78
  tests/mock_device/boot_out.txt
79
79
  tests/mock_device/boot_out.txt.license
80
- tests/mock_device/lib/adafruit_waveform/.gitkeep
80
+ tests/mock_device/import_styles_sub.py
81
+ tests/mock_device/lib/adafruit_waveform/.gitkeep
82
+ tests/mock_device_2/.gitignore
83
+ tests/mock_device_2/boot_out.txt
84
+ tests/mock_device_2/boot_out.txt.license
85
+ tests/mock_device_2/code.py
86
+ tests/mock_device_2/package/__init__.py
87
+ tests/mock_device_2/package/other.py
@@ -1,6 +1,5 @@
1
1
  appdirs
2
2
  Click
3
- findimports
4
3
  requests
5
4
  semver
6
5
  toml
@@ -1,6 +1,5 @@
1
1
  appdirs
2
2
  Click
3
- findimports
4
3
  requests
5
4
  semver
6
5
  toml
@@ -2,7 +2,10 @@
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
4
  # pylint: disable=all
5
+ import os, sys
5
6
  import adafruit_bus_device
6
7
  from adafruit_button import Button
7
8
  from adafruit_esp32spi import adafruit_esp32spi_socketpool
9
+ from adafruit_display_text import wrap_text_to_pixels, wrap_text_to_lines
8
10
  import adafruit_hid.consumer_control
11
+ import import_styles_sub
@@ -0,0 +1,3 @@
1
+ Adafruit CircuitPython 9.0.0 on 2019-08-02; Adafruit CircuitPlayground Express with samd21g18
2
+ Board ID:this_is_a_board
3
+ UID:AAAABBBBCCCC
@@ -0,0 +1,5 @@
1
+ # SPDX-FileCopyrightText: 2025 Neradoc
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ # pylint: disable=all
5
+ import adafruit_ntp
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2025 Neradoc
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ lib/*
@@ -0,0 +1,3 @@
1
+ # SPDX-FileCopyrightText: 2023 Tim Cocks, written for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT
@@ -0,0 +1,7 @@
1
+ # SPDX-FileCopyrightText: 2021 Jeff Epler for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ # pylint: disable=all
5
+ import adafruit_ssd1675
6
+ import import_styles_sub
7
+ import package
@@ -0,0 +1,6 @@
1
+ # SPDX-FileCopyrightText: 2025 Neradoc
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ # pylint: disable=all
5
+ import adafruit_spd1656
6
+ from .other import variable
@@ -0,0 +1,5 @@
1
+ # SPDX-FileCopyrightText: 2021 Jeff Epler for Adafruit Industries
2
+ #
3
+ # SPDX-License-Identifier: MIT
4
+ # pylint: disable=all
5
+ import adafruit_spd1608
@@ -42,6 +42,9 @@ from circup.command_utils import (
42
42
  ensure_latest_bundle,
43
43
  get_bundle,
44
44
  get_bundles_dict,
45
+ imports_from_code,
46
+ get_all_imports,
47
+ libraries_from_auto_file,
45
48
  )
46
49
  from circup.shared import PLATFORMS
47
50
  from circup.module import Module
@@ -94,10 +97,10 @@ def test_Bundle_lib_dir():
94
97
  "adafruit/adafruit-circuitpython-bundle-py/"
95
98
  "adafruit-circuitpython-bundle-py-TESTTAG/lib"
96
99
  )
97
- assert bundle.lib_dir("8mpy") == (
100
+ assert bundle.lib_dir("9mpy") == (
98
101
  circup.shared.DATA_DIR + "/"
99
- "adafruit/adafruit-circuitpython-bundle-8mpy/"
100
- "adafruit-circuitpython-bundle-8.x-mpy-TESTTAG/lib"
102
+ "adafruit/adafruit-circuitpython-bundle-9mpy/"
103
+ "adafruit-circuitpython-bundle-9.x-mpy-TESTTAG/lib"
101
104
  )
102
105
 
103
106
 
@@ -119,7 +122,7 @@ def test_get_bundles_dict():
119
122
  """
120
123
  with mock.patch(
121
124
  "circup.command_utils.BUNDLE_CONFIG_FILE", TEST_BUNDLE_CONFIG_JSON
122
- ), mock.patch("circup.shared.BUNDLE_CONFIG_LOCAL", ""):
125
+ ), mock.patch("circup.command_utils.BUNDLE_CONFIG_LOCAL", ""):
123
126
  bundles_dict = get_bundles_dict()
124
127
  assert bundles_dict == TEST_BUNDLE_DATA
125
128
 
@@ -378,6 +381,16 @@ def test_Module_mpy_mismatch():
378
381
  assert m2.outofdate is False
379
382
  assert m3.mpy_mismatch is True
380
383
  assert m3.outofdate is True
384
+ with mock.patch(
385
+ "circup.backends.DiskBackend.get_circuitpython_version",
386
+ return_value=("9.0.0", ""),
387
+ ):
388
+ assert m1.mpy_mismatch is False
389
+ assert m1.outofdate is False
390
+ assert m2.mpy_mismatch is True
391
+ assert m2.outofdate is True
392
+ assert m3.mpy_mismatch is True
393
+ assert m3.outofdate is True
381
394
 
382
395
 
383
396
  def test_Module_major_update_bad_versions():
@@ -419,14 +432,16 @@ def test_Module_row():
419
432
  repo = "https://github.com/adafruit/SomeLibrary.git"
420
433
  with mock.patch("circup.os.path.isfile", return_value=True), mock.patch(
421
434
  "circup.backends.DiskBackend.get_circuitpython_version",
422
- return_value=("8.0.0", ""),
435
+ return_value=("9.0.0", ""),
423
436
  ), mock.patch("circup.logger.warning") as mock_logger:
424
437
  backend = DiskBackend("mock_device", mock_logger)
425
438
  m = Module(name, backend, repo, "1.2.3", None, False, bundle, (None, None))
426
439
  assert m.row == ("module", "1.2.3", "unknown", "Major Version")
427
440
  m = Module(name, backend, repo, "1.2.3", "1.3.4", False, bundle, (None, None))
428
441
  assert m.row == ("module", "1.2.3", "1.3.4", "Minor Version")
429
- m = Module(name, backend, repo, "1.2.3", "1.2.3", True, bundle, ("9.0.0", None))
442
+ m = Module(
443
+ name, backend, repo, "1.2.3", "1.2.3", True, bundle, ("8.0.0", "9.0.0")
444
+ )
430
445
  assert m.row == ("module", "1.2.3", "1.2.3", "MPY Format")
431
446
 
432
447
 
@@ -806,7 +821,7 @@ def test_get_circuitpython_version():
806
821
  with mock.patch("circup.logger.warning") as mock_logger:
807
822
  backend = DiskBackend("tests/mock_device", mock_logger)
808
823
  assert backend.get_circuitpython_version() == (
809
- "8.1.0",
824
+ "9.0.0",
810
825
  "this_is_a_board",
811
826
  )
812
827
 
@@ -1117,8 +1132,33 @@ def test_show_match_py_command():
1117
1132
  assert "0 shown" in result.output
1118
1133
 
1119
1134
 
1120
- def test_libraries_from_imports():
1135
+ def test_imports_from_code():
1121
1136
  """Ensure that various styles of import all work"""
1137
+ test_file = str(pathlib.Path(__file__).parent / "import_styles.py")
1138
+ with open(test_file, "r", encoding="utf8") as fp:
1139
+ test_data = fp.read()
1140
+
1141
+ result = imports_from_code(test_data)
1142
+ print(result)
1143
+ assert result == [
1144
+ "adafruit_bus_device",
1145
+ "adafruit_button",
1146
+ "adafruit_button.Button",
1147
+ "adafruit_display_text",
1148
+ "adafruit_display_text.wrap_text_to_lines",
1149
+ "adafruit_display_text.wrap_text_to_pixels",
1150
+ "adafruit_esp32spi",
1151
+ "adafruit_esp32spi.adafruit_esp32spi_socketpool",
1152
+ "adafruit_hid",
1153
+ "adafruit_hid.consumer_control",
1154
+ "import_styles_sub",
1155
+ "os",
1156
+ "sys",
1157
+ ]
1158
+
1159
+
1160
+ def test_get_all_imports():
1161
+ """List all libraries from auto file recursively"""
1122
1162
  mod_names = [
1123
1163
  "adafruit_bus_device",
1124
1164
  "adafruit_button",
@@ -1129,20 +1169,131 @@ def test_libraries_from_imports():
1129
1169
  "adafruit_oauth2",
1130
1170
  "adafruit_requests",
1131
1171
  "adafruit_touchscreen",
1172
+ "adafruit_ntp",
1132
1173
  ]
1133
- test_file = str(pathlib.Path(__file__).parent / "import_styles.py")
1134
1174
 
1135
- result = circup.libraries_from_code_py(test_file, mod_names)
1175
+ with mock.patch("circup.logger.info") as mock_logger, mock.patch(
1176
+ "circup.os.path.isfile", return_value=True
1177
+ ), mock.patch(
1178
+ "circup.bundle.Bundle.lib_dir",
1179
+ return_value="tests",
1180
+ ):
1181
+ tests_dir = pathlib.Path(__file__).parent
1182
+ backend = DiskBackend(tests_dir / "mock_device", mock_logger)
1183
+
1184
+ test_file = str(tests_dir / "import_styles.py")
1185
+ with open(test_file, "r", encoding="utf8") as fp:
1186
+ test_data = fp.read()
1187
+
1188
+ result = get_all_imports(backend, test_data, mod_names, current_module="")
1189
+
1190
+ assert result == [
1191
+ "adafruit_bus_device",
1192
+ "adafruit_button",
1193
+ "adafruit_display_text",
1194
+ "adafruit_esp32spi",
1195
+ "adafruit_hid",
1196
+ "adafruit_ntp",
1197
+ ]
1198
+
1199
+
1200
+ def test_libraries_from_auto_file_local():
1201
+ """Check that we get all libraries from auto file argument.
1202
+ Testing here with a local file"""
1203
+ mod_names = [
1204
+ "adafruit_bus_device",
1205
+ "adafruit_button",
1206
+ "adafruit_display_shapes",
1207
+ "adafruit_display_text",
1208
+ "adafruit_esp32spi",
1209
+ "adafruit_hid",
1210
+ "adafruit_oauth2",
1211
+ "adafruit_requests",
1212
+ "adafruit_touchscreen",
1213
+ "adafruit_ntp",
1214
+ ]
1215
+
1216
+ auto_file = "./tests/import_styles.py"
1217
+
1218
+ with mock.patch("circup.logger.info") as mock_logger, mock.patch(
1219
+ "circup.os.path.isfile", return_value=True
1220
+ ), mock.patch(
1221
+ "circup.bundle.Bundle.lib_dir",
1222
+ return_value="tests",
1223
+ ):
1224
+ tests_dir = pathlib.Path(__file__).parent
1225
+ backend = DiskBackend(tests_dir / "mock_device", mock_logger)
1226
+
1227
+ result = libraries_from_auto_file(backend, auto_file, mod_names)
1228
+
1136
1229
  assert result == [
1137
1230
  "adafruit_bus_device",
1138
1231
  "adafruit_button",
1232
+ "adafruit_display_text",
1139
1233
  "adafruit_esp32spi",
1140
1234
  "adafruit_hid",
1235
+ "adafruit_ntp",
1236
+ ]
1237
+
1238
+
1239
+ def test_libraries_from_auto_file_board():
1240
+ """Check that we find code.py on the board if we give no auto_file argument"""
1241
+ mod_names = [
1242
+ "adafruit_bus_device",
1243
+ "adafruit_button",
1244
+ "adafruit_display_shapes",
1245
+ "adafruit_display_text",
1246
+ "adafruit_esp32spi",
1247
+ "adafruit_ssd1675",
1248
+ "adafruit_spd1656",
1249
+ "adafruit_spd1608",
1250
+ "adafruit_touchscreen",
1251
+ "adafruit_ntp",
1252
+ ]
1253
+
1254
+ auto_file = None
1255
+
1256
+ with mock.patch("circup.logger.info") as mock_logger, mock.patch(
1257
+ "circup.os.path.isfile", return_value=True
1258
+ ), mock.patch(
1259
+ "circup.bundle.Bundle.lib_dir",
1260
+ return_value="tests",
1261
+ ):
1262
+ tests_dir = pathlib.Path(__file__).parent
1263
+ backend = DiskBackend(tests_dir / "mock_device_2", mock_logger)
1264
+
1265
+ result = libraries_from_auto_file(backend, auto_file, mod_names)
1266
+
1267
+ assert result == [
1268
+ "adafruit_spd1608",
1269
+ "adafruit_spd1656",
1270
+ "adafruit_ssd1675",
1141
1271
  ]
1142
1272
 
1143
1273
 
1144
- def test_libraries_from_imports_bad():
1145
- """Ensure that we catch an import error"""
1274
+ def test_libraries_from_auto_file_none():
1275
+ """Check that we exit if we give no auto_file argument
1276
+ and there's no default code file"""
1277
+ mod_names = []
1278
+ auto_file = None
1279
+
1280
+ with mock.patch("circup.logger.info") as mock_logger, mock.patch(
1281
+ "circup.os.path.isfile", return_value=True
1282
+ ), mock.patch(
1283
+ "circup.bundle.Bundle.lib_dir",
1284
+ return_value="tests",
1285
+ ):
1286
+ tests_dir = pathlib.Path(__file__).parent
1287
+ backend = DiskBackend(tests_dir / "mock_device", mock_logger)
1288
+ try:
1289
+ libraries_from_auto_file(backend, auto_file, mod_names)
1290
+ raise Exception("Did not call exit")
1291
+ except SystemExit as ex:
1292
+ assert ex.code == 1
1293
+
1294
+
1295
+ def test_install_auto_file_bad():
1296
+ """Ensure that we catch an error when parsing auto file"""
1146
1297
  TEST_BUNDLE_MODULES = {"one.py": {}, "two.py": {}, "three.py": {}}
1147
1298
  runner = CliRunner()
1148
1299
 
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
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