circup 2.1.2__py3-none-any.whl → 2.2.0__py3-none-any.whl

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.
circup/backends.py CHANGED
@@ -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
circup/command_utils.py CHANGED
@@ -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):
circup/commands.py CHANGED
@@ -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
@@ -1,8 +1,8 @@
1
1
  circup/__init__.py,sha256=9A98U3DyA14tm-7_d88fGSlLfEKz9T_85t8kXkErqRg,662
2
- circup/backends.py,sha256=vJM2IstM6ITY23ry0tZvk54NFel8Mkvc60MtAXvez0o,39024
2
+ circup/backends.py,sha256=g9Q9xCGZidwsEDL2Ga2cm50YYB54IiqlKUPcxj-pWZA,40008
3
3
  circup/bundle.py,sha256=FEP4F470aJtwmm8jgTM3DgR3dj5SVwbX1tbyIRKVHn8,5327
4
- circup/command_utils.py,sha256=W5l9Llh4X8DkKihhcPK4CeKCKjbUbaX6N374atQKREU,23908
5
- circup/commands.py,sha256=4O7WtdNKKsjnx180og9PHwkq2NbN9_bZuCwMAQov1bY,28534
4
+ circup/command_utils.py,sha256=8CaCy34EQcrDwcnXbyRvTZ-aUU-yCJ0Om8V1QuRNSbA,29392
5
+ circup/commands.py,sha256=1UQ8EONlcEFQsytPO3LAHLcNHZIO0Fi5XCU47quzt08,27436
6
6
  circup/logging.py,sha256=hu4v8ljkXo8ru-cqs0W3PU-xEVvTO_qqMKDJM18OXbQ,1115
7
7
  circup/module.py,sha256=33_kdy5BZn6COyIjAFZMpw00rTtPiryQZWFXQkMF8FY,7435
8
8
  circup/shared.py,sha256=qD7eN2KJcLsfhi8xMqWiNaE_LT0ueOG_G15_5m47gMo,8923
@@ -12,9 +12,9 @@ circup/wwshell/README.rst,sha256=M_jFP0hwOcngF0RdosdeqmVOISNcPzyjTW3duzIu9A8,361
12
12
  circup/wwshell/README.rst.license,sha256=GhA0SoZGP7CReDam-JJk_UtIQIpQaZWQFzR26YSuMm4,107
13
13
  circup/wwshell/__init__.py,sha256=CAPZiYrouWboyPx4KiWLBG_vf_n0MmArGqaFyTXGKWk,398
14
14
  circup/wwshell/commands.py,sha256=-I5l7XeoDmvWWuZg5wHdt9qe__SBQ1EGmKwCDTBMeus,7454
15
- circup-2.1.2.dist-info/LICENSE,sha256=bVlIMmSL_pqLCqae4hzixy9pYXD808IbgsMoQXTNLBk,1076
16
- circup-2.1.2.dist-info/METADATA,sha256=ShTqRavaPVkIOUU5olG3Dq2kqI0WPsT-vjIXc6KijB8,13622
17
- circup-2.1.2.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
18
- circup-2.1.2.dist-info/entry_points.txt,sha256=FjTmwYD_ApgLRGifUrr_Ui1voW6fEzodQc3DKNzoAPc,69
19
- circup-2.1.2.dist-info/top_level.txt,sha256=Qx6E0eZgSBE10ciNKsLZx8-TTy_9fEVZh7NLmn24KcU,7
20
- circup-2.1.2.dist-info/RECORD,,
15
+ circup-2.2.0.dist-info/licenses/LICENSE,sha256=bVlIMmSL_pqLCqae4hzixy9pYXD808IbgsMoQXTNLBk,1076
16
+ circup-2.2.0.dist-info/METADATA,sha256=CAUjxIk4DKD_CXJ28nMv9q4WybfiFZAn7FJkkG61Bg4,13617
17
+ circup-2.2.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
18
+ circup-2.2.0.dist-info/entry_points.txt,sha256=FjTmwYD_ApgLRGifUrr_Ui1voW6fEzodQc3DKNzoAPc,69
19
+ circup-2.2.0.dist-info/top_level.txt,sha256=Qx6E0eZgSBE10ciNKsLZx8-TTy_9fEVZh7NLmn24KcU,7
20
+ circup-2.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5