Skip to content

Commit 02f47d8

Browse files
committed
[Core] Refactor new features support check
In 1989d0d a check for a daemon version was added. As this depends on knowing the version in which the feature will be merged, this approach is not suitable for this. Therefore, a features enum will help us list all features, without knowing in which version each was added.
1 parent 6ec1479 commit 02f47d8

File tree

6 files changed

+122
-16
lines changed

6 files changed

+122
-16
lines changed

deluge/_features.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from enum import IntFlag
2+
from functools import reduce
3+
from operator import or_ as _or_
4+
5+
6+
def _with_limits(enumeration):
7+
"""add NONE and ALL psuedo-members to enumeration"""
8+
none_member = enumeration(0)
9+
all_member = enumeration(reduce(_or_, enumeration))
10+
enumeration.NONE = none_member
11+
enumeration.ALL = all_member
12+
enumeration._member_map_['NONE'] = none_member
13+
enumeration._member_map_['ALL'] = all_member
14+
return enumeration
15+
16+
17+
@_with_limits
18+
class DelugeFeatures(IntFlag):
19+
BASE = 1 # marks all features up to 2.2.0

deluge/core/core.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import deluge.common
2323
import deluge.component as component
2424
from deluge import metafile, path_chooser_common
25+
from deluge._features import DelugeFeatures
2526
from deluge._libtorrent import LT_VERSION, lt
2627
from deluge.configmanager import ConfigManager, get_config_dir
2728
from deluge.core.alertmanager import AlertManager
@@ -1301,3 +1302,8 @@ def update_account(self, username: str, password: str, authlevel: str) -> bool:
13011302
@export(AUTH_LEVEL_ADMIN)
13021303
def remove_account(self, username: str) -> bool:
13031304
return self.authmanager.remove_account(username)
1305+
1306+
@export
1307+
def get_supported_features(self) -> int:
1308+
"""Returns the supported features."""
1309+
return DelugeFeatures.ALL.value

deluge/plugins/Blocklist/deluge_blocklist/gtkui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def disable(self):
6060
self.plugin.deregister_hook('on_apply_prefs', self._on_apply_prefs)
6161
self.plugin.deregister_hook('on_show_prefs', self._on_show_prefs)
6262

63-
del self.glade
63+
del self.builder
6464

6565
def update(self):
6666
def _on_get_status(status):

deluge/tests/test_client.py

Lines changed: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
# the additional special exception to link portions of this program with the OpenSSL library.
44
# See LICENSE for more details.
55
#
6+
from unittest.mock import patch
7+
68
import pytest
79
import pytest_twisted
810
from twisted.internet import defer
11+
from twisted.internet import error as twisted_error
912

1013
from deluge import error
14+
from deluge._features import DelugeFeatures
1115
from deluge.common import AUTH_LEVEL_NORMAL, get_localhost_auth, get_version
1216
from deluge.core.authmanager import AUTH_LEVEL_ADMIN
1317
from deluge.ui.client import Client, DaemonSSLProxy, client
@@ -72,6 +76,21 @@ def __on_disconnect(self):
7276
self.disconnect_callback()
7377

7478

79+
class NoFeatureDaemonSSLProxy(DaemonSSLProxy):
80+
def call(self, method, *args, **kwargs):
81+
def on_method_list(methods_list):
82+
return tuple(
83+
method_name
84+
for method_name in methods_list
85+
if method_name != 'core.get_supported_features'
86+
)
87+
88+
d = super().call(method, *args, **kwargs)
89+
if method == 'daemon.get_method_list':
90+
d.addCallback(on_method_list)
91+
return d
92+
93+
7594
@pytest.mark.usefixtures('daemon', 'client')
7695
class TestClient:
7796
def test_connect_no_credentials(self):
@@ -134,6 +153,21 @@ def on_failure(failure):
134153
d.addCallbacks(self.fail, on_failure)
135154
return d
136155

156+
def test_connect_invalid_host(self):
157+
username, password = get_localhost_auth()
158+
d = client.connect(
159+
'somehost', self.listen_port, username=username, password=password
160+
)
161+
162+
def on_failure(failure):
163+
assert (
164+
failure.trap(twisted_error.DNSLookupError)
165+
== twisted_error.DNSLookupError
166+
)
167+
168+
d.addCallbacks(self.fail, on_failure)
169+
return d
170+
137171
@pytest_twisted.inlineCallbacks
138172
def test_connect_with_password(self):
139173
username, password = get_localhost_auth()
@@ -181,12 +215,33 @@ def test_daemon_version(self):
181215
assert client.daemon_version == get_version()
182216

183217
@pytest_twisted.inlineCallbacks
184-
def test_daemon_version_check_min(self):
218+
def test_is_feature_supported(self):
185219
username, password = get_localhost_auth()
186220
yield client.connect(
187221
'localhost', self.listen_port, username=username, password=password
188222
)
189223

190-
assert client.daemon_version_check_min(get_version())
191-
assert not client.daemon_version_check_min(f'{get_version()}1')
192-
assert client.daemon_version_check_min('0.1.0')
224+
assert client._daemon_features == DelugeFeatures.ALL
225+
assert client.is_feature_supported(DelugeFeatures.BASE)
226+
assert not client.is_feature_supported(-1)
227+
228+
@pytest_twisted.inlineCallbacks
229+
def test_is_feature_supported_older_daemon(self):
230+
with patch('deluge.ui.client.DaemonSSLProxy', NoFeatureDaemonSSLProxy):
231+
username, password = get_localhost_auth()
232+
yield client.connect(
233+
'localhost', self.listen_port, username=username, password=password
234+
)
235+
236+
assert client._daemon_features == DelugeFeatures.NONE
237+
assert not client.is_feature_supported(DelugeFeatures.BASE)
238+
239+
@pytest_twisted.inlineCallbacks
240+
def test_daemon_features_resets_on_disconnect(self):
241+
username, password = get_localhost_auth()
242+
yield client.connect(
243+
'localhost', self.listen_port, username=username, password=password
244+
)
245+
assert client._daemon_features == DelugeFeatures.ALL
246+
yield client.disconnect()
247+
assert client._daemon_features == DelugeFeatures.NONE

deluge/tests/test_core.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import deluge.common
2121
import deluge.component as component
2222
import deluge.core.torrent
23+
from deluge._features import DelugeFeatures
2324
from deluge._libtorrent import lt
2425
from deluge.conftest import BaseTestCase
2526
from deluge.core.core import Core
@@ -509,3 +510,7 @@ def test_create_torrent(self, path, tmp_path, piece_length):
509510
assert f.read() == filecontent
510511

511512
lt.torrent_info(filecontent)
513+
514+
def test_get_supported_features(self):
515+
features = self.core.get_supported_features()
516+
assert features == DelugeFeatures.ALL

deluge/ui/client.py

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
from twisted.internet.protocol import ClientFactory
1616

1717
from deluge import error
18-
from deluge.common import VersionSplit, get_localhost_auth, get_version
18+
from deluge._features import DelugeFeatures
19+
from deluge.common import get_localhost_auth, get_version
1920
from deluge.decorators import deprecated
2021
from deluge.transfer import DelugeTransferProtocol
2122

@@ -561,6 +562,7 @@ def __init__(self):
561562
self._daemon_proxy = None
562563
self.disconnect_callback = None
563564
self.__started_standalone = False
565+
self._daemon_features = DelugeFeatures.NONE
564566

565567
def connect(
566568
self,
@@ -596,6 +598,30 @@ def on_connect_fail(reason):
596598
self.disconnect()
597599
return reason
598600

601+
def get_method_list(auth_level):
602+
d = self._daemon_proxy.call('daemon.get_method_list')
603+
d.addCallback(on_get_method_list, auth_level)
604+
return d
605+
606+
def on_get_method_list(daemon_methods, auth_level):
607+
log.debug('Daemon methods: %s', daemon_methods)
608+
if 'core.get_supported_features' in daemon_methods:
609+
log.info('Getting daemon supported features')
610+
d = self._daemon_proxy.call('core.get_supported_features')
611+
d.addCallback(on_get_daemon_features, auth_level)
612+
return d
613+
else:
614+
log.info(
615+
'Unable to get daemon supported features, new features will not be available'
616+
)
617+
self._daemon_features = DelugeFeatures.NONE
618+
return auth_level
619+
620+
def on_get_daemon_features(daemon_features, auth_level):
621+
self._daemon_features = DelugeFeatures(daemon_features)
622+
log.debug('Daemon features: %s', self._daemon_features)
623+
return auth_level
624+
599625
def on_authenticate(result, daemon_info):
600626
log.debug('Authentication successful: %s', result)
601627
return result
@@ -612,6 +638,7 @@ def authenticate(daemon_version, username, password):
612638
d = self._daemon_proxy.authenticate(username, password)
613639
d.addCallback(on_authenticate, daemon_version)
614640
d.addErrback(on_authenticate_fail)
641+
d.addCallback(get_method_list)
615642
return d
616643

617644
d.addCallbacks(on_connected)
@@ -624,6 +651,7 @@ def disconnect(self):
624651
"""
625652
Disconnects from the daemon.
626653
"""
654+
self._daemon_features = DelugeFeatures.NONE
627655
if self.is_standalone():
628656
self._daemon_proxy.disconnect()
629657
self.stop_standalone()
@@ -752,16 +780,9 @@ def daemon_version(self) -> str:
752780
"""
753781
return self._daemon_proxy.daemon_version if self.connected() else ''
754782

755-
def daemon_version_check_min(self, min_version=get_version()) -> bool:
756-
"""Check connected daemon against a minimum version.
757-
758-
Returns:
759-
If connected daemon meets minimum version requirement.
760-
"""
761-
if not (self.daemon_version and min_version):
762-
return False
763-
764-
return VersionSplit(self.daemon_version) >= VersionSplit(min_version)
783+
def is_feature_supported(self, feature: DelugeFeatures) -> bool:
784+
"""Check if the connected daemon supports a given feature"""
785+
return self._daemon_features & feature == feature
765786

766787
def register_event_handler(self, event, handler):
767788
"""

0 commit comments

Comments
 (0)