diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index a46ed14..6308238 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5.5.0 with: - python-version: "3.8" + python-version: "3.9" - name: Install dependencies run: | python -m pip install --upgrade pip @@ -29,7 +29,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5.5.0 with: - python-version: "3.8" + python-version: "3.9" - name: Lint with ruff run: | python -m pip install --upgrade pip @@ -42,25 +42,6 @@ jobs: mypy devolo_plc_api mypy tests || true - test_old: - name: Test with Python 3.8 - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v4.2.2 - - name: Set up Python 3.8 - uses: actions/setup-python@v5.5.0 - with: - python-version: 3.8 - check-latest: true - - name: Install dependencies - run: | - python -m pip install --upgrade pip - python -m pip install -e .[test] - - name: Test with pytest - run: | - pytest --cov=devolo_plc_api -p no:warnings - test: name: Test with Python ${{ matrix.python-version }} runs-on: ubuntu-latest diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml index a13cafe..ddc6751 100644 --- a/.github/workflows/pythonpublish.yml +++ b/.github/workflows/pythonpublish.yml @@ -12,7 +12,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5.5.0 with: - python-version: "3.8" + python-version: "3.9" - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/README.md b/README.md index da4ff19..f994ef7 100644 --- a/README.md +++ b/README.md @@ -12,18 +12,20 @@ This project implements parts of the devolo PLC devices API in Python. Communica Defining the system requirements with exact versions typically is difficult. But there is a tested environment: * Linux -* Python 3.8.12 -* pip 20.2.4 -* httpx 0.21.0 -* protobuf 3.17.3 -* segno 1.5.2 -* zeroconf 0.70.0 +* Python 3.9.22 +* pip 25.0.1 +* ifaddr 0.2.0 +* httpx 0.28.1 +* protobuf 5.28.3 +* segno 1.6.1 +* zeroconf 0.146.1 Other versions and even other operating systems might work. Feel free to tell us about your experience. If you want to run our unit tests, you also need: -* pytest 6.2.5 -* pytest-asyncio 0.15.1 -* pytest-httpx 0.18 +* pytest 7.4.4 +* pytest-asyncio 0.26.0 +* pytest-httpx 0.35.0 +* syrupy 4.9.1 ## Versioning diff --git a/devolo_plc_api/__init__.py b/devolo_plc_api/__init__.py index 9a37a53..8b6af53 100644 --- a/devolo_plc_api/__init__.py +++ b/devolo_plc_api/__init__.py @@ -1,4 +1,5 @@ """The devolo PLC API.""" + from importlib.metadata import PackageNotFoundError, version from .device import Device diff --git a/devolo_plc_api/clients/__init__.py b/devolo_plc_api/clients/__init__.py index 7128663..dcf339c 100644 --- a/devolo_plc_api/clients/__init__.py +++ b/devolo_plc_api/clients/__init__.py @@ -1,4 +1,5 @@ """Clients used to communicate with devolo devices.""" + from .protobuf import Protobuf __all__ = ["Protobuf"] diff --git a/devolo_plc_api/clients/protobuf.py b/devolo_plc_api/clients/protobuf.py index c1690a1..f65b493 100644 --- a/devolo_plc_api/clients/protobuf.py +++ b/devolo_plc_api/clients/protobuf.py @@ -1,4 +1,5 @@ """Google Protobuf client.""" + from __future__ import annotations import asyncio diff --git a/devolo_plc_api/device_api/__init__.py b/devolo_plc_api/device_api/__init__.py index e47898f..e9e3575 100644 --- a/devolo_plc_api/device_api/__init__.py +++ b/devolo_plc_api/device_api/__init__.py @@ -1,4 +1,5 @@ """The devolo device API.""" + import re from .deviceapi import DeviceApi diff --git a/devolo_plc_api/device_api/deviceapi.py b/devolo_plc_api/device_api/deviceapi.py index 7a2aedd..6715a34 100644 --- a/devolo_plc_api/device_api/deviceapi.py +++ b/devolo_plc_api/device_api/deviceapi.py @@ -1,4 +1,5 @@ """Implementation of the devolo device API.""" + from __future__ import annotations import functools diff --git a/devolo_plc_api/exceptions/__init__.py b/devolo_plc_api/exceptions/__init__.py index f35b5ad..b692047 100644 --- a/devolo_plc_api/exceptions/__init__.py +++ b/devolo_plc_api/exceptions/__init__.py @@ -1,4 +1,5 @@ """Exceptions used by the package.""" + from .device import DeviceNotFound, DevicePasswordProtected, DeviceUnavailable from .feature import FeatureNotSupported diff --git a/devolo_plc_api/helpers/__init__.py b/devolo_plc_api/helpers/__init__.py index 6382cfa..a9a047c 100644 --- a/devolo_plc_api/helpers/__init__.py +++ b/devolo_plc_api/helpers/__init__.py @@ -1,4 +1,5 @@ """Helper methods to allow advanced usage of information provided by the device.""" + from io import BytesIO from typing import Any diff --git a/devolo_plc_api/plcnet_api/__init__.py b/devolo_plc_api/plcnet_api/__init__.py index 1094440..70483df 100644 --- a/devolo_plc_api/plcnet_api/__init__.py +++ b/devolo_plc_api/plcnet_api/__init__.py @@ -1,4 +1,5 @@ """The devolo plcnet API.""" + from .getnetworkoverview_pb2 import GetNetworkOverview from .plcnetapi import PlcNetApi diff --git a/devolo_plc_api/plcnet_api/plcnetapi.py b/devolo_plc_api/plcnet_api/plcnetapi.py index fd8aff8..f7ca0fd 100644 --- a/devolo_plc_api/plcnet_api/plcnetapi.py +++ b/devolo_plc_api/plcnet_api/plcnetapi.py @@ -1,4 +1,5 @@ """Implementation of the devolo plcnet API.""" + from __future__ import annotations from typing import TYPE_CHECKING diff --git a/devolo_plc_api/zeroconf/__init__.py b/devolo_plc_api/zeroconf/__init__.py index 0fe6510..a15dac1 100644 --- a/devolo_plc_api/zeroconf/__init__.py +++ b/devolo_plc_api/zeroconf/__init__.py @@ -1,4 +1,5 @@ """Zeroconf dataclasses.""" + from __future__ import annotations from dataclasses import dataclass, field diff --git a/pyproject.toml b/pyproject.toml index 5dd8c00..d839fc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ asyncio_default_fixture_loop_scope = "function" [tool.ruff] exclude = ["*_pb2.py", "*.pyi"] line-length = 127 -target-version = "py38" +target-version = "py39" [tool.ruff.lint] ignore = ["ANN401", "COM812", "D203", "D205", "D212", "FBT001", "N818"] diff --git a/tests/__init__.py b/tests/__init__.py index 8df599d..c011eb1 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,4 +1,5 @@ """Unittests for devolo_plc_api.""" + from __future__ import annotations import json diff --git a/tests/conftest.py b/tests/conftest.py index c49d978..ee47379 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,10 @@ """Test configuration.""" + from __future__ import annotations from collections import OrderedDict from functools import partial -from typing import TYPE_CHECKING, AsyncGenerator, Generator +from typing import TYPE_CHECKING from unittest.mock import AsyncMock, Mock, patch import pytest @@ -16,6 +17,8 @@ from .mocks.zeroconf import MockAsyncServiceInfo, MockServiceBrowser if TYPE_CHECKING: + from collections.abc import AsyncGenerator, Generator + from syrupy.assertion import SnapshotAssertion pytest_plugins = [ @@ -35,9 +38,11 @@ def block_communication() -> Generator[None, None, None]: """Block external communication.""" adapter = OrderedDict() adapter["eth0"] = Adapter(name="eth0", nice_name="eth0", ips=[IP("192.0.2.100", network_prefix=24, nice_name="eth0")]) - with patch("devolo_plc_api.device.get_adapters", return_value=adapter.values()), patch( - "devolo_plc_api.device.AsyncZeroconf", AsyncMock - ), patch("devolo_plc_api.device.AsyncServiceInfo", MockAsyncServiceInfo): + with ( + patch("devolo_plc_api.device.get_adapters", return_value=adapter.values()), + patch("devolo_plc_api.device.AsyncZeroconf", AsyncMock), + patch("devolo_plc_api.device.AsyncServiceInfo", MockAsyncServiceInfo), + ): yield @@ -72,8 +77,9 @@ def patch_sleep() -> Generator[AsyncMock, None, None]: def service_browser(device_type: DeviceType) -> Generator[None, None, None]: """Patch mDNS service browser.""" service_browser = partial(MockServiceBrowser, device_type=device_type) - with patch("devolo_plc_api.device.AsyncServiceBrowser", service_browser), patch( - "devolo_plc_api.network.ServiceBrowser", service_browser + with ( + patch("devolo_plc_api.device.AsyncServiceBrowser", service_browser), + patch("devolo_plc_api.network.ServiceBrowser", service_browser), ): yield diff --git a/tests/fixtures/device_api.py b/tests/fixtures/device_api.py index f8a0238..341cb4d 100644 --- a/tests/fixtures/device_api.py +++ b/tests/fixtures/device_api.py @@ -1,8 +1,9 @@ """Fixtures for device API tests.""" + from __future__ import annotations from secrets import randbelow -from typing import TYPE_CHECKING, AsyncGenerator +from typing import TYPE_CHECKING import pytest import pytest_asyncio @@ -19,6 +20,8 @@ ) if TYPE_CHECKING: + from collections.abc import AsyncGenerator + from tests import TestData diff --git a/tests/fixtures/plcnet_api.py b/tests/fixtures/plcnet_api.py index ace09e3..fb2c8a7 100644 --- a/tests/fixtures/plcnet_api.py +++ b/tests/fixtures/plcnet_api.py @@ -1,5 +1,6 @@ """Fixtures for plcnet API tests.""" -from typing import AsyncGenerator + +from collections.abc import AsyncGenerator import pytest import pytest_asyncio diff --git a/tests/mocks/zeroconf.py b/tests/mocks/zeroconf.py index 0e783f4..26b94e2 100644 --- a/tests/mocks/zeroconf.py +++ b/tests/mocks/zeroconf.py @@ -1,4 +1,5 @@ """Mock methods from the Zeroconf module.""" + from __future__ import annotations import socket diff --git a/tests/test_device.py b/tests/test_device.py index 7778ff7..4a61027 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -1,4 +1,5 @@ """Test communicating with a devolo device.""" + from asyncio import AbstractEventLoop from unittest.mock import AsyncMock, Mock, patch @@ -130,9 +131,10 @@ def test_context_manager(self, test_data: TestData): @pytest.mark.asyncio async def test_state_change_removed(self, mock_device: Device): """Test that service information are not processed on state change to removed.""" - with patch("devolo_plc_api.device.Device._retry_zeroconf_info"), patch( - "devolo_plc_api.device.Device._get_service_info" - ) as gsi: + with ( + patch("devolo_plc_api.device.Device._retry_zeroconf_info"), + patch("devolo_plc_api.device.Device._get_service_info") as gsi, + ): mock_device._state_change(Mock(), PLCNETAPI, PLCNETAPI, ServiceStateChange.Removed) assert gsi.call_count == 0 diff --git a/tests/test_deviceapi.py b/tests/test_deviceapi.py index 64874fe..e57ff7a 100644 --- a/tests/test_deviceapi.py +++ b/tests/test_deviceapi.py @@ -1,4 +1,5 @@ """Test communicating with a the device API.""" + from __future__ import annotations import sys diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 92ddd5d..fc9fd72 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,4 +1,5 @@ """Test helper methods.""" + from syrupy.assertion import SnapshotAssertion from devolo_plc_api import wifi_qr_code diff --git a/tests/test_network.py b/tests/test_network.py index c4da56f..b59072c 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -1,4 +1,5 @@ """Test network discovery.""" + from socket import inet_aton from unittest.mock import Mock, patch @@ -19,8 +20,9 @@ class TestNetwork: @pytest.mark.asyncio async def test_async_discover_network(self, test_data: TestData, mock_info_from_service: Mock): """Test discovering the network asynchronously.""" - with patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser), patch( - "devolo_plc_api.network.Zeroconf.get_service_info", return_value="" + with ( + patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser), + patch("devolo_plc_api.network.Zeroconf.get_service_info", return_value=""), ): serial_number = test_data.device_info[SERVICE_TYPE].properties["SN"] mock_info_from_service.return_value = ZeroconfServiceInfo( @@ -33,8 +35,9 @@ async def test_async_discover_network(self, test_data: TestData, mock_info_from_ def test_discover_network(self, test_data: TestData, mock_info_from_service: Mock): """Test discovering the network synchronously.""" - with patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser), patch( - "devolo_plc_api.network.Zeroconf.get_service_info", return_value="" + with ( + patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser), + patch("devolo_plc_api.network.Zeroconf.get_service_info", return_value=""), ): serial_number = test_data.device_info[SERVICE_TYPE].properties["SN"] mock_info_from_service.return_value = ZeroconfServiceInfo( @@ -53,8 +56,9 @@ def test_add_wrong_state(self): def test_no_devices(self): """Test discovery with no devices.""" - with patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser), patch( - "devolo_plc_api.network.Zeroconf.get_service_info", return_value=None + with ( + patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser), + patch("devolo_plc_api.network.Zeroconf.get_service_info", return_value=None), ): discovered = network.discover_network(timeout=0.1) assert not discovered @@ -62,8 +66,9 @@ def test_no_devices(self): @pytest.mark.parametrize("mt", ["2600", "2601"]) def test_hcu(self, test_data: TestData, mt: str, mock_info_from_service: Mock): """Test ignoring Home Control Central Units.""" - with patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser), patch( - "devolo_plc_api.network.Zeroconf.get_service_info", return_value="" + with ( + patch("devolo_plc_api.network.ServiceBrowser", MockServiceBrowser), + patch("devolo_plc_api.network.Zeroconf.get_service_info", return_value=""), ): mock_info_from_service.return_value = ZeroconfServiceInfo(address=test_data.ip.encode(), properties={"MT": mt}) discovered = network.discover_network(timeout=0.1) diff --git a/tests/test_plcnetapi.py b/tests/test_plcnetapi.py index d5fe55d..7993004 100644 --- a/tests/test_plcnetapi.py +++ b/tests/test_plcnetapi.py @@ -1,4 +1,5 @@ """Test communicating with a the plcnet API.""" + import sys from http import HTTPStatus