Compare commits
477 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2eccab822 | ||
|
|
18a4687739 | ||
|
|
ab32fc2b5b | ||
|
|
d1bfdbb042 | ||
|
|
056c222d57 | ||
|
|
e8ce15021c | ||
|
|
35dc55668e | ||
|
|
dd39ac635f | ||
|
|
fa59c1ecf9 | ||
|
|
7b2cf4d252 | ||
|
|
cde0dea1e5 | ||
|
|
e7c7fb6d65 | ||
|
|
e717ef0306 | ||
|
|
d27ae8164c | ||
|
|
43b31da905 | ||
|
|
9c05ad8f7c | ||
|
|
b28106713e | ||
|
|
60c4107dbd | ||
|
|
5fd54fa603 | ||
|
|
1ea6cd2d7b | ||
|
|
283a499a02 | ||
|
|
f873c78912 | ||
|
|
831316ae2f | ||
|
|
bd834c4603 | ||
|
|
da8f6c26c5 | ||
|
|
f028b18310 | ||
|
|
a03d50463f | ||
|
|
7c5ba016f3 | ||
|
|
1eaca649cd | ||
|
|
1bc332b839 | ||
|
|
dfc10c91a5 | ||
|
|
27284f366e | ||
|
|
c3da149010 | ||
|
|
995f9392eb | ||
|
|
9662d8b924 | ||
| ae3919e4b4 | |||
|
|
cb3de91da8 | ||
|
|
9610d7dcb8 | ||
|
|
431ccac9b3 | ||
|
|
a6985550e1 | ||
|
|
35ca9cccec | ||
|
|
0bfbe875f4 | ||
|
|
b9c6698aca | ||
|
|
5f444f0f70 | ||
|
|
f05ff1896d | ||
|
|
dde7615e1b | ||
|
|
ed39f029b9 | ||
|
|
0227af6bbc | ||
|
|
524e552394 | ||
|
|
50561f5972 | ||
|
|
23baaeb6c2 | ||
|
|
2abf7552fc | ||
|
|
c773c20b8c | ||
|
|
ef128a6d09 | ||
|
|
2899011c73 | ||
|
|
11868c327e | ||
|
|
c24d48b4ea | ||
|
|
6c824c538e | ||
|
|
a01586b342 | ||
|
|
e75a7dbf3a | ||
|
|
74b07bee0a | ||
|
|
f12efa37fc | ||
|
|
99367e95a6 | ||
|
|
c1e1b4b269 | ||
|
|
e128d650e9 | ||
|
|
eacc3199fc | ||
|
|
982911c049 | ||
|
|
3d66b08d15 | ||
|
|
e1166b6169 | ||
|
|
4e4934f659 | ||
|
|
f3779b8f11 | ||
|
|
3a1f3b3add | ||
|
|
7e31cb34fc | ||
|
|
41ed0d2d9b | ||
|
|
c94b991dc6 | ||
|
|
2657494e49 | ||
|
|
45f0804b36 | ||
|
|
55b687db23 | ||
|
|
a3c162ffde | ||
|
|
435475b28f | ||
|
|
4f5e98738f | ||
|
|
484f3fae24 | ||
|
|
2a656a1f0d | ||
|
|
6e35e5bcc3 | ||
|
|
4abf6d9936 | ||
|
|
6109b84644 | ||
|
|
20c7d6337e | ||
|
|
b6f049f8af | ||
|
|
76bf98eefe | ||
|
|
98a65d9c1f | ||
|
|
a1b388a3cc | ||
|
|
8c9f84bcc4 | ||
|
|
678d40766b | ||
|
|
fdf824d680 | ||
|
|
44f2144724 | ||
|
|
04836d5725 | ||
|
|
582c55e1a5 | ||
|
|
656f1fb118 | ||
|
|
a110c11ea8 | ||
|
|
9887ee97f7 | ||
|
|
1307f3b611 | ||
|
|
fdd464d61f | ||
|
|
ffda566d7f | ||
|
|
44443b32ea | ||
|
|
2ee6f3171e | ||
|
|
c16e377dee | ||
|
|
fe79ea8d3b | ||
|
|
f018daf74e | ||
|
|
9b450c8ba2 | ||
|
|
a2548e52d5 | ||
|
|
70b9b49872 | ||
|
|
d30e02fb88 | ||
|
|
422b6305b7 | ||
|
|
d3515e9eed | ||
|
|
1c1307b070 | ||
|
|
5354155113 | ||
|
|
5bfe6c0185 | ||
|
|
bf7617eabe | ||
|
|
bd7b85170e | ||
|
|
8bc37f7ae5 | ||
|
|
e86487776f | ||
|
|
3622f29b75 | ||
|
|
30e80f3486 | ||
|
|
b658576873 | ||
|
|
cd9fb82552 | ||
|
|
dc6b445855 | ||
|
|
aea9ce6704 | ||
|
|
591a2e6eeb | ||
|
|
6f5ad6e663 | ||
|
|
dc5f2e4eab | ||
|
|
ba726e2047 | ||
|
|
fe326c84a4 | ||
|
|
eef8326519 | ||
|
|
d88003b5c5 | ||
|
|
ccbebf5f6b | ||
|
|
1d100e0e42 | ||
|
|
b76d947913 | ||
|
|
9a7bb8be00 | ||
|
|
57a72ea91a | ||
|
|
eec4af4f7a | ||
|
|
0b33838780 | ||
|
|
7e80f28528 | ||
|
|
b11f97bfc7 | ||
|
|
3a93a1e610 | ||
|
|
caef12c5a1 | ||
|
|
a2ae043684 | ||
|
|
2572c32afe | ||
|
|
65f0a3e109 | ||
|
|
27ae1f978d | ||
|
|
eb108fe5f5 | ||
|
|
38dd524b45 | ||
|
|
1624a39dba | ||
|
|
8f1accc568 | ||
|
|
5479ff44a6 | ||
|
|
1ef7651d6f | ||
|
|
258f08494f | ||
|
|
0c5dec02e6 | ||
|
|
73d5872ec7 | ||
|
|
bc30d36255 | ||
|
|
3cd60f12a9 | ||
|
|
3622c3df67 | ||
|
|
2c8d3e93e9 | ||
|
|
08e0c9455d | ||
|
|
3ec6e34bd3 | ||
|
|
1c8b32c326 | ||
|
|
42d9fe6c08 | ||
|
|
1f8443f164 | ||
|
|
d5d8ed607d | ||
|
|
5965afde9a | ||
|
|
c8ff0551c5 | ||
|
|
d19e4745df | ||
|
|
3ed849593d | ||
|
|
a286849393 | ||
|
|
84e048e0ee | ||
|
|
ac578b852a | ||
|
|
deea802a29 | ||
|
|
5b9415f6ef | ||
|
|
c476116883 | ||
|
|
dbb8374638 | ||
|
|
15aec597b8 | ||
|
|
bfae6066dc | ||
|
|
e0bbec2251 | ||
|
|
88a5f9a2e0 | ||
|
|
33a0528087 | ||
|
|
7d3eb6cd78 | ||
|
|
b2601428d1 | ||
|
|
db47b6f423 | ||
|
|
271389207e | ||
|
|
e7e8df0274 | ||
|
|
e5a20309a0 | ||
|
|
6cc6a1df39 | ||
|
|
01cceec6bf | ||
|
|
1f9bee3ffc | ||
|
|
2d0739005a | ||
|
|
31270a0286 | ||
|
|
a96fd8e84a | ||
|
|
56c1ddab31 | ||
|
|
01f0a2d046 | ||
|
|
d9ce3b25a9 | ||
|
|
bea088ec50 | ||
|
|
499dee6e03 | ||
|
|
fb6cd847a8 | ||
|
|
d3eda67ddb | ||
|
|
467740088c | ||
|
|
51f9670610 | ||
|
|
75e501204a | ||
|
|
a5b40b46d3 | ||
|
|
168f8ec86d | ||
|
|
1681b29f9e | ||
|
|
f582873328 | ||
|
|
01a1ac2661 | ||
|
|
1b2e115155 | ||
|
|
c1af505510 | ||
|
|
90299a099a | ||
|
|
86ccabe411 | ||
|
|
c6d430c0fc | ||
|
|
97e60ed4e0 | ||
|
|
32dab6beaf | ||
|
|
e3d266525d | ||
|
|
a76663f278 | ||
|
|
5f1e6ab835 | ||
|
|
dc8d43dfef | ||
|
|
f4618b3e7c | ||
|
|
225b490295 | ||
|
|
5615279eb3 | ||
|
|
b7eb10b7a2 | ||
|
|
ce7a29823e | ||
|
|
8987505241 | ||
|
|
b30e2a92e1 | ||
|
|
7bfda3b5a5 | ||
|
|
b29414d26e | ||
|
|
e73a094d24 | ||
|
|
c4e1c1ad8b | ||
|
|
255493fbcb | ||
|
|
02d3aa7f7a | ||
|
|
63dcbb9944 | ||
|
|
ae5cc2a9b4 | ||
|
|
abd225169a | ||
|
|
0875a4d834 | ||
|
|
49737096ff | ||
|
|
aa829a5804 | ||
|
|
575da0797d | ||
|
|
6806be4af4 | ||
|
|
4e9a70a22a | ||
|
|
abf46302a8 | ||
|
|
fbf5aa4d50 | ||
|
|
4fcc7bcc82 | ||
|
|
27160fe75d | ||
|
|
9047f8c072 | ||
|
|
a649d29881 | ||
|
|
523b64a2c7 | ||
|
|
d411bae64d | ||
|
|
e400b88aa5 | ||
|
|
c29fc11ab7 | ||
|
|
777e677170 | ||
|
|
ce3fd34288 | ||
|
|
276fdc26b8 | ||
|
|
2753e50118 | ||
|
|
aa8ab4e5cd | ||
|
|
fd39347e01 | ||
|
|
82ffa73b57 | ||
|
|
62f7a657cf | ||
|
|
4015574518 | ||
|
|
3e7aa71a44 | ||
|
|
e7853dfc3a | ||
|
|
f7c812261b | ||
|
|
c46d2fdc75 | ||
|
|
0306fae90c | ||
|
|
151d3585ff | ||
|
|
8b413ffe16 | ||
|
|
aa3c57ff22 | ||
|
|
d9c3c567e9 | ||
|
|
55b0139ebf | ||
|
|
3cf3919e68 | ||
|
|
717d015742 | ||
|
|
41c36cb368 | ||
|
|
3ff7197077 | ||
|
|
1910d6b4fc | ||
|
|
7465718020 | ||
|
|
b7a28e6d3c | ||
|
|
b931a2055b | ||
|
|
0a7f3fcfa0 | ||
|
|
af8034615c | ||
|
|
52706625f0 | ||
|
|
2c23dd455b | ||
|
|
4eea33612a | ||
|
|
c013e555ea | ||
|
|
65ff2d7742 | ||
|
|
17708f9a1c | ||
|
|
86ff01c5fb | ||
|
|
d783743e79 | ||
|
|
14f7caf70f | ||
|
|
0c922620a6 | ||
|
|
86255dd9df | ||
|
|
bfecb7593f | ||
|
|
f0d51771e2 | ||
| a05f7376c3 | |||
|
|
5552759282 | ||
|
|
cf7e9b1557 | ||
|
|
400e28e023 | ||
|
|
c14337b741 | ||
|
|
eb34a646f8 | ||
|
|
d39063daac | ||
|
|
05514e5d79 | ||
|
|
d467d2545b | ||
|
|
a9f9cf5e77 | ||
|
|
cc39336580 | ||
|
|
350f1ceb04 | ||
|
|
15d634d31a | ||
|
|
908eded30c | ||
|
|
91c07550f5 | ||
|
|
b28289ea6d | ||
|
|
5076a5dd4f | ||
|
|
d075358280 | ||
|
|
6dbddb6151 | ||
|
|
1e32009d29 | ||
|
|
ed7a738834 | ||
|
|
f797cdd08e | ||
|
|
820dc0a7be | ||
|
|
79b4948c9a | ||
|
|
cacb685e95 | ||
|
|
17c599c2be | ||
|
|
31b8badc0d | ||
|
|
dac1d8ec1d | ||
|
|
101ce4df79 | ||
|
|
751578094b | ||
|
|
12b7d9948d | ||
|
|
0d9219baa2 | ||
|
|
a634f227b0 | ||
|
|
de2deb406d | ||
|
|
3f5fb5e0fa | ||
|
|
b7f172f57e | ||
|
|
db08cc1ebc | ||
|
|
4730bc21f5 | ||
|
|
0ee6cb2aea | ||
|
|
32f1edca69 | ||
|
|
76c874ee67 | ||
|
|
8afd84ca71 | ||
|
|
3b43540b2a | ||
|
|
124f769b56 | ||
|
|
f76bcb674d | ||
|
|
86c0d0a111 | ||
|
|
4b757e8512 | ||
|
|
917f4330ec | ||
|
|
8fec75b3a2 | ||
|
|
fa501dad67 | ||
|
|
391eb0cd8c | ||
|
|
72cc5121cc | ||
|
|
9a345131bb | ||
|
|
141fb2bed6 | ||
|
|
23864f25bd | ||
|
|
148574e0c9 | ||
|
|
65f053fa21 | ||
|
|
2e127eb108 | ||
|
|
97f2353dc8 | ||
|
|
946544d59c | ||
|
|
a4949b0de8 | ||
|
|
3dfefd304c | ||
|
|
2ed81dd5e4 | ||
|
|
4aa4cceff3 | ||
|
|
71ca170ef0 | ||
|
|
96e3461740 | ||
|
|
9ab21c991e | ||
|
|
7370b7fd22 | ||
|
|
bff126c934 | ||
|
|
f4e0e1e634 | ||
|
|
1cb74fdae2 | ||
|
|
8c817b2b2b | ||
|
|
eae876aaa0 | ||
|
|
32d3a31859 | ||
|
|
06d8a956c6 | ||
|
|
84737a6f37 | ||
|
|
eb120d11f9 | ||
|
|
3c3a98ff38 | ||
|
|
dce9dcc7bf | ||
|
|
5eecbdcfcb | ||
|
|
14be02048b | ||
|
|
a4ee697062 | ||
|
|
b713c40181 | ||
|
|
858cb1e7d5 | ||
|
|
34841a45fd | ||
|
|
e0c9f4febd | ||
|
|
208a3197ab | ||
|
|
705c738788 | ||
|
|
de06e729f3 | ||
|
|
c477539f4a | ||
|
|
42d1f58ffc | ||
|
|
0c07ea45ac | ||
|
|
bb4dbbe1ab | ||
|
|
3240add299 | ||
|
|
b31e2d6d74 | ||
|
|
c7e4f8668a | ||
|
|
90fdfd3900 | ||
|
|
36d770e287 | ||
|
|
f0319e659d | ||
|
|
1536d611df | ||
|
|
9e2982aed9 | ||
|
|
776e32e2a3 | ||
|
|
076fe8e8c2 | ||
|
|
88b2b2de21 | ||
|
|
3ae0f361a2 | ||
|
|
38ccbb4342 | ||
|
|
3b51e9b018 | ||
|
|
250d6f5cd0 | ||
|
|
a2cd58df1d | ||
|
|
4e1ada026c | ||
|
|
47e16a716c | ||
|
|
43c662a01b | ||
|
|
74ffe6206c | ||
|
|
25043b6f76 | ||
|
|
91974d92b7 | ||
|
|
77842dd4b7 | ||
|
|
690e2d469c | ||
|
|
48a53dd217 | ||
|
|
dedff0613c | ||
|
|
f85994a153 | ||
|
|
871987681f | ||
|
|
a70097e9c1 | ||
|
|
983f3bd4ec | ||
|
|
98711dcb7a | ||
|
|
05530914a4 | ||
|
|
88c4ba5b69 | ||
|
|
b641d92cf9 | ||
|
|
1397ce664a | ||
|
|
78319511f7 | ||
|
|
7764bc44c0 | ||
|
|
e79f44b8a6 | ||
|
|
4003556012 | ||
|
|
d79c5d0ce5 | ||
|
|
c0c6daa4fe | ||
|
|
47d2edd0d4 | ||
|
|
8e7b1d40ad | ||
|
|
2b8a947a50 | ||
|
|
ac6e544c7e | ||
|
|
fc78ea66ef | ||
|
|
75cba36161 | ||
|
|
54d3595208 | ||
|
|
5e7a9db71c | ||
|
|
bd50b2c59c | ||
|
|
c59692f9bd | ||
|
|
d1898565d5 | ||
|
|
7e988aad15 | ||
|
|
e5a4c2951f | ||
|
|
ca74ab0d32 | ||
|
|
60b880ac01 | ||
|
|
5d7f385455 | ||
|
|
9bad77ed58 | ||
|
|
c4fa165776 | ||
|
|
4ff8f95661 | ||
|
|
836cd2bcd8 | ||
|
|
719002b34d | ||
|
|
9d17f600ad | ||
|
|
5b841ccc08 | ||
|
|
2414b98374 | ||
|
|
1c1801f5aa | ||
|
|
d8ade1ba3d | ||
|
|
041393ee6b | ||
|
|
15392d971e | ||
|
|
5b407d2356 | ||
|
|
e2d09b22fa | ||
|
|
0bb8167090 | ||
|
|
4491a187a2 | ||
|
|
a091ecc471 | ||
|
|
bc2daa2322 | ||
|
|
75e4ba52f5 | ||
|
|
024a8bc0cb | ||
|
|
c7173defe3 | ||
|
|
0b47132aa6 | ||
|
|
bf89ddaebf | ||
|
|
e6c79c6a36 | ||
|
|
703e0ab74f | ||
|
|
17def793e1 | ||
|
|
008596ee3b | ||
|
|
1335132ac7 | ||
|
|
315e2eb9eb | ||
|
|
5dce7135aa | ||
|
|
a46093a0e8 |
85 changed files with 6259 additions and 1737 deletions
50
.github/ISSUE_TEMPLATE.md
vendored
Normal file
50
.github/ISSUE_TEMPLATE.md
vendored
Normal 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
29
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal 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
57
.github/workflows/build.yml
vendored
Normal 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
34
.github/workflows/release.yml
vendored
Normal 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
14
.gitignore
vendored
|
|
@ -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
5
.isort.cfg
Normal 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
33
.pre-commit-config.yaml
Normal 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
399
.pylintrc
Normal 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
|
||||
38
.travis.yml
38
.travis.yml
|
|
@ -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
|
||||
17
CHANGES.rst
17
CHANGES.rst
|
|
@ -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.
|
||||
3
CODE_OF_CONDUCT.rst.license
Normal file
3
CODE_OF_CONDUCT.rst.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
122
CONTRIBUTING.rst
122
CONTRIBUTING.rst
|
|
@ -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
3
CONTRIBUTING.rst.license
Normal 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
324
LICENSES/CC-BY-4.0.txt
Normal 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
|
||||
reason–for example, because of any applicable exception or limitation to copyright–then
|
||||
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
19
LICENSES/MIT.txt
Normal 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
20
LICENSES/Unlicense.txt
Normal 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/>
|
||||
68
Makefile
68
Makefile
|
|
@ -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"
|
||||
366
README.rst
366
README.rst
|
|
@ -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
3
README.rst.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
710
circup.py
710
circup.py
|
|
@ -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
26
circup/__init__.py
Normal 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
1061
circup/backends.py
Normal file
File diff suppressed because it is too large
Load diff
170
circup/bundle.py
Normal file
170
circup/bundle.py
Normal 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
843
circup/command_utils.py
Normal 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
755
circup/commands.py
Normal 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)
|
||||
4
circup/config/bundle_config.json
Normal file
4
circup/config/bundle_config.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"adafruit": "adafruit/Adafruit_CircuitPython_Bundle",
|
||||
"circuitpython_community": "adafruit/CircuitPython_Community_Bundle"
|
||||
}
|
||||
3
circup/config/bundle_config.json.license
Normal file
3
circup/config/bundle_config.json.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2021 Patrick Walters
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
33
circup/logging.py
Normal file
33
circup/logging.py
Normal 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
209
circup/module.py
Normal 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
221
circup/shared.py
Normal 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
105
circup/wwshell/README.rst
Normal 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>`_.
|
||||
3
circup/wwshell/README.rst.license
Normal file
3
circup/wwshell/README.rst.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2024 Tim Cocks, written for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
14
circup/wwshell/__init__.py
Normal file
14
circup/wwshell/__init__.py
Normal 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
231
circup/wwshell/commands.py
Normal 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)
|
||||
)
|
||||
|
|
@ -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
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
3
docs/_static/favicon.ico.license
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2018 Phillip Torrone for Adafruit Industries
|
||||
|
||||
SPDX-License-Identifier: CC-BY-4.0
|
||||
198
docs/conf.py
198
docs/conf.py
|
|
@ -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",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
3
docs/index.rst.license
Normal 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
3
docs/logo.png.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
|
@ -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
|
||||
1
make.cmd
1
make.cmd
|
|
@ -1 +0,0 @@
|
|||
python make.py %*
|
||||
289
make.py
289
make.py
|
|
@ -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:]))
|
||||
4
optional_requirements.txt
Normal file
4
optional_requirements.txt
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pytest
|
||||
pytest-cov
|
||||
pytest-faulthandler
|
||||
pytest-random-order
|
||||
3
optional_requirements.txt.license
Normal file
3
optional_requirements.txt.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2024 Autogenerated by 'pip freeze'
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
52
pyproject.toml
Normal file
52
pyproject.toml
Normal 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
5
readthedocs.yml
Normal 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
6
requirements.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
appdirs
|
||||
Click
|
||||
requests
|
||||
semver
|
||||
toml
|
||||
update_checker
|
||||
3
requirements.txt.license
Normal file
3
requirements.txt.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
SPDX-FileCopyrightText: 2021 Autogenerated by 'pip freeze'
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
|
|
@ -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
|
||||
88
setup.py
88
setup.py
|
|
@ -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"]},
|
||||
)
|
||||
|
|
@ -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
6
tests/bad_python.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# SPDX-FileCopyrightText: 2021 Jeff Epler for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
# pylint: disable=all
|
||||
|
||||
if True:
|
||||
3
tests/bundle.json.license
Normal file
3
tests/bundle.json.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
3
tests/device.json.license
Normal file
3
tests/device.json.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
|
@ -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
11
tests/import_styles.py
Normal 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
|
||||
|
|
@ -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
BIN
tests/local_module_cp7.mpy
Normal file
Binary file not shown.
3
tests/local_module_cp7.mpy.license
Normal file
3
tests/local_module_cp7.mpy.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
11
tests/mock_device/apps/test_app/import_styles.py
Normal file
11
tests/mock_device/apps/test_app/import_styles.py
Normal 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
|
||||
5
tests/mock_device/apps/test_app/import_styles_sub.py
Normal file
5
tests/mock_device/apps/test_app/import_styles_sub.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# SPDX-FileCopyrightText: 2025 Neradoc
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
# pylint: disable=all
|
||||
import adafruit_ntp
|
||||
3
tests/mock_device/boot_out.txt
Normal file
3
tests/mock_device/boot_out.txt
Normal 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
|
||||
3
tests/mock_device/boot_out.txt.license
Normal file
3
tests/mock_device/boot_out.txt.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2023 Tim Cocks, written for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
11
tests/mock_device/import_styles.py
Normal file
11
tests/mock_device/import_styles.py
Normal 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
|
||||
5
tests/mock_device/import_styles_sub.py
Normal file
5
tests/mock_device/import_styles_sub.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# SPDX-FileCopyrightText: 2025 Neradoc
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
# pylint: disable=all
|
||||
import adafruit_ntp
|
||||
0
tests/mock_device/lib/adafruit_waveform/.gitkeep
Normal file
0
tests/mock_device/lib/adafruit_waveform/.gitkeep
Normal file
4
tests/mock_device_2/.gitignore
vendored
Normal file
4
tests/mock_device_2/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# SPDX-FileCopyrightText: 2025 Neradoc
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
lib/*
|
||||
3
tests/mock_device_2/boot_out.txt
Normal file
3
tests/mock_device_2/boot_out.txt
Normal 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
|
||||
3
tests/mock_device_2/boot_out.txt.license
Normal file
3
tests/mock_device_2/boot_out.txt.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2023 Tim Cocks, written for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
7
tests/mock_device_2/code.py
Normal file
7
tests/mock_device_2/code.py
Normal 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
|
||||
5
tests/mock_device_2/import_styles_sub.py
Normal file
5
tests/mock_device_2/import_styles_sub.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# SPDX-FileCopyrightText: 2025 Neradoc
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
# pylint: disable=all
|
||||
import adafruit_ntp
|
||||
6
tests/mock_device_2/package/__init__.py
Normal file
6
tests/mock_device_2/package/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# SPDX-FileCopyrightText: 2025 Neradoc
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
# pylint: disable=all
|
||||
import adafruit_spd1656
|
||||
from .other import variable
|
||||
5
tests/mock_device_2/package/other.py
Normal file
5
tests/mock_device_2/package/other.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# SPDX-FileCopyrightText: 2021 Jeff Epler for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
# pylint: disable=all
|
||||
import adafruit_spd1608
|
||||
3
tests/mount_exists.txt.license
Normal file
3
tests/mount_exists.txt.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
3
tests/mount_missing.txt.license
Normal file
3
tests/mount_missing.txt.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
|
@ -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!"
|
||||
|
|
|
|||
3
tests/test_bundle_config.json
Normal file
3
tests/test_bundle_config.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"test_bundle": "adafruit/Adafruit_CircuitPython_Bundle"
|
||||
}
|
||||
3
tests/test_bundle_config.json.license
Normal file
3
tests/test_bundle_config.json.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2021 Patrick Walters
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
3
tests/test_bundle_config_local.json
Normal file
3
tests/test_bundle_config_local.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"local_bundle": "Neradoc/Circuitpython_Keyboard_Layouts"
|
||||
}
|
||||
3
tests/test_bundle_config_local.json.license
Normal file
3
tests/test_bundle_config_local.json.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2021 Neradoc NeraOnGit@ri1.fr
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
1086
tests/test_circup.py
1086
tests/test_circup.py
File diff suppressed because it is too large
Load diff
3
tests/test_module.mpy.license
Normal file
3
tests/test_module.mpy.license
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# SPDX-FileCopyrightText: 2019 Nicholas Tollervey, written for Adafruit Industries
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
Loading…
Reference in a new issue