diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 54d22aa..5031f99 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,204 +5,12 @@ on: push: pull_request: ~ -env: - CACHE_VERSION: 1 - DEFAULT_PYTHON: 3.8 - PRE_COMMIT_HOME: ~/.cache/pre-commit - jobs: - # Separate job to pre-populate the base dependency cache - # This prevent upcoming jobs to do the same individually - prepare-base: - name: Prepare base dependencies - runs-on: ubuntu-latest - strategy: - matrix: - python-version: [3.8, 3.9, "3.10", "3.11"] - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - id: python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v3 - with: - path: venv - key: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }} - restore-keys: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}- - - name: Create Python virtual environment - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - python -m venv venv - . venv/bin/activate - pip install -U pip setuptools pre-commit - pip install -e '.[testing]' - - pre-commit: - name: Prepare pre-commit environment - runs-on: ubuntu-latest - needs: prepare-base - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4 - id: python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v3 - with: - path: venv - key: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache@v3 - with: - path: ${{ env.PRE_COMMIT_HOME }} - key: ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - restore-keys: ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit- - - name: Install pre-commit dependencies - if: steps.cache-precommit.outputs.cache-hit != 'true' - run: | - . venv/bin/activate - pre-commit install-hooks - - lint-pre-commit: - name: Check pre-commit - runs-on: ubuntu-latest - needs: pre-commit - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3 - - name: Set up Python ${{ env.DEFAULT_PYTHON }} - uses: actions/setup-python@v4 - id: python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v3 - with: - path: venv - key: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Restore pre-commit environment from cache - id: cache-precommit - uses: actions/cache@v3 - with: - path: ${{ env.PRE_COMMIT_HOME }} - key: ${{ env.CACHE_VERSION}}-${{ runner.os }}-pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} - - name: Fail job if cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Run pre-commit - run: | - . venv/bin/activate - pre-commit run --all-files --show-diff-on-failure - - pytest: - runs-on: ubuntu-latest - needs: prepare-base - strategy: - matrix: - python-version: [3.8, 3.9, "3.10", "3.11"] - name: >- - Run tests Python ${{ matrix.python-version }} - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - id: python - with: - python-version: ${{ matrix.python-version }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v3 - with: - path: venv - key: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Register Python problem matcher - run: | - echo "::add-matcher::.github/workflows/matchers/python.json" - - name: Install Pytest Annotation plugin - run: | - . venv/bin/activate - # Ideally this should be part of our dependencies - # However this plugin is fairly new and doesn't run correctly - # on a non-GitHub environment. - pip install pytest-github-actions-annotate-failures - - name: Run pytest - run: | - . venv/bin/activate - pytest \ - -qq \ - --timeout=20 \ - --durations=10 \ - --cov zigpy_cli \ - --cov-config pyproject.toml \ - -o console_output_style=count \ - -p no:sugar \ - tests - - name: Upload coverage artifact - uses: actions/upload-artifact@v3 - with: - name: coverage-${{ matrix.python-version }} - path: .coverage - - - coverage: - name: Process test coverage - runs-on: ubuntu-latest - needs: pytest - steps: - - name: Check out code from GitHub - uses: actions/checkout@v3 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - id: python - with: - python-version: ${{ env.DEFAULT_PYTHON }} - - name: Restore base Python virtual environment - id: cache-venv - uses: actions/cache@v3 - with: - path: venv - key: ${{ env.CACHE_VERSION}}-${{ runner.os }}-base-venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('pyproject.toml') }} - - name: Fail job if Python cache restore failed - if: steps.cache-venv.outputs.cache-hit != 'true' - run: | - echo "Failed to restore Python virtual environment from cache" - exit 1 - - name: Download all coverage artifacts - uses: actions/download-artifact@v3 - - name: Combine coverage results - run: | - . venv/bin/activate - coverage combine coverage*/.coverage* - coverage report - coverage xml - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 + shared-ci: + uses: zigpy/workflows/.github/workflows/ci.yml@dm/updates-for-toml + with: + CODE_FOLDER: zigpy_cli + CACHE_VERSION: 2 + PYTHON_VERSION_DEFAULT: 3.8.14 + PRE_COMMIT_CACHE_PATH: ~/.cache/pre-commit + MINIMUM_COVERAGE_PERCENTAGE: 98 \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2250487..5f5bf03 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,47 @@ repos: + - repo: https://github.com/asottile/pyupgrade + rev: v3.3.1 + hooks: + - id: pyupgrade + args: [--py38-plus] + + - repo: https://github.com/PyCQA/autoflake + rev: v2.0.2 + hooks: + - id: autoflake + - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.3.0 hooks: - id: black - args: ["--safe", "--quiet"] + args: + - --quiet + + - repo: https://github.com/pycqa/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + additional_dependencies: [Flake8-pyproject] + + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + + - repo: https://github.com/codespell-project/codespell + rev: v2.2.4 + hooks: + - id: codespell + additional_dependencies: [tomli] + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.2.0 + hooks: + - id: mypy + - repo: https://github.com/charliermarsh/ruff-pre-commit - rev: 'v0.0.246' + rev: v0.0.261 hooks: - id: ruff - args: ["--fix"] \ No newline at end of file + args: + - --fix \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 858949b..d51c6d2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,17 +40,341 @@ testing = [ zigpy = "zigpy_cli.__main__:cli" +[tool.isort] +profile = "black" +not_skip = "__init__.py" +force_sort_within_sections = true +multi_line_output = 3 +length_sort = false +balanced_wrapping = true +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +line_length = 88 +indent = " " +sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"] +default_section = "THIRDPARTY" +known_first_party = ["zigpy_cli", "tests"] +forced_separate = "tests" +combine_as_imports = true + +[tool.autoflake8] +in-place = true +recursive = false +expand-star-imports = false +exclude = [ + ".venv", + ".git", + ".tox", + "docs", + "venv", + "bin", + "lib", + "deps", + "build", +] + +[tool.black] +safe = true +quiet = true + +[tool.pylint.MAIN] +py-version = "3.8" +ignore = [ + "tests", +] +# Use a conservative default here; 2 should speed up most setups and not hurt +# any too bad. Override on command line as appropriate. +jobs = 2 +init-hook = """\ + from pathlib import Path; \ + import sys; \ + + from pylint.config import find_default_config_files; \ + + sys.path.append( \ + str(Path(next(find_default_config_files())).parent.joinpath('pylint/plugins')) + ) \ + """ +load-plugins = [ + "pylint.extensions.code_style", + "pylint.extensions.typing", +] +persistent = false +extension-pkg-allow-list = [ + "av.audio.stream", + "av.stream", + "ciso8601", + "orjson", + "cv2", +] +fail-on = [ + "I", +] + +[tool.pylint.CLASSES] +exclude-protected = "_DEVICE_REGISTRY" + +[tool.pylint.BASIC] +class-const-naming-style = "any" +good-names = [ + "_", + "ev", + "ex", + "fp", + "i", + "id", + "j", + "k", + "Run", + "ip", + "3321S", + "3130", + "3300S", + "3310S", + "3305S", + "3460L", + "3157100", + "mot003V0", + "mot003V6", +] + +[tool.pylint."MESSAGES CONTROL"] +# Reasons disabled: +# format - handled by black +# locally-disabled - it spams too much +# duplicate-code - unavoidable +# cyclic-import - doesn't test if both import on load +# abstract-class-little-used - prevents from setting right foundation +# unused-argument - generic callbacks and setup methods create a lot of warnings +# too-many-* - are not enforced for the sake of readability +# too-few-* - same as too-many-* +# abstract-method - with intro of async there are always methods missing +# inconsistent-return-statements - doesn't handle raise +# too-many-ancestors - it's too strict. +# wrong-import-order - isort guards this +# consider-using-f-string - str.format sometimes more readable +# --- +# Pylint CodeStyle plugin +# consider-using-namedtuple-or-dataclass - too opinionated +# consider-using-assignment-expr - decision to use := better left to devs +disable = [ + "format", + "abstract-class-little-used", + "abstract-method", + "cyclic-import", + "duplicate-code", + "global-statement", + "inconsistent-return-statements", + "locally-disabled", + "not-an-iterable", + "not-context-manager", + "redefined-variable-type", + "too-few-public-methods", + "too-many-ancestors", + "too-many-arguments", + "too-many-branches", + "too-many-instance-attributes", + "too-many-lines", + "too-many-locals", + "too-many-public-methods", + "too-many-return-statements", + "too-many-statements", + "too-many-boolean-expressions", + "unused-argument", + "unnecessary-pass", +] +enable = [ + #"useless-suppression", # temporarily every now and then to clean them up + "use-symbolic-message-instead", +] + +[tool.pylint.REPORTS] +score = false + +[tool.pylint.TYPECHECK] +ignored-classes = [ + "_CountingAttr", # for attrs +] +mixin-class-rgx = ".*[Mm]ix[Ii]n" + +[tool.pylint.FORMAT] +expected-line-ending-format = "LF" + +[tool.pylint.EXCEPTIONS] +overgeneral-exceptions = [ + "builtins.BaseException", + "builtins.Exception", +] + +[tool.pylint.TYPING] +runtime-typing = false + +[tool.pylint.CODE_STYLE] +max-line-length-suggestions = 88 + +[tool.pytest.ini_options] +testpaths = [ + "tests", +] +norecursedirs = [ + ".git", + "testing_config", +] +addopts = "--showlocals --verbose" +log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" +log_date_format = "%Y-%m-%d %H:%M:%S" +asyncio_mode = "auto" + +[tool.mypy] +python_version = 3.8 +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_calls = true +disallow_untyped_defs = true +follow_imports = "silent" +ignore_missing_imports = true +no_implicit_optional = true +strict_equality = true +warn_incomplete_stub = true +warn_redundant_casts = true +warn_return_any = true +warn_unreachable = true +warn_unused_configs = true +warn_unused_ignores = true +show_error_codes = true +show_error_context = true +error_summary = true + +install_types = true +non_interactive = true + +disable_error_code = [ + "arg-type", + "assignment", + "attr-defined", + "call-arg", + "dict-item", + "index", + "misc", + "no-any-return", + "no-untyped-call", + "no-untyped-def", + "override", + "return-value", + "union-attr", + "var-annotated", +] + +[tool.coverage.run] +source = ["zigpy_cli"] + +[tool.flake8] +exclude = [ + ".venv", + ".git", + "docs", + "venv", + "bin", + "lib", + "deps", + "build", +] +# To work with Black +max-line-length = 88 +# E501: line too long +# W503: Line break occurred before a binary operator +# E203: Whitespace before ':' +# D202 No blank lines allowed after function docstring +ignore = [ + "E501", + "W503", + "E203", + "D202", +] + +[tool.pyupgrade] +py37plus = true + +[tool.codespell] +ignore-words-list = "hass, dout" +skip = [ + "./.*", + "test/*", +] +quiet-level = 2 + +[tool.pydocstyle] +ignore = [ + "D202", + "D203", + "D213", +] + [tool.ruff] +target-version = "py38" + select = [ - # Pyflakes - "F", - # Pycodestyle - "E", - "W", - # isort - "I001" + "B007", # Loop control variable {name} not used within loop body + "B014", # Exception handler with duplicate exception + "C", # complexity + "D", # docstrings + "E", # pycodestyle + "F", # pyflakes/autoflake + "ICN001", # import concentions; {name} should be imported as {asname} + "PGH004", # Use specific rule codes when using noqa + "PLC0414", # Useless import alias. Import alias does not rename original package. + "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass + "SIM117", # Merge with-statements that use the same scope + "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() + "SIM201", # Use {left} != {right} instead of not {left} == {right} + "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} + "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. + "SIM401", # Use get from dict with default instead of an if block + "T20", # flake8-print + "TRY004", # Prefer TypeError exception for invalid type + "RUF006", # Store a reference to the return value of asyncio.create_task + "UP", # pyupgrade + "W", # pycodestyle +] + +ignore = [ + "D100", # Missing docstring in public module + "D101", # Missing docstring in public class + "D102", # Missing docstring in public method + "D103", # Missing docstring in public function + "D104", # Missing docstring in public package + "D105", # Missing docstring in magic method + "D106", # Missing docstring in public nested class + "D107", # Missing docstring in `__init__` + "D202", # No blank lines allowed after function docstring + "D203", # 1 blank line required before class docstring + "D205", # 1 blank line required between summary line and description + "D213", # Multi-line docstring summary should start at the second line + "D400", # First line should end with a period + "D401", # First line of docstring should be in imperative mood: + "D406", # Section name should end with a newline + "D407", # Section name underlining + "D415", # First line should end with a period, question mark, or exclamation point + "E501", # line too long + # the rules below this line should be corrected + "PGH004", # Use specific rule codes when using `noqa` +] + +extend-exclude = [ + "tests" ] -src = ["zigpy_cli", "tests"] + +[tool.ruff.flake8-pytest-style] +fixture-parentheses = false + +[tool.ruff.pyupgrade] +keep-runtime-typing = true [tool.ruff.isort] +force-sort-within-sections = true known-first-party = ["zigpy_cli", "tests"] +combine-as-imports = true + +[tool.ruff.mccabe] +max-complexity = 25 diff --git a/zigpy_cli/__main__.py b/zigpy_cli/__main__.py index 862928f..08e2b60 100644 --- a/zigpy_cli/__main__.py +++ b/zigpy_cli/__main__.py @@ -1,5 +1,5 @@ +from zigpy_cli.cli import cli # noqa: F401 import zigpy_cli.database # noqa: F401 import zigpy_cli.ota # noqa: F401 import zigpy_cli.pcap # noqa: F401 import zigpy_cli.radio # noqa: F401 -from zigpy_cli.cli import cli # noqa: F401 diff --git a/zigpy_cli/database.py b/zigpy_cli/database.py index 021ba72..5ddf6e8 100644 --- a/zigpy_cli/database.py +++ b/zigpy_cli/database.py @@ -24,9 +24,7 @@ def db(): def sqlite3_split_statements(sql: str) -> list[str]: - """ - Splits SQL into a list of statements. - """ + """Splits SQL into a list of statements.""" statements = [] statement = "" @@ -48,9 +46,7 @@ def sqlite3_split_statements(sql: str) -> list[str]: def sqlite3_recover(path: pathlib.Path) -> str: - """ - Recovers the contents of an SQLite database as valid SQL. - """ + """Recovers the contents of an SQLite database as valid SQL.""" return subprocess.check_output(["sqlite3", str(path), ".recover"]).decode("utf-8") @@ -70,9 +66,7 @@ def get_table_versions(cursor) -> dict[str, str]: async def test_database(path: pathlib.Path): - """ - Opens the zigpy database with zigpy and attempts to load its contents. - """ + """Opens the zigpy database with zigpy and attempts to load its contents.""" with tempfile.TemporaryDirectory() as dir_name: dir_path = pathlib.Path(dir_name) @@ -121,7 +115,7 @@ def recover(input_path, output_path): pragma_user_version, ) - if zigpy.appdb.DB_VERSION != max_table_version: + if max_table_version != zigpy.appdb.DB_VERSION: LOGGER.warning( "Zigpy's current DB version is %s but the maximum table version is %s!", zigpy.appdb.DB_VERSION, diff --git a/zigpy_cli/ota.py b/zigpy_cli/ota.py index 0d6f1ee..22a63dd 100644 --- a/zigpy_cli/ota.py +++ b/zigpy_cli/ota.py @@ -8,9 +8,9 @@ import subprocess import click -import zigpy.types as t from zigpy.ota.image import ElementTagId, HueSBLOTAImage, parse_ota_image from zigpy.ota.validators import validate_ota_image +import zigpy.types as t from zigpy.types.named import _hex_string_to_bytes from zigpy.util import convert_install_code as zigpy_convert_install_code