diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ddee38e..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 }} @@ -32,10 +32,10 @@ jobs: - name: Build Packages run: | - make dist + 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/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/Makefile b/Makefile index 760b4b8..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 @@ -30,11 +33,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 @@ -44,7 +50,7 @@ pytest: nopost: @bash check.sh --nopost -tag: +tag: version git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" build: check @@ -54,7 +60,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..38dfc3a 100755 --- a/check.sh +++ b/check.sh @@ -3,9 +3,10 @@ # 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() { echo -e "$(tput setaf 2)$1$(tput sgr0)" @@ -28,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") @@ -39,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 @@ -49,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 @@ -59,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 @@ -69,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 fafac26..684b745 100755 --- a/install.sh +++ b/install.sh @@ -1,22 +1,25 @@ #!/bin/bash -LIBRARY_NAME=`grep -m 1 name pyproject.toml | awk -F" = " '{print substr($2,2,length($2)-2)}'` -CONFIG=/boot/config.txt -DATESTAMP=`date "+%Y-%m-%d-%H-%M-%S"` +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") CONFIG_BACKUP=false APT_HAS_UPDATED=false -RESOURCES_TOP_DIR=$HOME/Pimoroni -WD=`pwd` +RESOURCES_TOP_DIR="$HOME/Pimoroni" +VENV_BASH_SNIPPET="$RESOURCES_TOP_DIR/auto_venv.sh" +VENV_DIR="$HOME/.virtualenvs/pimoroni" 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 + if [ "$(id -u)" -eq 0 ]; then + fatal "Script should not be run as root. Try './install.sh'\n" fi } @@ -33,15 +36,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)" } @@ -51,51 +45,126 @@ 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 + 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 + # shellcheck disable=SC1091 + source "$VENV_DIR/bin/activate" + else + inform "Activating existing virtual Python environment in $VENV_DIR\n" + printf "source \"%s/bin/activate\"\n" "$VENV_DIR" + # shellcheck disable=SC1091 + 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 "^^^ 😬 previous command did not exit cleanly!" + 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 - mkdir -p $RESOURCES_TOP_DIR/config-backups/ - cp $CONFIG $RESOURCES_TOP_DIR/config-backups/$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_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 } 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 - 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 + # shellcheck disable=SC2086 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 } 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 @@ -116,8 +185,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") @@ -125,119 +194,155 @@ while [[ $# -gt 0 ]]; do esac done +printf "Installing %s...\n\n" "$LIBRARY_NAME" + 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 %s not found!\n" "$PYTHON" fi -PYTHON_VER=`$PYTHON --version` - -printf "$LIBRARY_NAME Python Library: Installer\n\n" +PYTHON_VER=$($PYTHON --version) inform "Checking Dependencies. Please wait..." +# Install toml and try to read pyproject.toml into bash variables + pip_pkg_install toml -CONFIG_VARS=`$PYTHON - < $UNINSTALLER +# Create a stub uninstaller file, we'll try to add the inverse of every +# install command run to here, though it's not complete. +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" 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 - pip_pkg_install $LIBRARY_NAME + inform "Installing stable library from pypi.\n" + 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 /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 + if [[ ! "$CMD" == printf* ]]; then + printf "Running: \"%s\"\n" "$CMD" + 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 "%s \n" "$CONFIG_LINE" | 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" - 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 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 - if [ $? -eq 0 ]; then + inform "Generating documentation.\n" + if $PYTHON -m pdoc "$MODULE_NAME" -o "$RESOURCES_DIR/docs" > /dev/null; then inform "Documentation saved to $RESOURCES_DIR/docs" success "Done!" else @@ -245,6 +350,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 %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 %s\n\n" "$UNINSTALLER" + +if [ "$CMD_ERRORS" = true ]; then + exit 1 +else + exit 0 +fi diff --git a/ioexpander/__init__.py b/ioexpander/__init__.py index fb49472..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 @@ -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: @@ -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) @@ -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 @@ -285,14 +285,14 @@ 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.""" 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)) @@ -689,7 +684,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 @@ -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 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/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/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 diff --git a/tox.ini b/tox.ini index 090fdb0..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 --format=github . + ruff check . codespell . deps = check-manifest diff --git a/uninstall.sh b/uninstall.sh index d5e1b5f..3314b7f 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,13 +1,22 @@ #!/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="/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" + if [ "$(id -u)" -eq 0 ]; then + printf "Script should not be run as root. Try './uninstall.sh'\n" exit 1 fi } @@ -46,16 +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