diff --git a/library/.coveragerc b/.coveragerc similarity index 100% rename from library/.coveragerc rename to .coveragerc diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..07620e3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,41 @@ +name: Build + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: Python ${{ matrix.python }} + runs-on: ubuntu-latest + strategy: + matrix: + python: ['3.9', '3.10', '3.11'] + + env: + RELEASE_FILE: ${{ github.event.repository.name }}-${{ github.event.release.tag_name || github.sha }}-py${{ matrix.python }} + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + + - name: Install Dependencies + run: | + make dev-deps + + - name: Build Packages + run: | + make build + + - name: Upload Packages + uses: actions/upload-artifact@v4 + with: + name: ${{ env.RELEASE_FILE }} + path: dist/ diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml new file mode 100644 index 0000000..ac672a5 --- /dev/null +++ b/.github/workflows/qa.yml @@ -0,0 +1,39 @@ +name: QA + +on: + pull_request: + push: + branches: + - main + +jobs: + test: + name: linting & spelling + runs-on: ubuntu-latest + env: + TERM: xterm-256color + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Set up Python '3,11' + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Install Dependencies + run: | + make dev-deps + + - name: Run Quality Assurance + run: | + make qa + + - 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 185c319..6f8cff7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,37 +1,41 @@ -name: Python Tests +name: Tests on: pull_request: push: branches: - - master + - main jobs: test: + name: Python ${{ matrix.python }} runs-on: ubuntu-latest strategy: matrix: - python: [2.7, 3.6, 3.7, 3.8] + python: ['3.9', '3.10', '3.11'] steps: - - uses: actions/checkout@v2 + - name: Checkout Code + uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} + - name: Install Dependencies run: | - python -m pip install --upgrade setuptools tox + make dev-deps + - name: Run Tests - working-directory: library run: | - tox -e py + make pytest + - name: Coverage + if: ${{ matrix.python == '3.9' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - working-directory: library run: | python -m pip install coveralls coveralls --service=github - if: ${{ matrix.python == '3.8' }} diff --git a/library/CHANGELOG.txt b/CHANGELOG.md similarity index 100% rename from library/CHANGELOG.txt rename to CHANGELOG.md diff --git a/library/MANIFEST.in b/MANIFEST.in similarity index 100% rename from library/MANIFEST.in rename to MANIFEST.in diff --git a/Makefile b/Makefile index 598237c..56cf0df 100644 --- a/Makefile +++ b/Makefile @@ -1,70 +1,66 @@ -LIBRARY_VERSION=$(shell cat library/setup.py | grep version | awk -F"'" '{print $$2}') -LIBRARY_NAME=$(shell cat library/setup.py | grep name | awk -F"'" '{print $$2}') +LIBRARY_NAME := $(shell hatch project metadata name 2> /dev/null) +LIBRARY_VERSION := $(shell hatch version 2> /dev/null) -.PHONY: usage install uninstall +.PHONY: usage install uninstall check pytest qa build-deps check tag wheel sdist clean dist testdeploy deploy usage: +ifdef LIBRARY_NAME @echo "Library: ${LIBRARY_NAME}" @echo "Version: ${LIBRARY_VERSION}\n" +else + @echo "WARNING: You should 'make dev-deps'\n" +endif @echo "Usage: make , where target is one of:\n" - @echo "install: install the library locally from source" - @echo "uninstall: uninstall the local library" - @echo "check: peform basic integrity checks on the codebase" - @echo "python-readme: generate library/README.rst from README.md" - @echo "python-wheels: build python .whl files for distribution" - @echo "python-sdist: build python source distribution" - @echo "python-clean: clean python build and dist directories" - @echo "python-dist: build all python distribution files" - @echo "python-testdeploy: build all and deploy to test PyPi" - @echo "tag: tag the repository with the current version" + @echo "install: install the library locally from source" + @echo "uninstall: uninstall the local library" + @echo "dev-deps: install Python dev dependencies" + @echo "check: perform basic integrity checks on the codebase" + @echo "qa: run linting and package QA" + @echo "pytest: run Python test fixtures" + @echo "clean: clean Python build and dist directories" + @echo "build: build Python distribution files" + @echo "testdeploy: build and upload to test PyPi" + @echo "deploy: build and upload to PyPi" + @echo "tag: tag the repository with the current version\n" + +version: + @hatch version install: - ./install.sh + ./install.sh --unstable uninstall: ./uninstall.sh -check: - @echo "Checking for trailing whitespace" - @! grep -IUrn --color "[[:blank:]]$$" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO - @echo "Checking for DOS line-endings" - @! grep -IUrn --color " " --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile - @echo "Checking library/CHANGELOG.txt" - @cat library/CHANGELOG.txt | grep ^${LIBRARY_VERSION} - @echo "Checking library/${LIBRARY_NAME}/__init__.py" - @cat library/${LIBRARY_NAME}/__init__.py | grep "^__version__ = '${LIBRARY_VERSION}'" - -tag: - git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" +dev-deps: + python3 -m pip install -r requirements-dev.txt + sudo apt install dos2unix shellcheck -python-readme: library/README.md +check: + @bash check.sh -python-license: library/LICENSE.txt +shellcheck: + shellcheck *.sh -library/README.md: README.md library/CHANGELOG.txt - cp README.md library/README.md - printf "\n# Changelog\n\n" >> library/README.md - cat library/CHANGELOG.txt >> library/README. +qa: + tox -e qa -library/LICENSE.txt: LICENSE - cp LICENSE library/LICENSE.txt +pytest: + tox -e py -python-wheels: python-readme python-license - cd library; python3 setup.py bdist_wheel - cd library; python setup.py bdist_wheel +nopost: + @bash check.sh --nopost -python-sdist: python-readme python-license - cd library; python setup.py sdist +tag: version + git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}" -python-clean: - -rm -r library/dist - -rm -r library/build - -rm -r library/*.egg-info +build: check + @hatch build -python-dist: python-clean python-wheels python-sdist - ls library/dist +clean: + -rm -r dist -python-testdeploy: python-dist - twine upload --repository-url https://test.pypi.org/legacy/ library/dist/* +testdeploy: build + twine upload --repository testpypi dist/* -python-deploy: check python-dist - twine upload library/dist/* +deploy: nopost build + twine upload dist/* diff --git a/README.md b/README.md index d2f845a..757d2d3 100644 --- a/README.md +++ b/README.md @@ -22,3 +22,6 @@ Latest/development library from GitHub: # Important! **This code should not be used for medical diagnosis, as the basis for a real smoke or fire detector, or in life-critical situations. It's for fun/novelty use only, so bear that in mind while using it.** + +# Changelog + diff --git a/check.sh b/check.sh new file mode 100755 index 0000000..38dfc3a --- /dev/null +++ b/check.sh @@ -0,0 +1,84 @@ +#!/bin/bash + +# 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))}') +TERM=${TERM:="xterm-256color"} + +success() { + echo -e "$(tput setaf 2)$1$(tput sgr0)" +} + +inform() { + echo -e "$(tput setaf 6)$1$(tput sgr0)" +} + +warning() { + echo -e "$(tput setaf 1)$1$(tput sgr0)" +} + +while [[ $# -gt 0 ]]; do + K="$1" + case $K in + -p|--nopost) + NOPOST=true + shift + ;; + *) + if [[ $1 == -* ]]; then + printf "Unrecognised option: %s\n" "$1"; + exit 1 + fi + POSITIONAL_ARGS+=("$1") + shift + esac +done + +inform "Checking $LIBRARY_NAME $LIBRARY_VERSION\n" + +inform "Checking for trailing whitespace..." +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 + success "No trailing whitespace found." +fi +printf "\n" + +inform "Checking for DOS line-endings..." +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 + success "No DOS line-endings found." +fi +printf "\n" + +inform "Checking CHANGELOG.md..." +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 + success "Changes found for version ${LIBRARY_VERSION}." +fi +printf "\n" + +inform "Checking for git tag ${LIBRARY_VERSION}..." +if ! git tag -l | grep -E "${LIBRARY_VERSION}$"; then + warning "Missing git tag for version ${LIBRARY_VERSION}" +fi +printf "\n" + +if [[ $NOPOST ]]; then + inform "Checking for .postN on library version..." + if [[ "$POST_VERSION" != "" ]]; then + warning "Found .$POST_VERSION on library version." + inform "Please only use these for testpypi releases." + exit 1 + else + success "OK" + fi +fi diff --git a/examples/detect-particles.py b/examples/detect-particles.py index 94503e4..9af005d 100755 --- a/examples/detect-particles.py +++ b/examples/detect-particles.py @@ -4,8 +4,9 @@ # smoke or fire detector, or in life-critical situations. It's # for fun/novelty use only, so bear that in mind while using it. -import time import datetime +import time + from max30105 import MAX30105, HeartRate max30105 = MAX30105() @@ -16,10 +17,10 @@ max30105.set_led_pulse_amplitude(2, 0.0) max30105.set_led_pulse_amplitude(3, 12.5) -max30105.set_slot_mode(1, 'red') -max30105.set_slot_mode(2, 'ir') -max30105.set_slot_mode(3, 'green') -max30105.set_slot_mode(4, 'off') +max30105.set_slot_mode(1, "red") +max30105.set_slot_mode(2, "ir") +max30105.set_slot_mode(3, "green") +max30105.set_slot_mode(4, "off") hr = HeartRate(max30105) @@ -50,7 +51,7 @@ for fun/novelty use only, so bear that in mind while using it. This example uses the green LED to detect the amount of green -light reflected back to the sensor. An increase in relected +light reflected back to the sensor. An increase in reflected light should correlate to an increase in particles in front of the sensor. @@ -63,7 +64,7 @@ delay = 10 -print("Starting readings in {} seconds...\n".format(delay)) +print(f"Starting readings in {delay} seconds...\n") time.sleep(delay) try: @@ -89,15 +90,15 @@ detected = True else: detected = False - print("Value: {:.2f} // Mean: {:.2f} // Delta: {:.2f} // \ -Change detected: {}".format(d, mean, delta, detected)) - f.write("{:.2f},".format(d)) - f.write("{:.2f},".format(mean)) - f.write("{:.2f},".format(delta)) - f.write("{},".format(detected)) + print(f"Value: {d:.2f} // Mean: {mean:.2f} // Delta: {delta:.2f} // \ +Change detected: {detected}") + f.write(f"{d:.2f},") + f.write(f"{mean:.2f},") + f.write(f"{delta:.2f},") + f.write(f"{detected},") time.sleep(0.05) temp = max30105.get_temperature() - f.write("{:.2f}\n".format(temp)) + f.write(f"{temp:.2f}\n") time.sleep(0.05) except KeyboardInterrupt: diff --git a/examples/get-temperature.py b/examples/get-temperature.py index 802eb73..44fd3af 100755 --- a/examples/get-temperature.py +++ b/examples/get-temperature.py @@ -5,7 +5,7 @@ # for fun/novelty use only, so bear that in mind while using it. import time -import datetime + from max30105 import MAX30105 max30105 = MAX30105() @@ -13,13 +13,13 @@ delay = 10 -print("Starting readings in {} seconds...\n".format(delay)) +print(f"Starting readings in {delay} seconds...\n") time.sleep(delay) try: while True: temp = max30105.get_temperature() - print("{:.2f}\n".format(temp)) + print(f"{temp:.2f}\n") time.sleep(1.0) except KeyboardInterrupt: diff --git a/examples/graph-heartbeat.py b/examples/graph-heartbeat.py index 931381b..6f83e75 100755 --- a/examples/graph-heartbeat.py +++ b/examples/graph-heartbeat.py @@ -4,6 +4,7 @@ # for fun/novelty use only, so bear that in mind while using it. import time + from max30105 import MAX30105, HeartRate max30105 = MAX30105() @@ -13,10 +14,10 @@ max30105.set_led_pulse_amplitude(2, 12.5) max30105.set_led_pulse_amplitude(3, 0) -max30105.set_slot_mode(1, 'red') -max30105.set_slot_mode(2, 'ir') -max30105.set_slot_mode(3, 'off') -max30105.set_slot_mode(4, 'off') +max30105.set_slot_mode(1, "red") +max30105.set_slot_mode(2, "ir") +max30105.set_slot_mode(3, "off") +max30105.set_slot_mode(4, "off") hr = HeartRate(max30105) @@ -42,7 +43,7 @@ delay = 10 -print("Starting readings in {} seconds...\n".format(delay)) +print(f"Starting readings in {delay} seconds...\n") time.sleep(delay) try: diff --git a/examples/read-heartbeat.py b/examples/read-heartbeat.py index 71ac8a1..2733e10 100755 --- a/examples/read-heartbeat.py +++ b/examples/read-heartbeat.py @@ -4,6 +4,7 @@ # for fun/novelty use only, so bear that in mind while using it. import time + from max30105 import MAX30105, HeartRate max30105 = MAX30105() @@ -13,15 +14,15 @@ max30105.set_led_pulse_amplitude(2, 12.5) max30105.set_led_pulse_amplitude(3, 0) -max30105.set_slot_mode(1, 'red') -max30105.set_slot_mode(2, 'ir') -max30105.set_slot_mode(3, 'off') -max30105.set_slot_mode(4, 'off') +max30105.set_slot_mode(1, "red") +max30105.set_slot_mode(2, "ir") +max30105.set_slot_mode(3, "off") +max30105.set_slot_mode(4, "off") def display_heartrate(beat, bpm, avg_bpm): - print("{} BPM: {:.2f} AVG: {:.2f}".format("<3" if beat else " ", - bpm, avg_bpm)) + beat = "<3" if beat else " " + print(f"{beat} BPM: {bpm:.2f} AVG: {avg_bpm:.2f}") hr = HeartRate(max30105) @@ -48,7 +49,7 @@ def display_heartrate(beat, bpm, avg_bpm): delay = 10 -print("Starting readings in {} seconds...\n".format(delay)) +print(f"Starting readings in {delay} seconds...\n") time.sleep(delay) try: diff --git a/examples/test.py b/examples/test.py index 27ef5a6..0d5fd88 100644 --- a/examples/test.py +++ b/examples/test.py @@ -1,6 +1,7 @@ -import smbus2 import time +import smbus2 + bus = smbus2.SMBus(1) while True: @@ -10,4 +11,3 @@ print("Waiting...") time.sleep(0.5) print("read") - diff --git a/install.sh b/install.sh index d8ddcc4..3db90bc 100755 --- a/install.sh +++ b/install.sh @@ -1,22 +1,370 @@ #!/bin/bash +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") +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" +USAGE="./install.sh (--unstable)" +POSITIONAL_ARGS=() +FORCE=false +UNSTABLE=false +PYTHON="python" +CMD_ERRORS=false -printf "MAX301015 Python Library: Installer\n\n" -if [ $(id -u) -ne 0 ]; then - printf "Script must be run as root. Try 'sudo ./install.sh'\n" +user_check() { + if [ "$(id -u)" -eq 0 ]; then + fatal "Script should not be run as root. Try './install.sh'\n" + fi +} + +confirm() { + if $FORCE; then + true + else + read -r -p "$1 [y/N] " response < /dev/tty + if [[ $response =~ ^(yes|y|Y)$ ]]; then + true + else + false + fi + fi +} + +success() { + echo -e "$(tput setaf 2)$1$(tput sgr0)" +} + +inform() { + echo -e "$(tput setaf 6)$1$(tput sgr0)" +} + +warning() { + 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_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_DIR/$CONFIG_FILE" >> "$UNINSTALLER" + fi + fi +} + +function apt_pkg_install { + 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 %s\n" "$PACKAGE" + dpkg -L "$PACKAGE" > /dev/null 2>&1 + if [ "$?" == "1" ]; then + PACKAGES_NEEDED+=("$PACKAGE") + fi + done + PACKAGES="${PACKAGES_NEEDED[*]}" + if ! [ "$PACKAGES" == "" ]; then + 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" + 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 + K="$1" + case $K in + -u|--unstable) + UNSTABLE=true + shift + ;; + -f|--force) + FORCE=true + shift + ;; + -p|--python) + PYTHON=$2 + shift + shift + ;; + *) + if [[ $1 == -* ]]; then + printf "Unrecognised option: %s\n" "$1"; + printf "Usage: %s\n" "$USAGE"; + exit 1 + fi + POSITIONAL_ARGS+=("$1") + shift + esac +done + +printf "Installing %s...\n\n" "$LIBRARY_NAME" + +user_check +venv_check + +if [ ! -f "$(which "$PYTHON")" ]; then + fatal "Python path %s not found!\n" "$PYTHON" +fi + +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" +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 + +printf "\n" + +inform "Installing for $PYTHON_VER...\n" -printf "Done!\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 + +# 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" +fi + +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 + 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_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" + 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 + inform "Generating documentation.\n" + if $PYTHON -m pdoc "$LIBRARY_NAME" -o "$RESOURCES_DIR/docs" > /dev/null; then + inform "Documentation saved to $RESOURCES_DIR/docs" + success "Done!" + else + warning "Error: Failed to generate documentation." + fi +fi + +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/library/LICENSE.txt b/library/LICENSE.txt deleted file mode 100644 index a4876a2..0000000 --- a/library/LICENSE.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Pimoroni Ltd. - -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. diff --git a/library/README.md b/library/README.md deleted file mode 100644 index 757d2d3..0000000 --- a/library/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# MAX30105 - Heart Rate, Oximeter, Smoke Sensor - -[![Build Status](https://travis-ci.com/pimoroni/max30105-python.svg?branch=master)](https://travis-ci.com/pimoroni/max30105-python) -[![Coverage Status](https://coveralls.io/repos/github/pimoroni/max30105-python/badge.svg?branch=master)](https://coveralls.io/github/pimoroni/max30105-python?branch=master) -[![PyPi Package](https://img.shields.io/pypi/v/max30105.svg)](https://pypi.python.org/pypi/max30105) -[![Python Versions](https://img.shields.io/pypi/pyversions/max30105.svg)](https://pypi.python.org/pypi/max30105) - -The MAX30105 is an precision optical sensor that can be used to measure heart rate, pulse oximetry (SPO2 / blood oxygen saturation), and smoke (and other particles). - -# Installing - -Stable library from PyPi: - -* Just run `sudo pip install max30105` - -Latest/development library from GitHub: - -* `git clone https://github.com/pimoroni/max30105-python` -* `cd max30105-python` -* `sudo ./install.sh` - -# Important! - -**This code should not be used for medical diagnosis, as the basis for a real smoke or fire detector, or in life-critical situations. It's for fun/novelty use only, so bear that in mind while using it.** - -# Changelog - diff --git a/library/setup.cfg b/library/setup.cfg deleted file mode 100644 index 5c3c3ea..0000000 --- a/library/setup.cfg +++ /dev/null @@ -1,11 +0,0 @@ -[flake8] -exclude = - test.py - .tox, - .eggs, - .git, - __pycache__, - build, - dist -ignore = - E501 diff --git a/library/setup.py b/library/setup.py deleted file mode 100755 index f9eac82..0000000 --- a/library/setup.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python - -""" -Copyright (c) 2019 Pimoroni - -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. -""" - -try: - from setuptools import setup -except ImportError: - from distutils.core import setup - -classifiers = ['Development Status :: 4 - Beta', - 'Operating System :: POSIX :: Linux', - 'License :: OSI Approved :: MIT License', - 'Intended Audience :: Developers', - 'Programming Language :: Python :: 2.6', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Topic :: Software Development', - 'Topic :: System :: Hardware'] - -setup( - name='max30105', - version='0.0.5', - author='Philip Howard', - author_email='phil@pimoroni.com', - description="""Python library for the MAX30105 Smoke/Pulse Detector""", - long_description=open('README.md').read() + '\n' + open('CHANGELOG.txt').read(), - long_description_content_type="text/markdown", - license='MIT', - keywords='Raspberry Pi', - url='http://www.pimoroni.com', - classifiers=classifiers, - packages=['max30105'], - install_requires=['i2cdevice>=0.0.7'] -) diff --git a/library/tox.ini b/library/tox.ini deleted file mode 100644 index 9897973..0000000 --- a/library/tox.ini +++ /dev/null @@ -1,24 +0,0 @@ -[tox] -envlist = py{27,35,37,38},qa -skip_missing_interpreters = True - -[testenv] -commands = - python setup.py install - coverage run -m py.test -v -r wsx - coverage report -deps = - mock - pytest>=3.1 - pytest-cov - -[testenv:qa] -commands = - check-manifest --ignore test.py,tox.ini,tests/*,.coveragerc - python setup.py sdist bdist_wheel - twine check dist/* - flake8 --ignore E501 -deps = - check-manifest - flake8 - twine diff --git a/library/max30105/__init__.py b/max30105/__init__.py similarity index 65% rename from library/max30105/__init__.py rename to max30105/__init__.py index fda3d92..e10787b 100644 --- a/library/max30105/__init__.py +++ b/max30105/__init__.py @@ -1,11 +1,11 @@ """MAX30105 Driver.""" -from i2cdevice import Device, Register, BitField, _int_to_bytes -from i2cdevice.adapter import LookupAdapter, Adapter import struct import time +from i2cdevice import BitField, Device, Register, _int_to_bytes +from i2cdevice.adapter import Adapter, LookupAdapter -__version__ = '0.0.5' +__version__ = "0.0.5" CHIP_ID = 0x15 I2C_ADDRESS = 0x57 @@ -17,27 +17,27 @@ def bit(n): class LEDModeAdapter(Adapter): LOOKUP = [ - 'off', - 'red', - 'ir', - 'green', - 'off', - 'pilot_red', - 'pilot_ir', - 'pilot_green' + "off", + "red", + "ir", + "green", + "off", + "pilot_red", + "pilot_ir", + "pilot_green" ] def _decode(self, value): try: - return self.LOOPUP[value] + return self.LOOKUP[value] except IndexError: - return 'off' + return "off" def _encode(self, value): try: return self.LOOKUP.index(value) except ValueError: - raise ValueError('Invalid slot mode {}'.format(value)) + raise ValueError("Invalid slot mode {}".format(value)) class PulseAmplitudeAdapter(Adapter): @@ -54,7 +54,7 @@ class TemperatureAdapter(Adapter): """Convert fractional and integer temp readings to degrees C.""" def _decode(self, value): - integer, fractional = struct.unpack('= 2 else 'off', - slot3='green' if leds_enable >= 3 else 'off') + self._max30105.set("LED_MODE_CONTROL", + slot1="red", + slot2="ir" if leds_enable >= 2 else "off", + slot3="green" if leds_enable >= 3 else "off") self.clear_fifo() def soft_reset(self, timeout=5.0): """Reset device.""" - self._max30105.set('MODE_CONFIG', reset=True) + self._max30105.set("MODE_CONFIG", reset=True) t_start = time.time() - while self._max30105.get('MODE_CONFIG').reset and time.time() - t_start < timeout: + while self._max30105.get("MODE_CONFIG").reset and time.time() - t_start < timeout: time.sleep(0.001) - if self._max30105.get('MODE_CONFIG').reset: + if self._max30105.get("MODE_CONFIG").reset: raise RuntimeError("Timeout: Failed to soft reset MAX30105.") def clear_fifo(self): """Clear samples FIFO.""" - self._max30105.set('FIFO_READ', pointer=0) - self._max30105.set('FIFO_WRITE', pointer=0) - self._max30105.set('FIFO_OVERFLOW', counter=0) + self._max30105.set("FIFO_READ", pointer=0) + self._max30105.set("FIFO_WRITE", pointer=0) + self._max30105.set("FIFO_OVERFLOW", counter=0) def get_samples(self): """Return contents of sample FIFO.""" - ptr_r = self._max30105.get('FIFO_READ').pointer - ptr_w = self._max30105.get('FIFO_WRITE').pointer + ptr_r = self._max30105.get("FIFO_READ").pointer + ptr_w = self._max30105.get("FIFO_WRITE").pointer if ptr_r == ptr_w: return None @@ -396,7 +396,7 @@ def get_chip_id(self): """Return the revision and part IDs.""" self.setup() - part_id = self._max30105.get('PART_ID') + part_id = self._max30105.get("PART_ID") return part_id.revision, part_id.part @@ -404,16 +404,16 @@ def get_temperature(self, timeout=5.0): """Return the die temperature.""" self.setup() - self._max30105.set('INT_ENABLE_2', die_temp_ready_en=True) - self._max30105.set('DIE_TEMP_CONFIG', temp_en=True) + self._max30105.set("INT_ENABLE_2", die_temp_ready_en=True) + self._max30105.set("DIE_TEMP_CONFIG", temp_en=True) t_start = time.time() - while not self._max30105.get('INT_STATUS_2').die_temp_ready: + while not self._max30105.get("INT_STATUS_2").die_temp_ready: time.sleep(0.01) if time.time() - t_start > timeout: - raise RuntimeError('Timeout: Waiting for INT_STATUS_2, die_temp_ready.') + raise RuntimeError("Timeout: Waiting for INT_STATUS_2, die_temp_ready.") - return self._max30105.get('DIE_TEMP').temperature + return self._max30105.get("DIE_TEMP").temperature def set_mode(self, mode): """Set the sensor mode. @@ -421,7 +421,7 @@ def set_mode(self, mode): :param mode: Mode, either red_only, red_ir or green_red_ir """ - self._max30105.set('MODE_CONFIG', mode=mode) + self._max30105.set("MODE_CONFIG", mode=mode) def set_slot_mode(self, slot, mode): """Set the mode of a single slot. @@ -431,13 +431,13 @@ def set_slot_mode(self, slot, mode): """ if slot == 1: - self._max30105.set('LED_MODE_CONTROL', slot1=mode) + self._max30105.set("LED_MODE_CONTROL", slot1=mode) elif slot == 2: - self._max30105.set('LED_MODE_CONTROL', slot2=mode) + self._max30105.set("LED_MODE_CONTROL", slot2=mode) elif slot == 3: - self._max30105.set('LED_MODE_CONTROL', slot3=mode) + self._max30105.set("LED_MODE_CONTROL", slot3=mode) elif slot == 4: - self._max30105.set('LED_MODE_CONTROL', slot4=mode) + self._max30105.set("LED_MODE_CONTROL", slot4=mode) else: raise ValueError("Invalid LED slot: {}".format(slot)) @@ -449,11 +449,11 @@ def set_led_pulse_amplitude(self, led, amplitude): """ if led == 1: - self._max30105.set('LED_PULSE_AMPLITUDE', led1_mA=amplitude) + self._max30105.set("LED_PULSE_AMPLITUDE", led1_mA=amplitude) elif led == 2: - self._max30105.set('LED_PULSE_AMPLITUDE', led2_mA=amplitude) + self._max30105.set("LED_PULSE_AMPLITUDE", led2_mA=amplitude) elif led == 3: - self._max30105.set('LED_PULSE_AMPLITUDE', led3_mA=amplitude) + self._max30105.set("LED_PULSE_AMPLITUDE", led3_mA=amplitude) else: raise ValueError("Invalid LED: {}".format(led)) @@ -463,23 +463,23 @@ def set_fifo_almost_full_count(self, count): :param count: Count of remaining samples, from 0 to 15 """ - self._max30105.set('FIFO_CONFIG', fifo_almost_full=count) + self._max30105.set("FIFO_CONFIG", fifo_almost_full=count) def set_fifo_almost_full_enable(self, value): """Enable the FIFO-almost-full flag.""" - self._max30105.set('INT_ENABLE_1', a_full_en=value) + self._max30105.set("INT_ENABLE_1", a_full_en=value) def set_data_ready_enable(self, value): """Enable the data-ready flag.""" - self._max30105.set('INT_ENABLE_1', data_ready_en=value) + self._max30105.set("INT_ENABLE_1", data_ready_en=value) def set_ambient_light_compensation_overflow_enable(self, value): """Enable the ambient light compensation overflow flag.""" - self._max30105.set('INT_ENABLE_1', alc_overflow_en=value) + self._max30105.set("INT_ENABLE_1", alc_overflow_en=value) def set_proximity_enable(self, value): """Enable the proximity interrupt flag.""" - self._max30105.set('INT_ENABLE_1', prox_int_en=value) + self._max30105.set("INT_ENABLE_1", prox_int_en=value) def set_proximity_threshold(self, value): """Set the threshold of the proximity sensor. @@ -489,7 +489,7 @@ def set_proximity_threshold(self, value): :param value: threshold value from 0 to 255 """ - self._max30105.set('PROX_INT_THRESHOLD', threshold=value) + self._max30105.set("PROX_INT_THRESHOLD", threshold=value) def get_fifo_almost_full_status(self): """Get the FIFO-almost-full flag. @@ -499,17 +499,17 @@ def get_fifo_almost_full_status(self): The flag is cleared upon read. """ - return self._max30105.get('INT_STATUS_1').a_full + return self._max30105.get("INT_STATUS_1").a_full def get_data_ready_status(self): """Get the data-ready flag. - In particle-sensing mode this interrupt triggeres when a new sample has been placed into the FIFO. + In particle-sensing mode this interrupt triggers when a new sample has been placed into the FIFO. This flag is cleared upon read, or upon `get_samples()` """ - return self._max30105.get('INT_STATUS_1').data_ready + return self._max30105.get("INT_STATUS_1").data_ready def get_ambient_light_compensation_overflow_status(self): """Get the ambient light compensation overflow status flag. @@ -519,7 +519,7 @@ def get_ambient_light_compensation_overflow_status(self): This flag is cleared upon read. """ - return self._max30105.get('INT_STATUS_1').alc_overflow + return self._max30105.get("INT_STATUS_1").alc_overflow def get_proximity_triggered_threshold_status(self): """Get the proximity triggered threshold status flag. @@ -529,7 +529,7 @@ def get_proximity_triggered_threshold_status(self): This flag is cleared upon read. """ - return self._max30105.get('INT_STATUS_1').prox_int + return self._max30105.get("INT_STATUS_1").prox_int def get_power_ready_status(self): """Get the power ready status flag. @@ -537,7 +537,7 @@ def get_power_ready_status(self): Returns True if the sensor has successfully powered up and is ready to collect data. """ - return self._max30105.get('INT_STATUS_1').pwr_ready + return self._max30105.get("INT_STATUS_1").pwr_ready def get_die_temp_ready_status(self): """Get the die temperature ready flag. @@ -547,4 +547,4 @@ def get_die_temp_ready_status(self): This flag is cleared upon read, or upon `get_temperature`. """ - return self._max30105.get('INT_STATUS_2').die_temp_ready + return self._max30105.get("INT_STATUS_2").die_temp_ready diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..061d7da --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,115 @@ +[build-system] +requires = ["hatchling", "hatch-fancy-pypi-readme"] +build-backend = "hatchling.build" + +[project] +name = "max30105" +dynamic = ["version", "readme"] +description = "Python library for the MAX30105 Smoke/Pulse Detector" +license = {file = "LICENSE"} +requires-python = ">= 3.7" +authors = [ + { name = "Philip Howard", email = "phil@pimoroni.com" }, +] +maintainers = [ + { name = "Philip Howard", email = "phil@pimoroni.com" }, +] +keywords = [ + "Pi", + "Raspberry", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Topic :: System :: Hardware", +] +dependencies = [ + "i2cdevice>=1.0.0" +] + +[project.urls] +GitHub = "https://www.github.com/pimoroni/max30105-python" +Homepage = "https://www.pimoroni.com" + +[tool.hatch.version] +path = "max30105/__init__.py" + +[tool.hatch.build] +include = [ + "max30105", + "README.md", + "CHANGELOG.md", + "LICENSE" +] + +[tool.hatch.build.targets.sdist] +include = [ + "*" +] +exclude = [ + ".*", + "dist" +] + +[tool.hatch.metadata.hooks.fancy-pypi-readme] +content-type = "text/markdown" +fragments = [ + { path = "README.md" }, + { text = "\n" }, + { path = "CHANGELOG.md" } +] + +[tool.ruff] +exclude = [ + '.tox', + '.egg', + '.git', + '__pycache__', + 'build', + 'dist' +] +line-length = 200 + +[tool.codespell] +skip = """ +./.tox,\ +./.egg,\ +./.git,\ +./__pycache__,\ +./build,\ +./dist.\ +""" + +[tool.isort] +line_length = 200 + +[tool.check-manifest] +ignore = [ + '.stickler.yml', + 'boilerplate.md', + 'check.sh', + 'install.sh', + 'uninstall.sh', + 'Makefile', + 'tox.ini', + 'tests/*', + 'examples/*', + '.coveragerc', + 'requirements-dev.txt' +] + +[tool.pimoroni] +apt_packages = [] +configtxt = [] +commands = [] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..525b042 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,9 @@ +check-manifest +ruff +codespell +isort +twine +hatch +hatch-fancy-pypi-readme +tox +pdoc diff --git a/library/test.py b/test.py similarity index 73% rename from library/test.py rename to test.py index 4e598e8..0aaea38 100755 --- a/library/test.py +++ b/test.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import time + from max30105 import MAX30105, HeartRate max30105 = MAX30105() @@ -10,23 +11,24 @@ max30105.set_led_pulse_amplitude(2, 0) max30105.set_led_pulse_amplitude(3, 0) -max30105.set_slot_mode(1, 'red') -max30105.set_slot_mode(2, 'ir') -max30105.set_slot_mode(3, 'green') -max30105.set_slot_mode(4, 'off') +max30105.set_slot_mode(1, "red") +max30105.set_slot_mode(2, "ir") +max30105.set_slot_mode(3, "green") +max30105.set_slot_mode(4, "off") colours = {"red": 1, "ir": 2, "green": 3} hr = HeartRate(max30105) try: - print("Temperature: {:.2f}C".format(max30105.get_temperature())) + temperature = max30105.get_temperature() + print(f"Temperature: {temperature:.2f}C") for c in colours: - print("\nLighting {} LED".format(c.upper())) + print(f"\nLighting {c.upper()} LED") max30105.set_led_pulse_amplitude(colours[c], 12.5) time.sleep(0.5) - print("Reading {} LED".format(c.upper())) + print(f"Reading {c.upper()} LED") i = 0 while i < 10: diff --git a/library/tests/test_features.py b/tests/test_features.py similarity index 100% rename from library/tests/test_features.py rename to tests/test_features.py index f8f0d37..4e2b44d 100644 --- a/library/tests/test_features.py +++ b/tests/test_features.py @@ -1,5 +1,5 @@ -from i2cdevice import MockSMBus import pytest +from i2cdevice import MockSMBus class MockSMBusNoTimeout(MockSMBus): diff --git a/library/tests/test_setup.py b/tests/test_setup.py similarity index 100% rename from library/tests/test_setup.py rename to tests/test_setup.py index bc5e7d0..452b951 100644 --- a/library/tests/test_setup.py +++ b/tests/test_setup.py @@ -1,5 +1,5 @@ -from i2cdevice import MockSMBus import pytest +from i2cdevice import MockSMBus class MockSMBusNoTimeout(MockSMBus): diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..4726cef --- /dev/null +++ b/tox.ini @@ -0,0 +1,34 @@ +[tox] +envlist = py,qa +skip_missing_interpreters = True +isolated_build = true +minversion = 4.0.0 + +[testenv] +commands = + coverage run -m pytest -v -r wsx + coverage report +deps = + mock + pytest>=3.1 + pytest-cov + build + +[testenv:qa] +commands = + check-manifest + python -m build --no-isolation + python -m twine check dist/* + isort --check . + ruff check . + codespell . +deps = + check-manifest + ruff + codespell + isort + twine + build + hatch + hatch-fancy-pypi-readme + diff --git a/uninstall.sh b/uninstall.sh index 3ce06cc..3314b7f 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -1,24 +1,72 @@ #!/bin/bash -PACKAGE="max30105" +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="python" -printf "MAX30105 Python Library: Uninstaller\n\n" -if [ $(id -u) -ne 0 ]; then - printf "Script must be run as root. Try 'sudo ./uninstall.sh'\n" - exit 1 -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" + exit 1 + fi +} -cd library +user_check() { + if [ "$(id -u)" -eq 0 ]; then + printf "Script should not be run as root. Try './uninstall.sh'\n" + exit 1 + fi +} -printf "Unnstalling for Python 2..\n" -pip uninstall $PACKAGE +confirm() { + if $FORCE; then + true + else + read -r -p "$1 [y/N] " response < /dev/tty + if [[ $response =~ ^(yes|y|Y)$ ]]; then + true + else + false + fi + fi +} -if [ -f "/usr/bin/pip3" ]; then - printf "Uninstalling for Python 3..\n" - pip3 uninstall $PACKAGE -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)" +} + +inform() { + echo -e "$(tput setaf 6)$1$(tput sgr0)" +} + +warning() { + echo -e "$(tput setaf 1)$1$(tput sgr0)" +} -cd .. +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" + +if [ -d "$RESOURCES_DIR" ]; then + if confirm "Would you like to delete $RESOURCES_DIR?"; then + rm -r "$RESOURCES_DIR" + fi +fi printf "Done!\n"