From b8ee83f063ec1f819727dea7f181945475cb57a7 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Thu, 12 May 2022 15:40:52 +0000 Subject: [PATCH 1/4] Add mypy to project --- .pre-commit-config.yaml | 5 +++++ pyproject.toml | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c985881..85f8f1a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -104,6 +104,11 @@ repos: language: system types: [python] entry: poetry run isort + - id: mypy + name: ๐Ÿ†Ž Static type checking using mypy + language: system + types: [python] + entry: poetry run mypy - id: no-commit-to-branch name: ๐Ÿ›‘ Don't commit to main branch language: system diff --git a/pyproject.toml b/pyproject.toml index 551c64f..a6b7a15 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,6 +82,43 @@ source = ["gridnet"] profile = "black" multi_line_output = 3 +[tool.mypy] +# Specify the target platform details in config, so your developers are +# free to run mypy on Windows, Linux, or macOS and get consistent +# results. +platform = "linux" +python_version = 3.9 + +# flake8-mypy expects the two following for sensible formatting +show_column_numbers = true + +# show error messages from unrelated files +follow_imports = "normal" + +# suppress errors about unsatisfied imports +ignore_missing_imports = true + +# be strict +check_untyped_defs = true +disallow_any_generics = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_defs = true +disallow_untyped_decorators = false # thanks backoff :( +no_implicit_optional = true +no_implicit_reexport = true +strict_optional = true +warn_incomplete_stub = true +warn_no_return = true +warn_redundant_casts = true +warn_return_any = true +warn_unused_configs = true +warn_unused_ignores = true + +# No incremental mode +cache_dir = "/dev/null" + [tool.pylint.BASIC] good-names = [ "_", @@ -123,5 +160,5 @@ paths = ["gridnet"] verbose = true [build-system] -requires = ["setuptools", "poetry-core>=1.0.0"] +requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" From e6cad735dfb6133bb60d17269604727860c2e758 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Thu, 12 May 2022 15:44:49 +0000 Subject: [PATCH 2/4] Add typing github workflow --- .github/workflows/typing.yaml | 52 +++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/typing.yaml diff --git a/.github/workflows/typing.yaml b/.github/workflows/typing.yaml new file mode 100644 index 0000000..03349dd --- /dev/null +++ b/.github/workflows/typing.yaml @@ -0,0 +1,52 @@ +--- +name: Typing + +# yamllint disable-line rule:truthy +on: + push: + pull_request: + workflow_dispatch: + +jobs: + mypy: + name: mypy on Python ${{ matrix.python }} + runs-on: ubuntu-latest + strategy: + matrix: + python: ["3.9", "3.10"] + steps: + - name: โคต๏ธ Check out code from GitHub + uses: actions/checkout@v2.4.0 + - name: ๐Ÿ— Set up Python ${{ matrix.python }} + id: python + uses: actions/setup-python@v2.3.2 + with: + python-version: ${{ matrix.python }} + - name: ๐Ÿ— Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + - name: โคต๏ธ Restore cached Python PIP packages + uses: actions/cache@v2.1.7 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: pip-${{ runner.os }}-v1-${{ steps.python.outputs.python-version }}-${{ hashFiles('.github/workflows/requirements.txt') }} + restore-keys: | + pip-${{ runner.os }}-v1-${{ steps.python.outputs.python-version }}- + - name: ๐Ÿ— Install workflow dependencies + run: | + pip install -r .github/workflows/requirements.txt + poetry config virtualenvs.create true + poetry config virtualenvs.in-project true + - name: โคต๏ธ Restore cached Python virtual environment + id: cached-poetry-dependencies + uses: actions/cache@v2.1.7 + with: + path: .venv + key: venv-${{ runner.os }}-v1-${{ steps.python.outputs.python-version }}-${{ hashFiles('poetry.lock') }} + restore-keys: | + venv-${{ runner.os }}-v1-${{ steps.python.outputs.python-version }}- + - name: ๐Ÿ— Install dependencies + run: poetry install --no-interaction + - name: ๐Ÿš€ Run mypy + run: poetry run mypy gridnet tests From 303083eb20206841bcf9c35b08ac6cab6e0ceb3e Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Thu, 12 May 2022 15:45:16 +0000 Subject: [PATCH 3/4] Apply stricter typing to the project --- gridnet/gridnet.py | 8 ++++---- gridnet/models.py | 16 ++++++++-------- tests/__init__.py | 2 +- tests/test_gridnet.py | 19 ++++++++++--------- tests/test_models.py | 5 +++-- 5 files changed, 26 insertions(+), 24 deletions(-) diff --git a/gridnet/gridnet.py b/gridnet/gridnet.py index d7e2f87..49fa72d 100644 --- a/gridnet/gridnet.py +++ b/gridnet/gridnet.py @@ -22,7 +22,7 @@ class GridNet: """Main class for handling connections with the devices.""" host: str - request_timeout: int = 10 + request_timeout: float = 10.0 session: aiohttp.ClientSession | None = None _close_session: bool = False @@ -32,8 +32,8 @@ async def _request( uri: str, *, method: str = hdrs.METH_GET, - data: dict | None = None, - ) -> dict[str, Any]: + data: dict[str, Any] | None = None, + ) -> Any: """Handle a request to the device. Args: @@ -115,7 +115,7 @@ async def __aenter__(self) -> GridNet: """ return self - async def __aexit__(self, *_exc_info) -> None: + async def __aexit__(self, *_exc_info: str) -> None: """Async exit. Args: diff --git a/gridnet/models.py b/gridnet/models.py index b92743c..9a9511d 100644 --- a/gridnet/models.py +++ b/gridnet/models.py @@ -25,7 +25,7 @@ def from_dict(data: dict[str, Any]) -> SmartBridge: """ data = data["elec"] - def convert(value): + def convert(value: float) -> float: """Convert the unit of measurement. Args: @@ -35,7 +35,7 @@ def convert(value): Value in kWh rounded with 1 decimal. """ value = value / 1000 - return round(value, 1) + return float(round(value, 1)) return SmartBridge( power_flow=data["power"]["now"].get("value"), @@ -67,10 +67,10 @@ def from_dict(data: dict[str, Any]) -> Device: """ return Device( - n2g_id=data.get("id"), - model=data.get("model"), - batch=data.get("batch"), - firmware=data.get("fw"), - hardware=data.get("hw"), - manufacturer=data.get("mf"), + n2g_id=data["id"], + model=data["model"], + batch=data["batch"], + firmware=data["fw"], + hardware=data["hw"], + manufacturer=data["mf"], ) diff --git a/tests/__init__.py b/tests/__init__.py index e1cc472..0f7eb78 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,7 +2,7 @@ import os -def load_fixtures(filename): +def load_fixtures(filename: str) -> str: """Load a fixture.""" path = os.path.join(os.path.dirname(__file__), "fixtures", filename) with open(path, encoding="utf-8") as fptr: diff --git a/tests/test_gridnet.py b/tests/test_gridnet.py index e8a1bf2..b42bd39 100644 --- a/tests/test_gridnet.py +++ b/tests/test_gridnet.py @@ -5,6 +5,7 @@ import aiohttp import pytest +from aresponses import Response, ResponsesMockServer from gridnet import GridNet from gridnet.exceptions import GridNetConnectionError, GridNetError @@ -13,7 +14,7 @@ @pytest.mark.asyncio -async def test_json_request(aresponses): +async def test_json_request(aresponses: ResponsesMockServer) -> None: """Test JSON response is handled correctly.""" aresponses.add( "example.com", @@ -32,7 +33,7 @@ async def test_json_request(aresponses): @pytest.mark.asyncio -async def test_internal_session(aresponses): +async def test_internal_session(aresponses: ResponsesMockServer) -> None: """Test JSON response is handled correctly.""" aresponses.add( "example.com", @@ -49,10 +50,10 @@ async def test_internal_session(aresponses): @pytest.mark.asyncio -async def test_timeout(aresponses): +async def test_timeout(aresponses: ResponsesMockServer) -> None: """Test request timeout from the API.""" # Faking a timeout by sleeping - async def response_handler(_): + async def response_handler(_: aiohttp.ClientResponse) -> Response: await asyncio.sleep(0.2) return aresponses.Response( body="Goodmorning!", text=load_fixtures("smartbridge.json") @@ -67,7 +68,7 @@ async def response_handler(_): @pytest.mark.asyncio -async def test_client_error(): +async def test_client_error() -> None: """Test request client error from the API.""" async with aiohttp.ClientSession() as session: client = GridNet(host="example.com", session=session) @@ -79,7 +80,7 @@ async def test_client_error(): @pytest.mark.asyncio @pytest.mark.parametrize("status", [401, 403]) -async def test_http_error401(aresponses, status): +async def test_http_error401(aresponses: ResponsesMockServer, status: int) -> None: """Test HTTP 401 response handling.""" aresponses.add( "example.com", @@ -95,7 +96,7 @@ async def test_http_error401(aresponses, status): @pytest.mark.asyncio -async def test_http_error400(aresponses): +async def test_http_error400(aresponses: ResponsesMockServer) -> None: """Test HTTP 404 response handling.""" aresponses.add( "example.com", @@ -111,7 +112,7 @@ async def test_http_error400(aresponses): @pytest.mark.asyncio -async def test_http_error500(aresponses): +async def test_http_error500(aresponses: ResponsesMockServer) -> None: """Test HTTP 500 response handling.""" aresponses.add( "example.com", @@ -130,7 +131,7 @@ async def test_http_error500(aresponses): @pytest.mark.asyncio -async def test_no_success(aresponses): +async def test_no_success(aresponses: ResponsesMockServer) -> None: """Test a message without a success message throws.""" aresponses.add( "example.com", diff --git a/tests/test_models.py b/tests/test_models.py index 08dfc06..8b21e04 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,6 +1,7 @@ """Test the models.""" import aiohttp import pytest +from aresponses import ResponsesMockServer from gridnet import Device, GridNet, SmartBridge @@ -8,7 +9,7 @@ @pytest.mark.asyncio -async def test_device(aresponses): +async def test_device(aresponses: ResponsesMockServer) -> None: """Test request from the device - Device object.""" aresponses.add( "example.com", @@ -33,7 +34,7 @@ async def test_device(aresponses): @pytest.mark.asyncio -async def test_smartbridge(aresponses): +async def test_smartbridge(aresponses: ResponsesMockServer) -> None: """Test request from the device - SmartBridge object.""" aresponses.add( "example.com", From f38d93983742784b78d4f0c3b0480eb8be8b6ee8 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Thu, 12 May 2022 15:46:22 +0000 Subject: [PATCH 4/4] Add badges to documentation --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 966eaad..38efc06 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ [![Code Quality][code-quality-shield]][code-quality] [![Maintainability][maintainability-shield]][maintainability-url] [![Code Coverage][codecov-shield]][codecov-url] + [![Build Status][build-shield]][build-url] +[![Typing Status][typing-shield]][typing-url] Asynchronous Python client for Net2Grid devices. @@ -175,6 +177,8 @@ SOFTWARE. [project-stage-shield]: https://img.shields.io/badge/project%20stage-experimental-yellow.svg [pypi]: https://pypi.org/project/gridnet/ [python-versions-shield]: https://img.shields.io/pypi/pyversions/gridnet +[typing-shield]: https://github.com/klaasnicolaas/python-gridnet/actions/workflows/typing.yaml/badge.svg +[typing-url]: https://github.com/klaasnicolaas/python-gridnet/actions/workflows/typing.yaml [releases-shield]: https://img.shields.io/github/release/klaasnicolaas/python-gridnet.svg [releases]: https://github.com/klaasnicolaas/python-gridnet/releases [stars-shield]: https://img.shields.io/github/stars/klaasnicolaas/python-gridnet.svg