Compare commits

...

477 commits
0.0.2 ... main

Author SHA1 Message Date
foamyguy
f2eccab822
Merge pull request #253 from FoamyGuy/autofile_outside_root_subimports
fix subimport finding for auto-files that are not in the root.
2025-08-19 09:39:55 -05:00
foamyguy
18a4687739 use 9.x in all test mock devices. Fix package subimport logic. Fix tests. Update from_auto_files_local test to mimic Fruit Jam OS usage. 2025-08-14 10:21:11 -05:00
foamyguy
ab32fc2b5b fix subimport finding for auto-files that are not in the root. 2025-08-14 09:25:45 -05:00
foamyguy
d1bfdbb042
Merge pull request #247 from FoamyGuy/remove_cporg_bundle
remove circuitpython_org bundle
2025-04-18 09:18:17 -05:00
foamyguy
056c222d57 remove circuitpython_org bundle 2025-04-18 07:52:04 -05:00
foamyguy
e8ce15021c
Merge pull request #246 from dhalbert/10.x-bundles
Add 10.x bundles
2025-04-08 08:47:13 -05:00
Dan Halbert
35dc55668e Add 10.x bundles 2025-04-04 17:48:03 -04:00
foamyguy
dd39ac635f
Merge pull request #244 from Neradoc/auto-install-sub-imports
Auto install sub imports
2025-03-31 09:57:08 -05:00
foamyguy
fa59c1ecf9 remove findimports req, remove comment from test_circup 2025-03-31 09:41:15 -05:00
Neradoc
7b2cf4d252 inform the user of what auto file is being read
link to the documentation on code.py names
2025-03-20 23:49:17 +01:00
Neradoc
cde0dea1e5 sort results from get_all_imports() just for fun
modify mock_device_2 and add a new package module to test relative .imports
2025-02-16 18:04:01 +01:00
Neradoc
e7c7fb6d65 fix tests finding all in imports_from_code 2025-02-16 18:04:01 +01:00
Neradoc
e717ef0306 fix relative paths in package modules
fix finding submodules as "from A import B,C,D"
skip stars (*) in: "from A import *"
2025-02-16 18:04:01 +01:00
Neradoc
d27ae8164c Make auto-install find user code recursively. Find code.py alternatives.
Auto install:
- Move finding libraries from auto-file into command utils (libraries_from_auto_file)
- Find the first possible code.py alternative in order (like main.py)
- Replace libraries_from_code_py using the ast module instead of findimports
- Get all imports to find local python imports
- Find all dependencies from user code recursively
Update backends:
- Add get_file_content in Backends subclasses
- Remove no longer used get_auto_file_path()
- Add list_dir() to DiskBackend
Update tests
- Add non-bundle imports
- Add submodule import
- Add another mock device to test finding code.py
2025-02-16 18:04:01 +01:00
Neradoc
43b31da905 fix mock variable in test_get_bundles_dict 2025-02-16 18:04:01 +01:00
foamyguy
9c05ad8f7c
Merge pull request #242 from dhalbert/drop-8.x-bundles
drop 8.x bundle support
2025-02-12 14:54:28 -06:00
Dan Halbert
b28106713e GitHub Actions: stop using ::set-output 2025-02-08 15:46:01 -05:00
Dan Halbert
60c4107dbd update test to 9 2025-02-08 15:19:26 -05:00
Dan Halbert
5fd54fa603 drop 8.x bundle support 2025-02-08 11:23:29 -05:00
foamyguy
1ea6cd2d7b
Merge pull request #240 from slaftos/example-list
Add --list and --rename options to circup example command
2025-01-20 17:18:50 -06:00
Brian K. Jackson(Arakkis)
283a499a02 Remove inacurate comments 2025-01-16 19:19:51 -05:00
Brian K. Jackson(Arakkis)
f873c78912 pylint cares about file encoding 2025-01-15 15:29:08 -05:00
Brian K. Jackson(Arakkis)
831316ae2f Substitute json load/dump for pickle serialization 2025-01-15 15:09:56 -05:00
Brian K. Jackson(Arakkis)
bd834c4603 Add --list and --rename options to example
Added caching of example library example metadata, to speed up tab completion and --list returns

--rename option will rename output to code.py if example resolves to single file

Changed connection announcement to include board name
2025-01-12 07:50:34 -05:00
foamyguy
da8f6c26c5
Merge pull request #236 from FoamyGuy/wwshell_fixes
wwshell fixes
2025-01-03 16:20:26 -06:00
foamyguy
f028b18310 remove prints, fix missing port argument. 2025-01-03 14:56:07 -06:00
Dan Halbert
a03d50463f
Merge pull request #223 from FoamyGuy/wwshell
wwshell file management CLI
2025-01-02 10:47:28 -05:00
Dan Halbert
7c5ba016f3
Merge pull request #226 from FoamyGuy/updating_name_refs_and_versions
Updating name refs and versions + zsh tab completion
2024-12-29 16:13:44 -05:00
Dan Halbert
1eaca649cd
Merge pull request #235 from Neradoc/fix-version-read-1
Slight improvement to reading MPY files module version
2024-12-29 16:12:04 -05:00
Neradoc
1bc332b839 fix sphinx warning 2024-12-29 19:56:10 +01:00
Neradoc
dfc10c91a5 avoid reading random strings (like IP addresses) as version numbers in mpy files
always scan files in a package by alphabetical order so that `__init__` is read first
then keep the first version number found, don't update it later
2024-12-29 19:41:15 +01:00
Dan Halbert
27284f366e
Merge pull request #231 from FoamyGuy/support_py312
python 3.12 support
2024-07-26 23:38:48 -04:00
foamyguy
c3da149010 similarity lines config 2024-07-15 16:12:42 -05:00
foamyguy
995f9392eb python 3.12 support 2024-07-15 16:08:06 -05:00
Scott Shawcroft
9662d8b924
Merge pull request #230 from adafruit/ww-portno
Support web workflow on alternate port numbers
2024-07-08 11:10:58 -07:00
ae3919e4b4 Support web workflow on alternate port numbers 2024-07-08 10:11:49 -05:00
foamyguy
cb3de91da8
Merge pull request #229 from Jessseee/installing_pypi_stubs
Adding option to install library stubs from PyPi
2024-07-01 15:39:02 -05:00
Jesse Visser
9610d7dcb8
add option to install library stubs from pypi 2024-06-17 20:34:33 +02:00
foamyguy
431ccac9b3 merge main 2024-06-17 11:51:23 -05:00
foamyguy
a6985550e1 Merge branch 'main' into wwshell_merge_main
# Conflicts:
#	setup.py
2024-06-17 11:32:02 -05:00
foamyguy
35ca9cccec
remove specific version from click link
Co-authored-by: Dan Halbert <halbert@adafruit.com>
2024-06-08 06:48:32 -05:00
foamyguy
0bfbe875f4 another name change. update version. add zsh instruction 2024-06-07 19:13:53 -05:00
foamyguy
b9c6698aca Merge branch 'main' into updating_name_refs_and_versions 2024-06-07 19:11:48 -05:00
foamyguy
5f444f0f70
Merge pull request #225 from FoamyGuy/use_dynamic_dependencies
enable dynamic depencies
2024-06-07 13:53:51 -05:00
foamyguy
f05ff1896d enable dynamic depencies 2024-06-07 13:45:46 -05:00
foamyguy
dde7615e1b
Merge pull request #224 from FoamyGuy/fix_release_build
add build for release
2024-06-03 17:09:14 -05:00
foamyguy
ed39f029b9 add build for release 2024-06-03 17:03:01 -05:00
foamyguy
0227af6bbc
Merge pull request #214 from sjev/1-switch-from-setuppy-to-pyprojecttoml
switch from setuppy to pyprojecttoml
2024-06-03 16:55:57 -05:00
foamyguy
524e552394 fix release action 2024-06-03 16:41:46 -05:00
foamyguy
50561f5972 fix release action condition 2024-06-03 16:30:33 -05:00
foamyguy
23baaeb6c2 configure auto version 2024-06-03 16:16:45 -05:00
foamyguy
2abf7552fc change release action for pyproject.toml instead of setup.py 2024-06-03 16:10:48 -05:00
foamyguy
c773c20b8c min version 3.9 2024-06-03 15:55:55 -05:00
foamyguy
ef128a6d09 remove devcontainer mention from contributing, change install dev reqs command. 2024-06-01 11:21:16 -05:00
Jev
2899011c73 removed devcontainer 2024-05-31 20:32:34 +02:00
foamyguy
11868c327e wwshell mkdir implementation 2024-05-30 17:26:40 -05:00
foamyguy
c24d48b4ea wwshell readme 2024-05-28 20:51:54 -05:00
foamyguy
6c824c538e pylint fixes 2024-05-28 20:32:16 -05:00
foamyguy
a01586b342 implement rm 2024-05-28 18:26:35 -05:00
foamyguy
e75a7dbf3a bringing in wwshell 2024-05-25 12:14:52 -05:00
Scott Shawcroft
74b07bee0a
Merge pull request #219 from FoamyGuy/fix_local_dir_nondevice
fix version overriding arg
2024-05-20 11:44:15 -07:00
foamyguy
f12efa37fc fix version overriding arg 2024-05-20 11:07:50 -05:00
foamyguy
99367e95a6 change CircUp -> Circup 2024-05-20 10:43:19 -05:00
Scott Shawcroft
c1e1b4b269
Merge pull request #217 from FoamyGuy/webworkflow_pass_env
env var CIRCUP_WEBWORKFLOW_PASSWORD
2024-05-13 15:27:11 -07:00
foamyguy
e128d650e9 allow env var for webworkflow password 2024-05-12 12:31:24 -05:00
Dan Halbert
eacc3199fc
Merge pull request #216 from FoamyGuy/local_install
Support Local Path Install
2024-05-12 11:53:29 -04:00
foamyguy
982911c049 install tab completion for local files. return early if no name. avoid multiple exists checks. 2024-05-12 09:50:40 -05:00
foamyguy
3d66b08d15 update install help comment 2024-05-11 12:08:38 -05:00
foamyguy
e1166b6169 web workflow local install 2024-05-11 12:06:28 -05:00
Jev Kuznetsov
4e4934f659 move requirements to external txt files 2024-05-06 17:11:47 +00:00
Jev Kuznetsov
f3779b8f11 Revert "remove requirement files"
This reverts commit 55b687db23.
2024-05-06 17:08:59 +00:00
Jev Kuznetsov
3a1f3b3add Revert "add dummy requirements.txt as workaround for keeping CI script working."
This reverts commit c94b991dc6.
2024-05-06 17:08:30 +00:00
Jev Kuznetsov
7e31cb34fc Revert "add dev requirements install (+ autoformat indent)"
This reverts commit 41ed0d2d9b.
2024-05-06 17:08:24 +00:00
Jev Kuznetsov
41ed0d2d9b add dev requirements install (+ autoformat indent) 2024-05-06 15:25:22 +00:00
Jev Kuznetsov
c94b991dc6 add dummy requirements.txt as workaround for keeping CI script working. 2024-05-06 15:17:05 +00:00
Jev
2657494e49 change license 2024-05-06 13:09:34 +00:00
Jev
45f0804b36 move build dep to dev reqs 2024-05-06 13:07:05 +00:00
Jev
55b687db23 remove requirement files 2024-05-06 08:21:21 +00:00
Jev
a3c162ffde downgrade to 3.11 2024-05-06 08:21:11 +00:00
Jev
435475b28f pff.. rst is not md 2024-05-06 00:39:39 +02:00
Jev
4f5e98738f remove setup.py 2024-05-06 00:29:57 +02:00
Jev
484f3fae24 run pre-commit, add licenses 2024-05-06 00:25:58 +02:00
Jev
2a656a1f0d rewrite 2024-05-05 21:55:39 +00:00
Jev
6e35e5bcc3 remove header 2024-05-05 21:54:42 +00:00
Jev
4abf6d9936 add note 2024-05-05 21:54:22 +00:00
Jev
6109b84644 try to fix header 2024-05-05 21:52:07 +00:00
Jev
20c7d6337e add devcontainer instructions 2024-05-05 21:50:53 +00:00
Jev
b6f049f8af auto install dev reqs 2024-05-05 21:50:37 +00:00
Jev
76bf98eefe add build 2024-05-05 21:39:00 +00:00
Jev
98a65d9c1f add pyproject 2024-05-05 21:19:54 +00:00
Jev
a1b388a3cc rename setup 2024-05-05 21:19:24 +00:00
Jev
8c9f84bcc4 rename prompt 2024-05-05 19:36:50 +00:00
Jev
678d40766b add devcontainer files 2024-05-05 19:15:04 +00:00
Dan Halbert
fdf824d680
Merge pull request #212 from justmobilize/update-import
Update import
2024-04-30 13:19:58 -04:00
Justin Myers
44f2144724 Update import 2024-04-30 09:58:31 -07:00
foamyguy
04836d5725 local module installs 2024-04-26 16:57:40 -05:00
Scott Shawcroft
582c55e1a5
Merge pull request #210 from FoamyGuy/upgrade_flag
Upgrade flag
2024-04-23 09:38:10 -07:00
Scott Shawcroft
656f1fb118
Merge pull request #211 from FoamyGuy/example_command
Example command
2024-04-23 09:37:33 -07:00
foamyguy
a110c11ea8 docstrings for location arg 2024-04-22 18:03:28 -05:00
foamyguy
9887ee97f7 capital -U flag for upgrade 2024-04-22 17:54:53 -05:00
foamyguy
1307f3b611 update help text in readme 2024-04-16 19:41:45 -05:00
foamyguy
fdd464d61f cleanup and code format 2024-04-16 19:40:14 -05:00
foamyguy
ffda566d7f example command for WebBackend 2024-04-13 16:56:07 -05:00
foamyguy
44443b32ea starting example command 2024-04-13 16:45:02 -05:00
foamyguy
2ee6f3171e format 2024-04-12 17:55:01 -05:00
foamyguy
c16e377dee upgrade flag for install 2024-04-12 17:54:27 -05:00
foamyguy
fe79ea8d3b
Merge pull request #208 from FoamyGuy/refactoring_and_issue_fixes
Refactoring and issue fixes
2024-04-09 17:54:45 -05:00
foamyguy
f018daf74e language en for docs 2024-04-08 12:25:06 -05:00
foamyguy
9b450c8ba2 mock bad data instead of json.load side effect 2024-04-08 12:19:26 -05:00
foamyguy
a2548e52d5 change module for mock json error 2024-04-08 12:14:01 -05:00
foamyguy
70b9b49872 change module for mock logger 2024-04-08 12:03:27 -05:00
foamyguy
d30e02fb88 change module for mock json fail 2024-04-08 11:56:57 -05:00
foamyguy
422b6305b7 remove extra action and prints 2024-04-08 11:49:14 -05:00
foamyguy
d3515e9eed mock current_tag prop instead of json.load 2024-04-08 11:42:41 -05:00
foamyguy
1c1307b070 try unconditional print 2024-04-08 11:04:48 -05:00
foamyguy
5354155113 raise exception instead of print 2024-04-08 10:30:44 -05:00
foamyguy
5bfe6c0185 remove warnings 2024-04-08 10:23:36 -05:00
foamyguy
bf7617eabe try stderr? format 2024-04-08 10:21:07 -05:00
foamyguy
bd7b85170e try stderr? 2024-04-08 10:17:27 -05:00
foamyguy
8bc37f7ae5 warnings instead of prints 2024-04-08 10:05:26 -05:00
foamyguy
e86487776f try single test with print? 2024-04-08 09:53:35 -05:00
foamyguy
3622f29b75 try enable print output from pytest 2024-04-08 09:49:07 -05:00
foamyguy
30e80f3486 can I see prints? format 2024-04-08 09:39:37 -05:00
foamyguy
b658576873 can I see prints? 2024-04-08 09:37:43 -05:00
foamyguy
cd9fb82552 add modulename to mock. remove duplicate 2024-04-08 09:27:56 -05:00
foamyguy
dc6b445855 format 2024-04-08 09:21:05 -05:00
foamyguy
aea9ce6704 isfile and isdir? 2024-04-08 09:18:52 -05:00
foamyguy
591a2e6eeb code format 2024-04-08 09:15:03 -05:00
foamyguy
6f5ad6e663 try fix for test_esnure_latest_bundle's 2024-04-08 09:10:08 -05:00
foamyguy
dc5f2e4eab try fix for test_get_bundle() 2024-04-08 09:05:10 -05:00
foamyguy
ba726e2047 add mocked lib folder for module update dir test 2024-04-08 08:40:53 -05:00
foamyguy
fe326c84a4 fixing more tests 2024-04-07 12:27:58 -05:00
foamyguy
eef8326519 fixing more tests 2024-04-07 10:56:35 -05:00
foamyguy
d88003b5c5 starting to fix tests 2024-04-07 10:32:23 -05:00
foamyguy
ccbebf5f6b add optional requirements 2024-04-06 12:34:04 -05:00
foamyguy
1d100e0e42 code format + pylint fixes 2024-04-06 12:14:09 -05:00
foamyguy
b76d947913 ignore list for modules not to warn about 2024-04-06 11:40:24 -05:00
foamyguy
9a7bb8be00 update major version mpy warning 2024-04-06 11:26:38 -05:00
foamyguy
57a72ea91a change requirements to match setup.py 2024-04-06 11:21:26 -05:00
foamyguy
eec4af4f7a show error for bundle-add and bundle-remove without arguments. fix bundle-add 2024-04-06 11:07:13 -05:00
foamyguy
0b33838780 use click.style instead of two click lines. 2024-04-06 10:48:17 -05:00
foamyguy
7e80f28528 confirm before clobbering requirements.txt. fix VERBOSE reference in update 2024-04-06 10:46:31 -05:00
foamyguy
b11f97bfc7 code format + copyright for logging.py 2024-04-02 20:02:38 -05:00
foamyguy
3a93a1e610 adding copyrights and comments. remove some usage of VERBOSE global 2024-04-02 19:50:43 -05:00
foamyguy
caef12c5a1 moving class definitions to own files, fix CPY_VERSION references in modules 2024-04-02 19:17:52 -05:00
foamyguy
a2ae043684 moving class definitions to own files 2024-04-02 19:07:17 -05:00
foamyguy
2572c32afe allow --help to be run boardless 2024-04-01 17:17:15 -05:00
foamyguy
65f0a3e109 start refactor 2024-04-01 17:03:09 -05:00
Dan Halbert
27ae1f978d
Merge pull request #205 from vladak/web_api_check
refuse any device with web API version stricly less than 4
2024-03-09 11:05:59 -05:00
Vladimir Kotal
eb108fe5f5 refuse any device with web API version stricly less than 4
fixes #204
2024-03-07 21:38:27 +01:00
Dan Halbert
38dd524b45
Merge pull request #202 from justmobilize/add-missing-on-update
Add missing requirements on update
2024-03-05 14:59:20 -05:00
Justin Myers
1624a39dba Add missing requirements on update 2024-03-05 11:39:12 -08:00
Scott Shawcroft
8f1accc568
Merge pull request #163 from vladak/web_workflow
add support for web workflow
2024-03-05 10:45:52 -08:00
foamyguy
5479ff44a6 non_writeable drive error message. 2024-03-03 18:50:22 -06:00
foamyguy
1ef7651d6f function doc 2024-03-03 17:14:21 -06:00
foamyguy
258f08494f cleanup and add error message 2024-03-03 17:08:54 -06:00
foamyguy
0c5dec02e6 use free_space instead of device_versions to verify presence 2024-03-03 16:17:46 -06:00
foamyguy
73d5872ec7 code format 2024-03-03 16:02:34 -06:00
foamyguy
bc30d36255 lookup host from circuitpyhon.local 2024-03-03 16:01:57 -06:00
foamyguy
3cd60f12a9 fix directory_module test 2024-03-03 12:58:38 -06:00
foamyguy
3622c3df67 inside the arguments 2024-03-03 12:22:16 -06:00
foamyguy
2c8d3e93e9 different line 2024-03-03 12:18:56 -06:00
foamyguy
08e0c9455d add a different one? 2024-03-03 12:17:02 -06:00
foamyguy
3ec6e34bd3 try moving this? 2024-03-03 12:14:31 -06:00
foamyguy
1c8b32c326 ignore locals 2024-03-03 11:57:04 -06:00
foamyguy
42d9fe6c08 code format and pylint 2024-03-03 11:52:10 -06:00
foamyguy
1f8443f164 remove extra sep 2024-03-03 09:45:51 -06:00
tyeth
d5d8ed607d Set logging to truncate log file automatically 2024-03-03 02:03:23 +00:00
tyeth
5965afde9a Add free space check for folders + nesting 2024-03-03 01:29:46 +00:00
tyeth
c8ff0551c5 Win:os.sep in Module.name/DiskBackend.library_path 2024-03-02 23:55:45 +00:00
tyeth
d19e4745df check for writable/free_space + auto_file fallback 2024-03-02 23:55:37 +00:00
Tyeth Gundry
3ed849593d Add commandline option for timeout, sync existing REQUEST_TIMEOUT 2024-03-02 19:17:09 +00:00
foamyguy
a286849393 timeouts on all requests. code format 2024-03-02 12:06:45 -06:00
foamyguy
84e048e0ee fix for name when module is directory 2024-03-02 11:56:25 -06:00
foamyguy
ac578b852a simplify logic in Module init 2024-03-02 11:20:59 -06:00
foamyguy
deea802a29 lots of prints 2024-03-02 10:59:59 -06:00
Dan Halbert
5b9415f6ef
Merge pull request #201 from FoamyGuy/connection_manager_name_mapping 2024-02-29 20:12:05 -05:00
foamyguy
c476116883 connection manager name mapping 2024-02-29 16:27:23 -06:00
foamyguy
dbb8374638 fix for json response files key. Use requests Session with retries in WebBackend. timeout on a requests call. code format 2024-02-26 17:30:04 -06:00
foamyguy
15aec597b8 Merge branch 'web_workflow_tweak' into web_workflow 2024-02-26 16:45:06 -06:00
foamyguy
bfae6066dc use self.password instead of parse url 2024-02-26 16:30:23 -06:00
foamyguy
e0bbec2251 move password check and host check to WebBackend. 2024-02-26 16:23:44 -06:00
foamyguy
88a5f9a2e0 code format 2024-02-26 15:54:23 -06:00
foamyguy
33a0528087 Merge branch 'main' into web_workflow
# Conflicts:
#	circup/__init__.py
#	tests/test_circup.py
2024-02-26 15:46:40 -06:00
foamyguy
7d3eb6cd78 extract_metadata changes from #198 2024-02-26 15:35:01 -06:00
Scott Shawcroft
b2601428d1
Merge pull request #198 from dhalbert/fix-mpy-update-checking 2024-02-13 15:04:13 -08:00
Dan Halbert
db47b6f423 update tests 2024-02-13 16:29:31 -05:00
Dan Halbert
271389207e update to actions/checkout@v4 2024-02-13 15:56:33 -05:00
Dan Halbert
e7e8df0274 Handle mpy version checking for Circuitpython 9 2024-02-13 15:45:28 -05:00
tyeth
e5a20309a0 Dispose of request sockets + create DIRs first
Saw ConnectionResetError: [WinError 10054] An existing connection was forcibly closed by the remote host
2024-02-11 20:34:49 +00:00
tyeth
6cc6a1df39 Tweak for Web Workflow with sub paths 2024-02-11 17:43:47 +00:00
foamyguy
01cceec6bf remove prints 2023-12-18 10:48:32 -06:00
foamyguy
1f9bee3ffc black format b4 pylint. fix tests tests to passing 2023-12-18 10:43:34 -06:00
foamyguy
2d0739005a parse_boot_out helper. change DiskBackend() to take boot_out string instead of version info tuple 2023-12-13 18:35:43 -06:00
foamyguy
31270a0286 is_deice_present() for WebBackend. pylint fixes. equalize behavior when device is not present. 2023-12-13 18:17:36 -06:00
foamyguy
a96fd8e84a refactor Module() not to take path. is_deice_present() for DiskBackend 2023-12-12 19:33:35 -06:00
foamyguy
56c1ddab31 allow mock version_info in DiskBackend 2023-12-11 16:52:13 -06:00
foamyguy
01f0a2d046 removing path argument from backend functions and find_modules(). add backend.get_file_path(). remove target url argument (including passwrd) from install_http() 2023-12-04 16:50:29 -06:00
foamyguy
d9ce3b25a9 copyright for shared.py and backends.py 2023-12-04 15:19:00 -06:00
foamyguy
bea088ec50 don't pass library path. 2023-12-02 11:57:24 -06:00
foamyguy
499dee6e03 move libraries_from_code_py out of backend. fix CPY_VERSION issue for web backend 2023-11-30 06:44:22 -06:00
foamyguy
fb6cd847a8 starting move backends to own file 2023-11-29 07:55:44 -06:00
foamyguy
d3eda67ddb fix typo, change name to DiskBackend, remove wrapper _get_circuitpython_version 2023-11-29 07:21:44 -06:00
foamyguy
467740088c remove extra tick mark 2023-11-27 09:51:09 -06:00
foamyguy
51f9670610 update readme for web workflow support 2023-11-27 09:50:17 -06:00
foamyguy
75e501204a license for mock boot_out 2023-11-22 18:06:07 -06:00
foamyguy
a5b40b46d3 fixing tests 2023-11-22 18:02:48 -06:00
foamyguy
168f8ec86d remove prints 2023-11-22 16:58:46 -06:00
foamyguy
1681b29f9e working on windows USB fix 2023-11-22 22:37:37 -06:00
foamyguy
f582873328 Backend super class. 2023-11-21 19:17:43 -06:00
foamyguy
01a1ac2661 code format 2023-11-21 18:10:01 -06:00
foamyguy
1b2e115155 remove prints, fix trailing slash difference 2023-11-21 18:04:21 -06:00
foamyguy
c1af505510 main command function arguments access 2023-11-21 17:36:56 -06:00
foamyguy
90299a099a fix --path argument functionality 2023-11-20 11:01:58 -06:00
foamyguy
86ccabe411 more refactoring, some successful USB workflow functionality 2023-11-20 10:28:29 -06:00
foamyguy
c6d430c0fc more refactoring 2023-11-17 18:16:13 -06:00
foamyguy
97e60ed4e0 starting refactor 2023-11-17 17:55:50 -06:00
foamyguy
32dab6beaf Merge branch 'main' into web_workflow 2023-11-17 16:15:22 -06:00
Dan Halbert
e3d266525d
Merge pull request #194 from aricooperdavis/patch-1
Fix renamed `is_valid()` method call
2023-11-16 13:00:58 -05:00
Ari Cooper Davis
a76663f278
Fix renamed is_valid() method call 2023-11-16 17:40:47 +00:00
foamyguy
5f1e6ab835
Merge pull request #192 from FoamyGuy/toml_req
add toml to dependency list
2023-11-13 16:40:31 -06:00
foamyguy
dc8d43dfef add toml to dependency list 2023-11-13 14:53:52 -06:00
foamyguy
f4618b3e7c fix for None ctx.parent. Fix file scheme condition 2023-11-13 11:39:28 -06:00
foamyguy
225b490295 fixes for USB workflow and --auto 2023-11-13 11:27:55 -06:00
foamyguy
5615279eb3 Merge branch 'main' into web_workflow
# Conflicts:
#	circup/__init__.py
2023-11-13 11:01:08 -06:00
foamyguy
b7eb10b7a2 fix for file protocol 2023-11-13 10:50:45 -06:00
foamyguy
ce7a29823e
Merge pull request #188 from FoamyGuy/pyproject_toml_circup_dependencies
Add support for Pyproject toml circup dependencies
2023-11-13 10:37:39 -06:00
foamyguy
8987505241 implementing --auto for webworkflow 2023-11-10 17:51:40 -06:00
foamyguy
b30e2a92e1 function docstring 2023-11-04 11:15:55 -05:00
foamyguy
7bfda3b5a5 remove debugging print 2023-11-04 11:11:32 -05:00
foamyguy
b29414d26e check for and install circup_dependencies from pyproject.toml 2023-11-04 11:10:33 -05:00
Scott Shawcroft
e73a094d24
Merge pull request #185 from imnotjames/use-semver-3
chore: bump semver to 3.0.x
2023-10-30 10:39:02 -07:00
foamyguy
c4e1c1ad8b
Merge pull request #184 from dhalbert/9.x-support
add 9.x-support; don't give up if download fails
2023-10-29 19:36:35 -05:00
James Ward
255493fbcb
chore: bump semver to 3.0.x 2023-10-29 17:17:33 -04:00
Dan Halbert
02d3aa7f7a add 9.x-support; don't give up if download fails 2023-10-29 00:04:43 -04:00
foamyguy
63dcbb9944 disable too-many-args for main 2023-10-28 12:10:03 -05:00
foamyguy
ae5cc2a9b4 Merge branch 'main' into web_workflow
# Conflicts:
#	circup/__init__.py
2023-10-28 11:34:03 -05:00
Scott Shawcroft
abd225169a
Merge pull request #182 from MakerClassCZ/override-params
Add board-id and cpy-version params
2023-10-23 13:06:57 -07:00
Vladimír Smitka
0875a4d834 Attempt to fix splitted long lines 2023-10-22 22:08:28 +02:00
Vladimír Smitka
49737096ff Attempt to remove one if-else
I got: circup/__init__.py:1177:0: R0912: Too many branches (13/12) (too-many-branches)
2023-10-22 21:52:20 +02:00
Vladimír Smitka
aa829a5804 Fix line lengths 2023-10-22 19:29:27 +02:00
Vladimír Smitka
575da0797d Add board-id and cpy-version params 2023-10-22 18:38:16 +02:00
Melissa LeBlanc-Williams
6806be4af4
Merge pull request #179 from adafruit/dependabot/pip/urllib3-1.26.18
Bump urllib3 from 1.26.17 to 1.26.18
2023-10-18 09:54:12 -07:00
dependabot[bot]
4e9a70a22a
Bump urllib3 from 1.26.17 to 1.26.18
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.17 to 1.26.18.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.17...1.26.18)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 01:24:38 +00:00
Scott Shawcroft
abf46302a8
Merge pull request #177 from dhalbert/no-7.x
7.x bundles are no longer built
2023-10-04 13:19:08 -07:00
Dan Halbert
fbf5aa4d50 7.x bundles are no longer built 2023-10-04 15:09:35 -04:00
Dan Halbert
4fcc7bcc82
Merge pull request #176 from adafruit/dependabot/pip/urllib3-1.26.17
Bump urllib3 from 1.26.5 to 1.26.17
2023-10-04 12:02:57 -04:00
dependabot[bot]
27160fe75d
Bump urllib3 from 1.26.5 to 1.26.17
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.26.5 to 1.26.17.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.26.5...1.26.17)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-03 04:39:00 +00:00
Alec Delaney
9047f8c072
Merge pull request #173 from adafruit/dependabot/pip/pygments-2.15.0
Bump pygments from 2.7.4 to 2.15.0
2023-07-28 00:42:35 -04:00
Alec Delaney
a649d29881
Merge pull request #175 from adafruit/dependabot/pip/certifi-2023.7.22
Bump certifi from 2022.12.7 to 2023.7.22
2023-07-28 00:40:13 -04:00
dependabot[bot]
523b64a2c7
Bump certifi from 2022.12.7 to 2023.7.22
Bumps [certifi](https://github.com/certifi/python-certifi) from 2022.12.7 to 2023.7.22.
- [Commits](https://github.com/certifi/python-certifi/compare/2022.12.07...2023.07.22)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-25 20:48:26 +00:00
dependabot[bot]
d411bae64d
Bump pygments from 2.7.4 to 2.15.0
Bumps [pygments](https://github.com/pygments/pygments) from 2.7.4 to 2.15.0.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.7.4...2.15.0)

---
updated-dependencies:
- dependency-name: pygments
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-07-20 13:11:46 +00:00
Dan Halbert
e400b88aa5
Merge pull request #168 from adafruit/dependabot/pip/requests-2.31.0
Bump requests from 2.26.0 to 2.31.0
2023-06-01 09:04:12 -04:00
dependabot[bot]
c29fc11ab7
Bump requests from 2.26.0 to 2.31.0
Bumps [requests](https://github.com/psf/requests) from 2.26.0 to 2.31.0.
- [Release notes](https://github.com/psf/requests/releases)
- [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md)
- [Commits](https://github.com/psf/requests/compare/v2.26.0...v2.31.0)

---
updated-dependencies:
- dependency-name: requests
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-05-23 02:04:52 +00:00
Dan Halbert
777e677170
Merge pull request #167 from furbrain/main
correctly install asyncio from requirements.txt
2023-04-26 21:21:43 -04:00
Phil Underwood
ce3fd34288 Make changes alphabetised 2023-04-26 21:57:39 +01:00
Phil Underwood
276fdc26b8 correctly install asyncio form requirementes 2023-04-26 17:19:59 +01:00
Dan Halbert
2753e50118
Merge pull request #166 from Neradoc/fix-auto
Fix auto requiring an argument
2023-04-12 20:47:09 -04:00
Neradoc
aa8ab4e5cd fix --auto requiring an argument 2023-04-13 02:19:22 +02:00
Dan Halbert
fd39347e01
Merge pull request #160 from Neradoc/nera-auto-install
Auto install improvements
2023-04-10 15:25:49 -04:00
Neradoc
82ffa73b57 Other way to find local path 2023-04-02 02:18:15 +02:00
Neradoc
62f7a657cf
Update circup/__init__.py
Co-authored-by: Dan Halbert <halbert@halwitz.org>
2023-04-01 17:11:23 +02:00
jposada202020
4015574518
Merge pull request #159 from Neradoc/find-all-files-in-packages
Find all files in packages
2023-03-20 20:44:59 -04:00
Neradoc
3e7aa71a44 find all files in packages 2023-03-21 01:39:29 +01:00
Neradoc
e7853dfc3a fix rebase 2023-03-21 01:31:53 +01:00
Neradoc
f7c812261b move install help to the options, remove no-auto 2023-03-21 01:28:25 +01:00
Neradoc
c46d2fdc75 using os.sep to find the ./ prefix 2023-03-21 01:27:31 +01:00
Neradoc
0306fae90c auto install improvements 2023-03-21 01:27:31 +01:00
Vladimir Kotal
151d3585ff fix install of multi-file packages 2023-02-22 22:10:42 +01:00
Vladimir Kotal
8b413ffe16 add support for web workflow
fixes #156
2023-02-22 20:41:46 +01:00
Scott Shawcroft
aa3c57ff22
Merge pull request #161 from Neradoc/update-pylint
Update to pylint 2.15.5 to avoid an error in pip installing it
2023-02-21 10:30:41 -08:00
Neradoc
d9c3c567e9 update to pylint 2.15.5 to avoid an error in pip installing it 2023-02-08 05:58:23 +01:00
Dan Halbert
55b0139ebf
Merge pull request #155 from Neradoc/wrong-mpy-file-response
Fix crash when bad MPY file
2022-12-09 18:58:11 -05:00
Dan Halbert
3cf3919e68
Merge pull request #158 from adafruit/dependabot/pip/certifi-2022.12.7
Bump certifi from 2019.6.16 to 2022.12.7
2022-12-09 18:57:51 -05:00
dependabot[bot]
717d015742
Bump certifi from 2019.6.16 to 2022.12.7
Bumps [certifi](https://github.com/certifi/python-certifi) from 2019.6.16 to 2022.12.7.
- [Release notes](https://github.com/certifi/python-certifi/releases)
- [Commits](https://github.com/certifi/python-certifi/compare/2019.06.16...2022.12.07)

---
updated-dependencies:
- dependency-name: certifi
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-08 06:20:12 +00:00
Neradoc
41c36cb368 change Bad format to Invalid 2022-10-31 17:05:48 +01:00
Neradoc
3ff7197077 detect bad mpy files and suggest replacing them 2022-10-31 17:05:48 +01:00
Dan Halbert
1910d6b4fc
Merge pull request #153 from Neradoc/filter-all-pip-versions
fix filtering all pip version strings in requirements.txt
2022-09-16 18:28:06 -04:00
Neradoc
7465718020 fix filtering all pip version strings in requirements.txt 2022-08-25 23:51:26 +02:00
foamyguy
b7a28e6d3c
Merge pull request #148 from Neradoc/ignore-type-annotations-for-dunders
fix reading __version__ and __repo__ with type annotations
2022-07-05 08:52:18 -05:00
Alec Delaney
b931a2055b
Merge pull request #149 from tekktrik/dev/fix-incompatibility
Update pinned importlib_metadata version
2022-07-05 09:22:01 -04:00
Alec Delaney
0a7f3fcfa0
Switch underscore for hyphen 2022-07-04 23:52:02 -04:00
Alec Delaney
af8034615c
Update pinned importlib_metadata version 2022-07-04 23:29:29 -04:00
Neradoc
52706625f0 fix reading __version__ and __repo__ with type annotations 2022-07-05 03:31:08 +02:00
Dan Halbert
2c23dd455b
Merge pull request #145 from FoamyGuy/8mpy_platform
add platform entry for 8mpy
2022-06-08 08:53:35 -04:00
Dan Halbert
4eea33612a
determine _bundle_count dynamically 2022-06-08 08:48:59 -04:00
foamyguy
c013e555ea bundle count in test 2022-06-06 12:42:57 -05:00
foamyguy
65ff2d7742 update versions in pre-commit config 2022-06-06 11:29:19 -05:00
foamyguy
17708f9a1c add platform entry for 8mpy 2022-06-06 11:17:30 -05:00
Dan Halbert
86ff01c5fb
Merge pull request #142 from dhalbert/handle-circuitpython_typing
special-case circuitpython_typing
2022-02-18 10:41:18 -05:00
Dan Halbert
d783743e79 special-case circuitpython_typing 2022-02-18 09:43:20 -05:00
Dan Halbert
14f7caf70f
Merge pull request #138 from adafruit/dhalbert-add-update-checker
add update_checker to install_requires
2021-12-24 14:57:13 -05:00
Dan Halbert
0c922620a6
add update_checker to install_requires 2021-12-24 14:50:03 -05:00
foamyguy
86255dd9df
Merge pull request #136 from dhalbert/circup-update-check
Report if a newer version of circup is available
2021-12-24 08:02:13 -06:00
Dan Halbert
bfecb7593f Report if a newer version of circup is available 2021-12-22 12:52:42 -05:00
Jeff Epler
f0d51771e2
Merge pull request #129 from adafruit/dependabot/pip/babel-2.9.1
Bump babel from 2.7.0 to 2.9.1
2021-12-16 07:29:44 -07:00
a05f7376c3
Merge remote-tracking branch 'origin/main' into dependabot/pip/babel-2.9.1 2021-12-16 08:23:36 -06:00
Scott Shawcroft
5552759282
Merge pull request #132 from FoamyGuy/remove_6x
remove 6.x platforms
2021-12-15 10:49:33 -08:00
foamyguy
cf7e9b1557 fix tests for no 6.x bundle 2021-12-14 17:13:58 -06:00
foamyguy
400e28e023 remove jet brains IDE dir from gitignore 2021-12-14 14:16:13 -06:00
foamyguy
c14337b741 use python37 in actions. gitignore IDE dir 2021-12-14 14:02:34 -06:00
foamyguy
eb34a646f8 remove 6.x platforms 2021-12-14 13:41:43 -06:00
dependabot[bot]
d39063daac
Bump babel from 2.7.0 to 2.9.1
Bumps [babel](https://github.com/python-babel/babel) from 2.7.0 to 2.9.1.
- [Release notes](https://github.com/python-babel/babel/releases)
- [Changelog](https://github.com/python-babel/babel/blob/master/CHANGES)
- [Commits](https://github.com/python-babel/babel/compare/v2.7.0...v2.9.1)

---
updated-dependencies:
- dependency-name: babel
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-21 18:38:55 +00:00
Neradoc
05514e5d79
Add commands for a local list of 3rd party bundles (#128)
* Add local bundle commands

- the local bundles take precedence over the built-in ones
- bundle-show lists the bundles
  --modules lists the modules in each bundle
- bundle-add adds a bundle from the user/repo github string
  it does some level of checking that the repo's and zips URLs exist
- bundle-remove removes a bundle from the local list

* allow overwritting the built-in modules manually

* remove show_bundles_info

* filter github URLs into github repo string

* avoid duplicates if adding built-ins as local, but allow it (to override priority order)

* put bundle-remove --reset back

* small change in bundle-show, local first

* message on built-in in bundle-remove
2021-09-13 18:27:18 -07:00
Patrick
d467d2545b
move bundle config to JSON format (#126)
* move bundle config to JSON format

* correct docstring, change print back to logging

* remove make reference from contributing

* change layout and install for config file
2021-09-08 16:07:21 -07:00
Neradoc
a9f9cf5e77
Display the URL to update Circuitpython (#124)
* Display the URL to update circuitpython

- Display the URL to circuitpython.org, using the board's ID if available.
- Fix a crash if boot_out.txt contains more than 1 ";" (if boot.py prints some).
- Display a clean error if `boot_out.txt` is missing (can easily happen with --path).

* change internal Bundle.url to the base URL of the bundle

* "install --requirement" allows any path and checks it exists with click.

* "show <match>" forces lower case matching (install already does).
2021-09-06 13:27:25 -07:00
Patrick
cc39336580
special case adafruit_display_button (#123) 2021-08-20 15:56:20 -07:00
dgriswo
350f1ceb04
fix Pimoroni LTR559 install error (#120) 2021-08-20 13:53:23 -07:00
dgriswo
15d634d31a
add BOARDLESS_COMMANDS and allow to run without an attached board (#121) 2021-08-19 16:28:05 -07:00
Scott Shawcroft
908eded30c
Merge pull request #119 from FoamyGuy/circuitpython_org_bundle
adding circuitpython org bundle
2021-08-12 09:27:51 -07:00
foamyguy
91c07550f5 adding circuitpython org bundle 2021-08-12 08:44:21 -05:00
Jeff Epler
b28289ea6d
Merge pull request #118 from jepler/install-auto
Implement `circup install --auto`
2021-08-10 10:06:34 -05:00
Jeff Epler
5076a5dd4f Fix handling of various import styles, add test 2021-08-07 11:34:20 -05:00
Jeff Epler
d075358280 Implement circup install --auto
`--auto` uses the `findimports` module to determine what is used by
code.py (or the filename given by --auto-file=, such as --auto-file=boot.py)

`requirements.txt` is regenerated and its license information has been
detached so that it can later be updated more easily with `pip freeze`.
2021-08-07 08:53:23 -05:00
Neradoc
6dbddb6151
check for requirements in the right place (#115) 2021-07-23 15:07:40 -07:00
Scott Shawcroft
1e32009d29
Merge pull request #116 from lesamouraipourpre/license-rename
Remove space from end of filename
2021-07-14 10:36:58 -07:00
James Carr
ed7a738834 Remove space from end of filename 2021-07-14 13:31:41 +01:00
Neradoc
f797cdd08e
add module names completion for install (#103)
* add module names completion for install

* sort completion suggestions

* add instructions in the readme

* switch to click 8

* update setup.py to click 8

* fix from rebase (missing bundle list)

* try not to update bundles for shell completion
2021-06-27 14:02:30 -07:00
Neradoc
820dc0a7be
handle MPY versions from CP 7.x (#109) 2021-06-27 11:35:17 -07:00
Neradoc
79b4948c9a
Support for community bundle and future bundles (#110)
* switch to a Bundle class, with a global bundles list

* Test for missing directories in ensure_latest_bundle

* docstrings fixes and doc update
2021-06-26 15:33:25 -07:00
dependabot[bot]
cacb685e95
Bump urllib3 from 1.25.8 to 1.26.5 and Requests to 2.*
* Bump urllib3 from 1.25.8 to 1.26.5
* Requests to 2.* to resolve dependency version conflicts

Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.8 to 1.26.5.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.25.8...1.26.5)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* up requests version

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Patrick <4002194+askpatrickw@users.noreply.github.com>
2021-06-03 13:27:31 -07:00
Patrick
17c599c2be
Merge pull request #107 from askpatrickw/add-python-flag-help
improve py flag docs and help
2021-05-08 10:03:19 -07:00
Patrick
31b8badc0d improve py flag docs and help 2021-05-07 18:16:39 -07:00
Patrick
dac1d8ec1d
Merge pull request #105 from adafruit/dependabot/pip/urllib3-1.25.8
Bump urllib3 from 1.25.3 to 1.25.8
2021-05-07 17:40:41 -07:00
dependabot[bot]
101ce4df79
Bump urllib3 from 1.25.3 to 1.25.8
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.3 to 1.25.8.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.25.3...1.25.8)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-30 21:57:31 +00:00
Patrick
751578094b
Merge pull request #102 from adafruit/dependabot/pip/py-1.10.0
Bump py from 1.8.0 to 1.10.0
2021-04-20 13:03:59 -07:00
dependabot[bot]
12b7d9948d
Bump py from 1.8.0 to 1.10.0
Bumps [py](https://github.com/pytest-dev/py) from 1.8.0 to 1.10.0.
- [Release notes](https://github.com/pytest-dev/py/releases)
- [Changelog](https://github.com/pytest-dev/py/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/py/compare/1.8.0...1.10.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-20 19:46:10 +00:00
Melissa LeBlanc-Williams
0d9219baa2
Merge pull request #99 from askpatrickw/add-7
Add support for CP7 MPY bundles
2021-04-08 09:03:00 -07:00
Patrick
a634f227b0 Add support for CP7 MPY bundles 2021-04-08 08:35:25 -07:00
Patrick
de2deb406d
Merge pull request #97 from adafruit/dependabot/pip/pygments-2.7.4
Bump pygments from 2.4.2 to 2.7.4
2021-03-30 09:15:26 -07:00
dependabot[bot]
3f5fb5e0fa
Bump pygments from 2.4.2 to 2.7.4
Bumps [pygments](https://github.com/pygments/pygments) from 2.4.2 to 2.7.4.
- [Release notes](https://github.com/pygments/pygments/releases)
- [Changelog](https://github.com/pygments/pygments/blob/master/CHANGES)
- [Commits](https://github.com/pygments/pygments/compare/2.4.2...2.7.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-29 22:50:36 +00:00
Scott Shawcroft
b7f172f57e
Merge pull request #95 from askpatrickw/fix-docs
Add missing read the docs config
2021-03-29 15:23:27 -07:00
Patrick
db08cc1ebc
Add missing read the docs config
RTD has been failing
https://readthedocs.org/projects/circup/builds/13343939/

Adding this config file which matches the cookiecutter repo
2021-03-26 10:13:51 -07:00
Patrick
4730bc21f5
Merge pull request #92 from adafruit/dependabot/pip/jinja2-2.11.3
Bump jinja2 from 2.10.1 to 2.11.3
2021-03-25 10:07:20 -07:00
Patrick
0ee6cb2aea
Merge pull request #94 from lesamouraipourpre/head-redirect
Reduce bandwidth to github when running CircUp
2021-03-25 10:07:08 -07:00
James Carr
32f1edca69 Replace test_get_latest_tag() with test_get_latest_release_from_url() 2021-03-25 09:55:41 +00:00
James Carr
76c874ee67 Reinstituted get_latest_tag() function so that the tests still pass.
It creates, caches and returns the latest version of the Bundle.
2021-03-25 09:35:32 +00:00
James Carr
8afd84ca71 * Replace the requests.get() calls which are trying to get redirected to a
latest version with a requests.head() call in a new function
  get_latest_release_from_url(url) which will use the 'Location' header and
  parse the end of it as the tag.

* Remove the method get_latest_tag() and replace it with the global
  LATEST_BUNDLE_VERSION, so that it is only retrieved once per run.
2021-03-24 14:52:07 +00:00
dependabot[bot]
3b43540b2a
Bump jinja2 from 2.10.1 to 2.11.3
Bumps [jinja2](https://github.com/pallets/jinja) from 2.10.1 to 2.11.3.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/master/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/2.10.1...2.11.3)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-20 03:35:19 +00:00
Patrick
124f769b56
Merge pull request #87 from askpatrickw/add-badges
Add Badges
2021-02-19 23:14:24 -08:00
Patrick
f76bcb674d Add Badges 2021-02-19 23:08:59 -08:00
Patrick
86c0d0a111
Fix 84 - Apply Cookiecutter style to Circup Repo (#85)
* add pre-commit, remove duplicate redundant from makefile, fix all the lints
* Use pre-commit checks in build action
* Renamed files for REUSE to work in Ubuntu
* Move pre-commit earlier to fail faster
* remove makefile dependency and update docs
2021-02-19 09:19:15 -08:00
Patrick
4b757e8512
Merge pull request #83 from askpatrickw/fix-81
Use requirements from the bundle
2021-02-17 22:26:17 -08:00
Patrick
917f4330ec Use requirements from the bundle 2021-02-17 00:36:27 -08:00
Patrick
8fec75b3a2
Merge pull request #80 from Neradoc/fix-79
Accept version strings with v
2021-02-13 16:04:57 -08:00
Patrick
fa501dad67 get requirements.text from bundle 2021-02-13 15:49:27 -08:00
Neradoc
391eb0cd8c test suite: test for repo and bad major_update 2021-02-11 20:59:03 +01:00
Neradoc
72cc5121cc warn the user about a bad version string and ask for help 2021-02-11 16:55:07 +01:00
Neradoc
9a345131bb fix Module.repo: we don't get the repo from mpy files 2021-02-10 19:32:39 +01:00
Neradoc
141fb2bed6 Don't crash on bad version string, assume major update 2021-02-09 02:55:00 +01:00
Melissa LeBlanc-Williams
23864f25bd
Merge pull request #72 from askpatrickw/fix-71
retrieve default branch via regex
2021-02-05 13:29:04 -08:00
Melissa LeBlanc-Williams
148574e0c9
Merge pull request #77 from adafruit/dependabot/pip/bleach-3.3.0
Bump bleach from 3.1.4 to 3.3.0
2021-02-03 10:12:19 -08:00
dependabot[bot]
65f053fa21
Bump bleach from 3.1.4 to 3.3.0
Bumps [bleach](https://github.com/mozilla/bleach) from 3.1.4 to 3.3.0.
- [Release notes](https://github.com/mozilla/bleach/releases)
- [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES)
- [Commits](https://github.com/mozilla/bleach/compare/v3.1.4...v3.3.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-02 22:39:33 +00:00
Patrick
2e127eb108
Merge pull request #76 from askpatrickw/remove-5x-bundle
Remove 5.x Bundle

I'm going to approve and push this live because it is breaking for folks (this is the second person I've seen mention it today.)
2021-01-31 13:23:42 -08:00
askpatricw
97f2353dc8 remove 5.x and test for 2 not 3 bundles 2021-01-31 12:46:54 -08:00
askpatricw
946544d59c retrieve default branch via regex 2021-01-27 02:39:56 -08:00
Melissa LeBlanc-Williams
a4949b0de8
Merge pull request #69 from askpatrickw/fix-50
Install libraries' dependencies
2021-01-26 17:17:23 -08:00
askpatricw
3dfefd304c make check 2021-01-26 00:21:32 -08:00
askpatricw
2ed81dd5e4 fix #70 2021-01-26 00:21:02 -08:00
askpatricw
4aa4cceff3 make check 2021-01-25 23:05:28 -08:00
askpatricw
71ca170ef0 Handle case of one non-mcu dependency 2021-01-25 23:04:48 -08:00
askpatricw
96e3461740 final fixs and comment cleanups 2021-01-24 14:40:51 -08:00
askpatricw
9ab21c991e some fixes from testing 2021-01-24 00:30:43 -08:00
askpatricw
7370b7fd22 disable pylint too-many-lines 2021-01-23 19:01:22 -08:00
askpatricw
bff126c934 clean up comments, ran make check 2021-01-23 18:58:50 -08:00
askpatricw
f4e0e1e634 recursively check for dependencies and install 2021-01-23 18:53:21 -08:00
askpatricw
1cb74fdae2 clean non-standard library names 2021-01-23 14:48:59 -08:00
askpatricw
8c817b2b2b another revision 2021-01-23 14:31:41 -08:00
askpatricw
eae876aaa0 get_dependencies added 2021-01-23 12:14:51 -08:00
Melissa LeBlanc-Williams
32d3a31859
Merge pull request #67 from askpatrickw/fix-65
Fix 65 - Multi module install
2021-01-21 15:02:39 -08:00
askpatricw
06d8a956c6 multi module install and updated readme 2021-01-20 00:40:13 -08:00
askpatricw
84737a6f37 Pass more than module on install and uninstall 2021-01-20 00:10:08 -08:00
Patrick
eb120d11f9
Merge pull request #64 from askpatrickw/readme-updates
Non functional changes which I am comfortable merging with my own review.
No need for a release with these changes mostly a README update.
2021-01-12 17:14:53 -08:00
askpatricw
3c3a98ff38 CP mispelled 2021-01-12 17:08:36 -08:00
askpatricw
dce9dcc7bf update Readme 2021-01-12 17:05:20 -08:00
Patrick
5eecbdcfcb
Merge pull request #62 from cpforbes/show-match
Add MATCH parameter to the show command to limit the list of modules.
2021-01-06 11:02:41 -08:00
Melissa LeBlanc-Williams
14be02048b
Merge pull request #63 from askpatrickw/black-and-pylint
Black and pylint Suport in dev environment
2021-01-06 11:55:14 -07:00
askpatricw
a4ee697062 remove dependencies add pylint 2021-01-06 00:28:59 -08:00
askpatricw
b713c40181 Change Make to run pylint and black 2021-01-06 00:21:44 -08:00
Jeff Epler
858cb1e7d5
Merge pull request #61 from makermelissa/master
Ignore error caused by bug in shutil.rmtree
2021-01-05 19:37:34 -06:00
Craig Forbes
34841a45fd Add MATCH parameter to show command the limit the list of modules. 2021-01-05 17:33:59 -06:00
Melissa LeBlanc-Williams
e0c9f4febd Update unit test to match change 2021-01-05 14:34:38 -08:00
Melissa LeBlanc-Williams
208a3197ab Ignore error caused by bug in shutil.rmtree 2021-01-05 14:29:16 -08:00
Melissa LeBlanc-Williams
705c738788
Merge pull request #52 from slootsky/feature/echo_versions_on_update_with_verbose
show the device and bundle versions on update when verbose is enabled
2021-01-05 13:55:47 -07:00
Melissa LeBlanc-Williams
de06e729f3
Update circup.py
Updated to make the text a little nicer
2021-01-05 12:53:26 -08:00
Melissa LeBlanc-Williams
c477539f4a
Merge pull request #60 from cpforbes/set-path
Add --path option to support non-standard mount points or multiple devices.
2021-01-04 17:09:09 -07:00
Craig Forbes
42d1f58ffc Reformat with black. 2021-01-04 17:49:48 -06:00
Craig Forbes
0c07ea45ac Update tests for circup.py changes to support --path option.
Update pluggy required version to fix error on python 3.8
2021-01-04 17:05:32 -06:00
Craig Forbes
bb4dbbe1ab Add --path global option to set the mount point 2021-01-04 17:05:27 -06:00
Melissa LeBlanc-Williams
3240add299
Merge pull request #57 from askpatrickw/upgrade-semver
update to latest stable semver
2021-01-04 14:34:54 -07:00
Melissa LeBlanc-Williams
b31e2d6d74
Update circup.py
Removed unused compare
2021-01-04 13:12:57 -08:00
Melissa LeBlanc-Williams
c7e4f8668a
Merge branch 'master' into upgrade-semver 2021-01-04 14:08:58 -07:00
Melissa LeBlanc-Williams
90fdfd3900
Merge pull request #47 from askpatrickw/major-semver
Warn on major version changes
2021-01-04 14:07:16 -07:00
askpatricw
36d770e287 pylint fix 2020-12-30 19:26:56 -08:00
askpatricw
f0319e659d black formatting 2020-12-30 19:23:39 -08:00
askpatricw
1536d611df update to latest stable semver 2020-12-30 19:19:02 -08:00
Patrick
9e2982aed9
Merge pull request #54 from askpatrickw/run-black
CI and local testing passed.
I feel comfortable with these code format changes going to merge.
2020-12-30 18:41:43 -08:00
askpatricw
776e32e2a3 make check now matches CI, re-ran on all files 2020-12-30 18:30:26 -08:00
askpatricw
076fe8e8c2 allow latest version of Black (as Blinka does) 2020-12-30 18:01:49 -08:00
askpatricw
88b2b2de21 Ran make check and added .vscode ignore 2020-12-30 17:52:41 -08:00
Patrick
3ae0f361a2
Merge pull request #51 from slootsky/feature/fix_call_to_make
add make\   to call to make.py
2020-12-29 14:07:43 -08:00
slootsky
38ccbb4342 show the device and bundle versions on update when verbose is enabled 2020-12-23 23:36:19 -05:00
slootsky
3b51e9b018 add make\ to call to make.py 2020-12-23 23:22:08 -05:00
Melissa LeBlanc-Williams
250d6f5cd0
Merge pull request #49 from askpatrickw/fix-version-option
Don't pass version and Click will figure it out
2020-12-21 18:42:49 -07:00
Patrick
a2cd58df1d
Delete settings.json 2020-12-22 01:40:50 +00:00
askpatricw
4e1ada026c Don't pass version and Click will figure it ou 2020-12-21 16:29:09 -08:00
Kattni
47e16a716c
Merge pull request #46 from askpatrickw/use-scm-version
Update setup and add automated semver
2020-12-21 13:30:33 -05:00
askpatricw
43c662a01b fix test and dependency version in setup.py 2020-12-18 12:42:44 -08:00
askpatricw
74ffe6206c one more pylint improvement 2020-12-18 12:03:44 -08:00
askpatricw
25043b6f76 pylint and black 2020-12-18 11:58:39 -08:00
askpatricw
91974d92b7 Warn on major version changes 2020-12-18 11:42:14 -08:00
askpatricw
77842dd4b7 Update setup and add automated semver 2020-12-18 01:02:18 -08:00
Kattni
690e2d469c
Merge pull request #45 from askpatrickw/bump-version-031
Bump version to 0.3.1
2020-12-16 18:25:30 -05:00
askpatricw
48a53dd217 Bump version to 0.3.1 2020-12-16 15:19:04 -08:00
Nicholas Tollervey
dedff0613c
Merge pull request #42 from askpatrickw/lib-directory
create the lib folder if it does not exist on the device
2020-11-06 11:17:34 +00:00
Nicholas Tollervey
f85994a153
Merge pull request #41 from askpatrickw/string-errors
String Errors
2020-11-06 11:16:34 +00:00
Nicholas Tollervey
871987681f
Merge pull request #40 from ntoll/click-update
Bump version of click in package dependencies.
2020-11-06 11:16:12 +00:00
Nicholas H.Tollervey
a70097e9c1
Merge branch 'click-update' of github.com:ntoll/circup into click-update 2020-11-06 09:21:39 +00:00
Nicholas H.Tollervey
983f3bd4ec
Bump version of click in package dependencies. 2020-11-06 09:18:08 +00:00
askpatricw
98711dcb7a create the lib folder if it does not exist on the device 2020-11-05 18:14:42 -08:00
askpatricw
05530914a4 one case 2020-10-23 23:05:28 -07:00
askpatricw
88c4ba5b69 Changed logging-format-style to old 2020-10-23 22:59:26 -07:00
askpatricw
b641d92cf9 Using Ci to test pylint ignore 2020-10-23 15:13:06 -07:00
askpatricw
1397ce664a missed case 2020-10-19 23:21:29 -07:00
askpatricw
78319511f7 update readme 2020-10-18 00:56:51 -07:00
askpatricw
7764bc44c0 logs correctly disable bad pylint check 2020-10-18 00:32:56 -07:00
askpatricw
e79f44b8a6 ran black to format 2020-10-16 17:31:56 -07:00
askpatricw
4003556012 String Errors in 3.9.0 2020-10-16 17:20:58 -07:00
Nicholas H.Tollervey
d79c5d0ce5
Bump version of click in package dependencies. 2020-10-12 09:18:34 +01:00
Dan Halbert
c0c6daa4fe
Merge pull request #38 from adafruit/0.2.2-pypyi-release-force
Bump release again, this time with a PR
2020-08-02 18:52:30 -04:00
Dan Halbert
47d2edd0d4
Bump release again, this time with a PR
A simple push and release did not run release.yml, though I think it should have.
2020-08-02 18:38:05 -04:00
Dan Halbert
8e7b1d40ad had to bump version manually 2020-08-02 16:10:45 -04:00
Dan Halbert
2b8a947a50
Merge pull request #35 from adafruit/5.x-6.x
Update to 5.x and 6.x bundles
2020-08-02 12:47:15 -04:00
Dan Halbert
ac6e544c7e
pylint
Remove implicit string concatenation in assignment.
2020-08-02 11:24:55 -04:00
Dan Halbert
fc78ea66ef
Update to 5.x and 6.x bundles
Fixes #34.
2020-08-02 11:03:27 -04:00
Melissa LeBlanc-Williams
75cba36161
Merge pull request #33 from makermelissa/master
Added Github Actions, Pylinted and Black Formatted.
2020-04-17 14:38:22 -07:00
Melissa LeBlanc-Williams
54d3595208 Removed upload assets job from release 2020-04-17 12:51:20 -07:00
Melissa LeBlanc-Williams
5e7a9db71c Updated Github Actions to run make commands 2020-04-17 12:26:40 -07:00
Melissa LeBlanc-Williams
bd50b2c59c Removed travis and moved make.py to subfolder 2020-04-17 11:41:45 -07:00
Melissa LeBlanc-Williams
c59692f9bd Renamed requirements file for consistency 2020-04-17 11:13:52 -07:00
Melissa LeBlanc-Williams
d1898565d5 Actionified, Black Formatted and Pylinted 2020-04-17 11:08:53 -07:00
Kattni
7e988aad15
Merge pull request #31 from makermelissa/master
Bumped Bleach to 3.1.4 due to Security Vulnerability
2020-04-06 12:07:18 -04:00
Melissa LeBlanc-Williams
e5a4c2951f Bumped Bleach to 3.1.4 due to Security Vulnerability 2020-03-30 14:17:39 -07:00
Nicholas H.Tollervey
ca74ab0d32
Merge branch 'stevenabadie-29' 2020-03-30 15:40:32 +01:00
Nicholas H.Tollervey
60b880ac01
Minor updates to requirements.txt handling (comments) and bumped to version 0.0.8. 2020-03-30 15:40:08 +01:00
Steven Abadie
5d7f385455 Update README with requirements.txt command options
Adds missing install command instructions also. #29
2020-03-27 16:57:24 -06:00
Steven Abadie
9bad77ed58 Add requirements.txt file support for install command
This moves majority of existing install command to separate function and adds check for existing modules already on device. This is only checking against the names of the modules and would require further work to check versions of modules.
2020-03-27 16:14:12 -06:00
Steven Abadie
c4fa165776 Add requirement option to freeze command to save requirements.txt file
Saves output of modules found on connected device to a requirements.txt
file in the current working directory. #29
2020-03-27 16:14:12 -06:00
Nicholas H.Tollervey
4ff8f95661
Bump version to 0.0.7 2020-03-25 08:04:54 +00:00
Nicholas H.Tollervey
836cd2bcd8
Merge branch 'stevenabadie-27' 2020-03-25 07:57:29 +00:00
Nicholas H.Tollervey
719002b34d
Merge branch '27' of https://github.com/stevenabadie/circup into stevenabadie-27 2020-03-25 07:56:21 +00:00
Nicholas H.Tollervey
9d17f600ad
Merge branch 'master' of github.com:adafruit/circup 2020-03-25 07:43:34 +00:00
Steven Abadie
5b841ccc08 Update README with new commands 2020-03-24 20:57:52 -06:00
Steven Abadie
2414b98374 Add uninstall command that removes given module from device
Close #27
2020-03-24 20:57:09 -06:00
Limor "Ladyada" Fried
1c1801f5aa
Merge pull request #26 from adafruit/dependabot/pip/bleach-3.1.2
Bump bleach from 3.1.1 to 3.1.2
2020-03-24 14:48:08 -04:00
dependabot[bot]
d8ade1ba3d
Bump bleach from 3.1.1 to 3.1.2
Bumps [bleach](https://github.com/mozilla/bleach) from 3.1.1 to 3.1.2.
- [Release notes](https://github.com/mozilla/bleach/releases)
- [Changelog](https://github.com/mozilla/bleach/blob/master/CHANGES)
- [Commits](https://github.com/mozilla/bleach/compare/v3.1.1...v3.1.2)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-24 17:39:40 +00:00
Nicholas H.Tollervey
041393ee6b
Appease the code coverage gods. 2020-03-07 19:00:59 +00:00
Nicholas H.Tollervey
15392d971e
Merge branch 'joedevivo-jd/main' 2020-03-07 19:00:00 +00:00
Nicholas H.Tollervey
5b407d2356
Minor PEP8 tidy. ;-) 2020-03-07 18:59:42 +00:00
Joe DeVivo
e2d09b22fa Run circup via python -m circup 2020-03-06 15:24:18 -07:00
Melissa LeBlanc-Williams
0bb8167090 Bumped version for release 2020-02-24 11:29:37 -08:00
Limor "Ladyada" Fried
4491a187a2
bump for security fix 2020-02-24 14:18:06 -05:00
Melissa LeBlanc-Williams
a091ecc471 Bumped for version release 2019-10-31 09:22:47 -07:00
Melissa LeBlanc-Williams
bc2daa2322
Merge pull request #21 from ntoll/github-collywobbles
Handle HTTP Error codes when ensuring bundle update.
2019-10-31 09:13:36 -07:00
Nicholas H.Tollervey
75e4ba52f5
Fixes #20. 2019-10-30 08:43:02 +00:00
Melissa LeBlanc-Williams
024a8bc0cb bumped version for release 2019-10-07 07:55:23 -07:00
Nicholas H.Tollervey
c7173defe3
Add --py flag to specify installation of Python source modules, otherwise use .mpy modules as default. Add a friendly confirmation message upon installation. 2019-10-07 14:40:57 +01:00
Nicholas H.Tollervey
0b47132aa6
Merge branch 'master' into install 2019-10-06 18:18:34 +01:00
Nicholas H.Tollervey
bf89ddaebf
Newline separated list of available packages. 2019-10-06 18:17:11 +01:00
Melissa LeBlanc-Williams
e6c79c6a36
Merge pull request #18 from sommersoft/pypi_test
Add CHANGES.rst To PyPI Manifest
2019-10-04 15:12:49 -07:00
sommersoft
703e0ab74f __version__ bump, with changes documented. 2019-10-04 17:06:22 -05:00
sommersoft
17def793e1 force 'CHANGES.rst' to be included in the PyPI package 2019-10-04 16:49:01 -05:00
Nicholas H.Tollervey
008596ee3b
Merge branch 'master' into install 2019-10-02 11:49:31 +01:00
Nicholas H.Tollervey
1335132ac7
Merge branch 'master' into install 2019-09-06 16:50:25 +01:00
Nicholas H.Tollervey
315e2eb9eb
Add an install command proof-of-concept. ;-) 2019-09-06 16:37:06 +01:00
Nicholas H.Tollervey
5dce7135aa
Fix merge conflict 2019-09-06 15:59:13 +01:00
Nicholas H.Tollervey
a46093a0e8
Add show command 2019-09-06 15:07:49 +01:00
85 changed files with 6259 additions and 1737 deletions

50
.github/ISSUE_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,50 @@
<!-- SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
SPDX-License-Identifier: MIT -->
Thank you for opening an issue on an Adafruit Python library repository. To
improve the speed of resolution please review the following guidelines and
common troubleshooting steps below before creating the issue:
- **Do not use GitHub issues for troubleshooting projects and issues.** Instead use
the forums at http://forums.adafruit.com to ask questions and troubleshoot why
something isn't working as expected. In many cases the problem is a common issue
that you will more quickly receive help from the forum community. GitHub issues
are meant for known defects in the code. If you don't know if there is a defect
in the code then start with troubleshooting on the forum first.
- **If following a tutorial or guide be sure you didn't miss a step.** Carefully
check all of the steps and commands to run have been followed. Consult the
forum if you're unsure or have questions about steps in a guide/tutorial.
- **For Python/Raspberry Pi projects check these very common issues to ensure they don't apply**:
- If you are receiving an **ImportError: No module named...** error then a
library the code depends on is not installed. Check the tutorial/guide or
README to ensure you have installed the necessary libraries. Usually the
missing library can be installed with the `pip` tool, but check the tutorial/guide
for the exact command.
- **Be sure you are supplying adequate power to the board.** Check the specs of
your board and power in an external power supply. In many cases just
plugging a board into your computer is not enough to power it and other
peripherals.
- **Double check all soldering joints and connections.** Flakey connections
cause many mysterious problems. See the [guide to excellent soldering](https://learn.adafruit.com/adafruit-guide-excellent-soldering/tools) for examples of good solder joints.
If you're sure this issue is a defect in the code and checked the steps above
please fill in the following fields to provide enough troubleshooting information.
You may delete the guideline and text above to just leave the following details:
- Platform/operating system (i.e. Raspberry Pi with Raspbian operating system,
Windows 32-bit, Windows 64-bit, Mac OSX 64-bit, etc.): **INSERT PLATFORM/OPERATING
SYSTEM HERE**
- Python version (run `python -version` or `python3 -version`): **INSERT PYTHON
VERSION HERE**
- Error message you are receiving, including any Python exception traces: **INSERT
ERROR MESAGE/EXCEPTION TRACES HERE***
- List the steps to reproduce the problem below (if possible attach code or commands
to run): **LIST REPRO STEPS BELOW**

29
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,29 @@
<!-- SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
SPDX-License-Identifier: MIT -->
Thank you for creating a pull request to contribute to Adafruit's GitHub code!
Before you open the request please review the following guidelines and tips to
help it be more easily integrated:
- **Describe the scope of your change--i.e. what the change does and what parts
of the code were modified.** This will help us understand any risks of integrating
the code.
- **Describe any known limitations with your change.** For example if the change
doesn't apply to a supported platform of the library please mention it.
- **Please run any tests or examples that can exercise your modified code.** We
strive to not break users of the code and running tests/examples helps with this
process.
Thank you again for contributing! We will try to test and integrate the change
as soon as we can, but be aware we have many GitHub repositories to manage and
can't immediately respond to every request. There is no need to bump or check in
on a pull request (it will clutter the discussion of the request).
Also don't be worried if the request is closed or not integrated--sometimes the
priorities of Adafruit's GitHub code (education, ease of use) might not match the
priorities of the pull request. Don't fret, the open source community thrives on
forks and GitHub makes it easy to keep your changes in a forked repo.
After reviewing the guidelines above you can delete this text from the pull request.

57
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,57 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
# SPDX-License-Identifier: MIT
name: Build CI
on: [pull_request, push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Dump GitHub context
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
run: echo "$GITHUB_CONTEXT"
- name: Translate Repo Name For Build Tools filename_prefix
id: repo-name
run: echo "repo-name=circup" >> $GITHUB_OUTPUT
- name: Set up Python 3.11
uses: actions/setup-python@v1
with:
python-version: 3.11
- name: Pip install Sphinx & pre-commit
run: |
pip install --force-reinstall Sphinx sphinx-rtd-theme pre-commit
- name: Versions
run: |
python3 --version
pre-commit --version
- name: Checkout Current Repo
uses: actions/checkout@v4
with:
submodules: true
show-progress: false
- name: Library version
run: git describe --dirty --always --tags
- name: Pre-commit hooks
run: |
pre-commit run --all-files
- name: Checkout tools repo
uses: actions/checkout@v4
with:
repository: adafruit/actions-ci-circuitpython-libs
path: actions-ci
show-progress: false
- name: Install dependencies
# (e.g. - apt-get: gettext, etc; pip: circuitpython-build-tools, requirements.txt; etc.)
run: |
source actions-ci/install.sh
- name: Run Test Suite
run: |
pytest --random-order --cov-config .coveragerc --cov-report term-missing --cov=circup
- name: Build docs
working-directory: docs
run: |
sphinx-build -E -W -b html . _build/html

34
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,34 @@
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
# SPDX-FileCopyrightText: 2021 James Carr
#
# SPDX-License-Identifier: MIT
name: Release Actions
on:
release:
types: [published]
jobs:
upload-pypi:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
filter: 'blob:none'
depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build setuptools wheel twine
- name: Build and publish
env:
TWINE_USERNAME: ${{ secrets.pypi_username }}
TWINE_PASSWORD: ${{ secrets.pypi_password }}
run: |
python -m build
twine upload dist/*

14
.gitignore vendored
View file

@ -1,3 +1,7 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@ -15,6 +19,7 @@ downloads/
eggs/
.eggs/
lib/
!tests/mock_device/lib/
lib64/
parts/
sdist/
@ -89,6 +94,7 @@ venv/
ENV/
env.bak/
venv.bak/
*_venv/
# Spyder project settings
.spyderproject
@ -105,3 +111,11 @@ venv.bak/
# vim
*.swp
# VSCode
.vscode/
.DS_STORE
# emacs
*~

5
.isort.cfg Normal file
View file

@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2023 Vladimír Kotal
#
# SPDX-License-Identifier: Unlicense
[settings]
profile = black

33
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: 2020 Diego Elio Pettenò
#
# SPDX-License-Identifier: Unlicense
repos:
- repo: https://github.com/python/black
rev: 22.3.0
hooks:
- id: black
exclude: "^tests/bad_python.py$"
- repo: https://github.com/pycqa/pylint
rev: v3.1.0
hooks:
- id: pylint
name: lint (examples)
types: [python]
files: ^examples/
args:
- --disable=missing-docstring,invalid-name,bad-whitespace
- id: pylint
name: lint (code)
types: [python]
exclude: "^(docs/|examples/|setup.py$|tests/bad_python.py$)"
- repo: https://github.com/fsfe/reuse-tool
rev: v0.14.0
hooks:
- id: reuse
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.2.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
- id: trailing-whitespace

399
.pylintrc Normal file
View file

@ -0,0 +1,399 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
[MASTER]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# Add files or directories to the ignore-list. They should be base names, not
# paths.
ignore=CVS
# Add files or directories matching the regex patterns to the ignore-list. The
# regex matches against base names, not paths.
ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint.
# jobs=1
jobs=2
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Specify a configuration file.
#rcfile=
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=too-many-lines, consider-using-f-string, use-dict-literal, global-statement, invalid-name, fixme, import-error
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=
[REPORTS]
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio).You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages
reports=no
# Activate the evaluation score.
score=yes
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=5
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
logging-format-style=old
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
# notes=FIXME,XXX,TODO
notes=FIXME,XXX
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,future.builtins
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
# expected-line-ending-format=
expected-line-ending-format=LF
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Maximum number of characters on a single line.
max-line-length=100
# Maximum number of lines in a module
max-module-lines=1000
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[SIMILARITIES]
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=yes
# Minimum lines number of a similarity.
min-similarity-lines=8
[BASIC]
# Regular expression matching correct argument names
argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Regular expression matching correct attribute names
attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct class names
# class-rgx=[A-Z_][a-zA-Z0-9]+$
class-rgx=[A-Z_][a-zA-Z0-9_]+$
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Regular expression matching correct function names
function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Good variable names which should always be accepted, separated by a comma
# good-names=i,j,k,ex,Run,_
good-names=r,g,b,w,i,j,k,n,x,y,z,ex,ok,Run,_
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression matching correct method names
method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty
# Regular expression matching correct variable names
variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
[IMPORTS]
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=optparse,tkinter.tix
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Maximum number of attributes for a class (see R0902).
# max-attributes=7
max-attributes=11
# Maximum number of boolean expressions in a if statement
max-bool-expr=5
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of statements in function / method body
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=1
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception

View file

@ -1,38 +0,0 @@
# This is a common .travis.yml for generating library release zip files for
# CircuitPython library releases using circuitpython-build-tools.
# See https://github.com/adafruit/circuitpython-build-tools for detailed setup
# instructions.
dist: xenial
language: python
python:
- "3.6"
cache:
pip: true
env:
- DEPLOY_PYPI="true"
deploy:
- provider: releases
api_key: "$GITHUB_TOKEN"
file_glob: true
file: "$TRAVIS_BUILD_DIR/bundles/*"
skip_cleanup: true
overwrite: true
on:
tags: true
- provider: pypi
user: adafruit-travis
password:
secure: b1GKsTNzHzDPf7mwl4euv8YkTrBXoURbfu2zpCJdGzDdKnl0iQMzZQkzwobB+6PXt2uTEEy47460rmVifzsXCyiQ+UtVW6SulL5h4ju3tWExJqB/k0Fp4EHakONdg1bKiIbNX2KNPGz3FK/EyuyhNGLgw2BVOfWUnpFVrlPGAtXR5u6pFMIRM4oN80yFJhPrNYCv2MEzLFllwTnl7GlPp/A1UXnJbouofiVr9Y7MbVmGw+CiprBNXUQVycj49MwGdX2aiQdmVwE25ODAI5AgH2TKTgHNNmlpR9fDcqo1HNqqXubsnT8XwChK5TvogQ32kM7aPK5Tt6+JnZ7eM4LH3gIotOnxbQ0LyhXwhZequqd/dcpoHv+3C/Ok32wEehEQ2EWyXllOykFOZEdzebNSTHBiXZwmE1eIumhncqk4BwzKxUFmKZwv9/N0tgG0UZAVa34DLqSYVjOIKkAqYLyrQOouT8F8uwaP55x4jJt560K86iVaLZbLE2GRCTT3vInfoH/9LauAAGqqsDHsv1bxHrwNaruT18x668sNEVgY+varPusR0Ppn837o/u9FCpFBLxP7o3aGyeKQGShsxDCOBNTha4U1IzJDf/fOG9nluwWOXcTmtOyiRQpZ+RAIqcuAKwxW3Gwl83C3VzT8joMJwlUHebaXySbi/qRCr1KG/5M=
on:
tags: true
condition: $DEPLOY_PYPI = "true"
install:
- pip install -e ".[dev]"
script:
- make check

View file

@ -1,17 +0,0 @@
Release History
===============
0.0.1
-----
Initial release.
* Core project scaffolding.
* ``circup freeze`` - lists version details for all modules found on the
connected CIRCUITPYTHON device.
* ``circup list`` - lists all modules requiring an update found on the the
connected CIRCUITPYTHON device.
* ``circup update`` - interactively update out-of-date modules found on the
connected CIRCUITPYTHON device.
* 100% test coverage.
* Documentation.

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

View file

@ -18,3 +18,125 @@ If you have an employment contract with your employer please make sure that
they don't automatically own your work product. Make sure to get any necessary
approvals before contributing. Another term for this contribution off-hours is
moonlighting.
Developer Setup
---------------
.. note::
Please try to use Python 3.9+ while developing Circup. This is so we can
use the
`Black code formatter <https://black.readthedocs.io/en/stable/index.html>`_
and so that we're supporting versions which still receive security updates.
Clone the repository and from the root of the project,
If you'd like you can setup a virtual environment and activate it.::
python3 -m venv .env
source .env/bin/activate
install the development requirements::
pip install -r optional_requirements.txt
Run the test suite::
pytest --random-order --cov-config .coveragerc --cov-report term-missing --cov=circup
How Does Circup Work?
#####################
The ``circup`` tool checks for a connected CircuitPython device by
interrogating the local filesystem to find a path to a directory which ends
with ``"CIRCUITPYTHON"`` (the name under which a CircuitPython device is
mounted by the host operating system). This is handled in the ``find_device``
function.
A Python module on a connected device is represented by an instance of the
``Module`` class. This class provides useful methods for discerning if the
module is out of date, returning useful representations of it in order to
display information to the user, or updating the module on the connected
device with whatever the version is in the latest Adafruit CircuitPython
Bundle.
All of the libraries included in the Adafruit CircuitPython Bundle contain,
somewhere within their code, two metadata objects called ``__version__`` and
``__repo__``.
The ``__repo__`` object is a string containing the GitHub repository URL, as
used to clone the project.
The ``__version__`` object is interesting because *within the source code in
Git* the value is **always** the string ``"0.0.0-auto.0"``. When a new release
is made of the bundle, this value is automatically replaced by the build
scripts to the correct version information, which will always conform to the
`semver standard <https://semver.org/>`_.
Given this context, the ``circup`` tool will check a configuration file
to discern what *it* thinks is the latest version of the bundle. If there is
no configuration file (for example, on first run), then the bundle version is
assumed to be ``"0"``.
Next, it checks GitHub for the tag value (denoting the version) of the very
latest bundle release. Bundle versions are based upon the date of release, for
instance ``"20190904"``. If the latest version on GitHub is later than the
version ``circup`` currently has, then the latest version of the bundle
is automatically downloaded and cached away somewhere.
In this way, the ``circup`` tool is able to have available to it both a path
to a connected CIRCUITPYTHON devce and a copy of the latest version, including
the all important version information, of the Adafruit CircuitPython Bundle.
Exactly the same function (``get_modules``) is used to extract the metadata
from the modules on both the connected device and in the bundle cache. This
metadata is used to instantiate instances of the ``Module`` class which is
subsequently used to facilitate the various commands the tool makes available.
These commands are defined at the very end of the ``circup.py`` code.
Unit tests can be found in the ``tests`` directory. Circup uses
`pytest <http://www.pytest.org/en/latest/>`_ style testing conventions. Test
functions should include a comment to describe its *intention*. We currently
have 100% unit test coverage for all the core functionality (excluding
functions used to define the CLI commands).
To run the full test suite, type::
pytest --random-order --cov-config .coveragerc --cov-report term-missing --cov=circup
All code is formatted using the stylistic conventions enforced by
`black <https://black.readthedocs.io/en/stable/>`_. Python coding standard are
enforced by Pylint and verification of licensing is handled by REUSE. All of these
are run using pre-commit, which you can run by using::
pip install pre-commit
pre-commit run --all-files
Please see the output from ``pre-commit`` for more information about the various
available options to help you work with the code base.
Before submitting a PR, please remember to ``pre-commit run --all-files``.
But if you forget the CI process in Github will run it for you. ;-)
Circup uses the `Click <https://click.palletsprojects.com>`_ module to
run command-line interaction. The
`AppDirs <https://pypi.org/project/appdirs/>`_ module is used to determine
where to store user-specific assets created by the tool in such a way that
meets the host operating system's usual conventions. The
`python-semver <https://github.com/k-bx/python-semver>`_ package is used to
validate and compare the semver values associated with modules. The ubiquitous
`requests <http://python-requests.org/>`_ module is used for HTTP activity.
Documentation, generated by `Sphinx <http://www.sphinx-doc.org/en/master/>`_,
is based on this README and assembled by assets in the ``doc`` subdirectory.
The latest version of the docs will be found on
`Read the Docs <https://circup.readthedocs.io/>`_.
Discussion of this tool happens on the Adafruit CircuitPython
`Discord channel <https://discord.gg/rqrKDjU>`_.

3
CONTRIBUTING.rst.license Normal file
View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

324
LICENSES/CC-BY-4.0.txt Normal file
View file

@ -0,0 +1,324 @@
Creative Commons Attribution 4.0 International Creative Commons Corporation
("Creative Commons") is not a law firm and does not provide legal services
or legal advice. Distribution of Creative Commons public licenses does not
create a lawyer-client or other relationship. Creative Commons makes its licenses
and related information available on an "as-is" basis. Creative Commons gives
no warranties regarding its licenses, any material licensed under their terms
and conditions, or any related information. Creative Commons disclaims all
liability for damages resulting from their use to the fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and conditions
that creators and other rights holders may use to share original works of
authorship and other material subject to copyright and certain other rights
specified in the public license below. The following considerations are for
informational purposes only, are not exhaustive, and do not form part of our
licenses.
Considerations for licensors: Our public licenses are intended for use by
those authorized to give the public permission to use material in ways otherwise
restricted by copyright and certain other rights. Our licenses are irrevocable.
Licensors should read and understand the terms and conditions of the license
they choose before applying it. Licensors should also secure all rights necessary
before applying our licenses so that the public can reuse the material as
expected. Licensors should clearly mark any material not subject to the license.
This includes other CC-licensed material, or material used under an exception
or limitation to copyright. More considerations for licensors : wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: By using one of our public licenses, a licensor
grants the public permission to use the licensed material under specified
terms and conditions. If the licensor's permission is not necessary for any
reasonfor example, because of any applicable exception or limitation to copyrightthen
that use is not regulated by the license. Our licenses grant only permissions
under copyright and certain other rights that a licensor has authority to
grant. Use of the licensed material may still be restricted for other reasons,
including because others have copyright or other rights in the material. A
licensor may make special requests, such as asking that all changes be marked
or described. Although not required by our licenses, you are encouraged to
respect those requests where reasonable. More considerations for the public
: wiki.creativecommons.org/Considerations_for_licensees Creative Commons Attribution
4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree to
be bound by the terms and conditions of this Creative Commons Attribution
4.0 International Public License ("Public License"). To the extent this Public
License may be interpreted as a contract, You are granted the Licensed Rights
in consideration of Your acceptance of these terms and conditions, and the
Licensor grants You such rights in consideration of benefits the Licensor
receives from making the Licensed Material available under these terms and
conditions.
Section 1 Definitions.
a. Adapted Material means material subject to Copyright and Similar Rights
that is derived from or based upon the Licensed Material and in which the
Licensed Material is translated, altered, arranged, transformed, or otherwise
modified in a manner requiring permission under the Copyright and Similar
Rights held by the Licensor. For purposes of this Public License, where the
Licensed Material is a musical work, performance, or sound recording, Adapted
Material is always produced where the Licensed Material is synched in timed
relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright and Similar
Rights in Your contributions to Adapted Material in accordance with the terms
and conditions of this Public License.
c. Copyright and Similar Rights means copyright and/or similar rights closely
related to copyright including, without limitation, performance, broadcast,
sound recording, and Sui Generis Database Rights, without regard to how the
rights are labeled or categorized. For purposes of this Public License, the
rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
d. Effective Technological Measures means those measures that, in the absence
of proper authority, may not be circumvented under laws fulfilling obligations
under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996,
and/or similar international agreements.
e. Exceptions and Limitations means fair use, fair dealing, and/or any other
exception or limitation to Copyright and Similar Rights that applies to Your
use of the Licensed Material.
f. Licensed Material means the artistic or literary work, database, or other
material to which the Licensor applied this Public License.
g. Licensed Rights means the rights granted to You subject to the terms and
conditions of this Public License, which are limited to all Copyright and
Similar Rights that apply to Your use of the Licensed Material and that the
Licensor has authority to license.
h. Licensor means the individual(s) or entity(ies) granting rights under this
Public License.
i. Share means to provide material to the public by any means or process that
requires permission under the Licensed Rights, such as reproduction, public
display, public performance, distribution, dissemination, communication, or
importation, and to make material available to the public including in ways
that members of the public may access the material from a place and at a time
individually chosen by them.
j. Sui Generis Database Rights means rights other than copyright resulting
from Directive 96/9/EC of the European Parliament and of the Council of 11
March 1996 on the legal protection of databases, as amended and/or succeeded,
as well as other essentially equivalent rights anywhere in the world.
k. You means the individual or entity exercising the Licensed Rights under
this Public License. Your has a corresponding meaning.
Section 2 Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License, the Licensor
hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive,
irrevocable license to exercise the Licensed Rights in the Licensed Material
to:
A. reproduce and Share the Licensed Material, in whole or in part; and
B. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt, where Exceptions
and Limitations apply to Your use, this Public License does not apply, and
You do not need to comply with its terms and conditions.
3. Term. The term of this Public License is specified in Section 6(a).
4. Media and formats; technical modifications allowed. The Licensor authorizes
You to exercise the Licensed Rights in all media and formats whether now known
or hereafter created, and to make technical modifications necessary to do
so. The Licensor waives and/or agrees not to assert any right or authority
to forbid You from making technical modifications necessary to exercise the
Licensed Rights, including technical modifications necessary to circumvent
Effective Technological Measures. For purposes of this Public License, simply
making modifications authorized by this Section 2(a)(4) never produces Adapted
Material.
5. Downstream recipients.
A. Offer from the Licensor Licensed Material. Every recipient of the Licensed
Material automatically receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this Public License.
B. No downstream restrictions. You may not offer or impose any additional
or different terms or conditions on, or apply any Effective Technological
Measures to, the Licensed Material if doing so restricts exercise of the Licensed
Rights by any recipient of the Licensed Material.
6. No endorsement. Nothing in this Public License constitutes or may be construed
as permission to assert or imply that You are, or that Your use of the Licensed
Material is, connected with, or sponsored, endorsed, or granted official status
by, the Licensor or others designated to receive attribution as provided in
Section 3(a)(1)(A)(i).
b. Other rights.
1. Moral rights, such as the right of integrity, are not licensed under this
Public License, nor are publicity, privacy, and/or other similar personality
rights; however, to the extent possible, the Licensor waives and/or agrees
not to assert any such rights held by the Licensor to the limited extent necessary
to allow You to exercise the Licensed Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this Public License.
3. To the extent possible, the Licensor waives any right to collect royalties
from You for the exercise of the Licensed Rights, whether directly or through
a collecting society under any voluntary or waivable statutory or compulsory
licensing scheme. In all other cases the Licensor expressly reserves any right
to collect such royalties.
Section 3 License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the following
conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified form), You must:
A. retain the following if it is supplied by the Licensor with the Licensed
Material:
i. identification of the creator(s) of the Licensed Material and any others
designated to receive attribution, in any reasonable manner requested by the
Licensor (including by pseudonym if designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of warranties;
v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
B. indicate if You modified the Licensed Material and retain an indication
of any previous modifications; and
C. indicate the Licensed Material is licensed under this Public License, and
include the text of, or the URI or hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner
based on the medium, means, and context in which You Share the Licensed Material.
For example, it may be reasonable to satisfy the conditions by providing a
URI or hyperlink to a resource that includes the required information.
3. If requested by the Licensor, You must remove any of the information required
by Section 3(a)(1)(A) to the extent reasonably practicable.
4. If You Share Adapted Material You produce, the Adapter's License You apply
must not prevent recipients of the Adapted Material from complying with this
Public License.
Section 4 Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that apply to
Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract,
reuse, reproduce, and Share all or a substantial portion of the contents of
the database;
b. if You include all or a substantial portion of the database contents in
a database in which You have Sui Generis Database Rights, then the database
in which You have Sui Generis Database Rights (but not its individual contents)
is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share all or
a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not replace
Your obligations under this Public License where the Licensed Rights include
other Copyright and Similar Rights.
Section 5 Disclaimer of Warranties and Limitation of Liability.
a. Unless otherwise separately undertaken by the Licensor, to the extent possible,
the Licensor offers the Licensed Material as-is and as-available, and makes
no representations or warranties of any kind concerning the Licensed Material,
whether express, implied, statutory, or other. This includes, without limitation,
warranties of title, merchantability, fitness for a particular purpose, non-infringement,
absence of latent or other defects, accuracy, or the presence or absence of
errors, whether or not known or discoverable. Where disclaimers of warranties
are not allowed in full or in part, this disclaimer may not apply to You.
b. To the extent possible, in no event will the Licensor be liable to You
on any legal theory (including, without limitation, negligence) or otherwise
for any direct, special, indirect, incidental, consequential, punitive, exemplary,
or other losses, costs, expenses, or damages arising out of this Public License
or use of the Licensed Material, even if the Licensor has been advised of
the possibility of such losses, costs, expenses, or damages. Where a limitation
of liability is not allowed in full or in part, this limitation may not apply
to You.
c. The disclaimer of warranties and limitation of liability provided above
shall be interpreted in a manner that, to the extent possible, most closely
approximates an absolute disclaimer and waiver of all liability.
Section 6 Term and Termination.
a. This Public License applies for the term of the Copyright and Similar Rights
licensed here. However, if You fail to comply with this Public License, then
Your rights under this Public License terminate automatically.
b. Where Your right to use the Licensed Material has terminated under Section
6(a), it reinstates:
1. automatically as of the date the violation is cured, provided it is cured
within 30 days of Your discovery of the violation; or
2. upon express reinstatement by the Licensor.
c. For the avoidance of doubt, this Section 6(b) does not affect any right
the Licensor may have to seek remedies for Your violations of this Public
License.
d. For the avoidance of doubt, the Licensor may also offer the Licensed Material
under separate terms or conditions or stop distributing the Licensed Material
at any time; however, doing so will not terminate this Public License.
e. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
Section 7 Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different terms or
conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the Licensed
Material not stated herein are separate from and independent of the terms
and conditions of this Public License.
Section 8 Interpretation.
a. For the avoidance of doubt, this Public License does not, and shall not
be interpreted to, reduce, limit, restrict, or impose conditions on any use
of the Licensed Material that could lawfully be made without permission under
this Public License.
b. To the extent possible, if any provision of this Public License is deemed
unenforceable, it shall be automatically reformed to the minimum extent necessary
to make it enforceable. If the provision cannot be reformed, it shall be severed
from this Public License without affecting the enforceability of the remaining
terms and conditions.
c. No term or condition of this Public License will be waived and no failure
to comply consented to unless expressly agreed to by the Licensor.
d. Nothing in this Public License constitutes or may be interpreted as a limitation
upon, or waiver of, any privileges and immunities that apply to the Licensor
or You, including from the legal processes of any jurisdiction or authority.
Creative Commons is not a party to its public licenses. Notwithstanding, Creative
Commons may elect to apply one of its public licenses to material it publishes
and in those instances will be considered the "Licensor." The text of the
Creative Commons public licenses is dedicated to the public domain under the
CC0 Public Domain Dedication. Except for the limited purpose of indicating
that material is shared under a Creative Commons public license or as otherwise
permitted by the Creative Commons policies published at creativecommons.org/policies,
Creative Commons does not authorize the use of the trademark "Creative Commons"
or any other trademark or logo of Creative Commons without its prior written
consent including, without limitation, in connection with any unauthorized
modifications to any of its public licenses or any other arrangements, understandings,
or agreements concerning use of licensed material. For the avoidance of doubt,
this paragraph does not form part of the public licenses.
Creative Commons may be contacted at creativecommons.org.

19
LICENSES/MIT.txt Normal file
View file

@ -0,0 +1,19 @@
MIT License Copyright (c) <year> <copyright holders>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

20
LICENSES/Unlicense.txt Normal file
View file

@ -0,0 +1,20 @@
This is free and unencumbered software released into the public domain.
Anyone is free to copy, modify, publish, use, compile, sell, or distribute
this software, either in source code form or as a compiled binary, for any
purpose, commercial or non-commercial, and by any means.
In jurisdictions that recognize copyright laws, the author or authors of this
software dedicate any and all copyright interest in the software to the public
domain. We make this dedication for the benefit of the public at large and
to the detriment of our heirs and successors. We intend this dedication to
be an overt act of relinquishment in perpetuity of all present and future
rights to this software under copyright law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. For more information,
please refer to <https://unlicense.org/>

View file

@ -1,68 +0,0 @@
XARGS := xargs -0 $(shell test $$(uname) = Linux && echo -r)
GREP_T_FLAG := $(shell test $$(uname) = Linux && echo -T)
all:
@echo "\nThere is no default Makefile target right now. Try:\n"
@echo "make clean - reset the project and remove auto-generated assets."
@echo "make pyflakes - run the PyFlakes code checker."
@echo "make pycodestyle - run the PEP8 style checker."
@echo "make test - run the test suite."
@echo "make coverage - view a report on test coverage."
@echo "make tidy - tidy code with the 'black' formatter."
@echo "make check - run all the checkers and tests."
@echo "make dist - make a dist/wheel for the project."
@echo "make publish-test - publish the project to PyPI test instance."
@echo "make publish-live - publish the project to PyPI production."
@echo "make docs - run sphinx to create project documentation.\n"
clean:
rm -rf build
rm -rf dist
rm -rf .coverage
rm -rf .eggs
rm -rf .pytest_cache
rm -rf .tox
rm -rf docs/_build
find . \( -name '*.py[co]' -o -name dropin.cache \) -delete
find . \( -name '*.bak' -o -name dropin.cache \) -delete
find . \( -name '*.tgz' -o -name dropin.cache \) -delete
find . | grep -E "(__pycache__)" | xargs rm -rf
pyflakes:
# search the current directory tree for .py files, skipping docs and _build, var directories, feeding them to pyflakes
find . \( -name _build -o -name var -o -path ./docs -o -name .env \) -type d -prune -o -name '*.py' -print0 | $(XARGS) pyflakes
pycodestyle:
# search the current directory tree for .py files, skipping _build and var directories, feeding them to pycodestyle
find . \( -name _build -o -name var -o -name .env \) -type d -prune -o -name '*.py' -print0 | $(XARGS) -n 1 pycodestyle --repeat --exclude=docs/*,.vscode/* --ignore=E731,E402,W504,W503
test: clean
pytest --random-order
coverage: clean
pytest --random-order --cov-config .coveragerc --cov-report term-missing --cov=circup tests/
tidy: clean
@echo "\nTidying code with black..."
black -l 79 circup.py
black -l 79 tests
check: clean tidy pycodestyle pyflakes coverage
dist: check
@echo "\nChecks pass, good to package..."
python setup.py sdist bdist_wheel
publish-test: dist
@echo "\nPackaging complete... Uploading to PyPi..."
twine upload -r test --sign dist/*
publish-live: dist
@echo "\nPackaging complete... Uploading to PyPi..."
twine upload --sign dist/*
docs: clean
$(MAKE) -C docs html
@echo "\nDocumentation can be found here:"
@echo file://`pwd`/docs/_build/html/index.html
@echo "\n"

View file

@ -1,6 +1,26 @@
CircUp
Circup
======
.. image:: https://readthedocs.org/projects/circup/badge/?version=latest
:target: https://circuitpython.readthedocs.io/projects/circup/en/latest/
:alt: Documentation Status
.. image:: https://img.shields.io/discord/327254708534116352.svg
:target: https://adafru.it/discord
:alt: Discord
.. image:: https://github.com/adafruit/circup/workflows/Build%20CI/badge.svg
:target: https://github.com/adafruit/circup/actions
:alt: Build Status
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
:alt: Code Style: Black
A tool to manage and update libraries (modules) on a CircuitPython device.
.. contents::
@ -8,7 +28,7 @@ A tool to manage and update libraries (modules) on a CircuitPython device.
Installation
------------
Circup requires Python 3.5 or higher.
Circup requires Python 3.9 or higher.
In a `virtualenv <https://virtualenv.pypa.io/en/latest/>`_,
``pip install circup`` should do the trick. This is the simplest way to make it
@ -19,7 +39,7 @@ If you have no idea what a virtualenv is, try the following command,
.. note::
If you use the ``pip3`` command to install CircUp you must make sure that
If you use the ``pip3`` command to install Circup you must make sure that
your path contains the directory into which the script will be installed.
To discover this path,
@ -28,15 +48,15 @@ If you have no idea what a virtualenv is, try the following command,
* On Windows, type the same command, but append ``Scripts`` to the
resulting path.
What?
-----
What does Circup Do?
--------------------
Each CircuitPython library on the device (``.py``, *NOT* ``.mpy`` at this time)
usually has a version number as metadata within the module.
Each CircuitPython library on the device usually has a version number as
metadata within the module.
This utility looks at all the libraries on the device and checks if they are
the most recent (compared to the versions found in the most recent version of
the Adafruit CircuitPython Bundle). If the libraries are out of date, the
the Adafruit CircuitPython Bundle and Circuitpython Community Bundle). If the libraries are out of date, the
utility helps you update them.
The Adafruit CircuitPython Bundle can be found here:
@ -48,11 +68,18 @@ found here:
https://circuitpython.org/libraries
The Circuitpython Community Bundle can be found here:
https://github.com/adafruit/CircuitPython_Community_Bundle/releases/latest
Usage
-----
If you need more detailed help using Circup see the Learn Guide article
`"Use Circup to easily keep your CircuitPython libraries up to date" <https://learn.adafruit.com/keep-your-circuitpython-libraries-on-devices-up-to-date-with-circup/>`_.
First, plug in a device running CircuiPython. This should appear as a mounted
storage device called ``CIRCUITPYTHON``.
storage device called ``CIRCUITPY``.
To get help, just type the command::
@ -62,15 +89,57 @@ To get help, just type the command::
A tool to manage and update libraries on a CircuitPython device.
Options:
--verbose Comprehensive logging is sent to stdout.
--version Show the version and exit.
--help Show this message and exit.
--verbose Comprehensive logging is sent to stdout.
--path DIRECTORY Path to CircuitPython directory. Overrides automatic
path detection.
--host TEXT Hostname or IP address of a device. Overrides automatic
path detection.
--password TEXT Password to use for authentication when --host is used.
--timeout INTEGER Specify the timeout in seconds for any network
operations.
--board-id TEXT Manual Board ID of the CircuitPython device. If provided
in combination with --cpy-version, it overrides the
detected board ID.
--cpy-version TEXT Manual CircuitPython version. If provided in combination
with --board-id, it overrides the detected CPy version.
--version Show the version and exit.
--help Show this message and exit.
Commands:
freeze Output details of all the modules found on the connected...
list Lists all out of date modules found on the connected
CIRCUITPYTHON...
update Checks for out-of-date modules on the connected CIRCUITPYTHON...
bundle-add Add bundles to the local bundles list, by "user/repo"...
bundle-remove Remove one or more bundles from the local bundles list.
bundle-show Show the list of bundles, default and local, with URL,...
example Copy named example(s) from a bundle onto the device.
freeze Output details of all the modules found on the connected...
install Install a named module(s) onto the device.
list Lists all out of date modules found on the connected...
show Show a list of available modules in the bundle.
uninstall Uninstall a named module(s) from the connected device.
update Update modules on the device. Use --all to automatically
update all modules without Major Version warnings.
To automatically install all modules imported by ``code.py``,
:code:`$ circup install --auto`::
$ circup install --auto
Found device at /Volumes/CIRCUITPY, running CircuitPython 7.0.0-alpha.5.
Searching for dependencies for: ['adafruit_bmp280']
Ready to install: ['adafruit_bmp280', 'adafruit_bus_device', 'adafruit_register']
Installed 'adafruit_bmp280'.
Installed 'adafruit_bus_device'.
Installed 'adafruit_register'.
To search for a specific module containing the name bme:
:code:`$ circup show bme`::
$ circup show bme
Found device at /Volumes/CIRCUITPY, running CircuitPython 6.1.0-beta.2.
adafruit_bme280
adafruit_bme680
2 shown of 257 packages.
To show version information for all the modules currently on a connected
CIRCUITPYTHON device::
@ -80,14 +149,18 @@ CIRCUITPYTHON device::
adafruit_bme280==2.3.1
adafruit_ble==1.0.2
With :code:`$ circup freeze -r`, Circup will save, in the current working directory,
a requirements.txt file with a list of all modules currently installed on the
connected device.
To list all the modules that require an update::
$ circup list
The following modules are out of date or probably need an update.
Module Version Latest
------------------ -------- --------
adafruit_binascii v1.0 1.0.1
Module Version Latest
------------------ -------- --------
adafruit_binascii v1.0 1.0.1
adafruit_ble 1.0.2 4.0
To interactively update the out-of-date modules::
@ -101,24 +174,130 @@ To interactively update the out-of-date modules::
Update 'adafruit_ble'? [y/N]: Y
OK
Install a module or modules onto the connected device with::
$ circup install adafruit_thermal_printer
Installed 'adafruit_thermal_printer'.
$ circup install adafruit_thermal_printer adafruit_bus_io
Installed 'adafruit_thermal_printer'.
Installed 'adafruit_bus_io'.
If you need to work with the original .py version of a module, use the --py
flag.
$ circup install --py adafruit_thermal_printer
Installed 'adafruit_thermal_printer'.
You can also install a list of modules from a requirements.txt file in
the current working directory with::
$ circup install -r requirements.txt
Installed 'adafruit_bmp280'.
Installed 'adafruit_lis3mdl'.
Installed 'adafruit_lsm6ds'.
Installed 'adafruit_sht31d'.
Installed 'neopixel'.
Uninstall a module or modules like this::
$ circup uninstall adafruit_thermal_printer
Uninstalled 'adafruit_thermal_printer'.
$ circup uninstall adafruit_thermal_printer adafruit_bus_io
Uninstalled 'adafruit_thermal_printer'.
Uninstalled 'adafruit_bus_io'.
Use the ``--verbose`` flag to see the logs as the command is working::
$ circup --verbose freeze
Logging to /home/ntoll/.cache/circup/log/circup.log
INFO: Started 2019-09-05 13:13:41.031822
INFO: Freeze
INFO: Found device: /media/ntoll/CIRCUITPY
10/18/2020 00:54:43 INFO: ### Started Circup ###
10/18/2020 00:54:43 INFO: Found device: /Volumes/CIRCUITPY
Found device at /Volumes/CIRCUITPY, running CircuitPython 6.0.0-alpha.1-1352-gf0b37313c.
10/18/2020 00:54:44 INFO: Freeze
10/18/2020 00:54:44 INFO: Found device: /Volumes/CIRCUITPY
... etc ...
Finally, the ``--version`` flag will tell you the current version of the
The ``--path`` flag let's you pass in a different path to the CircuitPython
mounted volume. This is helpful when you have renamed or have more than one
CircuitPython devices attached::
$ circup --path /run/media/user/CIRCUITPY1 list
The ``--version`` flag will tell you the current version of the
``circup`` command itself::
$ circup --version
CircUp, A CircuitPython module updater. Version 0.0.1
Circup, A CircuitPython module updater. Version 0.0.1
To use circup via the `Web Workflow <https://learn.adafruit.com/getting-started-with-web-workflow-using-the-code-editor>`_. on devices that support it. Use the ``--host`` and ``--password`` arguments before your circup command.::
$ circup --host 192.168.1.119 --password s3cr3t install adafruit_hid
$ circup --host cpy-9573b2.local --password s3cr3t install adafruit_hid
That's it!
Library Name Autocomplete
-------------------------
When enabled, circup will autocomplete library names, simliar to other command line tools.
For example:
``circup install n`` + tab -``circup install neopixel`` (+tab: offers ``neopixel`` and ``neopixel_spi`` completions)
``circup install a`` + tab -``circup install adafruit\_`` + m a g + tab -``circup install adafruit_magtag``
How to Activate Library Name Autocomplete
-----------------------------------------
In order to activate shell completion, you need to inform your shell that completion is available for your script. Any Click application automatically provides support for that.
For Bash, add this to ~/.bashrc::
eval "$(_CIRCUP_COMPLETE=bash_source circup)"
For Zsh, add this to ~/.zshrc::
autoload -U compinit; compinit
eval "$(_CIRCUP_COMPLETE=zsh_source circup)"
For Fish, add this to ~/.config/fish/completions/foo-bar.fish::
eval (env _CIRCUP_COMPLETE=fish_source circup)
Open a new shell to enable completion. Or run the eval command directly in your current shell to enable it temporarily.
### Activation Script
The above eval examples will invoke your application every time a shell is started. This may slow down shell startup time significantly.
Alternatively, export the generated completion code as a static script to be executed. You can ship this file with your builds; tools like Git do this. At least Zsh will also cache the results of completion files, but not eval scripts.
For Bash::
_CIRCUP_COMPLETE=bash_source circup circup-complete.sh
For Zsh::
_CIRCUP_COMPLETE=zsh_source circup circup-complete.sh
For Fish::
_CIRCUP_COMPLETE=fish_source circup circup-complete.sh
In .bashrc or .zshrc, source the script instead of the eval command::
. /path/to/circup-complete.sh
For Fish, add the file to the completions directory::
_CIRCUP_COMPLETE=fish_source circup ~/.config/fish/completions/circup-complete.fish
.. note::
If you find a bug, or you want to suggest an enhancement or new feature
@ -126,145 +305,6 @@ That's it!
https://github.com/adafruit/circup
Developer Setup
---------------
.. note::
Please try to use Python 3.6+ while developing CircUp. This is so we can
use the
`Black code formatter <https://black.readthedocs.io/en/stable/index.html>`_
(which only works with Python 3.6+).
Clone the repository then make a virtualenv. From the root of the project,
install the requirements::
pip install -e ".[dev]"
Run the test suite::
make check
.. warning::
Whenever you run ``make check``, to ensure the test suite starts from a
known clean state, all auto-generated assets are deleted. This includes
assets generated by running ``pip install -e ".[dev]"``, including the
``circup`` command itself. Simply re-run ``pip`` to re-generate the
assets.
There is a Makefile that helps with most of the common workflows associated
with development. Typing "make" on its own will list the options thus::
$ make
There is no default Makefile target right now. Try:
make clean - reset the project and remove auto-generated assets.
make pyflakes - run the PyFlakes code checker.
make pycodestyle - run the PEP8 style checker.
make test - run the test suite.
make coverage - view a report on test coverage.
make tidy - tidy code with the 'black' formatter.
make check - run all the checkers and tests.
make dist - make a dist/wheel for the project.
make publish-test - publish the project to PyPI test instance.
make publish-live - publish the project to PyPI production.
make docs - run sphinx to create project documentation.
.. note::
On Windows there is a ``make.cmd`` file that calls ``make.py``: a script
that works in a similar way to the ``make`` command on Unix-like operating
systems. Typing ``make`` will display help for the various commands it
provides that are equivalent of those in the Unix Makefile.
How?
####
The ``circup`` tool checks for a connected CircuitPython device by
interrogating the local filesystem to find a path to a directory which ends
with ``"CIRCUITPYTHON"`` (the name under which a CircuitPython device is
mounted by the host operating system). This is handled in the ``find_device``
function.
A Python module on a connected device is represented by an instance of the
``Module`` class. This class provides useful methods for discerning if the
module is out of date, returning useful representations of it in order to
display information to the user, or updating the module on the connected
device with whatever the version is in the latest Adafruit CircuitPython
Bundle.
All of the libraries included in the Adafruit CircuitPython Bundle contain,
somewhere within their code, two metadata objects called ``__version__`` and
``__repo__``.
The ``__repo__`` object is a string containing the GitHub repository URL, as
used to clone the project.
The ``__version__`` object is interesting because *within the source code in
Git* the value is **always** the string ``"0.0.0-auto.0"``. When a new release
is made of the bundle, this value is automatically replaced by the build
scripts to the correct version information, which will always conform to the
`semver standard <https://semver.org/>`_.
Given this context, the ``circup`` tool will check a configuration file
to discern what *it* thinks is the latest version of the bundle. If there is
no configuration file (for example, on first run), then the bundle version is
assumed to be ``"0"``.
Next, it checks GitHub for the tag value (denoting the version) of the very
latest bundle release. Bundle versions are based upon the date of release, for
instance ``"20190904"``. If the latest version on GitHub is later than the
version ``circup`` currently has, then the latest version of the bundle
is automatically downloaded and cached away somewhere.
In this way, the ``circup`` tool is able to have available to it both a path
to a connected CIRCUITPYTHON devce and a copy of the latest version, including
the all important version information, of the Adafruit CircuitPython Bundle.
Exactly the same function (``get_modules``) is used to extract the metadata
from the modules on both the connected device and in the bundle cache. This
metadata is used to instantiate instances of the ``Module`` class which is
subsequently used to facilitate the various commands the tool makes available.
These commands are defined at the very end of the ``circup.py`` code.
Unit tests can be found in the ``tests`` directory. CircUp uses
`pytest <http://www.pytest.org/en/latest/>`_ style testing conventions. Test
functions should include a comment to describe its *intention*. We currently
have 100% unit test coverage for all the core functionality (excluding
functions used to define the CLI commands).
To run the full test suite, type::
make check
All code is formatted using the stylistic conventions enforced by
`black <https://black.readthedocs.io/en/stable/>`_. The tidying of code
formatting is part of the ``make check`` process, but you can also just use::
make tidy
Please see the output from ``make`` for more information about the various
available options to help you work with the code base. TL;DR ``make check``
runs everything.
Before submitting a PR, please remember to ``make check``. ;-)
CircUp uses the `Click <https://click.palletsprojects.com/en/7.x/>`_ module to
run command-line interaction. The
`AppDirs <https://pypi.org/project/appdirs/>`_ module is used to determine
where to store user-specific assets created by the tool in such a way that
meets the host operating system's usual conventions. The
`python-semver <https://github.com/k-bx/python-semver>`_ package is used to
validate and compare the semver values associated with modules. The ubiquitous
`requests <http://python-requests.org/>`_ module is used for HTTP activity.
Documentation, generated by `Sphinx <http://www.sphinx-doc.org/en/master/>`_,
is based on this README and assembled by assets in the ``doc`` subdirectory.
The latest version of the docs will be found on
`Read the Docs <https://circup.readthedocs.io/>`_.
Discussion of this tool happens on the Adafruit CircuitPython
`Discord channel <https://discord.gg/rqrKDjU>`_.

3
README.rst.license Normal file
View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

710
circup.py
View file

@ -1,710 +0,0 @@
"""
CircUp -- a utility to manage and update libraries on a CircuitPython device.
Copyright (c) 2019 Adafruit Industries
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import logging
import appdirs
import os
import sys
import ctypes
import glob
import re
import requests
import click
import shutil
import json
import zipfile
from datetime import datetime
from semver import compare
from subprocess import check_output
# Useful constants.
#: The unique USB vendor ID for Adafruit boards.
VENDOR_ID = 9114
#: The regex used to extract ``__version__`` and ``__repo__`` assignments.
DUNDER_ASSIGN_RE = re.compile(r"""^__\w+__\s*=\s*['"].+['"]$""")
#: Flag to indicate if the command is being run in verbose mode.
VERBOSE = False
#: The location of data files used by circup (following OS conventions).
DATA_DIR = appdirs.user_data_dir(appname="circup", appauthor="adafruit")
#: The path to the JSON file containing the metadata about the current bundle.
BUNDLE_DATA = os.path.join(DATA_DIR, "circup.json")
#: The path to the zip file containing the current library bundle.
BUNDLE_ZIP = os.path.join(DATA_DIR, "adafruit-circuitpython-bundle-{}.zip")
#: The path to the directory into which the current bundle is unzipped.
BUNDLE_DIR = os.path.join(DATA_DIR, "adafruit_circuitpython_bundle_{}")
#: The directory containing the utility's log file.
LOG_DIR = appdirs.user_log_dir(appname="circup", appauthor="adafruit")
#: The location of the log file for the utility.
LOGFILE = os.path.join(LOG_DIR, "circup.log")
#: The version of CircuitPython found on the connected device.
CPY_VERSION = ""
# Ensure DATA_DIR / LOG_DIR related directories and files exist.
if not os.path.exists(DATA_DIR): # pragma: no cover
os.makedirs(DATA_DIR)
if not os.path.exists(LOG_DIR): # pragma: no cover
os.makedirs(LOG_DIR)
# Setup logging.
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logfile_handler = logging.FileHandler(LOGFILE)
log_formatter = logging.Formatter("%(levelname)s: %(message)s")
logfile_handler.setFormatter(log_formatter)
logger.addHandler(logfile_handler)
# IMPORTANT
# ---------
# Keep these metadata assignments simple and single-line. They are parsed
# somewhat naively by setup.py.
__title__ = "circup"
__description__ = "A tool to manage/update libraries on CircuitPython devices."
__version__ = "0.0.1"
__license__ = "MIT"
__url__ = "https://github.com/adafruit/circup"
__author__ = "Adafruit Industries"
__email__ = "ntoll@ntoll.org"
class Module:
"""
Represents a CircuitPython module.
"""
def __init__(self, path, repo, device_version, bundle_version, mpy):
"""
The ``self.file`` and ``self.name`` attributes are constructed from
the ``path`` value. If the path is to a directory based module, the
resulting self.file value will be None, and the name will be the
basename of the directory path.
:param str path: The path to the module on the connected CIRCUITPYTHON
device.
:param str repo: The URL of the Git repository for this module.
:param str device_version: The semver value for the version on device.
:param str bundle_version: The semver value for the version in bundle.
:param bool mpy: Flag to indicate if the module is byte-code compiled.
"""
self.path = path
if os.path.isfile(self.path):
# Single file module.
self.file = os.path.basename(path)
self.name = self.file.replace(".py", "").replace(".mpy", "")
else:
# Directory based module.
self.file = None
self.name = os.path.basename(os.path.dirname(self.path))
self.repo = repo
self.device_version = device_version
self.bundle_version = bundle_version
self.mpy = mpy
# Figure out the bundle path.
self.bundle_path = None
if self.mpy:
# Byte compiled, now check CircuitPython version.
major_version = CPY_VERSION.split(".")[0]
bundle_platform = "{}mpy".format(major_version)
else:
# Regular Python
bundle_platform = "py"
for path, subdirs, files in os.walk(
BUNDLE_DIR.format(bundle_platform)
):
if os.path.basename(path) == "lib":
if self.file:
self.bundle_path = os.path.join(path, self.file)
else:
self.bundle_path = os.path.join(path, self.name)
logger.info(self)
@property
def outofdate(self):
"""
Returns a boolean to indicate if this module is out of date.
:return: Truthy indication if the module is out of date.
"""
if self.device_version and self.bundle_version:
try:
return compare(self.device_version, self.bundle_version) < 0
except ValueError as ex:
logger.warning(
"Module '{}' has incorrect semver value.".format(self.name)
)
logger.warning(ex)
return True # Assume out of date to try to update.
@property
def row(self):
"""
Returns a tuple of items to display in a table row to show the module's
name, local version and remote version.
:return: A tuple containing the module's name, version on the connected
device and version in the latest bundle.
"""
loc = self.device_version if self.device_version else "unknown"
rem = self.bundle_version if self.bundle_version else "unknown"
return (self.name, loc, rem)
def update(self):
"""
Delete the module on the device, then copy the module from the bundle
back onto the device.
The caller is expected to handle any exceptions raised.
"""
if os.path.isdir(self.path):
# Delete and copy the directory.
shutil.rmtree(self.path)
shutil.copytree(self.bundle_path, self.path)
else:
# Delete and copy file.
os.remove(self.path)
shutil.copyfile(self.bundle_path, self.path)
def __repr__(self):
"""
Helps with log files.
:return: A repr of a dictionary containing the module's metadata.
"""
return repr(
{
"path": self.path,
"file": self.file,
"name": self.name,
"repo": self.repo,
"device_version": self.device_version,
"bundle_version": self.bundle_version,
"bundle_path": self.bundle_path,
"mpy": self.mpy,
}
)
def find_device():
"""
Return the location on the filesystem for the connected Adafruit device.
This is based upon how Mu discovers this information.
:return: The path to the device on the local filesystem.
"""
device_dir = None
# Attempt to find the path on the filesystem that represents the plugged in
# CIRCUITPY board.
if os.name == "posix":
# Linux / OSX
for mount_command in ["mount", "/sbin/mount"]:
try:
mount_output = check_output(mount_command).splitlines()
mounted_volumes = [x.split()[2] for x in mount_output]
for volume in mounted_volumes:
if volume.endswith(b"CIRCUITPY"):
device_dir = volume.decode("utf-8")
except FileNotFoundError:
next
elif os.name == "nt":
# Windows
def get_volume_name(disk_name):
"""
Each disk or external device connected to windows has an attribute
called "volume name". This function returns the volume name for the
given disk/device.
Based upon answer given here: http://stackoverflow.com/a/12056414
"""
vol_name_buf = ctypes.create_unicode_buffer(1024)
ctypes.windll.kernel32.GetVolumeInformationW(
ctypes.c_wchar_p(disk_name),
vol_name_buf,
ctypes.sizeof(vol_name_buf),
None,
None,
None,
None,
0,
)
return vol_name_buf.value
#
# In certain circumstances, volumes are allocated to USB
# storage devices which cause a Windows popup to raise if their
# volume contains no media. Wrapping the check in SetErrorMode
# with SEM_FAILCRITICALERRORS (1) prevents this popup.
#
old_mode = ctypes.windll.kernel32.SetErrorMode(1)
try:
for disk in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
path = "{}:\\".format(disk)
if (
os.path.exists(path)
and get_volume_name(path) == "CIRCUITPY"
):
device_dir = path
# Report only the FIRST device found.
break
finally:
ctypes.windll.kernel32.SetErrorMode(old_mode)
else:
# No support for unknown operating systems.
raise NotImplementedError('OS "{}" not supported.'.format(os.name))
logger.info("Found device: {}".format(device_dir))
return device_dir
def get_latest_tag():
"""
Find the value of the latest tag for the Adafruit CircuitPython library
bundle.
:return: The most recent tag value for the project.
"""
url = (
"https://github.com/adafruit/Adafruit_CircuitPython_Bundle"
"/releases/latest"
)
logger.info("Requesting tag information: {}".format(url))
response = requests.get(url)
logger.info("Response url: {}".format(response.url))
tag = response.url.rsplit("/", 1)[-1]
logger.info("Tag: '{}'".format(tag))
return tag
def extract_metadata(path):
"""
Given an file path, return a dictionary containing metadata extracted from
dunder attributes found therein. Works with both *.py and *.mpy files.
For Python source files, such metadata assignments should be simple and
single-line. For example::
__version__ = "1.1.4"
__repo__ = "https://github.com/adafruit/SomeLibrary.git"
For byte compiled *.mpy files, a brute force / backtrack approach is used
to find the __version__ number in the file -- see comments in the
code for the implementation details.
:param str path: The path to the file containing the metadata.
:return: The dunder based metadata found in the file, as a dictionary.
"""
result = {}
if path.endswith(".py"):
result["mpy"] = False
with open(path, encoding="utf-8") as source_file:
content = source_file.read()
lines = content.split("\n")
for line in lines:
if DUNDER_ASSIGN_RE.search(line):
exec(line, result)
if "__builtins__" in result:
del result[
"__builtins__"
] # Side effect of using exec, not needed.
if result:
logger.info("Extracted metadata: {}".format(result))
return result
elif path.endswith(".mpy"):
result["mpy"] = True
with open(path, "rb") as mpy_file:
content = mpy_file.read()
# Find the start location of the "__version__" (prepended with byte
# value of 11 to indicate length of "__version__").
loc = content.find(b"\x0b__version__")
if loc > -1:
# Backtrack until a byte value of the offset is reached.
offset = 1
while offset < loc:
val = int(content[loc - offset])
if val == offset - 1: # Off by one..!
# Found version, extract the number given boundaries.
start = loc - offset + 1 # No need for prepended length.
end = loc # Up to the start of the __version__.
version = content[start:end] # Slice the version number.
# Create a string version as metadata in the result.
result = {
"__version__": version.decode("utf-8"),
"mpy": True,
}
break # Nothing more to do.
offset += 1 # ...and again but backtrack by one.
return result
def find_modules():
"""
Extracts metadata from the connected device and available bundle and
returns this as a list of Module instances representing the modules on the
device.
:return: A list of Module instances describing the current state of the
modules on the connected device.
"""
try:
device_modules = get_device_versions()
bundle_modules = get_bundle_versions()
result = []
for name, device_metadata in device_modules.items():
if name in bundle_modules:
bundle_metadata = bundle_modules[name]
path = device_metadata["path"]
repo = device_metadata.get("__repo__")
device_version = device_metadata.get("__version__")
bundle_version = bundle_metadata.get("__version__")
mpy = device_metadata["mpy"]
result.append(
Module(path, repo, device_version, bundle_version, mpy)
)
return result
except Exception as ex:
# If it's not possible to get the device and bundle metadata, bail out
# with a friendly message and indication of what's gone wrong.
logger.exception(ex)
click.echo("There was a problem: {}".format(ex))
sys.exit(1)
def get_bundle_versions():
"""
Returns a dictionary of metadata from modules in the latest known release
of the library bundle. Uses the Python version (rather than the compiled
version) of the library modules.
:return: A dictionary of metadata about the modules available in the
library bundle.
"""
ensure_latest_bundle()
path = None
for path, subdirs, files in os.walk(BUNDLE_DIR.format("py")):
if os.path.basename(path) == "lib":
break
return get_modules(path)
def get_circuitpython_version(device_path):
"""
Returns the version number of CircuitPython running on the board connected
via ``device_path``. This is obtained from the ``boot_out.txt`` file on the
device, whose content will start with something like this::
Adafruit CircuitPython 4.1.0 on 2019-08-02;
:param str device_path: The path to the connected board.
:return: The version string for CircuitPython running on the connected
board.
"""
with open(os.path.join(device_path, "boot_out.txt")) as boot:
circuit_python, board = boot.read().split(";")
return circuit_python.split(" ")[-3]
def get_device_versions():
"""
Returns a dictionary of metadata from modules on the connected device.
:return: A dictionary of metadata about the modules available on the
connected device.
"""
device_path = find_device()
return get_modules(os.path.join(device_path, "lib"))
def get_modules(path):
"""
Get a dictionary containing metadata about all the Python modules found in
the referenced path.
:param str path: The directory in which to find modules.
:return: A dictionary containing metadata about the found modules.
"""
result = {}
if not path:
return result
single_file_py_mods = glob.glob(os.path.join(path, "*.py"))
single_file_mpy_mods = glob.glob(os.path.join(path, "*.mpy"))
directory_mods = [
d
for d in glob.glob(os.path.join(path, "*", ""))
if not os.path.basename(os.path.normpath(d)).startswith(".")
]
single_file_mods = single_file_py_mods + single_file_mpy_mods
for sfm in [
f for f in single_file_mods if not os.path.basename(f).startswith(".")
]:
metadata = extract_metadata(sfm)
metadata["path"] = sfm
result[
os.path.basename(sfm).replace(".py", "").replace(".mpy", "")
] = metadata
for dm in directory_mods:
name = os.path.basename(os.path.dirname(dm))
metadata = {}
py_files = glob.glob(os.path.join(dm, "*.py"))
mpy_files = glob.glob(os.path.join(dm, "*.mpy"))
all_files = py_files + mpy_files
for source in [
f for f in all_files if not os.path.basename(f).startswith(".")
]:
metadata = extract_metadata(source)
if "__version__" in metadata:
metadata["path"] = dm
result[name] = metadata
break
else:
# No version metadata found.
result[name] = {"path": dm, "mpy": bool(mpy_files)}
return result
def ensure_latest_bundle():
"""
Ensure that there's a copy of the latest library bundle available so circup
can check the metadata contained therein.
"""
logger.info("Checking for library updates.")
tag = get_latest_tag()
old_tag = "0"
if os.path.isfile(BUNDLE_DATA):
with open(BUNDLE_DATA, encoding="utf-8") as data:
try:
old_tag = json.load(data)["tag"]
except json.decoder.JSONDecodeError as ex:
# Sometimes (why?) the JSON file becomes corrupt. In which case
# log it and carry on as if setting up for first time.
logger.error("Could not parse {}".format(BUNDLE_DATA))
logger.exception(ex)
if tag > old_tag:
logger.info("New version available ({}).".format(tag))
get_bundle(tag)
with open(BUNDLE_DATA, "w", encoding="utf-8") as data:
json.dump({"tag": tag}, data)
else:
logger.info("Current library bundle up to date ({}).".format(tag))
def get_bundle(tag):
"""
Downloads and extracts the version of the bundle with the referenced tag.
:param str tag: The GIT tag to use to download the bundle.
:return: The location of the resulting zip file in a temporary location on
the local filesystem.
"""
urls = {
"py": (
"https://github.com/adafruit/Adafruit_CircuitPython_Bundle"
"/releases/download"
"/{tag}/adafruit-circuitpython-bundle-py-{tag}.zip".format(tag=tag)
),
"4mpy": (
"https://github.com/adafruit/Adafruit_CircuitPython_Bundle"
"/releases/download"
"/{tag}/adafruit-circuitpython-bundle-4.x-mpy-{tag}.zip".format(
tag=tag
)
),
"5mpy": (
"https://github.com/adafruit/Adafruit_CircuitPython_Bundle/"
"releases/download"
"/{tag}/adafruit-circuitpython-bundle-5.x-mpy-{tag}.zip".format(
tag=tag
)
),
}
click.echo("Downloading latest version information.\n")
for platform, url in urls.items():
logger.info("Downloading bundle: {}".format(url))
r = requests.get(url, stream=True)
if r.status_code != requests.codes.ok:
logger.warning("Unable to connect to {}".format(url))
r.raise_for_status()
total_size = int(r.headers.get("Content-Length"))
temp_zip = BUNDLE_ZIP.format(platform)
with click.progressbar(
r.iter_content(1024), length=total_size
) as bar, open(temp_zip, "wb") as f:
for chunk in bar:
f.write(chunk)
bar.update(len(chunk))
logger.info("Saved to {}".format(temp_zip))
temp_dir = BUNDLE_DIR.format(platform)
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir)
with zipfile.ZipFile(temp_zip, "r") as zfile:
zfile.extractall(temp_dir)
click.echo("\nOK\n")
# ----------- CLI command definitions ----------- #
# The following functions have IO side effects (for instance they emit to
# stdout). Ergo, these are not checked with unit tests. Most of the
# functionality they provide is provided by the functions above, which *are*
# tested. Most of the logic of the following functions is to prepare things for
# presentation to / interaction with the user.
@click.group()
@click.option(
"--verbose", is_flag=True, help="Comprehensive logging is sent to stdout."
)
@click.version_option(
version=__version__,
prog_name="CircUp",
message="%(prog)s, A CircuitPython module updater. Version %(version)s",
)
def main(verbose): # pragma: no cover
"""
A tool to manage and update libraries on a CircuitPython device.
"""
if verbose:
# Configure additional logging to stdout.
global VERBOSE
VERBOSE = True
verbose_handler = logging.StreamHandler(sys.stdout)
verbose_handler.setLevel(logging.INFO)
verbose_handler.setFormatter(log_formatter)
logger.addHandler(verbose_handler)
click.echo("Logging to {}\n".format(LOGFILE))
logger.info("### Started {}".format(datetime.now()))
device_path = find_device()
if device_path is None:
click.secho("Could not find a connected Adafruit device.", fg="red")
sys.exit(1)
global CPY_VERSION
CPY_VERSION = get_circuitpython_version(device_path)
click.echo(
"Found device at {}, running CircuitPython {}.".format(
device_path, CPY_VERSION
)
)
cp_release = requests.get(
"https://github.com/adafruit/circuitpython/releases/latest", timeout=2
)
latest_version = cp_release.url.split("/")[-1]
try:
if compare(CPY_VERSION, latest_version) < 0:
click.secho(
"A newer version of CircuitPython ({}) is available.".format(
latest_version
),
fg="green",
)
except ValueError as ex:
logger.warning("CircuitPython has incorrect semver value.")
logger.warning(ex)
@main.command()
def freeze(): # pragma: no cover
"""
Output details of all the modules found on the connected CIRCUITPYTHON
device.
"""
logger.info("Freeze")
modules = find_modules()
if modules:
for module in modules:
output = "{}=={}".format(module.name, module.device_version)
click.echo(output)
logger.info(output)
else:
click.echo("No modules found on the device.")
@main.command()
def list(): # pragma: no cover
"""
Lists all out of date modules found on the connected CIRCUITPYTHON device.
"""
logger.info("List")
# Grab out of date modules.
data = [("Module", "Version", "Latest")]
modules = [m.row for m in find_modules() if m.outofdate]
if modules:
data += modules
# Nice tabular display.
col_width = [0, 0, 0]
for row in data:
for i, word in enumerate(row):
col_width[i] = max(len(word) + 2, col_width[i])
dashes = tuple(("-" * (width - 1) for width in col_width))
data.insert(1, dashes)
click.echo(
"The following modules are out of date or probably need "
"an update.\n"
)
for row in data:
output = ""
for i in range(3):
output += row[i].ljust(col_width[i])
if not VERBOSE:
click.echo(output)
logger.info(output)
else:
click.echo("All modules found on the device are up to date.")
@main.command(
short_help=(
"Update modules on the device. "
"Use --all to automatically update all modules."
)
)
@click.option("--all", is_flag=True)
def update(all): # pragma: no cover
"""
Checks for out-of-date modules on the connected CIRCUITPYTHON device, and
prompts the user to confirm updating such modules.
"""
logger.info("Update")
# Grab out of date modules.
modules = [m for m in find_modules() if m.outofdate]
if modules:
click.echo("Found {} module[s] needing update.".format(len(modules)))
if not all:
click.echo("Please indicate which modules you wish to update:\n")
for module in modules:
update_flag = all
if not update_flag:
update_flag = click.confirm("Update '{}'?".format(module.name))
if update_flag:
try:
module.update()
click.echo("Updated {}".format(module.name))
except Exception as ex:
logger.exception(ex)
click.echo(
"Something went wrong, {} (check the logs)".format(
str(ex)
)
)
else:
click.echo("None of the modules found on the device need an update.")

26
circup/__init__.py Normal file
View file

@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
Circup -- a utility to manage and update libraries on a CircuitPython device.
"""
from circup.shared import DATA_DIR, BAD_FILE_FORMAT, extract_metadata, _get_modules_file
from circup.backends import WebBackend, DiskBackend
from circup.logging import logger
# Useful constants.
__version__ = "0.0.0-auto.0"
__repo__ = "https://github.com/adafruit/circup.git"
from circup.commands import *
# Allows execution via `python -m circup ...`
# pylint: disable=no-value-for-parameter
if __name__ == "__main__": # pragma: no cover
main()

1061
circup/backends.py Normal file

File diff suppressed because it is too large Load diff

170
circup/bundle.py Normal file
View file

@ -0,0 +1,170 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, 2024 Tim Cocks, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
Class that represents a specific release of a Bundle.
"""
import os
import sys
import click
import requests
from circup.shared import (
DATA_DIR,
PLATFORMS,
REQUESTS_TIMEOUT,
tags_data_load,
get_latest_release_from_url,
)
from circup.logging import logger
class Bundle:
"""
All the links and file names for a bundle
"""
def __init__(self, repo):
"""
Initialise a Bundle created from its github info.
Construct all the strings in one place.
:param str repo: Repository string for github: "user/repository"
"""
vendor, bundle_id = repo.split("/")
bundle_id = bundle_id.lower().replace("_", "-")
self.key = repo
#
self.url = "https://github.com/" + repo
self.basename = bundle_id + "-{platform}-{tag}"
self.urlzip = self.basename + ".zip"
self.dir = os.path.join(DATA_DIR, vendor, bundle_id + "-{platform}")
self.zip = os.path.join(DATA_DIR, bundle_id + "-{platform}.zip")
self.url_format = self.url + "/releases/download/{tag}/" + self.urlzip
# tag
self._current = None
self._latest = None
def lib_dir(self, platform):
"""
This bundle's lib directory for the platform.
:param str platform: The platform identifier (py/6mpy/...).
:return: The path to the lib directory for the platform.
"""
tag = self.current_tag
return os.path.join(
self.dir.format(platform=platform),
self.basename.format(platform=PLATFORMS[platform], tag=tag),
"lib",
)
def examples_dir(self, platform):
"""
This bundle's examples directory for the platform.
:param str platform: The platform identifier (py/6mpy/...).
:return: The path to the examples directory for the platform.
"""
tag = self.current_tag
return os.path.join(
self.dir.format(platform=platform),
self.basename.format(platform=PLATFORMS[platform], tag=tag),
"examples",
)
def requirements_for(self, library_name, toml_file=False):
"""
The requirements file for this library.
:param str library_name: The name of the library.
:return: The path to the requirements.txt file.
"""
platform = "py"
tag = self.current_tag
found_file = os.path.join(
self.dir.format(platform=platform),
self.basename.format(platform=PLATFORMS[platform], tag=tag),
"requirements",
library_name,
"requirements.txt" if not toml_file else "pyproject.toml",
)
if os.path.isfile(found_file):
with open(found_file, "r", encoding="utf-8") as read_this:
return read_this.read()
return None
@property
def current_tag(self):
"""
Lazy load current cached tag from the BUNDLE_DATA json file.
:return: The current cached tag value for the project.
"""
if self._current is None:
self._current = tags_data_load(logger).get(self.key, "0")
return self._current
@current_tag.setter
def current_tag(self, tag):
"""
Set the current cached tag (after updating).
:param str tag: The new value for the current tag.
:return: The current cached tag value for the project.
"""
self._current = tag
@property
def latest_tag(self):
"""
Lazy find the value of the latest tag for the bundle.
:return: The most recent tag value for the project.
"""
if self._latest is None:
self._latest = get_latest_release_from_url(
self.url + "/releases/latest", logger
)
return self._latest
def validate(self):
"""
Test the existence of the expected URLs (not their content)
"""
tag = self.latest_tag
if not tag or tag == "releases":
if "--verbose" in sys.argv:
click.secho(f' Invalid tag "{tag}"', fg="red")
return False
for platform in PLATFORMS.values():
url = self.url_format.format(platform=platform, tag=tag)
r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
# pylint: disable=no-member
if r.status_code != requests.codes.ok:
if "--verbose" in sys.argv:
click.secho(f" Unable to find {os.path.split(url)[1]}", fg="red")
return False
# pylint: enable=no-member
return True
def __repr__(self):
"""
Helps with log files.
:return: A repr of a dictionary containing the Bundles's metadata.
"""
return repr(
{
"key": self.key,
"url": self.url,
"urlzip": self.urlzip,
"dir": self.dir,
"zip": self.zip,
"url_format": self.url_format,
"current": self._current,
"latest": self._latest,
}
)

843
circup/command_utils.py Normal file
View file

@ -0,0 +1,843 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, 2024 Tim Cocks, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
Functions called from commands in order to provide behaviors and return information.
"""
import ast
import ctypes
import glob
import os
from subprocess import check_output
import sys
import shutil
import zipfile
import json
import re
import toml
import requests
import click
from circup.shared import (
PLATFORMS,
REQUESTS_TIMEOUT,
_get_modules_file,
BUNDLE_CONFIG_OVERWRITE,
BUNDLE_CONFIG_FILE,
BUNDLE_CONFIG_LOCAL,
BUNDLE_DATA,
NOT_MCU_LIBRARIES,
tags_data_load,
)
from circup.logging import logger
from circup.module import Module
from circup.bundle import Bundle
WARNING_IGNORE_MODULES = (
"typing-extensions",
"pyasn1",
"circuitpython-typing",
)
CODE_FILES = [
"code.txt",
"code.py",
"main.py",
"main.txt",
"code.txt.py",
"code.py.txt",
"code.txt.txt",
"code.py.py",
"main.txt.py",
"main.py.txt",
"main.txt.txt",
"main.py.py",
]
class CodeParsingException(Exception):
"""Exception thrown when parsing code with ast fails"""
def clean_library_name(assumed_library_name):
"""
Most CP repos and library names are look like this:
repo: Adafruit_CircuitPython_LC709203F
library: adafruit_lc709203f
But some do not and this handles cleaning that up.
Also cleans up if the pypi or reponame is passed in instead of the
CP library name.
:param str assumed_library_name: An assumed name of a library from user
or requirements.txt entry
:return: str proper library name
"""
not_standard_names = {
# Assumed Name : Actual Name
"adafruit_adafruitio": "adafruit_io",
"adafruit_asyncio": "asyncio",
"adafruit_busdevice": "adafruit_bus_device",
"adafruit_connectionmanager": "adafruit_connection_manager",
"adafruit_display_button": "adafruit_button",
"adafruit_neopixel": "neopixel",
"adafruit_sd": "adafruit_sdcard",
"adafruit_simpleio": "simpleio",
"pimoroni_ltr559": "pimoroni_circuitpython_ltr559",
}
if "circuitpython" in assumed_library_name:
# convert repo or pypi name to common library name
assumed_library_name = (
assumed_library_name.replace("-circuitpython-", "_")
.replace("_circuitpython_", "_")
.replace("-", "_")
)
if assumed_library_name in not_standard_names:
return not_standard_names[assumed_library_name]
return assumed_library_name
def completion_for_install(ctx, param, incomplete):
"""
Returns the list of available modules for the command line tab-completion
with the ``circup install`` command.
"""
# pylint: disable=unused-argument
available_modules = get_bundle_versions(get_bundles_list(), avoid_download=True)
module_names = {m.replace(".py", "") for m in available_modules}
if incomplete:
module_names = [name for name in module_names if name.startswith(incomplete)]
module_names.extend(glob.glob(f"{incomplete}*"))
return sorted(module_names)
def completion_for_example(ctx, param, incomplete):
"""
Returns the list of available modules for the command line tab-completion
with the ``circup example`` command.
"""
# pylint: disable=unused-argument, consider-iterating-dictionary
available_examples = get_bundle_examples(get_bundles_list(), avoid_download=True)
matching_examples = [
example_path
for example_path in available_examples.keys()
if example_path.startswith(incomplete)
]
return sorted(matching_examples)
def ensure_latest_bundle(bundle):
"""
Ensure that there's a copy of the latest library bundle available so circup
can check the metadata contained therein.
:param Bundle bundle: the target Bundle object.
"""
logger.info("Checking library updates for %s.", bundle.key)
tag = bundle.latest_tag
do_update = False
if tag == bundle.current_tag:
for platform in PLATFORMS:
# missing directories (new platform added on an existing install
# or side effect of pytest or network errors)
do_update = do_update or not os.path.isdir(bundle.lib_dir(platform))
else:
do_update = True
if do_update:
logger.info("New version available (%s).", tag)
try:
get_bundle(bundle, tag)
tags_data_save_tag(bundle.key, tag)
except requests.exceptions.HTTPError as ex:
# See #20 for reason for this
click.secho(
(
"There was a problem downloading that platform bundle. "
"Skipping and using existing download if available."
),
fg="red",
)
logger.exception(ex)
else:
logger.info("Current bundle up to date %s.", tag)
def find_device():
"""
Return the location on the filesystem for the connected CircuitPython device.
This is based upon how Mu discovers this information.
:return: The path to the device on the local filesystem.
"""
device_dir = None
# Attempt to find the path on the filesystem that represents the plugged in
# CIRCUITPY board.
if os.name == "posix":
# Linux / OSX
for mount_command in ["mount", "/sbin/mount"]:
try:
mount_output = check_output(mount_command).splitlines()
mounted_volumes = [x.split()[2] for x in mount_output]
for volume in mounted_volumes:
if volume.endswith(b"CIRCUITPY"):
device_dir = volume.decode("utf-8")
except FileNotFoundError:
continue
elif os.name == "nt":
# Windows
def get_volume_name(disk_name):
"""
Each disk or external device connected to windows has an attribute
called "volume name". This function returns the volume name for the
given disk/device.
Based upon answer given here: http://stackoverflow.com/a/12056414
"""
vol_name_buf = ctypes.create_unicode_buffer(1024)
ctypes.windll.kernel32.GetVolumeInformationW(
ctypes.c_wchar_p(disk_name),
vol_name_buf,
ctypes.sizeof(vol_name_buf),
None,
None,
None,
None,
0,
)
return vol_name_buf.value
#
# In certain circumstances, volumes are allocated to USB
# storage devices which cause a Windows popup to raise if their
# volume contains no media. Wrapping the check in SetErrorMode
# with SEM_FAILCRITICALERRORS (1) prevents this popup.
#
old_mode = ctypes.windll.kernel32.SetErrorMode(1)
try:
for disk in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
path = "{}:\\".format(disk)
if os.path.exists(path) and get_volume_name(path) == "CIRCUITPY":
device_dir = path
# Report only the FIRST device found.
break
finally:
ctypes.windll.kernel32.SetErrorMode(old_mode)
else:
# No support for unknown operating systems.
raise NotImplementedError('OS "{}" not supported.'.format(os.name))
logger.info("Found device: %s", device_dir)
return device_dir
def find_modules(backend, bundles_list):
"""
Extracts metadata from the connected device and available bundles and
returns this as a list of Module instances representing the modules on the
device.
:param Backend backend: Backend with the device connection.
:param List[Bundle] bundles_list: List of supported bundles as Bundle objects.
:return: A list of Module instances describing the current state of the
modules on the connected device.
"""
# pylint: disable=broad-except,too-many-locals
try:
device_modules = backend.get_device_versions()
bundle_modules = get_bundle_versions(bundles_list)
result = []
for key, device_metadata in device_modules.items():
if key in bundle_modules:
path = device_metadata["path"]
bundle_metadata = bundle_modules[key]
repo = bundle_metadata.get("__repo__")
bundle = bundle_metadata.get("bundle")
device_version = device_metadata.get("__version__")
bundle_version = bundle_metadata.get("__version__")
mpy = device_metadata["mpy"]
compatibility = device_metadata.get("compatibility", (None, None))
module_name = (
path.split(os.sep)[-1]
if not path.endswith(os.sep)
else path[:-1].split(os.sep)[-1] + os.sep
)
m = Module(
module_name,
backend,
repo,
device_version,
bundle_version,
mpy,
bundle,
compatibility,
)
result.append(m)
return result
except Exception as ex:
# If it's not possible to get the device and bundle metadata, bail out
# with a friendly message and indication of what's gone wrong.
logger.exception(ex)
click.echo("There was a problem: {}".format(ex))
sys.exit(1)
# pylint: enable=broad-except,too-many-locals
def get_bundle(bundle, tag):
"""
Downloads and extracts the version of the bundle with the referenced tag.
The resulting zip file is saved on the local filesystem.
:param Bundle bundle: the target Bundle object.
:param str tag: The GIT tag to use to download the bundle.
"""
click.echo(f"Downloading latest bundles for {bundle.key} ({tag}).")
for platform, github_string in PLATFORMS.items():
# Report the platform: "8.x-mpy", etc.
click.echo(f"{github_string}:")
url = bundle.url_format.format(platform=github_string, tag=tag)
logger.info("Downloading bundle: %s", url)
r = requests.get(url, stream=True, timeout=REQUESTS_TIMEOUT)
# pylint: disable=no-member
if r.status_code != requests.codes.ok:
logger.warning("Unable to connect to %s", url)
r.raise_for_status()
# pylint: enable=no-member
total_size = int(r.headers.get("Content-Length"))
temp_zip = bundle.zip.format(platform=platform)
with click.progressbar(
r.iter_content(1024), label="Extracting:", length=total_size
) as pbar, open(temp_zip, "wb") as zip_fp:
for chunk in pbar:
zip_fp.write(chunk)
pbar.update(len(chunk))
logger.info("Saved to %s", temp_zip)
temp_dir = bundle.dir.format(platform=platform)
if os.path.isdir(temp_dir):
shutil.rmtree(temp_dir)
with zipfile.ZipFile(temp_zip, "r") as zfile:
zfile.extractall(temp_dir)
bundle.current_tag = tag
click.echo("\nOK\n")
def get_bundle_examples(bundles_list, avoid_download=False):
"""
Return a dictionary of metadata from examples in the all of the bundles
specified by bundles_list argument.
:param List[Bundle] bundles_list: List of supported bundles as Bundle objects.
:param bool avoid_download: if True, download the bundle only if missing.
:return: A dictionary of metadata about the examples available in the
library bundle.
"""
# pylint: disable=too-many-nested-blocks,too-many-locals
all_the_examples = dict()
bundle_examples = dict()
try:
for bundle in bundles_list:
if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
ensure_latest_bundle(bundle)
path = bundle.examples_dir("py")
meta_saved = os.path.join(path, "../bundle_examples.json")
if os.path.exists(meta_saved):
with open(meta_saved, "r", encoding="utf-8") as f:
bundle_examples = json.load(f)
all_the_examples.update(bundle_examples)
bundle_examples.clear()
continue
path_examples = _get_modules_file(path, logger)
for lib_name, lib_metadata in path_examples.items():
for _dir_level in os.walk(lib_metadata["path"]):
for _file in _dir_level[2]:
_parts = _dir_level[0].split(os.path.sep)
_lib_name_index = _parts.index(lib_name)
_dirs = _parts[_lib_name_index:]
if _dirs[-1] == "":
_dirs.pop(-1)
slug = f"{os.path.sep}".join(_dirs + [_file.replace(".py", "")])
bundle_examples[slug] = os.path.join(_dir_level[0], _file)
all_the_examples[slug] = os.path.join(_dir_level[0], _file)
with open(meta_saved, "w", encoding="utf-8") as f:
json.dump(bundle_examples, f)
bundle_examples.clear()
except NotADirectoryError:
# Bundle does not have new style examples directory
# so we cannot include its examples.
pass
return all_the_examples
def get_bundle_versions(bundles_list, avoid_download=False):
"""
Returns a dictionary of metadata from modules in the latest known release
of the library bundle. Uses the Python version (rather than the compiled
version) of the library modules.
:param List[Bundle] bundles_list: List of supported bundles as Bundle objects.
:param bool avoid_download: if True, download the bundle only if missing.
:return: A dictionary of metadata about the modules available in the
library bundle.
"""
all_the_modules = dict()
for bundle in bundles_list:
if not avoid_download or not os.path.isdir(bundle.lib_dir("py")):
ensure_latest_bundle(bundle)
path = bundle.lib_dir("py")
path_modules = _get_modules_file(path, logger)
for name, module in path_modules.items():
module["bundle"] = bundle
if name not in all_the_modules: # here we decide the order of priority
all_the_modules[name] = module
return all_the_modules
def get_bundles_dict():
"""
Retrieve the dictionary from BUNDLE_CONFIG_FILE (JSON).
Put the local dictionary in front, so it gets priority.
It's a dictionary of bundle string identifiers.
:return: Combined dictionaries from the config files.
"""
bundle_dict = get_bundles_local_dict()
try:
with open(BUNDLE_CONFIG_OVERWRITE, "rb") as bundle_config_json:
bundle_config = json.load(bundle_config_json)
except (FileNotFoundError, json.decoder.JSONDecodeError):
with open(BUNDLE_CONFIG_FILE, "rb") as bundle_config_json:
bundle_config = json.load(bundle_config_json)
for name, bundle in bundle_config.items():
if bundle not in bundle_dict.values():
bundle_dict[name] = bundle
return bundle_dict
def get_bundles_local_dict():
"""
Retrieve the local bundles from BUNDLE_CONFIG_LOCAL (JSON).
:return: Raw dictionary from the config file(s).
"""
try:
with open(BUNDLE_CONFIG_LOCAL, "rb") as bundle_config_json:
bundle_config = json.load(bundle_config_json)
if not isinstance(bundle_config, dict) or not bundle_config:
logger.error("Local bundle list invalid. Skipped.")
raise FileNotFoundError("Bad local bundle list")
return bundle_config
except (FileNotFoundError, json.decoder.JSONDecodeError):
return dict()
def get_bundles_list():
"""
Retrieve the list of bundles from the config dictionary.
:return: List of supported bundles as Bundle objects.
"""
bundle_config = get_bundles_dict()
bundles_list = [Bundle(bundle_config[b]) for b in bundle_config]
logger.info("Using bundles: %s", ", ".join(b.key for b in bundles_list))
return bundles_list
def get_circup_version():
"""Return the version of circup that is running. If not available, return None.
:return: Current version of circup, or None.
"""
try:
from importlib import metadata # pylint: disable=import-outside-toplevel
except ImportError:
try:
import importlib_metadata as metadata # pylint: disable=import-outside-toplevel
except ImportError:
return None
try:
return metadata.version("circup")
except metadata.PackageNotFoundError:
return None
def get_dependencies(*requested_libraries, mod_names, to_install=()):
"""
Return a list of other CircuitPython libraries required by the given list
of libraries
:param tuple requested_libraries: The libraries to search for dependencies
:param object mod_names: All the modules metadata from bundle
:param list(str) to_install: Modules already selected for installation.
:return: tuple of module names to install which we build
"""
# pylint: disable=too-many-branches
# Internal variables
_to_install = to_install
_requested_libraries = []
_rl = requested_libraries[0]
if not requested_libraries[0]:
# If nothing is requested, we're done
return _to_install
for lib_name in _rl:
lower_lib_name = lib_name.lower()
if lower_lib_name in NOT_MCU_LIBRARIES:
logger.info(
"Skipping %s. It is not for microcontroller installs.", lib_name
)
else:
# Canonicalize, with some exceptions:
# adafruit-circuitpython-something => adafruit_something
canonical_lib_name = clean_library_name(lower_lib_name)
try:
# Don't process any names we can't find in mod_names
mod_names[canonical_lib_name] # pylint: disable=pointless-statement
_requested_libraries.append(canonical_lib_name)
except KeyError:
if canonical_lib_name not in WARNING_IGNORE_MODULES:
if os.path.exists(canonical_lib_name):
_requested_libraries.append(canonical_lib_name)
else:
click.secho(
f"WARNING:\n\t{canonical_lib_name} "
f"is not a known CircuitPython library.",
fg="yellow",
)
if not _requested_libraries:
# If nothing is requested, we're done
return _to_install
for library in list(_requested_libraries):
if library not in _to_install:
_to_install = _to_install + (library,)
# get the requirements.txt from bundle
try:
bundle = mod_names[library]["bundle"]
requirements_txt = bundle.requirements_for(library)
if requirements_txt:
_requested_libraries.extend(
libraries_from_requirements(requirements_txt)
)
circup_dependencies = get_circup_dependencies(bundle, library)
for circup_dependency in circup_dependencies:
_requested_libraries.append(circup_dependency)
except KeyError:
# don't check local file for further dependencies
pass
# we've processed this library, remove it from the list
_requested_libraries.remove(library)
return get_dependencies(
tuple(_requested_libraries), mod_names=mod_names, to_install=_to_install
)
def get_circup_dependencies(bundle, library):
"""
Get the list of circup dependencies from pyproject.toml
e.g.
[circup]
circup_dependencies = ["dependency_name_here"]
:param bundle: The Bundle to look within
:param library: The Library to find pyproject.toml for and get dependencies from
:return: The list of dependency libraries that were found
"""
try:
pyproj_toml = bundle.requirements_for(library, toml_file=True)
if pyproj_toml:
pyproj_toml_data = toml.loads(pyproj_toml)
dependencies = pyproj_toml_data["circup"]["circup_dependencies"]
if isinstance(dependencies, list):
return dependencies
if isinstance(dependencies, str):
return (dependencies,)
return tuple()
except KeyError:
# no circup_dependencies in pyproject.toml
return tuple()
def libraries_from_requirements(requirements):
"""
Clean up supplied requirements.txt and turn into tuple of CP libraries
:param str requirements: A string version of a requirements.txt
:return: tuple of library names
"""
libraries = ()
for line in requirements.split("\n"):
line = line.lower().strip()
if line.startswith("#") or line == "":
# skip comments
pass
else:
# Remove everything after any pip style version specifiers
line = re.split("[<>=~[;]", line)[0].strip()
libraries = libraries + (line,)
return libraries
def save_local_bundles(bundles_data):
"""
Save the list of local bundles to the settings.
:param str key: The bundle's identifier/key.
"""
if len(bundles_data) > 0:
with open(BUNDLE_CONFIG_LOCAL, "w", encoding="utf-8") as data:
json.dump(bundles_data, data)
else:
if os.path.isfile(BUNDLE_CONFIG_LOCAL):
os.unlink(BUNDLE_CONFIG_LOCAL)
def tags_data_save_tag(key, tag):
"""
Add or change the saved tag value for a bundle.
:param str key: The bundle's identifier/key.
:param str tag: The new tag for the bundle.
"""
tags_data = tags_data_load(logger)
tags_data[key] = tag
with open(BUNDLE_DATA, "w", encoding="utf-8") as data:
json.dump(tags_data, data)
def imports_from_code(full_content):
"""
Parse the given code.py file and return the imported libraries
Note that it's impossible at that level to differentiate between
import module.property and import module.submodule, so we try both
:param str full_content: Code to read imports from
:param str module_name: Name of the module the code is from
:return: sequence of library names
"""
# pylint: disable=too-many-branches
try:
par = ast.parse(full_content)
except (SyntaxError, ValueError) as err:
raise CodeParsingException(err) from err
imports = set()
for thing in ast.walk(par):
# import module and import module.submodule
if isinstance(thing, ast.Import):
for alias in thing.names:
imports.add(alias.name)
# from x import y
if isinstance(thing, ast.ImportFrom):
if thing.module:
# from [.][.]module import names
module = ("." * thing.level) + thing.module
imports.add(module)
for alias in thing.names:
imports.add(".".join([module, alias.name]))
else:
# from . import names
for alias in thing.names:
imports.add(alias.name)
# import parent modules (in practice it's the __init__.py)
for name in list(imports):
if "*" in name:
imports.remove(name)
continue
names = name.split(".")
for i in range(len(names)):
module = ".".join(names[: i + 1])
if module:
imports.add(module)
return sorted(imports)
def get_all_imports( # pylint: disable=too-many-arguments,too-many-locals, too-many-branches
backend, auto_file_content, auto_file_path, mod_names, current_module, visited=None
):
"""
Recursively retrieve imports from files on the backend
:param Backend backend: The current backend object
:param str auto_file_content: Content of the python file to analyse
:param str auto_file_path: Path to the python file to analyse
:param list mod_names: Lits of supported bundle mod names
:param str current_module: Name of the call context module if recursive call
:param set visited: Modules previously visited
:return: sequence of library names
"""
if visited is None:
visited = set()
visited.add(current_module)
requested_installs = []
try:
imports = imports_from_code(auto_file_content)
except CodeParsingException as err:
click.secho(f"Error parsing {current_module}:\n {err}", fg="red")
sys.exit(2)
for install in imports:
if install in visited:
continue
if install in mod_names:
requested_installs.append(install)
else:
# relative module paths
if install.startswith(".."):
install_module = ".".join(current_module.split(".")[:-2])
install_module = install_module + "." + install[2:]
elif install.startswith("."):
install_module = ".".join(current_module.split(".")[:-1])
install_module = install_module + "." + install[1:]
else:
install_module = install
# possible files for the module: .py or __init__.py (if directory)
file_name = os.path.join(*install_module.split(".")) + ".py"
try:
file_location = os.path.join(
*auto_file_path.replace(str(backend.device_location), "").split(
"/"
)[:-1]
)
full_location = os.path.join(file_location, file_name)
except TypeError:
# file is in root of CIRCUITPY
full_location = file_name
exists = backend.file_exists(full_location)
if not exists:
file_name = os.path.join(*install_module.split("."), "__init__.py")
full_location = file_name
exists = backend.file_exists(full_location)
if not exists:
continue
install_module += ".__init__"
# get the content and parse it recursively
auto_file_content = backend.get_file_content(full_location)
if auto_file_content:
sub_imports = get_all_imports(
backend,
auto_file_content,
auto_file_path,
mod_names,
install_module,
visited,
)
requested_installs.extend(sub_imports)
return sorted(requested_installs)
# [r for r in requested_installs if r in mod_names]
def libraries_from_auto_file(backend, auto_file, mod_names):
"""
Parse the input auto_file path and/or use the workflow to find the most
appropriate code.py script. Then return the list of imports
:param Backend backend: The current backend object
:param str auto_file: Path of the candidate auto file or None
:return: sequence of library names
"""
# find the current main file based on Circuitpython's rules
if auto_file is None:
root_files = [
file["name"] for file in backend.list_dir("") if not file["directory"]
]
for main_file in CODE_FILES:
if main_file in root_files:
auto_file = main_file
break
# still no code file found
if auto_file is None:
click.secho(
"No default code file found. See valid names:\n"
"https://docs.circuitpython.org/en/latest/README.html#behavior",
fg="red",
)
sys.exit(1)
# pass a local file with "./" or "../"
is_relative = auto_file.split(os.sep)[0] in [os.path.curdir, os.path.pardir]
if os.path.isabs(auto_file) or is_relative:
with open(auto_file, "r", encoding="UTF8") as fp:
auto_file_content = fp.read()
else:
auto_file_content = backend.get_file_content(auto_file)
if auto_file_content is None:
click.secho(f"Auto file not found: {auto_file}", fg="red")
sys.exit(1)
# from file name to module name (in case it's in a subpackage)
click.secho(f"Finding imports from: {auto_file}", fg="green")
current_module = auto_file.rstrip(".py").replace(os.path.sep, ".")
return get_all_imports(
backend, auto_file_content, auto_file, mod_names, current_module
)
def get_device_path(host, port, password, path):
"""
:param host Hostname or IP address.
:param password REST API password.
:param path File system path.
:return device URL or None if the device cannot be found.
"""
if path:
device_path = path
elif host:
# pylint: enable=no-member
device_path = f"http://:{password}@{host}:{port}"
else:
device_path = find_device()
return device_path
def sorted_by_directory_then_alpha(list_of_files):
"""
Sort the list of files into alphabetical seperated
with directories grouped together before files.
"""
dirs = {}
files = {}
for cur_file in list_of_files:
if cur_file["directory"]:
dirs[cur_file["name"]] = cur_file
else:
files[cur_file["name"]] = cur_file
sorted_dir_names = sorted(dirs.keys())
sorted_file_names = sorted(files.keys())
sorted_full_list = []
for cur_name in sorted_dir_names:
sorted_full_list.append(dirs[cur_name])
for cur_name in sorted_file_names:
sorted_full_list.append(files[cur_name])
return sorted_full_list

755
circup/commands.py Normal file
View file

@ -0,0 +1,755 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, 2024 Tim Cocks, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
# ----------- CLI command definitions ----------- #
The following functions have IO side effects (for instance they emit to
stdout). Ergo, these are not checked with unit tests. Most of the
functionality they provide is provided by the functions from util_functions.py,
and the respective Backends which *are* tested. Most of the logic of the following
functions is to prepare things for presentation to / interaction with the user.
"""
import os
import subprocess
import time
import sys
import re
import logging
import update_checker
from semver import VersionInfo
import click
import requests
from circup.backends import WebBackend, DiskBackend
from circup.logging import logger, log_formatter, LOGFILE
from circup.shared import BOARDLESS_COMMANDS, get_latest_release_from_url
from circup.bundle import Bundle
from circup.command_utils import (
get_device_path,
get_circup_version,
find_modules,
get_bundles_list,
completion_for_install,
get_bundle_versions,
libraries_from_requirements,
libraries_from_auto_file,
get_dependencies,
get_bundles_local_dict,
save_local_bundles,
get_bundles_dict,
completion_for_example,
get_bundle_examples,
)
@click.group()
@click.option(
"--verbose", is_flag=True, help="Comprehensive logging is sent to stdout."
)
@click.option(
"--path",
type=click.Path(exists=True, file_okay=False),
help="Path to CircuitPython directory. Overrides automatic path detection.",
)
@click.option(
"--host",
help="Hostname or IP address of a device. Overrides automatic path detection.",
)
@click.option(
"--port", help="Port to contact. Overrides automatic path detection.", default=80
)
@click.option(
"--password",
help="Password to use for authentication when --host is used."
" You can optionally set an environment variable CIRCUP_WEBWORKFLOW_PASSWORD"
" instead of passing this argument. If both exist the CLI arg takes precedent.",
)
@click.option(
"--timeout",
default=30,
help="Specify the timeout in seconds for any network operations.",
)
@click.option(
"--board-id",
default=None,
help="Manual Board ID of the CircuitPython device. If provided in combination "
"with --cpy-version, it overrides the detected board ID.",
)
@click.option(
"--cpy-version",
default=None,
help="Manual CircuitPython version. If provided in combination "
"with --board-id, it overrides the detected CPy version.",
)
@click.version_option(
prog_name="Circup",
message="%(prog)s, A CircuitPython module updater. Version %(version)s",
)
@click.pass_context
def main( # pylint: disable=too-many-locals
ctx, verbose, path, host, port, password, timeout, board_id, cpy_version
): # pragma: no cover
"""
A tool to manage and update libraries on a CircuitPython device.
"""
# pylint: disable=too-many-arguments,too-many-branches,too-many-statements,too-many-locals, R0801
ctx.ensure_object(dict)
ctx.obj["TIMEOUT"] = timeout
if password is None:
password = os.getenv("CIRCUP_WEBWORKFLOW_PASSWORD")
device_path = get_device_path(host, port, password, path)
using_webworkflow = "host" in ctx.params.keys() and ctx.params["host"] is not None
if using_webworkflow:
if host == "circuitpython.local":
click.echo("Checking versions.json on circuitpython.local to find hostname")
versions_resp = requests.get(
"http://circuitpython.local/cp/version.json", timeout=timeout
)
host = f'{versions_resp.json()["hostname"]}.local'
click.echo(f"Using hostname: {host}")
device_path = device_path.replace("circuitpython.local", host)
try:
ctx.obj["backend"] = WebBackend(
host=host,
port=port,
password=password,
logger=logger,
timeout=timeout,
version_override=cpy_version,
)
except ValueError as e:
click.secho(e, fg="red")
time.sleep(0.3)
sys.exit(1)
except RuntimeError as e:
click.secho(e, fg="red")
sys.exit(1)
else:
try:
ctx.obj["backend"] = DiskBackend(
device_path,
logger,
version_override=cpy_version,
)
except ValueError as e:
print(e)
if verbose:
# Configure additional logging to stdout.
ctx.obj["verbose"] = True
verbose_handler = logging.StreamHandler(sys.stdout)
verbose_handler.setLevel(logging.INFO)
verbose_handler.setFormatter(log_formatter)
logger.addHandler(verbose_handler)
click.echo("Logging to {}\n".format(LOGFILE))
else:
ctx.obj["verbose"] = False
logger.info("### Started Circup ###")
# If a newer version of circup is available, print a message.
logger.info("Checking for a newer version of circup")
version = get_circup_version()
if version:
update_checker.update_check("circup", version)
# stop early if the command is boardless
if ctx.invoked_subcommand in BOARDLESS_COMMANDS or "--help" in sys.argv:
return
ctx.obj["DEVICE_PATH"] = device_path
latest_version = get_latest_release_from_url(
"https://github.com/adafruit/circuitpython/releases/latest", logger
)
if device_path is None or not ctx.obj["backend"].is_device_present():
click.secho("Could not find a connected CircuitPython device.", fg="red")
sys.exit(1)
else:
cpy_version, board_id = (
ctx.obj["backend"].get_circuitpython_version()
if board_id is None or cpy_version is None
else (cpy_version, board_id)
)
click.echo(
"Found device {} at {}, running CircuitPython {}.".format(
board_id, device_path, cpy_version
)
)
try:
if VersionInfo.parse(cpy_version) < VersionInfo.parse(latest_version):
click.secho(
"A newer version of CircuitPython ({}) is available.".format(
latest_version
),
fg="green",
)
if board_id:
url_download = f"https://circuitpython.org/board/{board_id}"
else:
url_download = "https://circuitpython.org/downloads"
click.secho("Get it here: {}".format(url_download), fg="green")
except ValueError as ex:
logger.warning("CircuitPython has incorrect semver value.")
logger.warning(ex)
@main.command()
@click.option("-r", "--requirement", is_flag=True)
@click.pass_context
def freeze(ctx, requirement): # pragma: no cover
"""
Output details of all the modules found on the connected CIRCUITPYTHON
device. Option -r saves output to requirements.txt file
"""
logger.info("Freeze")
modules = find_modules(ctx.obj["backend"], get_bundles_list())
if modules:
output = []
for module in modules:
output.append("{}=={}".format(module.name, module.device_version))
for module in output:
click.echo(module)
logger.info(module)
if requirement:
cwd = os.path.abspath(os.getcwd())
for i, module in enumerate(output):
output[i] += "\n"
overwrite = None
if os.path.exists(os.path.join(cwd, "requirements.txt")):
overwrite = click.confirm(
click.style(
"\nrequirements.txt file already exists in this location.\n"
"Do you want to overwrite it?",
fg="red",
),
default=False,
)
else:
overwrite = True
if overwrite:
with open(
cwd + "/" + "requirements.txt", "w", newline="\n", encoding="utf-8"
) as file:
file.truncate(0)
file.writelines(output)
else:
click.echo("No modules found on the device.")
@main.command("list")
@click.pass_context
def list_cli(ctx): # pragma: no cover
"""
Lists all out of date modules found on the connected CIRCUITPYTHON device.
"""
logger.info("List")
# Grab out of date modules.
data = [("Module", "Version", "Latest", "Update Reason")]
modules = [
m.row
for m in find_modules(ctx.obj["backend"], get_bundles_list())
if m.outofdate
]
if modules:
data += modules
# Nice tabular display.
col_width = [0, 0, 0, 0]
for row in data:
for i, word in enumerate(row):
col_width[i] = max(len(word) + 2, col_width[i])
dashes = tuple(("-" * (width - 1) for width in col_width))
data.insert(1, dashes)
click.echo(
"The following modules are out of date or probably need an update.\n"
"Major Updates may include breaking changes. Review before updating.\n"
"MPY Format changes from Circuitpython 8 to 9 require an update.\n"
)
for row in data:
output = ""
for index, cell in enumerate(row):
output += cell.ljust(col_width[index])
if "--verbose" not in sys.argv:
click.echo(output)
logger.info(output)
else:
click.echo("All modules found on the device are up to date.")
# pylint: disable=too-many-arguments,too-many-locals
@main.command()
@click.argument(
"modules", required=False, nargs=-1, shell_complete=completion_for_install
)
@click.option(
"pyext",
"--py",
is_flag=True,
help="Install the .py version of the module(s) instead of the mpy version.",
)
@click.option(
"-r",
"--requirement",
type=click.Path(exists=True, dir_okay=False),
help="specify a text file to install all modules listed in the text file."
" Typically requirements.txt.",
)
@click.option(
"--auto", "-a", is_flag=True, help="Install the modules imported in code.py."
)
@click.option(
"--upgrade", "-U", is_flag=True, help="Upgrade modules that are already installed."
)
@click.option(
"--stubs",
"-s",
is_flag=True,
help="Install stubs module from PyPi for context in IDE.",
)
@click.option(
"--auto-file",
default=None,
help="Specify the name of a file on the board to read for auto install."
" Also accepts an absolute path or a local ./ path.",
)
@click.pass_context
def install(
ctx, modules, pyext, requirement, auto, auto_file, upgrade=False, stubs=False
): # pragma: no cover
"""
Install a named module(s) onto the device. Multiple modules
can be installed at once by providing more than one module name, each
separated by a space. Modules can be from a Bundle or local filepaths.
"""
# pylint: disable=too-many-branches
# TODO: Ensure there's enough space on the device
available_modules = get_bundle_versions(get_bundles_list())
mod_names = {}
for module, metadata in available_modules.items():
mod_names[module.replace(".py", "").lower()] = metadata
if requirement:
with open(requirement, "r", encoding="utf-8") as rfile:
requirements_txt = rfile.read()
requested_installs = libraries_from_requirements(requirements_txt)
elif auto or auto_file:
requested_installs = libraries_from_auto_file(
ctx.obj["backend"], auto_file, mod_names
)
else:
requested_installs = modules
requested_installs = sorted(set(requested_installs))
click.echo(f"Searching for dependencies for: {requested_installs}")
to_install = get_dependencies(requested_installs, mod_names=mod_names)
device_modules = ctx.obj["backend"].get_device_versions()
if to_install is not None:
to_install = sorted(to_install)
click.echo(f"Ready to install: {to_install}\n")
for library in to_install:
ctx.obj["backend"].install_module(
ctx.obj["DEVICE_PATH"],
device_modules,
library,
pyext,
mod_names,
upgrade,
)
if stubs:
library_stubs = "adafruit-circuitpython-{}".format(
library.replace("adafruit_", "")
)
try:
output = subprocess.check_output(["pip", "install", library_stubs])
if (
f"Requirement already satisfied: {library_stubs}"
in output.decode()
):
click.echo(f"'{library}' stubs already installed.")
else:
click.echo(f"Installed '{library}' stubs.")
except subprocess.CalledProcessError:
click.secho(
f"Could not install stubs module {library_stubs}", fg="yellow"
)
@main.command()
@click.option("--overwrite", is_flag=True, help="Overwrite the file if it exists.")
@click.option("--list", "-ls", "op_list", is_flag=True, help="List available examples.")
@click.option("--rename", is_flag=True, help="Install the example as code.py.")
@click.argument(
"examples", required=False, nargs=-1, shell_complete=completion_for_example
)
@click.pass_context
def example(ctx, examples, op_list, rename, overwrite):
"""
Copy named example(s) from a bundle onto the device. Multiple examples
can be installed at once by providing more than one example name, each
separated by a space.
"""
if op_list:
if examples:
click.echo("\n".join(completion_for_example(ctx, "", examples)))
else:
click.echo("Available example libraries:")
available_examples = get_bundle_examples(
get_bundles_list(), avoid_download=True
)
lib_names = {
str(key.split(os.path.sep)[0]): value
for key, value in available_examples.items()
}
click.echo("\n".join(sorted(lib_names.keys())))
return
for example_arg in examples:
available_examples = get_bundle_examples(
get_bundles_list(), avoid_download=True
)
if example_arg in available_examples:
filename = available_examples[example_arg].split(os.path.sep)[-1]
install_metadata = {"path": available_examples[example_arg]}
filename = available_examples[example_arg].split(os.path.sep)[-1]
if rename:
if os.path.isfile(available_examples[example_arg]):
filename = "code.py"
install_metadata["target_name"] = filename
if overwrite or not ctx.obj["backend"].file_exists(filename):
click.echo(
f"{'Copying' if not overwrite else 'Overwriting'}: {filename}"
)
ctx.obj["backend"].install_module_py(install_metadata, location="")
else:
click.secho(
f"File: {filename} already exists. Use --overwrite if you wish to replace it.",
fg="red",
)
else:
click.secho(
f"Error: {example_arg} was not found in any local bundle examples.",
fg="red",
)
# pylint: enable=too-many-arguments,too-many-locals
@main.command()
@click.argument("match", required=False, nargs=1)
def show(match): # pragma: no cover
"""
Show a list of available modules in the bundle. These are modules which
*could* be installed on the device.
If MATCH is specified only matching modules will be listed.
"""
available_modules = get_bundle_versions(get_bundles_list())
module_names = sorted([m.replace(".py", "") for m in available_modules])
if match is not None:
match = match.lower()
module_names = [m for m in module_names if match in m]
click.echo("\n".join(module_names))
click.echo(
"{} shown of {} packages.".format(len(module_names), len(available_modules))
)
@main.command()
@click.argument("module", nargs=-1)
@click.pass_context
def uninstall(ctx, module): # pragma: no cover
"""
Uninstall a named module(s) from the connected device. Multiple modules
can be uninstalled at once by providing more than one module name, each
separated by a space.
"""
device_path = ctx.obj["DEVICE_PATH"]
print(f"Uninstalling {module} from {device_path}")
for name in module:
device_modules = ctx.obj["backend"].get_device_versions()
name = name.lower()
mod_names = {}
for module_item, metadata in device_modules.items():
mod_names[module_item.replace(".py", "").lower()] = metadata
if name in mod_names:
metadata = mod_names[name]
module_path = metadata["path"]
ctx.obj["backend"].uninstall(device_path, module_path)
click.echo("Uninstalled '{}'.".format(name))
else:
click.echo("Module '{}' not found on device.".format(name))
continue
# pylint: disable=too-many-branches
@main.command(
short_help=(
"Update modules on the device. "
"Use --all to automatically update all modules without Major Version warnings."
)
)
@click.option(
"update_all",
"--all",
is_flag=True,
help="Update all modules without Major Version warnings.",
)
@click.pass_context
# pylint: disable=too-many-locals
def update(ctx, update_all): # pragma: no cover
"""
Checks for out-of-date modules on the connected CIRCUITPYTHON device, and
prompts the user to confirm updating such modules.
"""
logger.info("Update")
# Grab current modules.
bundles_list = get_bundles_list()
installed_modules = find_modules(ctx.obj["backend"], bundles_list)
modules_to_update = [m for m in installed_modules if m.outofdate]
if not modules_to_update:
click.echo("None of the module[s] found on the device need an update.")
return
# Process out of date modules
updated_modules = []
click.echo("Found {} module[s] needing update.".format(len(modules_to_update)))
if not update_all:
click.echo("Please indicate which module[s] you wish to update:\n")
for module in modules_to_update:
update_flag = update_all
if "--verbose" in sys.argv:
click.echo(
"Device version: {}, Bundle version: {}".format(
module.device_version, module.bundle_version
)
)
if isinstance(module.bundle_version, str) and not VersionInfo.is_valid(
module.bundle_version
):
click.secho(
f"WARNING: Library {module.name} repo has incorrect __version__"
"\n\tmetadata. Circup will assume it needs updating."
"\n\tPlease file an issue in the library repo.",
fg="yellow",
)
if module.repo:
click.secho(f"\t{module.repo}", fg="yellow")
if not update_flag:
if module.bad_format:
click.secho(
f"WARNING: '{module.name}': module corrupted or in an"
" unknown mpy format. Updating is required.",
fg="yellow",
)
update_flag = click.confirm("Do you want to update?")
elif module.mpy_mismatch:
click.secho(
f"WARNING: '{module.name}': mpy format doesn't match the"
" device's Circuitpython version. Updating is required.",
fg="yellow",
)
update_flag = click.confirm("Do you want to update?")
elif module.major_update:
update_flag = click.confirm(
(
"'{}' is a Major Version update and may contain breaking "
"changes. Do you want to update?".format(module.name)
)
)
else:
update_flag = click.confirm("Update '{}'?".format(module.name))
if update_flag:
# pylint: disable=broad-except
try:
ctx.obj["backend"].update(module)
updated_modules.append(module.name)
click.echo("Updated {}".format(module.name))
except Exception as ex:
logger.exception(ex)
click.echo("Something went wrong, {} (check the logs)".format(str(ex)))
# pylint: enable=broad-except
if not updated_modules:
return
# We updated modules, look to see if any requirements are missing
click.echo(
"Checking {} updated module[s] for missing requirements.".format(
len(updated_modules)
)
)
available_modules = get_bundle_versions(bundles_list)
mod_names = {}
for module, metadata in available_modules.items():
mod_names[module.replace(".py", "").lower()] = metadata
missing_modules = get_dependencies(updated_modules, mod_names=mod_names)
device_modules = ctx.obj["backend"].get_device_versions()
# Process newly needed modules
if missing_modules is not None:
installed_module_names = [m.name for m in installed_modules]
missing_modules = set(missing_modules) - set(installed_module_names)
missing_modules = sorted(list(missing_modules))
click.echo(f"Ready to install: {missing_modules}\n")
for library in missing_modules:
ctx.obj["backend"].install_module(
ctx.obj["DEVICE_PATH"], device_modules, library, False, mod_names
)
# pylint: enable=too-many-branches
@main.command("bundle-show")
@click.option("--modules", is_flag=True, help="List all the modules per bundle.")
def bundle_show(modules):
"""
Show the list of bundles, default and local, with URL, current version
and latest version retrieved from the web.
"""
local_bundles = get_bundles_local_dict().values()
bundles = get_bundles_list()
available_modules = get_bundle_versions(bundles)
for bundle in bundles:
if bundle.key in local_bundles:
click.secho(bundle.key, fg="yellow")
else:
click.secho(bundle.key, fg="green")
click.echo(" " + bundle.url)
click.echo(" version = " + bundle.current_tag)
if modules:
click.echo("Modules:")
for name, mod in sorted(available_modules.items()):
if mod["bundle"] == bundle:
click.echo(f" {name} ({mod.get('__version__', '-')})")
@main.command("bundle-add")
@click.argument("bundle", nargs=-1)
@click.pass_context
def bundle_add(ctx, bundle):
"""
Add bundles to the local bundles list, by "user/repo" github string.
A series of tests to validate that the bundle exists and at least looks
like a bundle are done before validating it. There might still be errors
when the bundle is downloaded for the first time.
"""
if len(bundle) == 0:
click.secho(
"Must pass bundle argument, expecting github URL or `user/repository` string.",
fg="red",
)
return
bundles_dict = get_bundles_local_dict()
modified = False
for bundle_repo in bundle:
# cleanup in case seombody pastes the URL to the repo/releases
bundle_repo = re.sub(
r"https?://github.com/([^/]+/[^/]+)(/.*)?", r"\1", bundle_repo
)
if bundle_repo in bundles_dict.values():
click.secho("Bundle already in list.", fg="yellow")
click.secho(" " + bundle_repo, fg="yellow")
continue
try:
bundle_added = Bundle(bundle_repo)
except ValueError:
click.secho(
"Bundle string invalid, expecting github URL or `user/repository` string.",
fg="red",
)
click.secho(" " + bundle_repo, fg="red")
continue
result = requests.get(
"https://github.com/" + bundle_repo, timeout=ctx.obj["TIMEOUT"]
)
# pylint: disable=no-member
if result.status_code == requests.codes.NOT_FOUND:
click.secho("Bundle invalid, the repository doesn't exist (404).", fg="red")
click.secho(" " + bundle_repo, fg="red")
continue
# pylint: enable=no-member
if not bundle_added.validate():
click.secho(
"Bundle invalid, is the repository a valid circup bundle ?", fg="red"
)
click.secho(" " + bundle_repo, fg="red")
continue
# note: use bun as the dictionary key for uniqueness
bundles_dict[bundle_repo] = bundle_repo
modified = True
click.echo("Added " + bundle_repo)
click.echo(" " + bundle_added.url)
if modified:
# save the bundles list
save_local_bundles(bundles_dict)
# update and get the new bundles for the first time
get_bundle_versions(get_bundles_list())
@main.command("bundle-remove")
@click.argument("bundle", nargs=-1)
@click.option("--reset", is_flag=True, help="Remove all local bundles.")
def bundle_remove(bundle, reset):
"""
Remove one or more bundles from the local bundles list.
"""
if reset:
save_local_bundles({})
return
if len(bundle) == 0:
click.secho(
"Must pass bundle argument or --reset, expecting github URL or "
"`user/repository` string. Run circup bundle-show to see a list of bundles.",
fg="red",
)
return
bundle_config = list(get_bundles_dict().values())
bundles_local_dict = get_bundles_local_dict()
modified = False
for bun in bundle:
# cleanup in case somebody pastes the URL to the repo/releases
bun = re.sub(r"https?://github.com/([^/]+/[^/]+)(/.*)?", r"\1", bun)
found = False
for name, repo in list(bundles_local_dict.items()):
if bun in (name, repo):
found = True
click.secho(f"Bundle {repo}")
do_it = click.confirm("Do you want to remove that bundle ?")
if do_it:
click.secho("Removing the bundle from the local list", fg="yellow")
click.secho(f" {bun}", fg="yellow")
modified = True
del bundles_local_dict[name]
if not found:
if bun in bundle_config:
click.secho("Cannot remove built-in module:" "\n " + bun, fg="red")
else:
click.secho(
"Bundle not found in the local list, nothing removed:"
"\n " + bun,
fg="red",
)
if modified:
save_local_bundles(bundles_local_dict)

View file

@ -0,0 +1,4 @@
{
"adafruit": "adafruit/Adafruit_CircuitPython_Bundle",
"circuitpython_community": "adafruit/CircuitPython_Community_Bundle"
}

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2021 Patrick Walters
#
# SPDX-License-Identifier: MIT

33
circup/logging.py Normal file
View file

@ -0,0 +1,33 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, 2024 Tim Cocks, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
Logging utilities and configuration used by circup
"""
import os
import logging
from logging.handlers import RotatingFileHandler
import appdirs
from circup.shared import DATA_DIR
#: The directory containing the utility's log file.
LOG_DIR = appdirs.user_log_dir(appname="circup", appauthor="adafruit")
#: The location of the log file for the utility.
LOGFILE = os.path.join(LOG_DIR, "circup.log")
# Ensure DATA_DIR / LOG_DIR related directories and files exist.
if not os.path.exists(DATA_DIR): # pragma: no cover
os.makedirs(DATA_DIR)
if not os.path.exists(LOG_DIR): # pragma: no cover
os.makedirs(LOG_DIR)
# Setup logging.
logger = logging.getLogger(__name__)
logger.setLevel(logging.INFO)
logfile_handler = RotatingFileHandler(LOGFILE, maxBytes=10_000_000, backupCount=0)
log_formatter = logging.Formatter(
"%(asctime)s %(levelname)s: %(message)s", datefmt="%m/%d/%Y %H:%M:%S"
)
logfile_handler.setFormatter(log_formatter)
logger.addHandler(logfile_handler)

209
circup/module.py Normal file
View file

@ -0,0 +1,209 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, 2024 Tim Cocks, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
Class that represents a specific CircuitPython module on a device or in a Bundle.
"""
import os
from urllib.parse import urljoin, urlparse
from semver import VersionInfo
from circup.shared import BAD_FILE_FORMAT
from circup.backends import WebBackend
from circup.logging import logger
class Module:
"""
Represents a CircuitPython module.
"""
# pylint: disable=too-many-arguments
def __init__(
self,
name,
backend,
repo,
device_version,
bundle_version,
mpy,
bundle,
compatibility,
):
"""
The ``self.file`` and ``self.name`` attributes are constructed from
the ``path`` value. If the path is to a directory based module, the
resulting self.file value will be None, and the name will be the
basename of the directory path.
:param str name: The file name of the module.
:param Backend backend: The backend that the module is on.
:param str repo: The URL of the Git repository for this module.
:param str device_version: The semver value for the version on device.
:param str bundle_version: The semver value for the version in bundle.
:param bool mpy: Flag to indicate if the module is byte-code compiled.
:param Bundle bundle: Bundle object where the module is located.
:param (str,str) compatibility: Min and max versions of CP compatible with the mpy.
"""
self.name = name
self.backend = backend
self.path = (
urljoin(backend.library_path, name, allow_fragments=False)
if isinstance(backend, WebBackend)
else os.path.join(backend.library_path, name)
)
url = urlparse(self.path, allow_fragments=False)
if (
url.path.endswith("/")
if isinstance(backend, WebBackend)
else self.path.endswith(os.sep)
):
self.file = None
self.name = self.path.split(
"/" if isinstance(backend, WebBackend) else os.sep
)[-2]
else:
self.file = os.path.basename(url.path)
self.name = (
os.path.basename(url.path).replace(".py", "").replace(".mpy", "")
)
self.repo = repo
self.device_version = device_version
self.bundle_version = bundle_version
self.mpy = mpy
self.min_version = compatibility[0]
self.max_version = compatibility[1]
# Figure out the bundle path.
self.bundle_path = None
if self.mpy:
# Byte compiled, now check CircuitPython version.
major_version = self.backend.get_circuitpython_version()[0].split(".")[0]
bundle_platform = "{}mpy".format(major_version)
else:
# Regular Python
bundle_platform = "py"
# module path in the bundle
search_path = bundle.lib_dir(bundle_platform)
if self.file:
self.bundle_path = os.path.join(search_path, self.file)
else:
self.bundle_path = os.path.join(search_path, self.name)
logger.info(self)
# pylint: enable=too-many-arguments
@property
def outofdate(self):
"""
Returns a boolean to indicate if this module is out of date.
Treat mismatched MPY versions as out of date.
:return: Truthy indication if the module is out of date.
"""
if self.mpy_mismatch:
return True
if self.device_version and self.bundle_version:
try:
return VersionInfo.parse(self.device_version) < VersionInfo.parse(
self.bundle_version
)
except ValueError as ex:
logger.warning("Module '%s' has incorrect semver value.", self.name)
logger.warning(ex)
return True # Assume out of date to try to update.
@property
def bad_format(self):
"""A boolean indicating that the mpy file format could not be identified"""
return self.mpy and self.device_version == BAD_FILE_FORMAT
@property
def mpy_mismatch(self):
"""
Returns a boolean to indicate if this module's MPY version is compatible
with the board's current version of Circuitpython. A min or max version
that evals to False means no limit.
:return: Boolean indicating if the MPY versions don't match.
"""
if not self.mpy:
return False
try:
cpv = VersionInfo.parse(self.backend.get_circuitpython_version()[0])
except ValueError as ex:
logger.warning("CircuitPython has incorrect semver value.")
logger.warning(ex)
try:
if self.min_version and cpv < VersionInfo.parse(self.min_version):
return True # CP version too old
if self.max_version and cpv >= VersionInfo.parse(self.max_version):
return True # MPY version too old
except (TypeError, ValueError) as ex:
logger.warning(
"Module '%s' has incorrect MPY compatibility information.", self.name
)
logger.warning(ex)
return False
@property
def major_update(self):
"""
Returns a boolean to indicate if this is a major version update.
:return: Boolean indicating if this is a major version upgrade
"""
try:
if (
VersionInfo.parse(self.device_version).major
== VersionInfo.parse(self.bundle_version).major
):
return False
except (TypeError, ValueError) as ex:
logger.warning("Module '%s' has incorrect semver value.", self.name)
logger.warning(ex)
return True # Assume Major Version udpate.
@property
def row(self):
"""
Returns a tuple of items to display in a table row to show the module's
name, local version and remote version, and reason to update.
:return: A tuple containing the module's name, version on the connected
device, version in the latest bundle and reason to update.
"""
loc = self.device_version if self.device_version else "unknown"
rem = self.bundle_version if self.bundle_version else "unknown"
if self.mpy_mismatch:
update_reason = "MPY Format"
elif self.major_update:
update_reason = "Major Version"
else:
update_reason = "Minor Version"
return (self.name, loc, rem, update_reason)
def __repr__(self):
"""
Helps with log files.
:return: A repr of a dictionary containing the module's metadata.
"""
return repr(
{
"path": self.path,
"file": self.file,
"name": self.name,
"repo": self.repo,
"device_version": self.device_version,
"bundle_version": self.bundle_version,
"bundle_path": self.bundle_path,
"mpy": self.mpy,
"min_version": self.min_version,
"max_version": self.max_version,
}
)

221
circup/shared.py Normal file
View file

@ -0,0 +1,221 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
# SPDX-FileCopyrightText: 2023 Tim Cocks, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
Utilities that are shared and used by both click CLI command functions
and Backend class functions.
"""
import glob
import os
import re
import json
import importlib.resources
import appdirs
import requests
#: Version identifier for a bad MPY file format
BAD_FILE_FORMAT = "Invalid"
#: The location of data files used by circup (following OS conventions).
DATA_DIR = appdirs.user_data_dir(appname="circup", appauthor="adafruit")
#: Module formats list (and the other form used in github files)
PLATFORMS = {"py": "py", "9mpy": "9.x-mpy", "10mpy": "10.x-mpy"}
#: Timeout for requests calls like get()
REQUESTS_TIMEOUT = 30
#: The path to the JSON file containing the metadata about the bundles.
BUNDLE_CONFIG_FILE = importlib.resources.files("circup") / "config/bundle_config.json"
#: Overwrite the bundles list with this file (only done manually)
BUNDLE_CONFIG_OVERWRITE = os.path.join(DATA_DIR, "bundle_config.json")
#: The path to the JSON file containing the local list of bundles.
BUNDLE_CONFIG_LOCAL = os.path.join(DATA_DIR, "bundle_config_local.json")
#: The path to the JSON file containing the metadata about the bundles.
BUNDLE_DATA = os.path.join(DATA_DIR, "circup.json")
#: The libraries (and blank lines) which don't go on devices
NOT_MCU_LIBRARIES = [
"",
"adafruit-blinka",
"adafruit-blinka-bleio",
"adafruit-blinka-displayio",
"adafruit-circuitpython-typing",
"circuitpython_typing",
"pyserial",
]
#: Commands that do not require an attached board
BOARDLESS_COMMANDS = ["show", "bundle-add", "bundle-remove", "bundle-show"]
def _get_modules_file(path, logger):
"""
Get a dictionary containing metadata about all the Python modules found in
the referenced file system path.
:param str path: The directory in which to find modules.
:return: A dictionary containing metadata about the found modules.
"""
result = {}
if not path:
return result
single_file_py_mods = glob.glob(os.path.join(path, "*.py"))
single_file_mpy_mods = glob.glob(os.path.join(path, "*.mpy"))
package_dir_mods = [
d
for d in glob.glob(os.path.join(path, "*", ""))
if not os.path.basename(os.path.normpath(d)).startswith(".")
]
single_file_mods = single_file_py_mods + single_file_mpy_mods
for sfm in [f for f in single_file_mods if not os.path.basename(f).startswith(".")]:
metadata = extract_metadata(sfm, logger)
metadata["path"] = sfm
result[os.path.basename(sfm).replace(".py", "").replace(".mpy", "")] = metadata
for package_path in package_dir_mods:
name = os.path.basename(os.path.dirname(package_path))
py_files = glob.glob(os.path.join(package_path, "**/*.py"), recursive=True)
mpy_files = glob.glob(os.path.join(package_path, "**/*.mpy"), recursive=True)
all_files = py_files + mpy_files
# put __init__ first if any, assumed to have the version number
all_files.sort()
# default value
result[name] = {"path": package_path, "mpy": bool(mpy_files)}
# explore all the submodules to detect bad ones
for source in [f for f in all_files if not os.path.basename(f).startswith(".")]:
metadata = extract_metadata(source, logger)
if "__version__" in metadata:
# don't replace metadata if already found
if "__version__" not in result[name]:
metadata["path"] = package_path
result[name] = metadata
# break now if any of the submodules has a bad format
if metadata["__version__"] == BAD_FILE_FORMAT:
break
return result
def extract_metadata(path, logger):
# pylint: disable=too-many-locals,too-many-branches
"""
Given a file path, return a dictionary containing metadata extracted from
dunder attributes found therein. Works with both .py and .mpy files.
For Python source files, such metadata assignments should be simple and
single-line. For example::
__version__ = "1.1.4"
__repo__ = "https://github.com/adafruit/SomeLibrary.git"
For byte compiled .mpy files, a brute force / backtrack approach is used
to find the __version__ number in the file -- see comments in the
code for the implementation details.
:param str path: The path to the file containing the metadata.
:return: The dunder based metadata found in the file, as a dictionary.
"""
result = {}
logger.info("%s", path)
if path.endswith(".py"):
result["mpy"] = False
with open(path, "r", encoding="utf-8") as source_file:
content = source_file.read()
#: The regex used to extract ``__version__`` and ``__repo__`` assignments.
dunder_key_val = r"""(__\w+__)(?:\s*:\s*\w+)?\s*=\s*(?:['"]|\(\s)(.+)['"]"""
for match in re.findall(dunder_key_val, content):
result[match[0]] = str(match[1])
if result:
logger.info("Extracted metadata: %s", result)
elif path.endswith(".mpy"):
find_by_regexp_match = False
result["mpy"] = True
with open(path, "rb") as mpy_file:
content = mpy_file.read()
# Track the MPY version number
mpy_version = content[0:2]
compatibility = None
loc = -1
# Find the start location of the __version__
if mpy_version == b"M\x03":
# One byte for the length of "__version__"
loc = content.find(b"__version__") - 1
compatibility = (None, "7.0.0-alpha.1")
elif mpy_version == b"C\x05":
# Two bytes for the length of "__version__" in mpy version 5
loc = content.find(b"__version__") - 2
compatibility = ("7.0.0-alpha.1", "8.99.99")
elif mpy_version == b"C\x06":
# Two bytes in mpy version 6
find_by_regexp_match = True
compatibility = ("9.0.0-alpha.1", None)
if find_by_regexp_match:
# Too hard to find the version positionally.
# Find the first thing that looks like an x.y.z version number.
match = re.search(rb"([\d]+\.[\d]+\.[\d]+)\x00", content)
if match:
result["__version__"] = match.group(1).decode("utf-8")
elif loc > -1:
# Backtrack until a byte value of the offset is reached.
offset = 1
while offset < loc:
val = int(content[loc - offset])
if mpy_version == b"C\x05":
val = val // 2
if val == offset - 1: # Off by one..!
# Found version, extract the number given boundaries.
start = loc - offset + 1 # No need for prepended length.
end = loc # Up to the start of the __version__.
version = content[start:end] # Slice the version number.
# Create a string version as metadata in the result.
result["__version__"] = version.decode("utf-8")
break # Nothing more to do.
offset += 1 # ...and again but backtrack by one.
if compatibility:
result["compatibility"] = compatibility
else:
# not a valid MPY file
result["__version__"] = BAD_FILE_FORMAT
return result
def tags_data_load(logger):
"""
Load the list of the version tags of the bundles on disk.
:return: a dict() of tags indexed by Bundle identifiers/keys.
"""
tags_data = None
try:
with open(BUNDLE_DATA, encoding="utf-8") as data:
try:
tags_data = json.load(data)
except json.decoder.JSONDecodeError as ex:
# Sometimes (why?) the JSON file becomes corrupt. In which case
# log it and carry on as if setting up for first time.
logger.error("Could not parse %s", BUNDLE_DATA)
logger.exception(ex)
except FileNotFoundError:
pass
if not isinstance(tags_data, dict):
tags_data = {}
return tags_data
def get_latest_release_from_url(url, logger):
"""
Find the tag name of the latest release by using HTTP HEAD and decoding the redirect.
:param str url: URL to the latest release page on a git repository.
:return: The most recent tag value for the release.
"""
logger.info("Requesting redirect information: %s", url)
response = requests.head(url, timeout=REQUESTS_TIMEOUT)
responseurl = response.url
if response.is_redirect:
responseurl = response.headers["Location"]
tag = responseurl.rsplit("/", 1)[-1]
logger.info("Tag: '%s'", tag)
return tag

105
circup/wwshell/README.rst Normal file
View file

@ -0,0 +1,105 @@
wwshell
=======
.. image:: https://readthedocs.org/projects/circup/badge/?version=latest
:target: https://circuitpython.readthedocs.io/projects/circup/en/latest/
:alt: Documentation Status
.. image:: https://img.shields.io/discord/327254708534116352.svg
:target: https://adafru.it/discord
:alt: Discord
.. image:: https://github.com/adafruit/circup/workflows/Build%20CI/badge.svg
:target: https://github.com/adafruit/circup/actions
:alt: Build Status
.. image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/psf/black
:alt: Code Style: Black
A tool to manage files on a CircuitPython device via wireless workflows.
Currently supports Web Workflow.
.. contents::
Installation
------------
wwshell is bundled along with Circup. When you install Circup you'll get wwshell automatically.
Circup requires Python 3.5 or higher.
In a `virtualenv <https://virtualenv.pypa.io/en/latest/>`_,
``pip install circup`` should do the trick. This is the simplest way to make it
work.
If you have no idea what a virtualenv is, try the following command,
``pip3 install --user circup``.
.. note::
If you use the ``pip3`` command to install CircUp you must make sure that
your path contains the directory into which the script will be installed.
To discover this path,
* On Unix-like systems, type ``python3 -m site --user-base`` and append
``bin`` to the resulting path.
* On Windows, type the same command, but append ``Scripts`` to the
resulting path.
What does wwshell do?
---------------------
It lets you view, delete, upload, and download files from your Circuitpython device
via wireless workflows. Similar to ampy, but operates over wireless workflow rather
than USB serial.
Usage
-----
To use web workflow you need to enable it by putting WIFI credentials and a web workflow
password into your settings.toml file. `See here <https://learn.adafruit.com/getting-started-with-web-workflow-using-the-code-editor/device-setup>`_,
To get help, just type the command::
$ wwshell
Usage: wwshell [OPTIONS] COMMAND [ARGS]...
A tool to manage files CircuitPython device over web workflow.
Options:
--verbose Comprehensive logging is sent to stdout.
--path DIRECTORY Path to CircuitPython directory. Overrides automatic path
detection.
--host TEXT Hostname or IP address of a device. Overrides automatic
path detection.
--password TEXT Password to use for authentication when --host is used.
You can optionally set an environment variable
CIRCUP_WEBWORKFLOW_PASSWORD instead of passing this
argument. If both exist the CLI arg takes precedent.
--timeout INTEGER Specify the timeout in seconds for any network
operations.
--version Show the version and exit.
--help Show this message and exit.
Commands:
get Download a copy of a file or directory from the device to the...
ls Lists the contents of a directory.
put Upload a copy of a file or directory from the local computer to...
rm Delete a file on the device.
.. note::
If you find a bug, or you want to suggest an enhancement or new feature
feel free to create an issue or submit a pull request here:
https://github.com/adafruit/circup
Discussion of this tool happens on the Adafruit CircuitPython
`Discord channel <https://discord.gg/rqrKDjU>`_.

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2024 Tim Cocks, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

View file

@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: 2024 Tim Cocks, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
wwshell is a CLI utility for managing files on CircuitPython devices via wireless workflows.
It currently supports Web Workflow.
"""
from .commands import main
# Allows execution via `python -m circup ...`
# pylint: disable=no-value-for-parameter
if __name__ == "__main__":
main()

231
circup/wwshell/commands.py Normal file
View file

@ -0,0 +1,231 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, 2024 Tim Cocks, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""
# ----------- CLI command definitions ----------- #
The following functions have IO side effects (for instance they emit to
stdout). Ergo, these are not checked with unit tests. Most of the
functionality they provide is provided by the functions from util_functions.py,
and the respective Backends which *are* tested. Most of the logic of the following
functions is to prepare things for presentation to / interaction with the user.
"""
import os
import time
import sys
import logging
import update_checker
import click
import requests
from circup.backends import WebBackend
from circup.logging import logger, log_formatter, LOGFILE
from circup.shared import BOARDLESS_COMMANDS
from circup.command_utils import (
get_device_path,
get_circup_version,
sorted_by_directory_then_alpha,
)
@click.group()
@click.option(
"--verbose", is_flag=True, help="Comprehensive logging is sent to stdout."
)
@click.option(
"--path",
type=click.Path(exists=True, file_okay=False),
help="Path to CircuitPython directory. Overrides automatic path detection.",
)
@click.option(
"--host",
help="Hostname or IP address of a device. Overrides automatic path detection.",
default="circuitpython.local",
)
@click.option(
"--port",
help="HTTP port that the web workflow is listening on.",
default=80,
)
@click.option(
"--password",
help="Password to use for authentication when --host is used."
" You can optionally set an environment variable CIRCUP_WEBWORKFLOW_PASSWORD"
" instead of passing this argument. If both exist the CLI arg takes precedent.",
)
@click.option(
"--timeout",
default=30,
help="Specify the timeout in seconds for any network operations.",
)
@click.version_option(
prog_name="CircFile",
message="%(prog)s, A CircuitPython web workflow file managemenr. Version %(version)s",
)
@click.pass_context
def main( # pylint: disable=too-many-locals
ctx,
verbose,
path,
host,
port,
password,
timeout,
): # pragma: no cover
"""
A tool to manage files CircuitPython device over web workflow.
"""
# pylint: disable=too-many-arguments,too-many-branches,too-many-statements,too-many-locals, R0801
ctx.ensure_object(dict)
ctx.obj["TIMEOUT"] = timeout
if password is None:
password = os.getenv("CIRCUP_WEBWORKFLOW_PASSWORD")
device_path = get_device_path(host, port, password, path)
using_webworkflow = "host" in ctx.params.keys() and ctx.params["host"] is not None
if using_webworkflow:
if host == "circuitpython.local":
click.echo("Checking versions.json on circuitpython.local to find hostname")
versions_resp = requests.get(
"http://circuitpython.local/cp/version.json", timeout=timeout
)
host = f'{versions_resp.json()["hostname"]}.local'
click.echo(f"Using hostname: {host}")
device_path = device_path.replace("circuitpython.local", host)
try:
ctx.obj["backend"] = WebBackend(
host=host, port=port, password=password, logger=logger, timeout=timeout
)
except ValueError as e:
click.secho(e, fg="red")
time.sleep(0.3)
sys.exit(1)
except RuntimeError as e:
click.secho(e, fg="red")
sys.exit(1)
if verbose:
# Configure additional logging to stdout.
ctx.obj["verbose"] = True
verbose_handler = logging.StreamHandler(sys.stdout)
verbose_handler.setLevel(logging.INFO)
verbose_handler.setFormatter(log_formatter)
logger.addHandler(verbose_handler)
click.echo("Logging to {}\n".format(LOGFILE))
else:
ctx.obj["verbose"] = False
logger.info("### Started Circfile ###")
# If a newer version of circfile is available, print a message.
logger.info("Checking for a newer version of circfile")
version = get_circup_version()
if version:
update_checker.update_check("circfile", version)
# stop early if the command is boardless
if ctx.invoked_subcommand in BOARDLESS_COMMANDS or "--help" in sys.argv:
return
ctx.obj["DEVICE_PATH"] = device_path
if device_path is None or not ctx.obj["backend"].is_device_present():
click.secho("Could not find a connected CircuitPython device.", fg="red")
sys.exit(1)
else:
click.echo("Found device at {}.".format(device_path))
@main.command("ls")
@click.argument("file", required=True, nargs=1, default="/")
@click.pass_context
def ls_cli(ctx, file): # pragma: no cover
"""
Lists the contents of a directory. Defaults to root directory
if not supplied.
"""
logger.info("ls")
if not file.endswith("/"):
file += "/"
click.echo(f"running: ls {file}")
files = ctx.obj["backend"].list_dir(file)
click.echo("Size\tName")
for cur_file in sorted_by_directory_then_alpha(files):
click.echo(
f"{cur_file['file_size']}\t{cur_file['name']}{'/' if cur_file['directory'] else ''}"
)
@main.command("put")
@click.argument("file", required=True, nargs=1)
@click.argument("location", required=False, nargs=1, default="")
@click.option("--overwrite", is_flag=True, help="Overwrite the file if it exists.")
@click.pass_context
def put_cli(ctx, file, location, overwrite):
"""
Upload a copy of a file or directory from the local computer
to the device
"""
click.echo(f"Attempting PUT: {file} at {location} overwrite? {overwrite}")
if not ctx.obj["backend"].file_exists(f"{location}{file}"):
ctx.obj["backend"].upload_file(file, location)
click.echo(f"Successfully PUT {location}{file}")
else:
if overwrite:
click.secho(
f"{location}{file} already exists. Overwriting it.", fg="yellow"
)
ctx.obj["backend"].upload_file(file, location)
click.echo(f"Successfully PUT {location}{file}")
else:
click.secho(
f"{location}{file} already exists. Pass --overwrite if you wish to replace it.",
fg="red",
)
# pylint: enable=too-many-arguments,too-many-locals
@main.command("get")
@click.argument("file", required=True, nargs=1)
@click.argument("location", required=False, nargs=1)
@click.pass_context
def get_cli(ctx, file, location): # pragma: no cover
"""
Download a copy of a file or directory from the device to the local computer.
"""
click.echo(f"running: get {file} {location}")
ctx.obj["backend"].download_file(file, location)
@main.command("rm")
@click.argument("file", nargs=1)
@click.pass_context
def rm_cli(ctx, file): # pragma: no cover
"""
Delete a file on the device.
"""
click.echo(f"running: rm {file}")
ctx.obj["backend"].uninstall(
ctx.obj["backend"].device_location, ctx.obj["backend"].get_file_path(file)
)
@main.command("mkdir")
@click.argument("directory", nargs=1)
@click.pass_context
def mkdir_cli(ctx, directory): # pragma: no cover
"""
Create
"""
click.echo(f"running: mkdir {directory}")
ctx.obj["backend"].create_directory(
ctx.obj["backend"].device_location, ctx.obj["backend"].get_file_path(directory)
)

View file

@ -1,20 +0,0 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

BIN
docs/_static/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

3
docs/_static/favicon.ico.license vendored Normal file
View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2018 Phillip Torrone for Adafruit Industries
SPDX-License-Identifier: CC-BY-4.0

View file

@ -1,79 +1,185 @@
# Configuration file for the Sphinx documentation builder.
#
# This file only contains a selection of the most common options. For a full
# list see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -*- coding: utf-8 -*-
# -- Path setup --------------------------------------------------------------
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import os
import sys
sys.path.insert(0, os.path.abspath('..'))
sys.path.insert(0, os.path.abspath(".."))
# -- Project information -----------------------------------------------------
project = 'CircUp'
copyright = '2019, Adafruit Industries'
author = 'Adafruit Industries'
# The full version, including alpha/beta/rc tags
import circup
release = circup.__version__
# -- General configuration ---------------------------------------------------
# -- General configuration ------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx.ext.viewcode',
"sphinx.ext.autodoc",
"sphinx.ext.intersphinx",
"sphinx.ext.napoleon",
"sphinx.ext.todo",
"sphinx.ext.viewcode",
]
# TODO: Please Read!
# Uncomment the below if you use native CircuitPython modules such as
# digitalio, micropython and busio. List the modules you use. Without it, the
# autodoc module docs will fail to generate with a warning.
# autodoc_mock_imports = ["digitalio", "busio"]
intersphinx_mapping = {
"python": ("https://docs.python.org/3.4", None),
"CircuitPython": ("https://circuitpython.readthedocs.io/en/latest/", None),
}
# Show the docstring from both the class and its __init__() method.
autoclass_content = "both"
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
source_suffix = ".rst"
# The master toctree document.
master_doc = "index"
# General information about the project.
project = "Circup"
copyright = "2019, Adafruit Industries"
author = "Adafruit Industries"
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = "1.0"
# The full version, including alpha/beta/rc tags.
release = "1.0"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
language = "en"
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# This patterns also effect to html_static_path and html_extra_path
exclude_patterns = [
"_build",
"Thumbs.db",
".DS_Store",
".env",
"CODE_OF_CONDUCT.md",
]
# The reST default role (used for this markup: `text`) to use for all
# documents.
#
default_role = "any"
# If true, '()' will be appended to :func: etc. cross-reference text.
#
add_function_parentheses = True
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output -------------------------------------------------
# If this is True, todo emits a warning for each TODO entries. The default is False.
todo_emit_warnings = True
napoleon_numpy_docstring = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = 'alabaster'
on_rtd = os.environ.get("READTHEDOCS", None) == "True"
if not on_rtd: # only import and set the theme if we're building docs locally
try:
import sphinx_rtd_theme
html_theme = "sphinx_rtd_theme"
except:
html_theme = "default"
html_theme_path = ["."]
else:
html_theme_path = ["."]
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
html_logo = 'logo.png'
# The name of an image file (relative to this directory) to use as a favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#
html_favicon = "_static/favicon.ico"
html_theme_options = {
'description': "The CircuitPython Library Updater",
'logo_name': True,
'logo_text_align': 'center',
'github_user': 'adafruit',
'github_repo': 'circup',
'page_width': '1200px',
# Output file base name for HTML help builder.
htmlhelp_basename = "Adafruit Circup doc"
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
# 'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': '',
# Latex figure (float) alignment
# 'figure_align': 'htbp',
}
html_sidebars = {
'**': [
'about.html',
'searchbox.html',
]
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(
master_doc,
"Adafruit_Circup.tex",
"Adafruit Circup Documentation",
author,
"manual",
),
]
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(
master_doc,
"Circup",
"Adafruit Circup Library Documentation",
[author],
1,
),
]
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(
master_doc,
"Adafruit Circup",
"Adafruit Circup Documentation",
author,
"Adafruit Circup",
),
]

View file

@ -1,10 +1,12 @@
.. CircUp documentation master file, created by
.. Circup documentation master file, created by
sphinx-quickstart on Mon Sep 2 10:58:36 2019.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
.. include:: ../README.rst
.. include:: ../circup/wwshell/README.rst
.. include:: ../CONTRIBUTING.rst
API
@ -13,7 +15,6 @@ API
.. automodule:: circup
:members:
.. include:: ../CHANGES.rst
License
=======

3
docs/index.rst.license Normal file
View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

3
docs/logo.png.license Normal file
View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

View file

@ -1,35 +0,0 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build
if "%1" == "" goto help
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.http://sphinx-doc.org/
exit /b 1
)
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View file

@ -1 +0,0 @@
python make.py %*

289
make.py
View file

@ -1,289 +0,0 @@
#!python3
"""
A "pretend" make command written in Python for Windows users. :-)
"""
import os
import sys
import fnmatch
import shutil
import subprocess
PYTEST = "pytest"
PYFLAKES = "pyflakes"
PYCODESTYLE = "pycodestyle"
BLACK = "black"
INCLUDE_PATTERNS = {"*.py"}
EXCLUDE_PATTERNS = {"build/*", "docs/*"}
_exported = {}
def _walk(
start_from=".", include_patterns=None, exclude_patterns=None, recurse=True
):
if include_patterns:
_include_patterns = set(os.path.normpath(p) for p in include_patterns)
else:
_include_patterns = set()
if exclude_patterns:
_exclude_patterns = set(os.path.normpath(p) for p in exclude_patterns)
else:
_exclude_patterns = set()
for dirpath, dirnames, filenames in os.walk(start_from):
for filename in filenames:
filepath = os.path.normpath(os.path.join(dirpath, filename))
if not any(
fnmatch.fnmatch(filepath, pattern)
for pattern in _include_patterns
):
continue
if any(
fnmatch.fnmatch(filepath, pattern)
for pattern in _exclude_patterns
):
continue
yield filepath
if not recurse:
break
def _process_code(executable, use_python, *args):
"""
Perform some action (check, translate etc.) across the .py files
in the codebase, skipping docs and build artefacts.
"""
if use_python:
execution = ["python", executable]
else:
execution = [executable]
returncodes = set()
for filepath in _walk(".", INCLUDE_PATTERNS, EXCLUDE_PATTERNS, False):
p = subprocess.run(execution + [filepath] + list(args))
returncodes.add(p.returncode)
for filepath in _walk("tests", INCLUDE_PATTERNS, EXCLUDE_PATTERNS):
p = subprocess.run(execution + [filepath] + list(args))
returncodes.add(p.returncode)
return max(returncodes)
def _rmtree(dirpath, cascade_errors=False):
"""
Remove a directory and its contents, including subdirectories.
"""
try:
shutil.rmtree(dirpath)
except OSError:
if cascade_errors:
raise
def _rmfiles(start_from, pattern):
"""
Remove files from a directory and its descendants.
Starting from `start_from` directory and working downwards,
remove all files which match `pattern`, eg *.pyc.
"""
for filepath in _walk(start_from, {pattern}):
os.remove(filepath)
def export(function):
"""
Decorator to tag certain functions as exported, meaning
that they show up as a command, with arguments, when this
file is run.
"""
_exported[function.__name__] = function
return function
@export
def test(*pytest_args):
"""
Run the test suite.
Call py.test to run the test suite with additional args.
The subprocess runner will raise an exception if py.test exits
with a failure value. This forces things to stop if tests fail.
"""
print("\ntest")
return subprocess.run([PYTEST] + list(pytest_args)).returncode
@export
def coverage():
"""
View a report on test coverage.
Call py.test with coverage turned on.
"""
print("\ncoverage")
return subprocess.run(
[
PYTEST,
"--cov-config",
".coveragerc",
"--cov-report",
"term-missing",
"--cov=circup",
"tests/",
]
).returncode
@export
def pyflakes(*pyflakes_args):
"""
Run the PyFlakes code checker.
Call pyflakes on all .py files outside the docs and contrib directories.
"""
print("\npyflakes")
os.environ["PYFLAKES_BUILTINS"] = "_"
return _process_code(PYFLAKES, False, *pyflakes_args)
@export
def pycodestyle(*pycodestyle_args):
"""
Run the PEP8 style checker.
"""
print("\nPEP8")
args = ("--ignore=E731,E402,W504,W503",) + pycodestyle_args
return _process_code(PYCODESTYLE, False, *args)
@export
def pep8(*pep8_args):
"""
Run the PEP8 style checker.
"""
return pycodestyle(*pep8_args)
@export
def tidy(*tidy_args):
"""
Run black against the code and tests.
"""
print("\nTidy code")
args = (BLACK, "-l", "79", "circup.py")
result = subprocess.run(args).returncode
if result > 0:
return result
args = (BLACK, "-l", "79", "tests")
return subprocess.run(args).returncode
@export
def check():
"""
Run all the checkers and tests.
"""
print("\nCheck")
funcs = [clean, tidy, pyflakes, pycodestyle, coverage]
for func in funcs:
return_code = func()
if return_code != 0:
return return_code
return 0
@export
def clean():
"""
Reset the project and remove auto-generated assets.
"""
print("\nClean")
_rmtree("build")
_rmtree("dist")
_rmtree("circup.egg-info")
_rmtree("coverage")
_rmtree("docs/build")
_rmfiles(".", "*.pyc")
return 0
@export
def dist():
"""
Generate a source distribution and a binary wheel.
"""
check()
print("Checks pass; good to package")
return subprocess.run(
["python", "setup.py", "sdist", "bdist_wheel"]
).returncode
@export
def publish_test():
"""
Upload to a test PyPI.
"""
dist()
print("Packaging complete; upload to PyPI")
return subprocess.run(
["twine", "upload", "-r", "test", "--sign", "dist/*"]
).returncode
@export
def publish_live():
"""
Upload to PyPI.
"""
dist()
print("Packaging complete; upload to PyPI")
return subprocess.run(["twine", "upload", "--sign", "dist/*"]).returncode
@export
def docs():
"""
Build the docs.
"""
cwd = os.getcwd()
os.chdir("docs")
try:
return subprocess.run(["cmd", "/c", "make.bat", "html"]).returncode
except Exception:
return 1
finally:
os.chdir(cwd)
@export
def help():
"""
Display all commands with their description in alphabetical order.
"""
module_doc = sys.modules["__main__"].__doc__ or "check"
print(module_doc + "\n" + "=" * len(module_doc) + "\n")
for command, function in sorted(_exported.items()):
doc = function.__doc__
print("make {}{}".format(command, doc))
def main(command="help", *args):
"""
Dispatch on command name, passing all remaining parameters to the
module-level function.
"""
try:
function = _exported[command]
except KeyError:
raise RuntimeError("No such command: %s" % command)
else:
return function(*args)
if __name__ == "__main__":
sys.exit(main(*sys.argv[1:]))

View file

@ -0,0 +1,4 @@
pytest
pytest-cov
pytest-faulthandler
pytest-random-order

View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2024 Autogenerated by 'pip freeze'
SPDX-License-Identifier: MIT

52
pyproject.toml Normal file
View file

@ -0,0 +1,52 @@
# SPDX-FileCopyrightText: 2024 Jev Kuznetsov, ROX Automation
#
# SPDX-License-Identifier: MIT
[build-system]
requires = ["setuptools>=61.0", "setuptools-scm"]
build-backend = "setuptools.build_meta"
[project]
name = "circup"
dynamic = ["version", "dependencies", "optional-dependencies"]
description = "A tool to manage/update libraries on CircuitPython devices."
readme = "README.rst"
authors = [{ name = "Adafruit Industries", email = "circuitpython@adafruit.com" }]
license = { file = "LICENSE" }
classifiers = [
"Development Status :: 3 - Alpha",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: Education",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX",
"Operating System :: MacOS :: MacOS X",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Education",
"Topic :: Software Development :: Embedded Systems",
"Topic :: System :: Software Distribution"
]
keywords = ["adafruit", "blinka", "circuitpython", "micropython", "libraries"]
requires-python = ">=3.9"
[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
optional-dependencies = {optional = {file = ["optional_requirements.txt"]}}
[tool.setuptools_scm]
[project.scripts]
circup = "circup:main"
wwshell = "circup.wwshell:main"
[project.urls]
homepage = "https://github.com/adafruit/circup"
[tool.setuptools.packages.find]
where = ["."] # This tells setuptools to look in the project root directory
include = ["circup"] # This pattern includes your main package and any sub-packages within it

5
readthedocs.yml Normal file
View file

@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2017 Scott Shawcroft, written for Adafruit Industries
#
# SPDX-License-Identifier: Unlicense
requirements_file: requirements.txt

6
requirements.txt Normal file
View file

@ -0,0 +1,6 @@
appdirs
Click
requests
semver
toml
update_checker

3
requirements.txt.license Normal file
View file

@ -0,0 +1,3 @@
SPDX-FileCopyrightText: 2021 Autogenerated by 'pip freeze'
SPDX-License-Identifier: MIT

View file

@ -1,51 +0,0 @@
alabaster==0.7.12
appdirs==1.4.3
atomicwrites==1.3.0
attrs==19.1.0
Babel==2.7.0
black==19.3b0
bleach==3.1.0
certifi==2019.6.16
chardet==3.0.4
Click==7.0
coverage==4.5.4
docutils==0.15.2
idna==2.8
imagesize==1.1.0
importlib-metadata==0.20
Jinja2==2.10.1
MarkupSafe==1.1.1
more-itertools==7.2.0
packaging==19.1
pkginfo==1.5.0.1
pluggy==0.12.0
py==1.8.0
pycodestyle==2.5.0
pyflakes==2.1.1
Pygments==2.4.2
pyparsing==2.4.2
pytest==5.1.2
pytest-cov==2.7.1
pytest-faulthandler==2.0.1
pytest-random-order==1.0.4
pytz==2019.2
readme-renderer==24.0
requests==2.22.0
requests-toolbelt==0.9.1
semver==2.8.1
six==1.12.0
snowballstemmer==1.9.0
Sphinx==2.2.0
sphinxcontrib-applehelp==1.0.1
sphinxcontrib-devhelp==1.0.1
sphinxcontrib-htmlhelp==1.0.2
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.2
sphinxcontrib-serializinghtml==1.1.3
toml==0.10.0
tqdm==4.35.0
twine==1.13.0
urllib3==1.25.3
wcwidth==0.1.7
webencodings==0.5.1
zipp==0.6.0

View file

@ -1,88 +0,0 @@
#!/usr/bin/env python3
import os
import re
from setuptools import setup
base_dir = os.path.dirname(__file__)
DUNDER_ASSIGN_RE = re.compile(r"""^__\w+__\s*=\s*['"].+['"]$""")
about = {}
with open(os.path.join(base_dir, "circup.py"), encoding="utf8") as f:
for line in f:
if DUNDER_ASSIGN_RE.search(line):
exec(line, about)
with open(os.path.join(base_dir, "README.rst"), encoding="utf8") as f:
readme = f.read()
with open(os.path.join(base_dir, "CHANGES.rst"), encoding="utf8") as f:
changes = f.read()
install_requires = [
"semver>=2.8.1",
"Click==7.0",
"appdirs>=1.4.3",
"requests>=2.22.0",
]
extras_require = {
"tests": [
"pytest",
"pytest-cov",
"pytest-random-order>=1.0.0",
"pytest-faulthandler",
"coverage",
"pycodestyle",
"pyflakes",
"black",
],
"docs": ["sphinx"],
"package": [
# Wheel building and PyPI uploading
"wheel",
"twine",
],
}
extras_require["dev"] = (
extras_require["tests"]
+ extras_require["docs"]
+ extras_require["package"]
)
extras_require["all"] = list(
{req for extra, reqs in extras_require.items() for req in reqs}
)
setup(
name=about["__title__"],
version=about["__version__"],
description=about["__description__"],
long_description="{}\n\n{}".format(readme, changes),
author=about["__author__"],
author_email=about["__email__"],
url=about["__url__"],
license=about["__license__"],
py_modules=["circup"],
install_requires=install_requires,
extras_require=extras_require,
classifiers=[
"Development Status :: 3 - Alpha",
"Environment :: Console",
"Intended Audience :: Developers",
"Intended Audience :: Education",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Topic :: Education",
"Topic :: Software Development :: Embedded Systems",
"Topic :: System :: Software Distribution",
],
entry_points={"console_scripts": ["circup=circup:main"]},
)

View file

@ -1,3 +1,10 @@
# A simple directory based Python module that's missing metadata.
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""A simple directory based Python module that's missing metadata."""
def hello():
"""A hello function"""
return "Hello, World!"

6
tests/bad_python.py Normal file
View file

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# pylint: disable=all
if True:

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

View file

@ -1,7 +1,6 @@
{
"adafruit_74hc595.py": {
"__version__": "1.0.2",
"__repo__": "https://github.com/adafruit/Adafruit_CircuitPython_74HC595.git",
"path": "/media/ntoll/CIRCUITPY/lib/adafruit_74hc595.py",
"mpy": false
}

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

View file

@ -1,8 +1,13 @@
# A simple directory based Python module containing expected "local" metadata.
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""A simple directory based Python module containing expected "local" metadata."""
__version__ = "3.2.1"
__repo__ = "https://github.com/adafruit/SomeModule.git"
def hello():
"""A hello function"""
return "Hello, World!"

11
tests/import_styles.py Normal file
View file

@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# pylint: disable=all
import os, sys
import adafruit_bus_device
from adafruit_button import Button
from adafruit_esp32spi import adafruit_esp32spi_socketpool
from adafruit_display_text import wrap_text_to_pixels, wrap_text_to_lines
import adafruit_hid.consumer_control
import import_styles_sub

View file

@ -1,8 +1,13 @@
# A simple Python file containing expected "local" metadata.
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""A simple Python file containing expected "local" metadata."""
__version__ = "1.2.3"
__repo__ = "https://github.com/adafruit/SomeLibrary.git"
def hello():
"""A hello function"""
return "Hello, World!"

BIN
tests/local_module_cp7.mpy Normal file

Binary file not shown.

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

View file

@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# pylint: disable=all
import os, sys
import adafruit_bus_device
from adafruit_button import Button
from adafruit_esp32spi import adafruit_esp32spi_socketpool
from adafruit_display_text import wrap_text_to_pixels, wrap_text_to_lines
import adafruit_hid.consumer_control
import import_styles_sub

View file

@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2025 Neradoc
#
# SPDX-License-Identifier: MIT
# pylint: disable=all
import adafruit_ntp

View file

@ -0,0 +1,3 @@
Adafruit CircuitPython 9.0.0 on 2019-08-02; Adafruit CircuitPlayground Express with samd21g18
Board ID:this_is_a_board
UID:AAAABBBBCCCC

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2023 Tim Cocks, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

View file

@ -0,0 +1,11 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# pylint: disable=all
import os, sys
import adafruit_bus_device
from adafruit_button import Button
from adafruit_esp32spi import adafruit_esp32spi_socketpool
from adafruit_display_text import wrap_text_to_pixels, wrap_text_to_lines
import adafruit_hid.consumer_control
import import_styles_sub

View file

@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2025 Neradoc
#
# SPDX-License-Identifier: MIT
# pylint: disable=all
import adafruit_ntp

4
tests/mock_device_2/.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: 2025 Neradoc
#
# SPDX-License-Identifier: MIT
lib/*

View file

@ -0,0 +1,3 @@
Adafruit CircuitPython 9.0.0 on 2019-08-02; Adafruit CircuitPlayground Express with samd21g18
Board ID:this_is_a_board
UID:AAAABBBBCCCC

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2023 Tim Cocks, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

View file

@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# pylint: disable=all
import adafruit_ssd1675
import import_styles_sub
import package

View file

@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2025 Neradoc
#
# SPDX-License-Identifier: MIT
# pylint: disable=all
import adafruit_ntp

View file

@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: 2025 Neradoc
#
# SPDX-License-Identifier: MIT
# pylint: disable=all
import adafruit_spd1656
from .other import variable

View file

@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2021 Jeff Epler for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# pylint: disable=all
import adafruit_spd1608

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT

View file

@ -1,8 +1,13 @@
# A simple Python file containing expected "remote" metadata.
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT
"""A simple Python file containing expected "remote" metadata."""
__version__ = "2.3.4"
__repo__ = "https://github.com/adafruit/SomeLibrary.git"
def hello():
"""A hello function"""
return "Hello, World!"

View file

@ -0,0 +1,3 @@
{
"test_bundle": "adafruit/Adafruit_CircuitPython_Bundle"
}

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2021 Patrick Walters
#
# SPDX-License-Identifier: MIT

View file

@ -0,0 +1,3 @@
{
"local_bundle": "Neradoc/Circuitpython_Keyboard_Layouts"
}

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2021 Neradoc NeraOnGit@ri1.fr
#
# SPDX-License-Identifier: MIT

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,3 @@
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
#
# SPDX-License-Identifier: MIT