From 3eb426c68a4457f43182df3005793c0a02eb8f4a Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 24 Nov 2023 09:30:15 +0000 Subject: [PATCH 1/8] CI & QA: Update packaging from latest boilerplate. --- .github/workflows/build.yml | 2 +- Makefile | 2 +- README.md | 2 +- check.sh | 1 + install.sh | 196 +++++++++++++++++++++++++++++------- pyproject.toml | 7 +- requirements-dev.txt | 1 + tox.ini | 2 +- uninstall.sh | 14 ++- 9 files changed, 184 insertions(+), 43 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ddee38e..87200ef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,7 +32,7 @@ jobs: - name: Build Packages run: | - make dist + make build - name: Upload Packages uses: actions/upload-artifact@v3 diff --git a/Makefile b/Makefile index 760b4b8..9e0c15c 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ clean: -rm -r dist testdeploy: build - twine upload --repository-url https://test.pypi.org/legacy/ dist/* + twine upload --repository testpypi dist/* deploy: nopost build twine upload dist/* diff --git a/README.md b/README.md index 11af3a7..4972c13 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # IO Expander -[![Build Status](https://travis-ci.com/pimoroni/ioe-python.svg?branch=master)](https://travis-ci.com/pimoroni/ioe-python) +[![Build Status](https://img.shields.io/github/actions/workflow/status/pimoroni/ioe-python/test.yml?branch=main)](https://github.com/pimoroni/ioe-python/actions/workflows/test.yml) [![Coverage Status](https://coveralls.io/repos/github/pimoroni/ioe-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/ioe-python?branch=master) [![PyPi Package](https://img.shields.io/pypi/v/pimoroni-ioexpander.svg)](https://pypi.python.org/pypi/pimoroni-ioexpander) [![Python Versions](https://img.shields.io/pypi/pyversions/pimoroni-ioexpander.svg)](https://pypi.python.org/pypi/pimoroni-ioexpander) diff --git a/check.sh b/check.sh index cbb1565..4395d89 100755 --- a/check.sh +++ b/check.sh @@ -6,6 +6,7 @@ NOPOST=$1 LIBRARY_NAME=`hatch project metadata name` LIBRARY_VERSION=`hatch version | awk -F "." '{print $1"."$2"."$3}'` POST_VERSION=`hatch version | awk -F "." '{print substr($4,0,length($4))}'` +TERM=${TERM:="xterm-256color"} success() { echo -e "$(tput setaf 2)$1$(tput sgr0)" diff --git a/install.sh b/install.sh index fafac26..c4d9f57 100755 --- a/install.sh +++ b/install.sh @@ -1,22 +1,26 @@ #!/bin/bash LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` -CONFIG=/boot/config.txt +MODULE_NAME='ioexpander' +CONFIG_FILE=config.txt +CONFIG_DIR="/boot/firmware" DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` CONFIG_BACKUP=false APT_HAS_UPDATED=false -RESOURCES_TOP_DIR=$HOME/Pimoroni +RESOURCES_TOP_DIR="$HOME/Pimoroni" +VENV_BASH_SNIPPET="$RESOURCES_TOP_DIR/auto_venv.sh" +VENV_DIR="$HOME/.virtualenvs/pimoroni" WD=`pwd` USAGE="./install.sh (--unstable)" POSITIONAL_ARGS=() FORCE=false UNSTABLE=false -PYTHON="/usr/bin/python3" +PYTHON="python" +CMD_ERRORS=false user_check() { if [ $(id -u) -eq 0 ]; then - printf "Script should not be run as root. Try './install.sh'\n" - exit 1 + fatal "Script should not be run as root. Try './install.sh'\n" fi } @@ -51,19 +55,91 @@ inform() { } warning() { - echo -e "$(tput setaf 1)$1$(tput sgr0)" + echo -e "$(tput setaf 1)⚠ WARNING:$(tput sgr0) $1" +} + +fatal() { + echo -e "$(tput setaf 1)⚠ FATAL:$(tput sgr0) $1" + exit 1 +} + +find_config() { + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + CONFIG_DIR="/boot" + if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then + fatal "Could not find $CONFIG_FILE!" + fi + else + if [ -f "/boot/$CONFIG_FILE" ] && [ ! -L "/boot/$CONFIG_FILE" ]; then + warning "Oops! It looks like /boot/$CONFIG_FILE is not a link to $CONFIG_DIR/$CONFIG_FILE" + warning "You might want to fix this!" + fi + fi + inform "Using $CONFIG_FILE in $CONFIG_DIR" +} + +venv_bash_snippet() { + inform "Checking for $VENV_BASH_SNIPPET\n" + if [ ! -f $VENV_BASH_SNIPPET ]; then + inform "Creating $VENV_BASH_SNIPPET\n" + mkdir -p $RESOURCES_TOP_DIR + cat << EOF > $VENV_BASH_SNIPPET +# Add "source $VENV_BASH_SNIPPET" to your ~/.bashrc to activate +# the Pimoroni virtual environment automagically! +VENV_DIR="$VENV_DIR" +if [ ! -f \$VENV_DIR/bin/activate ]; then + printf "Creating user Python environment in \$VENV_DIR, please wait...\n" + mkdir -p \$VENV_DIR + python3 -m venv --system-site-packages \$VENV_DIR +fi +printf " ↓ ↓ ↓ ↓ Hello, we've activated a Python venv for you. To exit, type \"deactivate\".\n" +source \$VENV_DIR/bin/activate +EOF + fi +} + +venv_check() { + PYTHON_BIN=`which $PYTHON` + if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then + printf "This script should be run in a virtual Python environment.\n" + if confirm "Would you like us to create and/or use a default one?"; then + printf "\n" + if [ ! -f $VENV_DIR/bin/activate ]; then + inform "Creating a new virtual Python environment in $VENV_DIR, please wait...\n" + mkdir -p $VENV_DIR + /usr/bin/python3 -m venv $VENV_DIR --system-site-packages + venv_bash_snippet + source $VENV_DIR/bin/activate + else + inform "Activating existing virtual Python environment in $VENV_DIR\n" + printf "source $VENV_DIR/bin/activate\n" + source $VENV_DIR/bin/activate + fi + else + printf "\n" + fatal "Please create and/or activate a virtual Python environment and try again!\n" + fi + fi + printf "\n" +} + +check_for_error() { + if [ $? -ne 0 ]; then + CMD_ERRORS=true + warning "^^^ 😬" + fi } function do_config_backup { if [ ! $CONFIG_BACKUP == true ]; then CONFIG_BACKUP=true FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" - inform "Backing up $CONFIG to /boot/$FILENAME\n" - sudo cp $CONFIG /boot/$FILENAME + inform "Backing up $CONFIG_DIR/$CONFIG_FILE to $CONFIG_DIR/$FILENAME\n" + sudo cp $CONFIG_DIR/$CONFIG_FILE $CONFIG_DIR/$FILENAME mkdir -p $RESOURCES_TOP_DIR/config-backups/ - cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$FILENAME + cp $CONFIG_DIR/$CONFIG_FILE $RESOURCES_TOP_DIR/config-backups/$FILENAME if [ -f "$UNINSTALLER" ]; then - echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG" >> $UNINSTALLER + echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG_DIR/$CONFIG_FILE" >> $UNINSTALLER fi fi } @@ -71,6 +147,7 @@ function do_config_backup { function apt_pkg_install { PACKAGES=() PACKAGES_IN=("$@") + # Check the list of packages and only run update/install if we need to for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do PACKAGE="${PACKAGES_IN[$i]}" if [ "$PACKAGE" == "" ]; then continue; fi @@ -82,12 +159,14 @@ function apt_pkg_install { done PACKAGES="${PACKAGES[@]}" if ! [ "$PACKAGES" == "" ]; then - echo "Installing missing packages: $PACKAGES" + printf "\n" + inform "Installing missing packages: $PACKAGES" if [ ! $APT_HAS_UPDATED ]; then sudo apt update APT_HAS_UPDATED=true fi sudo apt install -y $PACKAGES + check_for_error if [ -f "$UNINSTALLER" ]; then echo "apt uninstall -y $PACKAGES" >> $UNINSTALLER fi @@ -95,7 +174,9 @@ function apt_pkg_install { } function pip_pkg_install { + # A null Keyring prevents pip stalling in the background PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring $PYTHON -m pip install --upgrade "$@" + check_for_error } while [[ $# -gt 0 ]]; do @@ -125,29 +206,33 @@ while [[ $# -gt 0 ]]; do esac done +printf "Installing $LIBRARY_NAME...\n\n" + user_check +venv_check -if [ ! -f "$PYTHON" ]; then - printf "Python path $PYTHON not found!\n" - exit 1 +if [ ! -f `which $PYTHON` ]; then + fatal "Python path $PYTHON not found!\n" fi PYTHON_VER=`$PYTHON --version` -printf "$LIBRARY_NAME Python Library: Installer\n\n" - inform "Checking Dependencies. Please wait..." +# Install toml and try to read pyproject.toml into bash variables + pip_pkg_install toml CONFIG_VARS=`$PYTHON - < $UNINSTALLER printf "It's recommended you run these steps manually.\n" printf "If you want to run the full script, open it in\n" printf "an editor and remove 'exit 1' from below.\n" exit 1 +source $VIRTUAL_ENV/bin/activate EOF -if $UNSTABLE; then - warning "Installing unstable library from source.\n\n" -else - printf "Installing stable library from pypi.\n\n" -fi +printf "\n" inform "Installing for $PYTHON_VER...\n" + +# Install apt packages from pyproject.toml / tool.pimoroni.apt_packages apt_pkg_install "${APT_PACKAGES[@]}" + +printf "\n" + if $UNSTABLE; then + warning "Installing unstable library from source.\n" pip_pkg_install . else + inform "Installing stable library from pypi.\n" pip_pkg_install $LIBRARY_NAME fi + if [ $? -eq 0 ]; then success "Done!\n" echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER @@ -201,27 +295,40 @@ fi cd $WD +find_config + +# Run the setup commands from pyproject.toml / tool.pimoroni.commands + for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do CMD="${SETUP_CMDS[$i]}" - # Attempt to catch anything that touches /boot/config.txt and trigger a backup - if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG"* ]] || [[ "$CMD" == *"\$CONFIG"* ]]; then + # Attempt to catch anything that touches config.txt and trigger a backup + if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG_DIR/$CONFIG_FILE"* ]] || [[ "$CMD" == *"\$CONFIG_DIR/\$CONFIG_FILE"* ]]; then do_config_backup fi eval $CMD + check_for_error done +printf "\n" + +# Add the config.txt entries from pyproject.toml / tool.pimoroni.configtxt + for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do CONFIG_LINE="${CONFIG_TXT[$i]}" if ! [ "$CONFIG_LINE" == "" ]; then do_config_backup - inform "Adding $CONFIG_LINE to $CONFIG\n" - sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG - if ! grep -q "^$CONFIG_LINE" $CONFIG; then - printf "$CONFIG_LINE\n" | sudo tee --append $CONFIG + inform "Adding $CONFIG_LINE to $CONFIG_DIR/$CONFIG_FILE" + sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG_DIR/$CONFIG_FILE + if ! grep -q "^$CONFIG_LINE" $CONFIG_DIR/$CONFIG_FILE; then + printf "$CONFIG_LINE\n" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE fi fi done +printf "\n" + +# Just a straight copy of the examples/ dir into ~/Pimoroni/board/examples + if [ -d "examples" ]; then if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then inform "Copying examples to $RESOURCES_DIR" @@ -233,10 +340,13 @@ fi printf "\n" +# Use pdoc to generate basic documentation from the installed module + if confirm "Would you like to generate documentation?"; then + inform "Installing pdoc. Please wait..." pip_pkg_install pdoc - printf "Generating documentation.\n" - $PYTHON -m pdoc ioexpander -o $RESOURCES_DIR/docs > /dev/null + inform "Generating documentation.\n" + $PYTHON -m pdoc $MODULE_NAME -o $RESOURCES_DIR/docs > /dev/null if [ $? -eq 0 ]; then inform "Documentation saved to $RESOURCES_DIR/docs" success "Done!" @@ -245,6 +355,22 @@ if confirm "Would you like to generate documentation?"; then fi fi -success "\nAll done!" -inform "If this is your first time installing you should reboot for hardware changes to take effect.\n" -inform "Find uninstall steps in $UNINSTALLER\n" +printf "\n" + +if [ "$CMD_ERRORS" = true ]; then + warning "One or more setup commands appear to have failed." + printf "This might prevent things from working properly.\n" + printf "Make sure your OS is up to date and try re-running this installer.\n" + printf "If things still don't work, report this or find help at $GITHUB_URL.\n\n" +else + success "\nAll done!" +fi + +printf "If this is your first time installing you should reboot for hardware changes to take effect.\n" +printf "Find uninstall steps in $UNINSTALLER\n\n" + +if [ "$CMD_ERRORS" = true ]; then + exit 1 +else + exit 0 +fi \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 2207e00..a6a13e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -100,6 +100,9 @@ skip = """ [tool.isort] line_length = 220 +[tool.black] +line-length = 220 + [tool.check-manifest] ignore = [ '.stickler.yml', @@ -114,7 +117,7 @@ ignore = [ '.coveragerc' ] -[pimoroni] -apt_packages = ["python3-rpi.gpio", "python3-smbus"] +[tool.pimoroni] +apt_packages = [] configtxt = [] commands = [] diff --git a/requirements-dev.txt b/requirements-dev.txt index 08e9dec..525b042 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,3 +6,4 @@ twine hatch hatch-fancy-pypi-readme tox +pdoc diff --git a/tox.ini b/tox.ini index 090fdb0..44c8654 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ commands = python -m build --no-isolation python -m twine check dist/* isort --check . - ruff --format=github . + ruff . codespell . deps = check-manifest diff --git a/uninstall.sh b/uninstall.sh index d5e1b5f..f213fc5 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -3,11 +3,20 @@ FORCE=false LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` RESOURCES_DIR=$HOME/Pimoroni/$LIBRARY_NAME -PYTHON="/usr/bin/python3" +PYTHON="python" + + +venv_check() { + PYTHON_BIN=`which $PYTHON` + if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then + printf "This script should be run in a virtual Python environment.\n" + exit 1 + fi +} user_check() { if [ $(id -u) -eq 0 ]; then - printf "Script should not be run as root. Try './install.sh'\n" + printf "Script should not be run as root. Try './uninstall.sh'\n" exit 1 fi } @@ -49,6 +58,7 @@ warning() { printf "$LIBRARY_NAME Python Library: Uninstaller\n\n" user_check +venv_check printf "Uninstalling for Python 3...\n" $PYTHON -m pip uninstall $LIBRARY_NAME From 60053ea14529a6f672d39ee058f0f5b63d367938 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Fri, 24 Nov 2023 09:45:30 +0000 Subject: [PATCH 2/8] QA: Cherry pick black suggestions for better code consistency. --- ioexpander/__init__.py | 8 +- ioexpander/ioe_regs.py | 176 ++++++++++++++++++++--------------------- tests/conftest.py | 11 +-- tests/test_io.py | 2 +- tests/test_setup.py | 2 +- 5 files changed, 100 insertions(+), 99 deletions(-) diff --git a/ioexpander/__init__.py b/ioexpander/__init__.py index fb49472..8429d6c 100644 --- a/ioexpander/__init__.py +++ b/ioexpander/__init__.py @@ -240,8 +240,8 @@ def i2c_write8(self, reg, value): def i2c_write16(self, reg_l, reg_h, value): """Write two (8+8bit) registers to the device, as a single write if they are consecutive.""" - val_l = value & 0xff - val_h = (value >> 8) & 0xff + val_l = value & 0xFF + val_h = (value >> 8) & 0xFF if reg_h == reg_l + 1: msg_w = i2c_msg.write(self._i2c_addr, [reg_l, val_l, val_h]) self._i2c_dev.i2c_rdwr(msg_w) @@ -285,7 +285,7 @@ def read_switch_counter(self, pin): # The switch counter is 7-bit # The most significant bit encodes the current GPIO state - return value & 0x7f, value & 0x80 == 0x80 + return value & 0x7F, value & 0x80 == 0x80 def clear_switch_counter(self, pin): """Clear the switch count value on a pin to 0.""" @@ -689,7 +689,7 @@ def input(self, pin, adc_timeout=1): self.i2c_write8(self.REG_AINDIDS0, 1 << io_pin.adc_channel) con0value = self.i2c_read8(self.REG_ADCCON0) - con0value = con0value & ~0x0f + con0value = con0value & ~0x0F con0value = con0value | io_pin.adc_channel con0value = con0value & ~(1 << 7) # ADCF - Clear the conversion complete flag diff --git a/ioexpander/ioe_regs.py b/ioexpander/ioe_regs.py index 57b26e6..bfd0965 100644 --- a/ioexpander/ioe_regs.py +++ b/ioexpander/ioe_regs.py @@ -3,9 +3,9 @@ class REGS: CHIP_ID = 0xE26A CHIP_VERSION = 2 - REG_CHIP_ID_L = 0xfa - REG_CHIP_ID_H = 0xfb - REG_VERSION = 0xfc + REG_CHIP_ID_L = 0xFA + REG_CHIP_ID_H = 0xFB + REG_VERSION = 0xFC # Rotary encoder REG_ENC_EN = 0x04 @@ -52,12 +52,12 @@ class REGS: REG_PCON = 0x47 # Read only REG_TCON = 0x48 REG_TMOD = 0x49 - REG_TL0 = 0x4a - REG_TL1 = 0x4b - REG_TH0 = 0x4c - REG_TH1 = 0x4d - REG_CKCON = 0x4e - REG_WKCON = 0x4f # Read only + REG_TL0 = 0x4A + REG_TL1 = 0x4B + REG_TH0 = 0x4C + REG_TH1 = 0x4D + REG_CKCON = 0x4E + REG_WKCON = 0x4F # Read only REG_P1 = 0x50 # protect_bits 3 6 # Bit addressing REG_SFRS = 0x51 # TA protected # Read only REG_CAPCON0 = 0x52 @@ -68,10 +68,10 @@ class REGS: REG_CKEN = 0x57 # TA protected # Read only REG_SCON = 0x58 REG_SBUF = 0x59 - REG_SBUF_1 = 0x5a - REG_EIE = 0x5b # Read only - REG_EIE1 = 0x5c # Read only - REG_CHPCON = 0x5f # TA protected # Read only + REG_SBUF_1 = 0x5A + REG_EIE = 0x5B # Read only + REG_EIE1 = 0x5C # Read only + REG_CHPCON = 0x5F # TA protected # Read only REG_P2 = 0x60 # Bit addressing REG_AUXR1 = 0x62 REG_BODCON0 = 0x63 # TA protected @@ -81,55 +81,55 @@ class REGS: REG_IAPAH = 0x67 # Read only REG_IE = 0x68 # Read only REG_SADDR = 0x69 - REG_WDCON = 0x6a # TA protected - REG_BODCON1 = 0x6b # TA protected - REG_P3M1 = 0x6c - REG_P3S = 0xc0 # Page 1 # Reassigned from 0x6c to avoid collision - REG_P3M2 = 0x6d - REG_P3SR = 0xc1 # Page 1 # Reassigned from 0x6d to avoid collision - REG_IAPFD = 0x6e # Read only - REG_IAPCN = 0x6f # Read only + REG_WDCON = 0x6A # TA protected + REG_BODCON1 = 0x6B # TA protected + REG_P3M1 = 0x6C + REG_P3S = 0xC0 # Page 1 # Reassigned from 0x6c to avoid collision + REG_P3M2 = 0x6D + REG_P3SR = 0xC1 # Page 1 # Reassigned from 0x6d to avoid collision + REG_IAPFD = 0x6E # Read only + REG_IAPCN = 0x6F # Read only REG_P3 = 0x70 # Bit addressing REG_P0M1 = 0x71 # protect_bits 2 - REG_P0S = 0xc2 # Page 1 # Reassigned from 0x71 to avoid collision + REG_P0S = 0xC2 # Page 1 # Reassigned from 0x71 to avoid collision REG_P0M2 = 0x72 # protect_bits 2 - REG_P0SR = 0xc3 # Page 1 # Reassigned from 0x72 to avoid collision + REG_P0SR = 0xC3 # Page 1 # Reassigned from 0x72 to avoid collision REG_P1M1 = 0x73 # protect_bits 3 6 - REG_P1S = 0xc4 # Page 1 # Reassigned from 0x73 to avoid collision + REG_P1S = 0xC4 # Page 1 # Reassigned from 0x73 to avoid collision REG_P1M2 = 0x74 # protect_bits 3 6 - REG_P1SR = 0xc5 # Page 1 # Reassigned from 0x74 to avoid collision + REG_P1SR = 0xC5 # Page 1 # Reassigned from 0x74 to avoid collision REG_P2S = 0x75 REG_IPH = 0x77 # Read only - REG_PWMINTC = 0xc6 # Page 1 # Read only # Reassigned from 0x77 to avoid collision + REG_PWMINTC = 0xC6 # Page 1 # Read only # Reassigned from 0x77 to avoid collision REG_IP = 0x78 # Read only REG_SADEN = 0x79 - REG_SADEN_1 = 0x7a - REG_SADDR_1 = 0x7b - REG_I2DAT = 0x7c # Read only - REG_I2STAT = 0x7d # Read only - REG_I2CLK = 0x7e # Read only - REG_I2TOC = 0x7f # Read only + REG_SADEN_1 = 0x7A + REG_SADDR_1 = 0x7B + REG_I2DAT = 0x7C # Read only + REG_I2STAT = 0x7D # Read only + REG_I2CLK = 0x7E # Read only + REG_I2TOC = 0x7F # Read only REG_I2CON = 0x80 # Read only REG_I2ADDR = 0x81 # Read only REG_ADCRL = 0x82 REG_ADCRH = 0x83 REG_T3CON = 0x84 - REG_PWM4H = 0xc7 # Page 1 # Reassigned from 0x84 to avoid collision + REG_PWM4H = 0xC7 # Page 1 # Reassigned from 0x84 to avoid collision REG_RL3 = 0x85 - REG_PWM5H = 0xc8 # Page 1 # Reassigned from 0x85 to avoid collision + REG_PWM5H = 0xC8 # Page 1 # Reassigned from 0x85 to avoid collision REG_RH3 = 0x86 - REG_PIOCON1 = 0xc9 # Page 1 # Reassigned from 0x86 to avoid collision + REG_PIOCON1 = 0xC9 # Page 1 # Reassigned from 0x86 to avoid collision REG_TA = 0x87 # Read only REG_T2CON = 0x88 REG_T2MOD = 0x89 - REG_RCMP2L = 0x8a - REG_RCMP2H = 0x8b - REG_TL2 = 0x8c - REG_PWM4L = 0xca # Page 1 # Reassigned from 0x8c to avoid collision - REG_TH2 = 0x8d - REG_PWM5L = 0xcb # Page 1 # Reassigned from 0x8d to avoid collision - REG_ADCMPL = 0x8e - REG_ADCMPH = 0x8f + REG_RCMP2L = 0x8A + REG_RCMP2H = 0x8B + REG_TL2 = 0x8C + REG_PWM4L = 0xCA # Page 1 # Reassigned from 0x8c to avoid collision + REG_TH2 = 0x8D + REG_PWM5L = 0xCB # Page 1 # Reassigned from 0x8d to avoid collision + REG_ADCMPL = 0x8E + REG_ADCMPH = 0x8F REG_PSW = 0x90 # Read only REG_PWMPH = 0x91 REG_PWM0H = 0x92 @@ -140,50 +140,50 @@ class REGS: REG_FBD = 0x97 REG_PWMCON0 = 0x98 REG_PWMPL = 0x99 - REG_PWM0L = 0x9a - REG_PWM1L = 0x9b - REG_PWM2L = 0x9c - REG_PWM3L = 0x9d - REG_PIOCON0 = 0x9e - REG_PWMCON1 = 0x9f - REG_ACC = 0xa0 # Read only - REG_ADCCON1 = 0xa1 - REG_ADCCON2 = 0xa2 - REG_ADCDLY = 0xa3 - REG_C0L = 0xa4 - REG_C0H = 0xa5 - REG_C1L = 0xa6 - REG_C1H = 0xa7 - REG_ADCCON0 = 0xa8 - REG_PICON = 0xa9 # Read only - REG_PINEN = 0xaa # Read only - REG_PIPEN = 0xab # Read only - REG_PIF = 0xac # Read only - REG_C2L = 0xad - REG_C2H = 0xae - REG_EIP = 0xaf # Read only - REG_B = 0xb0 # Read only - REG_CAPCON3 = 0xb1 - REG_CAPCON4 = 0xb2 - REG_SPCR = 0xb3 - REG_SPCR2 = 0xcc # Page 1 # Reassigned from 0xb3 to avoid collision - REG_SPSR = 0xb4 - REG_SPDR = 0xb5 - REG_AINDIDS0 = 0xb6 - REG_AINDIDS1 = None # Added to have common code with SuperIO - REG_EIPH = 0xb7 # Read only - REG_SCON_1 = 0xb8 - REG_PDTEN = 0xb9 # TA protected - REG_PDTCNT = 0xba # TA protected - REG_PMEN = 0xbb - REG_PMD = 0xbc - REG_EIP1 = 0xbe # Read only - REG_EIPH1 = 0xbf # Read only + REG_PWM0L = 0x9A + REG_PWM1L = 0x9B + REG_PWM2L = 0x9C + REG_PWM3L = 0x9D + REG_PIOCON0 = 0x9E + REG_PWMCON1 = 0x9F + REG_ACC = 0xA0 # Read only + REG_ADCCON1 = 0xA1 + REG_ADCCON2 = 0xA2 + REG_ADCDLY = 0xA3 + REG_C0L = 0xA4 + REG_C0H = 0xA5 + REG_C1L = 0xA6 + REG_C1H = 0xA7 + REG_ADCCON0 = 0xA8 + REG_PICON = 0xA9 # Read only + REG_PINEN = 0xAA # Read only + REG_PIPEN = 0xAB # Read only + REG_PIF = 0xAC # Read only + REG_C2L = 0xAD + REG_C2H = 0xAE + REG_EIP = 0xAF # Read only + REG_B = 0xB0 # Read only + REG_CAPCON3 = 0xB1 + REG_CAPCON4 = 0xB2 + REG_SPCR = 0xB3 + REG_SPCR2 = 0xCC # Page 1 # Reassigned from 0xb3 to avoid collision + REG_SPSR = 0xB4 + REG_SPDR = 0xB5 + REG_AINDIDS0 = 0xB6 + REG_AINDIDS1 = None # Added to have common code with SuperIO + REG_EIPH = 0xB7 # Read only + REG_SCON_1 = 0xB8 + REG_PDTEN = 0xB9 # TA protected + REG_PDTCNT = 0xBA # TA protected + REG_PMEN = 0xBB + REG_PMD = 0xBC + REG_EIP1 = 0xBE # Read only + REG_EIPH1 = 0xBF # Read only - REG_USER_FLASH = 0xd0 - REG_FLASH_PAGE = 0xf0 + REG_USER_FLASH = 0xD0 + REG_FLASH_PAGE = 0xF0 - REG_INT = 0xf9 + REG_INT = 0xF9 MASK_INT_TRIG = 0x1 MASK_INT_OUT = 0x2 BIT_INT_TRIGD = 0 @@ -194,10 +194,10 @@ class REGS: REG_INT_MASK_P1 = 0x01 REG_INT_MASK_P3 = 0x03 - REG_VERSION = 0xfc - REG_ADDR = 0xfd + REG_VERSION = 0xFC + REG_ADDR = 0xFD - REG_CTRL = 0xfe # 0 = Sleep, 1 = Reset, 2 = Read Flash, 3 = Write Flash, 4 = Addr Unlock + REG_CTRL = 0xFE # 0 = Sleep, 1 = Reset, 2 = Read Flash, 3 = Write Flash, 4 = Addr Unlock MASK_CTRL_SLEEP = 0x1 MASK_CTRL_RESET = 0x2 MASK_CTRL_FREAD = 0x4 diff --git a/tests/conftest.py b/tests/conftest.py index e1870f4..ba91a0d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ import pytest -@pytest.fixture(scope='function', autouse=True) +@pytest.fixture(scope="function", autouse=True) def cleanup(): """This fixture removes modules under test from sys.modules. @@ -20,19 +20,20 @@ def cleanup(): pass -@pytest.fixture(scope='function', autouse=False) +@pytest.fixture(scope="function", autouse=False) def smbus2(): """Mock smbus2 module.""" smbus2 = mock.MagicMock() smbus2.i2c_msg.read().__iter__.return_value = [0b00000000] - sys.modules['smbus2'] = smbus2 + sys.modules["smbus2"] = smbus2 yield smbus2 - del sys.modules['smbus2'] + del sys.modules["smbus2"] -@pytest.fixture(scope='function') +@pytest.fixture(scope="function") def ioe(): from ioexpander import IOE + yield IOE(skip_chip_id_check=True) del sys.modules["ioexpander"] diff --git a/tests/test_io.py b/tests/test_io.py index 359a61c..13f5315 100644 --- a/tests/test_io.py +++ b/tests/test_io.py @@ -35,7 +35,7 @@ def test_adc_input(smbus2): smbus2.i2c_msg.read().__iter__.return_value = [0b10000000, 0b10000000] result = ioe.input(7) - assert type(result) is float + assert isinstance(result, float) # (128 << 4) | 128 / 4095.0 * 3.3 # round to 2dp to account for FLOATING POINT WEIRDNESS! diff --git a/tests/test_setup.py b/tests/test_setup.py index c72ee79..8318105 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -29,7 +29,7 @@ def test_setup_valid_chip_id(smbus2): self._i2c_dev.i2c_rdwr(msg_r) return list(msg_r)[0] """ - smbus2.i2c_msg.read.side_effect = [[0x6a, 0xe2]] + smbus2.i2c_msg.read.side_effect = [[0x6A, 0xE2]] ioe = IOE() del ioe From 2288b71d2c02c09e727ea12dd1a46aa8f10234cf Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 10 Jan 2024 14:37:56 +0000 Subject: [PATCH 3/8] Sync with latest boilerplate. * CI: Update GitHub Actions versions. * QA: Add shellcheck and fix/ignore all issues. * install.sh: slightly better feedback for setup commands. --- .github/workflows/build.yml | 6 +- .github/workflows/qa.yml | 9 ++- .github/workflows/test.yml | 2 +- Makefile | 5 +- check.sh | 20 +++--- install.sh | 120 +++++++++++++++++------------------- uninstall.sh | 14 ++--- 7 files changed, 87 insertions(+), 89 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 87200ef..07620e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,10 +19,10 @@ jobs: steps: - name: Checkout Code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} @@ -35,7 +35,7 @@ jobs: make build - name: Upload Packages - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: ${{ env.RELEASE_FILE }} path: dist/ diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 4f85883..ac672a5 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -10,16 +10,15 @@ jobs: test: name: linting & spelling runs-on: ubuntu-latest - env: TERM: xterm-256color steps: - name: Checkout Code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Set up Python '3,11' - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: '3.11' @@ -34,3 +33,7 @@ jobs: - name: Run Code Checks run: | make check + + - name: Run Bash Code Checks + run: | + make shellcheck diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 016a678..6f8cff7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -19,7 +19,7 @@ jobs: uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} diff --git a/Makefile b/Makefile index 9e0c15c..34f4a7d 100644 --- a/Makefile +++ b/Makefile @@ -30,11 +30,14 @@ uninstall: dev-deps: python3 -m pip install -r requirements-dev.txt - sudo apt install dos2unix + sudo apt install dos2unix shellcheck check: @bash check.sh +shellcheck: + shellcheck *.sh + qa: tox -e qa diff --git a/check.sh b/check.sh index 4395d89..38dfc3a 100755 --- a/check.sh +++ b/check.sh @@ -3,9 +3,9 @@ # This script handles some basic QA checks on the source NOPOST=$1 -LIBRARY_NAME=`hatch project metadata name` -LIBRARY_VERSION=`hatch version | awk -F "." '{print $1"."$2"."$3}'` -POST_VERSION=`hatch version | awk -F "." '{print substr($4,0,length($4))}'` +LIBRARY_NAME=$(hatch project metadata name) +LIBRARY_VERSION=$(hatch version | awk -F "." '{print $1"."$2"."$3}') +POST_VERSION=$(hatch version | awk -F "." '{print substr($4,0,length($4))}') TERM=${TERM:="xterm-256color"} success() { @@ -29,7 +29,7 @@ while [[ $# -gt 0 ]]; do ;; *) if [[ $1 == -* ]]; then - printf "Unrecognised option: $1\n"; + printf "Unrecognised option: %s\n" "$1"; exit 1 fi POSITIONAL_ARGS+=("$1") @@ -40,8 +40,7 @@ done inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" inform "Checking for trailing whitespace..." -grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO -if [[ $? -eq 0 ]]; then +if grep -IUrn --color "[[:blank:]]$" --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO; then warning "Trailing whitespace found!" exit 1 else @@ -50,8 +49,7 @@ fi printf "\n" inform "Checking for DOS line-endings..." -grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile -if [[ $? -eq 0 ]]; then +if grep -lIUrn --color $'\r' --exclude-dir=dist --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile; then warning "DOS line-endings found!" exit 1 else @@ -60,8 +58,7 @@ fi printf "\n" inform "Checking CHANGELOG.md..." -cat CHANGELOG.md | grep ^${LIBRARY_VERSION} > /dev/null 2>&1 -if [[ $? -eq 1 ]]; then +if ! grep "^${LIBRARY_VERSION}" CHANGELOG.md > /dev/null 2>&1; then warning "Changes missing for version ${LIBRARY_VERSION}! Please update CHANGELOG.md." exit 1 else @@ -70,8 +67,7 @@ fi printf "\n" inform "Checking for git tag ${LIBRARY_VERSION}..." -git tag -l | grep -E "${LIBRARY_VERSION}$" -if [[ $? -eq 1 ]]; then +if ! git tag -l | grep -E "${LIBRARY_VERSION}$"; then warning "Missing git tag for version ${LIBRARY_VERSION}" fi printf "\n" diff --git a/install.sh b/install.sh index c4d9f57..059d3c4 100755 --- a/install.sh +++ b/install.sh @@ -1,15 +1,13 @@ #!/bin/bash -LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` -MODULE_NAME='ioexpander' +LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') CONFIG_FILE=config.txt CONFIG_DIR="/boot/firmware" -DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` +DATESTAMP=$(date "+%Y-%m-%d-%H-%M-%S") CONFIG_BACKUP=false APT_HAS_UPDATED=false RESOURCES_TOP_DIR="$HOME/Pimoroni" VENV_BASH_SNIPPET="$RESOURCES_TOP_DIR/auto_venv.sh" VENV_DIR="$HOME/.virtualenvs/pimoroni" -WD=`pwd` USAGE="./install.sh (--unstable)" POSITIONAL_ARGS=() FORCE=false @@ -19,7 +17,7 @@ CMD_ERRORS=false user_check() { - if [ $(id -u) -eq 0 ]; then + if [ "$(id -u)" -eq 0 ]; then fatal "Script should not be run as root. Try './install.sh'\n" fi } @@ -37,15 +35,6 @@ confirm() { fi } -prompt() { - read -r -p "$1 [y/N] " response < /dev/tty - if [[ $response =~ ^(yes|y|Y)$ ]]; then - true - else - false - fi -} - success() { echo -e "$(tput setaf 2)$1$(tput sgr0)" } @@ -80,10 +69,10 @@ find_config() { venv_bash_snippet() { inform "Checking for $VENV_BASH_SNIPPET\n" - if [ ! -f $VENV_BASH_SNIPPET ]; then + if [ ! -f "$VENV_BASH_SNIPPET" ]; then inform "Creating $VENV_BASH_SNIPPET\n" - mkdir -p $RESOURCES_TOP_DIR - cat << EOF > $VENV_BASH_SNIPPET + mkdir -p "$RESOURCES_TOP_DIR" + cat << EOF > "$VENV_BASH_SNIPPET" # Add "source $VENV_BASH_SNIPPET" to your ~/.bashrc to activate # the Pimoroni virtual environment automagically! VENV_DIR="$VENV_DIR" @@ -99,21 +88,23 @@ EOF } venv_check() { - PYTHON_BIN=`which $PYTHON` + PYTHON_BIN=$(which "$PYTHON") if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then printf "This script should be run in a virtual Python environment.\n" if confirm "Would you like us to create and/or use a default one?"; then printf "\n" - if [ ! -f $VENV_DIR/bin/activate ]; then + if [ ! -f "$VENV_DIR/bin/activate" ]; then inform "Creating a new virtual Python environment in $VENV_DIR, please wait...\n" - mkdir -p $VENV_DIR - /usr/bin/python3 -m venv $VENV_DIR --system-site-packages + mkdir -p "$VENV_DIR" + /usr/bin/python3 -m venv "$VENV_DIR" --system-site-packages venv_bash_snippet - source $VENV_DIR/bin/activate + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" else inform "Activating existing virtual Python environment in $VENV_DIR\n" - printf "source $VENV_DIR/bin/activate\n" - source $VENV_DIR/bin/activate + printf "source \"%s/bin/activate\"\n" "$VENV_DIR" + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" fi else printf "\n" @@ -126,7 +117,7 @@ venv_check() { check_for_error() { if [ $? -ne 0 ]; then CMD_ERRORS=true - warning "^^^ 😬" + warning "^^^ 😬 previous command did not exit cleanly!" fi } @@ -135,29 +126,29 @@ function do_config_backup { CONFIG_BACKUP=true FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" inform "Backing up $CONFIG_DIR/$CONFIG_FILE to $CONFIG_DIR/$FILENAME\n" - sudo cp $CONFIG_DIR/$CONFIG_FILE $CONFIG_DIR/$FILENAME - mkdir -p $RESOURCES_TOP_DIR/config-backups/ - cp $CONFIG_DIR/$CONFIG_FILE $RESOURCES_TOP_DIR/config-backups/$FILENAME + sudo cp "$CONFIG_DIR/$CONFIG_FILE $CONFIG_DIR/$FILENAME" + mkdir -p "$RESOURCES_TOP_DIR/config-backups/" + cp $CONFIG_DIR/$CONFIG_FILE "$RESOURCES_TOP_DIR/config-backups/$FILENAME" if [ -f "$UNINSTALLER" ]; then - echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG_DIR/$CONFIG_FILE" >> $UNINSTALLER + echo "cp $RESOURCES_TOP_DIR/config-backups/$FILENAME $CONFIG_DIR/$CONFIG_FILE" >> "$UNINSTALLER" fi fi } function apt_pkg_install { - PACKAGES=() + PACKAGES_NEEDED=() PACKAGES_IN=("$@") # Check the list of packages and only run update/install if we need to for ((i = 0; i < ${#PACKAGES_IN[@]}; i++)); do PACKAGE="${PACKAGES_IN[$i]}" if [ "$PACKAGE" == "" ]; then continue; fi - printf "Checking for $PACKAGE\n" - dpkg -L $PACKAGE > /dev/null 2>&1 + printf "Checking for %s\n" "$PACKAGE" + dpkg -L "$PACKAGE" > /dev/null 2>&1 if [ "$?" == "1" ]; then - PACKAGES+=("$PACKAGE") + PACKAGES_NEEDED+=("$PACKAGE") fi done - PACKAGES="${PACKAGES[@]}" + PACKAGES="${PACKAGES_NEEDED[*]}" if ! [ "$PACKAGES" == "" ]; then printf "\n" inform "Installing missing packages: $PACKAGES" @@ -165,10 +156,10 @@ function apt_pkg_install { sudo apt update APT_HAS_UPDATED=true fi - sudo apt install -y $PACKAGES + sudo apt install -y "$PACKAGES" check_for_error if [ -f "$UNINSTALLER" ]; then - echo "apt uninstall -y $PACKAGES" >> $UNINSTALLER + echo "apt uninstall -y $PACKAGES" >> "$UNINSTALLER" fi fi } @@ -197,8 +188,8 @@ while [[ $# -gt 0 ]]; do ;; *) if [[ $1 == -* ]]; then - printf "Unrecognised option: $1\n"; - printf "Usage: $USAGE\n"; + printf "Unrecognised option: %s\n" "$1"; + printf "Usage: %s\n" "$USAGE"; exit 1 fi POSITIONAL_ARGS+=("$1") @@ -206,16 +197,16 @@ while [[ $# -gt 0 ]]; do esac done -printf "Installing $LIBRARY_NAME...\n\n" +printf "Installing %s...\n\n" "$LIBRARY_NAME" user_check venv_check -if [ ! -f `which $PYTHON` ]; then - fatal "Python path $PYTHON not found!\n" +if [ ! -f "$(which "$PYTHON")" ]; then + fatal "Python path %s not found!\n" "$PYTHON" fi -PYTHON_VER=`$PYTHON --version` +PYTHON_VER=$($PYTHON --version) inform "Checking Dependencies. Please wait..." @@ -223,7 +214,8 @@ inform "Checking Dependencies. Please wait..." pip_pkg_install toml -CONFIG_VARS=`$PYTHON - < $UNINSTALLER +cat << EOF > "$UNINSTALLER" printf "It's recommended you run these steps manually.\n" printf "If you want to run the full script, open it in\n" printf "an editor and remove 'exit 1' from below.\n" @@ -285,27 +279,30 @@ if $UNSTABLE; then pip_pkg_install . else inform "Installing stable library from pypi.\n" - pip_pkg_install $LIBRARY_NAME + pip_pkg_install "$LIBRARY_NAME" fi +# shellcheck disable=SC2181 # One of two commands run, depending on --unstable flag if [ $? -eq 0 ]; then success "Done!\n" - echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> $UNINSTALLER + echo "$PYTHON -m pip uninstall $LIBRARY_NAME" >> "$UNINSTALLER" fi -cd $WD - find_config +printf "\n" + # Run the setup commands from pyproject.toml / tool.pimoroni.commands +inform "Running setup commands...\n" for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do CMD="${SETUP_CMDS[$i]}" # Attempt to catch anything that touches config.txt and trigger a backup if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG_DIR/$CONFIG_FILE"* ]] || [[ "$CMD" == *"\$CONFIG_DIR/\$CONFIG_FILE"* ]]; then do_config_backup fi - eval $CMD + printf "\"%s\"\n" "$CMD" + eval "$CMD" check_for_error done @@ -320,7 +317,7 @@ for ((i = 0; i < ${#CONFIG_TXT[@]}; i++)); do inform "Adding $CONFIG_LINE to $CONFIG_DIR/$CONFIG_FILE" sudo sed -i "s/^#$CONFIG_LINE/$CONFIG_LINE/" $CONFIG_DIR/$CONFIG_FILE if ! grep -q "^$CONFIG_LINE" $CONFIG_DIR/$CONFIG_FILE; then - printf "$CONFIG_LINE\n" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE + printf "%s \n" "$CONFIG_LINE" | sudo tee --append $CONFIG_DIR/$CONFIG_FILE fi fi done @@ -332,8 +329,8 @@ printf "\n" if [ -d "examples" ]; then if confirm "Would you like to copy examples to $RESOURCES_DIR?"; then inform "Copying examples to $RESOURCES_DIR" - cp -r examples/ $RESOURCES_DIR - echo "rm -r $RESOURCES_DIR" >> $UNINSTALLER + cp -r examples/ "$RESOURCES_DIR" + echo "rm -r $RESOURCES_DIR" >> "$UNINSTALLER" success "Done!" fi fi @@ -346,8 +343,7 @@ if confirm "Would you like to generate documentation?"; then inform "Installing pdoc. Please wait..." pip_pkg_install pdoc inform "Generating documentation.\n" - $PYTHON -m pdoc $MODULE_NAME -o $RESOURCES_DIR/docs > /dev/null - if [ $? -eq 0 ]; then + if $PYTHON -m pdoc "$LIBRARY_NAME" -o "$RESOURCES_DIR/docs" > /dev/null; then inform "Documentation saved to $RESOURCES_DIR/docs" success "Done!" else @@ -361,16 +357,16 @@ if [ "$CMD_ERRORS" = true ]; then warning "One or more setup commands appear to have failed." printf "This might prevent things from working properly.\n" printf "Make sure your OS is up to date and try re-running this installer.\n" - printf "If things still don't work, report this or find help at $GITHUB_URL.\n\n" + printf "If things still don't work, report this or find help at %s.\n\n" "$GITHUB_URL" else success "\nAll done!" fi printf "If this is your first time installing you should reboot for hardware changes to take effect.\n" -printf "Find uninstall steps in $UNINSTALLER\n\n" +printf "Find uninstall steps in %s\n\n" "$UNINSTALLER" if [ "$CMD_ERRORS" = true ]; then exit 1 else exit 0 -fi \ No newline at end of file +fi diff --git a/uninstall.sh b/uninstall.sh index f213fc5..3314b7f 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,13 +1,13 @@ #!/bin/bash FORCE=false -LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` +LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') RESOURCES_DIR=$HOME/Pimoroni/$LIBRARY_NAME PYTHON="python" venv_check() { - PYTHON_BIN=`which $PYTHON` + PYTHON_BIN=$(which $PYTHON) if [[ $VIRTUAL_ENV == "" ]] || [[ $PYTHON_BIN != $VIRTUAL_ENV* ]]; then printf "This script should be run in a virtual Python environment.\n" exit 1 @@ -15,7 +15,7 @@ venv_check() { } user_check() { - if [ $(id -u) -eq 0 ]; then + if [ "$(id -u)" -eq 0 ]; then printf "Script should not be run as root. Try './uninstall.sh'\n" exit 1 fi @@ -55,17 +55,17 @@ warning() { echo -e "$(tput setaf 1)$1$(tput sgr0)" } -printf "$LIBRARY_NAME Python Library: Uninstaller\n\n" +printf "%s Python Library: Uninstaller\n\n" "$LIBRARY_NAME" user_check venv_check printf "Uninstalling for Python 3...\n" -$PYTHON -m pip uninstall $LIBRARY_NAME +$PYTHON -m pip uninstall "$LIBRARY_NAME" -if [ -d $RESOURCES_DIR ]; then +if [ -d "$RESOURCES_DIR" ]; then if confirm "Would you like to delete $RESOURCES_DIR?"; then - rm -r $RESOURCES_DIR + rm -r "$RESOURCES_DIR" fi fi From 1d9e9599466408468b4c2bd85c0e2e33b4c60a92 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 10 Jan 2024 14:51:24 +0000 Subject: [PATCH 4/8] Sync with latest boilerplate. * install.sh: fix quoting bug in do_config_backup. * install.sh: don't output printf commands. --- install.sh | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 059d3c4..bb671f2 100755 --- a/install.sh +++ b/install.sh @@ -126,7 +126,7 @@ function do_config_backup { CONFIG_BACKUP=true FILENAME="config.preinstall-$LIBRARY_NAME-$DATESTAMP.txt" inform "Backing up $CONFIG_DIR/$CONFIG_FILE to $CONFIG_DIR/$FILENAME\n" - sudo cp "$CONFIG_DIR/$CONFIG_FILE $CONFIG_DIR/$FILENAME" + sudo cp "$CONFIG_DIR/$CONFIG_FILE" "$CONFIG_DIR/$FILENAME" mkdir -p "$RESOURCES_TOP_DIR/config-backups/" cp $CONFIG_DIR/$CONFIG_FILE "$RESOURCES_TOP_DIR/config-backups/$FILENAME" if [ -f "$UNINSTALLER" ]; then @@ -301,7 +301,9 @@ for ((i = 0; i < ${#SETUP_CMDS[@]}; i++)); do if [[ "$CMD" == *"raspi-config"* ]] || [[ "$CMD" == *"$CONFIG_DIR/$CONFIG_FILE"* ]] || [[ "$CMD" == *"\$CONFIG_DIR/\$CONFIG_FILE"* ]]; then do_config_backup fi - printf "\"%s\"\n" "$CMD" + if [[ ! "$CMD" == printf* ]]; then + printf "Running: \"%s\"\n" "$CMD" + fi eval "$CMD" check_for_error done From 683eb17a231bbc6efbfd7e3ce75227ce5e50464d Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 22 May 2024 13:38:59 +0100 Subject: [PATCH 5/8] Sync with boilerplate. * install.sh: drop quotes around apt packages. * Makefile: fail make tag if dev tools not installed. * tox.ini: update ruff invocation to avoid deprecation warning. * install.sh: drop symlink warning for /boot/config.txt. --- Makefile | 5 ++++- install.sh | 8 ++------ tox.ini | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 34f4a7d..56cf0df 100644 --- a/Makefile +++ b/Makefile @@ -22,6 +22,9 @@ endif @echo "deploy: build and upload to PyPi" @echo "tag: tag the repository with the current version\n" +version: + @hatch version + install: ./install.sh --unstable @@ -47,7 +50,7 @@ pytest: nopost: @bash check.sh --nopost -tag: +tag: version git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" build: check diff --git a/install.sh b/install.sh index bb671f2..3db90bc 100755 --- a/install.sh +++ b/install.sh @@ -58,11 +58,6 @@ find_config() { if [ ! -f "$CONFIG_DIR/$CONFIG_FILE" ]; then fatal "Could not find $CONFIG_FILE!" fi - else - if [ -f "/boot/$CONFIG_FILE" ] && [ ! -L "/boot/$CONFIG_FILE" ]; then - warning "Oops! It looks like /boot/$CONFIG_FILE is not a link to $CONFIG_DIR/$CONFIG_FILE" - warning "You might want to fix this!" - fi fi inform "Using $CONFIG_FILE in $CONFIG_DIR" } @@ -156,7 +151,8 @@ function apt_pkg_install { sudo apt update APT_HAS_UPDATED=true fi - sudo apt install -y "$PACKAGES" + # shellcheck disable=SC2086 + sudo apt install -y $PACKAGES check_for_error if [ -f "$UNINSTALLER" ]; then echo "apt uninstall -y $PACKAGES" >> "$UNINSTALLER" diff --git a/tox.ini b/tox.ini index 44c8654..4726cef 100644 --- a/tox.ini +++ b/tox.ini @@ -20,7 +20,7 @@ commands = python -m build --no-isolation python -m twine check dist/* isort --check . - ruff . + ruff check . codespell . deps = check-manifest From 89f9fc604fe70ad6d996f3bd3583820ab193899e Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 22 May 2024 13:52:23 +0100 Subject: [PATCH 6/8] QA: Python tidyup. --- ioexpander/__init__.py | 47 +++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/ioexpander/__init__.py b/ioexpander/__init__.py index 8429d6c..65741d3 100644 --- a/ioexpander/__init__.py +++ b/ioexpander/__init__.py @@ -4,7 +4,7 @@ from . import ioe_regs, sioe_regs -__version__ = '0.0.5' +__version__ = "0.0.5" # These values encode our desired pin function: IO, ADC, PWM @@ -186,7 +186,7 @@ def __init__( if not skip_chip_id_check: chip_id = self.get_chip_id() if chip_id != self._chip_id: - raise RuntimeError("Chip ID invalid: {:04x} expected: {:04x}.".format(chip_id, self._chip_id)) + raise RuntimeError(f"Chip ID invalid: {chip_id:04x} expected: {self._chip_id:04x}.") # Reset the chip if requested, to put it into a known state if perform_reset: @@ -253,7 +253,7 @@ def i2c_write16(self, reg_l, reg_h, value): def get_pin(self, pin): """Get a pin definition from its index.""" if pin < 1 or pin > len(self._pins): - raise ValueError("Pin should be in range 1-{}.".format(len(self._pins))) + raise ValueError(f"Pin should be in range 1-{len(self._pins)}.") return self._pins[pin - 1] @@ -262,7 +262,7 @@ def setup_switch_counter(self, pin, mode=IN_PU): io_pin = self.get_pin(pin) if io_pin.port not in (0, 1): - raise ValueError("Pin {} does not support switch counting.".format(pin)) + raise ValueError(f"Pin {pin} does not support switch counting.") if mode not in [IN, IN_PU]: raise ValueError("Pin mode should be one of IN or IN_PU") @@ -277,7 +277,7 @@ def read_switch_counter(self, pin): io_pin = self.get_pin(pin) if io_pin.port not in (0, 1): - raise ValueError("Pin {} does not support switch counting.".format(pin)) + raise ValueError(f"Pin {pin} does not support switch counting.") sw_reg = [self.REG_SWITCH_P00, self.REG_SWITCH_P10][io_pin.port] + io_pin.pin @@ -292,7 +292,7 @@ def clear_switch_counter(self, pin): io_pin = self.get_pin(pin) if io_pin.port not in (0, 1): - raise ValueError("Pin {} does not support switch counting.".format(pin)) + raise ValueError(f"Pin {pin} does not support switch counting.") sw_reg = [self.REG_SWITCH_P00, self.REG_SWITCH_P10][io_pin.port] + io_pin.pin @@ -307,15 +307,15 @@ def setup_rotary_encoder(self, channel, pin_a, pin_b, pin_c=None, count_microste enc_channel_a = self.get_pin(pin_a).enc_channel enc_channel_b = self.get_pin(pin_b).enc_channel if enc_channel_a is None: - raise ValueError("Pin {} does not support an encoder.".format(pin_a)) + raise ValueError(f"Pin {pin_a} does not support an encoder.") if enc_channel_b is None: - raise ValueError("Pin {} does not support an encoder.".format(pin_b)) + raise ValueError(f"Pin {pin_b} does not support an encoder.") self.set_mode(pin_a, PIN_MODE_PU, schmitt_trigger=True) self.set_mode(pin_b, PIN_MODE_PU, schmitt_trigger=True) if pin_c is not None: if pin_c < 1 or pin_c > len(self._pins): - raise ValueError("Pin C should be in range 1-{}, or None.".format(len(self._pins))) + raise ValueError(f"Pin C should be in range 1-{len(self._pins)}, or None.") self.set_mode(pin_c, PIN_MODE_OD) self.output(pin_c, 0) @@ -480,12 +480,12 @@ def reset(self): def get_pwm_module(self, pin): if pin < 1 or pin > len(self._pins): - raise ValueError("Pin should be in range 1-{}.".format(len(self._pins))) + raise ValueError(f"Pin should be in range 1-{len(self._pins)}.") io_pin = self._pins[pin - 1] if PIN_MODE_PWM not in io_pin.type: io_mode = (PIN_MODE_PWM >> 2) & 0b11 - raise ValueError("Pin {} does not support {}!".format(pin, MODE_NAMES[io_mode])) + raise ValueError(f"Pin {pin} does not support {MODE_NAMES[io_mode]}!") if isinstance(io_pin, DUAL_PWM_PIN) and io_pin.is_using_alt(): if io_pin.is_using_alt(): @@ -538,7 +538,7 @@ def set_pwm_control(self, divider, pwm_module=0): 128: 0b111, }[divider] except KeyError: - raise ValueError("A clock divider of {}".format(divider)) + raise ValueError(f"A clock divider of {divider}") # TODO: This currently sets GP, PWMTYP and FBINEN to 0 # It might be desirable to make these available to the user @@ -608,17 +608,12 @@ def set_mode(self, pin, mode, schmitt_trigger=False, invert=False): initial_state = mode >> 4 if io_mode != PIN_MODE_IO and mode not in io_pin.type: - raise ValueError("Pin {} does not support {}!".format(pin, MODE_NAMES[io_mode])) + raise ValueError("Pin {pin} does not support {MODE_NAMES[io_mode]}!") io_pin.mode = mode if self._debug: print( - "Setting pin {pin} to mode {mode} {name}, state: {state}".format( - pin=pin, - mode=MODE_NAMES[io_mode], - name=GPIO_NAMES[gpio_mode], - state=STATE_NAMES[initial_state], - ) + f"Setting pin {pin} to mode {MODE_NAMES[io_mode]} {GPIO_NAMES[gpio_mode]}, state: {STATE_NAMES[initial_state]}" ) if mode == PIN_MODE_PWM: @@ -681,7 +676,7 @@ def input(self, pin, adc_timeout=1): if io_pin.mode == PIN_MODE_ADC: if self._debug: - print("Reading ADC from pin {}".format(pin)) + print(f"Reading ADC from pin {pin}") if io_pin.adc_channel > 8: self.i2c_write8(self.REG_AINDIDS1, 1 << (io_pin.adc_channel - 8)) @@ -708,7 +703,7 @@ def input(self, pin, adc_timeout=1): return (reading / 4095.0) * self._vref else: if self._debug: - print("Reading IO from pin {}".format(pin)) + print(f"Reading IO from pin {pin}") pv = self.get_bit(self.get_pin_regs(io_pin).p, io_pin.pin) return HIGH if pv else LOW @@ -723,7 +718,7 @@ def output(self, pin, value, load=True, wait_for_load=True): if io_pin.mode == PIN_MODE_PWM: if self._debug: - print("Outputting PWM to pin: {pin}".format(pin=pin)) + print(f"Outputting PWM to pin: {pin}") if isinstance(io_pin, DUAL_PWM_PIN) and io_pin.is_using_alt(): alt_regs = self.get_alt_pwm_regs(io_pin) @@ -738,11 +733,11 @@ def output(self, pin, value, load=True, wait_for_load=True): else: if value == LOW: if self._debug: - print("Outputting LOW to pin: {pin} (or HIGH if inverted)".format(pin=pin)) + print(f"Outputting LOW to pin: {pin} (or HIGH if inverted)") self.change_bit(self.get_pin_regs(io_pin).p, io_pin.pin, io_pin.is_inverted()) elif value == HIGH: if self._debug: - print("Outputting HIGH to pin: {pin} (or LOW if inverted)".format(pin=pin)) + print(f"Outputting HIGH to pin: {pin} (or LOW if inverted)") self.change_bit(self.get_pin_regs(io_pin).p, io_pin.pin, not io_pin.is_inverted()) def get_pwm_regs(self, pin): @@ -774,7 +769,7 @@ def get_pin_regs(self, pin): def switch_pwm_to_alt(self, pin): if pin < 1 or pin > len(self._pins): - raise ValueError("Pin should be in range 1-{}.".format(len(self._pins))) + raise ValueError(f"Pin should be in range 1-{len(self._pins)}.") io_pin = self._pins[pin - 1] @@ -993,7 +988,7 @@ def set_watchdog_control(self, divider): 256: 0b111, # 1.638s }[divider] except KeyError: - raise ValueError("A clock divider of {}".format(divider)) + raise ValueError(f"A clock divider of {divider}") wdt = self.i2c_read8(self.REG_WDCON) wdt = wdt & 0b11111000 # Clear the WDPS bits From b87f55eff0932861914a523f9c9eba3f07065dd9 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 22 May 2024 13:53:04 +0100 Subject: [PATCH 7/8] Prep for v1.0.0. --- CHANGELOG.md | 7 +++++++ ioexpander/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2342075..7585692 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +1.0.0 +----- + +* Add dependency on smbus2 +* Add support for alternate i2c bus number +* Port to hatch/pyproject.toml + 0.0.5 ----- diff --git a/ioexpander/__init__.py b/ioexpander/__init__.py index 65741d3..95f494b 100644 --- a/ioexpander/__init__.py +++ b/ioexpander/__init__.py @@ -4,7 +4,7 @@ from . import ioe_regs, sioe_regs -__version__ = "0.0.5" +__version__ = "1.0.0" # These values encode our desired pin function: IO, ADC, PWM From 8d1ed4b4dbba1123ba05e7cc32dd7034839694c6 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Wed, 22 May 2024 14:20:28 +0100 Subject: [PATCH 8/8] install.sh: use module name for docs. --- install.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install.sh b/install.sh index 3db90bc..684b745 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,6 @@ #!/bin/bash LIBRARY_NAME=$(grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}') +MODULE_NAME="ioexpander" CONFIG_FILE=config.txt CONFIG_DIR="/boot/firmware" DATESTAMP=$(date "+%Y-%m-%d-%H-%M-%S") @@ -341,7 +342,7 @@ if confirm "Would you like to generate documentation?"; then inform "Installing pdoc. Please wait..." pip_pkg_install pdoc inform "Generating documentation.\n" - if $PYTHON -m pdoc "$LIBRARY_NAME" -o "$RESOURCES_DIR/docs" > /dev/null; then + if $PYTHON -m pdoc "$MODULE_NAME" -o "$RESOURCES_DIR/docs" > /dev/null; then inform "Documentation saved to $RESOURCES_DIR/docs" success "Done!" else