Compare commits

..

3 commits

Author SHA1 Message Date
Calum Lind
0b5f45b486
Merge branch 'release-2.1.1' 2022-07-10 13:20:41 +01:00
Calum Lind
05e13a6b20
Release 2.1.1 2022-07-10 13:15:45 +01:00
Calum Lind
20efcfd345
Release 2.1.0 2022-06-28 19:52:35 +01:00
578 changed files with 335973 additions and 350108 deletions

View file

@ -19,37 +19,37 @@ on:
jobs:
windows_package:
runs-on: windows-2022
runs-on: windows-2019
if: (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'package'))
strategy:
matrix:
arch: [x64, x86]
python: ["3.9"]
libtorrent: [2.0.7, 1.2.19]
libtorrent: [2.0.6, 1.2.15]
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
fetch-depth: 0
# Checkout Deluge source to subdir to enable packaging any tag/commit
- name: Checkout Deluge source
uses: actions/checkout@v4
uses: actions/checkout@v3
with:
ref: ${{ github.event.inputs.ref }}
fetch-depth: 0
path: deluge_src
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python}}
architecture: ${{ matrix.arch }}
cache: pip
- name: Prepare pip
run: python -m pip install wheel setuptools==68.*
run: python -m pip install wheel
- name: Install GTK
run: |
@ -62,14 +62,11 @@ jobs:
python -m pip install --no-index --find-links="C:\GTK\release\python" pycairo PyGObject
- name: Install Python dependencies
# Pillow no longer provides 32-bit wheels for Windows
# so specify only-binary to install old version.
run: >
python -m pip install
--only-binary=pillow
twisted[tls]==22.8.0
twisted[tls]==22.4.0
libtorrent==${{ matrix.libtorrent }}
pyinstaller
pyinstaller==4.10
pygame
-r requirements.txt
@ -84,13 +81,12 @@ jobs:
run: |
pyinstaller --clean delugewin.spec --distpath freeze
- name: Verify Deluge exes
working-directory: packaging/win/freeze/Deluge/
- name: Fix OpenSSL for libtorrent x64
if: ${{ matrix.arch == 'x64' }}
working-directory: packaging/win/freeze/Deluge
run: |
deluge-debug.exe -v
deluged-debug.exe -v
deluge-web-debug.exe -v
deluge-console -v
cp libssl-1_1.dll libssl-1_1-x64.dll
cp libcrypto-1_1.dll libcrypto-1_1-x64.dll
- name: Make Deluge Installer
working-directory: ./packaging/win
@ -98,7 +94,7 @@ jobs:
python setup_nsis.py
makensis /Darch=${{ matrix.arch }} deluge-win-installer.nsi
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v2
with:
name: deluge-py${{ matrix.python }}-lt${{ matrix.libtorrent }}-${{ matrix.arch }}
path: packaging/win/*.exe

View file

@ -6,25 +6,22 @@ on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
inputs:
core-dump:
description: "Set to 1 to enable retrieving core dump from crashes"
default: "0"
jobs:
test-linux:
runs-on: ubuntu-22.04
runs-on: ubuntu-20.04
strategy:
matrix:
python-version: ["3.7", "3.10"]
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
@ -36,8 +33,8 @@ jobs:
- name: Install dependencies
run: |
pip install --upgrade pip wheel setuptools
pip install -r requirements-ci.txt
pip install --upgrade pip wheel
pip install -r requirements.txt -r requirements-tests.txt
pip install -e .
- name: Install security dependencies
@ -49,21 +46,18 @@ jobs:
TESTSSL_VER: 3.0.6
TESTSSL_URL: https://codeload.github.com/drwetter/testssl.sh/tar.gz/refs/tags/v
- name: Setup core dump catch and store
if: github.event.inputs.core-dump == '1'
- name: Setup core dump directory
run: |
sudo mkdir /cores/ && sudo chmod 777 /cores/
echo "/cores/%E.%p" | sudo tee /proc/sys/kernel/core_pattern
ulimit -c unlimited
sudo apt install glibc-tools
echo "DEBUG_PREFIX=catchsegv python -X dev -m" >> $GITHUB_ENV
- name: Test with pytest
run: |
ulimit -c unlimited # Enable core dumps to be captured
python -c 'from deluge._libtorrent import lt; print(lt.__version__)';
$DEBUG_PREFIX pytest -v -m "not (todo or gtkui)" deluge
catchsegv python -X dev -m pytest -v -m "not (todo or gtkui)" deluge
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v2
# capture all crashes as build artifacts
if: failure()
with:
@ -71,19 +65,19 @@ jobs:
path: /cores
test-windows:
runs-on: windows-2022
runs-on: windows-2019
strategy:
matrix:
python-version: ["3.7", "3.10"]
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
cache: "pip"
@ -91,8 +85,8 @@ jobs:
- name: Install dependencies
run: |
pip install --upgrade pip wheel setuptools
pip install -r requirements-ci.txt
pip install --upgrade pip wheel
pip install -r requirements.txt -r requirements-tests.txt
pip install -e .
- name: Test with pytest

View file

@ -15,23 +15,30 @@ jobs:
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- uses: actions/checkout@v2
with:
fetch-depth: 0
- uses: actions/setup-python@v5
- uses: actions/setup-python@v2
with:
python-version: "3.10"
cache: "pip"
cache-dependency-path: "requirements*.txt"
python-version: "3.8"
- name: Cache pip
uses: actions/cache@v2
with:
# This path is specific to Ubuntu
path: ~/.cache/pip
# Look to see if there is a cache hit for the corresponding requirements file
key: ${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Install dependencies
run: |
pip install --upgrade pip wheel
pip install tox
sudo apt-get install enchant-2
sudo apt-get install enchant
- name: Build docs with tox
- name: Test with tox
env:
TOX_ENV: docs
run: |

View file

@ -11,7 +11,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
- name: Run pre-commit linting
uses: pre-commit/action@v3.0.1
uses: pre-commit/action@v2.0.2

4
.gitignore vendored
View file

@ -12,14 +12,14 @@ __pycache__/
*.tar.*
.tox/
deluge/i18n/*/
deluge.pot
deluge/ui/web/js/*.js
deluge/ui/web/js/extjs/ext-extensions*.js
*.desktop
*.metainfo.xml
*.appdata.xml
.build_data*
osx/app
RELEASE-VERSION
.venv*
# used by setuptools to cache downloaded eggs
/.eggs
_pytest_temp/

View file

@ -6,25 +6,35 @@ exclude: >
deluge/tests/data/.*svg|
)$
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.6.4
- repo: https://github.com/psf/black
rev: 22.3.0
hooks:
- id: ruff
name: Chk Ruff
args: [--fix]
- id: ruff-format
name: Fmt Ruff
- id: black
name: Fmt Black
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
rev: v2.5.1
hooks:
- id: prettier
name: Fmt Prettier
# Workaround to list modified files only.
args: [--list-different]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
name: Fmt isort
- repo: https://github.com/pycqa/flake8
rev: 4.0.1
hooks:
- id: flake8
name: Chk Flake8
additional_dependencies:
- pep8-naming==0.12.1
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.1.0
hooks:
- id: double-quote-string-fixer
name: Fix Double-quotes
- id: end-of-file-fixer
name: Fix End-of-files
exclude_types: [javascript, css]
@ -34,8 +44,8 @@ repos:
- id: trailing-whitespace
name: Fix Trailing whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
rev: v2.31.0
hooks:
- id: pyupgrade
args: [--py37-plus]
args: [--py36-plus]
stages: [manual]

View file

@ -5,14 +5,6 @@
# Required
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.10"
jobs:
post_checkout:
- git fetch --unshallow || true
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/source/conf.py
@ -22,8 +14,9 @@ formats: all
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- requirements: requirements.txt
- requirements: docs/requirements.txt
- method: pip
- method: setuptools
path: .

View file

@ -1,15 +1,5 @@
# Changelog
## 2.2.x (TBA)
### Breaking changes
- Python 3.6 support removed (Python >= 3.7)
### Web UI
- Accept network interface name in addition to IP adress in "Incoming Address"
## 2.1.1 (2022-07-10)
### Core

View file

@ -50,7 +50,7 @@ All modules will require the [common](#common) section dependencies.
- [PyGObject]
- [Pycairo]
- [librsvg] _>= 2_
- [ayatanaappindicator3] w/GIR - Optional: Ubuntu system tray icon.
- [libappindicator3] w/GIR - Optional: Ubuntu system tray icon.
### MacOS
@ -95,6 +95,6 @@ All modules will require the [common](#common) section dependencies.
[mako]: https://www.makotemplates.org/
[pygame]: https://www.pygame.org/
[libnotify]: https://developer.gnome.org/libnotify/
[ayatanaappindicator3]: https://lazka.github.io/pgi-docs/AyatanaAppIndicator3-0.1/index.html
[python-appindicator]: https://packages.ubuntu.com/xenial/python-appindicator
[librsvg]: https://wiki.gnome.org/action/show/Projects/LibRsvg
[ifaddr]: https://pypi.org/project/ifaddr/

View file

@ -1,6 +0,0 @@
from twisted.web.http import Request
__request__: Request
def _(string: str) -> str: ...
def _n(string: str) -> str: ...

View file

@ -14,7 +14,6 @@ Example:
>>> from deluge._libtorrent import lt
"""
from deluge.common import VersionSplit, get_version
from deluge.error import LibtorrentImportError

View file

@ -85,6 +85,7 @@ def bdecode(x):
class Bencached:
__slots__ = ['bencoded']
def __init__(self, s):

View file

@ -7,7 +7,6 @@
#
"""Common functions for various parts of Deluge to use."""
import base64
import binascii
import functools
@ -24,21 +23,15 @@ import tarfile
import time
from contextlib import closing
from datetime import datetime
from importlib import resources
from io import BytesIO
from pathlib import Path
from urllib.parse import unquote_plus, urljoin
from urllib.request import pathname2url
import pkg_resources
from deluge.decorators import deprecated
from deluge.error import InvalidPathError
try:
from importlib.metadata import distribution
except ImportError:
from pkg_resources import get_distribution as distribution
try:
import chardet
except ImportError:
@ -97,7 +90,7 @@ def get_version():
Returns:
str: The version of Deluge.
"""
return distribution('Deluge').version
return pkg_resources.get_distribution('Deluge').version
def get_default_config_dir(filename=None):
@ -297,22 +290,20 @@ def get_pixmap(fname):
return resource_filename('deluge', os.path.join('ui', 'data', 'pixmaps', fname))
def resource_filename(module: str, path: str) -> str:
"""Get filesystem path for a non-python resource.
def resource_filename(module, path):
"""Get filesystem path for a resource.
Abstracts getting module resource files. Originally created to
workaround pkg_resources.resource_filename limitations with
multiple Deluge packages installed.
This function contains a work-around for pkg_resources.resource_filename
not returning the correct path with multiple packages installed.
So if there's a second deluge package, installed globally and another in
develop mode somewhere else, while pkg_resources.get_distribution('Deluge')
returns the proper deluge instance, pkg_resources.resource_filename
does not, it returns the first found on the python path, which is wrong.
"""
path = Path(path)
try:
with resources.as_file(resources.files(module) / path) as resource_file:
return str(resource_file)
except AttributeError:
# Python <= 3.8
with resources.path(module, path.parts[0]) as resource_file:
return str(resource_file.joinpath(*path.parts[1:]))
return pkg_resources.get_distribution('Deluge').get_resource_filename(
pkg_resources._manager, os.path.join(*(module.split('.') + [path]))
)
def open_file(path, timestamp=None):
@ -424,31 +415,25 @@ def translate_size_units():
def fsize(fsize_b, precision=1, shortform=False):
"""Formats the bytes value into a string with KiB, MiB, GiB or TiB units.
"""Formats the bytes value into a string with KiB, MiB or GiB units.
Args:
fsize_b (int): The filesize in bytes.
precision (int): The output float precision, 1 by default.
shortform (bool): The output short|long form, False (long form) by default.
precision (int): The filesize float precision.
Returns:
str: A formatted string in KiB, MiB, GiB or TiB units.
str: A formatted string in KiB, MiB or GiB units.
Examples:
>>> fsize(112245)
'109.6 KiB'
>>> fsize(112245, precision=0)
'110 KiB'
>>> fsize(112245, shortform=True)
'109.6 K'
Note:
This function has been refactored for performance with the
fsize units being translated outside the function.
Notice that short forms K|M|G|T are synonymous here with
KiB|MiB|GiB|TiB. They are powers of 1024, not 1000.
"""
if fsize_b >= 1024**4:
@ -484,7 +469,7 @@ def fpcnt(dec, precision=2):
Args:
dec (float): The ratio in the range [0.0, 1.0].
precision (int): The output float precision, 2 by default.
precision (int): The percentage float precision.
Returns:
str: A formatted string representing a percentage.
@ -508,8 +493,6 @@ def fspeed(bps, precision=1, shortform=False):
Args:
bps (int): The speed in bytes per second.
precision (int): The output float precision, 1 by default.
shortform (bool): The output short|long form, False (long form) by default.
Returns:
str: A formatted string representing transfer speed.
@ -518,10 +501,6 @@ def fspeed(bps, precision=1, shortform=False):
>>> fspeed(43134)
'42.1 KiB/s'
Note:
Notice that short forms K|M|G|T are synonymous here with
KiB|MiB|GiB|TiB. They are powers of 1024, not 1000.
"""
if bps < 1024**2:
@ -558,7 +537,7 @@ def fpeer(num_peers, total_peers):
total_peers (int): The total number of peers.
Returns:
str: A formatted string 'num_peers (total_peers)' or if total_peers < 0, just 'num_peers'.
str: A formatted string 'num_peers (total_peers)' or total_peers < 0, just 'num_peers'.
Examples:
>>> fpeer(10, 20)
@ -607,16 +586,16 @@ def ftime(secs):
time_str = f'{secs // 604800}w {secs // 86400 % 7}d'
else:
time_str = f'{secs // 31449600}y {secs // 604800 % 52}w'
return time_str
def fdate(seconds, date_only=False, precision_secs=False):
"""Formats a date time string in the locale's date representation based on the system's timezone.
"""Formats a date time string in the locale's date representation based on the systems timezone.
Args:
seconds (float): Time in seconds since the Epoch.
date_only (bool): Whether to include only the date, False by default.
precision_secs (bool): Include seconds in time format, False by default.
precision_secs (bool): Include seconds in time format.
Returns:
str: A string in the locale's datetime representation or "" if seconds < 0
@ -641,14 +620,10 @@ def tokenize(text):
Returns:
list: A list of strings and/or numbers.
Note:
This function is used to implement robust tokenization of user input
It automatically coerces integer and floating point numbers, ignores
whitespace and knows how to separate numbers from strings even without
whitespace.
Possible optimization: move the 2 regexes outside of function.
This function is used to implement robust tokenization of user input
It automatically coerces integer and floating point numbers, ignores
whitespace and knows how to separate numbers from strings even without
whitespace.
"""
tokenized_input = []
for token in re.split(r'(\d+(?:\.\d+)?)', text):
@ -669,16 +644,12 @@ size_units = [
{'prefix': 'GiB', 'divider': 1024**3},
{'prefix': 'TiB', 'divider': 1024**4},
{'prefix': 'PiB', 'divider': 1024**5},
{'prefix': 'k', 'divider': 1000**1},
{'prefix': 'm', 'divider': 1000**2},
{'prefix': 'g', 'divider': 1000**3},
{'prefix': 't', 'divider': 1000**4},
{'prefix': 'p', 'divider': 1000**5},
{'prefix': 'KB', 'divider': 1000**1},
{'prefix': 'MB', 'divider': 1000**2},
{'prefix': 'GB', 'divider': 1000**3},
{'prefix': 'TB', 'divider': 1000**4},
{'prefix': 'PB', 'divider': 1000**5},
{'prefix': 'm', 'divider': 1000**2},
]
@ -721,16 +692,6 @@ def parse_human_size(size):
raise InvalidSize(msg % (size, tokens))
def anchorify_urls(text: str) -> str:
"""
Wrap all occurrences of text URLs with HTML
"""
url_pattern = r'((htt)|(ft)|(ud))ps?://\S+'
html_href_pattern = r'<a href="\g<0>">\g<0></a>'
return re.sub(url_pattern, html_href_pattern, text)
def is_url(url):
"""
A simple test to check if the URL is valid
@ -872,7 +833,7 @@ def create_magnet_uri(infohash, name=None, trackers=None):
Args:
infohash (str): The info-hash of the torrent.
name (str, optional): The name of the torrent.
trackers (list or dict, optional): A list of trackers or a dict or some {tracker: tier} pairs.
trackers (list or dict, optional): A list of trackers or dict or {tracker: tier} pairs.
Returns:
str: A magnet URI string.
@ -914,7 +875,7 @@ def get_path_size(path):
return os.path.getsize(path)
dir_size = 0
for p, dummy_dirs, files in os.walk(path):
for (p, dummy_dirs, files) in os.walk(path):
for _file in files:
filename = os.path.join(p, _file)
dir_size += os.path.getsize(filename)

View file

@ -59,16 +59,11 @@ class Component:
Deluge core.
**update()** - This method is called every 1 second by default while the
Component is in a *Started* state. The interval can be
Componented is in a *Started* state. The interval can be
specified during instantiation. The update() timer can be
paused by instructing the :class:`ComponentRegistry` to pause
this Component.
**pause()** - This method is called when the component is being paused.
**resume()** - This method is called when the component resumes from a Paused
state.
**shutdown()** - This method is called when the client is exiting. If the
Component is in a "Started" state when this is called, a
call to stop() will be issued prior to shutdown().
@ -85,10 +80,10 @@ class Component:
**Stopped** - The Component has either been stopped or has yet to be started.
**Stopping** - The Component has had its stop method called, but it hasn't
**Stopping** - The Component has had it's stop method called, but it hasn't
fully stopped yet.
**Paused** - The Component has had its update timer stopped, but will
**Paused** - The Component has had it's update timer stopped, but will
still be considered in a Started state.
"""
@ -116,8 +111,9 @@ class Component:
_ComponentRegistry.deregister(self)
def _component_start_timer(self):
self._component_timer = LoopingCall(self.update)
self._component_timer.start(self._component_interval)
if hasattr(self, 'update'):
self._component_timer = LoopingCall(self.update)
self._component_timer.start(self._component_interval)
def _component_start(self):
def on_start(result):
@ -133,10 +129,13 @@ class Component:
return fail(result)
if self._component_state == 'Stopped':
self._component_state = 'Starting'
d = deferLater(reactor, 0, self.start)
d.addCallbacks(on_start, on_start_fail)
self._component_starting_deferred = d
if hasattr(self, 'start'):
self._component_state = 'Starting'
d = deferLater(reactor, 0, self.start)
d.addCallbacks(on_start, on_start_fail)
self._component_starting_deferred = d
else:
d = maybeDeferred(on_start, None)
elif self._component_state == 'Starting':
return self._component_starting_deferred
elif self._component_state == 'Started':
@ -166,11 +165,14 @@ class Component:
return result
if self._component_state != 'Stopped' and self._component_state != 'Stopping':
self._component_state = 'Stopping'
d = maybeDeferred(self.stop)
d.addCallback(on_stop)
d.addErrback(on_stop_fail)
self._component_stopping_deferred = d
if hasattr(self, 'stop'):
self._component_state = 'Stopping'
d = maybeDeferred(self.stop)
d.addCallback(on_stop)
d.addErrback(on_stop_fail)
self._component_stopping_deferred = d
else:
d = maybeDeferred(on_stop, None)
if self._component_state == 'Stopping':
return self._component_stopping_deferred
@ -180,12 +182,13 @@ class Component:
def _component_pause(self):
def on_pause(result):
self._component_state = 'Paused'
if self._component_timer and self._component_timer.running:
self._component_timer.stop()
if self._component_state == 'Started':
d = maybeDeferred(self.pause)
d.addCallback(on_pause)
if self._component_timer and self._component_timer.running:
d = maybeDeferred(self._component_timer.stop)
d.addCallback(on_pause)
else:
d = succeed(None)
elif self._component_state == 'Paused':
d = succeed(None)
else:
@ -202,10 +205,9 @@ class Component:
def _component_resume(self):
def on_resume(result):
self._component_state = 'Started'
self._component_start_timer()
if self._component_state == 'Paused':
d = maybeDeferred(self.resume)
d = maybeDeferred(self._component_start_timer)
d.addCallback(on_resume)
else:
d = fail(
@ -220,7 +222,9 @@ class Component:
def _component_shutdown(self):
def on_stop(result):
return maybeDeferred(self.shutdown)
if hasattr(self, 'shutdown'):
return maybeDeferred(self.shutdown)
return succeed(None)
d = self._component_stop()
d.addCallback(on_stop)
@ -241,12 +245,6 @@ class Component:
def shutdown(self):
pass
def pause(self):
pass
def resume(self):
pass
class ComponentRegistry:
"""The ComponentRegistry holds a list of currently registered :class:`Component` objects.

View file

@ -38,7 +38,6 @@ this can only be done for the 'config file version' and not for the 'format'
version as this will be done internally.
"""
import json
import logging
import os

View file

@ -3,7 +3,7 @@
# the additional special exception to link portions of this program with the OpenSSL library.
# See LICENSE for more details.
#
import asyncio
import tempfile
import warnings
from unittest.mock import Mock, patch
@ -12,7 +12,7 @@ import pytest
import pytest_twisted
from twisted.internet import reactor
from twisted.internet.defer import Deferred, maybeDeferred
from twisted.internet.error import CannotListenError, ProcessTerminated
from twisted.internet.error import CannotListenError
from twisted.python.failure import Failure
import deluge.component as _component
@ -42,18 +42,15 @@ def mock_callback():
The returned Mock instance will have a `deferred` attribute which will complete when the callback has been called.
"""
def reset(timeout=0.5, *args, **kwargs):
def reset():
if mock.called:
original_reset_mock(*args, **kwargs)
if mock.deferred:
mock.deferred.cancel()
deferred = Deferred(canceller=lambda x: deferred.callback(None))
deferred.addTimeout(timeout, reactor)
original_reset_mock()
deferred = Deferred()
deferred.addTimeout(0.5, reactor)
mock.side_effect = lambda *args, **kw: deferred.callback((args, kw))
mock.deferred = deferred
mock = Mock()
mock.__qualname__ = 'mock'
original_reset_mock = mock.reset_mock
mock.reset_mock = reset
mock.reset_mock()
@ -62,9 +59,8 @@ def mock_callback():
@pytest.fixture
def config_dir(tmp_path):
config_dir = tmp_path / 'config'
deluge.configmanager.set_config_dir(config_dir)
yield config_dir
deluge.configmanager.set_config_dir(tmp_path)
yield tmp_path
@pytest_twisted.async_yield_fixture()
@ -88,10 +84,9 @@ async def client(request, config_dir, monkeypatch, listen_port):
@pytest_twisted.async_yield_fixture
async def daemon(request, config_dir, tmp_path):
async def daemon(request, config_dir):
listen_port = DEFAULT_LISTEN_PORT
logfile = tmp_path / 'daemon.log'
logfile = f'daemon_{request.node.name}.log'
if hasattr(request.cls, 'daemon_custom_script'):
custom_script = request.cls.daemon_custom_script
else:
@ -121,10 +116,7 @@ async def daemon(request, config_dir, tmp_path):
raise exception_error
daemon.listen_port = listen_port
yield daemon
try:
await daemon.kill()
except ProcessTerminated:
pass
await daemon.kill()
@pytest.fixture(autouse=True)
@ -145,7 +137,7 @@ def common_fixture(config_dir, request, monkeypatch, listen_port):
@pytest_twisted.async_yield_fixture(scope='function')
async def component():
async def component(request):
"""Verify component registry is clean, and clean up after test."""
if len(_component._ComponentRegistry.components) != 0:
warnings.warn(
@ -198,18 +190,3 @@ def mock_mkstemp(tmp_path):
tmp_file = tempfile.mkstemp(dir=tmp_path)
with patch('tempfile.mkstemp', return_value=tmp_file):
yield tmp_file
def pytest_collection_modifyitems(session, config, items) -> None:
"""
Automatically runs async tests with pytest_twisted.ensureDeferred
"""
function_items = (item for item in items if isinstance(item, pytest.Function))
for function_item in function_items:
function = function_item.obj
if hasattr(function, '__func__'):
# methods need to be unwrapped.
function = function.__func__
if asyncio.iscoroutinefunction(function):
# This is how pytest_twisted marks ensureDeferred tests
setattr(function, '_pytest_twisted_mark', 'async_test')

View file

@ -14,16 +14,10 @@ This should typically only be used by the Core. Plugins should utilize the
`:mod:EventManager` for similar functionality.
"""
import contextlib
import logging
import threading
import time
from collections import defaultdict
from functools import partial
from typing import Any, Callable
from types import SimpleNamespace
from twisted.internet import reactor, task, threads
from twisted.internet import reactor
import deluge.component as component
from deluge._libtorrent import lt
@ -37,7 +31,7 @@ class AlertManager(component.Component):
def __init__(self):
log.debug('AlertManager init...')
component.Component.__init__(self, 'AlertManager')
component.Component.__init__(self, 'AlertManager', interval=0.3)
self.session = component.get('Core').session
# Increase the alert queue size so that alerts don't get lost.
@ -58,88 +52,48 @@ class AlertManager(component.Component):
self.session.apply_settings({'alert_mask': alert_mask})
# handlers is a dictionary of lists {"alert_type": [handler1,h2,..]}
self.handlers = defaultdict(list)
self.handlers_timeout_secs = 2
self.handlers = {}
self.delayed_calls = []
self._event = threading.Event()
def update(self):
pass
def start(self):
thread = threading.Thread(
target=self.wait_for_alert_in_thread, name='alert-poller', daemon=True
)
thread.start()
self._event.set()
def stop(self):
self.cancel_delayed_calls()
def pause(self):
self._event.clear()
def resume(self):
self._event.set()
def wait_for_alert_in_thread(self):
while self._component_state not in ('Stopping', 'Stopped'):
if self.check_delayed_calls():
time.sleep(0.05)
continue
if self.session.wait_for_alert(1000) is None:
continue
if self._event.wait():
threads.blockingCallFromThread(reactor, self.maybe_handle_alerts)
def on_delayed_call_timeout(self, result, timeout, **kwargs):
log.warning('Alert handler was timed-out before being called %s', kwargs)
def cancel_delayed_calls(self):
"""Cancel all delayed handlers."""
for delayed_call in self.delayed_calls:
delayed_call.cancel()
self.delayed_calls = []
def check_delayed_calls(self) -> bool:
"""Returns True if any handler calls are delayed."""
self.delayed_calls = [dc for dc in self.delayed_calls if not dc.called]
return len(self.delayed_calls) > 0
def maybe_handle_alerts(self) -> None:
if self._component_state != 'Started':
return
self.delayed_calls = [dc for dc in self.delayed_calls if dc.active()]
self.handle_alerts()
def register_handler(self, alert_type: str, handler: Callable[[Any], None]) -> None:
def stop(self):
for delayed_call in self.delayed_calls:
if delayed_call.active():
delayed_call.cancel()
self.delayed_calls = []
def register_handler(self, alert_type, handler):
"""
Registers a function that will be called when 'alert_type' is pop'd
in handle_alerts. The handler function should look like: handler(alert)
Where 'alert' is the actual alert object from libtorrent.
Args:
alert_type: String representation of the libtorrent alert name.
Can be supplied with or without `_alert` suffix.
handler: Callback function when the alert is raised.
:param alert_type: str, this is string representation of the alert name
:param handler: func(alert), the function to be called when the alert is raised
"""
if alert_type and alert_type.endswith('_alert'):
alert_type = alert_type[: -len('_alert')]
if alert_type not in self.handlers:
# There is no entry for this alert type yet, so lets make it with an
# empty list.
self.handlers[alert_type] = []
# Append the handler to the list in the handlers dictionary
self.handlers[alert_type].append(handler)
log.debug('Registered handler for alert %s', alert_type)
def deregister_handler(self, handler: Callable[[Any], None]):
def deregister_handler(self, handler):
"""
De-registers the `handler` function from all alert types.
De-registers the `:param:handler` function from all alert types.
Args:
handler: The handler function to deregister.
:param handler: func, the handler function to deregister
"""
for alert_type_handlers in self.handlers.values():
with contextlib.suppress(ValueError):
alert_type_handlers.remove(handler)
# Iterate through all handlers and remove 'handler' where found
for (dummy_key, value) in self.handlers.items():
if handler in value:
# Handler is in this alert type list
value.remove(handler)
def handle_alerts(self):
"""
@ -158,32 +112,26 @@ class AlertManager(component.Component):
num_alerts,
)
# Loop through all alerts in the queue
for alert in alerts:
alert_type = alert.what()
alert_type = type(alert).__name__
# Display the alert message
if log.isEnabledFor(logging.DEBUG):
log.debug('%s: %s', alert_type, decode_bytes(alert.message()))
if alert_type not in self.handlers:
continue
# Call any handlers for this alert type
for handler in self.handlers[alert_type]:
if log.isEnabledFor(logging.DEBUG):
log.debug('Handling alert: %s', alert_type)
d = task.deferLater(reactor, 0, handler, alert)
on_handler_timeout = partial(
self.on_delayed_call_timeout,
handler=handler.__qualname__,
alert_type=alert_type,
)
d.addTimeout(
self.handlers_timeout_secs,
reactor,
onTimeoutCancel=on_handler_timeout,
)
self.delayed_calls.append(d)
if alert_type in self.handlers:
for handler in self.handlers[alert_type]:
if log.isEnabledFor(logging.DEBUG):
log.debug('Handling alert: %s', alert_type)
# Copy alert attributes
alert_copy = SimpleNamespace(
**{
attr: getattr(alert, attr)
for attr in dir(alert)
if not attr.startswith('__')
}
)
self.delayed_calls.append(reactor.callLater(0, handler, alert_copy))
def set_alert_queue_size(self, queue_size):
"""Sets the maximum size of the libtorrent alert queue"""

View file

@ -12,16 +12,17 @@ import logging
import os
import shutil
import tempfile
import threading
from base64 import b64decode, b64encode
from typing import Any, Dict, List, Optional, Tuple, Union
from urllib.request import URLError, urlopen
from twisted.internet import defer, reactor, task, threads
from twisted.internet import defer, reactor, task
from twisted.web.client import Agent, readBody
import deluge.common
import deluge.component as component
from deluge import metafile, path_chooser_common
from deluge import path_chooser_common
from deluge._libtorrent import LT_VERSION, lt
from deluge.configmanager import ConfigManager, get_config_dir
from deluge.core.alertmanager import AlertManager
@ -198,7 +199,7 @@ class Core(component.Component):
self.session_status_timer_interval = 0.5
self.session_status_timer = task.LoopingCall(self.session.post_session_stats)
self.alertmanager.register_handler(
'session_stats', self._on_alert_session_stats
'session_stats_alert', self._on_alert_session_stats
)
self.session_rates_timer_interval = 2
self.session_rates_timer = task.LoopingCall(self._update_session_rates)
@ -991,33 +992,31 @@ class Core(component.Component):
path,
tracker,
piece_length,
comment=None,
target=None,
webseeds=None,
private=False,
created_by=None,
trackers=None,
add_to_session=False,
torrent_format=metafile.TorrentFormat.V1,
comment,
target,
webseeds,
private,
created_by,
trackers,
add_to_session,
):
if isinstance(torrent_format, str):
torrent_format = metafile.TorrentFormat(torrent_format)
log.debug('creating torrent..')
return threads.deferToThread(
self._create_torrent_thread,
path,
tracker,
piece_length,
comment=comment,
target=target,
webseeds=webseeds,
private=private,
created_by=created_by,
trackers=trackers,
add_to_session=add_to_session,
torrent_format=torrent_format,
)
threading.Thread(
target=self._create_torrent_thread,
args=(
path,
tracker,
piece_length,
comment,
target,
webseeds,
private,
created_by,
trackers,
add_to_session,
),
).start()
def _create_torrent_thread(
self,
@ -1031,41 +1030,27 @@ class Core(component.Component):
created_by,
trackers,
add_to_session,
torrent_format,
):
from deluge import metafile
filecontent = metafile.make_meta_file_content(
metafile.make_meta_file(
path,
tracker,
piece_length,
comment=comment,
target=target,
webseeds=webseeds,
private=private,
created_by=created_by,
trackers=trackers,
torrent_format=torrent_format,
)
write_file = False
if target or not add_to_session:
write_file = True
if not target:
target = metafile.default_meta_file_path(path)
filename = os.path.split(target)[-1]
if write_file:
with open(target, 'wb') as _file:
_file.write(filecontent)
filedump = b64encode(filecontent)
log.debug('torrent created!')
if add_to_session:
options = {}
options['download_location'] = os.path.split(path)[0]
self.add_torrent_file(filename, filedump, options)
return filename, filedump
with open(target, 'rb') as _file:
filedump = b64encode(_file.read())
self.add_torrent_file(os.path.split(target)[1], filedump, options)
@export
def upload_plugin(self, filename: str, filedump: Union[str, bytes]) -> None:

View file

@ -7,7 +7,6 @@
#
"""The Deluge daemon"""
import logging
import os
import socket

View file

@ -8,7 +8,6 @@
"""PluginManager for Core"""
import logging
from twisted.internet import defer

View file

@ -7,7 +7,6 @@
#
"""RPCServer Module"""
import logging
import os
import sys
@ -47,11 +46,13 @@ TCallable = TypeVar('TCallable', bound=Callable)
@overload
def export(func: TCallable) -> TCallable: ...
def export(func: TCallable) -> TCallable:
...
@overload
def export(auth_level: int) -> Callable[[TCallable], TCallable]: ...
def export(auth_level: int) -> Callable[[TCallable], TCallable]:
...
def export(auth_level=AUTH_LEVEL_DEFAULT):
@ -273,9 +274,9 @@ class DelugeRPCProtocol(DelugeTransferProtocol):
raise IncompatibleClient(deluge.common.get_version())
ret = component.get('AuthManager').authorize(*args, **kwargs)
if ret:
self.factory.authorized_sessions[self.transport.sessionno] = (
self.AuthLevel(ret, args[0])
)
self.factory.authorized_sessions[
self.transport.sessionno
] = self.AuthLevel(ret, args[0])
self.factory.session_protocols[self.transport.sessionno] = self
except Exception as ex:
send_error()
@ -544,8 +545,8 @@ class RPCServer(component.Component):
:type event: :class:`deluge.event.DelugeEvent`
"""
log.debug('intevents: %s', self.factory.interested_events)
# Use copy of `interested_events` since it can mutate while iterating.
for session_id, interest in self.factory.interested_events.copy().items():
# Find sessions interested in this event
for session_id, interest in self.factory.interested_events.items():
if event.name in interest:
log.debug('Emit Event: %s %s', event.name, event.args)
# This session is interested so send a RPC_EVENT

View file

@ -1138,8 +1138,9 @@ class Torrent:
'download_location': lambda: self.options['download_location'],
'seeds_peers_ratio': lambda: -1.0
if self.status.num_incomplete == 0
# Use -1.0 to signify infinity
else (self.status.num_complete / self.status.num_incomplete),
else ( # Use -1.0 to signify infinity
self.status.num_complete / self.status.num_incomplete
),
'seed_rank': lambda: self.status.seed_rank,
'state': lambda: self.state,
'stop_at_ratio': lambda: self.options['stop_at_ratio'],
@ -1543,18 +1544,20 @@ class Torrent:
self.status.pieces, self.handle.piece_availability()
):
if piece:
# Completed.
pieces.append(3)
pieces.append(3) # Completed.
elif avail_piece:
# Available, just not downloaded nor being downloaded.
pieces.append(1)
pieces.append(
1
) # Available, just not downloaded nor being downloaded.
else:
# Missing, no known peer with piece, or not asked for yet.
pieces.append(0)
pieces.append(
0
) # Missing, no known peer with piece, or not asked for yet.
for peer_info in self.handle.get_peer_info():
if peer_info.downloading_piece_index >= 0:
# Being downloaded from peer.
pieces[peer_info.downloading_piece_index] = 2
pieces[
peer_info.downloading_piece_index
] = 2 # Being downloaded from peer.
return pieces

View file

@ -7,7 +7,6 @@
#
"""TorrentManager handles Torrent objects"""
import datetime
import logging
import operator
@ -51,10 +50,10 @@ from deluge.event import (
log = logging.getLogger(__name__)
LT_DEFAULT_ADD_TORRENT_FLAGS = (
lt.torrent_flags.paused
| lt.torrent_flags.auto_managed
| lt.torrent_flags.update_subscribe
| lt.torrent_flags.apply_ip_filter
lt.add_torrent_params_flags_t.flag_paused
| lt.add_torrent_params_flags_t.flag_auto_managed
| lt.add_torrent_params_flags_t.flag_update_subscribe
| lt.add_torrent_params_flags_t.flag_apply_ip_filter
)
@ -203,32 +202,34 @@ class TorrentManager(component.Component):
# Register alert functions
alert_handles = [
'external_ip',
'performance',
'add_torrent',
'metadata_received',
'torrent_finished',
'torrent_paused',
'torrent_checked',
'torrent_resumed',
'tracker_reply',
'tracker_announce',
'tracker_warning',
'tracker_error',
'file_renamed',
'file_error',
'file_completed',
'storage_moved',
'storage_moved_failed',
'state_update',
'state_changed',
'save_resume_data',
'save_resume_data_failed',
'fastresume_rejected',
'external_ip_alert',
'performance_alert',
'add_torrent_alert',
'metadata_received_alert',
'torrent_finished_alert',
'torrent_paused_alert',
'torrent_checked_alert',
'torrent_resumed_alert',
'tracker_reply_alert',
'tracker_announce_alert',
'tracker_warning_alert',
'tracker_error_alert',
'file_renamed_alert',
'file_error_alert',
'file_completed_alert',
'storage_moved_alert',
'storage_moved_failed_alert',
'state_update_alert',
'state_changed_alert',
'save_resume_data_alert',
'save_resume_data_failed_alert',
'fastresume_rejected_alert',
]
for alert_handle in alert_handles:
on_alert_func = getattr(self, ''.join(['on_alert_', alert_handle]))
on_alert_func = getattr(
self, ''.join(['on_alert_', alert_handle.replace('_alert', '')])
)
self.alerts.register_handler(alert_handle, on_alert_func)
# Define timers
@ -291,8 +292,8 @@ class TorrentManager(component.Component):
if torrent.options['remove_at_ratio']:
self.remove(torrent_id)
break
torrent.pause()
if not torrent.status.paused:
torrent.pause()
def __getitem__(self, torrent_id):
"""Return the Torrent with torrent_id.
@ -368,11 +369,11 @@ class TorrentManager(component.Component):
add_torrent_params.flags = (
(
LT_DEFAULT_ADD_TORRENT_FLAGS
| lt.torrent_flags.duplicate_is_error
| lt.torrent_flags.upload_mode
| lt.add_torrent_params_flags_t.flag_duplicate_is_error
| lt.add_torrent_params_flags_t.flag_upload_mode
)
^ lt.torrent_flags.auto_managed
^ lt.torrent_flags.paused
^ lt.add_torrent_params_flags_t.flag_auto_managed
^ lt.add_torrent_params_flags_t.flag_paused
)
torrent_handle = self.session.add_torrent(add_torrent_params)
@ -480,12 +481,16 @@ class TorrentManager(component.Component):
# Set flags: enable duplicate_is_error & override_resume_data, disable auto_managed.
add_torrent_params['flags'] = (
LT_DEFAULT_ADD_TORRENT_FLAGS | lt.torrent_flags.duplicate_is_error
) ^ lt.torrent_flags.auto_managed
LT_DEFAULT_ADD_TORRENT_FLAGS
| lt.add_torrent_params_flags_t.flag_duplicate_is_error
| lt.add_torrent_params_flags_t.flag_override_resume_data
) ^ lt.add_torrent_params_flags_t.flag_auto_managed
if options['seed_mode']:
add_torrent_params['flags'] |= lt.torrent_flags.seed_mode
add_torrent_params['flags'] |= lt.add_torrent_params_flags_t.flag_seed_mode
if options['super_seeding']:
add_torrent_params['flags'] |= lt.torrent_flags.super_seeding
add_torrent_params[
'flags'
] |= lt.add_torrent_params_flags_t.flag_super_seeding
return torrent_id, add_torrent_params

View file

@ -166,8 +166,7 @@ def deprecated(func):
class CoroutineDeferred(defer.Deferred):
"""Wraps a coroutine in a Deferred.
It will dynamically pass through the underlying coroutine without wrapping where apporpriate.
"""
It will dynamically pass through the underlying coroutine without wrapping where apporpriate."""
def __init__(self, coro: Coroutine):
# Delay this import to make sure a reactor was installed first
@ -196,33 +195,17 @@ class CoroutineDeferred(defer.Deferred):
d = defer.ensureDeferred(self.coro)
d.chainDeferred(self)
def _callback_activate(self):
"""Verify awaited status before calling activate."""
def addCallbacks(self, *args, **kwargs): # noqa: N802
assert not self.awaited, 'Cannot add callbacks to an already awaited coroutine.'
self.activate()
def addCallback(self, *args, **kwargs): # noqa: N802
self._callback_activate()
return super().addCallback(*args, **kwargs)
def addCallbacks(self, *args, **kwargs): # noqa: N802
self._callback_activate()
return super().addCallbacks(*args, **kwargs)
def addErrback(self, *args, **kwargs): # noqa: N802
self._callback_activate()
return super().addErrback(*args, **kwargs)
def addBoth(self, *args, **kwargs): # noqa: N802
self._callback_activate()
return super().addBoth(*args, **kwargs)
_RetT = TypeVar('_RetT')
def maybe_coroutine(
f: Callable[..., Coroutine[Any, Any, _RetT]],
f: Callable[..., Coroutine[Any, Any, _RetT]]
) -> 'Callable[..., defer.Deferred[_RetT]]':
"""Wraps a coroutine function to make it usable as a normal function that returns a Deferred."""

View file

@ -13,7 +13,6 @@ This module describes the types of events that can be generated by the daemon
and subsequently emitted to the clients.
"""
known_events = {}

View file

@ -6,7 +6,7 @@
# See LICENSE for more details.
#
import email.message
import cgi
import logging
import os.path
import zlib
@ -133,10 +133,9 @@ class HTTPDownloaderAgent:
content_disp = headers.getRawHeaders(b'content-disposition')[0].decode(
'utf-8'
)
message = email.message.EmailMessage()
message['content-disposition'] = content_disp
new_file_name = message.get_filename()
if new_file_name:
content_disp_params = cgi.parse_header(content_disp)[1]
if 'filename' in content_disp_params:
new_file_name = content_disp_params['filename']
new_file_name = sanitise_filename(new_file_name)
new_file_name = os.path.join(
os.path.split(self.filename)[0], new_file_name
@ -153,10 +152,7 @@ class HTTPDownloaderAgent:
self.filename = new_file_name
cont_type_header = headers.getRawHeaders(b'content-type')[0].decode()
message = email.message.EmailMessage()
message['content-type'] = cont_type_header
cont_type = message.get_content_type()
params = message['content-type'].params
cont_type, params = cgi.parse_header(cont_type_header)
# Only re-ecode text content types.
encoding = None
if cont_type.startswith('text/'):

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more