Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ on:

jobs:
test:
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest]
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"]
include:
- os: windows-latest
python-version: "3.12"
steps:
- uses: actions/checkout@v4

Expand All @@ -22,23 +26,37 @@ jobs:
with:
python-version: ${{ matrix.python-version }}

- name: Install uv
uses: astral-sh/setup-uv@v7

- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install coverage coveralls
python -m pip install -r dev-requirements.txt
python -m pip install -e .
uv sync --dev

- name: Run tests
run: |
tox
python tests/test_import.py
coverage run --source=pyfronius --module pytest
uv run tox
uv run tests/test_import.py
uv run coverage run --source=pyfronius --module pytest

- name: Upload coverage to Coveralls
if: success()
- name: Upload coverage data to coveralls.io
run: |
uv pip install --upgrade coveralls || true
uv run coveralls || true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
coverage report
coveralls
COVERALLS_FLAG_NAME: ${{ matrix.os }}-${{ matrix.python-version }}
COVERALLS_PARALLEL: true

coveralls:
name: Indicate completion to coveralls.io
needs: [test]
runs-on: ubuntu-latest
container: python:3-slim
steps:
- name: Install coveralls
run: pip3 install --upgrade coveralls
- name: Finished
run: coveralls --finish || true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ jobs:
steps:
- uses: actions/checkout@v4

- name: Set up Python 3.11
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: "3.13"

- name: Install dependencies
run: |
Expand Down
12 changes: 0 additions & 12 deletions Pipfile

This file was deleted.

3 changes: 0 additions & 3 deletions dev-requirements.txt

This file was deleted.

18 changes: 18 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,21 @@ Repository = "https://github.com/nielstron/pyfronius/"

[tool.setuptools.packages.find]
include = ["pyfronius*"]

[tool.uv.workspace]
members = [
".",
]

[tool.uv.sources]
pyfronius = { workspace = true }

[dependency-groups]
dev = [
"aiounittest>=1.5.0",
"coverage>=7.6.1",
"pre-commit>=3.5.0",
"pyfronius",
"pytest>=8.3.5",
"tox>=4.25.0",
]
7 changes: 7 additions & 0 deletions tests/test_structure/fronius_mock_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,17 @@ def translate_path(self, path):
path = path.split("#", 1)[0]
# Don't forget explicit trailing slash when normalizing. Issue17324
trailing_slash = path.rstrip().endswith("/")
# Unquote first (standard behavior)
try:
path = urllib.parse.unquote(path, errors="surrogatepass")
except UnicodeDecodeError:
path = urllib.parse.unquote(path)
# After unquoting, convert URL query parameters to Windows-safe filename format:
# Split at ? and URL-encode the query string portion
if "?" in path:
base, query = path.split("?", 1)
# URL encode the query string, keeping = safe for readability
path = f"{base}___{query}"
path = posixpath.normpath(path)
words = path.split("/")
words = filter(None, words)
Expand Down
11 changes: 7 additions & 4 deletions tests/test_web_v0.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# general requirements
import unittest

from tests.util import AsyncTestCaseSetup
from .util import AsyncTestCaseSetup, _get_unused_port, ADDRESS
from .test_structure.server_control import Server
from .test_structure.fronius_mock_server import FroniusRequestHandler, FroniusServer
from http.server import SimpleHTTPRequestHandler
Expand All @@ -22,18 +22,21 @@
GET_INVERTER_INFO,
)

ADDRESS = "localhost"


class NoFroniusWebTest(AsyncTestCaseSetup):
server = None
api_version = pyfronius.API_VERSION.V0
server_control = None
port = 0
url = "http://localhost:80"
url = f"http://{ADDRESS}:80"
session = None
fronius = None

async def setUp(self):
# Pick an unused port to ensure the connection attempt fails deterministically
self.port = _get_unused_port()
self.url = "http://{}:{}".format(ADDRESS, self.port)

async def test_no_server(self):
# set up a fronius client and aiohttp session
self.session = aiohttp.ClientSession()
Expand Down
11 changes: 7 additions & 4 deletions tests/test_web_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import unittest


from tests.util import AsyncTestCaseSetup
from .util import AsyncTestCaseSetup, _get_unused_port, ADDRESS
from .test_structure.server_control import Server
from .test_structure.fronius_mock_server import FroniusRequestHandler, FroniusServer
from http.server import SimpleHTTPRequestHandler
Expand All @@ -32,18 +32,21 @@
GET_INVERTER_INFO,
)

ADDRESS = "localhost"


class NoFroniusWebTest(AsyncTestCaseSetup):
server = None
api_version = pyfronius.API_VERSION.V1
server_control = None
port = 0
url = "http://localhost:80"
url = f"http://{ADDRESS}:80"
session = None
fronius = None

async def setUp(self):
# Pick an unused port to ensure the connection attempt fails deterministically
self.port = _get_unused_port()
self.url = "http://{}:{}".format(ADDRESS, self.port)

async def test_no_server(self):
# set up a fronius client and aiohttp session
self.session = aiohttp.ClientSession()
Expand Down
11 changes: 11 additions & 0 deletions tests/util.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import asyncio
import contextlib
import socket

import aiounittest
from aiounittest import async_test

ADDRESS = "localhost"


class AsyncTestCaseSetup(aiounittest.AsyncTestCase):
async def setUp(self):
Expand All @@ -24,3 +28,10 @@ async def wrapped_attr():
return res
else:
return attr


def _get_unused_port() -> int:
"""Return an unused localhost port for negative connection tests."""
with contextlib.closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock:
sock.bind((ADDRESS, 0))
return sock.getsockname()[1]
Loading