diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 5fcb545..0000000 --- a/.flake8 +++ /dev/null @@ -1,22 +0,0 @@ -[flake8] -max-line-length = 80 -#max-complexity = 10 -doctests = True -# W503: Line break occurred before a binary operator -# D100 - D103 documentation info, should be removed after fixes -ignore = - W503, - D100, - D101, - D102, - D103 - -noqa-require-code = True - -exclude = - .venv - .git, - __pycache__ - -per-file-ignores = - __init__.py:F401 diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a93df3e..b8a2fa4 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -2,6 +2,7 @@ name: CI env: venv-path: '.venv' + DEFAULT_PYTHON: "3.12" on: push: @@ -13,43 +14,30 @@ on: workflow_dispatch: jobs: - lint: + ruff: + name: Ruff runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: Install Poetry - uses: snok/install-poetry@v1.3.4 - with: - virtualenvs-create: true - virtualenvs-in-project: true - - - name: Load cached venv - id: cached-poetry-dependencies - uses: actions/cache@v4 + - name: โคต๏ธ Check out code from GitHub + uses: actions/checkout@v4.1.1 + - name: ๐Ÿ— Set up Poetry + run: pipx install poetry + - name: ๐Ÿ— Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v5.0.0 with: - path: .venv - key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} - - - name: Install dependencies - if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' - run: poetry install --no-interaction --no-root - - - name: Install library - run: poetry install --no-interaction - - - name: Run linters + python-version: ${{ env.DEFAULT_PYTHON }} + cache: "poetry" + - name: ๐Ÿ— Install workflow dependencies run: | - source .venv/bin/activate - black --config pyproject.toml --check --diff --verbose . - isort --check --diff --verbose . - flake8 --config .flake8 --verbose --count . + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + - name: ๐Ÿ— Install Python dependencies + run: poetry install --no-interaction + - name: ๐Ÿš€ Run ruff linter + run: poetry run ruff check --output-format=github . + - name: ๐Ÿš€ Run ruff formatter + run: poetry run ruff format --check . pytest: runs-on: ubuntu-latest diff --git a/poetry.lock b/poetry.lock index e1f1282..7efd521 100644 --- a/poetry.lock +++ b/poetry.lock @@ -11,66 +11,6 @@ files = [ {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, ] -[[package]] -name = "black" -version = "24.2.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, - {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, - {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, - {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, - {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, - {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, - {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, - {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, - {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, - {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, - {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, - {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, - {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, - {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, - {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, - {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, - {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, - {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, - {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, - {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, - {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, - {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "colorama" version = "0.4.6" @@ -96,22 +36,6 @@ files = [ [package.extras] test = ["pytest (>=6)"] -[[package]] -name = "flake8" -version = "7.0.0" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.8.1" -files = [ - {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"}, - {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.11.0,<2.12.0" -pyflakes = ">=3.2.0,<3.3.0" - [[package]] name = "iniconfig" version = "2.0.0" @@ -123,42 +47,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - [[package]] name = "orjson" version = "3.9.14" @@ -243,32 +131,6 @@ files = [ [package.extras] proxy = ["pysocks"] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "platformdirs" -version = "4.2.0" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = ">=3.8" -files = [ - {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, - {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, -] - -[package.extras] -docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] - [[package]] name = "pluggy" version = "1.4.0" @@ -284,17 +146,6 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] -[[package]] -name = "pycodestyle" -version = "2.11.1" -description = "Python style guide checker" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"}, - {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"}, -] - [[package]] name = "pydantic" version = "2.6.1" @@ -405,17 +256,6 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" -[[package]] -name = "pyflakes" -version = "3.2.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, - {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, -] - [[package]] name = "pytest" version = "8.0.1" @@ -456,6 +296,32 @@ pytest = ">=7.0.0,<9" docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +[[package]] +name = "ruff" +version = "0.2.2" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"}, + {file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"}, + {file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"}, + {file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"}, + {file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"}, + {file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"}, + {file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"}, + {file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"}, +] + [[package]] name = "tomli" version = "2.0.1" @@ -481,4 +347,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = ">=3.10,<4.0" -content-hash = "39360308ed91933198e9ac982b202da85aeac6d84e7857a23b3548235e56d35c" +content-hash = "3816e8bec7131071d106c1a4554d3bfb7521798b0fdeebb33eadad73ca92cfc5" diff --git a/pyproject.toml b/pyproject.toml index aeab54f..70af6dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,9 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: System :: Networking" ] -exclude = ["tests/*"] +packages = [ + { include = "roombapy" }, +] [tool.poetry.scripts] roomba-connect = "roombapy.entry_points:connect" @@ -25,16 +27,67 @@ orjson = ">=3.9.13" paho-mqtt = ">=1.5.1,<3.0.0" pydantic = ">=1" -[tool.poetry.dev-dependencies] -black = "^24.2" -flake8 = "^7.0" -isort = "^5.13" +[tool.poetry.group.dev.dependencies] pytest = "^8.0" pytest-asyncio = "^0.23" +ruff = "^0.2.2" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" -[tool.black] +[tool.ruff] line-length = 80 + +[tool.ruff.lint] +ignore = [ + "ANN001", # Parameter type + "ANN101", # Self... explanatory + "ANN102", # cls... just as useless + "ANN202", + "ANN204", # init return type + "ANN205", + "ANN201", # Return type + "ANN401", # Opinioated warning on disallowing dynamically typed expressions + "ARG002", + "BLE001", + "B007", + "C901", + "D105", + "D106", + "D203", # Conflicts with other rules + "D404", + "D213", # Conflicts with other rules + "DTZ005", + "EM101", + "EM102", + "FBT002", + "FBT003", + "G002", + "G004", + "TRY002", + "TRY003", + "TRY300", + "TRY400", + "COM812", # Conflicts with other rules + "ISC001", # Conflicts with other rules + "D100", # documentation info, should be removed after fixes + "D101", # documentation info, should be removed after fixes + "D102", # documentation info, should be removed after fixes + "D103", # documentation info, should be removed after fixes + "PERF102", + "PLE1205", + "PLR2004", # Just annoying, not really useful + "PLR0912", + "PLR1714", + "PLW2901", + "RET507", + "SIM102", + "SLF001", + "T201", + "UP007", +] +select = ["ALL"] + +[tool.ruff.format] +docstring-code-format = true \ No newline at end of file diff --git a/roombapy/__init__.py b/roombapy/__init__.py index 4d3e195..20e241a 100644 --- a/roombapy/__init__.py +++ b/roombapy/__init__.py @@ -5,3 +5,12 @@ from .roomba import Roomba, RoombaConnectionError from .roomba_factory import RoombaFactory from .roomba_info import RoombaInfo + +__all__ = [ + "RoombaDiscovery", + "RoombaPassword", + "Roomba", + "RoombaConnectionError", + "RoombaFactory", + "RoombaInfo", +] diff --git a/roombapy/discovery.py b/roombapy/discovery.py index df8f492..2cb7502 100644 --- a/roombapy/discovery.py +++ b/roombapy/discovery.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import logging import socket from typing import Optional @@ -66,7 +68,7 @@ def _broadcast_message(self, amount): self.server_socket.sendto( self.roomba_message.encode(), (self.udp_address, self.udp_port) ) - self.log.debug("Broadcast message sent: " + str(i)) + self.log.debug("Broadcast message sent: %s", i) def _send_message(self, udp_address): self.server_socket.sendto( diff --git a/roombapy/getpassword.py b/roombapy/getpassword.py index fdebf7e..993b1b2 100644 --- a/roombapy/getpassword.py +++ b/roombapy/getpassword.py @@ -68,7 +68,7 @@ def _get_response(self): except socket.timeout: self.log.warning("Socket timeout") return None - except socket.error as e: + except OSError as e: self.log.debug("Socket error", e) return None diff --git a/roombapy/roomba.py b/roombapy/roomba.py index e57f2c4..ff1fc91 100755 --- a/roombapy/roomba.py +++ b/roombapy/roomba.py @@ -1,8 +1,6 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Python 3.* (thanks to pschmitt for adding Python 3 compatibility). +"""Python 3.* (thanks to pschmitt for adding Python 3 compatibility). Program to connect to Roomba 980 vacuum cleaner, dcode json, and forward to mqtt server. @@ -32,12 +30,9 @@ class RoombaConnectionError(Exception): """Roomba connection exception.""" - pass - class Roomba: - """ - This is a Class for Roomba 900 series WiFi connected Vacuum cleaners. + """This is a Class for Roomba 900 series WiFi connected Vacuum cleaners. Requires firmware version 2.0 and above (not V1.0). Tested with Roomba 980 username (blid) and password are required, and can be found using the @@ -116,9 +111,7 @@ def _connect(self): is_connected = self.remote_client.connect() if not is_connected: raise RoombaConnectionError( - "Unable to connect to Roomba at {}".format( - self.remote_client.address - ) + f"Unable to connect to Roomba at {self.remote_client.address}" ) return is_connected @@ -245,8 +238,7 @@ def set_preference(self, preference, setting): self.remote_client.publish("delta", str_command) def dict_merge(self, dct, merge_dct): - """ - Recursive dict merge. + """Recursive dict merge. Inspired by :meth:``dict.update()``, instead of updating only top-level keys, dict_merge recurses down into dicts @@ -267,8 +259,7 @@ def dict_merge(self, dct, merge_dct): dct[k] = merge_dct[k] def decode_payload(self, topic, payload): - """ - Format json for pretty printing. + """Format json for pretty printing. Returns string sutiable for logging, and a dict of the json data """ @@ -304,8 +295,7 @@ def decode_payload(self, topic, payload): return formatted_data, dict(json_data) def decode_topics(self, state, prefix=None): - """ - Decode json data dict and publish as individual topics. + """Decode json data dict and publish as individual topics. Publish to brokerFeedback/topic the keys are concatinated with _ to make one unique topic name strings are expressely converted @@ -361,8 +351,7 @@ def decode_topics(self, state, prefix=None): self.update_state_machine() def update_state_machine(self, new_state=None): - """ - Roomba progresses through states (phases). + """Roomba progresses through states (phases). Normal Sequence is "" -> charge -> run -> hmPostMsn -> charge Mid mission recharge is "" -> charge -> run -> hmMidMsn -> charge @@ -378,7 +367,6 @@ def update_state_machine(self, new_state=None): Assume hmPostMsn -> charge = end of mission (finalize map) Anything else = continue with existing map """ - current_mission = self.current_state try: @@ -447,13 +435,15 @@ def update_state_machine(self, new_state=None): ) and self.cleanMissionStatus_phase == "hmUsrDock": self.current_state = ROOMBA_STATES["cancelled"] elif ( - self.current_state == ROOMBA_STATES["hmUsrDock"] - or self.current_state == ROOMBA_STATES["cancelled"] - ) and self.cleanMissionStatus_phase == "charge": - self.current_state = ROOMBA_STATES["dockend"] - elif ( - self.current_state == ROOMBA_STATES["hmPostMsn"] + ( + self.current_state == ROOMBA_STATES["hmUsrDock"] + or self.current_state == ROOMBA_STATES["cancelled"] + ) and self.cleanMissionStatus_phase == "charge" + or ( + self.current_state == ROOMBA_STATES["hmPostMsn"] + and self.cleanMissionStatus_phase == "charge" + ) ): self.current_state = ROOMBA_STATES["dockend"] elif ( @@ -462,19 +452,16 @@ def update_state_machine(self, new_state=None): ): self.current_state = ROOMBA_STATES["charge"] + elif self.cleanMissionStatus_phase not in ROOMBA_STATES: + self.log.error( + "Can't find state %s in predefined Roomba states, " + "please create a new issue: " + "https://github.com/pschmitt/roombapy/issues/new", + self.cleanMissionStatus_phase, + ) + self.current_state = None else: - if self.cleanMissionStatus_phase not in ROOMBA_STATES: - self.log.error( - "Can't find state %s in predefined Roomba states, " - "please create a new issue: " - "https://github.com/pschmitt/roombapy/issues/new", - self.cleanMissionStatus_phase, - ) - self.current_state = None - else: - self.current_state = ROOMBA_STATES[ - self.cleanMissionStatus_phase - ] + self.current_state = ROOMBA_STATES[self.cleanMissionStatus_phase] if new_state is not None: self.current_state = ROOMBA_STATES[new_state] diff --git a/roombapy/roomba_factory.py b/roombapy/roomba_factory.py index d39402c..cffa926 100644 --- a/roombapy/roomba_factory.py +++ b/roombapy/roomba_factory.py @@ -3,9 +3,7 @@ class RoombaFactory: - """ - Allows you to create Roomba class to control your robot - """ + """Allows you to create Roomba class to control your robot.""" @staticmethod def create_roomba( diff --git a/roombapy/roomba_info.py b/roombapy/roomba_info.py index 3ab349d..f173f1a 100644 --- a/roombapy/roomba_info.py +++ b/roombapy/roomba_info.py @@ -1,5 +1,7 @@ +from __future__ import annotations + from functools import cached_property -from typing import Dict, Optional +from typing import Optional try: from pydantic.v1 import BaseModel, Field, field_validator @@ -15,7 +17,7 @@ class RoombaInfo(BaseModel): mac: str robot_name: str = Field(alias="robotname") sku: str - capabilities: Dict[str, int] = Field(alias="cap") + capabilities: dict[str, int] = Field(alias="cap") password: Optional[str] = None @field_validator("hostname") diff --git a/tests/abstract_test_roomba.py b/tests/abstract_test_roomba.py index e36a58a..aebb631 100644 --- a/tests/abstract_test_roomba.py +++ b/tests/abstract_test_roomba.py @@ -1,4 +1,4 @@ -from roombapy import RoombaFactory +from roombapy import Roomba, RoombaFactory ROOMBA_CONFIG = { "host": "127.0.0.1", @@ -18,7 +18,7 @@ def get_default_roomba( password=ROOMBA_CONFIG["password"], continuous=ROOMBA_CONFIG["continuous"], delay=ROOMBA_CONFIG["delay"], - ): + ) -> Roomba: return RoombaFactory.create_roomba( address=address, blid=blid, @@ -33,8 +33,8 @@ class Message: pass message = Message - setattr(message, "topic", topic) - setattr(message, "payload", payload) - setattr(message, "qos", "qos") + message.topic = topic + message.payload = payload + message.qos = "qos" return message diff --git a/tests/ruff.toml b/tests/ruff.toml new file mode 100644 index 0000000..b9916bc --- /dev/null +++ b/tests/ruff.toml @@ -0,0 +1,15 @@ +# This extend our general Ruff rules specifically for tests +extend = "../pyproject.toml" + +lint.extend-select = [ + "PT", # Use @pytest.fixture without parentheses +] + +lint.extend-ignore = [ + "S101", # Use of assert detected. As these are tests... + "S105", # Detection of passwords... + "S106", # Detection of passwords... + "SLF001", # Tests will access private/protected members... + "TCH002", # pytest doesn't like this one... + "PLR0913", # we're overwriting function that has many arguments +] diff --git a/tests/unit/test_decode.py b/tests/test_decode.py similarity index 78% rename from tests/unit/test_decode.py rename to tests/test_decode.py index 974873f..edcc8d8 100644 --- a/tests/unit/test_decode.py +++ b/tests/test_decode.py @@ -13,35 +13,35 @@ """ -def test_skip_garbage(): +def test_skip_garbage() -> None: assert _decode_data(b"\x0f\x00\xff\xf0") is None -def test_skip_own_messages(): +def test_skip_own_messages() -> None: assert _decode_data(RoombaDiscovery.roomba_message.encode()) is None -def test_skip_broken_json(): +def test_skip_broken_json() -> None: assert _decode_data(b'{"test": 1') is None -def test_skip_unknown_json(): +def test_skip_unknown_json() -> None: assert _decode_data(b'{"test": 1}') is None -def test_skip_unknown_hostname(): +def test_skip_unknown_hostname() -> None: assert _decode_data(b'{"hostname": "test"}') is None assert _decode_data(TEST_ROOMBA_INFO.encode()) is None -def test_skip_hostnames_without_blid(): +def test_skip_hostnames_without_blid() -> None: decoded = _decode_data( TEST_ROOMBA_INFO.replace("hostname_placeholder", "iRobot-").encode() ) assert decoded is None -def test_allow_approved_hostnames(): +def test_allow_approved_hostnames() -> None: blid = "test" for hostname in [f"Roomba-{blid}", f"iRobot-{blid}"]: decoded = _decode_data( diff --git a/tests/test_roomba_integration.py b/tests/test_roomba_integration.py index a635cb7..049a5bb 100644 --- a/tests/test_roomba_integration.py +++ b/tests/test_roomba_integration.py @@ -6,7 +6,7 @@ class TestRoombaIntegration(abstract_test_roomba.AbstractTestRoomba): - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_roomba_connect(self, event_loop): # given roomba = self.get_default_roomba() @@ -18,7 +18,7 @@ async def test_roomba_connect(self, event_loop): # then assert is_connected - @pytest.mark.asyncio + @pytest.mark.asyncio() async def test_roomba_connect_error(self, event_loop): # given roomba = self.get_default_roomba(blid="wrong")