diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7a16000..8bb9e1b 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -8,7 +8,7 @@ - [ ] Version number in \_\_init\_\_.py was properly set. diff --git a/.github/workflows/convert_todos_to_issues.yml b/.github/workflows/convert_todos_to_issues.yml index f3296db..ddf9ce9 100644 --- a/.github/workflows/convert_todos_to_issues.yml +++ b/.github/workflows/convert_todos_to_issues.yml @@ -17,4 +17,4 @@ jobs: TOKEN: ${{ secrets.GITHUB_TOKEN }} LABEL: "# TODO:" COMMENT_MARKER: "#" - id: "todo" \ No newline at end of file + id: "todo" diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 60cb457..65304bd 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -4,6 +4,19 @@ on: [push] jobs: + format: + name: Check formatting + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Check formatting + uses: pre-commit/action@v2.0.0 + lint: name: Lint runs-on: ubuntu-latest @@ -19,7 +32,7 @@ jobs: python -m pip install --upgrade pip pip install flake8 flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --ignore=E303,W503 --statistics --exclude=.*,devolo_plc_api/*/devolo_idl_*.py + flake8 . --count --exit-zero --statistics - name: Lint with pylint run: | pip install pylint @@ -76,7 +89,7 @@ jobs: run: | python -m pip install --upgrade pip pip install wheel coveralls==1.10.0 - export COVERALLS_REPO_TOKEN=${{ secrets.COVERALLS_TOKEN }} + export COVERALLS_REPO_TOKEN=${{ secrets.COVERALLS_TOKEN }} coveralls - name: Clean up coverage uses: geekyeggo/delete-artifact@v1 diff --git a/.gitignore b/.gitignore index 14b4afa..195435c 100644 --- a/.gitignore +++ b/.gitignore @@ -138,6 +138,5 @@ dmypy.json ### PyCharm ### .idea - ### Testfile ### -test.py \ No newline at end of file +test.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..220d8ac --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: +- repo: https://github.com/pre-commit/mirrors-yapf + rev: '' + hooks: + - id: yapf +- repo: https://github.com/pycqa/isort + rev: '' + hooks: + - id: isort +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: '' + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace diff --git a/.yapfignore b/.yapfignore new file mode 100644 index 0000000..b8fe93e --- /dev/null +++ b/.yapfignore @@ -0,0 +1 @@ +devolo_plc_api/*/devolo_idl_*.py diff --git a/devolo_plc_api/clients/protobuf.py b/devolo_plc_api/clients/protobuf.py index c2afa3f..64c17e3 100644 --- a/devolo_plc_api/clients/protobuf.py +++ b/devolo_plc_api/clients/protobuf.py @@ -31,8 +31,10 @@ def __init__(self): def __getattr__(self, attr: str) -> Callable: """ Catch attempts to call methods synchronously. """ + def method(*args, **kwargs): return self._loop.run_until_complete(getattr(self, async_method)(*args, **kwargs)) + async_method = f"async_{attr}" if hasattr(self.__class__, async_method): return method @@ -48,9 +50,7 @@ async def _async_get(self, sub_url: str, timeout: float = TIMEOUT) -> Response: url = f"{self.url}{sub_url}" self._logger.debug("Getting from %s", url) try: - return await self._session.get(url, - auth=DigestAuth(self._user, self._password), - timeout=timeout) + return await self._session.get(url, auth=DigestAuth(self._user, self._password), timeout=timeout) except TypeError: raise DevicePasswordProtected("The used password is wrong.") from None @@ -59,10 +59,7 @@ async def _async_post(self, sub_url: str, content: bytes, timeout: float = TIMEO url = f"{self.url}{sub_url}" self._logger.debug("Posting to %s", url) try: - return await self._session.post(url, - auth=DigestAuth(self._user, self._password), - content=content, - timeout=timeout) + return await self._session.post(url, auth=DigestAuth(self._user, self._password), content=content, timeout=timeout) except TypeError: raise DevicePasswordProtected("The used password is wrong.") from None diff --git a/devolo_plc_api/device.py b/devolo_plc_api/device.py index b928b30..c429b8a 100644 --- a/devolo_plc_api/device.py +++ b/devolo_plc_api/device.py @@ -36,7 +36,10 @@ def __init__(self, ip: str, password: Optional[str] = None, zeroconf_instance: O self.device = None self.plcnet = None - self._info: Dict = {"_dvl-plcnetapi._tcp.local.": {}, "_dvl-deviceapi._tcp.local.": {}} + self._info: Dict = { + "_dvl-plcnetapi._tcp.local.": {}, + "_dvl-deviceapi._tcp.local.": {}, + } self._logger = logging.getLogger(self.__class__.__name__) self._zeroconf_instance = zeroconf_instance @@ -77,10 +80,7 @@ async def _get_device_info(self): self.mt_number = self._info[service_type].get("MT", 0) self.product = self._info[service_type].get("Product", "") - self.device = DeviceApi(ip=self.ip, - session=self._session, - info=self._info[service_type], - password=self.password) + self.device = DeviceApi(ip=self.ip, session=self._session, info=self._info[service_type], password=self.password) async def _get_plcnet_info(self): """ Get information from the devolo PlcNet API. """ @@ -93,9 +93,7 @@ async def _get_plcnet_info(self): self.mac = self._info[service_type]['PlcMacAddress'] self.technology = self._info[service_type].get("PlcTechnology", "") - self.plcnet = PlcNetApi(ip=self.ip, - session=self._session, - info=self._info[service_type]) + self.plcnet = PlcNetApi(ip=self.ip, session=self._session, info=self._info[service_type]) async def _get_zeroconf_info(self, service_type: str): """ Browse for the desired mDNS service types and query them. """ diff --git a/devolo_plc_api/device_api/deviceapi.py b/devolo_plc_api/device_api/deviceapi.py index e5ff90c..da3d930 100644 --- a/devolo_plc_api/device_api/deviceapi.py +++ b/devolo_plc_api/device_api/deviceapi.py @@ -19,11 +19,7 @@ class DeviceApi(Protobuf): :param password: Password of the device """ - def __init__(self, - ip: str, - session: AsyncClient, - info: Dict, - password: Optional[str]): + def __init__(self, ip: str, session: AsyncClient, info: Dict, password: Optional[str]): super().__init__() self._ip = ip self._port = info['Port'] @@ -38,13 +34,17 @@ def __init__(self, def _feature(feature: str): # type: ignore # pylint: disable=no-self-argument """ Decorator to filter unsupported features before querying the device. """ + def feature_decorator(method: Callable): + def wrapper(self, *args, **kwargs): if feature in self.features: return method(self, *args, **kwargs) else: raise FeatureNotSupported(f"The device does not support {method}.") + return wrapper + return feature_decorator @_feature("led") diff --git a/devolo_plc_api/plcnet_api/plcnetapi.py b/devolo_plc_api/plcnet_api/plcnetapi.py index 0e27e52..c6c07af 100644 --- a/devolo_plc_api/plcnet_api/plcnetapi.py +++ b/devolo_plc_api/plcnet_api/plcnetapi.py @@ -17,10 +17,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): super().__init__() self._ip = ip self._mac = info['PlcMacAddress'] diff --git a/example_async.py b/example_async.py index 1ba707e..e2ad754 100644 --- a/example_async.py +++ b/example_async.py @@ -2,7 +2,6 @@ from devolo_plc_api.device import Device - # IP of the device to query IP = "192.168.0.10" diff --git a/example_sync.py b/example_sync.py index c391c1f..c21139f 100644 --- a/example_sync.py +++ b/example_sync.py @@ -1,6 +1,5 @@ from devolo_plc_api.device import Device - # IP of the device to query IP = "192.168.0.10" @@ -68,7 +67,6 @@ def run(): # Start WPS push button configuration. If WPS was started successfully, True is returned, otherwise False. print("WPS started" if dpa.device.start_wps() else "WPS start failed") - # Get PLC network overview with enriched information like firmware version, # {'network': # {'devices': diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 5434bbe..0000000 --- a/mypy.ini +++ /dev/null @@ -1,11 +0,0 @@ -[mypy] -ignore_missing_imports = True - -[mypy-devolo_plc_api.plcnet_api.devolo_idl_proto_plcnetapi_getnetworkoverview_pb2] -ignore_errors = True - -[mypy-devolo_plc_api.device_api.devolo_idl_proto_deviceapi_updatefirmware_pb2] -ignore_errors = True - -[mypy-devolo_plc_api.device_api.devolo_idl_proto_deviceapi_wifinetwork_pb2] -ignore_errors = True \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 9af7e6f..30bd012 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,34 @@ -[aliases] -test=pytest \ No newline at end of file +[flake8] +max-complexity = 10 +max-cognitive-complexity = 5 +max-line-length = 127 +ignore = E303,W503 +exclude = .*,devolo_plc_api/*/devolo_idl_*.py + +[isort] +filter_files = True +ignore_whitespace = True +line_length = 127 +multi_line_output = 1 +order_by_type = True +skip_glob = *_pb2.py + +[mypy] +ignore_missing_imports = True + +[mypy-devolo_plc_api.plcnet_api.devolo_idl_proto_plcnetapi_getnetworkoverview_pb2] +ignore_errors = True + +[mypy-devolo_plc_api.device_api.devolo_idl_proto_deviceapi_updatefirmware_pb2] +ignore_errors = True + +[mypy-devolo_plc_api.device_api.devolo_idl_proto_deviceapi_wifinetwork_pb2] +ignore_errors = True + +[yapf] +blank_line_before_nested_class_or_def = True +column_limit = 127 +force_multiline_dict = True +split_all_comma_separated_values = True +split_before_arithmetic_operator = True +split_complex_comprehension = True diff --git a/setup.py b/setup.py index 9b3c299..5ce65eb 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,28 @@ -import setuptools +import shlex +from subprocess import check_call -from devolo_plc_api import __version__ +from setuptools import find_packages, setup +from setuptools.command.develop import develop +from devolo_plc_api import __version__ with open("README.md", "r") as fh: long_description = fh.read() -setuptools.setup( + +# Create post develop command class for hooking into the python setup process +# This command will run after dependencies are installed +class PostDevelopCommand(develop): + + def run(self): + try: + check_call(shlex.split("pre-commit install")) + except Exception: + print("Unable to run 'pre-commit install'") + develop.run(self) + + +setup( name="devolo_plc_api", version=__version__, author="Markus Bong, Guido Schmitz", @@ -15,7 +31,7 @@ long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/2Fake/devolo_plc_api", - packages=setuptools.find_packages(exclude=("tests*",)), + packages=find_packages(exclude=("tests*")), classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", @@ -27,13 +43,16 @@ "zeroconf>=0.27.0", ], extras_require={ + "dev": [ + "pre-commit", + ], "test": [ "asynctest;python_version<'3.8'", "pytest", "pytest-asyncio", "pytest-cov", "pytest-mock", - ] + ], }, python_requires='>=3.7', ) diff --git a/tests/conftest.py b/tests/conftest.py index 91be08e..ec3300b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,13 +3,12 @@ import pytest - -pytest_plugins = ['tests.fixtures.device', - 'tests.fixtures.device_api', - 'tests.fixtures.plcnet_api', - 'tests.fixtures.protobuf', - ] - +pytest_plugins = [ + 'tests.fixtures.device', + 'tests.fixtures.device_api', + 'tests.fixtures.plcnet_api', + 'tests.fixtures.protobuf', +] file = pathlib.Path(__file__).parent / "test_data.json" with file.open("r") as fh: diff --git a/tests/fixtures/device_api.py b/tests/fixtures/device_api.py index a83ed1c..5cc1ecd 100644 --- a/tests/fixtures/device_api.py +++ b/tests/fixtures/device_api.py @@ -2,9 +2,10 @@ from unittest.mock import patch import pytest -from devolo_plc_api.device_api.deviceapi import DeviceApi from httpx import AsyncClient, Response +from devolo_plc_api.device_api.deviceapi import DeviceApi + try: from unittest.mock import AsyncMock except ImportError: @@ -18,10 +19,7 @@ def device_api(request, feature): patch("asyncio.get_running_loop", asyncio.new_event_loop): asyncio.new_event_loop() request.cls.device_info["_dvl-deviceapi._tcp.local."]["Features"] = feature - yield DeviceApi(request.cls.ip, - AsyncClient(), - request.cls.device_info["_dvl-deviceapi._tcp.local."], - "password") + yield DeviceApi(request.cls.ip, AsyncClient(), request.cls.device_info["_dvl-deviceapi._tcp.local."], "password") @pytest.fixture() diff --git a/tests/fixtures/plcnet_api.py b/tests/fixtures/plcnet_api.py index b87c87e..030a739 100644 --- a/tests/fixtures/plcnet_api.py +++ b/tests/fixtures/plcnet_api.py @@ -2,9 +2,10 @@ from unittest.mock import patch import pytest -from devolo_plc_api.plcnet_api.plcnetapi import PlcNetApi from httpx import AsyncClient, Response +from devolo_plc_api.plcnet_api.plcnetapi import PlcNetApi + try: from unittest.mock import AsyncMock except ImportError: @@ -17,9 +18,7 @@ def plcnet_api(request): patch("devolo_plc_api.clients.protobuf.Protobuf._async_post", new=AsyncMock(return_value=Response)), \ patch("asyncio.get_running_loop", asyncio.new_event_loop): asyncio.new_event_loop() - yield PlcNetApi(request.cls.ip, - AsyncClient(), - request.cls.device_info["_dvl-plcnetapi._tcp.local."]) + yield PlcNetApi(request.cls.ip, AsyncClient(), request.cls.device_info["_dvl-plcnetapi._tcp.local."]) @pytest.fixture() diff --git a/tests/mocks/mock_zeroconf.py b/tests/mocks/mock_zeroconf.py index 3f15387..a1621ce 100644 --- a/tests/mocks/mock_zeroconf.py +++ b/tests/mocks/mock_zeroconf.py @@ -10,6 +10,7 @@ class Zeroconf: + def get_service_info(self, service_type, name): service_info = ServiceInfo(service_type, name) service_info.addresses = [socket.inet_aton(test_data['ip'])] diff --git a/tests/stubs/protobuf.py b/tests/stubs/protobuf.py index 0003648..d96b1d6 100644 --- a/tests/stubs/protobuf.py +++ b/tests/stubs/protobuf.py @@ -11,6 +11,7 @@ class StubProtobuf(Protobuf): + def __init__(self): self._logger = logging.getLogger("ProtobufMock") self._loop = asyncio.new_event_loop() diff --git a/tests/test_data.json b/tests/test_data.json index e845622..796683d 100644 --- a/tests/test_data.json +++ b/tests/test_data.json @@ -20,4 +20,4 @@ } }, "ip": "192.168.0.10" -} \ No newline at end of file +} diff --git a/tests/test_deviceapi.py b/tests/test_deviceapi.py index 5b0ca25..20f2bb3 100644 --- a/tests/test_deviceapi.py +++ b/tests/test_deviceapi.py @@ -9,11 +9,14 @@ from asynctest import CoroutineMock as AsyncMock from devolo_plc_api.device_api.devolo_idl_proto_deviceapi_ledsettings_pb2 import LedSettingsGet, LedSettingsSetResponse -from devolo_plc_api.device_api.devolo_idl_proto_deviceapi_wifinetwork_pb2 import ( - WifiConnectedStationsGet, WifiGuestAccessGet, WifiGuestAccessSetResponse, WifiNeighborAPsGet, WifiRepeatedAPsGet, - WifiWpsPbcStart) -from devolo_plc_api.exceptions.feature import FeatureNotSupported from devolo_plc_api.device_api.devolo_idl_proto_deviceapi_updatefirmware_pb2 import UpdateFirmwareCheck, UpdateFirmwareStart +from devolo_plc_api.device_api.devolo_idl_proto_deviceapi_wifinetwork_pb2 import (WifiConnectedStationsGet, + WifiGuestAccessGet, + WifiGuestAccessSetResponse, + WifiNeighborAPsGet, + WifiRepeatedAPsGet, + WifiWpsPbcStart) +from devolo_plc_api.exceptions.feature import FeatureNotSupported class TestDeviceApi: diff --git a/tests/test_profobuf.py b/tests/test_profobuf.py index 482472e..c509285 100644 --- a/tests/test_profobuf.py +++ b/tests/test_profobuf.py @@ -7,6 +7,7 @@ class TestProtobuf: + def test_attribute_error(self, mock_protobuf): with pytest.raises(AttributeError): mock_protobuf.test()