Skip to content

Commit

Permalink
Fixed Compatiblity Issues with Python 3.9 (#18)
Browse files Browse the repository at this point in the history
According to #17, the code does not work with Python 3.9. I have updated tox to include 3.9 and also 3.12. I dropped 3.8 because it never worked and support for python will end in October 2024.

This PR contains code changes to make it compatible with 3.9. Most of the changes fix the new typing syntax. In api.py, the decorator for re-login needs to be moved to a function instead of a static method.

- Updated testing environment (tox and github).
- Fixed compatibility issues with Python 3.9.
- Added tox path to gitignore.
- Updated README with tox information.
  • Loading branch information
stegm authored Nov 13, 2024
1 parent 10093f0 commit 21dcb4e
Show file tree
Hide file tree
Showing 9 changed files with 64 additions and 36 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11"]
python-version: ["3.9", "3.10", "3.11", "3.12"]

steps:
- uses: actions/checkout@v2
Expand Down Expand Up @@ -36,7 +36,7 @@ jobs:
run: |
pipenv run build
- name: Upload packages to github
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/*
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ __pycache__
*.egg-info
build/
dist/
.tox/
pip-wheel-metadata/
.env.local
coverage.xml
Expand Down
9 changes: 9 additions & 0 deletions doc/developer.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,17 @@ tox

Available environments:

* `py39-pydantic1` - Python 3.9 with Pydantic 1.x
* `py39-pydantic2` - Python 3.9 with Pydantic 2.x
* `py310-pydantic1` - Python 3.10 with Pydantic 1.x
* `py310-pydantic2` - Python 3.10 with Pydantic 2.x
* `py311-pydantic1` - Python 3.11 with Pydantic 1.x
* `py311-pydantic2` - Python 3.11 with Pydantic 2.x
* `py312-pydantic1` - Python 3.12 with Pydantic 1.x
* `py312-pydantic2` - Python 3.12 with Pydantic 2.x

If `tox` should use `pyenv`, the package `tox-pyenv-redux` must be installed manually.
It cannot be installed in pipenv dev, because it is incompatible with github actions.

## Running smoke tests

Expand Down
28 changes: 14 additions & 14 deletions pykoplenti/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,20 @@ def __init__(self, status_code: int, error: str):
self.error = error


def _relogin(fn):
"""Decorator for automatic re-login if session was expired."""

@functools.wraps(fn)
async def _wrapper(self: "ApiClient", *args, **kwargs):
with contextlib.suppress(AuthenticationException, NotAuthorizedException):
return await fn(self, *args, **kwargs)
_logger.debug("Request failed - try to re-login")
await self._login()
return await fn(self, *args, **kwargs)

return _wrapper


class ApiClient(contextlib.AbstractAsyncContextManager):
"""Client for the REST-API of Kostal Plenticore inverters.
Expand Down Expand Up @@ -369,20 +383,6 @@ async def _check_response(self, resp: ClientResponse):
# we got an undocumented status code
raise ApiException(f"Unknown API response [{resp.status}] - {error}")

@staticmethod
def _relogin(fn):
"""Decorator for automatic re-login if session was expired."""

@functools.wraps(fn)
async def _wrapper(self, *args, **kwargs):
with contextlib.suppress(AuthenticationException, NotAuthorizedException):
return await fn(self, *args, **kwargs)
_logger.debug("Request failed - try to re-login")
await self._login()
return await fn(self, *args, **kwargs)

return _wrapper

async def logout(self):
"""Logs the current user out."""
self._key = None
Expand Down
10 changes: 5 additions & 5 deletions pykoplenti/model.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import datetime
from typing import Final, Iterator, Mapping
from typing import Final, Iterator, Mapping, Optional

import pydantic
from pydantic import BaseModel, Field
Expand Down Expand Up @@ -78,11 +78,11 @@ def __repr__(self):
class SettingsData(BaseModel):
"""Represents a single settings data."""

min: str | None
max: str | None
default: str | None
min: Optional[str]
max: Optional[str]
default: Optional[str]
access: str
unit: str | None
unit: Optional[str]
id: str
type: str

Expand Down
6 changes: 4 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ classifiers =
Intended Audience :: Developers
License :: OSI Approved :: Apache Software License
Programming Language :: Python :: 3
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Programming Language :: Python :: 3.12
Topic :: Software Development :: Libraries

[options]
Expand Down
4 changes: 2 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Callable
from typing import Any, Callable, Union
from unittest.mock import AsyncMock, MagicMock

from aiohttp import ClientResponse, ClientSession
Expand Down Expand Up @@ -30,7 +30,7 @@ def client_response_factory(
) -> Callable[[int, Any], MagicMock]:
"""Provides a factory to add responses to a ClientSession."""

def factory(status: int = 200, json: list[Any] | dict[Any, Any] | None = None):
def factory(status: int = 200, json: Union[list[Any], dict[Any, Any], None] = None):
response = MagicMock(spec_set=ClientResponse, name="request Mock")
response.status = status
if json is not None:
Expand Down
36 changes: 26 additions & 10 deletions tests/test_extendedapiclient.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Callable, Iterable
from typing import Any, Callable, Iterable, Union
from unittest.mock import ANY, MagicMock, call

import pytest
Expand Down Expand Up @@ -40,7 +40,9 @@ class TestVirtualProcessDataValuesDcSum:
async def test_virtual_process_data(
self,
pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
client_response_factory: Callable[
[int, Union[list[Any], dict[Any, Any]]], MagicMock
],
websession: MagicMock,
):
"""Test virtual process data for PV power if depencies are present."""
Expand Down Expand Up @@ -70,7 +72,9 @@ async def test_virtual_process_data(
async def test_virtual_process_data_value(
self,
pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
client_response_factory: Callable[
[int, Union[list[Any], dict[Any, Any]]], MagicMock
],
websession: MagicMock,
):
"""Test virtual process data for PV power."""
Expand Down Expand Up @@ -142,7 +146,9 @@ class TestVirtualProcessDataValuesEnergyToGrid:
async def test_virtual_process_data(
self,
pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
client_response_factory: Callable[
[int, Union[list[Any], dict[Any, Any]]], MagicMock
],
websession: MagicMock,
scope: str,
):
Expand Down Expand Up @@ -183,7 +189,9 @@ async def test_virtual_process_data(
async def test_virtual_process_data_value(
self,
pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
client_response_factory: Callable[
[int, Union[list[Any], dict[Any, Any]]], MagicMock
],
websession: MagicMock,
scope: str,
):
Expand Down Expand Up @@ -269,10 +277,12 @@ async def test_virtual_process_data_value(
@pytest.mark.asyncio
async def test_virtual_process_data_no_dc_sum(
pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
client_response_factory: Callable[
[int, Union[list[Any], dict[Any, Any]]], MagicMock
],
websession: MagicMock,
):
"""Test if no virtual process data is present if dependecies are missing."""
"""Test if no virtual process data is present if dependencies are missing."""
client_response_factory(
200,
[
Expand All @@ -297,7 +307,9 @@ async def test_virtual_process_data_no_dc_sum(
@pytest.mark.asyncio
async def test_virtual_process_data_and_normal_process_data(
pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
client_response_factory: Callable[
[int, Union[list[Any], dict[Any, Any]]], MagicMock
],
websession: MagicMock,
):
"""Test if virtual and non-virtual process values can be requested."""
Expand Down Expand Up @@ -362,7 +374,9 @@ async def test_virtual_process_data_and_normal_process_data(
@pytest.mark.asyncio
async def test_virtual_process_data_not_all_requested(
pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
client_response_factory: Callable[
[int, Union[list[Any], dict[Any, Any]]], MagicMock
],
websession: MagicMock,
):
"""Test if not all available virtual process data are requested."""
Expand Down Expand Up @@ -429,7 +443,9 @@ async def test_virtual_process_data_not_all_requested(
@pytest.mark.asyncio
async def test_virtual_process_data_multiple_requested(
pykoplenti_extended_client: pykoplenti.ExtendedApiClient,
client_response_factory: Callable[[int, list[Any] | dict[Any, Any]], MagicMock],
client_response_factory: Callable[
[int, Union[list[Any], dict[Any, Any]]], MagicMock
],
websession: MagicMock,
):
"""Test if multiple virtual process data are requested."""
Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py3{10,11}-pydantic{1,2}
envlist = py3{9,10,11,12}-pydantic{1,2}

[testenv]
description = Executes pytest
Expand Down

0 comments on commit 21dcb4e

Please sign in to comment.