Skip to content

Commit

Permalink
Drop Python 3.8 support and add Python 3.14
Browse files Browse the repository at this point in the history
  • Loading branch information
rmartin16 committed Nov 1, 2024
1 parent c4f4aac commit a58db3c
Show file tree
Hide file tree
Showing 15 changed files with 55 additions and 76 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jobs:
uses: actions/setup-python@v5.2.0
with:
python-version: ${{ env.LATEST_PYTHON_VER }}
allow-prereleases: true
cache: pip
check-latest: true
cache-dependency-path: ${{ github.workspace }}/pyproject.toml
Expand Down Expand Up @@ -130,7 +131,7 @@ jobs:
needs: [ verify, package ]
strategy:
matrix:
PYTHON_VER: [ 3.13-dev, 3.12, 3.11, "3.10", pypy3.10, 3.9, 3.8 ]
PYTHON_VER: [ "3.14", "3.13", "3.12", "3.11", "3.10", "pypy3.10", "3.9" ]
uses: ./.github/workflows/test.yml
secrets: inherit
with:
Expand Down Expand Up @@ -207,6 +208,7 @@ jobs:
uses: actions/setup-python@v5.2.0
with:
python-version: ${{ env.LATEST_PYTHON_VER }}
allow-prereleases: true
cache: pip
cache-dependency-path: ${{ github.workspace }}/setup.cfg

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ jobs:
uses: actions/setup-python@v5.2.0
with:
python-version: ${{ inputs.python-version }}
allow-prereleases: true
cache: pip
check-latest: true
cache-dependency-path: ${{ github.workspace }}/setup.cfg
Expand Down
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ repos:
- id: ruff
args:
- --fix
- --unsafe-fixes
- id: ruff-format

- repo: https://github.com/pre-commit/mirrors-mypy
Expand Down
9 changes: 4 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "qbittorrent-api"
requires-python = ">=3.8"
requires-python = ">=3.9"
description = "Python client for qBittorrent v4.1+ Web API."
authors = [{name = "Russell Martin"}]
maintainers = [{name = "Russell Martin"}]
Expand All @@ -16,12 +16,12 @@ classifiers = [
"Operating System :: OS Independent",
"Environment :: Console",
"Programming Language :: Python :: 3",
"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",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Utilities",
Expand All @@ -40,8 +40,7 @@ dev = [
"coverage[toml] ==7.6.4",
"furo ==2024.8.6",
"mypy ==1.13.0",
"pre-commit <3.6.0 ; python_version < '3.9'",
"pre-commit ==4.0.1 ; python_version >= '3.9'",
"pre-commit ==4.0.1",
"pytest ==8.3.3",
"tox ==4.23.2",
"twine ==5.1.1",
Expand Down Expand Up @@ -70,7 +69,7 @@ readme = {file = ["README.md", "CHANGELOG.md", "LICENSE"], content-type = "text/
# section must be present to trigger its use

[tool.ruff]
target-version = "py38"
target-version = "py39"

[tool.ruff.lint]
select = ["C40", "C9", "E", "F", "PLE", "S", "W", "YTT", "I", "UP", "SIM"]
Expand Down
5 changes: 3 additions & 2 deletions src/qbittorrentapi/_attrdict.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from collections.abc import Mapping, MutableMapping, Sequence
from re import compile as re_compile
from typing import Any, Dict, Mapping, MutableMapping, Sequence, TypeVar
from typing import Any, TypeVar

K = TypeVar("K")
V = TypeVar("V")
Expand Down Expand Up @@ -254,7 +255,7 @@ def __delattr__(self, key: str, force: bool = False) -> None:
)


class AttrDict(Dict[str, V], MutableAttr[V]):
class AttrDict(dict[str, V], MutableAttr[V]):
"""A dict that implements MutableAttr."""

_sequence_type: type
Expand Down
4 changes: 2 additions & 2 deletions src/qbittorrentapi/_version_support.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from functools import lru_cache
from functools import cache
from typing import Final, Literal

import packaging.version
Expand Down Expand Up @@ -71,7 +71,7 @@
MOST_RECENT_SUPPORTED_API_VERSION: Final[Literal["2.11.2"]] = "2.11.2"


@lru_cache(maxsize=None)
@cache
def v(version: str) -> packaging.version.Version:
"""Caching version parser."""
return packaging.version.Version(version)
Expand Down
3 changes: 2 additions & 1 deletion src/qbittorrentapi/app.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from __future__ import annotations

import os
from collections.abc import Iterable, Mapping, Sequence
from json import dumps
from logging import Logger, getLogger
from typing import Any, AnyStr, Iterable, Mapping, Sequence, Union
from typing import Any, AnyStr, Union

from qbittorrentapi.auth import AuthAPIMixIn
from qbittorrentapi.definitions import (
Expand Down
3 changes: 2 additions & 1 deletion src/qbittorrentapi/client.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from typing import Any, Mapping
from collections.abc import Mapping
from typing import Any

from qbittorrentapi.log import LogAPIMixIn
from qbittorrentapi.rss import RSSAPIMixIn
Expand Down
71 changes: 21 additions & 50 deletions src/qbittorrentapi/definitions.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
from __future__ import annotations

import sys
from collections import UserList
from collections.abc import Iterable, Mapping, Sequence
from enum import Enum
from typing import (
TYPE_CHECKING,
Any,
Generic,
Iterable,
Mapping,
Sequence,
Tuple,
TypeVar,
Union,
)
Expand Down Expand Up @@ -42,7 +38,7 @@
#: Type for List input to API method.
ListInputT = Iterable[Mapping[str, JsonValueT]]
#: Type for Files input to API method.
FilesToSendT = Mapping[str, Union[bytes, Tuple[str, bytes]]]
FilesToSendT = Mapping[str, Union[bytes, tuple[str, bytes]]]


class APINames(str, Enum):
Expand Down Expand Up @@ -250,50 +246,25 @@ def _normalize(cls, data: Mapping[str, V] | T) -> AttrDict[V] | T:
return data


# Python 3.8 does not support UserList as a proper Generic
if sys.version_info < (3, 9):

class List(UserList, Generic[ListEntryT]):
"""Base definition for list-like objects returned from qBittorrent."""

def __init__(
self,
list_entries: ListInputT | None = None,
entry_class: type[ListEntryT] | None = None,
**kwargs: Any,
):
super().__init__(
[
(
entry_class(data=entry, **kwargs)
if entry_class is not None and isinstance(entry, Mapping)
else entry
)
for entry in list_entries or []
]
)

else:

class List(UserList[ListEntryT]):
"""Base definition for list-like objects returned from qBittorrent."""

def __init__(
self,
list_entries: ListInputT | None = None,
entry_class: type[ListEntryT] | None = None,
**kwargs: Any,
):
super().__init__(
[
(
entry_class(data=entry, **kwargs) # type: ignore[misc]
if entry_class is not None and isinstance(entry, Mapping)
else entry
)
for entry in list_entries or []
]
)
class List(UserList[ListEntryT]):
"""Base definition for list-like objects returned from qBittorrent."""

def __init__(
self,
list_entries: ListInputT | None = None,
entry_class: type[ListEntryT] | None = None,
**kwargs: Any,
):
super().__init__(
[
(
entry_class(data=entry, **kwargs) # type: ignore[misc]
if entry_class is not None and isinstance(entry, Mapping)
else entry
)
for entry in list_entries or []
]
)


class ListEntry(Dictionary[JsonValueT]):
Expand Down
4 changes: 2 additions & 2 deletions src/qbittorrentapi/request.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from __future__ import annotations

from collections.abc import Iterable
from collections.abc import Iterable, Mapping
from json import loads
from logging import Logger, NullHandler, getLogger
from os import environ
from time import sleep
from typing import TYPE_CHECKING, Any, Literal, Mapping, TypeVar, cast
from typing import TYPE_CHECKING, Any, Literal, TypeVar, cast
from urllib.parse import ParseResult, urljoin, urlparse

from requests import Response, Session
Expand Down
2 changes: 1 addition & 1 deletion src/qbittorrentapi/rss.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from collections.abc import Mapping
from json import dumps
from typing import Mapping

from qbittorrentapi.app import AppAPIMixIn
from qbittorrentapi.definitions import (
Expand Down
3 changes: 2 additions & 1 deletion src/qbittorrentapi/search.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

from typing import Iterable, Mapping, cast
from collections.abc import Iterable, Mapping
from typing import cast

from qbittorrentapi.app import AppAPIMixIn
from qbittorrentapi.definitions import (
Expand Down
4 changes: 1 addition & 3 deletions src/qbittorrentapi/torrents.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
from __future__ import annotations

import errno
from collections.abc import Iterable, Mapping, MutableMapping
from logging import Logger, getLogger
from os import path
from os import strerror as os_strerror
from typing import (
IO,
Any,
Callable,
Iterable,
Literal,
Mapping,
MutableMapping,
TypeVar,
Union,
cast,
Expand Down
2 changes: 1 addition & 1 deletion src/qbittorrentapi/transfer.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Iterable
from collections.abc import Iterable

from qbittorrentapi._version_support import v
from qbittorrentapi.app import AppAPIMixIn
Expand Down
15 changes: 9 additions & 6 deletions tests/test_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,8 +637,9 @@ def request500(*args, **kwargs):
client.auth_log_in()
with monkeypatch.context() as m:
m.setattr(client, "_request", request500)
with caplog.at_level(logging.DEBUG, logger="qbittorrentapi"), pytest.raises(
exceptions.HTTP500Error
with (
caplog.at_level(logging.DEBUG, logger="qbittorrentapi"),
pytest.raises(exceptions.HTTP500Error),
):
client.app_version()
assert "Retry attempt" in caplog.text
Expand All @@ -647,17 +648,19 @@ def request500(*args, **kwargs):
def test_request_retry_skip(caplog):
client = Client(VERIFY_WEBUI_CERTIFICATE=False)
client.auth_log_in()
with caplog.at_level(logging.DEBUG, logger="qbittorrentapi"), pytest.raises(
exceptions.MissingRequiredParameters400Error
with (
caplog.at_level(logging.DEBUG, logger="qbittorrentapi"),
pytest.raises(exceptions.MissingRequiredParameters400Error),
):
client.torrents_rename()
assert "Retry attempt" not in caplog.text


def test_verbose_logging(caplog):
client = Client(VERBOSE_RESPONSE_LOGGING=True, VERIFY_WEBUI_CERTIFICATE=False)
with caplog.at_level(logging.DEBUG, logger="qbittorrentapi"), pytest.raises(
exceptions.NotFound404Error
with (
caplog.at_level(logging.DEBUG, logger="qbittorrentapi"),
pytest.raises(exceptions.NotFound404Error),
):
client.torrents_rename(torrent_hash="asdf", new_torrent_name="erty")
assert "Response status" in caplog.text
Expand Down

0 comments on commit a58db3c

Please sign in to comment.