circup 2.1.2__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.2/circup.egg-info → circup-2.2.0}/PKG-INFO +3 -3
  2. {circup-2.1.2 → circup-2.2.0}/circup/backends.py +51 -19
  3. {circup-2.1.2 → circup-2.2.0}/circup/command_utils.py +165 -11
  4. {circup-2.1.2 → circup-2.2.0}/circup/commands.py +5 -25
  5. {circup-2.1.2 → circup-2.2.0/circup.egg-info}/PKG-INFO +3 -3
  6. {circup-2.1.2 → circup-2.2.0}/circup.egg-info/SOURCES.txt +8 -1
  7. {circup-2.1.2 → circup-2.2.0}/circup.egg-info/requires.txt +0 -1
  8. {circup-2.1.2 → circup-2.2.0}/requirements.txt +0 -1
  9. {circup-2.1.2 → circup-2.2.0}/tests/import_styles.py +3 -0
  10. circup-2.2.0/tests/mock_device/import_styles_sub.py +5 -0
  11. circup-2.2.0/tests/mock_device_2/.gitignore +4 -0
  12. circup-2.2.0/tests/mock_device_2/boot_out.txt +3 -0
  13. circup-2.2.0/tests/mock_device_2/boot_out.txt.license +3 -0
  14. circup-2.2.0/tests/mock_device_2/code.py +7 -0
  15. circup-2.2.0/tests/mock_device_2/package/__init__.py +6 -0
  16. circup-2.2.0/tests/mock_device_2/package/other.py +5 -0
  17. {circup-2.1.2 → circup-2.2.0}/tests/test_circup.py +145 -6
  18. {circup-2.1.2 → circup-2.2.0}/.github/ISSUE_TEMPLATE.md +0 -0
  19. {circup-2.1.2 → circup-2.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  20. {circup-2.1.2 → circup-2.2.0}/.github/workflows/build.yml +0 -0
  21. {circup-2.1.2 → circup-2.2.0}/.github/workflows/release.yml +0 -0
  22. {circup-2.1.2 → circup-2.2.0}/.gitignore +0 -0
  23. {circup-2.1.2 → circup-2.2.0}/.isort.cfg +0 -0
  24. {circup-2.1.2 → circup-2.2.0}/.pre-commit-config.yaml +0 -0
  25. {circup-2.1.2 → circup-2.2.0}/.pylintrc +0 -0
  26. {circup-2.1.2 → circup-2.2.0}/CODE_OF_CONDUCT.rst +0 -0
  27. {circup-2.1.2 → circup-2.2.0}/CODE_OF_CONDUCT.rst.license +0 -0
  28. {circup-2.1.2 → circup-2.2.0}/CONTRIBUTING.rst +0 -0
  29. {circup-2.1.2 → circup-2.2.0}/CONTRIBUTING.rst.license +0 -0
  30. {circup-2.1.2 → circup-2.2.0}/LICENSE +0 -0
  31. {circup-2.1.2 → circup-2.2.0}/LICENSES/CC-BY-4.0.txt +0 -0
  32. {circup-2.1.2 → circup-2.2.0}/LICENSES/MIT.txt +0 -0
  33. {circup-2.1.2 → circup-2.2.0}/LICENSES/Unlicense.txt +0 -0
  34. {circup-2.1.2 → circup-2.2.0}/README.rst +0 -0
  35. {circup-2.1.2 → circup-2.2.0}/README.rst.license +0 -0
  36. {circup-2.1.2 → circup-2.2.0}/circup/__init__.py +0 -0
  37. {circup-2.1.2 → circup-2.2.0}/circup/bundle.py +0 -0
  38. {circup-2.1.2 → circup-2.2.0}/circup/config/bundle_config.json +0 -0
  39. {circup-2.1.2 → circup-2.2.0}/circup/config/bundle_config.json.license +0 -0
  40. {circup-2.1.2 → circup-2.2.0}/circup/logging.py +0 -0
  41. {circup-2.1.2 → circup-2.2.0}/circup/module.py +0 -0
  42. {circup-2.1.2 → circup-2.2.0}/circup/shared.py +0 -0
  43. {circup-2.1.2 → circup-2.2.0}/circup/wwshell/README.rst +0 -0
  44. {circup-2.1.2 → circup-2.2.0}/circup/wwshell/README.rst.license +0 -0
  45. {circup-2.1.2 → circup-2.2.0}/circup/wwshell/__init__.py +0 -0
  46. {circup-2.1.2 → circup-2.2.0}/circup/wwshell/commands.py +0 -0
  47. {circup-2.1.2 → circup-2.2.0}/circup.egg-info/dependency_links.txt +0 -0
  48. {circup-2.1.2 → circup-2.2.0}/circup.egg-info/entry_points.txt +0 -0
  49. {circup-2.1.2 → circup-2.2.0}/circup.egg-info/top_level.txt +0 -0
  50. {circup-2.1.2 → circup-2.2.0}/docs/_static/favicon.ico +0 -0
  51. {circup-2.1.2 → circup-2.2.0}/docs/_static/favicon.ico.license +0 -0
  52. {circup-2.1.2 → circup-2.2.0}/docs/conf.py +0 -0
  53. {circup-2.1.2 → circup-2.2.0}/docs/index.rst +0 -0
  54. {circup-2.1.2 → circup-2.2.0}/docs/index.rst.license +0 -0
  55. {circup-2.1.2 → circup-2.2.0}/docs/logo.png +0 -0
  56. {circup-2.1.2 → circup-2.2.0}/docs/logo.png.license +0 -0
  57. {circup-2.1.2 → circup-2.2.0}/optional_requirements.txt +0 -0
  58. {circup-2.1.2 → circup-2.2.0}/optional_requirements.txt.license +0 -0
  59. {circup-2.1.2 → circup-2.2.0}/pyproject.toml +0 -0
  60. {circup-2.1.2 → circup-2.2.0}/readthedocs.yml +0 -0
  61. {circup-2.1.2 → circup-2.2.0}/requirements.txt.license +0 -0
  62. {circup-2.1.2 → circup-2.2.0}/setup.cfg +0 -0
  63. {circup-2.1.2 → circup-2.2.0}/tests/__init__.py +0 -0
  64. {circup-2.1.2 → circup-2.2.0}/tests/bad_module/__init__.py +0 -0
  65. {circup-2.1.2 → circup-2.2.0}/tests/bad_module/my_module.py +0 -0
  66. {circup-2.1.2 → circup-2.2.0}/tests/bad_python.py +0 -0
  67. {circup-2.1.2 → circup-2.2.0}/tests/bundle.json +0 -0
  68. {circup-2.1.2 → circup-2.2.0}/tests/bundle.json.license +0 -0
  69. {circup-2.1.2 → circup-2.2.0}/tests/device.json +0 -0
  70. {circup-2.1.2 → circup-2.2.0}/tests/device.json.license +0 -0
  71. {circup-2.1.2 → circup-2.2.0}/tests/dir_module/__init__.py +0 -0
  72. {circup-2.1.2 → circup-2.2.0}/tests/dir_module/my_module.py +0 -0
  73. {circup-2.1.2 → circup-2.2.0}/tests/local_module.py +0 -0
  74. {circup-2.1.2 → circup-2.2.0}/tests/local_module_cp7.mpy +0 -0
  75. {circup-2.1.2 → circup-2.2.0}/tests/local_module_cp7.mpy.license +0 -0
  76. {circup-2.1.2 → circup-2.2.0}/tests/mock_device/boot_out.txt +0 -0
  77. {circup-2.1.2 → circup-2.2.0}/tests/mock_device/boot_out.txt.license +0 -0
  78. {circup-2.1.2 → circup-2.2.0}/tests/mock_device/lib/adafruit_waveform/.gitkeep +0 -0
  79. {circup-2.1.2 → circup-2.2.0}/tests/mount_exists.txt +0 -0
  80. {circup-2.1.2 → circup-2.2.0}/tests/mount_exists.txt.license +0 -0
  81. {circup-2.1.2 → circup-2.2.0}/tests/mount_missing.txt +0 -0
  82. {circup-2.1.2 → circup-2.2.0}/tests/mount_missing.txt.license +0 -0
  83. {circup-2.1.2 → circup-2.2.0}/tests/remote_module.py +0 -0
  84. {circup-2.1.2 → circup-2.2.0}/tests/test_bundle_config.json +0 -0
  85. {circup-2.1.2 → circup-2.2.0}/tests/test_bundle_config.json.license +0 -0
  86. {circup-2.1.2 → circup-2.2.0}/tests/test_bundle_config_local.json +0 -0
  87. {circup-2.1.2 → circup-2.2.0}/tests/test_bundle_config_local.json.license +0 -0
  88. {circup-2.1.2 → circup-2.2.0}/tests/test_module.mpy +0 -0
  89. {circup-2.1.2 → circup-2.2.0}/tests/test_module.mpy.license +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: circup
3
- Version: 2.1.2
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)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: circup
3
- Version: 2.1.2
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,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
+ Adafruit CircuitPython 8.1.0 on 2019-08-02; Adafruit CircuitPlayground Express with samd21g18
2
+ Board ID:this_is_a_board
3
+ UID:AAAABBBBCCCC
@@ -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
@@ -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
 
@@ -1129,8 +1132,33 @@ def test_show_match_py_command():
1129
1132
  assert "0 shown" in result.output
1130
1133
 
1131
1134
 
1132
- def test_libraries_from_imports():
1135
+ def test_imports_from_code():
1133
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"""
1134
1162
  mod_names = [
1135
1163
  "adafruit_bus_device",
1136
1164
  "adafruit_button",
@@ -1141,20 +1169,131 @@ def test_libraries_from_imports():
1141
1169
  "adafruit_oauth2",
1142
1170
  "adafruit_requests",
1143
1171
  "adafruit_touchscreen",
1172
+ "adafruit_ntp",
1144
1173
  ]
1145
- test_file = str(pathlib.Path(__file__).parent / "import_styles.py")
1146
1174
 
1147
- 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
+
1148
1229
  assert result == [
1149
1230
  "adafruit_bus_device",
1150
1231
  "adafruit_button",
1232
+ "adafruit_display_text",
1151
1233
  "adafruit_esp32spi",
1152
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",
1153
1252
  ]
1154
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",
1271
+ ]
1272
+
1273
+
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
+
1155
1294
 
1156
- def test_libraries_from_imports_bad():
1157
- """Ensure that we catch an import error"""
1295
+ def test_install_auto_file_bad():
1296
+ """Ensure that we catch an error when parsing auto file"""
1158
1297
  TEST_BUNDLE_MODULES = {"one.py": {}, "two.py": {}, "three.py": {}}
1159
1298
  runner = CliRunner()
1160
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
File without changes