diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9963731afa0..b48c35e87ab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -84,7 +84,7 @@ jobs: if: matrix.architecture != 'aarch64' uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.12' architecture: ${{ matrix.architecture }} cache: 'pip' cache-dependency-path: | diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 7d76a41dd99..edd7a357dd3 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -7,10 +7,10 @@ jobs: - uses: actions/checkout@v4 with: submodules: 'true' - - name: Setup Python 3.9 + - name: Setup Python 3.12 uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.12 cache: 'pip' - run: python -m pip install -r requirements.txt - name: Install mypy diff --git a/.github/workflows/periodic_unittests.yml b/.github/workflows/periodic_unittests.yml index 89da111efab..7d2416e26c6 100644 --- a/.github/workflows/periodic_unittests.yml +++ b/.github/workflows/periodic_unittests.yml @@ -14,7 +14,7 @@ jobs: submodules: 'true' - uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.12' cache: 'pip' - uses: actions/cache/restore@v4 id: restore_cache diff --git a/.github/workflows/pr-comment-validate.yml b/.github/workflows/pr-comment-validate.yml index a1bc388e6a5..183c0ffdc92 100644 --- a/.github/workflows/pr-comment-validate.yml +++ b/.github/workflows/pr-comment-validate.yml @@ -11,8 +11,8 @@ jobs: needs: set_pending_status strategy: matrix: - os: [ubuntu-latest, windows-latest] # macos-latest not tested due to crashing. - version: ["3.9", "3.10"] + os: [ubuntu-latest, windows-latest, macos-latest] + version: ["3.12"] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -29,6 +29,10 @@ jobs: with: path: src/libsodium.dll key: cache_libsodium_dll + - shell: bash + if: matrix.os == 'macos-latest' + run: | + cp /opt/homebrew/opt/libsodium/lib/libsodium.dylib /Library/Frameworks/Python.framework/Versions/3.12/lib/libsodium.dylib - run: python -m pip install -r requirements.txt - run: | cd src diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml index c749a922764..7561a12faed 100644 --- a/.github/workflows/ruff.yml +++ b/.github/workflows/ruff.yml @@ -7,10 +7,10 @@ jobs: - uses: actions/checkout@v4 with: submodules: 'true' - - name: Setup Python 3.8 + - name: Setup Python 3.12 uses: actions/setup-python@v5 with: - python-version: 3.8 + python-version: 3.12 - name: Install ruff run: pip install ruff - name: Get changed Python files diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index add46cfa8c1..4d9996f4717 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -9,7 +9,7 @@ jobs: submodules: 'true' - uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.12' cache: 'pip' - run: python -m pip install -r requirements.txt - name: Run unit tests @@ -24,7 +24,7 @@ jobs: submodules: 'true' - uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.12' cache: 'pip' - uses: actions/cache/restore@v4 id: restore_cache @@ -45,12 +45,11 @@ jobs: submodules: 'true' - uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.12' cache: 'pip' - shell: bash run: | - cp /opt/homebrew/opt/libsodium/lib/libsodium.dylib /Library/Frameworks/Python.framework/Versions/3.9/lib/libsodium.dylib - sed -i".backup" 's|libtorrent==2.0.9|https://tribler.org/libtorrent-2.0.11-cp39-cp39-macosx_14_0_arm64.whl|' requirements.txt + cp /opt/homebrew/opt/libsodium/lib/libsodium.dylib /Library/Frameworks/Python.framework/Versions/3.12/lib/libsodium.dylib - run: python -m pip install -r requirements.txt - name: Run unit tests run: | diff --git a/.github/workflows/weekly_mutation_tests.yml b/.github/workflows/weekly_mutation_tests.yml index da8fb8f0870..94a921d4374 100644 --- a/.github/workflows/weekly_mutation_tests.yml +++ b/.github/workflows/weekly_mutation_tests.yml @@ -18,11 +18,11 @@ jobs: run: | cd tribler echo "NUM_COMMITS=$(git log --oneline --since '7 days ago' | wc -l)" >> $GITHUB_ENV - - name: Setup Python 3.10 + - name: Setup Python 3.12 uses: actions/setup-python@v5 if: ${{ env.NUM_COMMITS > 0 }} with: - python-version: '3.10' + python-version: '3.12' cache: 'pip' - name: Install dependencies if: ${{ env.NUM_COMMITS > 0 }} diff --git a/.ruff.toml b/.ruff.toml index 084fc2badc6..fb9ba57a1e0 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -55,7 +55,6 @@ lint.ignore = [ "TRY300", "TRY301", "TRY401", - "UP006", # Backward compatibility, remove after Python >= 3.9 ] exclude = [ @@ -92,7 +91,7 @@ line-length = 120 # Allow unused variables when underscore-prefixed. lint.dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" -target-version = "py39" +target-version = "py312" [lint.pylint] max-args = 6 diff --git a/README.rst b/README.rst index 669fbe04d84..7cdb99d40ed 100644 --- a/README.rst +++ b/README.rst @@ -156,7 +156,7 @@ This file is part of Tribler, Copyright 2004-2024. Tribler is licensed under the :target: https://discord.gg/UpPUcVGESe :alt: Join Discord chat -.. |python_3_10| image:: https://img.shields.io/badge/python-3.10-blue.svg +.. |python_3_12| image:: https://img.shields.io/badge/python-3.12-blue.svg :target: https://www.python.org/ .. |flathub| image:: https://img.shields.io/flathub/downloads/org.tribler.Tribler diff --git a/build/setup.py b/build/setup.py index a761a12dbd5..5713f7b06f2 100644 --- a/build/setup.py +++ b/build/setup.py @@ -79,8 +79,7 @@ def read_requirements(file_name: str, directory: str = ".") -> list[str]: "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.12", "Topic :: Communications :: File Sharing", "Topic :: Security :: Cryptography", "Operating System :: OS Independent", diff --git a/doc/.readthedocs.yaml b/doc/.readthedocs.yaml index 14a9c074bf0..05a11c2ae66 100644 --- a/doc/.readthedocs.yaml +++ b/doc/.readthedocs.yaml @@ -11,7 +11,7 @@ build: os: ubuntu-22.04 tools: # Check https://docs.readthedocs.io/en/stable/config-file/v2.html#build-tools-python - python: "3.10" + python: "3.12" apt_packages: - graphviz jobs: diff --git a/mypy.ini b/mypy.ini index f095d4d3b95..97134d861c4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,2 +1,3 @@ [mypy] ignore_missing_imports = True +python_version = 3.12 diff --git a/requirements.txt b/requirements.txt index c2c7acd24ee..c91a1824571 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ bitarray configobj ipv8-rust-tunnels -libtorrent==2.0.9 +libtorrent==2.0.11 lz4 pillow pony diff --git a/src/run_unit_tests.py b/src/run_unit_tests.py index 2a298128745..929cbd13367 100644 --- a/src/run_unit_tests.py +++ b/src/run_unit_tests.py @@ -24,9 +24,6 @@ """ The unit tests on Mac lock up on multiprocess getaddrinfo calls. We establish the lan addresses once here before spawning any children. - - File "/Users/runner/hostedtoolcache/Python/3.9.20/x64/lib/python3.9/socket.py", line 966, in getaddrinfo -| for res in _socket.getaddrinfo(host, port, family, type, proto, flags): """ from ipv8.messaging.interfaces.lan_addresses.interfaces import get_lan_addresses get_lan_addresses() diff --git a/src/tribler/core/content_discovery/cache.py b/src/tribler/core/content_discovery/cache.py index fc9c9ded984..f1190b7908c 100644 --- a/src/tribler/core/content_discovery/cache.py +++ b/src/tribler/core/content_discovery/cache.py @@ -1,12 +1,13 @@ from __future__ import annotations from binascii import hexlify -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Self from ipv8.requestcache import RandomNumberCache, RequestCache -from typing_extensions import Self if TYPE_CHECKING: + from collections.abc import Callable + from ipv8.types import Peer from tribler.core.database.store import ProcessingResult diff --git a/src/tribler/core/content_discovery/community.py b/src/tribler/core/content_discovery/community.py index dd39bebf483..770b466b737 100644 --- a/src/tribler/core/content_discovery/community.py +++ b/src/tribler/core/content_discovery/community.py @@ -8,7 +8,7 @@ from binascii import hexlify, unhexlify from importlib.metadata import PackageNotFoundError, version from itertools import count -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any from ipv8.community import Community, CommunitySettings from ipv8.lazy_community import lazy_wrapper @@ -27,10 +27,10 @@ from tribler.core.database.orm_bindings.torrent_metadata import LZ4_EMPTY_ARCHIVE, entries_to_chunk from tribler.core.database.store import MetadataStore, ObjState, ProcessingResult from tribler.core.notifier import Notification, Notifier -from tribler.core.torrent_checker.dataclasses import HealthInfo +from tribler.core.torrent_checker.healthdataclasses import HealthInfo if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Callable, Sequence from ipv8.types import Peer diff --git a/src/tribler/core/content_discovery/payload.py b/src/tribler/core/content_discovery/payload.py index 9bed3985156..bd299ab3221 100644 --- a/src/tribler/core/content_discovery/payload.py +++ b/src/tribler/core/content_discovery/payload.py @@ -1,13 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Self from ipv8.messaging.lazy_payload import VariablePayload, vp_compile from ipv8.messaging.serialization import default_serializer -from typing_extensions import Self if TYPE_CHECKING: - from tribler.core.torrent_checker.dataclasses import HealthInfo + from tribler.core.torrent_checker.healthdataclasses import HealthInfo @vp_compile diff --git a/src/tribler/core/content_discovery/restapi/search_endpoint.py b/src/tribler/core/content_discovery/restapi/search_endpoint.py index 70b99d6877b..a4298529340 100644 --- a/src/tribler/core/content_discovery/restapi/search_endpoint.py +++ b/src/tribler/core/content_discovery/restapi/search_endpoint.py @@ -7,7 +7,6 @@ from aiohttp_apispec import docs, querystring_schema from ipv8.REST.schema import schema from marshmallow.fields import Integer, List, String -from typing_extensions import TypeAlias from tribler.core.database.queries import to_fts_query from tribler.core.database.restapi.database_endpoint import DatabaseEndpoint @@ -18,7 +17,7 @@ from tribler.core.content_discovery.community import ContentDiscoveryCommunity from tribler.core.restapi.rest_manager import TriblerRequest - RequestType: TypeAlias = TriblerRequest[tuple[ContentDiscoveryCommunity]] + type RequestType = TriblerRequest[tuple[ContentDiscoveryCommunity]] class RemoteQueryParameters(MetadataParameters): diff --git a/src/tribler/core/database/orm_bindings/torrent_metadata.py b/src/tribler/core/database/orm_bindings/torrent_metadata.py index 938a8a50509..c442a8f065f 100644 --- a/src/tribler/core/database/orm_bindings/torrent_metadata.py +++ b/src/tribler/core/database/orm_bindings/torrent_metadata.py @@ -4,12 +4,11 @@ from binascii import hexlify, unhexlify from datetime import datetime from struct import unpack -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Self from lz4.frame import LZ4FrameCompressor from pony import orm from pony.orm import Database, db_session -from typing_extensions import Self from tribler.core.database.serialization import ( EPOCH, diff --git a/src/tribler/core/database/orm_bindings/torrent_state.py b/src/tribler/core/database/orm_bindings/torrent_state.py index d239dd80395..2a9f3541155 100644 --- a/src/tribler/core/database/orm_bindings/torrent_state.py +++ b/src/tribler/core/database/orm_bindings/torrent_state.py @@ -1,11 +1,10 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Self from pony import orm -from typing_extensions import Self -from tribler.core.torrent_checker.dataclasses import HealthInfo +from tribler.core.torrent_checker.healthdataclasses import HealthInfo if TYPE_CHECKING: from dataclasses import dataclass diff --git a/src/tribler/core/database/restapi/database_endpoint.py b/src/tribler/core/database/restapi/database_endpoint.py index 45705bfccfc..ec0a27ab2b6 100644 --- a/src/tribler/core/database/restapi/database_endpoint.py +++ b/src/tribler/core/database/restapi/database_endpoint.py @@ -4,13 +4,13 @@ import json import typing from binascii import unhexlify +from typing import Self from aiohttp import web from aiohttp_apispec import docs, querystring_schema from ipv8.REST.schema import schema from marshmallow.fields import Boolean, Integer, String from pony.orm import db_session -from typing_extensions import Self, TypeAlias from tribler.core.database.queries import to_fts_query from tribler.core.database.restapi.schema import MetadataSchema, SearchMetadataParameters, TorrentSchema @@ -31,7 +31,7 @@ from tribler.core.restapi.rest_manager import TriblerRequest from tribler.core.torrent_checker.torrent_checker import TorrentChecker - RequestType: TypeAlias = TriblerRequest[tuple[MetadataStore]] + type RequestType = TriblerRequest[tuple[MetadataStore]] TORRENT_CHECK_TIMEOUT = 20 diff --git a/src/tribler/core/database/serialization.py b/src/tribler/core/database/serialization.py index 2005b367154..5a0c77f1ca6 100644 --- a/src/tribler/core/database/serialization.py +++ b/src/tribler/core/database/serialization.py @@ -3,12 +3,11 @@ import struct from binascii import hexlify from datetime import datetime, timedelta -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Self from ipv8.keyvault.crypto import default_eccrypto from ipv8.messaging.lazy_payload import VariablePayload, vp_compile from ipv8.messaging.serialization import VarLenUtf8, default_serializer -from typing_extensions import Self if TYPE_CHECKING: from ipv8.types import PrivateKey diff --git a/src/tribler/core/database/store.py b/src/tribler/core/database/store.py index 1c21d7a2a14..48dc79d524d 100644 --- a/src/tribler/core/database/store.py +++ b/src/tribler/core/database/store.py @@ -10,7 +10,7 @@ from os.path import getsize from pathlib import Path from time import sleep, time -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any from lz4.frame import LZ4FrameDecompressor from pony import orm @@ -30,9 +30,10 @@ TorrentMetadataPayload, read_payload_with_offset, ) -from tribler.core.torrent_checker.dataclasses import HealthInfo +from tribler.core.torrent_checker.healthdataclasses import HealthInfo if TYPE_CHECKING: + from collections.abc import Callable from sqlite3 import Connection from ipv8.types import PrivateKey @@ -395,7 +396,7 @@ def process_squashed_mdblob(self, chunk_data: bytes, external_thread: bool = Fal if health_info and len(health_info) == len(payload_list): with db_session: - for payload, (seeders, leechers, last_check) in zip(payload_list, health_info): + for payload, (seeders, leechers, last_check) in zip(payload_list, health_info, strict=False): if hasattr(payload, "infohash"): health = HealthInfo(payload.infohash, last_check=last_check, seeders=seeders, leechers=leechers) diff --git a/src/tribler/core/libtorrent/download_manager/dht_health_manager.py b/src/tribler/core/libtorrent/download_manager/dht_health_manager.py index 98429742d8b..ca591dbd335 100644 --- a/src/tribler/core/libtorrent/download_manager/dht_health_manager.py +++ b/src/tribler/core/libtorrent/download_manager/dht_health_manager.py @@ -3,12 +3,15 @@ import math from asyncio import Future from binascii import hexlify -from typing import Awaitable +from typing import TYPE_CHECKING import libtorrent as lt from ipv8.taskmanager import TaskManager -from tribler.core.torrent_checker.dataclasses import HealthInfo +from tribler.core.torrent_checker.healthdataclasses import HealthInfo + +if TYPE_CHECKING: + from collections.abc import Awaitable class DHTHealthManager(TaskManager): diff --git a/src/tribler/core/libtorrent/download_manager/download.py b/src/tribler/core/libtorrent/download_manager/download.py index 7a8822616bd..1091d019efa 100644 --- a/src/tribler/core/libtorrent/download_manager/download.py +++ b/src/tribler/core/libtorrent/download_manager/download.py @@ -12,10 +12,11 @@ from asyncio import CancelledError, Future, get_running_loop, sleep, wait_for from binascii import hexlify from collections import defaultdict +from collections.abc import Callable from contextlib import suppress from enum import Enum from pathlib import Path -from typing import TYPE_CHECKING, Any, Callable, TypedDict, cast +from typing import TYPE_CHECKING, Any, TypedDict, cast import libtorrent as lt from bitarray import bitarray @@ -276,9 +277,9 @@ def get_atp(self) -> dict: | lt.add_torrent_params_flags_t.flag_update_subscribe} if self.config.get_share_mode(): - atp["flags"] = cast(int, atp["flags"]) | lt.add_torrent_params_flags_t.flag_share_mode + atp["flags"] = cast("int", atp["flags"]) | lt.add_torrent_params_flags_t.flag_share_mode if self.config.get_upload_mode(): - atp["flags"] = cast(int, atp["flags"]) | lt.add_torrent_params_flags_t.flag_upload_mode + atp["flags"] = cast("int", atp["flags"]) | lt.add_torrent_params_flags_t.flag_upload_mode resume_data = self.config.get_engineresumedata() if not isinstance(self.tdef, TorrentDefNoMetainfo): @@ -350,7 +351,7 @@ def get_pieces_base64(self) -> bytes: """ Returns a base64 encoded bitmask of the pieces that we have. """ - binary_gen = (int(boolean) for boolean in cast(lt.torrent_handle, self.handle).status().pieces) + binary_gen = (int(boolean) for boolean in cast("lt.torrent_handle", self.handle).status().pieces) try: bits = bitarray(binary_gen) except ValueError: @@ -420,7 +421,7 @@ def on_save_resume_data_alert(self, alert: lt.save_resume_data_alert) -> None: if self.checkpoint_disabled: return - resume_data = (cast(dict[bytes, Any], lt.bdecode(alert.resume_data)) + resume_data = (cast("dict[bytes, Any]", lt.bdecode(alert.resume_data)) if isinstance(alert.resume_data, bytes) # Libtorrent 2.X else alert.resume_data) # Libtorrent 1.X # Make save_path relative if the torrent is saved in the Tribler state directory @@ -498,14 +499,14 @@ def on_metadata_received_alert(self, alert: lt.metadata_received_alert) -> None: Handle a metadata received alert. """ self._logger.info("On metadata received alert: %s", repr(alert)) - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) torrent_info = get_info_from_handle(self.handle) if not torrent_info: return try: - metadata = cast(MetainfoDict, {b"info": lt.bdecode(torrent_info.metadata())}) + metadata = cast("MetainfoDict", {b"info": lt.bdecode(torrent_info.metadata())}) except (RuntimeError, ValueError) as e: self._logger.warning(e) return @@ -594,7 +595,7 @@ def on_torrent_finished_alert(self, alert: lt.torrent_finished_alert) -> None: Handle a torrent finished alert. """ self._logger.info("On torrent finished alert: %s", repr(alert)) - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.update_lt_status(self.handle.status()) self.checkpoint() downloaded = self.get_state().total_download @@ -667,7 +668,7 @@ def move_storage(self, new_dir: Path) -> bool: Move the output files to a different location. """ if not isinstance(self.tdef, TorrentDefNoMetainfo): - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.handle.move_storage(str(new_dir)) self.config.set_dest_dir(new_dir) self.config.set_completed_dir(new_dir) @@ -679,7 +680,7 @@ def force_recheck(self) -> None: Force libtorrent to validate the files. """ if not isinstance(self.tdef, TorrentDefNoMetainfo): - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) if self.get_state().get_status() == DownloadStatus.STOPPED: self.pause_after_next_hashcheck = True self.checkpoint_after_next_hashcheck = True @@ -721,7 +722,7 @@ def get_peer_list(self, include_have: bool = True) -> list[PeerDict | PeerDictHa extended_version = peer_info.client except UnicodeDecodeError: extended_version = b"unknown" - peer_dict: PeerDict | PeerDictHave = cast(PeerDict, { + peer_dict: PeerDict | PeerDictHave = cast("PeerDict", { "id": hexlify(peer_info.pid.to_bytes()).decode(), "extended_version": extended_version, "ip": peer_info.ip[0], @@ -747,7 +748,7 @@ def get_peer_list(self, include_have: bool = True) -> list[PeerDict | PeerDictHa "upload_only": bool(peer_info.flags & peer_info.upload_only) }) if include_have: - peer_dict = cast(PeerDictHave, peer_dict) + peer_dict = cast("PeerDictHave", peer_dict) peer_dict["have"] = peer_info.pieces peers.append(peer_dict) return peers @@ -784,7 +785,7 @@ def get_tracker_status(self) -> dict[str, tuple[int, str]]: """ Retrieve an overview of the trackers and their statuses. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) # Make sure all trackers are in the tracker_status dict try: tracker_urls = {tracker["url"] for tracker in self.handle.trackers()} @@ -913,7 +914,7 @@ def add_trackers(self, trackers: list[bytes]) -> None: """ Add the given trackers to the handle. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) for tracker in trackers: self.handle.add_tracker({"url": tracker, "verified": False}) self.handle.force_reannounce() @@ -923,7 +924,7 @@ def get_magnet_link(self) -> str: """ Generate a magnet link for our download. """ - return lt.make_magnet_uri(cast(lt.torrent_handle, self.handle)) # Ensured by ``check_handle`` + return lt.make_magnet_uri(cast("lt.torrent_handle", self.handle)) # Ensured by ``check_handle`` @require_handle def add_peer(self, addr: tuple[str, int]) -> None: @@ -932,7 +933,7 @@ def add_peer(self, addr: tuple[str, int]) -> None: :param addr: The (hostname_ip,port) tuple to connect to """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.handle.connect_peer(addr, 0) @require_handle @@ -942,7 +943,7 @@ def add_url_seed(self, addr: str) -> None: :param addr: The URL address to connect to """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.handle.add_url_seed(addr) @require_handle @@ -950,7 +951,7 @@ def set_priority(self, priority: int) -> None: """ Set the priority of this download. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.handle.set_priority(priority) @require_handle @@ -958,7 +959,7 @@ def set_max_upload_rate(self, value: int) -> None: """ Set the maximum upload rate of this download. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.handle.set_upload_limit(value * 1024) @require_handle @@ -966,7 +967,7 @@ def set_max_download_rate(self, value: int) -> None: """ Set the maximum download rate of this download. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.handle.set_download_limit(value * 1024) @require_handle @@ -974,7 +975,7 @@ def apply_ip_filter(self, enable: bool) -> None: """ Enable the IP filter on this download. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.handle.apply_ip_filter(enable) def get_share_mode(self) -> bool: @@ -988,7 +989,7 @@ def set_share_mode(self, share_mode: bool) -> None: """ Set whether this download is in sharing mode. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.config.set_share_mode(share_mode) self.handle.set_share_mode(share_mode) @@ -1003,7 +1004,7 @@ def set_upload_mode(self, upload_mode: bool) -> None: """ Set whether this download is in upload mode. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.config.set_upload_mode(upload_mode) self.handle.set_upload_mode(upload_mode) @@ -1012,7 +1013,7 @@ def force_dht_announce(self) -> None: """ Force announce thid download on the DHT. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.handle.force_dht_announce() @require_handle @@ -1020,7 +1021,7 @@ def set_sequential_download(self, enable: bool) -> None: """ Set this download to sequential download mode. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.handle.set_sequential_download(enable) @check_handle(None) @@ -1028,7 +1029,7 @@ def set_piece_priorities(self, piece_priorities: list[int]) -> None: """ Set the priority for all pieces in the download. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.handle.prioritize_pieces(piece_priorities) @check_handle([]) @@ -1036,7 +1037,7 @@ def get_piece_priorities(self) -> list[int]: """ Get the priorities of all pieces in the download. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) return self.handle.piece_priorities() @check_handle(None) @@ -1044,7 +1045,7 @@ def set_file_priorities(self, file_priorities: list[int]) -> None: """ Set the priority for all files in the download. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.handle.prioritize_files(file_priorities) @check_handle(None) @@ -1052,7 +1053,7 @@ def set_file_priority(self, file_index: int, prio: int = 4) -> None: """ Set the priority for a particular file in the download. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.handle.file_priority(file_index, prio) @check_handle(None) @@ -1060,7 +1061,7 @@ def reset_piece_deadline(self, piece: int) -> None: """ Reset the deadline for the given piece. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.handle.reset_piece_deadline(piece) @check_handle(None) @@ -1068,7 +1069,7 @@ def set_piece_deadline(self, piece: int, deadline: int, flags: int = 0) -> None: """ Set the deadline for a given piece. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) self.handle.set_piece_deadline(piece, deadline, flags) @check_handle([]) @@ -1076,7 +1077,7 @@ def get_file_priorities(self) -> list[int]: """ Get the priorities of all files in the download. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) return self.handle.file_priorities() def file_piece_range(self, file_path: Path) -> list[int]: @@ -1107,7 +1108,7 @@ def get_file_completion(self, path: Path) -> float: """ Calculate the completion of a given file or directory. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) total = 0 have = 0 for piece_index in self.file_piece_range(path): @@ -1149,7 +1150,7 @@ def set_selected_file_or_dir(self, path: Path, selected: bool) -> None: """ Set a single file or directory to be selected or not. """ - self.handle = cast(lt.torrent_handle, self.handle) + self.handle = cast("lt.torrent_handle", self.handle) tree = self.tdef.torrent_file_tree prio = 4 if selected else 0 for index in tree.set_selected(Path(path), selected): diff --git a/src/tribler/core/libtorrent/download_manager/download_manager.py b/src/tribler/core/libtorrent/download_manager/download_manager.py index db885d2a6d2..159dbd3a9f3 100644 --- a/src/tribler/core/libtorrent/download_manager/download_manager.py +++ b/src/tribler/core/libtorrent/download_manager/download_manager.py @@ -16,7 +16,7 @@ from copy import deepcopy from pathlib import Path from tempfile import TemporaryDirectory -from typing import TYPE_CHECKING, Any, Callable, cast +from typing import TYPE_CHECKING, Any, cast import libtorrent as lt from configobj import ConfigObj @@ -33,7 +33,7 @@ from tribler.tribler_config import VERSION_SUBDIR if TYPE_CHECKING: - from collections.abc import Awaitable, Iterable + from collections.abc import Awaitable, Callable, Iterable from tribler.core.libtorrent.download_manager.dht_health_manager import DHTHealthManager from tribler.tribler_config import TriblerConfigManager @@ -455,7 +455,7 @@ def process_alert(self, alert: lt.alert, hops: int = 0) -> None: # noqa: C901, # Periodically, libtorrent will send us a state_update_alert, which contains the torrent status of # all torrents changed since the last time we received this alert. if alert_type == "state_update_alert": - for status in cast(lt.state_update_alert, alert).status: + for status in cast("lt.state_update_alert", alert).status: infohash = status.info_hash.to_bytes() if infohash not in self.downloads: logger.debug("Got state_update for unknown torrent %s", hexlify(infohash)) @@ -463,7 +463,7 @@ def process_alert(self, alert: lt.alert, hops: int = 0) -> None: # noqa: C901, self.downloads[infohash].update_lt_status(status) if alert_type == "state_changed_alert": - handle = cast(lt.state_changed_alert, alert).handle + handle = cast("lt.state_changed_alert", alert).handle infohash = handle.info_hash().to_bytes() if infohash not in self.downloads: logger.debug("Got state_change for unknown torrent %s", hexlify(infohash)) @@ -478,22 +478,22 @@ def process_alert(self, alert: lt.alert, hops: int = 0) -> None: # noqa: C901, or (not download.handle and alert_type == "add_torrent_alert") \ or (download.handle and alert_type == "torrent_removed_alert") if is_process_alert: - download.process_alert(cast(lt.torrent_alert, alert), alert_type) + download.process_alert(cast("lt.torrent_alert", alert), alert_type) else: logger.debug("Got alert for download without handle %s: %s", infohash, alert) elif infohash: logger.debug("Got alert for unknown download %s: %s", infohash, alert) if alert_type == "listen_succeeded_alert": - ls_alert = cast(lt.listen_succeeded_alert, alert) + ls_alert = cast("lt.listen_succeeded_alert", alert) self.listen_ports[hops][ls_alert.address] = ls_alert.port elif alert_type == "peer_disconnected_alert": self.notifier.notify(Notification.peer_disconnected, - peer_id=cast(lt.peer_disconnected_alert, alert).pid.to_bytes()) + peer_id=cast("lt.peer_disconnected_alert", alert).pid.to_bytes()) elif alert_type == "session_stats_alert": - ss_alert = cast(lt.session_stats_alert, alert) + ss_alert = cast("lt.session_stats_alert", alert) queued_disk_jobs = ss_alert.values["disk.queued_disk_jobs"] self.queued_write_bytes = ss_alert.values["disk.queued_write_bytes"] num_write_jobs = ss_alert.values["disk.num_write_jobs"] @@ -507,7 +507,7 @@ def process_alert(self, alert: lt.alert, hops: int = 0) -> None: # noqa: C901, # Unfortunately, the Python bindings don't have a direction attribute. # So, we'll have to resort to using the string representation of the alert instead. incoming = str(alert).startswith("<==") - decoded = cast(dict[bytes, Any], lt.bdecode(cast(lt.dht_pkt_alert, alert).pkt_buf)) + decoded = cast("dict[bytes, Any]", lt.bdecode(cast("lt.dht_pkt_alert", alert).pkt_buf)) if not decoded: return @@ -575,7 +575,7 @@ async def get_metainfo(self, infohash: bytes, timeout: float = 7, hops: int | No try: metainfo = download.tdef.get_metainfo() or await wait_for(shield(download.future_metainfo), timeout) - except (CancelledError, asyncio.TimeoutError) as e: + except (CancelledError, TimeoutError) as e: logger.warning("%s: %s (timeout=%f)", type(e).__name__, str(e), timeout) logger.info("Failed to retrieve metainfo for %s", infohash_hex) if raise_errors: @@ -748,7 +748,7 @@ async def start_handle(self, download: Download, atp: dict) -> None: if resume_data: atp_resume_data_skipped["resume_data"] = "" try: - resume_data_dict = cast(dict[bytes, Any], lt.bdecode(resume_data)) + resume_data_dict = cast("dict[bytes, Any]", lt.bdecode(resume_data)) # If the save_path is None or "" win32 SEGFAULTS, see https://github.com/Tribler/tribler/issues/8353 if not resume_data_dict.get(b"save_path"): resume_data_dict[b"save_path"] = atp.get("save_path", ".").encode() @@ -800,7 +800,7 @@ async def _async_add_torrent(self, ltsession: lt.session, infohash: bytes , atp: if self.dht_readiness_timeout > 0 and self.dht_ready_task is not None: try: await wait_for(shield(self.dht_ready_task), timeout=self.dht_readiness_timeout) - except asyncio.TimeoutError: + except TimeoutError: self._logger.warning("Timeout waiting for libtorrent DHT getting enough peers") ltsession.async_add_torrent(encode_atp(atp)) diff --git a/src/tribler/core/libtorrent/download_manager/stream.py b/src/tribler/core/libtorrent/download_manager/stream.py index 4e9b263fdf5..716292e5233 100644 --- a/src/tribler/core/libtorrent/download_manager/stream.py +++ b/src/tribler/core/libtorrent/download_manager/stream.py @@ -12,8 +12,7 @@ from io import BufferedReader from pathlib import Path from types import TracebackType - - from typing_extensions import Self + from typing import Self from tribler.core.libtorrent.download_manager.download import Download @@ -115,7 +114,7 @@ async def enable(self, file_index: int = 0, # Wait until completed await self.wait_for_pieces(pieces_needed) - def iter_pieces(self, have: bool | None = None, start_from: int | None = None) -> Generator[int, None, None]: + def iter_pieces(self, have: bool | None = None, start_from: int | None = None) -> Generator[int]: """ Generator function that yield the pieces for the active file index. """ diff --git a/src/tribler/core/libtorrent/restapi/downloads_endpoint.py b/src/tribler/core/libtorrent/restapi/downloads_endpoint.py index d0e70665df9..15bf5cd7cf8 100644 --- a/src/tribler/core/libtorrent/restapi/downloads_endpoint.py +++ b/src/tribler/core/libtorrent/restapi/downloads_endpoint.py @@ -7,7 +7,7 @@ from functools import lru_cache from pathlib import Path, PurePosixPath from time import time -from typing import TYPE_CHECKING, Any, Optional, TypedDict, cast +from typing import TYPE_CHECKING, Any, TypedDict, cast import libtorrent as lt from aiohttp import web @@ -160,7 +160,7 @@ def get_files_info_json(download: Download) -> list[JSONFilesInfo]: selected_files = download.config.get_selected_files() index_mapping = download.get_def().get_file_indices() for file_index, (fn, size) in enumerate(download.get_def().get_files_with_length()): - files_json.append(cast(JSONFilesInfo, { + files_json.append(cast("JSONFilesInfo", { "index": index_mapping[file_index], # We always return files in Posix format to make GUI independent of Core and simplify testing "name": str(PurePosixPath(fn)), @@ -450,8 +450,8 @@ async def add_download(self, request: Request) -> RESTResponse: # noqa: C901, P else: params[k] = v body = await request.read() - metainfo = cast(dict[bytes, Any], lt.bdecode(body)) - packed_selected_files = cast(Optional[list[int]], metainfo.pop(b"selected_files", None)) + metainfo = cast("dict[bytes, Any]", lt.bdecode(body)) + packed_selected_files = cast("list[int] | None", metainfo.pop(b"selected_files", None)) if packed_selected_files is not None: params["selected_files"] = packed_selected_files tdef = TorrentDef.load_from_dict(metainfo) @@ -1169,7 +1169,7 @@ async def prepare(self, request: BaseRequest) -> AbstractStreamWriter | None: if self._download.stream is None: self._download.add_stream() - self._download.stream = cast(Stream, self._download.stream) + self._download.stream = cast("Stream", self._download.stream) stream = self._download.stream start = start or 0 diff --git a/src/tribler/core/libtorrent/torrent_file_tree.py b/src/tribler/core/libtorrent/torrent_file_tree.py index ef36e7fe1f2..6eea9a9e6d8 100644 --- a/src/tribler/core/libtorrent/torrent_file_tree.py +++ b/src/tribler/core/libtorrent/torrent_file_tree.py @@ -5,9 +5,11 @@ from bisect import bisect from dataclasses import dataclass, field from pathlib import Path -from typing import TYPE_CHECKING, Dict, Generator, ItemsView, cast +from typing import TYPE_CHECKING, cast if TYPE_CHECKING: + from collections.abc import Generator, ItemsView + import libtorrent @@ -33,7 +35,7 @@ def calc_size(self) -> None: """ self.size = sum(d.size for d in self.directories.values()) + sum(f.size for f in self.files) - def iter_dirs(self) -> Generator[TorrentFileTree.Directory, None, None]: + def iter_dirs(self) -> Generator[TorrentFileTree.Directory]: """ Iterate through the subdirectories in this directory and then this directory itself. @@ -63,18 +65,19 @@ def tostr(self, depth: int = 0, name: str = "") -> str: return "\n" + "\t" * depth + f"Directory({name!r},{pretty_directories},{pretty_files}, {self.size} bytes)" - @dataclass(unsafe_hash=True) + @dataclass(unsafe_hash=True, order=True) class File: """ A File object that has a name (relative to its parent directory) and a file index in the torrent's file list. """ - name: str - index: int - size: int = 0 - selected: bool = True + name: str = field(compare=False) + index: int = field(compare=False) + size: int = field(compare=False, default=0) + selected: bool = field(compare=False, default=True) + sort_key: tuple[int | str, ...] = field(compare=True, default=()) - _sort_pattern = re.compile('([0-9]+)') # We use this for natural sorting (see sort_key()) + _sort_pattern = re.compile('([0-9]+)') # We use this for natural sorting def tostr(self, depth: int = 0) -> str: """ @@ -82,51 +85,11 @@ def tostr(self, depth: int = 0) -> str: """ return "\t" * depth + f"File({self.index}, {self.name}, {self.size} bytes)" - def sort_key(self) -> tuple[int | str, ...]: + def __post_init__(self) -> None: """ Sort File instances using natural sort based on their names, which SHOULD be unique. """ - return tuple(int(part) if part.isdigit() else part for part in self._sort_pattern.split(self.name)) - - def __lt__(self, other: TorrentFileTree.File) -> bool: - """ - Python 3.8 quirk/shortcoming is that File needs to be a SupportsRichComparisonT (instead of using a key). - """ - return self.sort_key() < other.sort_key() - - def __le__(self, other: TorrentFileTree.File) -> bool: - """ - Python 3.8 quirk/shortcoming is that File needs to be a SupportsRichComparisonT (instead of using a key). - """ - return self.sort_key() <= other.sort_key() - - def __gt__(self, other: TorrentFileTree.File) -> bool: - """ - Python 3.8 quirk/shortcoming is that File needs to be a SupportsRichComparisonT (instead of using a key). - """ - return self.sort_key() > other.sort_key() - - def __ge__(self, other: TorrentFileTree.File) -> bool: - """ - Python 3.8 quirk/shortcoming is that File needs to be a SupportsRichComparisonT (instead of using a key). - """ - return self.sort_key() >= other.sort_key() - - def __eq__(self, other: object) -> bool: - """ - Python 3.8 quirk/shortcoming is that File needs to be a SupportsRichComparisonT (instead of using a key). - """ - if isinstance(other, TorrentFileTree.File): - return self.sort_key() == other.sort_key() - return False - - def __ne__(self, other: object) -> bool: - """ - Python 3.8 quirk/shortcoming is that File needs to be a SupportsRichComparisonT (instead of using a key). - """ - if isinstance(other, TorrentFileTree.File): - return self.sort_key() != other.sort_key() - return True + self.sort_key = tuple(int(part) if part.isdigit() else part for part in self._sort_pattern.split(self.name)) def __init__(self, file_storage: libtorrent.file_storage) -> None: """ @@ -137,7 +100,7 @@ def __init__(self, file_storage: libtorrent.file_storage) -> None: self.root = TorrentFileTree.Directory() self.root.collapsed = False self.file_storage = file_storage - self.paths: Dict[Path, TorrentFileTree.Directory | TorrentFileTree.File] = {} + self.paths: dict[Path, TorrentFileTree.Directory | TorrentFileTree.File] = {} def __str__(self) -> str: """ @@ -265,7 +228,7 @@ def find_next_directory(self, from_path: Path) -> tuple[Directory, Path] | None: from_parts = from_path.parts for i in range(1, len(from_parts) + 1): parent_path = Path(os.sep.join(from_parts[:-i])) - parent = cast(TorrentFileTree.Directory, self.find(parent_path)) + parent = cast("TorrentFileTree.Directory", self.find(parent_path)) dir_in_parent = from_parts[-i] dir_indices = list(parent.directories.keys()) # Python 3 "quirk": dict keys() order is stable index_in_parent = dir_indices.index(dir_in_parent) @@ -287,7 +250,7 @@ def _view_get_fetch_path_and_dir(self, start_path: tuple[Directory, Path] | Path """ if isinstance(start_path, Path): fetch_path = start_path if self.path_is_dir(start_path) else start_path.parent - fetch_directory = cast(TorrentFileTree.Directory, self.find(fetch_path)) + fetch_directory = cast("TorrentFileTree.Directory", self.find(fetch_path)) return fetch_directory, fetch_path, start_path fetch_directory, fetch_path = start_path requested_fetch_path = fetch_path diff --git a/src/tribler/core/libtorrent/torrentdef.py b/src/tribler/core/libtorrent/torrentdef.py index d71dfa9583a..31dee133254 100644 --- a/src/tribler/core/libtorrent/torrentdef.py +++ b/src/tribler/core/libtorrent/torrentdef.py @@ -260,7 +260,7 @@ def yield_v2_files(directory: DirectoryV2 | FileV2, path: bytes = b"") -> Genera Get the full path and file spec for each file in a v2 subtree. """ if b"" in directory: - yield path, cast(FileSpecV2, directory[b""]) + yield path, cast("FileSpecV2", directory[b""]) else: for name, subdir in directory.items(): if subdir not in [".", ".."]: # BEP52-specified directory name filtering @@ -282,14 +282,14 @@ def get_length_from_metainfo(metainfo: MetainfoDict | MetainfoV2Dict, selectedfi """ # v2 torrent if b"file tree" in metainfo[b"info"]: - return _get_length_from_v2_dir(cast(InfoDictV2, metainfo[b"info"])[b"file tree"]) + return _get_length_from_v2_dir(cast("InfoDictV2", metainfo[b"info"])[b"file tree"]) # single-file v1 torrent if b"files" not in metainfo[b"info"]: - return cast(InfoDict, metainfo[b"info"])[b"length"] + return cast("InfoDict", metainfo[b"info"])[b"length"] # multi-file v1 torrent - files = cast(InfoDict, metainfo[b"info"])[b"files"] + files = cast("InfoDict", metainfo[b"info"])[b"files"] total = 0 for i in range(len(files)): path = files[i][b"path"] @@ -316,7 +316,7 @@ def __init__(self, metainfo: MetainfoDict | MetainfoV2Dict | None = None, :param ignore_validation: Whether we ignore the libtorrent validation. """ self._logger = logging.getLogger(self.__class__.__name__) - self.torrent_parameters: TorrentParameters = cast(TorrentParameters, {}) + self.torrent_parameters: TorrentParameters = cast("TorrentParameters", {}) self.metainfo: MetainfoDict | MetainfoV2Dict | None = metainfo self.infohash: bytes | None = None self._torrent_info: lt.torrent_info | None = None @@ -390,7 +390,7 @@ def load_torrent_info(self) -> None: Load the torrent info into memory from our metainfo if it does not exist. """ if self._torrent_info is None: - self._torrent_info = lt.torrent_info(cast(dict[bytes, Any], self.metainfo)) + self._torrent_info = lt.torrent_info(cast("dict[bytes, Any]", self.metainfo)) def torrent_info_loaded(self) -> bool: """ @@ -438,7 +438,7 @@ def load_from_memory(bencoded_data: bytes) -> TorrentDef: if metainfo is None: msg = "Data is not a bencoded string" raise ValueError(msg) - return TorrentDef.load_from_dict(cast(MetainfoDict, metainfo)) + return TorrentDef.load_from_dict(cast("MetainfoDict", metainfo)) @staticmethod def load_from_dict(metainfo: MetainfoDict | MetainfoV2Dict) -> TorrentDef: @@ -562,8 +562,8 @@ def get_nr_pieces(self) -> int: if self._torrent_info: self._torrent_info.num_pieces() if b"pieces" in self.metainfo[b"info"]: - return len(cast(InfoDict, self.metainfo[b"info"])[b"pieces"]) // 20 - return _get_pieces_from_v2_dir(cast(InfoDictV2, self.metainfo[b"info"])[b"file tree"], + return len(cast("InfoDict", self.metainfo[b"info"])[b"pieces"]) // 20 + return _get_pieces_from_v2_dir(cast("InfoDictV2", self.metainfo[b"info"])[b"file tree"], self.metainfo[b"info"][b"piece length"]) def get_infohash(self) -> bytes | None: @@ -614,7 +614,7 @@ def get_name_as_unicode(self) -> str: if self.metainfo is not None: if b"name.utf-8" in self.metainfo[b"info"]: with suppress(UnicodeError): - return cast(InfoDict, self.metainfo[b"info"])[b"name.utf-8"].decode() + return cast("InfoDict", self.metainfo[b"info"])[b"name.utf-8"].decode() if (name := self.metainfo[b"info"].get(b"name")) is not None: if (encoding := self.metainfo.get(b"encoding")) is not None: @@ -627,7 +627,7 @@ def get_name_as_unicode(self) -> str: return "" - def _get_all_files_as_unicode_with_length(self) -> Generator[tuple[Path, int], None, None]: # noqa: C901, PLR0912 + def _get_all_files_as_unicode_with_length(self) -> Generator[tuple[Path, int]]: # noqa: C901, PLR0912 """ Get a generator for files in the torrent def. No filtering is possible and all tricks are allowed to obtain a unicode list of filenames. @@ -635,10 +635,10 @@ def _get_all_files_as_unicode_with_length(self) -> Generator[tuple[Path, int], N :return: A unicode filename generator. """ if self.metainfo and b"files" in self.metainfo[b"info"]: - metainfo_v1 = cast(InfoDict, self.metainfo[b"info"]) + metainfo_v1 = cast("InfoDict", self.metainfo[b"info"]) # Multi-file v1 torrent - files = cast(FileDict, metainfo_v1[b"files"]) - storage = cast(lt.torrent_info, self.torrent_info).files() # If we have metainfo, we have torrent_info + files = cast("FileDict", metainfo_v1[b"files"]) + storage = cast("lt.torrent_info", self.torrent_info).files() # If we have metainfo, we have torrent_info for index, file_dict in enumerate(files): # Ignore padding files, to align with v2 metainfo @@ -686,7 +686,7 @@ def _get_all_files_as_unicode_with_length(self) -> Generator[tuple[Path, int], N elif self.metainfo and b"file tree" in self.metainfo[b"info"]: # v2 torrent; all paths are supposed to be UTF-8 here, but still need to be checked - metainfo_v2 = cast(InfoDictV2, self.metainfo[b"info"]) + metainfo_v2 = cast("InfoDictV2", self.metainfo[b"info"]) for path, fspec in yield_v2_files(metainfo_v2[b"file tree"]): # Try to convert the names in path to unicode, assuming that it was encoded as utf-8. try: @@ -705,7 +705,7 @@ def _get_all_files_as_unicode_with_length(self) -> Generator[tuple[Path, int], N elif self.metainfo: # Single-file v1 torrent - yield Path(self.get_name_as_unicode()), cast(InfoDict, self.metainfo[b"info"])[b"length"] + yield Path(self.get_name_as_unicode()), cast("InfoDict", self.metainfo[b"info"])[b"length"] def get_files_with_length(self, exts: set[str] | None = None) -> list[tuple[Path, int]]: """ @@ -785,7 +785,7 @@ def get_index_of_file_in_files(self, file: str | None) -> int: raise ValueError(msg) if file is not None and b"files" in self.metainfo[b"info"]: - info = cast(InfoDict, self.metainfo[b"info"]) + info = cast("InfoDict", self.metainfo[b"info"]) for i in range(len(info[b"files"])): file_dict = info[b"files"][i] @@ -797,7 +797,7 @@ def get_index_of_file_in_files(self, file: str | None) -> int: raise ValueError(msg) if file is not None and b"file tree" in self.metainfo[b"info"]: - infov2 = cast(InfoDictV2, self.metainfo[b"info"]) + infov2 = cast("InfoDictV2", self.metainfo[b"info"]) i = 0 fbytes = file.encode() for path, _ in yield_v2_files(infov2[b"file tree"]): @@ -823,7 +823,7 @@ def __init__(self, infohash: bytes, name: bytes, url: bytes | str | None = None) """ Create a new valid torrent def without metainfo. """ - torrent_parameters: TorrentParameters = cast(TorrentParameters, {b"name": name}) + torrent_parameters: TorrentParameters = cast("TorrentParameters", {b"name": name}) if url is not None: torrent_parameters[b"urllist"] = [url if isinstance(url, bytes) else url.encode()] super().__init__(torrent_parameters=torrent_parameters) diff --git a/src/tribler/core/libtorrent/torrents.py b/src/tribler/core/libtorrent/torrents.py index 11bde7bcfd4..63e3b1481d8 100644 --- a/src/tribler/core/libtorrent/torrents.py +++ b/src/tribler/core/libtorrent/torrents.py @@ -5,34 +5,35 @@ from contextlib import suppress from hashlib import sha1 from os.path import getsize -from typing import TYPE_CHECKING, Callable, TypedDict, TypeVar +from typing import TYPE_CHECKING, Concatenate, TypedDict import libtorrent as lt -from typing_extensions import ParamSpec if TYPE_CHECKING: - from collections.abc import Iterable + from collections.abc import Callable, Iterable from pathlib import Path from tribler.core.libtorrent.download_manager.download import Download from tribler.core.libtorrent.torrentdef import InfoDict logger = logging.getLogger(__name__) -WrappedParams = ParamSpec("WrappedParams") -WrappedReturn = TypeVar("WrappedReturn") -Wrapped = Callable[WrappedParams, WrappedReturn] # We are forced to type ignore in Python 3.9, revisit in 3.10! -def check_handle(default: WrappedReturn) -> Wrapped: +def check_handle[**WrappedParams, WrappedReturn]( + default: WrappedReturn + ) -> Callable[[Callable[Concatenate[Download, WrappedParams], WrappedReturn]], + Callable[Concatenate[Download, WrappedParams], WrappedReturn]]: """ Return the libtorrent handle if it's available, else return the default value. Author(s): Egbert Bouman """ - def wrap(f: Wrapped) -> Wrapped: + def wrap( + f: Callable[Concatenate[Download, WrappedParams], WrappedReturn] + ) -> Callable[Concatenate[Download, WrappedParams], WrappedReturn]: def invoke_func(self: Download, - *args: WrappedParams.args, **kwargs: WrappedParams.kwargs # type: ignore[valid-type] + *args: WrappedParams.args, **kwargs: WrappedParams.kwargs ) -> WrappedReturn: if self.handle and self.handle.is_valid(): return f(self, *args, **kwargs) @@ -43,7 +44,9 @@ def invoke_func(self: Download, return wrap -def require_handle(func: Wrapped) -> Wrapped: +def require_handle[**WrappedParams, WrappedReturn]( + func: Callable[Concatenate[Download, WrappedParams], WrappedReturn] + ) -> Callable[Concatenate[Download, WrappedParams], Future[WrappedReturn | None]]: """ Invoke the function once the handle is available. Returns a future that will fire once the function has completed. @@ -51,7 +54,7 @@ def require_handle(func: Wrapped) -> Wrapped: """ def invoke_func(self: Download, - *args: WrappedParams.args, **kwargs: WrappedParams.kwargs # type: ignore[valid-type] + *args: WrappedParams.args, **kwargs: WrappedParams.kwargs ) -> Future[WrappedReturn | None]: result_future: Future[WrappedReturn | None] = Future() diff --git a/src/tribler/core/libtorrent/trackers.py b/src/tribler/core/libtorrent/trackers.py index d15cefa7a8f..661df6fcf1c 100644 --- a/src/tribler/core/libtorrent/trackers.py +++ b/src/tribler/core/libtorrent/trackers.py @@ -171,7 +171,7 @@ def add_url_params(url: str, params: dict) -> str: # you may throw this part away if you don't like it :) parsed_get_args.update( {k: dumps(v) for k, v in parsed_get_args.items() - if isinstance(v, (bool, dict))} + if isinstance(v, bool | dict)} ) # Converting URL argument to proper query string diff --git a/src/tribler/core/notifier.py b/src/tribler/core/notifier.py index 93a524b74a0..b4f3f26f142 100644 --- a/src/tribler/core/notifier.py +++ b/src/tribler/core/notifier.py @@ -3,10 +3,12 @@ import typing from collections import defaultdict from enum import Enum -from typing import Callable from ipv8.messaging.anonymization.tunnel import Circuit +if typing.TYPE_CHECKING: + from collections.abc import Callable + class Desc(typing.NamedTuple): """ diff --git a/src/tribler/core/recommender/orm_query.py b/src/tribler/core/recommender/orm_query.py index 811df9385bf..0b72c14d230 100644 --- a/src/tribler/core/recommender/orm_query.py +++ b/src/tribler/core/recommender/orm_query.py @@ -5,8 +5,8 @@ from pony import orm if TYPE_CHECKING: + from collections.abc import Iterator from dataclasses import dataclass - from typing import Iterator from pony.orm import Database from pony.orm.core import Entity diff --git a/src/tribler/core/recommender/restapi/endpoint.py b/src/tribler/core/recommender/restapi/endpoint.py index bbeb1d507cb..b6cfa3f6bd4 100644 --- a/src/tribler/core/recommender/restapi/endpoint.py +++ b/src/tribler/core/recommender/restapi/endpoint.py @@ -10,12 +10,11 @@ from tribler.core.restapi.rest_endpoint import MAX_REQUEST_SIZE, RESTEndpoint, RESTResponse if TYPE_CHECKING: - from typing_extensions import TypeAlias from tribler.core.recommender.manager import Manager from tribler.core.restapi.rest_manager import TriblerRequest - RequestType: TypeAlias = TriblerRequest[tuple[Manager]] + type RequestType = TriblerRequest[tuple[Manager]] class RecommenderEndpoint(RESTEndpoint): diff --git a/src/tribler/core/rendezvous/orm_bindings/certificate.py b/src/tribler/core/rendezvous/orm_bindings/certificate.py index 68b330c5577..6f93a72336b 100644 --- a/src/tribler/core/rendezvous/orm_bindings/certificate.py +++ b/src/tribler/core/rendezvous/orm_bindings/certificate.py @@ -1,12 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Iterator +from typing import TYPE_CHECKING, Self from pony.orm import Database, Required -from typing_extensions import Self if TYPE_CHECKING: import dataclasses + from collections.abc import Iterator class IterRendezvousCertificate(type): # noqa: D101 @@ -27,7 +27,7 @@ class RendezvousCertificate(metaclass=IterRendezvousCertificate): start: float stop: float - def __init__(self, public_key: bytes, ip: bytes, port: int, ping: float, # noqa: D107, PLR0913 + def __init__(self, public_key: bytes, ip: bytes, port: int, ping: float, # noqa: D107 start: float, stop: float) -> None: ... @classmethod diff --git a/src/tribler/core/restapi/rest_endpoint.py b/src/tribler/core/restapi/rest_endpoint.py index 02f9e66a4f9..4975ce30c07 100644 --- a/src/tribler/core/restapi/rest_endpoint.py +++ b/src/tribler/core/restapi/rest_endpoint.py @@ -2,7 +2,7 @@ import json import logging -from typing import TYPE_CHECKING, Dict +from typing import TYPE_CHECKING from aiohttp import web from ipv8.taskmanager import TaskManager @@ -48,7 +48,7 @@ def __init__(self, middlewares: tuple = (), client_max_size: int = MAX_REQUEST_S Create a new root endpoint. """ super().__init__(middlewares, client_max_size) - self.endpoints: Dict[str, RESTEndpoint] = {} + self.endpoints: dict[str, RESTEndpoint] = {} def add_endpoint(self, prefix: str, endpoint: RESTEndpoint | IPV8RootEndpoint) -> None: """ @@ -70,7 +70,7 @@ def __init__(self, body: dict | list | bytes | str | None = None, headers: dict """ Create a new rest response. """ - if isinstance(body, (dict, list)): + if isinstance(body, dict | list): body = json.dumps(body) content_type = "application/json" super().__init__(body=body, headers=headers, content_type=content_type, status=status, **kwargs) diff --git a/src/tribler/core/restapi/rest_manager.py b/src/tribler/core/restapi/rest_manager.py index fcc85ff44f7..1bbc80a0af8 100644 --- a/src/tribler/core/restapi/rest_manager.py +++ b/src/tribler/core/restapi/rest_manager.py @@ -3,11 +3,10 @@ import logging import ssl import traceback -from asyncio.base_events import Server from functools import wraps from importlib.metadata import PackageNotFoundError, version from pathlib import Path -from typing import TYPE_CHECKING, Awaitable, Callable, Generic, TypeVar, cast +from typing import TYPE_CHECKING, Generic, TypeVar, cast from aiohttp import tcp_helpers, web, web_protocol from aiohttp.web_exceptions import HTTPNotFound, HTTPRequestEntityTooLarge @@ -26,6 +25,8 @@ if TYPE_CHECKING: import asyncio + from asyncio.base_events import Server + from collections.abc import Awaitable, Callable from aiohttp.abc import Request @@ -205,7 +206,7 @@ def get_api_port(self) -> int | None: Get the API port of the currently running server. """ if self.site: - return cast(Server, self.site._server).sockets[0].getsockname()[1] # noqa: SLF001 + return cast("Server", self.site._server).sockets[0].getsockname()[1] # noqa: SLF001 return None async def start(self) -> None: @@ -268,7 +269,7 @@ async def start_http_site(self, runner: web.AppRunner) -> None: str(e)) raise - current_port = api_port or cast(Server, self.site._server).sockets[0].getsockname()[1] # noqa: SLF001 + current_port = api_port or cast("Server", self.site._server).sockets[0].getsockname()[1] # noqa: SLF001 self.config.set("api/http_port_running", current_port) self.config.write() @@ -287,7 +288,7 @@ async def start_https_site(self, runner: web.AppRunner) -> None: await self.site_https.start() self._logger.info("Started HTTPS REST API: %s", self.site_https.name) - current_port = port or cast(Server, self.site_https._server).sockets[0].getsockname()[1] # noqa: SLF001 + current_port = port or cast("Server", self.site_https._server).sockets[0].getsockname()[1] # noqa: SLF001 self.config.set("api/https_port_running", current_port) self.config.write() diff --git a/src/tribler/core/restapi/shutdown_endpoint.py b/src/tribler/core/restapi/shutdown_endpoint.py index bb8b212da5e..1602172d518 100644 --- a/src/tribler/core/restapi/shutdown_endpoint.py +++ b/src/tribler/core/restapi/shutdown_endpoint.py @@ -1,4 +1,4 @@ -from typing import Callable +from collections.abc import Callable from aiohttp import web from aiohttp_apispec import docs diff --git a/src/tribler/core/rss/restapi/endpoint.py b/src/tribler/core/rss/restapi/endpoint.py index c4c4061a5b7..f1d239d374e 100644 --- a/src/tribler/core/rss/restapi/endpoint.py +++ b/src/tribler/core/rss/restapi/endpoint.py @@ -10,13 +10,12 @@ from tribler.core.restapi.rest_endpoint import RESTEndpoint, RESTResponse if TYPE_CHECKING: - from typing_extensions import TypeAlias from tribler.core.restapi.rest_manager import TriblerRequest from tribler.core.rss.rss import RSSWatcherManager from tribler.tribler_config import TriblerConfigManager - RequestType: TypeAlias = TriblerRequest[tuple[RSSWatcherManager, TriblerConfigManager]] + type RequestType = TriblerRequest[tuple[RSSWatcherManager, TriblerConfigManager]] class RSSEndpoint(RESTEndpoint): diff --git a/src/tribler/core/session.py b/src/tribler/core/session.py index bbda1d07deb..2db04a8c196 100644 --- a/src/tribler/core/session.py +++ b/src/tribler/core/session.py @@ -55,7 +55,7 @@ @contextmanager -def rust_enhancements(session: Session) -> Generator[None, None, None]: +def rust_enhancements(session: Session) -> Generator[None]: """ Attempt to import the IPv8 Rust anonymization backend. """ @@ -92,10 +92,9 @@ def rust_enhancements(session: Session) -> Generator[None, None, None]: async def _is_url_available(url: str, timeout: int=1) -> tuple[bool, bytes | None]: async with aiohttp.ClientSession() as session: try: - async with session.get(url, timeout=timeout) as response: + async with session.get(url, timeout=aiohttp.ClientTimeout(total=timeout)) as response: return True, await response.read() - except (asyncio.TimeoutError, aiohttp.client_exceptions.ClientConnectorError, - aiohttp.client_exceptions.ClientResponseError): + except (TimeoutError, aiohttp.client_exceptions.ClientConnectorError, aiohttp.client_exceptions.ClientResponseError): return False, None @@ -189,7 +188,7 @@ def _except_hook(self, typ: type[BaseException], value: BaseException, traceback """ logger.exception("Uncaught exception: %s", "".join(format_exception(typ, value, traceback))) if isinstance(value, Exception): - cast(EventsEndpoint, self.rest_manager.get_endpoint("/api/events")).on_tribler_exception(value) + cast("EventsEndpoint", self.rest_manager.get_endpoint("/api/events")).on_tribler_exception(value) def _asyncio_except_hook(self, loop: AbstractEventLoop, context: dict[str, Any]) -> None: """ @@ -205,7 +204,7 @@ def _asyncio_except_hook(self, loop: AbstractEventLoop, context: dict[str, Any]) elif isinstance(exc, Exception): logger.exception("Uncaught async exception: %s", "".join(format_exception(exc.__class__, exc, exc.__traceback__))) - cast(EventsEndpoint, self.rest_manager.get_endpoint("/api/events")).on_tribler_exception(exc) + cast("EventsEndpoint", self.rest_manager.get_endpoint("/api/events")).on_tribler_exception(exc) raise exc def attach_exception_handler(self) -> None: diff --git a/src/tribler/core/socks5/aiohttp_connector.py b/src/tribler/core/socks5/aiohttp_connector.py index c7044055ae8..f95fb5e0a06 100644 --- a/src/tribler/core/socks5/aiohttp_connector.py +++ b/src/tribler/core/socks5/aiohttp_connector.py @@ -2,7 +2,7 @@ import socket from asyncio import BaseTransport, wait_for -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING from aiohttp import TCPConnector from aiohttp.abc import AbstractResolver @@ -10,6 +10,8 @@ from tribler.core.socks5.client import Socks5Client, Socks5ClientUDPConnection if TYPE_CHECKING: + from collections.abc import Callable + from aiohttp.abc import ResolveResult diff --git a/src/tribler/core/socks5/client.py b/src/tribler/core/socks5/client.py index 4ac61b90a18..825df449139 100644 --- a/src/tribler/core/socks5/client.py +++ b/src/tribler/core/socks5/client.py @@ -4,7 +4,7 @@ import logging import socket from asyncio import BaseTransport, DatagramProtocol, DatagramTransport, Protocol, Queue, WriteTransport, get_event_loop -from typing import Callable, cast +from typing import TYPE_CHECKING, cast from ipv8.messaging.interfaces.udp.endpoint import DomainAddress from ipv8.messaging.serialization import PackError @@ -22,6 +22,9 @@ socks5_serializer, ) +if TYPE_CHECKING: + from collections.abc import Callable + class Socks5Error(Exception): """ @@ -47,7 +50,7 @@ def connection_made(self, transport: BaseTransport) -> None: """ Callback for when a transport is available. """ - self.transport = cast(DatagramTransport, transport) + self.transport = cast("DatagramTransport", transport) def datagram_received(self, data: bytes, _: tuple) -> None: """ @@ -109,7 +112,7 @@ async def _send(self, data: bytes) -> bytes: """ Send data to the remote and wait for an answer. """ - cast(WriteTransport, self.transport).write(data) + cast("WriteTransport", self.transport).write(data) return await self.queue.get() async def _login(self) -> None: @@ -194,7 +197,7 @@ async def associate_udp(self) -> None: Login and associate with the proxy. """ if self.connected: - connection = cast(tuple, self.connected_to) + connection = cast("tuple", self.connected_to) msg = f"Client already used for connecting to {connection[0]}:{connection[1]}" raise Socks5Error(msg) @@ -211,7 +214,7 @@ def sendto(self, data: bytes, target_addr: tuple) -> None: if not self.associated: msg = "Not associated yet. First call associate_udp." raise Socks5Error(msg) - cast(Socks5ClientUDPConnection, self.connection).sendto(data, target_addr) + cast("Socks5ClientUDPConnection", self.connection).sendto(data, target_addr) async def connect_tcp(self, target_addr: tuple) -> None: """ @@ -234,4 +237,4 @@ def write(self, data: bytes) -> None: if not self.connected: msg = "Not connected yet. First call connect_tcp." raise Socks5Error(msg) - cast(WriteTransport, self.transport).write(data) + cast("WriteTransport", self.transport).write(data) diff --git a/src/tribler/core/socks5/conversion.py b/src/tribler/core/socks5/conversion.py index 2b2b053abe5..caf20c4be1b 100644 --- a/src/tribler/core/socks5/conversion.py +++ b/src/tribler/core/socks5/conversion.py @@ -3,7 +3,7 @@ import logging import socket import struct -from typing import Any, Union +from typing import Any from ipv8.messaging.interfaces.udp.endpoint import DomainAddress, UDPv4Address from ipv8.messaging.lazy_payload import VariablePayload, vp_compile @@ -107,7 +107,7 @@ class UdpPacket(VariablePayload): data: bytes -class Socks5Address(Packer[Union[DomainAddress, tuple], Any]): +class Socks5Address(Packer[DomainAddress | tuple, Any]): """ A socks5 address data packer. """ diff --git a/src/tribler/core/socks5/server.py b/src/tribler/core/socks5/server.py index 3ba5bc1bf95..1b7ffff2132 100644 --- a/src/tribler/core/socks5/server.py +++ b/src/tribler/core/socks5/server.py @@ -2,7 +2,7 @@ import logging from asyncio import get_event_loop -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING from tribler.core.socks5.connection import Socks5Connection @@ -29,7 +29,7 @@ def __init__(self, hops: int, port: int | None = None, output_stream: TunnelDisp self.port = port self.output_stream = output_stream self.server: Server | None = None - self.sessions: List[Socks5Connection] = [] + self.sessions: list[Socks5Connection] = [] self.rust_endpoint = rust_endpoint async def start(self) -> None: diff --git a/src/tribler/core/torrent_checker/dataclasses.py b/src/tribler/core/torrent_checker/healthdataclasses.py similarity index 100% rename from src/tribler/core/torrent_checker/dataclasses.py rename to src/tribler/core/torrent_checker/healthdataclasses.py diff --git a/src/tribler/core/torrent_checker/torrent_checker.py b/src/tribler/core/torrent_checker/torrent_checker.py index 17fe2838162..4042dcd6868 100644 --- a/src/tribler/core/torrent_checker/torrent_checker.py +++ b/src/tribler/core/torrent_checker/torrent_checker.py @@ -7,7 +7,7 @@ from asyncio import CancelledError, DatagramTransport from binascii import hexlify from collections import defaultdict -from typing import TYPE_CHECKING, Any, Dict, List, Tuple, cast +from typing import TYPE_CHECKING, Any, cast from ipv8.taskmanager import TaskManager from pony.orm import db_session, desc, select @@ -15,7 +15,7 @@ from tribler.core.libtorrent.trackers import MalformedTrackerURLException, is_valid_url from tribler.core.notifier import Notification, Notifier -from tribler.core.torrent_checker.dataclasses import HEALTH_FRESHNESS_SECONDS, HealthInfo, TrackerResponse +from tribler.core.torrent_checker.healthdataclasses import HEALTH_FRESHNESS_SECONDS, HealthInfo, TrackerResponse from tribler.core.torrent_checker.torrentchecker_session import ( FakeDHTSession, TrackerSession, @@ -40,7 +40,7 @@ TORRENTS_CHECKED_RETURN_SIZE = 240 # Estimated torrents checked on default 4 hours idle run -def aggregate_responses_for_infohash(infohash: bytes, responses: List[TrackerResponse]) -> HealthInfo: +def aggregate_responses_for_infohash(infohash: bytes, responses: list[TrackerResponse]) -> HealthInfo: """ Finds the "best" health info (with the max number of seeders) for a specified infohash. """ @@ -63,7 +63,7 @@ def __init__(self, notifier: Notifier, tracker_manager: TrackerManager, metadata_store: MetadataStore, - socks_listen_ports: List[int] | None = None) -> None: + socks_listen_ports: list[int] | None = None) -> None: """ Create a new TorrentChecker. """ @@ -84,7 +84,7 @@ def __init__(self, # We keep track of the results of popular torrents checked by you. # The content_discovery community gossips this information around. - self._torrents_checked: Dict[bytes, HealthInfo] | None = None + self._torrents_checked: dict[bytes, HealthInfo] | None = None async def initialize(self) -> None: """ @@ -213,7 +213,7 @@ async def get_tracker_response(self, session: TrackerSession) -> TrackerResponse return result @property - def torrents_checked(self) -> Dict[bytes, HealthInfo]: + def torrents_checked(self) -> dict[bytes, HealthInfo]: """ Get the checked torrents and their health information. """ @@ -225,7 +225,7 @@ def torrents_checked(self) -> Dict[bytes, HealthInfo]: return self._torrents_checked @db_session - def load_torrents_checked_from_db(self) -> Dict[bytes, HealthInfo]: + def load_torrents_checked_from_db(self) -> dict[bytes, HealthInfo]: """ Load the health information from the database. """ @@ -271,7 +271,7 @@ def torrents_to_check(self) -> list: selected_torrents = popular_torrents + old_torrents return random.sample(selected_torrents, min(TORRENT_SELECTION_POOL_SIZE, len(selected_torrents))) - async def check_local_torrents(self) -> Tuple[List, List]: + async def check_local_torrents(self) -> tuple[list, list]: """ Perform a full health check on a few popular and old torrents in the database. """ @@ -353,7 +353,7 @@ async def check_torrent_health(self, infohash: bytes, timeout: float = 20, scrap responses = await asyncio.gather(*coroutines, return_exceptions=True) self._logger.info("%d responses for %s have been received: %s", len(responses), infohash_hex, str(responses)) successful_responses = [response for response in responses if not isinstance(response, Exception)] - health = aggregate_responses_for_infohash(infohash, cast(List[TrackerResponse], successful_responses)) + health = aggregate_responses_for_infohash(infohash, cast("list[TrackerResponse]", successful_responses)) if health.last_check == 0: self.notify(health) # We don't need to store this in the db, but we still need to notify the GUI else: @@ -372,7 +372,7 @@ def create_session_for_request(self, tracker_url: str, timeout: float = 20) -> T self._logger.warning("Dropping the request. Required amount of hops not reached. " "Required hops: %d. Actual hops: %d", required_hops, actual_hops) return None - listen_ports = cast(List[int], self.socks_listen_ports) # Guaranteed by check above + listen_ports = cast("list[int]", self.socks_listen_ports) # Guaranteed by check above proxy = ('127.0.0.1', listen_ports[required_hops - 1]) if required_hops > 0 else None session = create_tracker_session(tracker_url, timeout, proxy, self.socket_mgr) self._logger.info("Tracker session has been created: %s", str(session)) diff --git a/src/tribler/core/torrent_checker/torrentchecker_session.py b/src/tribler/core/torrent_checker/torrentchecker_session.py index f8990a072f0..8d1b4406e4c 100644 --- a/src/tribler/core/torrent_checker/torrentchecker_session.py +++ b/src/tribler/core/torrent_checker/torrentchecker_session.py @@ -6,11 +6,9 @@ import struct import time from abc import ABCMeta, abstractmethod -from asyncio import DatagramProtocol, Future, Task, ensure_future, get_event_loop -from asyncio import TimeoutError as AsyncTimeoutError -from typing import TYPE_CHECKING, Any, List, NoReturn, cast +from asyncio import DatagramProtocol, Future, Task, ensure_future, get_event_loop, timeout +from typing import TYPE_CHECKING, Any, NoReturn, cast -import async_timeout import libtorrent as lt from aiohttp import ClientResponseError, ClientSession, ClientTimeout from ipv8.taskmanager import TaskManager @@ -18,7 +16,7 @@ from tribler.core.libtorrent.trackers import add_url_params, parse_tracker_url from tribler.core.socks5.aiohttp_connector import Socks5Connector from tribler.core.socks5.client import Socks5Client -from tribler.core.torrent_checker.dataclasses import HealthInfo, TrackerResponse +from tribler.core.torrent_checker.healthdataclasses import HealthInfo, TrackerResponse if TYPE_CHECKING: from ipv8.messaging.interfaces.udp.endpoint import DomainAddress @@ -170,11 +168,11 @@ def process_scrape_response(self, body: bytes | None) -> TrackerResponse: if body is None: self.failed(msg="no response body") - response_dict = cast(dict[bytes, Any], lt.bdecode(body)) + response_dict = cast("dict[bytes, Any]", lt.bdecode(body)) if not response_dict: self.failed(msg="no valid response") - health_list: List[HealthInfo] = [] + health_list: list[HealthInfo] = [] now = int(time.time()) unprocessed_infohashes = set(self.infohash_list) @@ -352,7 +350,7 @@ async def connect_to_tracker(self) -> TrackerResponse: await self.cancel_pending_task("resolve") try: - async with async_timeout.timeout(self.timeout): + async with timeout(self.timeout): # We only resolve the hostname if we're not using a proxy. # If a proxy is used, the TunnelCommunity will resolve the hostname at the exit nodes. if not self.proxy: @@ -365,7 +363,7 @@ async def connect_to_tracker(self) -> TrackerResponse: self.ip_address = infos[0][-1][0] await self.connect() return await self.scrape() - except AsyncTimeoutError: + except TimeoutError: self.failed(msg="request timed out") except socket.gaierror as e: self.failed(msg=str(e)) @@ -383,7 +381,7 @@ async def connect(self) -> None: if isinstance(raw_response, Exception): self.failed(msg=str(raw_response)) - response = cast(bytes, raw_response) + response = cast("bytes", raw_response) # check message size if len(response) < 16: @@ -418,7 +416,7 @@ async def scrape(self) -> TrackerResponse: raw_response = await self.socket_mgr.send_request(message, self) if isinstance(raw_response, Exception): self.failed(msg=str(raw_response)) - response = cast(bytes, raw_response) + response = cast("bytes", raw_response) # check message size if len(response) < 8: diff --git a/src/tribler/core/torrent_checker/tracker_manager.py b/src/tribler/core/torrent_checker/tracker_manager.py index 38b596bd790..997df1efffe 100644 --- a/src/tribler/core/torrent_checker/tracker_manager.py +++ b/src/tribler/core/torrent_checker/tracker_manager.py @@ -42,7 +42,7 @@ def load_blacklist(self) -> None: if blacklist_file.exists(): with open(blacklist_file) as blacklist_file_handle: # Note that get_uniformed_tracker_url will strip the newline at the end of .readlines() - self.blacklist.extend([get_uniformed_tracker_url(url) for url in blacklist_file_handle.readlines()]) + self.blacklist.extend([get_uniformed_tracker_url(url) for url in blacklist_file_handle]) else: self._logger.info("No tracker blacklist file found at %s.", blacklist_file) diff --git a/src/tribler/core/tunnel/community.py b/src/tribler/core/tunnel/community.py index e158a64a357..7def918365e 100644 --- a/src/tribler/core/tunnel/community.py +++ b/src/tribler/core/tunnel/community.py @@ -3,13 +3,11 @@ import hashlib import math import time -from asyncio import Future, open_connection -from asyncio import TimeoutError as AsyncTimeoutError +from asyncio import Future, open_connection, timeout from binascii import hexlify, unhexlify from collections import Counter from typing import TYPE_CHECKING -import async_timeout from ipv8.messaging.anonymization.community import unpack_cell from ipv8.messaging.anonymization.hidden_services import HiddenTunnelCommunity, HiddenTunnelSettings from ipv8.messaging.anonymization.tunnel import ( @@ -441,7 +439,7 @@ async def on_http_request(self, source_address: Address, payload: HTTPRequestPay writer = None try: - async with async_timeout.timeout(10): + async with timeout(10): self.logger.debug("Opening TCP connection to %s", payload.target) reader, writer = await open_connection(*payload.target) writer.write(payload.request) @@ -456,7 +454,7 @@ async def on_http_request(self, source_address: Address, payload: HTTPRequestPay except OSError: self.logger.warning("Tunnel HTTP request failed") return - except AsyncTimeoutError: + except TimeoutError: self.logger.warning("Tunnel HTTP request timed out") return finally: diff --git a/src/tribler/core/versioning/manager.py b/src/tribler/core/versioning/manager.py index 0767b68b6d0..9e859c701ed 100644 --- a/src/tribler/core/versioning/manager.py +++ b/src/tribler/core/versioning/manager.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import TYPE_CHECKING -from aiohttp import ClientSession +from aiohttp import ClientSession, ClientTimeout from packaging.version import Version from tribler.tribler_config import TriblerConfigManager @@ -70,11 +70,10 @@ async def check_version(self) -> str | None: for url in urls: try: async with ClientSession(raise_for_status=True) as session: - response = await session.get(url, headers=headers, timeout=5.0) + response = await session.get(url, headers=headers, timeout=ClientTimeout(total=5.0)) response_dict = await response.json(content_type=None) response_version = response_dict["name"] - if response_version.startswith("v"): - response_version = response_version[1:] + response_version = response_version.removeprefix("v") except Exception as e: logger.info(e) continue # Case 1: this failed, but we may still have another URL to check. Continue. diff --git a/src/tribler/core/versioning/restapi/versioning_endpoint.py b/src/tribler/core/versioning/restapi/versioning_endpoint.py index 6f0b216abc2..b9b69cfa4cb 100644 --- a/src/tribler/core/versioning/restapi/versioning_endpoint.py +++ b/src/tribler/core/versioning/restapi/versioning_endpoint.py @@ -11,11 +11,10 @@ from tribler.tribler_config import VERSION_SUBDIR if TYPE_CHECKING: - from typing_extensions import TypeAlias from tribler.core.restapi.rest_manager import TriblerRequest from tribler.core.versioning.manager import VersioningManager - RequestType: TypeAlias = TriblerRequest[tuple[VersioningManager]] + type RequestType = TriblerRequest[tuple[VersioningManager]] class VersioningEndpoint(RESTEndpoint): diff --git a/src/tribler/test_unit/core/content_discovery/test_payload.py b/src/tribler/test_unit/core/content_discovery/test_payload.py index 3eb16e8914c..af4ada3b619 100644 --- a/src/tribler/test_unit/core/content_discovery/test_payload.py +++ b/src/tribler/test_unit/core/content_discovery/test_payload.py @@ -11,7 +11,7 @@ VersionRequest, VersionResponse, ) -from tribler.core.torrent_checker.dataclasses import HealthInfo, Source +from tribler.core.torrent_checker.healthdataclasses import HealthInfo, Source class TestContentDiscoveryPayloads(TestBase): diff --git a/src/tribler/test_unit/core/database/restapi/test_database_endpoint.py b/src/tribler/test_unit/core/database/restapi/test_database_endpoint.py index 6524f759e1c..7f9b7cb0726 100644 --- a/src/tribler/test_unit/core/database/restapi/test_database_endpoint.py +++ b/src/tribler/test_unit/core/database/restapi/test_database_endpoint.py @@ -1,7 +1,7 @@ from __future__ import annotations from asyncio import sleep -from typing import Callable +from typing import TYPE_CHECKING from unittest.mock import AsyncMock, Mock, call from ipv8.test.base import TestBase @@ -12,6 +12,9 @@ from tribler.core.database.serialization import REGULAR_TORRENT from tribler.core.restapi.rest_endpoint import HTTP_BAD_REQUEST +if TYPE_CHECKING: + from collections.abc import Callable + class TestDatabaseEndpoint(TestBase): """ diff --git a/src/tribler/test_unit/core/database/test_serialization.py b/src/tribler/test_unit/core/database/test_serialization.py index 9b23bafb093..34fdf877c98 100644 --- a/src/tribler/test_unit/core/database/test_serialization.py +++ b/src/tribler/test_unit/core/database/test_serialization.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from ipv8.keyvault.crypto import default_eccrypto from ipv8.test.base import TestBase @@ -26,8 +26,8 @@ def test_time2int(self) -> None: """ Test if time2int normalizes timestamps based on the supplied epoch. """ - date = datetime.fromtimestamp(1234, tz=timezone.utc) - epoch = datetime.fromtimestamp(1000, tz=timezone.utc) + date = datetime.fromtimestamp(1234, tz=UTC) + epoch = datetime.fromtimestamp(1000, tz=UTC) self.assertEqual(234, time2int(date, epoch)) @@ -35,9 +35,9 @@ def test_int2time(self) -> None: """ Test if int2time normalizes timestamps based on the supplied epoch. """ - epoch = datetime.fromtimestamp(1000, tz=timezone.utc) + epoch = datetime.fromtimestamp(1000, tz=UTC) - self.assertEqual(datetime.fromtimestamp(1234, tz=timezone.utc), int2time(234, epoch)) + self.assertEqual(datetime.fromtimestamp(1234, tz=UTC), int2time(234, epoch)) def test_read_payload_with_offset_unknown(self) -> None: """ diff --git a/src/tribler/test_unit/core/libtorrent/restapi/test_torrentinfo_endpoint.py b/src/tribler/test_unit/core/libtorrent/restapi/test_torrentinfo_endpoint.py index 3798c495317..ad01a6848a5 100644 --- a/src/tribler/test_unit/core/libtorrent/restapi/test_torrentinfo_endpoint.py +++ b/src/tribler/test_unit/core/libtorrent/restapi/test_torrentinfo_endpoint.py @@ -1,4 +1,3 @@ -from asyncio import TimeoutError as AsyncTimeoutError from pathlib import Path from ssl import SSLError from unittest.mock import AsyncMock, Mock, patch @@ -273,7 +272,7 @@ async def test_get_torrent_info_http_timeouterror(self) -> None: with patch.dict(tribler.core.libtorrent.restapi.torrentinfo_endpoint.__dict__, {"unshorten": mock_unshorten}), \ patch("tribler.core.libtorrent.restapi.torrentinfo_endpoint.query_uri", - AsyncMock(side_effect=AsyncTimeoutError("test"))): + AsyncMock(side_effect=TimeoutError("test"))): response = await self.endpoint.get_torrent_info(request) response_body_json = await response_to_json(response) @@ -386,7 +385,7 @@ async def test_get_torrent_info_https_timeouterror(self) -> None: with patch.dict(tribler.core.libtorrent.restapi.torrentinfo_endpoint.__dict__, {"unshorten": mock_unshorten}), \ patch("tribler.core.libtorrent.restapi.torrentinfo_endpoint.query_uri", - AsyncMock(side_effect=AsyncTimeoutError("test"))): + AsyncMock(side_effect=TimeoutError("test"))): response = await self.endpoint.get_torrent_info(request) response_body_json = await response_to_json(response) diff --git a/src/tribler/test_unit/core/restapi/test_rest_manager.py b/src/tribler/test_unit/core/restapi/test_rest_manager.py index ceb1aab35ff..b23e1c362f4 100644 --- a/src/tribler/test_unit/core/restapi/test_rest_manager.py +++ b/src/tribler/test_unit/core/restapi/test_rest_manager.py @@ -20,7 +20,7 @@ from tribler.test_unit.mocks import MockTriblerConfigManager if TYPE_CHECKING: - from typing_extensions import Self + from typing import Self class GenericRequest(MockRequest): diff --git a/src/tribler/test_unit/core/socks5/test_server.py b/src/tribler/test_unit/core/socks5/test_server.py index aa7b591678e..e3e483b1f89 100644 --- a/src/tribler/test_unit/core/socks5/test_server.py +++ b/src/tribler/test_unit/core/socks5/test_server.py @@ -3,7 +3,7 @@ import socket from asyncio import Transport, get_event_loop, sleep from asyncio.base_events import Server -from typing import Callable +from typing import TYPE_CHECKING from unittest.mock import Mock, patch from ipv8.test.base import TestBase @@ -12,6 +12,9 @@ from tribler.core.socks5.conversion import UdpPacket, socks5_serializer from tribler.core.socks5.server import Socks5Server +if TYPE_CHECKING: + from collections.abc import Callable + class MockTransport(Transport): """ diff --git a/src/tribler/test_unit/core/torrent_checker/mocks.py b/src/tribler/test_unit/core/torrent_checker/mocks.py index f860f0a0fcb..766ec5a5956 100644 --- a/src/tribler/test_unit/core/torrent_checker/mocks.py +++ b/src/tribler/test_unit/core/torrent_checker/mocks.py @@ -1,11 +1,12 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, Iterator, Set +from typing import TYPE_CHECKING -from tribler.core.torrent_checker.dataclasses import HealthInfo +from tribler.core.torrent_checker.healthdataclasses import HealthInfo if TYPE_CHECKING: - from typing_extensions import Self + from collections.abc import Callable, Iterator + from typing import Self class DBResult(list): @@ -100,7 +101,7 @@ class MockTrackerState(MockEntity): instances = [] - def __init__(self, url: str = "", last_check: int = 0, alive: bool = True, torrents: Set | None = None, + def __init__(self, url: str = "", last_check: int = 0, alive: bool = True, torrents: set | None = None, failures: int = 0) -> None: """ Create a new MockTrackerState and add it to our known instances. @@ -123,8 +124,8 @@ class MockTorrentState(MockEntity): instances = [] def __init__(self, infohash: bytes = b"", seeders: int = 0, leechers: int = 0, last_check: int = 0, # noqa: PLR0913 - self_checked: bool = False, has_data: bool = True, metadata: Set | None = None, - trackers: Set | None = None) -> None: + self_checked: bool = False, has_data: bool = True, metadata: set | None = None, + trackers: set | None = None) -> None: """ Create a new MockTrackerState and add it to our known instances. """ diff --git a/src/tribler/test_unit/core/torrent_checker/test_dataclasses.py b/src/tribler/test_unit/core/torrent_checker/test_dataclasses.py index 6e3e6330198..db8b87ba0a9 100644 --- a/src/tribler/test_unit/core/torrent_checker/test_dataclasses.py +++ b/src/tribler/test_unit/core/torrent_checker/test_dataclasses.py @@ -2,7 +2,7 @@ from ipv8.test.base import TestBase -from tribler.core.torrent_checker.dataclasses import HealthInfo +from tribler.core.torrent_checker.healthdataclasses import HealthInfo class TestHealthInfo(TestBase): diff --git a/src/tribler/test_unit/core/torrent_checker/test_torrent_checker.py b/src/tribler/test_unit/core/torrent_checker/test_torrent_checker.py index 5bf1ac9b0ef..7b236dd5ac7 100644 --- a/src/tribler/test_unit/core/torrent_checker/test_torrent_checker.py +++ b/src/tribler/test_unit/core/torrent_checker/test_torrent_checker.py @@ -12,7 +12,7 @@ import tribler from tribler.core.notifier import Notification, Notifier -from tribler.core.torrent_checker.dataclasses import ( +from tribler.core.torrent_checker.healthdataclasses import ( TOLERABLE_TIME_DRIFT, HealthInfo, TrackerResponse, diff --git a/src/tribler/test_unit/core/torrent_checker/test_torrentchecker_session.py b/src/tribler/test_unit/core/torrent_checker/test_torrentchecker_session.py index 4e127307d03..86ab1938b38 100644 --- a/src/tribler/test_unit/core/torrent_checker/test_torrentchecker_session.py +++ b/src/tribler/test_unit/core/torrent_checker/test_torrentchecker_session.py @@ -7,7 +7,7 @@ from ipv8.util import succeed from libtorrent import bencode -from tribler.core.torrent_checker.dataclasses import HealthInfo +from tribler.core.torrent_checker.healthdataclasses import HealthInfo from tribler.core.torrent_checker.torrentchecker_session import ( FakeBep33DHTSession, FakeDHTSession,