Skip to content

Commit

Permalink
Rework typing
Browse files Browse the repository at this point in the history
---
updated-dependencies:
- dependency-name: httpx
  dependency-type: direct:production
...

* Rework typing
* Mark package as typed
* Add Python 3.10

Co-authored-by: Markus Bong <2Fake1987@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Markus Bong <Markus.Bong@devolo.de>
  • Loading branch information
4 people authored Oct 18, 2021
1 parent 44a1af1 commit ba19740
Show file tree
Hide file tree
Showing 12 changed files with 69 additions and 48 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pythonpackage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.7, 3.8, 3.9]
python-version: ["3.7", "3.8", "3.9", "3.10"]
steps:
- name: Checkout sources
uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion devolo_plc_api/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
try:
from importlib.metadata import PackageNotFoundError, version
except ImportError:
from importlib_metadata import PackageNotFoundError, version # type: ignore[no-redef]
from importlib_metadata import PackageNotFoundError, version # type: ignore

try:
__version__ = version("devolo_plc_api")
Expand Down
6 changes: 4 additions & 2 deletions devolo_plc_api/clients/protobuf.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import annotations

import asyncio
import logging
from abc import ABC, abstractclassmethod
from typing import Callable
from typing import Any, Callable

from google.protobuf.json_format import MessageToDict
from httpx import AsyncClient, ConnectError, ConnectTimeout, DigestAuth, ReadTimeout, Response
Expand Down Expand Up @@ -69,6 +71,6 @@ async def _async_post(self, sub_url: str, content: bytes, timeout: float = TIMEO
raise DeviceUnavailable("The device is currenctly not available. Maybe on standby?") from None

@staticmethod
def _message_to_dict(message) -> dict:
def _message_to_dict(message) -> dict[str, Any]:
""" Convert message to dict with certain settings. """
return MessageToDict(message=message, including_default_value_fields=True, preserving_proto_field_name=True)
37 changes: 22 additions & 15 deletions devolo_plc_api/device.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from __future__ import annotations

import asyncio
import ipaddress
import logging
import struct
from datetime import date
from typing import Dict, Optional
from typing import Any

import httpx
from zeroconf import ServiceBrowser, ServiceInfo, ServiceStateChange, Zeroconf
Expand All @@ -12,9 +14,10 @@
from .exceptions.device import DeviceNotFound
from .plcnet_api.plcnetapi import PlcNetApi

EMPTY_INFO: Dict = {
"properties": {}
}
EMPTY_INFO: dict[str,
Any] = {
"properties": {}
}


class Device:
Expand All @@ -29,9 +32,11 @@ class Device:

def __init__(self,
ip: str,
plcnetapi: Optional[Dict] = None,
deviceapi: Optional[Dict] = None,
zeroconf_instance: Optional[Zeroconf] = None):
plcnetapi: dict[str,
Any] | None = None,
deviceapi: dict[str,
Any] | None = None,
zeroconf_instance: Zeroconf | None = None):
self.firmware_date = date.fromtimestamp(0)
self.firmware_version = ""
self.hostname = ""
Expand All @@ -46,13 +51,15 @@ def __init__(self,
self.plcnet = None

self._connected = False
self._info: Dict = {
"_dvl-plcnetapi._tcp.local.": plcnetapi or EMPTY_INFO,
"_dvl-deviceapi._tcp.local.": deviceapi or EMPTY_INFO,
}
self._info: dict[str,
dict[str,
Any]] = {
"_dvl-plcnetapi._tcp.local.": plcnetapi or EMPTY_INFO,
"_dvl-deviceapi._tcp.local.": deviceapi or EMPTY_INFO,
}
self._logger = logging.getLogger(f"{self.__class__.__module__}.{self.__class__.__name__}")
self._password = ""
self._session_instance: Optional[httpx.AsyncClient] = None
self._session_instance: httpx.AsyncClient | None = None
self._zeroconf_instance = zeroconf_instance
logging.captureWarnings(True)

Expand Down Expand Up @@ -90,7 +97,7 @@ def password(self, password: str):
if self.device:
self.device.password = password

async def async_connect(self, session_instance: Optional[httpx.AsyncClient] = None):
async def async_connect(self, session_instance: httpx.AsyncClient | None = None):
"""
Connect to a device asynchronous.
Expand Down Expand Up @@ -176,11 +183,11 @@ def _state_change(self, zeroconf: Zeroconf, service_type: str, name: str, state_
self._info[service_type] = self.info_from_service(service_info)

@staticmethod
def info_from_service(service_info: ServiceInfo) -> Optional[Dict]:
def info_from_service(service_info: ServiceInfo) -> dict[str, Any]:
""" Return prepared info from mDNS entries. """
properties = {}
if not service_info.addresses:
return None # No need to continue, if there is no IP address to contact the device
return {} # No need to continue, if there is no IP address to contact the device

total_length = len(service_info.text)
offset = 0
Expand Down
22 changes: 12 additions & 10 deletions devolo_plc_api/device_api/deviceapi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Callable, Dict
from __future__ import annotations

from typing import Any, Callable

from httpx import AsyncClient

Expand All @@ -18,7 +20,7 @@ class DeviceApi(Protobuf):
:param info: Information collected from the mDNS query
"""

def __init__(self, ip: str, session: AsyncClient, info: Dict):
def __init__(self, ip: str, session: AsyncClient, info: dict[str, Any]):
super().__init__()

self._ip = ip
Expand All @@ -30,7 +32,7 @@ def __init__(self, ip: str, session: AsyncClient, info: Dict):
self._version = info["properties"]["Version"]

features = info["properties"].get("Features", "")
self.features = features.split(",") if features else ["reset", "update", "led", "intmtg"]
self.features: list[str] = features.split(",") if features else ["reset", "update", "led", "intmtg"]
self.password = ""

def _feature(feature: str): # type: ignore # pylint: disable=no-self-argument
Expand All @@ -49,7 +51,7 @@ def wrapper(self, *args, **kwargs):
return feature_decorator

@_feature("led")
async def async_get_led_setting(self) -> dict:
async def async_get_led_setting(self) -> dict[str, Any]:
"""
Get LED setting asynchronously. This feature only works on devices, that announce the led feature.
Expand Down Expand Up @@ -78,7 +80,7 @@ async def async_set_led_setting(self, enable: bool) -> bool:
return bool(not response.result) # pylint: disable=no-member

@_feature("update")
async def async_check_firmware_available(self) -> dict:
async def async_check_firmware_available(self) -> dict[str, Any]:
"""
Check asynchronously, if a firmware update is available for the device.
Expand All @@ -105,7 +107,7 @@ async def async_start_firmware_update(self) -> bool:
return bool(not update_firmware.result) # pylint: disable=no-member

@_feature("wifi1")
async def async_get_wifi_connected_station(self) -> dict:
async def async_get_wifi_connected_station(self) -> dict[str, Any]:
"""
Get wifi stations connected to the device asynchronously. This feature only works on devices, that announce the wifi1
feature.
Expand All @@ -119,7 +121,7 @@ async def async_get_wifi_connected_station(self) -> dict:
return self._message_to_dict(wifi_connected_proto)

@_feature("wifi1")
async def async_get_wifi_guest_access(self) -> dict:
async def async_get_wifi_guest_access(self) -> dict[str, Any]:
"""
Get details about wifi guest access asynchronously. This feature only works on devices, that announce the wifi1
feature.
Expand Down Expand Up @@ -149,7 +151,7 @@ async def async_set_wifi_guest_access(self, enable: bool) -> bool:
return bool(not response.result) # pylint: disable=no-member

@_feature("wifi1")
async def async_get_wifi_neighbor_access_points(self) -> dict:
async def async_get_wifi_neighbor_access_points(self) -> dict[str, Any]:
"""
Get wifi access point in the neighborhood asynchronously. This feature only works on devices, that announce the wifi1
feature.
Expand All @@ -163,7 +165,7 @@ async def async_get_wifi_neighbor_access_points(self) -> dict:
return self._message_to_dict(wifi_neighbor_aps)

@_feature("wifi1")
async def async_get_wifi_repeated_access_points(self):
async def async_get_wifi_repeated_access_points(self) -> dict[str, Any]:
"""
Get repeated wifi access point asynchronously. This feature only works on repeater devices, that announce the wifi1
feature.
Expand All @@ -177,7 +179,7 @@ async def async_get_wifi_repeated_access_points(self):
return self._message_to_dict(wifi_connected_proto)

@_feature("wifi1")
async def async_start_wps(self):
async def async_start_wps(self) -> bool:
"""
Start WPS push button configuration.
Expand Down
9 changes: 5 additions & 4 deletions devolo_plc_api/network/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
from __future__ import annotations

import asyncio
import time
from typing import Dict

from zeroconf import ServiceBrowser, ServiceStateChange, Zeroconf

from ..device import Device

_devices: Dict[str,
_devices: dict[str,
Device] = {}


async def async_discover_network() -> Dict[str, Device]:
async def async_discover_network() -> dict[str, Device]:
"""
Discover all devices that expose the devolo device API via mDNS asynchronous.
Expand All @@ -22,7 +23,7 @@ async def async_discover_network() -> Dict[str, Device]:
return _devices


def discover_network() -> Dict[str, Device]:
def discover_network() -> dict[str, Device]:
"""
Discover devices that expose the devolo device API via mDNS synchronous.
Expand Down
7 changes: 4 additions & 3 deletions devolo_plc_api/plcnet_api/plcnetapi.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from typing import Dict
from __future__ import annotations
from typing import Any

from httpx import AsyncClient

Expand All @@ -17,7 +18,7 @@ class PlcNetApi(Protobuf):
:param info: Information collected from the mDNS query
"""

def __init__(self, ip: str, session: AsyncClient, info: Dict):
def __init__(self, ip: str, session: AsyncClient, info: dict[str, Any]):
super().__init__()

self._ip = ip
Expand All @@ -30,7 +31,7 @@ def __init__(self, ip: str, session: AsyncClient, info: Dict):

self.password = "" # PLC API is not password protected.

async def async_get_network_overview(self) -> dict:
async def async_get_network_overview(self) -> dict[str, Any]:
"""
Get a PLC network overview.
Expand Down
Empty file added devolo_plc_api/py.typed
Empty file.
8 changes: 8 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v0.5.3] - 2021/10/18

### Changed

- Rework typing
- Mark package as typed
- Add Python 3.10 to CI

## [v0.5.2] - 2021/09/01

### Changed
Expand Down
12 changes: 7 additions & 5 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import shlex
from subprocess import check_call

from setuptools import find_packages, setup
from setuptools import setup
from setuptools.command.develop import develop

with open("README.md", "r") as fh:
Expand Down Expand Up @@ -29,15 +29,17 @@ def run(self):
long_description=long_description,
long_description_content_type="text/markdown",
url="https://github.com/2Fake/devolo_plc_api",
packages=find_packages(exclude=("tests*",
)),
packages=["devolo_plc_api"],
package_data={
"devolo_plc_api": ["py.typed"],
},
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
],
install_requires=[
"httpx>=0.14,<0.20",
"httpx>=0.14,<0.21",
"importlib-metadata;python_version<'3.8'",
"protobuf",
"zeroconf>=0.27.0",
Expand All @@ -56,5 +58,5 @@ def run(self):
],
},
setup_requires=["setuptools_scm"],
python_requires='>=3.7',
python_requires=">=3.7",
)
2 changes: 1 addition & 1 deletion tests/fixtures/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def mock_device(request):
device._session = Mock()
device._session.aclose = AsyncMock()
device._zeroconf = Mock()
device._zeroconf.close = lambda: None
device._zeroconf.close = Mock()
yield device


Expand Down
10 changes: 4 additions & 6 deletions tests/test_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,10 @@ def test_connect(self, mocker, mock_device):
assert spy_connect.call_count == 1

@pytest.mark.asyncio
async def test_async_disconnect(self, mocker, mock_device):
spy_zeroconf = mocker.spy(mock_device._zeroconf, "close")
spy_session = mocker.spy(mock_device._session, "aclose")
async def test_async_disconnect(self, mock_device):
await mock_device.async_disconnect()
assert spy_zeroconf.call_count == 1
assert spy_session.call_count == 1
assert mock_device._zeroconf.close.call_count == 1
assert mock_device._session.aclose.call_count == 1
assert not mock_device._connected

def test_disconnect(self, mocker, mock_device):
Expand Down Expand Up @@ -143,4 +141,4 @@ def test__state_change_removed(self, mock_device, mock_zeroconf):
def test_info_from_service_no_address(self, mock_device):
service_info = Mock()
service_info.addresses = None
assert mock_device.info_from_service(service_info) is None
assert mock_device.info_from_service(service_info) == {}

0 comments on commit ba19740

Please sign in to comment.